Zero the whole-program validator's SC9001 families: the big JS graph builds

- 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)
This commit is contained in:
Chris Tate
2026-07-23 03:23:24 -05:00
parent db6f0eb2f2
commit 2b2dd83949
24 changed files with 648 additions and 93 deletions
@@ -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
@@ -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
@@ -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)`);
@@ -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<IrExpr, { kind: "arrIntrinsic" }>;
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<IrExpr, { kind: "callValue" }>;
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<IrExpr, { kind: "jsOp" }>;
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 <type>' 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<IrExpr, { kind: "dynCall" | "dynInvoke" }>;
if (kind === "dynInvoke") {
const inv = e as Extract<IrExpr, { kind: "dynInvoke" }>;
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 <type>' 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;
}
}
@@ -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(
@@ -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.
}
}
}
@@ -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<string, { method: IrBytesIntrinsicMethod; le: boolean }
const otherTag = L.armTag(valueT.unionId, others[0]!);
const narrowTag = L.armTag(f.type.unionId, others[0]!);
if (otherTag >= 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 };
}
}
@@ -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):
@@ -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<string, unknown>;
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
+6 -1
View File
@@ -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" ||
@@ -4303,6 +4303,25 @@
],
"diags": []
},
"<repo>/tests/corpus/2447-island-boundary-slots/main.ts": {
"order": [
"<repo>/tests/corpus/2447-island-boundary-slots/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/2448-entries-null-arms-regex-arrays.ts": {
"order": [
"<repo>/tests/corpus/2448-entries-null-arms-regex-arrays.ts"
],
"diags": []
},
"<repo>/tests/corpus/2449-js-dyn-worlds/main.js": {
"order": [
"<repo>/tests/corpus/2449-js-dyn-worlds/walker.js",
"<repo>/tests/corpus/2449-js-dyn-worlds/main.js"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
@@ -5807,6 +5826,12 @@
],
"diags": []
},
"<repo>/tests/diagnostics/island-boundary-slots.ts": {
"order": [
"<repo>/tests/diagnostics/island-boundary-slots.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/island.ts": {
"order": [
"<repo>/tests/diagnostics/island.ts"
@@ -6143,48 +6168,6 @@
],
"diags": []
},
"<repo>/tests/fixtures/node-types/argv-env.ts": {
"order": [
"<repo>/tests/fixtures/node-types/argv-env.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/fenced.ts": {
"order": [
"<repo>/tests/fixtures/node-types/fenced.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/import-fences.ts": {
"order": [
"<repo>/tests/fixtures/node-types/import-fences.ts"
],
"diags": [
{
"code": "SC1010",
"message": "the 'v8' module is not supported yet",
"loc": {
"file": "<repo>/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"
}
]
},
"<repo>/tests/fixtures/node-types/path-os.ts": {
"order": [
"<repo>/tests/fixtures/node-types/path-os.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/stream-capture.ts": {
"order": [
"<repo>/tests/fixtures/node-types/stream-capture.ts"
],
"diags": []
},
"<repo>/tests/fixtures/npm/cases/abandoned-handles/main.ts": {
"order": [
"<repo>/tests/fixtures/npm/cases/abandoned-handles/main.ts"
@@ -6536,6 +6519,48 @@
"hint": "set \"strictNullChecks\": true (or \"strict\": true) in the project tsconfig"
}
]
},
"<repo>/tests/fixtures/node-types/argv-env.ts": {
"order": [
"<repo>/tests/fixtures/node-types/argv-env.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/fenced.ts": {
"order": [
"<repo>/tests/fixtures/node-types/fenced.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/import-fences.ts": {
"order": [
"<repo>/tests/fixtures/node-types/import-fences.ts"
],
"diags": [
{
"code": "SC1010",
"message": "the 'v8' module is not supported yet",
"loc": {
"file": "<repo>/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"
}
]
},
"<repo>/tests/fixtures/node-types/path-os.ts": {
"order": [
"<repo>/tests/fixtures/node-types/path-os.ts"
],
"diags": []
},
"<repo>/tests/fixtures/node-types/stream-capture.ts": {
"order": [
"<repo>/tests/fixtures/node-types/stream-capture.ts"
],
"diags": []
}
}
}
@@ -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(","));
@@ -0,0 +1,4 @@
export declare const voidTags: string[];
declare const eventNames: string[];
export default eventNames;
export declare function numberedTags(): number[];
@@ -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];
}
@@ -0,0 +1,5 @@
{
"name": "taglists",
"type": "module",
"exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } }
}
@@ -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}:<null>` : `${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));
+86
View File
@@ -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");
@@ -0,0 +1 @@
{"compilerOptions":{"strict":true,"noImplicitAny":false}}
+13
View File
@@ -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;
@@ -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);
+1 -2
View File
@@ -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 = /(?<year>\d{4})-(?<month>\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/;
@@ -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 <type>' first — the cast checks the dynamic value at runtime and throws on mismatch
+5 -19
View File
@@ -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 |
12 |
+36 -1
View File
@@ -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("");
});
});