mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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(","));
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export declare const voidTags: string[];
|
||||
declare const eventNames: string[];
|
||||
export default eventNames;
|
||||
export declare function numberedTags(): number[];
|
||||
+5
@@ -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];
|
||||
}
|
||||
+5
@@ -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));
|
||||
@@ -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}}
|
||||
@@ -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,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
|
||||
@@ -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 |
|
||||
@@ -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("");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user