Compile native renderer startup callables and dictionaries

- Support stored builtin helpers, typed rest callbacks, and shared FileHandle values.
- Preserve async results, specialized function bodies, and checked class receivers.
- Compile live export dictionaries and keep native property names distinct.
This commit is contained in:
Chris Tate
2026-09-28 14:53:57 -05:00
committed by GitHub
parent b40a6f6079
commit 0053372853
33 changed files with 667 additions and 93 deletions
+2
View File
@@ -4507,6 +4507,8 @@ function emitDynamicLibCall(state: LibCallState): Temp {
return finish(`scr_dyn_this_get()`);
case "dyn.objKeys":
return finish(`scr_dyn_obj_keys(${arg(0)})`);
case "dyn.forInKeys":
return finish(`scr_dyn_for_in_keys(${arg(0)})`);
case "dyn.assign":
// Object.assign over dyn values: own members copy, the target
// returns (+1); non-object receivers throw like Node.
+16 -7
View File
@@ -1848,6 +1848,8 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
const sig = `static ScrDyn *${name}(ScrClosure *c, ScrDyn *const *args, size_t argc)`;
emitter.walkerProtos.push(`${sig}; /* dyn call thunk for ${key} */`);
const d: string[] = [`${sig} { /* dyn call thunk for ${key} */`];
const typedRest = t.rest === true && t.restAbi === "typed";
const hiddenRest = t.rest === true && !typedRest;
if (t.params.length === 0) d.push(` (void)args;`);
d.push(` (void)argc;`);
t.params.forEach((p, i) => {
@@ -1856,7 +1858,13 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
// anything; a number param throws the catchable TypeError).
d.push(` ${cDecl(p, `a${i}`)};`);
d.push(` {`);
d.push(` const ScrDyn *ad = ${i} < argc ? args[${i}] : scr_dyn_undefined();`);
const packsRest = typedRest && i === t.params.length - 1;
if (packsRest) {
d.push(` ScrDyn *ad = scr_dyn_new_arr();`);
d.push(` for (size_t ri = ${i}; ri < argc; ri++) scr_dyn_arr_push(ad, scr_dyn_retain(args[ri]));`);
} else {
d.push(` const ScrDyn *ad = ${i} < argc ? args[${i}] : scr_dyn_undefined();`);
}
if (p.kind === "dyn") {
d.push(` a${i} = scr_dyn_retain((ScrDyn *)ad);`);
} else if (p.kind === "jsval") {
@@ -1870,8 +1878,9 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
.flatMap((q, j) => (isRefCounted(q) ? [`${releaseCallC(q, `a${j}`)};`] : []));
d.push(` if (!a${i}) { ${undo.join(" ")}${undo.length > 0 ? " " : ""}return NULL; }`);
} else {
d.push(` ScrDynPath pp = { NULL, NULL, ${i} };`);
d.push(` a${i} = ${emitter.dynCheckHelper(p)}(ad, &pp);`);
if (!packsRest) d.push(` ScrDynPath pp = { NULL, NULL, ${i} };`);
d.push(` a${i} = ${emitter.dynCheckHelper(p)}(ad, ${packsRest ? "NULL" : "&pp"});`);
if (packsRest) d.push(` scr_dyn_release(ad);`);
const undo = t.params
.slice(0, i)
.flatMap((q, j) => (isRefCounted(q) ? [`${releaseCallC(q, `a${j}`)};`] : []));
@@ -1883,7 +1892,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
// param carries the call's arguments from index params.length on —
// the mustCall wrapper's `arguments`, a JS `...args`. Built fresh per
// call (+1, moved into the callee like every param).
if (t.rest) {
if (hiddenRest) {
d.push(` ScrDyn *rest = scr_dyn_new_arr();`);
d.push(` for (size_t ri = ${t.argumentsAll ? 0 : t.params.length}; ri < argc; ri++) {`);
d.push(` scr_dyn_arr_push(rest, scr_dyn_retain((ScrDyn *)args[ri]));`);
@@ -1891,8 +1900,8 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
}
// The closure CONSUMES its params (+1 each moved in — exactly what the
// builders above returned).
const castParams = ["ScrClosure *", ...t.params.map((p) => cType(p).trim()), ...(t.rest ? ["ScrDyn *"] : [])].join(", ");
const call = `((${cType(t.ret).trim()} (*)(${castParams}))c->fn)(${["c", ...t.params.map((_, i) => `a${i}`), ...(t.rest ? ["rest"] : [])].join(", ")})`;
const castParams = ["ScrClosure *", ...t.params.map((p) => cType(p).trim()), ...(hiddenRest ? ["ScrDyn *"] : [])].join(", ");
const call = `((${cType(t.ret).trim()} (*)(${castParams}))c->fn)(${["c", ...t.params.map((_, i) => `a${i}`), ...(hiddenRest ? ["rest"] : [])].join(", ")})`;
if (t.ret.kind === "void") {
d.push(` ${call};`);
d.push(` if (scr_exc_pending()) return NULL;`);
@@ -1926,7 +1935,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
const sigLit = cStringLiteral(Buffer.from(key, "utf8"));
emitter.walkerDefs.push(
`${sig} { /* box ${key} into dyn */`,
` return scr_dyn_new_func(scr_closure_retain(v), &${thunk}, ${t.params.length}, ${sigLit}, fname);`,
` return scr_dyn_new_func(scr_closure_retain(v), &${thunk}, ${t.params.length - (t.restAbi === "typed" ? 1 : 0)}, ${sigLit}, fname);`,
`}`,
``,
);
+50 -33
View File
@@ -46,6 +46,7 @@ const DYN_HANDLE_TAG_NUM: Record<string, number> = {
http2Stream: 5,
httpClientReq: 6,
child: 16,
fileHandle: 17,
};
export const DYN_KIND = {
@@ -2947,7 +2948,54 @@ export class LlDyn {
const host = this.host;
const B = new BlockBuilder();
const argNames: string[] = [];
const typedRest = t.rest === true && t.restAbi === "typed";
const packTail = (start: number): string => {
host.declare(`declare ptr @scr_dyn_new_arr()`);
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
const packed = B.tmp();
B.line(`${packed} = call ptr @scr_dyn_new_arr()`);
const riSlot = B.slot();
B.entryAllocas.push(`${riSlot} = alloca ${host.sizeType}`);
B.line(`store ${host.sizeType} ${start}, ptr ${riSlot}`);
const lc = B.newLabel("dfk.rc");
const lb = B.newLabel("dfk.rb");
const le = B.newLabel("dfk.re");
B.br(lc);
B.startBlock(lc);
const ri = B.tmp();
const cont = B.tmp();
B.line(`${ri} = load ${host.sizeType}, ptr ${riSlot}`);
B.line(`${cont} = icmp ult ${host.sizeType} ${ri}, %argc`);
B.condBr(cont, lb, le);
B.startBlock(lb);
const ap = B.tmp();
const av = B.tmp();
B.line(`${ap} = getelementptr inbounds ptr, ptr %args, ${host.sizeType} ${ri}`);
B.line(`${av} = load ptr, ptr ${ap}`);
const rv = this.retainDyn(B, av);
B.line(`call void @scr_dyn_arr_push(ptr ${packed}, ptr ${rv})`);
const ri2 = B.tmp();
B.line(`${ri2} = add ${host.sizeType} ${ri}, 1`);
B.line(`store ${host.sizeType} ${ri2}, ptr ${riSlot}`);
B.br(lc);
B.startBlock(le);
return packed;
};
t.params.forEach((p, i) => {
if (typedRest && i === t.params.length - 1) {
const packed = packTail(i);
const a = B.tmp();
B.line(`${a} = call ${this.valTy(p)} @${this.dynCheckHelper(p)}(ptr ${packed}, ptr null)`);
host.declare(`declare void @scr_dyn_release(ptr)`);
B.line(`call void @scr_dyn_release(ptr ${packed})`);
this.pendingBail(B, "dfk.rest", () => {
t.params.slice(0, i).forEach((q, j) => {
if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${argNames[j]})`);
});
}, "ptr null");
argNames.push(a);
return;
}
// JS arity: a missing argument IS the undefined dyn value.
const adSlot = B.slot();
B.entryAllocas.push(`${adSlot} = alloca ptr`);
@@ -3017,38 +3065,7 @@ export class LlDyn {
});
// VARIADIC (rest-marked) signatures: one extra trailing dyn-array
// param carries the call's arguments from index params.length on.
let rest: string | null = null;
if (t.rest) {
host.declare(`declare ptr @scr_dyn_new_arr()`);
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
rest = B.tmp();
B.line(`${rest} = call ptr @scr_dyn_new_arr()`);
const riSlot = B.slot();
B.entryAllocas.push(`${riSlot} = alloca ${host.sizeType}`);
B.line(`store ${host.sizeType} ${t.argumentsAll ? 0 : t.params.length}, ptr ${riSlot}`);
const lc = B.newLabel("dfk.rc");
const lb = B.newLabel("dfk.rb");
const le = B.newLabel("dfk.re");
B.br(lc);
B.startBlock(lc);
const ri = B.tmp();
const cont = B.tmp();
B.line(`${ri} = load ${host.sizeType}, ptr ${riSlot}`);
B.line(`${cont} = icmp ult ${host.sizeType} ${ri}, %argc`);
B.condBr(cont, lb, le);
B.startBlock(lb);
const ap = B.tmp();
const av = B.tmp();
B.line(`${ap} = getelementptr inbounds ptr, ptr %args, ${host.sizeType} ${ri}`);
B.line(`${av} = load ptr, ptr ${ap}`);
const rv = this.retainDyn(B, av);
B.line(`call void @scr_dyn_arr_push(ptr ${rest}, ptr ${rv})`);
const ri2 = B.tmp();
B.line(`${ri2} = add ${host.sizeType} ${ri}, 1`);
B.line(`store ${host.sizeType} ${ri2}, ptr ${riSlot}`);
B.br(lc);
B.startBlock(le);
}
const rest = t.rest && !typedRest ? packTail(t.argumentsAll ? 0 : t.params.length) : null;
// The closure CONSUMES its params (+1 each moved in).
const fnp = B.tmp();
const fn = B.tmp();
@@ -3104,7 +3121,7 @@ export class LlDyn {
`define internal ptr @${name}(ptr %v, ptr %fname) ${FN_ATTRS} { ; box ${key} into dyn`,
`entry:`,
` %c = call ptr @scr_closure_retain_v(ptr %v)`,
` %r = call ptr @scr_dyn_new_func(ptr %c, ptr @${thunk}, i32 ${t.params.length}, ptr ${sigLit}, ptr %fname)`,
` %r = call ptr @scr_dyn_new_func(ptr %c, ptr @${thunk}, i32 ${t.params.length - (t.restAbi === "typed" ? 1 : 0)}, ptr ${sigLit}, ptr %fname)`,
` ret ptr %r`,
`}`,
``,
@@ -822,6 +822,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
// (option/DataClone/cycle errors), and new RegExp's eager compile
// (catchable SyntaxError) — all may-throw generics over the checked-dynamic tree.
"dyn.objKeys": "scr_dyn_obj_keys",
"dyn.forInKeys": "scr_dyn_for_in_keys",
"dyn.hasOwn": "scr_dyn_has_own",
"dyn.assign": "scr_dyn_assign",
// variadic Object.assign: the source pack (push never throws; the
+6 -4
View File
@@ -13,13 +13,13 @@ import { unsignedHex } from "./literals.js";
* frontend, which separates name and counter with '.' exclusively.
*/
function sanitize(name: string): string {
function sanitize(name: string, property = false): string {
let result = "";
for (let i = 0; i < name.length; i++) {
const code = name.charCodeAt(i);
const ch = name.charAt(i);
result += code === 46 ? "_"
: (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 95
result += code === 46 && !property ? "_"
: (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || (code === 95 && !property)
? ch : `_x${unsignedHex(code)}_`;
}
return result;
@@ -71,7 +71,9 @@ export function mangleClassRelease(className: string): string {
return `sc_release_${sanitize(className)}`;
}
export function mangleField(fieldName: string): string {
return `sc_fld_${sanitize(fieldName)}`;
// Property keys are arbitrary strings. Encode both dots and underscores
// so a literal key cannot collide with either a separator or an escape.
return `sc_fld_${sanitize(fieldName, true)}`;
}
/** Hierarchy classes only (an `extends` anywhere): the per-class DIRECT
* release — the whole-object teardown stored in the class's vtable, which
@@ -40,6 +40,17 @@ const BASE: NativeLinkFeatures = {
};
describe("native link info recipes", () => {
test("FileHandle-only links include checked promise adapters without an engine", async () => {
const info = await createNativeLinkInfo({
programObject: "/out/app.o", target: MACOS_ARM64_TARGET,
features: { ...BASE, fileHandle: true }, ffi: null,
});
const sources = info.runtime_pack.source_sets.find((set) => set.name === "runtime")!.sources;
expect(sources).toContain("src/scr_file_handle.c");
expect(sources).toContain("src/scr_async_dyn.c");
expect(sources).not.toContain("src/scr_island.c");
});
test("feature source sets reproduce runtime and vendor gates", async () => {
const info = await createNativeLinkInfo({
programObject: "/out/app.o",
@@ -150,7 +150,7 @@ function runtimeSourceRecipe(
...(features.inspect ? ["scr_inspect.c"] : []),
...(features.dynInvoke || nativeFetch ? ["scr_dyn_invoke.c"] : []),
...(features.dc ? ["scr_dc.c"] : []),
...(features.dynAsync || features.dynInvoke || features.dc || nativeFetch
...(features.dynAsync || features.dynInvoke || features.dc || features.fileHandle || nativeFetch
? ["scr_async_dyn.c"]
: []),
...(features.zlib ? ["scr_zlib.c"] : []),
@@ -4485,7 +4485,7 @@ async function compileCInternal(
...(opts.inspect ? [rt(join(rtDir, "scr_inspect.c"))] : []),
...((opts.dynInvoke || nativeFetch) ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []),
...(opts.dynAsync || opts.dynInvoke || opts.dc || nativeFetch ? [rt(join(rtDir, "scr_async_dyn.c"))] : []),
...(opts.dynAsync || opts.dynInvoke || opts.dc || opts.fileHandle || nativeFetch ? [rt(join(rtDir, "scr_async_dyn.c"))] : []),
// The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
// libz on the default host-clang build, vendored objects on Zig builds)
// also serves the native fetch's gzip decoder — spread exactly once.
@@ -1976,6 +1976,13 @@ export function lowerShorthandValue(lowerer: Lowerer, prop: ts.ShorthandProperty
const propName = prop.name as ts.Identifier;
const loc = locOf(propName);
const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
// Import identifiers in shorthand position carry the property's symbol,
// so the ordinary identifier expression path cannot resolve them.
const builtin = lowerer.builtinImportOf(propName);
if (builtin) {
const callable = lowerer.lowerBuiltinCallableValue(builtin, loc);
if (callable) return callable;
}
if (symbol) {
if (lowerer.ctx.selfSymbol === symbol) {
return { kind: "selfRef", type: lowerer.ctx.selfType!, loc };
@@ -237,7 +237,9 @@ function lowerBuiltinOptionalDefault(
* declaration is not an import specifier. Returns the CANONICAL module
* name and the EXPORTED member name (not the local alias). */
export function builtinImportOf(lowerer: Lowerer, ident: ts.Identifier): { module: string; member: string } | null {
const symbol = lowerer.checker.getSymbolAtLocation(ident);
const symbol = ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident
? lowerer.checker.getShorthandAssignmentValueSymbol(ident.parent)
: lowerer.checker.getSymbolAtLocation(ident);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
if (!decl) return null;
// The CommonJS twin of the named import: a destructured require
@@ -229,7 +229,7 @@ export interface GenericInstance {
* the body builds (a re-demand is same-key recursion: the caller uses
* the PINNED fallback returnType and returnPinned locks it); "done" once
* returnType holds the inferred (or pinned) truth. */
implicitState?: "lowering" | "done";
implicitState?: "lowering" | "done" | "failed";
/** Same-key recursion observed the fallback return type mid-lowering, so
* the ABI is locked to it — the return post-pass coerces every return
* value to the pinned type instead of adopting the inferred one. */
@@ -1025,6 +1025,7 @@ function completeFuncValueArgs(
!hasExplicitJsDocReturn(decl) &&
mappedReturn !== null &&
promiseCarriesDyn(lowerer, mappedReturn) &&
!decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) &&
decl.parameters.some((param) => {
const mapped = lowerer.mapTypeOf(lowerer.typeOf(param.name));
return mapped === null || mapped.kind === "dyn";
@@ -1810,7 +1811,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
final = DYN; // no valued return: JS completes with undefined — the dyn undefined, today's slot
} else if (distinct.length === 1) {
const t = distinct[0]!;
final = !sawBare ? t : t.kind === "dyn" ? DYN : (lowerer.withUndefinedArmOf(t) ?? DYN);
final = isUnitType(t) ? DYN : !sawBare ? t : t.kind === "dyn" ? DYN : (lowerer.withUndefinedArmOf(t) ?? DYN);
} else {
final = DYN; // disagreeing returns: the checked-dynamic join
}
@@ -2179,6 +2180,7 @@ function runtimeOptionalHofGenericBinding(
if (!declSig) return null;
const retTs = lowerer.checker.getReturnTypeOfSignature(declSig);
if (retTs.flags & ts.TypeFlags.Any) return null;
if (retTs.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) return DYN;
if (lowerer.checker.isArrayType(retTs)) {
const elem = lowerer.checker.getTypeArguments(retTs as ts.TypeReference)[0];
// An implicit-any parameter poisons an inferred array result to
@@ -2309,6 +2311,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
argTypes: Map<ts.Symbol, ts.Type>,): GenericInstance {
const key = shapes.map((s) => typeKey(s.type)).join(",");
let inst = info.instances.get(key);
if (inst?.implicitState === "failed") lowerer.unsupported("SC1090", blame, `the failed specialization of '${info.baseName}'`);
if (inst) return inst;
if (info.instances.size >= MAX_GENERIC_INSTANCES) {
lowerer.unsupported(
@@ -2340,11 +2343,10 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
...(declared === null ? { implicitInferReturn: true as const } : {}),
};
info.instances.set(key, inst);
// EAGER lowering (nested, the lambda discipline): the call site needs
// the settled return type NOW. A body-level poison (a fenced parameter
// form) skips the function like lowerFunction's rule — calls then meet
// the pinned signature over a missing body, which the linker never
// sees because the poison also fenced the call statement.
// Eager lowering settles the return ABI before callers use it. A failed
// JS body must still have a throwing implementation: later references
// (including recursive ones) can already hold its cached signature.
const diagsBefore = lowerer.diags.length;
try {
// Implicit-any instances lower EAGERLY at the call site so their
// inferred return type is available immediately. They therefore do
@@ -2360,8 +2362,15 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
lowerer.implicitFns.push(fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
inst.implicitState = "done";
throw e;
inst.implicitState = "failed";
const types: IrType[] = shapes.map((shape) => shape.type);
if (info.member?.kind === "method") types.unshift({ kind: "object", className: info.member.cls.def.name });
const fn = lowerer.deferToRuntimeFence(diagsBefore, info.decl, {
kind: "function", name: inst.name, returnType: inst.returnType,
params: types.map((type, index) => ({ localId: `p.${index}`, name: `p${index}`, type })),
});
if (!fn) throw e;
lowerer.implicitFns.push(fn);
}
inst.implicitState = "done";
return inst;
@@ -5964,7 +5973,7 @@ export function lowerDynDispatchMethodCall(
arrayReceiver: boolean,
): IrExpr | null {
const method = access.name.text;
if ((!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(call.getSourceFile())) || call.questionDotToken || access.questionDotToken) return null;
if ((!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(call.getSourceFile())) || lowerer.chainBlocked(call, access)) return null;
if (call.arguments.some((arg) => ts.isSpreadElement(arg))) {
lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values");
}
@@ -10558,7 +10567,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
const helper = lowerer.narrowedArmHelper(receiver.type.unionId, receiverIr, locOf(access.expression));
if (helper !== null) return { kind: "call", callee: helper, args: [receiver], type: receiverIr, loc: locOf(access.expression) };
}
return receiver;
return receiver.type.kind === "dyn" ? lowerer.coerceInto(access.expression, receiver, receiverIr) : receiver;
};
const info = lowerer.classes.get(receiverIr.className);
if (!info) lowerer.flushDeferredClass(receiverIr.className);
@@ -9,6 +9,7 @@ import * as ts from "../ts7/adapter.js";
import { dirname, posix } from "node:path";
import { pathToFileURL } from "node:url";
import type { Lowerer } from "./lowerer.js";
import { OBJECT_CALLABLE_VALUES } from "./surfaces.js";
import { wasiGuestPath } from "../../wasi-paths.js";
import { BIGINT_T, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, IrExpr, IrFunction, IrJsOp, IrLocal, IrRecordShape, IrStmt, IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isJsonSafeType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionContainerArmsOk } from "../../ir/ir.js";
import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
@@ -989,16 +990,16 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const roots = lowerTlsRootCertificates(lowerer, bi, loc);
if (roots) return roots;
}
// JavaScript sources: a builtin member taken as a bare VALUE is
{
const callable = lowerer.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
// JavaScript sources: an otherwise unsupported builtin VALUE is
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(expr.getSourceFile())) {
return { kind: "strLit", value: `[builtin ${bi.module}.${bi.member}]`, type: STRING, loc };
}
{
const callable = lowerer.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
if (builtinModuleFnOf(lowerer, bi.module, bi.member)) {
lowerer.unsupported(
"SC1090",
@@ -3366,6 +3367,10 @@ function lowerPromiseThenPresence(
* verified — the name alone proves nothing. */
export function lowerIntrinsicProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
if (lowerer.chainBlocked(expr)) return null;
if (stdlibGlobalNameOf(lowerer, expr.expression) === "Object") {
const fn = own(OBJECT_CALLABLE_VALUES, expr.name.text);
if (fn) return lowerer.lowerNativeCallableValue(fn, `Object.${expr.name.text}`, locOf(expr));
}
// A never-tainted JS receiver type lowered checked-dynamic
// (neverTaintedJsType — `cmd.length` on `const cmd = ['pwd', []]`):
// stand down so the dyn keyed read below the chain answers, instead
@@ -4918,6 +4923,7 @@ export function lowerOptionalNumber(
) {
declared = shape.fields[0]!.type;
}
if (!declared && isJsSourceFile(expr.getSourceFile()) && recordKeyResultOk(lowerer, shape, DYN)) declared = DYN;
if (!declared) {
lowerer.unsupported(
"SC1090",
@@ -9466,7 +9472,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(access.expression) }
: lowerer.maybeNarrow(obj, access.expression);
}
return obj;
return obj.type.kind === "dyn" ? lowerer.coerceInto(access.expression, obj, receiverIr) : obj;
};
if (access.name.text === "cause" && receiverIr.className !== "%DOMException") {
let root = info;
@@ -15,11 +15,11 @@ import type { CycleEdge } from "../program.js";
import { invalidJsonModuleDiag, npmEmbedFailedDiag, requiresDynamicImportDiag } from "../../diagnostics/diagnostic.js";
import { BOOL, DYN, F64, IrClassDef, IrExpr, IrFunction, IrGlobal, IrRecordShape, IrStmt, IrType, IrUnionDef, JSVAL, RUNTIME_ERROR_CLASSES, STRING, SrcLoc, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, isUnitType } from "../../ir/ir.js";
import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, newFnCtx, staticImportNamespaceType, uncheckedOverloadHandleCall } from "./lowerer.js";
import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias } from "./lower-builtins.js";
import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias, stripTypeCasts } from "./lower-builtins.js";
import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js";
import { hasJsTypeAnnotation, isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js";
import { streamClassAliasDecl } from "./lower-stream.js";
import { stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf } from "./surfaces.js";
import { OBJECT_CALLABLE_VALUES, stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf } from "./surfaces.js";
import { collectNamespaceStmt, nsPathPrefix, trapDeclRootOf } from "./lower-namespaces.js";
import { collectExpandoMembers } from "./lower-expando.js";
import { recordTextCodecClass } from "../../ir/ir.js";
@@ -1410,6 +1410,29 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
for (const nameNode of boundIdentifiersOf(decl.name)) {
const diagsBefore = lowerer.diags.length;
try {
// Generic Object declarations do not describe the concrete
// native adapter ABI. Keep their stored values in shared module
// storage so separately lowered export helpers see the same closure.
const initializer = decl.initializer ? stripTypeCasts(decl.initializer) : undefined;
if (
isJsSourceFile(sf) && nameNode === decl.name && initializer &&
ts.isPropertyAccessExpression(initializer) &&
lowerer.isStdlibGlobal(initializer.expression, "Object") &&
Object.hasOwn(OBJECT_CALLABLE_VALUES, initializer.name.text)
) {
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
const g: IrGlobal = {
id: `%g.${tag}${nsPrefix}${nameNode.text}`, name: nameNode.text,
type: DYN, mutable: isLet, source: bindingSource(nameNode),
...(!isVarDeclared(decl) ? { tdz: true as const } : {}),
};
lowerer.globalsBySymbol.set(symbol, g);
lowerer.globalsList.push(g);
if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
}
continue;
}
// A JS file-scope evolving ARRAY (`const mustCallChecks = [];`
// — test/common's exit-accounting ledger): the strict type
// (any[]) has no mapping, but the VALUE is the dyn array the
@@ -7964,12 +7964,24 @@ function lowerBranchSwitch(
if (stdlibGlobalNameOf(lowerer, stmt.expression) === "globalThis") {
return { kind: "block", body: [], loc };
}
const recvT = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression));
if (recvT?.kind === "array") return lowerForInArray(lowerer, stmt, labels);
const receiver = lowerer.lowerExpr(stmt.expression);
const recvT = receiver.type;
if (recvT.kind === "dyn") {
const recv = lowerer.declareHiddenLocal("%indyn", DYN);
const ref: IrExpr = { kind: "varRef", localId: recv.id, type: DYN, loc };
const keys: IrExpr = {
kind: "dynCheck", value: { kind: "libCall", fn: "dyn.forInKeys", args: [ref], type: DYN, loc },
type: arrayOf(STRING), loc,
};
const loop = lowerForInOverKeys(lowerer, stmt, keys, labels, (key) => ({
kind: "libCall", fn: "dyn.hasOwn", args: [ref, key], type: BOOL, loc,
}));
return { kind: "block", body: [{ kind: "varDecl", localId: recv.id, init: receiver, loc }, loop], loc };
}
if (recvT.kind === "array") return lowerForInArray(lowerer, stmt, labels, receiver);
if (recvT?.kind === "record") {
const shape = lowerer.shapes.get(recvT.shapeId);
if (shape && !shape.tuple) {
const receiver = lowerer.lowerExpr(stmt.expression);
if (receiver.type.kind !== "record") lowerer.badType(stmt.expression, lowerer.typeOf(stmt.expression));
const rShape = lowerer.shapes.get(receiver.type.shapeId);
if (!rShape) throw new InternalCompilerError(`lowerer bug: unknown shape ${receiver.type.shapeId}`);
@@ -8120,9 +8132,8 @@ function lowerBranchSwitch(
* live-length guard is the per-visit presence check (keys removed by
* pops are skipped, exactly Node's HasProperty re-check; indices are
* dense, so `i < length` IS presence). */
function lowerForInArray(lowerer: Lowerer, stmt: ts.ForInStatement, labels: string[] | undefined): IrStmt {
function lowerForInArray(lowerer: Lowerer, stmt: ts.ForInStatement, labels: string[] | undefined, arrExpr: IrExpr): IrStmt {
const loc = locOf(stmt);
const arrExpr = lowerer.lowerExpr(stmt.expression);
if (arrExpr.type.kind !== "array") lowerer.badType(stmt.expression, lowerer.typeOf(stmt.expression));
lowerer.scopes.push(new Map());
try {
@@ -6709,8 +6709,32 @@ export class Lowerer {
// value — the call runs (a `never` thrower never comes back, so the
// trap is unreachable there), then the stranded TypeError.
let strandParams = false;
const narrowedParams = new Map<number, ReadonlySet<number>>();
const partialDynParams = new Map<number, IrType>();
for (let i = 0; i < fromT.params.length; i++) {
if (!this.coercibleValue(toT.params[i]!, fromT.params[i]!)) strandParams = true;
const actual = toT.params[i]!;
const expected = fromT.params[i]!;
if (this.coercibleValue(actual, expected)) continue;
if (actual.kind === "union" && expected.kind === "dyn") {
const arms = this.unions.get(actual.unionId)?.arms.filter((arm) => this.dynConvertible(arm)) ?? [];
if (arms.length > 0) {
partialDynParams.set(i, arms.length === 1 ? arms[0]! : { kind: "union", unionId: this.unions.intern(arms) });
continue;
}
}
// A stored native builtin can have a narrower supported overload
// than its public declaration. Preserve every supported union arm
// and reject only an invocation carrying an unsupported arm. This
// is the union counterpart of the checked single-arm extraction.
if (actual.kind === "union" && expected.kind === "union") {
const source = this.unions.get(actual.unionId);
const target = this.unions.get(expected.unionId);
if (source && target && target.arms.every((arm) => this.armTag(actual.unionId, arm) >= 0)) {
narrowedParams.set(i, new Set(source.arms.flatMap((arm, tag) => this.armTag(expected.unionId, arm) < 0 ? [tag] : [])));
continue;
}
}
strandParams = true;
}
let voidRet: "dyn" | "jsval" | "strand" | null = null;
let strandRet = false;
@@ -6757,7 +6781,24 @@ export class Lowerer {
} else {
const args = fromT.params.map((pt, i) => {
const aRef: IrExpr = { kind: "varRef", localId: `a.${i}`, type: toT.params[i]!, loc };
const converted = this.coerceToExpected(aRef, pt);
const partial = partialDynParams.get(i);
if (partial && aRef.type.kind === "union") {
const source = this.unions.get(aRef.type.unionId)!;
const supported = partial.kind === "union" ? this.unions.get(partial.unionId)!.arms : [partial];
const rejected = new Set(source.arms.flatMap((arm, tag) => supported.some((accepted) => typeEquals(accepted, arm)) ? [] : [tag]));
const narrow = partial.kind === "union"
? this.unionRetagHelper(aRef.type.unionId, partial.unionId, loc, rejected)
: this.narrowedArmHelper(aRef.type.unionId, partial, loc);
if (!narrow) throw new InternalCompilerError("lowerer bug: partial callable parameter stopped narrowing");
return this.coerceToExpected({ kind: "call", callee: narrow, args: [aRef], type: partial, loc }, pt);
}
const narrowed = narrowedParams.get(i);
const helper = narrowed && aRef.type.kind === "union" && pt.kind === "union"
? this.unionRetagHelper(aRef.type.unionId, pt.unionId, loc, narrowed)
: null;
const converted: IrExpr = helper
? { kind: "call", callee: helper, args: [aRef], type: pt, loc }
: this.coerceToExpected(aRef, pt);
if (!typeEquals(converted.type, pt)) throw new InternalCompilerError("lowerer bug: probed fn-adapter param stopped coercing");
return converted;
});
@@ -10343,10 +10384,9 @@ export class Lowerer {
const roots = lowerTlsRootCertificates(this, bi, loc);
if (roots) return roots;
}
// TypeScript builtin values admitted by an explicit surface-table
// contract materialize as interned closures. JavaScript sources retain
// their established builtin identity-token/fence policy.
if (!isJsSourceFile(expr.getSourceFile())) {
// Builtin values admitted by an explicit surface-table contract use
// the same interned native closures in TypeScript and JavaScript.
{
const callable = this.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
@@ -10379,8 +10419,12 @@ export class Lowerer {
loc: SrcLoc,
): IrExpr | null {
const fn = builtinModuleFnOf(this, bi.module, bi.member);
const valueParams = fn?.valueParams;
if (!fn || !valueParams) return null;
return fn ? this.lowerNativeCallableValue(fn, `${bi.module}.${bi.member}`, loc) : null;
}
lowerNativeCallableValue(fn: BuiltinModuleFn, display: string, loc: SrcLoc): IrExpr | null {
const valueParams = fn.valueParams;
if (!valueParams) return null;
const shapes: ParamShape[] = valueParams.map((param): ParamShape => {
if (param.mode === "rest") return { mode: "rest", type: arrayOf(param.type) };
if (param.mode === "optional") {
@@ -10406,7 +10450,7 @@ export class Lowerer {
const valueParam = valueParams[index];
if (!valueParam) {
throw new InternalCompilerError(
`builtin callable value '${bi.module}.${bi.member}' has a missing value parameter`,
`builtin callable value '${display}' has a missing value parameter`,
);
}
const ref: IrExpr = {
@@ -10416,16 +10460,16 @@ export class Lowerer {
loc,
};
if (valueParam.mode !== "optional") return ref;
if (valueParam.type.kind !== "string" || param.type.kind !== "union") {
if ((valueParam.type.kind !== "string" && valueParam.type.kind !== "f64") || param.type.kind !== "union") {
throw new InternalCompilerError(
`builtin callable value '${bi.module}.${bi.member}' has an unsupported optional default`,
`builtin callable value '${display}' has an unsupported optional default`,
);
}
const undefTag = this.armTag(param.type.unionId, UNDEFINED_T);
const valueTag = this.armTag(param.type.unionId, valueParam.type);
if (undefTag < 0 || valueTag < 0) {
throw new InternalCompilerError(
`builtin callable value '${bi.module}.${bi.member}' has an invalid optional ABI`,
`builtin callable value '${display}' has an invalid optional ABI`,
);
}
return {
@@ -10439,7 +10483,9 @@ export class Lowerer {
type: BOOL,
loc,
},
then: { kind: "strLit", value: valueParam.defaultValue, type: STRING, loc },
then: typeof valueParam.defaultValue === "string"
? { kind: "strLit", value: valueParam.defaultValue, type: STRING, loc }
: { kind: "numLit", value: valueParam.defaultValue, type: F64, loc },
else_: {
kind: "unionNarrow",
unionId: param.type.unionId,
@@ -608,7 +608,7 @@ export interface BuiltinModuleFn {
defaults?: string[];
/** The source-level function-value signature. Each admitted entry
* materializes as an interned zero-capture adapter over the fixed runtime
* libCall ABI. Optional parameters name the string default selected for
* libCall ABI. Optional parameters name the scalar default selected for
* omission or explicit undefined; rest parameters pack into one typed
* array slot. Entries with call-site-specific validation remain absent. */
valueParams?: BuiltinValueParam[];
@@ -616,7 +616,7 @@ export interface BuiltinModuleFn {
export type BuiltinValueParam =
| { mode: "required"; type: IrType }
| { mode: "optional"; type: IrType; defaultValue: string }
| { mode: "optional"; type: IrType; defaultValue: string | number }
| { mode: "rest"; type: IrType };
/** The common first-class shape for builtin functions whose supported
@@ -627,6 +627,17 @@ function exactValueParams(...types: IrType[]): BuiltinValueParam[] {
return types.map((type) => ({ mode: "required", type }));
}
/** Object helpers whose checked-native ABI is also their stored value ABI.
* These adapters operate on the live checked object/property table. */
export const OBJECT_CALLABLE_VALUES: Record<string, BuiltinModuleFn | undefined> = {
defineProperty: { fn: "dyn.defineProperty", params: [DYN, DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN, DYN) },
getOwnPropertyDescriptor: { fn: "dyn.getOwnPropertyDescriptor", params: [DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN) },
defineProperties: { fn: "dyn.defineProps", params: [DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN) },
keys: { fn: "dyn.objKeys", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
values: { fn: "dyn.objValues", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
entries: { fn: "dyn.objEntries", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
};
/** The lowerable surface of the supported node builtin modules, keyed by
* CANONICAL module name (both "fs" and "node:fs" land on "fs" — see
* canonicalBuiltinModule). Like STR_METHODS, membership is only half the
@@ -746,7 +757,11 @@ export const BUILTIN_MODULE_FNS: Record<string, Record<string, BuiltinModuleFn |
// open's optional flags/mode completion is special-cased in
// lowerBuiltinModuleCall; this row routes all import spellings and
// gives coverage the static member.
open: { fn: "fsp.open", params: [STRING, STRING, F64], result: { kind: "promise", inner: FILEHANDLE_T } },
open: { fn: "fsp.open", params: [STRING, STRING, F64], result: { kind: "promise", inner: FILEHANDLE_T }, valueParams: [
{ mode: "required", type: STRING },
{ mode: "optional", type: STRING, defaultValue: "r" },
{ mode: "optional", type: F64, defaultValue: 0o666 },
] },
},
// The bare module's POSIX-target binding; a win32 target rebinds it to
// the win32 table (builtinModuleFnsOf — Node on Windows IS path.win32).
+8 -4
View File
@@ -3869,6 +3869,7 @@ export type IrLibFn =
* empty array; null/undefined throw Node's catchable TypeError
* ("Cannot convert undefined or null to object"). */
| "dyn.objKeys"
| "dyn.forInKeys"
| "dyn.hasOwn"
| "dyn.assign"
/** Variadic Object.assign over CHECKED-DYNAMIC targets (`Object.assign(
@@ -6100,6 +6101,7 @@ export function canMarshalTypedFuncIntoIsland(
* (dynCheck's "expected IncomingMessage ..." texts). */
export const DYN_HANDLE_KINDS: ReadonlyMap<string, { tag: string; cls: string }> = new Map([
["child", { tag: "SCR_DYNH_CHILD", cls: "ChildProcess" }],
["fileHandle", { tag: "SCR_DYNH_FILE_HANDLE", cls: "FileHandle" }],
["httpReq", { tag: "SCR_DYNH_HTTP_REQ", cls: "IncomingMessage" }],
["httpRes", { tag: "SCR_DYNH_HTTP_RES", cls: "ServerResponse" }],
["netSocket", { tag: "SCR_DYNH_NET_SOCKET", cls: "Socket" }],
@@ -6281,10 +6283,11 @@ export function canBoxFuncIntoDyn(
): boolean {
return (
t.kind === "func" &&
// Only the legacy hidden-dyn rest ABI has a checked-dynamic call thunk.
// Typed rest stays static (its trailing array is compiler-packed), and
// island rest has its separate engine host-callback adapter.
(t.rest !== true || t.restAbi === undefined) &&
// Typed rest occupies the final native array parameter; its thunk
// checks a fresh array containing all remaining call arguments.
// Island rest keeps its separate engine host-callback adapter.
(t.rest !== true || t.restAbi === undefined ||
(t.restAbi === "typed" && t.params.at(-1)?.kind === "array")) &&
// A jsval (island) param converts through scr_jsval_from_dyn in the
// thunk (wrapped cells unwrap by reference, dyn data deep-copies) —
// the checker-'any' callback params of the routed-dispatch lane
@@ -7802,6 +7805,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.cloneTransferFail",
// the dyn Object walks throw on null/undefined receivers
"dyn.objKeys",
"dyn.forInKeys",
"dyn.hasOwn",
"dyn.assign",
// variadic Object.assign: spread flattening throws V8's spread-call
+1
View File
@@ -1098,6 +1098,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"error.domClone": { argTypes: [null, DYN], result: VOID },
"dyn.errInstanceof": { argTypes: [DYN, F64], result: BOOL },
"dyn.objKeys": { argTypes: [DYN], result: DYN },
"dyn.forInKeys": { argTypes: [DYN], result: DYN },
"dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL },
"dyn.assign": { argTypes: [DYN, DYN], result: DYN },
"dyn.packPush": { argTypes: [DYN, DYN], result: VOID },
@@ -0,0 +1,100 @@
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { analyze, compile } from "../src/index.js";
const sanitize = process.env["SCRIPTC_SAN"] === "1";
test.each(["c", "llvm"] as const)("native callable checks preserve supported arguments and reject before invoking (%s)", async (backend) => {
const dir = mkdtempSync(join(tmpdir(), "scriptc-callable-checks-"));
try {
const entry = join(dir, "main.ts");
writeFileSync(join(dir, "close.js"), `
export function closeUnknown(value) { return value.close(); }
export function enumerateUnknown(value) {
Object.defineProperties(value, { extra: { value: "visible", enumerable: true } });
const keys = [];
for (const key in value) keys.push(key);
return keys.join(",");
}
`);
writeFileSync(entry, `
import { open } from "node:fs/promises";
import { closeUnknown, enumerateUnknown } from "./close.js";
function join(prefix: string, ...parts: string[]): string { console.log("invoked"); return prefix + parts.join(":"); }
const boxed: unknown = join;
function argument(value: unknown): unknown { console.log("argument"); return value; }
if (typeof boxed === "function") {
try { boxed(argument("prefix:"), argument("valid"), argument(4)); }
catch (error) { if (error instanceof Error) console.log(error.name); }
console.log(boxed("after:", "one", "two"));
}
try { console.log(enumerateUnknown(boxed)); }
catch (error) { if (error instanceof Error) console.log(error.message.includes("for-in over this checked-dynamic kind is not supported yet")); }
const widened = open as (path: string, flags?: string | number, mode?: number | string) => ReturnType<typeof open>;
const operations = { open: widened };
function describe(value: unknown): string { console.log("described"); return typeof value; }
const describeKey = describe as (value: string | symbol) => string;
console.log(describeKey("supported"));
try { describeKey(Symbol.for("unsupported")); }
catch (error) { if (error instanceof Error) console.log(error.name); }
const file = process.argv[2]!;
try { await operations.open(file, 0); }
catch (error) { if (error instanceof Error) console.log(error.name); }
try { await operations.open(file, "w", "600"); }
catch (error) { if (error instanceof Error) console.log(error.name); }
const handle = await operations.open(file, "wx", 0o600);
const stored: unknown = handle;
await closeUnknown(stored);
console.log(handle.fd);
`);
const result = await compile(entry, { backend, dynamic: false, sanitize, outDir: dir, outPath: join(dir, "program") });
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
if (!result.ok) return;
const path = join(dir, "created.txt");
const child = spawnSync(result.binaryPath, [path], { encoding: "utf8" });
expect(child.status, child.stderr).toBe(0);
expect(child.stderr).toBe("");
expect(child.stdout).toBe("argument\nargument\nargument\nTypeError\ninvoked\nafter:one:two\ntrue\ndescribed\nstring\nTypeError\nTypeError\nTypeError\n-1\n");
expect(statSync(path).mode & 0o777).toBe(0o600);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test.each(["c", "llvm"] as const)("failed implicit specializations retain a callable throwing body (%s)", async (backend) => {
const dir = mkdtempSync(join(tmpdir(), "scriptc-failed-specialization-"));
try {
const pkg = join(dir, "node_modules", "callbacks");
mkdirSync(pkg, { recursive: true });
writeFileSync(join(dir, "package.json"), '{"type":"module"}');
writeFileSync(join(pkg, "package.json"), '{"name":"callbacks","type":"module","main":"index.js","types":"index.d.ts"}');
writeFileSync(join(pkg, "index.d.ts"), "export {};\n");
writeFileSync(join(pkg, "index.js"), `
const inspect = (value) => eval(value);
const first = { inspect };
const second = { inspect };
console.log(typeof first.inspect, first.inspect === second.inspect);
for (const callback of [first.inspect, second.inspect]) {
try { callback({}); }
catch (error) { console.log(String(error).includes("SC2011")); }
}
`);
const entry = join(dir, "main.js");
writeFileSync(entry, 'import "callbacks";');
const { coverage } = analyze(entry, { dynamic: false, npmStatic: "auto" });
expect(coverage.diagnostics).toEqual([]);
expect(coverage.runtimeFences?.length).toBeGreaterThan(0);
const result = await compile(entry, { backend, dynamic: false, npmStatic: "auto", sanitize, outDir: dir, outPath: join(dir, "program") });
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
if (!result.ok) return;
const child = spawnSync(result.binaryPath, [], { encoding: "utf8" });
expect(child.status, child.stderr).toBe(0);
expect(child.stderr).toBe("");
expect(child.stdout).toBe("function true\ntrue\ntrue\n");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
@@ -10849,6 +10849,48 @@
"<repo>/tests/corpus/module-fallback-functions/main.js"
],
"diags": []
},
"<repo>/tests/corpus/stored-native-builtins.js": {
"order": [
"<repo>/tests/corpus/stored-native-builtins.js"
],
"diags": []
},
"<repo>/tests/corpus/stored-object-helpers.js": {
"order": [
"<repo>/tests/corpus/stored-object-helpers.js"
],
"diags": []
},
"<repo>/tests/corpus/typed-rest-callables.ts": {
"order": [
"<repo>/tests/corpus/typed-rest-callables.ts"
],
"diags": []
},
"<repo>/tests/corpus/file-handle-values.ts": {
"order": [
"<repo>/tests/corpus/file-handle-values.ts"
],
"diags": []
},
"<repo>/tests/corpus/renderer-specializations.js": {
"order": [
"<repo>/tests/corpus/renderer-specializations.js"
],
"diags": []
},
"<repo>/tests/corpus/native-property-names.ts": {
"order": [
"<repo>/tests/corpus/native-property-names.ts"
],
"diags": []
},
"<repo>/tests/corpus/renderer-export-dictionaries.js": {
"order": [
"<repo>/tests/corpus/renderer-export-dictionaries.js"
],
"diags": []
}
}
}
@@ -32,7 +32,7 @@ const optional = [
["scr_inspect.c", "inspect"],
["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")],
["scr_dc.c", "dc"],
["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "nativeFetch")],
["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")],
["scr_zlib.c", "zlib"],
["scr_zlib_island.c", all("zlib", "dynamic")],
["scr_events.c", "events"],
@@ -18,7 +18,7 @@ const OPTIONAL = [
["scr_copying.c", "copying"], ["scr_file_handle.c", "fileHandle"],
["scr_regex.c", "regex"], ["scr_assert.c", any("assert", "regex", "symbol")],
["scr_inspect.c", "inspect"], ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")],
["scr_dc.c", "dc"], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "nativeFetch")],
["scr_dc.c", "dc"], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")],
["scr_zlib.c", "zlib"], ["scr_zlib_island.c", all("zlib", "dynamic")],
["scr_events.c", "events"], ["scr_readline.c", "events"],
["scr_events_emitter.c", "emitter"], ["scr_dyn_handle.c", any("emitter", "netEffective")],
+50
View File
@@ -255,6 +255,7 @@ static bool scr_file_handle_mode_valid(double mode) {
}
ScrFileHandle *scr_file_handle_open(ScrStr *path, ScrStr *flags, double mode) {
scr_file_handle_dyn_install();
if (!scr_file_handle_path_valid(path)) return NULL;
int of = scr_file_handle_open_flags(flags);
if (of < 0) return NULL;
@@ -510,3 +511,52 @@ ScrPromise *scr_file_handle_stat_promise(ScrFileHandle *h) {
return scr_promise_settled_ref(st, &scr_stats_retain_v,
&scr_stats_release_v, NULL);
}
/* The checked boundary retains the shared descriptor slot. Statically
* typed uses unwrap it before invoking the existing FileHandle surface. */
static ScrDyn *scr_file_handle_dyn_invoke(
void *h, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc, const char *what) {
(void)self;
(void)args;
(void)argc;
(void)what;
if (strcmp(method, "close") == 0) {
scr_file_handle_close(h);
ScrPromise *p = scr_promise_settled_ref(
scr_dyn_retain(scr_dyn_undefined()), &scr_dyn_retain_v,
&scr_dyn_release_v, NULL);
ScrDyn *out = scr_dyn_new_promise(p);
scr_promise_release(p);
return out;
}
static const char msg[] =
"FileHandle method calls through an 'unknown' value are not supported yet — narrow the value to FileHandle first";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
static ScrDyn *scr_file_handle_dyn_get(void *h, const char *key, size_t len) {
if (len == 2 && memcmp(key, "fd", 2) == 0) {
return scr_dyn_new_num(scr_file_handle_fd(h));
}
return NULL;
}
static bool scr_file_handle_dyn_set(
void *h, const char *key, size_t len, const ScrDyn *value) {
(void)h;
(void)key;
(void)len;
(void)value;
return false;
}
void scr_file_handle_dyn_install(void) {
static const ScrDynHandleOps ops = {
"FileHandle", &scr_file_handle_retain_v, &scr_file_handle_release_v,
&scr_file_handle_dyn_invoke, &scr_file_handle_dyn_get,
&scr_file_handle_dyn_set, NULL,
};
scr_dyn_handle_install(SCR_DYNH_FILE_HANDLE, &ops);
}
+31
View File
@@ -4267,6 +4267,37 @@ static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
ScrDyn *scr_dyn_obj_keys(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWALK_KEYS); }
/* Native dictionaries have only Object.prototype or a null prototype;
* neither contributes enumerable keys. Snapshot names without invoking
* getters; the loop rechecks live ownership before visiting each name. */
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v) {
switch (v->kind) {
case SCR_DYN_OBJ:
case SCR_DYN_ARR:
return scr_dyn_obj_keys(v);
case SCR_DYN_STR: {
ScrDyn *keys = scr_dyn_new_arr();
size_t length = (size_t)scr_str_utf16_len(v->v.str);
for (size_t i = 0; i < length; i++) {
char key[24];
int len = snprintf(key, sizeof key, "%zu", i);
scr_dyn_arr_push(keys, scr_dyn_objwalk_key(key, (size_t)len));
}
return keys;
}
case SCR_DYN_NULL:
case SCR_DYN_UNDEF:
case SCR_DYN_BOOL:
case SCR_DYN_NUM:
return scr_dyn_new_arr();
default: {
static const char msg[] = "for-in over this checked-dynamic kind is not supported yet";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
return NULL;
}
}
}
/* One source's own enumerable members onto an OBJ target. OBJ sources
* snapshot their keys, then check attributes and read values in order so
* getters can affect later entries. Arrays, strings, and bytes use their
+3
View File
@@ -3364,6 +3364,7 @@ typedef enum {
SCR_DYNH_EVENT, /* native static-fetch abort Event */
SCR_DYNH_ABORT_CONTROLLER, /* native static-fetch AbortController */
SCR_DYNH_CHILD, /* child_process.ChildProcess */
SCR_DYNH_FILE_HANDLE, /* fs/promises.FileHandle */
SCR_DYNH_COUNT,
} ScrDynHandleTag;
@@ -3523,6 +3524,7 @@ bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value);
* member nodes. null/undefined receivers throw Node's catchable
* TypeError. */
ScrDyn *scr_dyn_obj_keys(const ScrDyn *v);
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v);
/* Snapshot all own string keys of a SCR_DYN_OBJ in JS order. Returns +1. */
ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v);
/* Object.hasOwn over a dyn receiver: OBJ member presence, ARR index
@@ -3814,6 +3816,7 @@ typedef struct ScrDynHandleOps {
} ScrDynHandleOps;
void scr_dyn_handle_install(ScrDynHandleTag tag, const ScrDynHandleOps *ops);
void scr_file_handle_dyn_install(void);
/* The tag's class display name ("IncomingMessage") — error texts across
* units; answers "object" for an uninstalled tag (error paths only). */
const char *scr_dyn_handle_cls(const ScrDyn *d);
+16
View File
@@ -0,0 +1,16 @@
import { open, unlink } from "node:fs/promises";
import type { FileHandle } from "node:fs/promises";
const path = `/tmp/scriptc-file-handle-values-${process.pid}.txt`;
const handle = await open(path, "w+", 0o600);
try {
const stored: unknown = handle;
const alias: FileHandle = stored as FileHandle;
console.log(handle === alias, alias.fd === handle.fd);
await alias.writeFile("native", "utf8");
console.log((await handle.stat()).size);
await alias.close();
console.log(handle.fd, alias.fd);
await handle.close();
} finally {
await unlink(path);
}
+7
View File
@@ -0,0 +1,7 @@
const glyphs = { ".": [1, 2], "_": [3, 4], "$": 5, "_x24_": 6, "é": 7, "_xe9_": 8 };
console.log(glyphs["."].join(","), glyphs["_"].join(","));
console.log(glyphs["$"], glyphs["_x24_"], glyphs["é"], glyphs["_xe9_"]);
glyphs["."][0] = 9;
glyphs["_x24_"] = 10;
console.log(glyphs["."][0], glyphs["_"][0], glyphs["$"], glyphs["_x24_"]);
console.log(JSON.stringify(glyphs));
@@ -0,0 +1,28 @@
const define = Object.defineProperty;
function publish(target, all) {
for (var name in all) define(target, name, { get: all[name], enumerable: true });
}
let live = "first";
const exports = {};
publish(exports, { value: () => live, size: () => 2 });
console.log(exports.value, exports.size);
live = "second";
console.log(exports.value, Object.keys(exports).join(","));
let visits = 0;
const source = JSON.parse('{"10":"ten","2":"two","first":"one","removed":"gone"}');
define(source, "lazy", { get: () => { visits++; return "lazy"; }, enumerable: true });
define(source, "hidden", { value: "hidden" });
function receiver() { console.log("receiver"); return source; }
for (const key in receiver()) {
console.log(key);
if (key === "first") { delete source.removed; source.added = "late"; }
}
console.log(visits, Object.keys(source).join(","));
/** @param {any} value */
function names(value = null) {
const keys = [];
for (const key in value) keys.push(key);
return keys.join(",");
}
console.log(names(null), names(undefined), names(4), names(true));
console.log(names("a😀b"), names(["x", "y"]));
+37
View File
@@ -0,0 +1,37 @@
function collect(seed) {
var result = [String(seed)];
for (const [key, value] of Object.entries({ first: "one", second: "two" })) result.push(key + value);
for (const { name, count } of [{ name: "three", count: 3 }]) result.push(name + count);
var suffix = "!";
result.push(read());
function read() { return suffix; }
const later = () => last;
const last = "end";
result.push(later());
return result.join(",");
}
console.log(collect(1));
console.log(collect("two"));
class Parser {
value = null;
parse(input) { return input ? this.value : null; }
async settle(input) { return await Promise.resolve(input); }
}
const parser = new Parser();
console.log(parser.parse(false));
parser.value = "parsed";
console.log(parser.parse(true));
console.log(await parser.settle("ready"));
const parserAlias = { parser };
console.log(await parserAlias.parser.settle(7));
class Feed {
get id() { return "feed"; }
close(value) { console.log(this.id, value); }
}
function argument() { console.log("argument"); return "closed"; }
function makeFeed() { return new Feed(); }
let feed = null;
feed?.close(argument());
feed = makeFeed();
console.log(feed.id);
feed?.close(argument());
+30
View File
@@ -0,0 +1,30 @@
import { open, rename, unlink } from "node:fs/promises";
import { writeFileSync, statSync } from "node:fs";
import { join, basename } from "node:path";
import * as path from "node:path";
class Files {
static system = { open, rename, unlink };
}
const names = { join, basename };
console.log(typeof Files.system.open, typeof Files.system.rename, typeof Files.system.unlink);
console.log(names.join("a", "b", "c"), names.join(), names.basename("/a/b.ts", undefined));
console.log(names.join === path.join);
const file = `/tmp/scriptc-stored-builtins-${process.pid}.txt`;
writeFileSync(file, "hello");
try {
const handle = await Files.system.open(file, undefined, undefined);
const alias = handle;
console.log(handle === alias, await handle.readFile("utf8"));
await alias.close();
console.log(handle.fd);
await Files.system.rename(file, file + ".moved");
await Files.system.unlink(file + ".moved");
const created = await Files.system.open(file, "wx", 0o600);
console.log(statSync(file).isFile());
await created.close();
try { await Files.system.open(file + ".missing"); }
catch (error) { console.log(error.code); }
} finally {
await Files.system.unlink(file);
}
+20
View File
@@ -0,0 +1,20 @@
var define = Object.defineProperty;
var descriptor = Object.getOwnPropertyDescriptor;
var keys = Object.keys;
var values = Object.values;
var entries = Object.entries;
var defineMany = Object.defineProperties;
const target = {};
const alias = target;
let value = 2;
define(target, "value", { get: () => value, set: next => { value = next; }, enumerable: true, configurable: true });
console.log(alias === target, alias.value, keys(alias).join(","));
alias.value = 9;
console.log(value, values(target).join(","), entries(target)[0].join(":"));
const desc = descriptor(target, "value");
console.log(typeof desc.get, typeof desc.set, desc.enumerable, desc.configurable);
defineMany(target, { name: { value: "renderer", enumerable: true } });
console.log(target.name, keys(target).join(","), define === Object.defineProperty);
console.log(descriptor(target, "missing") === undefined);
try { define(target, "name", { value: "changed" }); }
catch (error) { console.log(error.name); }
+16
View File
@@ -0,0 +1,16 @@
function combine(prefix: string, ...values: string[]): string {
return prefix + values.join(":");
}
function total(...values: number[]): number {
return values.reduce((sum, value) => sum + value, 0);
}
const stored: unknown = combine;
const sum: unknown = total;
if (typeof stored === "function" && typeof sum === "function") {
console.log(stored("empty"), stored("many:", "a", "b"));
console.log(sum(), sum(1, 2, 3));
console.log(stored.call(null, "call:", "c", "d"));
console.log(stored.apply(null, ["apply:", "e", "f"]));
const bound = stored.bind(null, "bound:", "g");
console.log(bound("h", "i"));
}
+28
View File
@@ -128,6 +128,34 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
}
});
test.each(["stored-native-builtins.js", "stored-object-helpers.js", "renderer-specializations.js", "renderer-export-dictionaries.js"].flatMap((name) =>
(["c", "llvm"] as const).map((backend) => ({ name, backend })),
))("renderer startup $name compiles from shipped JavaScript ($backend)", async ({ name, backend }) => {
const dir = mkdtempSync(join(tmpdir(), "scriptc-renderer-startup-"));
try {
const pkg = join(dir, "node_modules", "renderer-startup");
mkdirSync(pkg, { recursive: true });
writeFileSync(join(dir, "package.json"), '{"type":"module"}');
writeFileSync(join(pkg, "package.json"), '{"name":"renderer-startup","type":"module","main":"index.js","types":"index.d.ts"}');
writeFileSync(join(pkg, "index.d.ts"), "export {};\n");
cpSync(join(repoRoot, "tests/corpus", name), join(pkg, "index.js"));
const entry = join(dir, "main.js");
writeFileSync(entry, 'import "renderer-startup";');
const { coverage } = analyze(entry, { npmStatic: "auto" });
expect(coverage.diagnostics).toEqual([]);
expect(coverage.runtimeFences ?? [], JSON.stringify(coverage.runtimeFences)).toEqual([]);
const result = await compile(entry, { backend, dynamic: false, npmStatic: "auto", sanitize, outDir: dir, outPath: join(dir, "program") });
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
if (!result.ok) return;
const [nodeRes, nativeRes] = await Promise.all([runBinary(process.execPath, [entry]), runBinary(result.binaryPath, [])]);
expect(nativeRes.stdout).toEqual(nodeRes.stdout);
expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr);
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test.each(["c", "llvm"] as const)("bundled class aliases preserve declared methods and callback fields (%s)", async (backend) => {
const entry = join(pilotRoot, "bundled-methods-cli.ts");
const { coverage } = analyze(entry, { npmStatic: "auto" });