mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Format toString(radix) with a number radix without a dyn box
n.toString(radix) passed the radix as a dyn value, so every call boxed a number and converted it back, and kept scr_json.c's dyn machinery linked into programs that use nothing else of it. A number-typed radix now calls scr_num_to_string_radix_f64, which truncates it as ToIntegerOrInfinity does and holds the formatting that moved from scr_json.c to scr_lib.c. Other radix types keep the generic entry point, which converts and then calls the same function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
06e987b848
commit
4b23d34efe
@@ -144,6 +144,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.toFixed0": "scr_num_to_fixed0",
|
||||
"num.toFixed": "scr_num_to_fixed",
|
||||
"num.toStringRadix": "scr_num_to_string_radix",
|
||||
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
|
||||
"num.sameValue": "scr_num_same_value",
|
||||
"intl.numFormatEnUs": "scr_intl_num_format_en_us",
|
||||
"number.isFinite": "scr_num_is_finite",
|
||||
|
||||
@@ -4105,10 +4105,32 @@ function lowerNumberToStringCall(
|
||||
}
|
||||
if (call.arguments.length === 1) {
|
||||
if (operand.type.kind !== "f64") return null;
|
||||
const arg = call.arguments[0]!;
|
||||
// A number radix needs no box: ToIntegerOrInfinity of a number is a
|
||||
// truncation, which the typed entry point applies itself.
|
||||
if (!ts.isSpreadElement(arg) && lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "f64") {
|
||||
const radix = lowerer.lowerExpr(arg);
|
||||
if (radix.type.kind === "f64") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadixF64",
|
||||
args: [operand, radix],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.coerceToExpected(radix, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.lowerExprExpecting(call.arguments[0]!, DYN)],
|
||||
args: [operand, lowerer.lowerExprExpecting(arg, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
|
||||
@@ -137,6 +137,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fetch.streamFrom",
|
||||
"num.toFixed",
|
||||
"num.toStringRadix",
|
||||
"num.toStringRadixF64",
|
||||
"insp.jsonDyn",
|
||||
// diagnostics_channel: publish runs subscribers synchronously (a throw
|
||||
// propagates — the documented divergence from triggerUncaughtException);
|
||||
|
||||
@@ -751,6 +751,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"num.toFixed0": { argTypes: [F64], result: STRING },
|
||||
"num.toFixed": { argTypes: [F64, F64], result: STRING },
|
||||
"num.toStringRadix": { argTypes: [F64, DYN], result: STRING },
|
||||
/** toString(radix) with a number radix (scr_lib.c): truncates it as
|
||||
* ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */
|
||||
"num.toStringRadixF64": { argTypes: [F64, F64], result: STRING },
|
||||
/** Object.is over two numbers — the spec's SameValue on doubles: NaN
|
||||
* equals NaN, +0 differs from -0, everything else is `===`. Plain bool
|
||||
* result; never throws. (Union-armed operands take unionEq's sameValue
|
||||
|
||||
@@ -13045,63 +13045,13 @@ double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *encoding) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Radix digit generation follows V8's DoubleToRadixStringView algorithm.
|
||||
* Copyright 2012 the V8 project authors. BSD license in
|
||||
* ../vendor/v8-number-radix/LICENSE. The decimal path retains Ryū. */
|
||||
/* toString(radix) with any radix value: undefined means 10, anything else
|
||||
* converts with ToNumber (which can run program code) first. */
|
||||
ScrStr *scr_num_to_string_radix(double value, const ScrDyn *argument) {
|
||||
double requested = 10;
|
||||
if (argument && argument->kind != SCR_DYN_UNDEF &&
|
||||
!scr_dyn_number_coerce_js(argument, &requested)) return NULL;
|
||||
requested = isnan(requested) ? 0 : trunc(requested);
|
||||
if (requested < 2 || requested > 36 || !isfinite(requested)) {
|
||||
static const char message[] = "toString() radix argument must be between 2 and 36";
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
int radix = (int)requested;
|
||||
if (radix == 10 || !isfinite(value) || value == 0) return scr_f64_to_scrstr(value);
|
||||
static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
char buffer[2200];
|
||||
const size_t middle = sizeof buffer / 2;
|
||||
size_t begin = middle, end = middle;
|
||||
bool negative = value < 0;
|
||||
value = fabs(value);
|
||||
double integer = floor(value), fraction = value - integer;
|
||||
double tolerance = 0.5 * (nextafter(value, INFINITY) - value);
|
||||
if (tolerance <= 0) tolerance = nextafter(0, INFINITY);
|
||||
if (fraction >= tolerance) {
|
||||
buffer[end++] = '.';
|
||||
do {
|
||||
fraction *= radix;
|
||||
tolerance *= radix;
|
||||
int digit = (int)fraction;
|
||||
buffer[end++] = digits[digit];
|
||||
fraction -= digit;
|
||||
if ((fraction > 0.5 || (fraction == 0.5 && (digit & 1))) && fraction + tolerance > 1) {
|
||||
for (;;) {
|
||||
end--;
|
||||
if (end == middle) { integer += 1; break; }
|
||||
char last = buffer[end];
|
||||
digit = last > '9' ? last - 'a' + 10 : last - '0';
|
||||
if (digit + 1 < radix) { buffer[end++] = digits[digit + 1]; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while (fraction >= tolerance);
|
||||
}
|
||||
/* V8's Double::Exponent is the unbiased exponent minus 52. Beyond
|
||||
* that precision boundary, trailing digits are unrepresented zeros. */
|
||||
while (integer / radix >= 0x1p53) {
|
||||
integer /= radix;
|
||||
buffer[--begin] = '0';
|
||||
}
|
||||
do {
|
||||
double remainder = fmod(integer, radix);
|
||||
buffer[--begin] = digits[(int)remainder];
|
||||
integer = (integer - remainder) / radix;
|
||||
} while (integer > 0);
|
||||
if (negative) buffer[--begin] = '-';
|
||||
return scr_str_new(buffer + begin, end - begin);
|
||||
return scr_num_to_string_radix_f64(value, requested);
|
||||
}
|
||||
|
||||
ScrDyn *scr_caught_to_dyn(const ScrCaught *c) {
|
||||
|
||||
@@ -10660,6 +10660,64 @@ ScrStr *scr_num_to_fixed0(double x) {
|
||||
* the receiver's non-finite arm, as ECMA-262 requires. Invalid precision
|
||||
* raises V8's catchable RangeError text; otherwise the result is +1. The
|
||||
* exact-binary-value digits come from scr_f64_to_fixed (scr_number.c). */
|
||||
/* Number.prototype.toString(radix) with a number radix, which
|
||||
* ToIntegerOrInfinity truncates (NaN to 0). Radix digit generation follows
|
||||
* V8's DoubleToRadixStringView algorithm. Copyright 2012 the V8 project
|
||||
* authors. BSD license in ../vendor/v8-number-radix/LICENSE. The decimal
|
||||
* path retains Ryū. */
|
||||
ScrStr *scr_num_to_string_radix_f64(double value, double requested) {
|
||||
requested = isnan(requested) ? 0 : trunc(requested);
|
||||
if (requested < 2 || requested > 36 || !isfinite(requested)) {
|
||||
static const char message[] = "toString() radix argument must be between 2 and 36";
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
int radix = (int)requested;
|
||||
if (radix == 10 || !isfinite(value) || value == 0) return scr_f64_to_scrstr(value);
|
||||
static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
char buffer[2200];
|
||||
const size_t middle = sizeof buffer / 2;
|
||||
size_t begin = middle, end = middle;
|
||||
bool negative = value < 0;
|
||||
value = fabs(value);
|
||||
double integer = floor(value), fraction = value - integer;
|
||||
double tolerance = 0.5 * (nextafter(value, INFINITY) - value);
|
||||
if (tolerance <= 0) tolerance = nextafter(0, INFINITY);
|
||||
if (fraction >= tolerance) {
|
||||
buffer[end++] = '.';
|
||||
do {
|
||||
fraction *= radix;
|
||||
tolerance *= radix;
|
||||
int digit = (int)fraction;
|
||||
buffer[end++] = digits[digit];
|
||||
fraction -= digit;
|
||||
if ((fraction > 0.5 || (fraction == 0.5 && (digit & 1))) && fraction + tolerance > 1) {
|
||||
for (;;) {
|
||||
end--;
|
||||
if (end == middle) { integer += 1; break; }
|
||||
char last = buffer[end];
|
||||
digit = last > '9' ? last - 'a' + 10 : last - '0';
|
||||
if (digit + 1 < radix) { buffer[end++] = digits[digit + 1]; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while (fraction >= tolerance);
|
||||
}
|
||||
/* V8's Double::Exponent is the unbiased exponent minus 52. Beyond
|
||||
* that precision boundary, trailing digits are unrepresented zeros. */
|
||||
while (integer / radix >= 0x1p53) {
|
||||
integer /= radix;
|
||||
buffer[--begin] = '0';
|
||||
}
|
||||
do {
|
||||
double remainder = fmod(integer, radix);
|
||||
buffer[--begin] = digits[(int)remainder];
|
||||
integer = (integer - remainder) / radix;
|
||||
} while (integer > 0);
|
||||
if (negative) buffer[--begin] = '-';
|
||||
return scr_str_new(buffer + begin, end - begin);
|
||||
}
|
||||
|
||||
ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
|
||||
double fd = isnan(fraction_digits) ? 0 : trunc(fraction_digits);
|
||||
if (!(fd >= 0 && fd <= 100)) {
|
||||
|
||||
@@ -73,6 +73,7 @@ bool scr_num_is_safe_integer(double x);
|
||||
ScrStr *scr_num_to_exponential(double x);
|
||||
ScrStr *scr_num_to_fixed0(double x);
|
||||
ScrStr *scr_num_to_fixed(double x, double fraction_digits);
|
||||
ScrStr *scr_num_to_string_radix_f64(double value, double radix);
|
||||
|
||||
/* Object.is over two numbers — the spec's SameValue on doubles: NaN
|
||||
* equals NaN, +0 differs from -0, everything else is ==. Never throws. */
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Number.prototype.toString(radix) with a number-typed radix takes the
|
||||
// typed entry point: ToIntegerOrInfinity truncates the radix (NaN to 0),
|
||||
// out-of-range radixes throw the same RangeError, and the argument is
|
||||
// evaluated once, before formatting. An optional radix keeps the generic
|
||||
// conversion, where undefined means 10.
|
||||
const radixes = [2, 2.9, 7, 10, 16, 16.5, 35.99, 36];
|
||||
const values = [0, -0, 1, -1, 255, -255.5, 0.1, 1 / 3, 2 ** 53, -(2 ** 60), 1e21, 123.456, NaN, Infinity, -Infinity];
|
||||
for (const radix of radixes) {
|
||||
const row: string[] = [];
|
||||
for (const value of values) row.push(value.toString(radix));
|
||||
console.log(radix, row.join(" "));
|
||||
}
|
||||
|
||||
let calls = 0;
|
||||
function nextRadix(): number {
|
||||
calls++;
|
||||
return 8 + calls;
|
||||
}
|
||||
console.log((1234).toString(nextRadix()), (1234).toString(nextRadix()), calls);
|
||||
|
||||
for (const bad of [-0, 0.5, 1, 1.99, 36.5, 37, -2, NaN, Infinity, -Infinity]) {
|
||||
try {
|
||||
console.log((42).toString(bad));
|
||||
} catch (error) {
|
||||
console.log(String(bad), error instanceof RangeError, (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRadix(i: number): number | undefined {
|
||||
return i % 2 === 0 ? undefined : 2 + i;
|
||||
}
|
||||
for (let i = 0; i < 4; i++) console.log((255).toString(maybeRadix(i)));
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < 2000; i++) total += (i * 7919).toString(2 + (i % 35)).length;
|
||||
console.log(total);
|
||||
Reference in New Issue
Block a user