From 2b2dd839499c76d7f75b4b57f127106b8257a542 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 23 Jul 2026 03:23:24 -0500 Subject: [PATCH] Zero the whole-program validator's SC9001 families: the big JS graph builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - island ('any') arguments at intrinsic/libCall/callValue slots take the validated exit (dynCheck's stance); non-exit-safe slots fence with the slot named, and the boundary pass backstops every jsOp/dynCall/dynInvoke argument so world-mixing sites can never ICE - a checker-'any' receiver/callee that LOWERED checked-dynamic dispatches to the DOM machinery: ambient-this reads are dynKeyGet, rest-args map/forEach ride the runtime dispatch, dyn callees take dynCall - Object.entries/values push unit-armed (null | undefined) fields as the null literal; new Set(seed) exits an island-handle seed as a validated copy - fence-bodied functions emit even when their ABI names an unregistered class, and a run()-end sweep rewrites leftover unregistered-class type slots to the inert f64 placeholder — the clang invalid-C escape closes with the ICE - RegExp[] compiles (REF elements, both backends; map callbacks returning other unrepresentable kinds meet a named fence), and the C emitter appends program-sized line arrays by loop, not spread (500k-line TUs overflowed the stack) --- .../src/backend/emission/emit-shapes.ts | 1 + .../compiler/src/backend/emission/emitter.ts | 14 ++- packages/compiler/src/backend/llvm/shapes.ts | 1 + .../src/frontend/lowering/lib-boundary.ts | 82 ++++++++++++- .../src/frontend/lowering/lower-calls.ts | 59 +++++++--- .../src/frontend/lowering/lower-classes.ts | 17 ++- .../src/frontend/lowering/lower-containers.ts | 34 +++++- .../src/frontend/lowering/lower-exprs.ts | 16 +++ .../compiler/src/frontend/lowering/lowerer.ts | 48 ++++++++ packages/compiler/src/frontend/types.ts | 7 +- .../test/ts7/baselines/order-parity.json | 109 +++++++++++------- .../corpus/2447-island-boundary-slots/main.ts | 60 ++++++++++ .../node_modules/taglists/index.d.ts | 4 + .../node_modules/taglists/index.js | 5 + .../node_modules/taglists/package.json | 5 + .../2448-entries-null-arms-regex-arrays.ts | 98 ++++++++++++++++ tests/corpus/2449-js-dyn-worlds/main.js | 86 ++++++++++++++ tests/corpus/2449-js-dyn-worlds/tsconfig.json | 1 + tests/corpus/2449-js-dyn-worlds/walker.js | 13 +++ tests/diagnostics/island-boundary-slots.ts | 9 ++ tests/diagnostics/regex.ts | 3 +- .../island-boundary-slots.ts.txt | 8 ++ tests/harness/__snapshots__/regex.ts.txt | 24 +--- tests/harness/errors.test.ts | 37 +++++- 24 files changed, 648 insertions(+), 93 deletions(-) create mode 100644 tests/corpus/2447-island-boundary-slots/main.ts create mode 100644 tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.d.ts create mode 100644 tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.js create mode 100644 tests/corpus/2447-island-boundary-slots/node_modules/taglists/package.json create mode 100644 tests/corpus/2448-entries-null-arms-regex-arrays.ts create mode 100644 tests/corpus/2449-js-dyn-worlds/main.js create mode 100644 tests/corpus/2449-js-dyn-worlds/tsconfig.json create mode 100644 tests/corpus/2449-js-dyn-worlds/walker.js create mode 100644 tests/diagnostics/island-boundary-slots.ts create mode 100644 tests/harness/__snapshots__/island-boundary-slots.ts.txt diff --git a/packages/compiler/src/backend/emission/emit-shapes.ts b/packages/compiler/src/backend/emission/emit-shapes.ts index 335c43e0..358bcf68 100644 --- a/packages/compiler/src/backend/emission/emit-shapes.ts +++ b/packages/compiler/src/backend/emission/emit-shapes.ts @@ -730,6 +730,7 @@ export interface ClassMeta { // machinery as record/object/union elements. elem.kind === "promise" || elem.kind === "jsval" || // island handles: scr_jsval_* adapters, no trace + elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace elem.kind === "netServer" || // server handles: scr_net_server_* adapters, no trace elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace diff --git a/packages/compiler/src/backend/emission/emitter.ts b/packages/compiler/src/backend/emission/emitter.ts index e0a446ec..5b3d404f 100644 --- a/packages/compiler/src/backend/emission/emitter.ts +++ b/packages/compiler/src/backend/emission/emitter.ts @@ -563,7 +563,7 @@ export class CEmitter { ); } if (this.unitInstances.size > 0) out.push(""); - out.push(...structDefs); + for (const line of structDefs) out.push(line); // program-sized: never spread for (const [key, sym] of this.regexInstances) { // One immortal ScrRegex per (pattern, flags) literal, pointing at the // interned source/flags strings. `.bc` starts NULL: the runtime @@ -641,9 +641,17 @@ export class CEmitter { // Type-directed JSON walkers (jsonStringify serializers, dynCheck // matchers/builders), interned per type during body emission above. if (this.walkerProtos.length > 0) { - out.push("", ...this.walkerProtos, "", ...this.walkerDefs); + out.push(""); + for (const line of this.walkerProtos) out.push(line); + out.push(""); + for (const line of this.walkerDefs) out.push(line); } - out.push("", ...body); + // Loop-appended, never spread: `body` scales with the PROGRAM (a large + // embedded graph emits hundreds of thousands of lines), and a spread + // push passes every line as a call argument — the engine's stack + // overflows long before memory matters. + out.push(""); + for (const line of body) out.push(line); const refGlobals = globals.filter((g) => isRefCounted(g.type)); // Interned function-value closures are IMMORTAL (rc == SIZE_MAX), so // an own-property table Object.defineProperties hung on one would diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index ea56d6cc..7b737b59 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -510,6 +510,7 @@ export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): stri elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace elem.kind === "netServer" || // server handles ([...set] drains): REF, no trace elem.kind === "jsval" || // island handles (`any[]` under --dynamic): REF, no trace + elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) (elem.kind === "array" && traceAdapter(host, elem) !== null); if (!useRef) { host.declare(`declare ptr @scr_arr_new(i32, i64)`); diff --git a/packages/compiler/src/frontend/lowering/lib-boundary.ts b/packages/compiler/src/frontend/lowering/lib-boundary.ts index ce9cfa9e..4577686f 100644 --- a/packages/compiler/src/frontend/lowering/lib-boundary.ts +++ b/packages/compiler/src/frontend/lowering/lib-boundary.ts @@ -38,7 +38,7 @@ * cleanly before. The validator stays the backstop for anything else. */ import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; -import { canAdaptDynFuncTo, IrExpr, IrType, SrcLoc, STRING, isUnitType, typeEquals } from "../../ir/nodes.js"; +import { canAdaptDynFuncTo, canMarshalTypedFuncIntoIsland, DYN, IrExpr, IrType, JSVAL, SrcLoc, STRING, isUnitType, typeEquals } from "../../ir/nodes.js"; import { LIB_FN_SIGS, REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/validate.js"; import { unionMismatchDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; @@ -66,6 +66,18 @@ function dynCheckable(L: Lowerer, want: IrType): boolean { * statement takes the runtime-fence path in JS sources. */ function coerceSlot(L: Lowerer, arg: IrExpr, want: IrType, what: string): IrExpr { if (typeEquals(arg.type, want)) return arg; + // An ISLAND value in a typed intrinsic slot (`/x/.test(anyText)`, + // `" ".repeat(anyN)` — checker-`any` arguments the surface signatures + // accept): the VALIDATED island exit, exactly the dynCheck stance one + // branch down — strict for primitives, JSON round-trip for composites, + // a lying handle throws the catchable TypeError. Targets outside the + // exit set keep the named fence. + if (arg.type.kind === "jsval" && want.kind !== "jsval") { + if (L.boundaryExitSafe(want)) { + return { kind: "jsExit", value: arg, type: want, loc: arg.loc }; + } + fence(L, "SC1100", arg.loc, `passing 'any'-typed values where '${L.fmt(want)}' is expected (${what})`); + } if (arg.type.kind === "dyn" && want.kind !== "dyn") { if (dynCheckable(L, want)) { return { kind: "dynCheck", value: arg, type: want, loc: arg.loc }; @@ -143,18 +155,22 @@ export function enforceLibBoundary(L: Lowerer, node: unknown): void { } if (kind === "arrIntrinsic") { const e = rec as unknown as Extract; - if (e.receiver.type.kind === "dyn" || isUnitType(e.receiver.type)) { + if (e.receiver.type.kind === "dyn" || e.receiver.type.kind === "jsval" || isUnitType(e.receiver.type)) { // No element type exists to validate a dyn receiver against — the - // honest answer is the operations-on-unknown fence. Other non-array - // receivers stay the validator's ICE (frontend breakage, not a - // checked-dynamic escape). + // honest answer is the operations-on-unknown fence. An island + // (jsval) receiver fences too: the exit would COPY the engine + // array, so a static intrinsic over it silently mutates the copy — + // the producing sites route jsval receivers through the engine's + // own methods instead. Other non-array receivers stay the + // validator's ICE (frontend breakage, not a checked-dynamic + // escape). fence(L, "SC1100", e.receiver.loc, `'.${e.method}()' on '${L.fmt(e.receiver.type)}' array receivers`); } return; } if (kind === "callValue") { const e = rec as unknown as Extract; - if (e.callee.type.kind === "dyn" || isUnitType(e.callee.type)) { + if (e.callee.type.kind === "dyn" || e.callee.type.kind === "jsval" || isUnitType(e.callee.type)) { fence(L, "SC1100", e.loc, `calling '${L.fmt(e.callee.type)}' values`); } if (e.callee.type.kind !== "func") return; // validator's ICE otherwise @@ -173,4 +189,58 @@ export function enforceLibBoundary(L: Lowerer, node: unknown): void { }); return; } + if (kind === "jsOp") { + // Engine-op arguments must be jsval. The producing sites marshal with + // jsvalIn, but a checker-`any` expression can LOWER to another world + // (dyn.this, the DOM WeakSet placeholder, JS rest-args) — re-apply + // jsvalIn's rules here: units become the engine's own units, typed + // values with an island representation marshal in, and dyn values (no + // DOM→engine bridge that preserves handles/functions) fence with + // jsvalIn's own message. + const e = rec as unknown as Extract; + e.args.forEach((a, i) => { + if (a.type.kind === "jsval") return; + if (isUnitType(a.type)) { + e.args[i] = { kind: "jsOp", op: a.type.kind === "undefinedT" ? "undefLit" : "nullLit", args: [], type: JSVAL, loc: a.loc }; + return; + } + if (a.type.kind === "dyn") { + fence(L, "SC1100", a.loc, "passing 'unknown' values into dynamically-executed ('any'-typed) code (validate with 'as ' first)"); + } + if ( + L.boundarySafe(a.type) || + (a.type.kind === "func" && canMarshalTypedFuncIntoIsland(a.type, (id) => L.shapes.get(id), (id) => L.unions.get(id))) + ) { + e.args[i] = { kind: "jsMarshal", value: a, type: JSVAL, loc: a.loc }; + return; + } + fence(L, "SC1090", a.loc, `'${L.fmt(a.type)}' values crossing into dynamically-executed ('any'-typed) code`); + }); + return; + } + if (kind === "dynCall" || kind === "dynInvoke") { + // Checked-dynamic call arguments must be dyn. Convertible typed values + // take the ordinary dynFrom crossing; island values have NO bridge + // into the DOM (a jsval handle cannot ride the deep-copy), so they + // fence — named, catchable at runtime in JS sources, never an ICE. + const e = rec as unknown as Extract; + if (kind === "dynInvoke") { + const inv = e as Extract; + if (inv.recv.type.kind !== "dyn") { + fence(L, "SC1100", inv.recv.loc, `'.${inv.method}()' calls through '${L.fmt(inv.recv.type)}' receivers in checked-dynamic positions`); + } + } + e.args.forEach((a, i) => { + if (a.type.kind === "dyn") return; + if (a.type.kind === "jsval") { + fence(L, "SC1100", a.loc, "passing 'any'-typed values into calls through 'unknown' values (validate with 'as ' first)"); + } + if (a.kind === "unitLit" || L.dynConvertible(a.type)) { + e.args[i] = { kind: "dynFrom", value: a, type: DYN, loc: a.loc }; + return; + } + fence(L, "SC1100", a.loc, `passing '${L.fmt(a.type)}' values into calls through 'unknown' values`); + }); + return; + } } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 313c4162..a2bd7fde 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2797,6 +2797,20 @@ export function lowerCall(L: Lowerer, expr: ts.CallExpression): IrExpr { L.isIslandExpr(expr.expression.expression) ) { const receiver = L.lowerExpr(expr.expression.expression); + // A checker-`any` receiver whose VALUE lives in the DOM (a + // checked-dynamic local behind the any-typed spelling — the JS + // WeakSet placeholder, rest-args arrays): the checked-dynamic + // method machinery owns it — receiver-kind dispatch, stored-member + // calls, honest fences — never an engine op over a dyn value. + if (receiver.type.kind === "dyn") { + const served = lowerDynReceiverMethodCall(L, expr, expr.expression); + if (served) return served; + L.unsupported( + "SC1100", + expr, + `'.${expr.expression.name.text}()' calls through 'unknown'-valued receivers in dynamically-executed positions`, + ); + } const args = expr.arguments.map((a) => L.jsvalIn(L.lowerExpr(a), a)); const result: IrExpr = { kind: "jsOp", op: "callMethod", name: expr.expression.name.text, @@ -2806,6 +2820,20 @@ export function lowerCall(L: Lowerer, expr: ts.CallExpression): IrExpr { } if (L.isIslandExpr(expr.expression)) { const callee = L.lowerExpr(expr.expression); + // A checker-`any` callee that LOWERED checked-dynamic (a dyn member + // chain's stored function): the DOM's own call — dynCall reads and + // calls the stored member with Node's is-not-a-function TypeError + // on refusal. Island-typed arguments meet the boundary pass's + // dynCall rule (no jsval→DOM bridge exists — the named fence). + if (callee.type.kind === "dyn") { + const args = expr.arguments.map((a) => L.lowerExprExpecting(a, DYN)); + const calleeName = ts.isPropertyAccessExpression(expr.expression) + ? expr.expression.getText() + : ts.isIdentifier(expr.expression) + ? expr.expression.text + : "value"; + return { kind: "dynCall", callee, calleeName, args, type: DYN, loc }; + } const args = expr.arguments.map((a) => L.jsvalIn(L.lowerExpr(a), a)); const result: IrExpr = { kind: "jsOp", op: "callFn", args: [callee, ...args], type: JSVAL, loc }; return islandPrimitiveExit(L, expr, result); @@ -6004,8 +6032,15 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression, vt = f.type; } else if (others.length === 1) { vt = others[0]!; - const narrowTag = L.armTag(f.type.unionId, vt); - value = { kind: "unionNarrow", unionId: f.type.unionId, tag: narrowTag, value: raw, type: vt, loc }; + // A UNIT other arm (`null | undefined` fields — the mixed- + // defaults spread idiom; undefined was filtered above, so + // the unit is null): units carry no payload, so the guarded + // push writes the unit LITERAL — unionNarrow to a unit arm + // (and unionWrap of a narrowed unit) is malformed IR; the + // literal is the one legal unit spelling. + value = isUnitType(vt) + ? { kind: "unitLit", unit: "null", type: vt, loc } + : { kind: "unionNarrow", unionId: f.type.unionId, tag: L.armTag(f.type.unionId, vt), value: raw, type: vt, loc }; } else { L.unsupported( "SC1090", @@ -6158,19 +6193,13 @@ export function lowerFunction(L: Lowerer, decl: ts.FunctionDeclaration): IrFunct ) { const captured = L.diags.splice(diagsBefore); L.runtimeFences.push(...captured); - // An ABI type naming a class that never REGISTERED (the formatter idiom's - // AstPath — the #private fence): the emitter would name a struct - // that does not exist, so no fence function can be built. No call - // site can lower either (producing the unregistered class's value - // fences first — the brokenGlobals/brokenLocals family), so the - // symbol is never referenced; the validator's registration check - // is the backstop for anything that slips through. - if ( - sig.params.some((p) => L.typeNamesUnregisteredClass(p.type)) || - L.typeNamesUnregisteredClass(bodyReturn) - ) { - return null; - } + // An ABI type naming a class that never REGISTERED (the sentence- + // walker idiom's path type — the #private fence) is fine to emit: + // callers CAN lower calls to this symbol (a same-typed param + // passes straight through — no construction needed), so the fence + // function must exist, and run()'s unregistered-class sweep + // rewrites every such slot to the inert f64 placeholder before + // emission — caller and fence stay ABI-consistent. const first = captured[0]!; const loc = locOf(decl); const pos = ts.getLineAndCharacterOfPosition( diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index ef03787b..1fb7901e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -4974,7 +4974,22 @@ export function lowerNew(L: Lowerer, expr: ts.NewExpression): IrExpr { if (!ts.isSpreadElement(argNode)) { const argIr = L.mapTypeOf(L.typeOf(argNode)); if (argIr?.kind === "array" && typeEquals(argIr.elem, mapped.elem)) { - return { kind: "setNew", seed: L.lowerExpr(argNode), type: mapped, loc }; + let seed = L.lowerExpr(argNode); + // A T[]-DECLARED seed whose value is an island handle (a + // package's exported array — the binding never held a + // static array): the VALIDATED exit copies the engine + // array out (strict elements, the catchable TypeError on a + // lying handle), and the bulk add proceeds on the copy — + // construction reads the seed once, so the aliasing + // divergence has nothing to observe. + if (seed.type.kind === "jsval" && L.boundaryExitSafe(arrayOf(mapped.elem))) { + seed = { kind: "jsExit", value: seed, type: arrayOf(mapped.elem), loc: seed.loc }; + } + if (typeEquals(seed.type, arrayOf(mapped.elem))) { + return { kind: "setNew", seed, type: mapped, loc }; + } + // Any other lowered kind falls through to the named fence + // below — never a mistyped seed into the validator. } } } diff --git a/packages/compiler/src/frontend/lowering/lower-containers.ts b/packages/compiler/src/frontend/lowering/lower-containers.ts index 71f86564..7b6ed958 100644 --- a/packages/compiler/src/frontend/lowering/lower-containers.ts +++ b/packages/compiler/src/frontend/lowering/lower-containers.ts @@ -402,10 +402,31 @@ import { own, WidthLift } from "./lowerer.js"; const { fnArg, arity } = hofCallbackArg(L, argNode, [elem], arrayOf(elem)); const fnRet = fnArg.type.ret; if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) { - // The result array U[] is unrepresentable (no void elements; ScrArr - // has no closure element kind). + // The result array U[] is unrepresentable here (no void elements; + // the map helper's closure-returning form has no fixture-backed + // story yet). L.badType(call, L.typeOf(call)); } + if ( + method === "map" && + (fnRet.kind === "map" || fnRet.kind === "set" || fnRet.kind === "url" || + fnRet.kind === "searchParams" || fnRet.kind === "generator" || fnRet.kind === "caught" || + fnRet.kind === "stats" || fnRet.kind === "spawnRes" || fnRet.kind === "netSocket" || + fnRet.kind === "dgramSocket" || fnRet.kind === "testCtx" || fnRet.kind === "httpReq" || + fnRet.kind === "httpRes" || fnRet.kind === "httpClientReq" || fnRet.kind === "secureCtx" || + fnRet.kind === "fsWatcher" || fnRet.kind === "childStream" || fnRet.kind === "procStream" || + isUnitType(fnRet)) + ) { + // The result would be an array of an element kind ScrArr has no + // home for (mapTypeOf's own array exclusions) — the callback return + // type bypasses that gate, so it is enforced here: a named fence, + // never a mistyped array into the backends. + L.unsupported( + "SC1090", + call, + `'.map()' with a callback returning '${L.fmt(fnRet)}' values (arrays of this element kind have no representation — store the values individually)`, + ); + } if (method === "map" && fnRet.kind === "dyn") { // A checked-dynamic callback return would make the result a // dyn-element STATIC array, which has no backend representation @@ -5159,7 +5180,14 @@ const DV_GETTERS: Record= 0 && narrowTag >= 0) { - const narrowed: IrExpr = { kind: "unionNarrow", unionId: f.type.unionId, tag: narrowTag, value: raw, type: others[0]!, loc }; + const other = others[0]!; + // A UNIT other arm pushes the unit LITERAL (undefined + // was filtered above, so the unit is null; units carry + // no payload and narrowing to a unit arm is malformed + // IR) — the fixed-shape helper's rule exactly. + const narrowed: IrExpr = isUnitType(other) + ? { kind: "unitLit", unit: "null", type: other, loc } + : { kind: "unionNarrow", unionId: f.type.unionId, tag: narrowTag, value: raw, type: other, loc }; return { kind: "unionWrap", unionId: valueT.unionId, tag: otherTag, value: narrowed, type: valueT, loc }; } } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index f8a49b20..78376709 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1040,6 +1040,22 @@ function lowerExprInner(L: Lowerer, expr: ts.Expression): IrExpr { // bound handle). if (L.isIslandExpr(expr.expression)) { const receiver = L.lowerExpr(expr.expression); + // The checker said 'any' but the VALUE lives in the DOM (`this` + // in a plain JS function — dyn.this — or a checked-dynamic local + // behind an any-typed spelling): the property read is the DOM's + // own keyed read, dyn results and dyn chains exactly like every + // checked-dynamic member access (a nullish receiver throws V8's + // catchable TypeError, exactly Node). Never a jsOp over a dyn — + // the two dynamic worlds don't share a value representation. + if (receiver.type.kind === "dyn") { + return { + kind: "dynKeyGet", + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + value: receiver, + type: DYN, + loc, + }; + } const read: IrExpr = { kind: "jsOp", op: "getProp", name: expr.name.text, args: [receiver], type: JSVAL, loc }; // A member the .d.ts DECLARES as a primitive exits eagerly to that // static type (`f.mediaType` on a package handle IS a string): diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 9a4603c9..a8c12bcd 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1633,6 +1633,20 @@ export class Lowerer { // the emitted program references, flushing deferred class diagnostics // that a reached type makes relevant. const artifacts = this.moduleArtifacts(functions); + // Types still naming a class that never REGISTERED after retention's + // flush (JS graphs whose class fences deferred to runtime — the + // sentence-walker's path params, printer tables whose func-typed + // fields spell the fenced class): the emitter would name a struct + // that does not exist — the compile-C escape family. No instance of + // such a class can ever exist (every construction site fenced), so + // the slots are inert by construction: rewrite each to the f64 dummy + // placeholder (boxNewC's uncollected-class stance, applied to unboxed + // slots), uniformly across params/locals/globals/fields/body types so + // every producer and consumer agrees. Programs with no unregistered + // reference are untouched — byte-stability holds. + if (this.diags.length === 0) { + this.sanitizeUnregisteredClassTypes([functions, this.globalsList, artifacts.classes, artifacts.records, artifacts.unions]); + } const module: IrModule | null = this.diags.length > 0 ? null @@ -1659,6 +1673,40 @@ export class Lowerer { }; } + /** The unregistered-class type sweep (run()'s last step before the + * module assembles): every `{kind:"object"}` TYPE naming a class with + * no registered ClassInfo is rewritten IN PLACE to the f64 dummy. + * classval types are exempt (they emit the class-independent + * `ScrClassObj *` — inert-but-valid storage, the validator's own + * stance), and only type objects rewrite — node-level classNames + * (`new`, upcasts) cannot reach here (their lowerings fence without a + * registered class), so the validator still backstops those. */ + sanitizeUnregisteredClassTypes(roots: unknown[]): void { + const isUnregisteredObjectType = (v: unknown): boolean => + typeof v === "object" && v !== null && + (v as { kind?: unknown }).kind === "object" && + typeof (v as { className?: unknown }).className === "string" && + !this.classes.has((v as { className: string }).className); + const sweep = (node: unknown): void => { + if (node === null || typeof node !== "object") return; + if (Array.isArray(node)) { + node.forEach((item, i) => { + if (isUnregisteredObjectType(item)) node[i] = F64; + else sweep(item); + }); + return; + } + const rec = node as Record; + for (const key of Object.keys(rec)) { + if (key === "loc") continue; + const v = rec[key]; + if (isUnregisteredObjectType(v)) rec[key] = F64; + else sweep(v); + } + }; + for (const root of roots) sweep(root); + } + /** True when `t` (recursively) names a class instance type with no * registered ClassInfo — the shape of a JS class whose collection fenced. * Used by run()'s global pruning; shapes/unions recurse with a seen-set diff --git a/packages/compiler/src/frontend/types.ts b/packages/compiler/src/frontend/types.ts index b21fd3ac..e2a9572f 100644 --- a/packages/compiler/src/frontend/types.ts +++ b/packages/compiler/src/frontend/types.ts @@ -707,12 +707,17 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the messages-building pattern (`const content: any[] = []`) — which // keeps its static array-of-handles representation. if (elem?.kind === "jsval" && !(elemTs.flags & ts.TypeFlags.Any)) return JSVAL; + // RegExp elements ride REF (scr_regex_retain_v/release_v, no trace — + // a regex holds only its bytecode and source): the derived-pattern + // idiom `[bases].map(ps => new RegExp(...))` builds real regex + // arrays, elements flow into the regex intrinsics unchanged, and + // indexOf/includes/=== are the REF kind's pointer identity — exactly + // JS object identity for RegExp values. if ( !elem || elem.kind === "void" || elem.kind === "map" || elem.kind === "set" || - elem.kind === "regex" || elem.kind === "url" || elem.kind === "searchParams" || elem.kind === "stats" || diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 34c35ad6..c70a4590 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -4303,6 +4303,25 @@ ], "diags": [] }, + "/tests/corpus/2447-island-boundary-slots/main.ts": { + "order": [ + "/tests/corpus/2447-island-boundary-slots/main.ts" + ], + "diags": [] + }, + "/tests/corpus/2448-entries-null-arms-regex-arrays.ts": { + "order": [ + "/tests/corpus/2448-entries-null-arms-regex-arrays.ts" + ], + "diags": [] + }, + "/tests/corpus/2449-js-dyn-worlds/main.js": { + "order": [ + "/tests/corpus/2449-js-dyn-worlds/walker.js", + "/tests/corpus/2449-js-dyn-worlds/main.js" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts" @@ -5807,6 +5826,12 @@ ], "diags": [] }, + "/tests/diagnostics/island-boundary-slots.ts": { + "order": [ + "/tests/diagnostics/island-boundary-slots.ts" + ], + "diags": [] + }, "/tests/diagnostics/island.ts": { "order": [ "/tests/diagnostics/island.ts" @@ -6143,48 +6168,6 @@ ], "diags": [] }, - "/tests/fixtures/node-types/argv-env.ts": { - "order": [ - "/tests/fixtures/node-types/argv-env.ts" - ], - "diags": [] - }, - "/tests/fixtures/node-types/fenced.ts": { - "order": [ - "/tests/fixtures/node-types/fenced.ts" - ], - "diags": [] - }, - "/tests/fixtures/node-types/import-fences.ts": { - "order": [ - "/tests/fixtures/node-types/import-fences.ts" - ], - "diags": [ - { - "code": "SC1010", - "message": "the 'v8' module is not supported yet", - "loc": { - "file": "/tests/fixtures/node-types/import-fences.ts", - "start": 422, - "end": 461 - }, - "milestone": "M4", - "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), installed npm packages (their code runs under --dynamic), and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, readline, events, stream, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, and node:test and node:module modules (bare or node:-prefixed) are supported" - } - ] - }, - "/tests/fixtures/node-types/path-os.ts": { - "order": [ - "/tests/fixtures/node-types/path-os.ts" - ], - "diags": [] - }, - "/tests/fixtures/node-types/stream-capture.ts": { - "order": [ - "/tests/fixtures/node-types/stream-capture.ts" - ], - "diags": [] - }, "/tests/fixtures/npm/cases/abandoned-handles/main.ts": { "order": [ "/tests/fixtures/npm/cases/abandoned-handles/main.ts" @@ -6536,6 +6519,48 @@ "hint": "set \"strictNullChecks\": true (or \"strict\": true) in the project tsconfig" } ] + }, + "/tests/fixtures/node-types/argv-env.ts": { + "order": [ + "/tests/fixtures/node-types/argv-env.ts" + ], + "diags": [] + }, + "/tests/fixtures/node-types/fenced.ts": { + "order": [ + "/tests/fixtures/node-types/fenced.ts" + ], + "diags": [] + }, + "/tests/fixtures/node-types/import-fences.ts": { + "order": [ + "/tests/fixtures/node-types/import-fences.ts" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'v8' module is not supported yet", + "loc": { + "file": "/tests/fixtures/node-types/import-fences.ts", + "start": 422, + "end": 461 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), installed npm packages (their code runs under --dynamic), and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, readline, events, stream, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, and node:test and node:module modules (bare or node:-prefixed) are supported" + } + ] + }, + "/tests/fixtures/node-types/path-os.ts": { + "order": [ + "/tests/fixtures/node-types/path-os.ts" + ], + "diags": [] + }, + "/tests/fixtures/node-types/stream-capture.ts": { + "order": [ + "/tests/fixtures/node-types/stream-capture.ts" + ], + "diags": [] } } } diff --git a/tests/corpus/2447-island-boundary-slots/main.ts b/tests/corpus/2447-island-boundary-slots/main.ts new file mode 100644 index 00000000..af6a791d --- /dev/null +++ b/tests/corpus/2447-island-boundary-slots/main.ts @@ -0,0 +1,60 @@ +// @dynamic +// Island ('any'-typed) values in typed builtin slots: the boundary pass +// wraps each argument in a validated island exit — strict primitives, the +// dynCheck stance — so regex tests over 'any' text, repeat/slice counts +// from 'any' numbers, and container seeds behind package declarations all +// run with Node's values. Two families in one program: intrinsic argument +// slots, and `new Set(seed)` over package-exported arrays. + +// ── intrinsic argument slots ───────────────────────────────────────────── +// A pragma-sniffing idiom: untyped helpers hand back 'any', and the +// results feed regex/string intrinsics directly. +const text: any = " @format\nrest of file"; +console.log(/^\s*@(?:format|prettier)/u.test(text)); +console.log(/^never$/.test(text)); + +// String intrinsic numeric slots from 'any' (usage-table padding). +const width: any = 3; +console.log("ab".repeat(width)); +console.log("-".repeat(width) + "|"); + +// slice with one and two 'any' indices; negative index rules unchanged. +const start: any = 2; +const end: any = 7; +console.log("indentation".slice(start)); +console.log("indentation".slice(start, end)); +console.log("indentation".slice(0 - start)); + +// The receiver side: an 'any' receiver runs the ENGINE's own method (the +// island call path), and the numeric result prints through the template. +const hay: any = "needle in haystack"; +console.log(`${hay.indexOf("in")}`); + +// Fractional and negative counts keep JS's ToInteger/range semantics +// through the exit (the exit is strict about KIND, not about integers). +const frac: any = 2.9; +console.log("xy".repeat(frac)); + +// Exits are values like any other: intrinsic results compose statically. +const n: any = 4; +const banner = "=".repeat(n) + " done " + "=".repeat(n); +console.log(banner, banner.length); + +// ── `new Set(seed)` over package-exported arrays ───────────────────────── +// The seed is an island handle behind a string[]/number[] declaration: it +// exits through the validated island copy, then bulk-adds — duplicates +// collapse and insertion order holds, exactly Node. The void-tags/ +// event-attributes package idiom. +import eventNames, { voidTags, numberedTags } from "taglists"; + +const tags = new Set(voidTags); +console.log(tags.size, tags.has("img"), tags.has("div")); + +const events = new Set(eventNames); +console.log(events.size, events.has("click"), events.has("submit")); +for (const e of events) console.log(e); + +// A CALL result (still a declared number[] handle) seeds the same way. +const nums = new Set(numberedTags()); +console.log(nums.size, nums.has(3), nums.has(9)); +console.log([...nums].join(",")); diff --git a/tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.d.ts b/tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.d.ts new file mode 100644 index 00000000..5c7ed71b --- /dev/null +++ b/tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.d.ts @@ -0,0 +1,4 @@ +export declare const voidTags: string[]; +declare const eventNames: string[]; +export default eventNames; +export declare function numberedTags(): number[]; diff --git a/tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.js b/tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.js new file mode 100644 index 00000000..03ec0efa --- /dev/null +++ b/tests/corpus/2447-island-boundary-slots/node_modules/taglists/index.js @@ -0,0 +1,5 @@ +export const voidTags = ["br", "hr", "img", "br", "input"]; +export default ["click", "focus", "click", "blur"]; +export function numberedTags() { + return [3, 1, 2, 3]; +} diff --git a/tests/corpus/2447-island-boundary-slots/node_modules/taglists/package.json b/tests/corpus/2447-island-boundary-slots/node_modules/taglists/package.json new file mode 100644 index 00000000..ddc47f76 --- /dev/null +++ b/tests/corpus/2447-island-boundary-slots/node_modules/taglists/package.json @@ -0,0 +1,5 @@ +{ + "name": "taglists", + "type": "module", + "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } } +} diff --git a/tests/corpus/2448-entries-null-arms-regex-arrays.ts b/tests/corpus/2448-entries-null-arms-regex-arrays.ts new file mode 100644 index 00000000..e1b7b01e --- /dev/null +++ b/tests/corpus/2448-entries-null-arms-regex-arrays.ts @@ -0,0 +1,98 @@ +// Three static families in one program: Object.entries/values over +// unit-armed (null | undefined) fields, RegExp values as array elements, +// and JSON.stringify over a dyn root holding undefined — all byte-exact +// against Node on both backends. +// +// ── Object.entries/values over shapes whose fields are `null | undefined` — +// the mixed-defaults spread idiom (`{ ...defaults, ...overrides }` where a +// default is null and the merged field types optional): a unit-armed field +// with null as its ONE value arm pushes the null literal, guarded by the +// undefined skip (an unset optional never made it into the object). +interface MergedDefaults { + rangeStart: number; + parser: string; + endOfLine?: null; + cursorOffset?: null; +} + +const base = { rangeStart: 0, parser: "babel" }; +const withNulls: MergedDefaults = { ...base, endOfLine: null }; + +for (const [k, v] of Object.entries(withNulls)) { + console.log(k, String(v)); +} +console.log("--"); +for (const v of Object.values(withNulls)) { + console.log(String(v)); +} + +// Both unit-armed fields present: each pushes its null. +const bothSet: MergedDefaults = { rangeStart: 3, parser: "flow", endOfLine: null, cursorOffset: null }; +console.log(Object.entries(bothSet).length, Object.values(bothSet).length); +for (const [k, v] of Object.entries(bothSet)) { + console.log(`${k}=${String(v)}`); +} + +// Neither present: the undefined guard skips both keys, exactly Node's +// missing-key answer for unset optionals. +const noneSet: MergedDefaults = { rangeStart: 7, parser: "meriyah" }; +console.log(Object.keys(noneSet).join(",")); +console.log(Object.entries(noneSet).length); + +// The defaults-merge shape over the entries — the consuming idiom: each +// [key, value] pair flows through destructuring into ordinary statics. +const seen: string[] = []; +for (const [k, v] of Object.entries(withNulls)) { + seen.push(v === null ? `${k}:` : `${k}:${String(v)}`); +} +console.log(seen.join(" ")); + +// ── RegExp values as ARRAY elements — the derived-pattern idiom: a base +// word list maps into compiled patterns (`[bases].map(ps => new +// ── RegExp(...))`), the array destructures, elements test/match like any +// regex value, and indexOf/includes/=== are object identity, exactly JS. +const PRAGMAS = ["format", "prettier"]; +const IGNORE_PRAGMAS = PRAGMAS.map((p) => `no${p}`); + +const [HAS_PRAGMA, HAS_IGNORE_PRAGMA] = [PRAGMAS, IGNORE_PRAGMAS].map( + (pragmas) => new RegExp(`^\\s*@(?:${pragmas.join("|")})\\b`), +); +console.log(HAS_PRAGMA.test(" @format now")); +console.log(HAS_PRAGMA.test(" @noformat now")); +console.log(HAS_IGNORE_PRAGMA.test(" @noprettier")); +console.log(HAS_PRAGMA.source); + +// Literal regex arrays: reads, length, for-of, and source/flags on the +// elements. +const checks: RegExp[] = [/^a+$/u, /b|c/, /end$/m]; +console.log(checks.length); +for (const re of checks) { + console.log(re.source, re.flags, re.test("aaa")); +} +console.log(checks[1].test("xbx"), checks[2].test("the end")); + +// Identity semantics: indexOf/includes compare references, like JS. +// (Two same-source LITERALS share one interned instance here where JS +// mints fresh objects — the documented interning divergence — so the +// fixture pins identity through the same reference only.) +const first = checks[0]; +console.log(checks.indexOf(first), checks.includes(first)); +console.log(checks.indexOf(HAS_PRAGMA)); // a different regex object: absent + +// push/pop keep the element home honest. +const grown: RegExp[] = []; +grown.push(/one/, /two/); +console.log(grown.length, grown[0].source); +const popped = grown.pop(); +console.log(popped === undefined ? "none" : popped.source, grown.length); + +// ── JSON.stringify over a dyn root holding undefined ───────────────────── +// JSON.stringify(undefined) is the undefined VALUE; printing it spells the +// word — both backends ride the same DOM walker (a nested-position writer +// would spell null; the root is special). +const u: unknown = undefined; +console.log(JSON.stringify(u)); +const held: unknown = { a: undefined, b: 1 }; +console.log(JSON.stringify(held)); +const nested: unknown = { list: [1, null], t: true }; +console.log(JSON.stringify(nested)); diff --git a/tests/corpus/2449-js-dyn-worlds/main.js b/tests/corpus/2449-js-dyn-worlds/main.js new file mode 100644 index 00000000..29d4b5a6 --- /dev/null +++ b/tests/corpus/2449-js-dyn-worlds/main.js @@ -0,0 +1,86 @@ +// @dynamic +// The two dynamic worlds in one JS graph, no ICEs anywhere: `this` in +// plain functions is the checked-dynamic AMBIENT receiver (DOM keyed +// reads, never an engine op over a dyn value); rest-args arrays dispatch +// their methods on the runtime receiver kind; and a graph typed by a +// runtime-fenced #private class still builds — the destructuring-param +// function compiles as its own fence body, the plain-param caller links +// against it, and every leftover class-typed slot emits valid code. +'use strict'; +import TreePath from "./walker.js"; + +// ── the ambient receiver ──────────────────────────────────────────────── +// Strict-mode plain calls leave `this` undefined, so the read throws V8's +// catchable TypeError, message-exactly. +function unbound() { + try { + return this.limit; + } catch (e) { + return "caught: " + e.message; + } +} +console.log(`${unbound()}`); + +// Chains throw at the FIRST read, like Node. +function chained() { + try { + return this.Parser.prototype; + } catch (e) { + return "chain: " + e.message; + } +} +console.log(`${chained()}`); + +// `this` itself is a value: truthiness and typeof see the undefined. +function probe() { + return (this ? "bound" : "unbound") + "/" + typeof this; +} +console.log(`${probe()}`); + +// ── rest-args are checked-dynamic arrays ──────────────────────────────── +// `.map`/`.forEach` over one dispatch on the RUNTIME receiver kind (the +// lazy-plugin-table idiom); the callback crosses as a boxed function. +function toLazy(...plugins) { + const out = plugins.map((p) => "p:" + p); + return out.join(","); +} +console.log(`${toLazy("alpha", "beta")}`); +console.log(`${toLazy()}`); +console.log(`${toLazy("solo")}`); + +function tally(...nums) { + let sum = 0; + nums.forEach((n) => { + sum += n; + }); + return sum; +} +console.log(`${tally(1, 2, 3.5)}`); + +// ── the fenced-class graph ────────────────────────────────────────────── +// None of the fenced code RUNS here — the graph loads, the healthy +// statements print, exactly Node. +/** + * @param {TreePath} path + */ +function isInWideSentence({ parent: sentenceNode }) { + return sentenceNode.usesSpaces === undefined; +} + +/** + * @param {TreePath} path + */ +function lineBreakConverts(path) { + return isInWideSentence(path); +} + +/** + * @param {TreePath} path + * @returns {string} + */ +function printTree(path) { + return String(path); +} + +console.log(typeof lineBreakConverts, typeof isInWideSentence, typeof printTree); +console.log("graph loaded"); diff --git a/tests/corpus/2449-js-dyn-worlds/tsconfig.json b/tests/corpus/2449-js-dyn-worlds/tsconfig.json new file mode 100644 index 00000000..0cd67a8d --- /dev/null +++ b/tests/corpus/2449-js-dyn-worlds/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"strict":true,"noImplicitAny":false}} diff --git a/tests/corpus/2449-js-dyn-worlds/walker.js b/tests/corpus/2449-js-dyn-worlds/walker.js new file mode 100644 index 00000000..83ff5d38 --- /dev/null +++ b/tests/corpus/2449-js-dyn-worlds/walker.js @@ -0,0 +1,13 @@ +'use strict'; +// A #private class: collection fences it (deferred to runtime in JS), so +// every type slot naming it must still emit valid code. +class TreePath { + #stack; + constructor(value) { + this.#stack = [value]; + } + get parent() { + return this.#stack.at(-2); + } +} +export default TreePath; diff --git a/tests/diagnostics/island-boundary-slots.ts b/tests/diagnostics/island-boundary-slots.ts new file mode 100644 index 00000000..baaee27c --- /dev/null +++ b/tests/diagnostics/island-boundary-slots.ts @@ -0,0 +1,9 @@ +// @dynamic +// Island ('any'-typed) values in builtin-call slots OUTSIDE the validated +// exit set: primitive and JSON-safe slots exit through the island bridge +// (corpus 2440); a slot typed as a FUNCTION has no island exit — an engine +// function cannot cross into a static callback slot — so the boundary +// pass fences with the slot named instead of handing the validator a +// jsval-typed argument. +const cb: any = () => {}; +setTimeout(cb, 1); diff --git a/tests/diagnostics/regex.ts b/tests/diagnostics/regex.ts index 9131cfcc..88ecd772 100644 --- a/tests/diagnostics/regex.ts +++ b/tests/diagnostics/regex.ts @@ -1,12 +1,11 @@ // The regex slice fences. test() on a g/y-flagged literal is the // statefulness fence (lastIndex is not modeled); named capture groups and // the d/v flags are outside the slice; method-as-value has no value form; -// regexes stay out of arrays and union arms. +// regexes stay out of union arms (ARRAYS of regexes compile — corpus 2448). const g = /ab/g.test("abab"); const y = /ab/y.test("abab"); const named = /(?\d{4})-(?\d{2})/; const indices = /cat/d; const sets = /[\p{L}]/v; const asValue = /x/.test; -const list: RegExp[] = [/a/, /b/]; const maybe: RegExp | undefined = /a/; diff --git a/tests/harness/__snapshots__/island-boundary-slots.ts.txt b/tests/harness/__snapshots__/island-boundary-slots.ts.txt new file mode 100644 index 00000000..4bef76ac --- /dev/null +++ b/tests/harness/__snapshots__/island-boundary-slots.ts.txt @@ -0,0 +1,8 @@ +island-boundary-slots.ts:9:12 - error SC1100: passing 'any'-typed values where '() => void' is expected (argument 1 of timers.setTimeout) is not supported yet + + 8 | const cb: any = () => {}; + 9 | setTimeout(cb, 1); + | ^~ + 10 | + + hint: validate with 'as ' first — the cast checks the dynamic value at runtime and throws on mismatch \ No newline at end of file diff --git a/tests/harness/__snapshots__/regex.ts.txt b/tests/harness/__snapshots__/regex.ts.txt index 2465c0f9..b0182c08 100644 --- a/tests/harness/__snapshots__/regex.ts.txt +++ b/tests/harness/__snapshots__/regex.ts.txt @@ -1,6 +1,6 @@ regex.ts:5:11 - error SC1121: '.test()' on a regex with the 'g' or 'y' flag is not supported yet - 4 | // regexes stay out of arrays and union arms. + 4 | // regexes stay out of union arms (ARRAYS of regexes compile — corpus 2448). 5 | const g = /ab/g.test("abab"); | ^~~~~~~~~~~~~~~~~~ 6 | const y = /ab/y.test("abab"); @@ -48,25 +48,11 @@ regex.ts:10:17 - error SC1090: regex methods as values (call 'test' directly) ar 9 | const sets = /[\p{L}]/v; 10 | const asValue = /x/.test; | ^~~~~~~~ - 11 | const list: RegExp[] = [/a/, /b/]; + 11 | const maybe: RegExp | undefined = /a/; -regex.ts:11:7 - error SC2001: values of type 'RegExp[]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') +regex.ts:11:7 - error SC2001: values of type 'RegExp | undefined' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') 10 | const asValue = /x/.test; - 11 | const list: RegExp[] = [/a/, /b/]; - | ^~~~ - 12 | const maybe: RegExp | undefined = /a/; - -regex.ts:11:24 - error SC2001: values of type 'RegExp[]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - - 10 | const asValue = /x/.test; - 11 | const list: RegExp[] = [/a/, /b/]; - | ^~~~~~~~~~ - 12 | const maybe: RegExp | undefined = /a/; - -regex.ts:12:7 - error SC2001: values of type 'RegExp | undefined' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - - 11 | const list: RegExp[] = [/a/, /b/]; - 12 | const maybe: RegExp | undefined = /a/; + 11 | const maybe: RegExp | undefined = /a/; | ^~~~~ - 13 | \ No newline at end of file + 12 | \ No newline at end of file diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 04b2358b..52bc564d 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -61,10 +61,14 @@ async function compileAndRun( for (const [extraName, text] of Object.entries(extraFiles)) { writeFileSync(join(outDir, extraName), text); } + // `// @dynamic` on the entry's FIRST line embeds the island engine — + // for runtime-fence shapes only mixed dynamic graphs can spell (the + // diagnostics suite's directive, applied to the run-and-observe lane). + const dynamic = /^\/\/ @dynamic\s*$/.test(source.split("\n", 1)[0] ?? ""); // Pinned: the uncaught-STDERR line shape (SEMANTICS.md divergence 11) is // pinned against the C reference; the LLVM lane's uncaught epilogue is // llvm-differential's parity job, not this suite's. - const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize, backend: "c" }); + const result = await compile(file, { outPath: join(outDir, name), outDir, sanitize, backend: "c", dynamic }); if (!result.ok) { throw new Error( "errors program failed to compile:\n" + @@ -355,3 +359,34 @@ console.log('never runs', a); ); }); }); + +describe("checked-dynamic/island boundary fences (scriptc-only)", () => { + test("an island-typed argument into a call through 'unknown' fences catchably", async () => { + // `this` in a plain JS function is the checked-dynamic ambient + // receiver; a member CALL through it with an 'any'-typed argument has + // no lowering (no jsval→DOM bridge exists), so the statement compiles + // to its runtime fence — an SC-coded catchable throw, never an ICE. + // Node would throw its own TypeError reading the member; the fence's + // wording is the divergence this pin owns. + const r = await compileAndRun( + "island-arg-dyn-call", + `// @dynamic +'use strict'; +function register(item) { + try { + this.registry(item); + return "unreachable"; + } catch (e) { + return "caught: " + e.message; + } +} +console.log(\`\${register(7)}\`); +`, + "mjs", + { "tsconfig.json": '{"compilerOptions":{"strict":true,"noImplicitAny":false}}\n' }, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toMatch(/^caught: .*\[SC1101 at .*island-arg-dyn-call\.mjs:5\]\n$/); + expect(r.stderr).toBe(""); + }); +});