diff --git a/packages/compiler/src/backend/c/exprs.ts b/packages/compiler/src/backend/c/exprs.ts index ef83247e..7a5e8011 100644 --- a/packages/compiler/src/backend/c/exprs.ts +++ b/packages/compiler/src/backend/c/exprs.ts @@ -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. diff --git a/packages/compiler/src/backend/c/walkers.ts b/packages/compiler/src/backend/c/walkers.ts index 32302490..2c11a6ab 100644 --- a/packages/compiler/src/backend/c/walkers.ts +++ b/packages/compiler/src/backend/c/walkers.ts @@ -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);`, `}`, ``, ); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 45680b79..e949ec61 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -46,6 +46,7 @@ const DYN_HANDLE_TAG_NUM: Record = { 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`, `}`, ``, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index b250229c..bb2b8f3b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -822,6 +822,7 @@ export const LIB_FN_SYMS: Record = { // (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 diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index f2572b5c..1fe34fc0 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -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 diff --git a/packages/compiler/src/backend/native-link-info.test.ts b/packages/compiler/src/backend/native-link-info.test.ts index 283224b6..006ef686 100644 --- a/packages/compiler/src/backend/native-link-info.test.ts +++ b/packages/compiler/src/backend/native-link-info.test.ts @@ -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", diff --git a/packages/compiler/src/backend/native-link-info.ts b/packages/compiler/src/backend/native-link-info.ts index cfa4fbf8..6a02c966 100644 --- a/packages/compiler/src/backend/native-link-info.ts +++ b/packages/compiler/src/backend/native-link-info.ts @@ -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"] : []), diff --git a/packages/compiler/src/backend/native-toolchain.ts b/packages/compiler/src/backend/native-toolchain.ts index 959a186b..7b8e64f1 100644 --- a/packages/compiler/src/backend/native-toolchain.ts +++ b/packages/compiler/src/backend/native-toolchain.ts @@ -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. diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 6d47c640..4da1c741 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -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 }; diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index f504b36b..cf5cf293 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 170fc4ec..d9c1b23e 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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,): 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); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 5f58ccdd..2ecccccc 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 46893936..1d979d04 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e7ab28eb..c48bcad8 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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 { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index db9df46c..06f244a7 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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>(); + const partialDynParams = new Map(); 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, diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 2c704fcf..2fab8b19 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -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 = { + 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 = 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 = 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 diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 0bc12701..15be09b9 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -1098,6 +1098,7 @@ export const LIB_FN_SIGS: Record { + 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; +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 }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 58bb5afd..8247260d 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -10849,6 +10849,48 @@ "/tests/corpus/module-fallback-functions/main.js" ], "diags": [] + }, + "/tests/corpus/stored-native-builtins.js": { + "order": [ + "/tests/corpus/stored-native-builtins.js" + ], + "diags": [] + }, + "/tests/corpus/stored-object-helpers.js": { + "order": [ + "/tests/corpus/stored-object-helpers.js" + ], + "diags": [] + }, + "/tests/corpus/typed-rest-callables.ts": { + "order": [ + "/tests/corpus/typed-rest-callables.ts" + ], + "diags": [] + }, + "/tests/corpus/file-handle-values.ts": { + "order": [ + "/tests/corpus/file-handle-values.ts" + ], + "diags": [] + }, + "/tests/corpus/renderer-specializations.js": { + "order": [ + "/tests/corpus/renderer-specializations.js" + ], + "diags": [] + }, + "/tests/corpus/native-property-names.ts": { + "order": [ + "/tests/corpus/native-property-names.ts" + ], + "diags": [] + }, + "/tests/corpus/renderer-export-dictionaries.js": { + "order": [ + "/tests/corpus/renderer-export-dictionaries.js" + ], + "diags": [] } } } diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index adc1b45d..46186498 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -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"], diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 623557c6..78116088 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -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")], diff --git a/packages/runtime/src/scr_file_handle.c b/packages/runtime/src/scr_file_handle.c index 95e0a8c8..d7eae32b 100644 --- a/packages/runtime/src/scr_file_handle.c +++ b/packages/runtime/src/scr_file_handle.c @@ -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); +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 167d8847..f0a62c8c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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 diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 37d7017b..59f415f4 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -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); diff --git a/tests/corpus/file-handle-values.ts b/tests/corpus/file-handle-values.ts new file mode 100644 index 00000000..78f2453a --- /dev/null +++ b/tests/corpus/file-handle-values.ts @@ -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); +} diff --git a/tests/corpus/native-property-names.ts b/tests/corpus/native-property-names.ts new file mode 100644 index 00000000..1263378a --- /dev/null +++ b/tests/corpus/native-property-names.ts @@ -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)); diff --git a/tests/corpus/renderer-export-dictionaries.js b/tests/corpus/renderer-export-dictionaries.js new file mode 100644 index 00000000..c3206685 --- /dev/null +++ b/tests/corpus/renderer-export-dictionaries.js @@ -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"])); diff --git a/tests/corpus/renderer-specializations.js b/tests/corpus/renderer-specializations.js new file mode 100644 index 00000000..378903c1 --- /dev/null +++ b/tests/corpus/renderer-specializations.js @@ -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()); diff --git a/tests/corpus/stored-native-builtins.js b/tests/corpus/stored-native-builtins.js new file mode 100644 index 00000000..cbac5bb4 --- /dev/null +++ b/tests/corpus/stored-native-builtins.js @@ -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); +} diff --git a/tests/corpus/stored-object-helpers.js b/tests/corpus/stored-object-helpers.js new file mode 100644 index 00000000..2ba09ea0 --- /dev/null +++ b/tests/corpus/stored-object-helpers.js @@ -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); } diff --git a/tests/corpus/typed-rest-callables.ts b/tests/corpus/typed-rest-callables.ts new file mode 100644 index 00000000..606bedb8 --- /dev/null +++ b/tests/corpus/typed-rest-callables.ts @@ -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")); +} diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index f4df496f..36f067e5 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -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" });