mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Keep held format arguments static and compiling
The runtime format branch packed its arguments with a checked-dynamic array literal, a dynamic operation: a console.log with a string first argument put its function in the dynamic tier, and the property-bag analysis read the format calls as possible dynamic stores. The held arguments now go to the formatter as a native list of checked values (insp.formatList and insp.formatPrimitives), which is a tier boundary, and the format and inspect entries join the helpers that never store into a class instance. A first argument whose type may hold a string only through a union, or whose checker type does not map, packed every argument as a checked value and failed to compile when one did not convert (7380's `console.log(top ? [...top] : "none", byName)`). It now takes the held-argument form: the lowered value is tested for a substitution and otherwise joins as before. In TypeScript an argument without a checked-dynamic form is refused at compile time only when the joined form cannot print it either; otherwise the branch that substitutes throws SC2020, as in JavaScript.
This commit is contained in:
@@ -1036,6 +1036,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"insp.dynS": "scr_insp_dyn_s",
|
||||
"insp.format": "scr_insp_format_pack",
|
||||
"insp.formatPrimitives": "scr_insp_format_primitives",
|
||||
"insp.formatList": "scr_insp_format_list",
|
||||
"insp.formatWithOptions": "scr_insp_format_with_options",
|
||||
"insp.inspect": "scr_insp_inspect",
|
||||
"insp.customEnabled": "scr_insp_custom_enabled",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
lowerInspectValue,
|
||||
lowerRuntimeFormat,
|
||||
runtimeFormatString,
|
||||
formatStringConverts,
|
||||
} from "./lower-inspect.js";
|
||||
import { streamSidesOf } from "./lower-stream.js";
|
||||
|
||||
@@ -372,7 +373,7 @@ export function lowerConsoleOutput(
|
||||
return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc);
|
||||
const runtime =
|
||||
nodes.length > 1 && !isStringLiteralLike(first) ? runtimeFormatString(lowerer, first!) : null;
|
||||
if (runtime === "other")
|
||||
if (runtime === "other" && !formatStringConverts(lowerer, first!))
|
||||
return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc);
|
||||
if (runtime !== null) {
|
||||
const held = heldFormatArgs(lowerer, nodes, loc);
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
funcOf,
|
||||
arrayOf,
|
||||
CLASS_FIELD_ATTRIBUTES_PREFIX,
|
||||
classDynViewSupported,
|
||||
DYN_CLASS_PROPERTIES,
|
||||
@@ -3205,7 +3206,8 @@ export function lowerFormatCall(
|
||||
|
||||
if (!firstIsLiteral) {
|
||||
const runtime = argNodes.length > 1 ? runtimeFormatString(lowerer, first) : null;
|
||||
if (runtime === "other") return lowerRuntimeFormat(lowerer, argNodes, loc);
|
||||
if (runtime === "other" && !formatStringConverts(lowerer, first))
|
||||
return lowerRuntimeFormat(lowerer, argNodes, loc);
|
||||
if (runtime !== null) {
|
||||
// A string that substitutes formats at run time; any other string
|
||||
// joins with the remaining arguments, as below.
|
||||
@@ -3576,6 +3578,14 @@ function runtimeFenceValue(
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether the held-argument form can test a format string whose type
|
||||
* only may hold a string: a union that converts to a checked-dynamic value,
|
||||
* or a value whose checker type does not map (its lowered type decides). */
|
||||
export function formatStringConverts(lowerer: Lowerer, first: ts.Expression): boolean {
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(first));
|
||||
return t === null || t.kind === "dyn" || lowerer.dynConvertible(t);
|
||||
}
|
||||
|
||||
/** How a non-literal format string is known: "string" and "dyn" values
|
||||
* test for a substitution at run time, "other" types (unions with a string
|
||||
* arm) format at run time, and null types never hold a string. */
|
||||
@@ -3673,14 +3683,10 @@ export function heldFormatArgs(
|
||||
"util.format of a void call result (call it as its own statement, then pass undefined)",
|
||||
);
|
||||
}
|
||||
if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) {
|
||||
// JavaScript keeps the space-joined branch, which printed these
|
||||
// values before, and refuses only a string that substitutes.
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(node))) {
|
||||
lowerer.noLowering(unconvertibleSurface(value.type), node, UNCONVERTIBLE_HINT);
|
||||
}
|
||||
// The space-joined branch prints these values as before; only a string
|
||||
// that substitutes refuses them, where it runs.
|
||||
if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type))
|
||||
unconvertible ??= { node, type: value.type };
|
||||
}
|
||||
// Unit values have no local: their effects run here and a literal
|
||||
// stands in.
|
||||
const k = value.type.kind;
|
||||
@@ -3694,16 +3700,20 @@ export function heldFormatArgs(
|
||||
return { node, ref: () => varRef(held.id, value.type, loc) };
|
||||
});
|
||||
const first = args[0]!.ref();
|
||||
// A lowered value that is neither a string nor convertible holds no
|
||||
// string, so it never substitutes.
|
||||
const specifies: IrExpr =
|
||||
first.type.kind === "string"
|
||||
? { kind: "libCall", fn: "insp.formatSpecifies", args: [first], type: BOOL, loc }
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "insp.formatSpecifiesDyn",
|
||||
args: [lowerer.coerceToExpected(first, DYN)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
: first.type.kind === "dyn" || lowerer.dynConvertible(first.type)
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "insp.formatSpecifiesDyn",
|
||||
args: [lowerer.coerceToExpected(first, DYN)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
}
|
||||
: { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
const format = (): IrExpr => {
|
||||
if (unconvertible !== null) {
|
||||
const fenced = unconvertible;
|
||||
@@ -3725,14 +3735,18 @@ export function heldFormatArgs(
|
||||
t.kind === "undefinedT" ||
|
||||
t.kind === "nullT" ||
|
||||
(t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms.every(primitive) ?? false));
|
||||
// A native list of the checked values: converting into it is a tier
|
||||
// boundary, so the statement stays static.
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: args.every((arg) => primitive(arg.ref().type)) ? "insp.formatPrimitives" : "insp.format",
|
||||
fn: args.every((arg) => primitive(arg.ref().type))
|
||||
? "insp.formatPrimitives"
|
||||
: "insp.formatList",
|
||||
args: [
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
kind: "arrayLit",
|
||||
elems: args.map((arg) => lowerer.coerceToExpected(arg.ref(), DYN)),
|
||||
type: DYN,
|
||||
type: arrayOf(DYN),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
@@ -3748,6 +3762,11 @@ export function heldFormatArgs(
|
||||
if (t.kind === "bytes") return true;
|
||||
return inspectSupport(lowerer, t, new Set(), { recursive: false }) === null;
|
||||
});
|
||||
// Without a joined branch every call formats: TypeScript refuses the
|
||||
// value at compile time.
|
||||
const refused = unconvertible as { node: ts.Expression; type: IrType } | null;
|
||||
if (!joinable && refused !== null && !isJsSourceFile(ts.getSourceFileOfNode(refused.node)))
|
||||
lowerer.noLowering(unconvertibleSurface(refused.type), refused.node, UNCONVERTIBLE_HINT);
|
||||
return { stmts, args, specifies, format, joinable };
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// The runtime formatter calls toString, valueOf and toJSON, and refuses
|
||||
// renderings it has no lowering for.
|
||||
"insp.format",
|
||||
"insp.formatList",
|
||||
"insp.formatWithOptions",
|
||||
"insp.inspect",
|
||||
// The options object reads the caller's options (getters included), and
|
||||
|
||||
@@ -4376,7 +4376,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"insp.format": { argTypes: [DYN], result: STRING },
|
||||
/** insp.format over arguments that are all strings, numbers, booleans,
|
||||
* null or undefined, without the checked-dynamic walk. */
|
||||
"insp.formatPrimitives": { argTypes: [DYN], result: STRING },
|
||||
"insp.formatPrimitives": { argTypes: [arrayOf(DYN)], result: STRING },
|
||||
"insp.formatList": { argTypes: [arrayOf(DYN)], result: STRING },
|
||||
"insp.formatWithOptions": { argTypes: [DYN, DYN], result: STRING },
|
||||
/** util.inspect(value, options, depth, colors) with the call's argument
|
||||
* count: the runtime reads the options and renders the checked-dynamic
|
||||
|
||||
@@ -407,6 +407,18 @@ const DYN_READ_ONLY = new Set<string>([
|
||||
"fs.utimesSync",
|
||||
"insp.dyn",
|
||||
"insp.dynS",
|
||||
// util.format and util.inspect over checked values: the inspect walk and
|
||||
// the format conversions read them; insp.registerCustom keeps a class's
|
||||
// custom inspect thunk in the runtime's registry.
|
||||
"insp.customIsSelf",
|
||||
"insp.customResult",
|
||||
"insp.format",
|
||||
"insp.formatList",
|
||||
"insp.formatPrimitives",
|
||||
"insp.formatSpecifiesDyn",
|
||||
"insp.formatWithOptions",
|
||||
"insp.inspect",
|
||||
"insp.registerCustom",
|
||||
// The dynamic-tier operators run their dyn twins (scr_js_value.c); the
|
||||
// conversions move a value between dyn and jsval, and js.toF64 is
|
||||
// dynCheck's number check.
|
||||
|
||||
@@ -2575,11 +2575,21 @@ static ScrStr *insp_prim_number(ScrDyn *v, int spec) {
|
||||
static const InspFmtOps insp_fmt_primitive_ops = {insp_prim_s, insp_prim_j, insp_prim_number,
|
||||
insp_prim_inspect};
|
||||
|
||||
ScrStr *scr_insp_format_primitives(ScrDyn *pack) {
|
||||
/* A native list of checked values (the compiled format calls) as the
|
||||
* runtime's argument pack. +1. */
|
||||
static ScrDyn *insp_list_pack(ScrArr *list) {
|
||||
ScrDyn *pack = scr_dyn_new_arr();
|
||||
for (uint32_t i = 0; i < list->len; i++) scr_dyn_arr_push(pack, scr_arr_get_ref(list, i));
|
||||
return pack;
|
||||
}
|
||||
|
||||
ScrStr *scr_insp_format_primitives(ScrArr *list) {
|
||||
ScrInspFormatOptions o;
|
||||
scr_insp_format_defaults(&o, "util.format");
|
||||
if (pack->kind != SCR_DYN_ARR) return scr_str_new("", 0);
|
||||
return insp_format_loop(pack->v.arr.items, pack->v.arr.len, &o, &insp_fmt_primitive_ops);
|
||||
ScrDyn *pack = insp_list_pack(list);
|
||||
ScrStr *out = insp_format_loop(pack->v.arr.items, pack->v.arr.len, &o, &insp_fmt_primitive_ops);
|
||||
scr_dyn_release(pack);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* The arguments of a dyn array pack (+1 text, or NULL pending). */
|
||||
@@ -2594,6 +2604,13 @@ ScrStr *scr_insp_format_pack(ScrDyn *pack) {
|
||||
return insp_fmt_pack(pack, &o);
|
||||
}
|
||||
|
||||
ScrStr *scr_insp_format_list(ScrArr *list) {
|
||||
ScrDyn *pack = insp_list_pack(list);
|
||||
ScrStr *out = scr_insp_format_pack(pack);
|
||||
scr_dyn_release(pack);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Whether a format string can substitute: a '%' before its last character.
|
||||
* A string without one formats as its arguments joined with spaces. */
|
||||
bool scr_insp_format_specifies(ScrStr *first) {
|
||||
|
||||
@@ -76,9 +76,13 @@ ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOpt
|
||||
/* util.format(...pack) and util.formatWithOptions(options, ...pack) over a
|
||||
* dyn array of the arguments (+1). */
|
||||
ScrStr *scr_insp_format_pack(ScrDyn *pack);
|
||||
/* util.format(...pack) when every argument is a string, number, boolean,
|
||||
/* util.format over a native list of checked values, the compiled calls'
|
||||
* held arguments (+1). */
|
||||
struct ScrArr;
|
||||
ScrStr *scr_insp_format_list(struct ScrArr *list);
|
||||
/* scr_insp_format_list when every argument is a string, number, boolean,
|
||||
* null or undefined, without the checked-dynamic walk (+1). */
|
||||
ScrStr *scr_insp_format_primitives(ScrDyn *pack);
|
||||
ScrStr *scr_insp_format_primitives(struct ScrArr *list);
|
||||
ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack);
|
||||
/* util.inspect(value, options, depth, colors) over checked-dynamic
|
||||
* arguments with Node's legacy-argument rules; argc is the call's
|
||||
|
||||
Reference in New Issue
Block a user