Expand static Test262 execution and exception behavior (#486)

* Expand static Test262 harness and exception semantics

* Ignore ASan fiber warning in dev source test

* Refresh TS7 parity baselines after main rebase
This commit is contained in:
Chris Tate
2026-09-27 09:21:29 -05:00
committed by GitHub
parent b2c160cb2a
commit 3d04a561ef
32 changed files with 737 additions and 217 deletions
+19 -1
View File
@@ -3266,6 +3266,10 @@ function emitDynamicExpr(
const test = `scr_caught_instanceof(${c.name}, ${target.pre}, ${target.post})`;
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
}
if (e.test === "object") {
const test = `scr_caught_is_object(${c.name})`;
return emitter.newTemp(e.type, e.negated ? `!${test}` : test);
}
const tag = { string: "SCR_EXC_STR", number: "SCR_EXC_F64", boolean: "SCR_EXC_BOOL" }[e.test];
return emitter.newTemp(e.type, `${c.name}->kind ${e.negated ? "!=" : "=="} ${tag}`);
}
@@ -3383,7 +3387,7 @@ function emitIntrinsicExpr(
// bindings and the unhandled dispatch see the dyn value
// itself — identity preserved.
emitter.line(
`scr_throw_ref(${reason.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`,
`scr_throw_ref_classified(${reason.name}, &${rc.retain}, &${rc.release}, NULL, scr_dyn_is_object(${reason.name}));${emitter.srcComment(e.loc)}`,
);
} else {
emitter.line(
@@ -3643,6 +3647,13 @@ function emitAsyncExpr(
} else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) {
const rc = vAdapters(t);
emitter.line(`scr_throw_obj(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
const rc = vAdapters(t);
emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
const rc = vAdapters(t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`);
} else {
const rc = vAdapters(t);
emitter.line(`scr_throw_ref(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
@@ -3699,6 +3710,13 @@ function emitAsyncExpr(
} else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) {
const rc = vAdapters(t);
emitter.line(`scr_throw_obj(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
const rc = vAdapters(t);
emitter.line(`scr_throw_primitive_ref(${a.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(e.loc)}`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
const rc = vAdapters(t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
emitter.line(`scr_throw_ref_classified(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${a.name}));${emitter.srcComment(e.loc)}`);
} else {
const rc = vAdapters(t);
emitter.line(`scr_throw_ref(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`);
+7
View File
@@ -721,6 +721,13 @@ function emitStmtBody(emitter: CEmitter, s: IrStmt): void {
// uncaught printer's "name: message" for Error instances).
const rc = vAdapters(t);
emitter.line(`scr_throw_obj(${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(s.loc)}`);
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
const rc = vAdapters(t);
emitter.line(`scr_throw_primitive_ref(${v.name}, &${rc.retain}, &${rc.release}, NULL);${emitter.srcComment(s.loc)}`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
const rc = vAdapters(t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
emitter.line(`scr_throw_ref_classified(${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)}, ${test}(${v.name}));${emitter.srcComment(s.loc)}`);
} else {
const rc = vAdapters(t);
emitter.line(`scr_throw_ref(${v.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(s.loc)}`);
+2 -2
View File
@@ -378,6 +378,7 @@ import { OVERFLOW_MEMBER } from "./shapes.js";
` }`,
` /* FALLTHROUGH: a non-Error hierarchy object is type-erased */`,
` case SCR_EXC_REF:`,
` case SCR_EXC_PRIMITIVE_REF:`,
` /* A thrown dyn value passes back BY REFERENCE (identity with`,
` * every other holder of the node — the traced-throw shape). */`,
` if (c->retain_fn == scr_dyn_retain_v) return scr_dyn_retain((ScrDyn *)c->payload);`,
@@ -1013,8 +1014,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` return sc_out;`);
d.push(` }`);
d.push(` if (d->kind == SCR_DYN_OBJ) {`);
d.push(` ScrDyn *m = scr_dyn_obj_get(d, k->data, k->len);`);
d.push(` return scr_dyn_retain(m ? m : scr_dyn_undefined());`);
d.push(` return scr_dyn_obj_read(d, k->data, k->len);`);
d.push(` }`);
d.push(` if (d->kind == SCR_DYN_JSVAL) {`);
d.push(` /* Island-held: o[k] reads the REAL engine property (getters`);
+4 -10
View File
@@ -2256,7 +2256,7 @@ export class LlDyn {
B.terminate(`ret ptr ${r}`);
B.startBlock(lNext);
}
// OBJ: the own member (+1) or the undefined singleton.
// OBJ: the own member or the inherited builtin Error constructor.
{
const isObj = B.tmp();
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
@@ -2264,15 +2264,9 @@ export class LlDyn {
const lNext = B.newLabel("kg.n");
B.condBr(isObj, lObj, lNext);
B.startBlock(lObj);
host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${host.sizeType})`);
const m = B.tmp();
B.line(`${m} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${kParts.data}, ${host.sizeType} ${kParts.len})`);
const has = B.tmp();
B.line(`${has} = icmp ne ptr ${m}, null`);
const u = this.undef(B);
const sel = B.tmp();
B.line(`${sel} = select i1 ${has}, ptr ${m}, ptr ${u}`);
const r = this.retainDyn(B, sel);
host.declare(`declare ptr @scr_dyn_obj_read(ptr, ptr, ${host.sizeType})`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_obj_read(ptr %d, ptr ${kParts.data}, ${host.sizeType} ${kParts.len})`);
B.terminate(`ret ptr ${r}`);
B.startBlock(lNext);
}
@@ -2449,6 +2449,18 @@ class LlEmitter {
const rc = vAdapters(this, t);
this.declare(`declare void @scr_throw_obj(ptr, ptr, ptr, ptr)`);
B.line(`call void @scr_throw_obj(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this, t)})`);
} else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") {
const rc = vAdapters(this, t);
this.declare(`declare void @scr_throw_primitive_ref(ptr, ptr, ptr, ptr)`);
B.line(`call void @scr_throw_primitive_ref(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr null)`);
} else if (t.kind === "dyn" || t.kind === "jsval") {
const rc = vAdapters(this, t);
const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object";
this.declare(`declare zeroext i1 @${test}(ptr)`);
this.declare(`declare void @scr_throw_ref_classified(ptr, ptr, ptr, ptr, i1 zeroext)`);
const object = B.tmp();
B.line(`${object} = call zeroext i1 @${test}(ptr ${v.name})`);
B.line(`call void @scr_throw_ref_classified(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this, t)}, i1 zeroext ${object})`);
} else {
const rc = vAdapters(this, t);
this.declare(`declare void @scr_throw_ref(ptr, ptr, ptr, ptr)`);
@@ -736,6 +736,15 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
B.line(`${n} = xor i1 ${t}, true`);
return { name: n, type: e.type };
}
if (e.test === "object") {
host.declare(`declare zeroext i1 @scr_caught_is_object(ptr)`);
const t = B.tmp();
B.line(`${t} = call zeroext i1 @scr_caught_is_object(ptr ${c.name})`);
if (e.negated !== true) return { name: t, type: e.type };
const n = B.tmp();
B.line(`${n} = xor i1 ${t}, true`);
return { name: n, type: e.type };
}
const tag = { string: 3, number: 1, boolean: 2 }[e.test];
const kp = B.tmp();
const k = B.tmp();
@@ -7065,9 +7065,8 @@ function loweredTemplateStrings(
* becomes the catch binding, and a handler VALUE would need a
* caught-typed closure parameter, which cannot exist. finally takes
* any () => void closure (its callback sees no arguments). then takes
* exactly one FULFILLMENT handler (any closure value of the settled
* value's type — the two-argument onRejected form stays fenced toward
* .catch); a promise-returning handler flattens through the async
* a fulfillment handler and optionally a rejection handler; a
* promise-returning handler flattens through the async
* return path, a receiver rejection passes through untouched (the
* wrapper's await re-throws it), and a handler throw rejects the
* result — the spec's onFulfilled rules by construction. Null for
@@ -7158,12 +7157,12 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const passthrough =
call.arguments.length === 0 ||
(call.arguments.length === 1 && isAbsentHandler(call.arguments[0]));
if (call.arguments.length !== 1 && !passthrough) {
if (call.arguments.length !== 1 && !passthrough && !(member === "then" && call.arguments.length === 2)) {
lowerer.noLowering(
`${member} with ${call.arguments.length} arguments`,
call,
member === "then"
? "the supported form takes exactly one fulfillment handler — chain .catch(...) for the rejection half"
? "the supported form takes a fulfillment handler and an optional rejection handler"
: `the supported form takes exactly one ${member === "catch" ? "inline handler" : "callback"}`,
);
}
@@ -7238,6 +7237,132 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
}
if (member === "then") {
if (call.arguments.length === 2) {
const fulfilledNode = call.arguments[0]!;
const rejectedNode = call.arguments[1]!;
const fulfilledAbsent = isAbsentHandler(fulfilledNode);
const rejectedAbsent = isAbsentHandler(rejectedNode);
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (resultT?.kind !== "promise") {
lowerer.noLowering("then with these handlers", call, "the result must be a representable promise");
}
const R = resultT.inner;
if (!fulfilledAbsent && inner.kind === "jsval") markJsvalHandlerParams(lowerer, fulfilledNode);
if (!fulfilledAbsent && inner.kind === "moduleNs") markModuleNsHandlerParams(lowerer, fulfilledNode, inner);
const boxHandler = (node: ts.Expression, allowDirect: boolean): { value: IrExpr; direct: boolean; returnsPromise: boolean } => {
const value = lowerer.lowerExpr(node);
if (allowDirect && value.type.kind === "func" && value.type.params.length <= 1 &&
(value.type.params.length === 0 || (inner.kind !== "void" && typeEquals(value.type.params[0]!, inner)))) {
return { value, direct: true, returnsPromise: value.type.ret.kind === "promise" };
}
if (value.type.kind === "dyn") return { value, direct: false, returnsPromise: false };
if (value.type.kind === "func" && value.type.params.length <= 1 &&
canBoxFuncIntoDyn(value.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
return { value: { kind: "dynFrom", value, type: DYN, loc }, direct: false, returnsPromise: value.type.ret.kind === "promise" };
}
lowerer.unsupported("SC1090", node, "then handlers that cannot be called with one value");
};
// Evaluate both callback expressions before the wrapper runs, in source order.
const fulfilled = fulfilledAbsent ? null : boxHandler(fulfilledNode, true);
const rejected = rejectedAbsent ? null : boxHandler(rejectedNode, false);
const fnName = `%fn${lowerer.lambdaCounter++}_then2`;
const paramTypes: IrType[] = [promT, ...(fulfilled ? [fulfilled.value.type] : []), ...(rejected ? [DYN] : [])];
const funcType: IrType & { kind: "func" } = { kind: "func", params: paramTypes, ret: resultT };
const fnCtx = newFnCtx(true, null, funcType, R);
fnCtx.isAsync = true;
lowerer.fnStack.push(fnCtx);
try {
const pLocal = lowerer.declareHiddenLocal("p", promT);
const fLocal = fulfilled ? lowerer.declareHiddenLocal("f", fulfilled.value.type) : null;
const rLocal = rejected ? lowerer.declareHiddenLocal("r", DYN) : null;
const eLocal = lowerer.declareHiddenLocal("e", CAUGHT);
const vLocal = inner.kind === "void" ? null : lowerer.declareHiddenLocal("v", inner);
const awaitE: IrExpr = {
kind: "awaitExpr",
value: { kind: "varRef", localId: pLocal.id, type: promT, loc },
type: inner,
loc,
};
const tryBody: IrStmt[] = vLocal
? [{ kind: "varDecl", localId: vLocal.id, init: awaitE, loc }]
: [{ kind: "exprStmt", expr: awaitE, loc }];
const resultOf = (value: IrExpr, returnsPromise = false): IrStmt[] => {
const settled: IrExpr = value.type.kind === "promise"
? { kind: "awaitExpr", value, type: value.type.inner, loc }
: returnsPromise
? { kind: "libCall", fn: "async.awaitDyn", args: [value], type: DYN, loc }
: value;
return R.kind === "void"
? [{ kind: "exprStmt", expr: settled, loc }, { kind: "return", value: null, loc }]
: [{ kind: "return", value: lowerer.coerceInto(call, settled, R), loc }];
};
const rejectedCall: IrExpr | null = rLocal ? {
kind: "dynCall",
callee: { kind: "varRef", localId: rLocal.id, type: DYN, loc },
calleeName: jsFuncNameOf(rejectedNode) ?? "onRejected",
args: [{ kind: "caughtToDyn", value: { kind: "varRef", localId: eLocal.id, type: CAUGHT, loc }, type: DYN, loc }],
type: DYN,
loc,
} : null;
const catchBody: IrStmt[] = rejectedCall
? resultOf(rejectedCall, rejected?.returnsPromise)
: [{ kind: "rethrow", localId: eLocal.id, loc }];
const body: IrStmt[] = [{ kind: "tryCatch", tryBody, catchBody, catchLocalId: eLocal.id, finallyBody: null, loc }];
// The fulfillment callback sits after the catch so a throw from it
// rejects the result instead of reaching onRejected.
if (fLocal) {
const settled: IrExpr = vLocal
? { kind: "varRef", localId: vLocal.id, type: inner, loc }
: dynUndefinedExpr(loc);
if (fulfilled!.direct && fLocal.type.kind === "func") {
body.push(...resultOf({
kind: "callValue",
callee: { kind: "varRef", localId: fLocal.id, type: fLocal.type, loc },
args: fLocal.type.params.length === 0 ? [] : [settled],
type: fLocal.type.ret,
loc,
}));
} else {
const arg: IrExpr = settled.type.kind === "dyn" ? settled
: canConvertToDyn(settled.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))
? { kind: "dynFrom", value: settled, type: DYN, loc }
: lowerer.unsupported("SC1090", fulfilledNode, "then fulfillment values that cannot cross the checked-dynamic boundary");
body.push(...resultOf({
kind: "dynCall",
callee: { kind: "varRef", localId: fLocal.id, type: DYN, loc },
calleeName: jsFuncNameOf(fulfilledNode) ?? "onFulfilled",
args: [arg], type: DYN, loc,
}, fulfilled?.returnsPromise));
}
} else if (vLocal) {
body.push(...resultOf({ kind: "varRef", localId: vLocal.id, type: inner, loc }));
} else if (R.kind === "void") {
body.push({ kind: "return", value: null, loc });
} else {
body.push({ kind: "return", value: lowerer.coerceInto(call, dynUndefinedExpr(loc), R), loc });
}
const ctx = lowerer.ctx;
const lifted: IrFunction = {
name: fnName,
params: [
{ localId: pLocal.id, name: pLocal.name, type: promT },
...(fLocal ? [{ localId: fLocal.id, name: fLocal.name, type: fLocal.type }] : []),
...(rLocal ? [{ localId: rLocal.id, name: rLocal.name, type: DYN }] : []),
],
returnType: R,
locals: ctx.locals,
captures: ctx.captures!,
body,
loc,
async: true,
};
lowerer.liftedFns.push(lifted);
const closure: IrExpr = { kind: "closure", fnName, captures: ctx.captureSources, type: funcType, loc };
return { kind: "callValue", callee: closure, args: [receiver, ...(fulfilled ? [fulfilled.value] : []), ...(rejected ? [rejected.value] : [])], type: resultT, loc };
} finally {
lowerer.fnStack.pop();
}
}
// The settled value is an island HANDLE: an inline handler's
// unannotated parameter binds it as jsval, whatever the checker's
// contextual type spelled (a module-namespace type has no mapping —
@@ -7104,7 +7104,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
"'typeof' catch-binding tests against non-literal strings",
);
}
if (b.text === "string" || b.text === "number" || b.text === "boolean") {
if (b.text === "string" || b.text === "number" || b.text === "boolean" || b.text === "object") {
return {
kind: "caughtTest",
value: { kind: "varRef", localId: local.id, type: CAUGHT, loc },
@@ -7117,8 +7117,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
lowerer.unsupported(
"SC1090",
b,
`'typeof' catch-binding tests against "${b.text}" (only "string"/"number"/"boolean" ` +
`narrow a catch binding; use 'instanceof' for objects)`,
`'typeof' catch-binding tests against "${b.text}" (only "string"/"number"/"boolean"/"object" are supported)`,
);
}
return null;
+6 -3
View File
@@ -5359,14 +5359,15 @@ export type IrExpr =
| { kind: "unionFuncEq"; unionId: string; tag: number; union: IrExpr; func: IrExpr; negated: boolean; type: IrType; loc: SrcLoc }
/** Runtime test on a catch binding (`value` is a caught-typed varRef,
* borrowed). The primitive tests ("string"/"number"/"boolean") compare
* the snapshot's kind tag — exactly what `typeof e === "..."` observes;
* the snapshot's kind tag. "object" also checks reference payloads,
* excluding callable and primitive references;
* "instanceof" requires `className` (a hierarchy class) and tests an OBJ
* payload's vtable preorder against its interval (false for every other
* payload kind). `negated` flips the result (the `!==` spelling). */
| {
kind: "caughtTest";
value: IrExpr;
test: "string" | "number" | "boolean" | "instanceof";
test: "string" | "number" | "boolean" | "object" | "instanceof";
className?: string;
negated?: boolean;
type: IrType;
@@ -6694,7 +6695,9 @@ export function moduleUsesDynAsync(mod: IrModule): boolean {
// A DYN-typed await reads through scr_await_dyn (the checked-dynamic tree-crossing
// await lives in the gated TU) — promise<dyn> receivers' awaits and
// the lifted then/catch helpers alike.
if (node.kind === "awaitExpr" && node.type !== undefined && node.type.kind === "dyn") {
const boxed = (node as { value?: { type?: { kind?: unknown; ret?: { kind?: unknown } } } }).value?.type;
if ((node.kind === "awaitExpr" && node.type?.kind === "dyn") ||
(node.kind === "dynFrom" && (boxed?.kind === "promise" || (boxed?.kind === "func" && boxed.ret?.kind === "promise")))) {
found = true;
return;
}
@@ -105,7 +105,10 @@ test.each([
if (!built.ok) continue;
const native = await exec(outPath);
expect(native.stdout).toBe(node.stdout);
expect(native.stderr).toBe(node.stderr);
const stderr = sanitize
? native.stderr.replace(/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, "")
: native.stderr;
expect(stderr).toBe(node.stderr);
}
} finally {
await rm(dir, { recursive: true, force: true });
@@ -6925,6 +6925,18 @@
],
"diags": []
},
"<repo>/tests/corpus/3001-caught-typeof-object.js": {
"order": [
"<repo>/tests/corpus/3001-caught-typeof-object.js"
],
"diags": []
},
"<repo>/tests/corpus/3002-promise-then-two.ts": {
"order": [
"<repo>/tests/corpus/3002-promise-then-two.ts"
],
"diags": []
},
"<repo>/tests/corpus/301-while.ts": {
"order": [
"<repo>/tests/corpus/301-while.ts"
@@ -7201,6 +7213,18 @@
],
"diags": []
},
"<repo>/tests/corpus/3060-dynamic-host-global.ts": {
"order": [
"<repo>/tests/corpus/3060-dynamic-host-global.ts"
],
"diags": []
},
"<repo>/tests/corpus/3061-concurrent-fiber-stacks.ts": {
"order": [
"<repo>/tests/corpus/3061-concurrent-fiber-stacks.ts"
],
"diags": []
},
"<repo>/tests/corpus/3062-runtime-optional-record-guards.ts": {
"order": [
"<repo>/tests/corpus/3062-runtime-optional-record-guards.ts"
@@ -7246,6 +7270,13 @@
],
"diags": []
},
"<repo>/tests/corpus/3068-debug-variables/main.ts": {
"order": [
"<repo>/tests/corpus/3068-debug-variables/other.ts",
"<repo>/tests/corpus/3068-debug-variables/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/400-fib.ts": {
"order": [
"<repo>/tests/corpus/400-fib.ts"
+11 -5
View File
@@ -230,7 +230,8 @@ static void scr_promise_trace(void *o, ScrTraceVisit visit, void *ctx) {
/* Waiters are fibers (not refcounted objects) — the settled payload and
* the combinator destinations are the strong references this promise
* owns (destination promises are cycle-headered). */
if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ) &&
if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
p->trace_fn) {
visit(p->payload, ctx);
}
@@ -244,7 +245,8 @@ static void scr_promise_gcfree(void *o) {
ScrPromise *p = (ScrPromise *)o;
if (p->payload_kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)p->payload);
} else if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ) &&
} else if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
p->payload && !p->trace_fn) {
p->release_fn(p->payload);
}
@@ -313,7 +315,8 @@ ScrPromise *scr_promise_retain(ScrPromise *p) {
static void scr_promise_release_payload(ScrPromise *p) {
if (p->payload_kind == SCR_EXC_STR) scr_str_release((ScrStr *)p->payload);
else if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ) &&
else if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
p->payload) p->release_fn(p->payload);
p->payload_kind = SCR_EXC_NONE;
p->payload = NULL;
@@ -1011,7 +1014,8 @@ static void scr_promise_settle_from(ScrPromise *dst, ScrPromise *src) {
dst->payload = NULL;
if (src->payload_kind == SCR_EXC_STR && src->payload) {
dst->payload = scr_str_retain((ScrStr *)src->payload);
} else if ((src->payload_kind == SCR_EXC_REF || src->payload_kind == SCR_EXC_OBJ) &&
} else if ((src->payload_kind == SCR_EXC_REF || src->payload_kind == SCR_EXC_OBJ ||
src->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
src->payload) {
dst->payload = src->retain_fn(src->payload);
}
@@ -1488,6 +1492,7 @@ static void scr_promise_rethrow(ScrPromise *p) {
case SCR_EXC_BOOL: scr_throw_bool(p->b); break;
case SCR_EXC_STR: scr_throw_str(scr_str_retain((ScrStr *)p->payload)); break;
case SCR_EXC_REF: scr_throw_ref(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
case SCR_EXC_PRIMITIVE_REF: scr_throw_primitive_ref(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
case SCR_EXC_OBJ: scr_throw_obj(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
case SCR_EXC_NONE:
case SCR_EXC_GENRET: /* unreachable: the sentinel never settles a promise */
@@ -3239,7 +3244,8 @@ static void scr_gen_slot_reset(ScrGenSlot *s) {
static void scr_gen_exc_reset(ScrExcCell *cell) {
if (cell->kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)cell->payload);
} else if (cell->kind == SCR_EXC_REF || cell->kind == SCR_EXC_OBJ) {
} else if (cell->kind == SCR_EXC_REF || cell->kind == SCR_EXC_OBJ ||
cell->kind == SCR_EXC_PRIMITIVE_REF) {
cell->release_fn(cell->payload);
}
memset(cell, 0, sizeof *cell);
+3 -1
View File
@@ -616,6 +616,7 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) {
case SCR_EXC_BOOL: return scr_dyn_new_bool(c->b);
case SCR_EXC_STR: return scr_dyn_new_str((ScrStr *)c->payload);
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF:
if (c->retain_fn == scr_dyn_retain_v) return scr_dyn_retain((ScrDyn *)c->payload);
return scr_dyn_new_obj();
case SCR_EXC_OBJ:
@@ -665,7 +666,8 @@ ScrDyn *scr_promise_reason_dyn(const ScrPromise *p) {
scr_str_release(v);
return d;
}
case SCR_EXC_REF: {
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF: {
void *v = scr_promise_payload_ref((ScrPromise *)p);
if (v == NULL) return scr_dyn_new_obj();
if (scr_promise_payload_is_dyn(p)) return (ScrDyn *)v; /* retained */
+27 -3
View File
@@ -75,7 +75,8 @@ bool scr_exc_pending(void) { return scr_exc_kind != SCR_EXC_NONE; }
static void scr_exc_reset(void) {
if (scr_exc_kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)scr_exc_payload);
} else if (scr_exc_kind == SCR_EXC_REF || scr_exc_kind == SCR_EXC_OBJ) {
} else if (scr_exc_kind == SCR_EXC_REF || scr_exc_kind == SCR_EXC_OBJ ||
scr_exc_kind == SCR_EXC_PRIMITIVE_REF) {
scr_exc_release_fn(scr_exc_payload);
}
scr_exc_kind = SCR_EXC_NONE;
@@ -121,7 +122,8 @@ void scr_caught_release(ScrCaught *c) {
if (--c->rc == 0) {
if (c->kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)c->payload);
} else if (c->kind == SCR_EXC_REF || c->kind == SCR_EXC_OBJ) {
} else if (c->kind == SCR_EXC_REF || c->kind == SCR_EXC_OBJ ||
c->kind == SCR_EXC_PRIMITIVE_REF) {
c->release_fn(c->payload);
}
scr_obj_free_note();
@@ -137,6 +139,9 @@ void scr_rethrow(const ScrCaught *c) {
case SCR_EXC_REF:
scr_throw_ref(c->retain_fn(c->payload), c->retain_fn, c->release_fn, c->trace_fn);
break;
case SCR_EXC_PRIMITIVE_REF:
scr_throw_primitive_ref(c->retain_fn(c->payload), c->retain_fn, c->release_fn, c->trace_fn);
break;
case SCR_EXC_OBJ:
scr_throw_obj(c->retain_fn(c->payload), c->retain_fn, c->release_fn, c->trace_fn);
break;
@@ -183,6 +188,10 @@ bool scr_caught_instanceof(const ScrCaught *c, size_t pre, size_t post) {
return pre <= vt->pre && vt->pre <= post;
}
bool scr_caught_is_object(const ScrCaught *c) {
return c->kind == SCR_EXC_OBJ || c->kind == SCR_EXC_REF;
}
ScrStr *scr_caught_to_string(const ScrCaught *c) {
switch (c->kind) {
case SCR_EXC_F64: return scr_f64_to_scrstr(c->f64);
@@ -198,6 +207,7 @@ ScrStr *scr_caught_to_string(const ScrCaught *c) {
}
/* fall through */
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF:
/* Object.prototype.toString — exact for thrown records and class
* instances without a toString override; thrown arrays/closures/union
* boxes print this too where Node would vary (SEMANTICS.md). */
@@ -275,7 +285,8 @@ void scr_library_check_exc(void) {
break;
}
/* fall through: non-Error hierarchy objects render like other refs */
case SCR_EXC_REF: {
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF: {
const char obj[] = "[object]";
memcpy(buf + n, obj, sizeof obj - 1);
n += sizeof obj - 1;
@@ -324,6 +335,18 @@ void scr_throw_ref(void *v, void *(*retain)(void *), void (*release)(void *),
scr_exc_trace_fn = trace;
}
void scr_throw_primitive_ref(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace) {
scr_throw_ref(v, retain, release, trace);
scr_exc_kind = SCR_EXC_PRIMITIVE_REF;
}
void scr_throw_ref_classified(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace, bool object) {
scr_throw_ref(v, retain, release, trace);
if (!object) scr_exc_kind = SCR_EXC_PRIMITIVE_REF;
}
void scr_throw_obj(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace) {
scr_throw_ref(v, retain, release, trace);
@@ -372,6 +395,7 @@ void scr_exc_print_uncaught(void) {
}
/* fall through: non-Error hierarchy objects render like other refs */
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF:
fputs("[object]", stderr);
break;
case SCR_EXC_NONE: /* main only calls this when pending */
+3 -2
View File
@@ -376,8 +376,9 @@ static void sf_throw_dyn_reason(ScrDyn *reason) {
scr_throw_str(scr_str_retain(reason->v.str));
return;
default:
scr_throw_ref(scr_dyn_retain(reason ? reason : scr_dyn_undefined()),
&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
scr_throw_ref_classified(scr_dyn_retain(reason ? reason : scr_dyn_undefined()),
&scr_dyn_retain_v, &scr_dyn_release_v, NULL,
scr_dyn_is_object(reason ? reason : scr_dyn_undefined()));
return;
}
}
+7
View File
@@ -1471,6 +1471,13 @@ ScrStr *scr_jsval_typeof(ScrJsval *a) {
return s;
}
bool scr_jsval_is_object(ScrJsval *a) {
ScrStr *type = scr_jsval_typeof(a);
bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0;
scr_str_release(type);
return object;
}
ScrStr *scr_jsval_to_str(ScrJsval *a) {
isl_entry();
return isl_js_to_str(a->v); /* NULL = bridged (e.g. a symbol) */
+31
View File
@@ -1387,6 +1387,13 @@ ScrStr *scr_dyn_typeof(const ScrDyn *d) {
return scr_str_new(s, strlen(s));
}
bool scr_dyn_is_object(const ScrDyn *d) {
ScrStr *type = scr_dyn_typeof(d);
bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0;
scr_str_release(type);
return object;
}
ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
if (d->kind == SCR_DYN_TYPED_REF) {
if (scr_dyn_isl_is_error(d)) return scr_str_new("[object Error]", 14);
@@ -2959,6 +2966,30 @@ bool scr_dyn_err_instanceof(const ScrDyn *d, double kind) {
return false;
}
ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len) {
ScrDyn *own = scr_dyn_obj_get(d, key, key_len);
if (own) return scr_dyn_retain(own);
if (key_len == 11 && memcmp(key, "constructor", 11) == 0) {
static const char *const tokens[] = {
"[builtin Error]", "[builtin TypeError]",
"[builtin RangeError]", "[builtin SyntaxError]",
};
for (size_t i = 0; i < scr_errdyn_n; i++) {
if (scr_errdyn_cache[i].dyn != d) continue;
const ScrVt *vt = scr_errdyn_cache[i].err->vt;
for (size_t kind = 0; kind < sizeof tokens / sizeof tokens[0]; kind++) {
if (vt != &scr_error_vts[kind]) continue;
ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind]));
ScrDyn *result = scr_dyn_new_str(token);
scr_str_release(token);
return result;
}
break;
}
}
return scr_dyn_retain(scr_dyn_undefined());
}
/* ── Object.keys/values/entries over the checked-dynamic tree ──────────────────────────
* JS own-key order: array-index keys ascending first, then the rest in
* insertion order. entries answers [key, value] pairs; values RETAIN
+20 -1
View File
@@ -2089,6 +2089,10 @@ typedef enum {
* Carries NO payload (the value rides the generator's ret slot), so
* scr_exc_clear/reset need no arm. Never escapes a generator fiber. */
SCR_EXC_GENRET,
/* Refcounted values whose JS typeof is not object (Symbol, BigInt,
* functions, and checked-dynamic primitives). The payload uses the
* same ownership as REF. */
SCR_EXC_PRIMITIVE_REF,
} ScrExcKind;
/* One cell per fiber (JS has one exception in flight per execution
@@ -2119,7 +2123,12 @@ void scr_throw_str(ScrStr *v); /* takes ownership */
* passes the payload type's `_v` adapters, like scr_union_new_ref); trace
* is non-NULL iff the payload type carries a cycle header. */
void scr_throw_ref(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace);
ScrTraceFn trace);
/* Store a reference whose JS typeof was checked at the throw site. */
void scr_throw_ref_classified(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace, bool object);
void scr_throw_primitive_ref(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace);
/* Same ownership contract as scr_throw_ref; the payload must be a
* hierarchy-class instance (vtable word present) — see SCR_EXC_OBJ. */
void scr_throw_obj(void *v, void *(*retain)(void *), void (*release)(void *),
@@ -2162,6 +2171,9 @@ void scr_rethrow(const ScrCaught *c);
/* `e instanceof C` on a catch binding: an OBJ payload whose vtable preorder
* lies inside C's interval. False for every other payload kind. */
bool scr_caught_instanceof(const ScrCaught *c, size_t pre, size_t post);
/* `typeof e === "object"` on a catch binding. Throw sites choose REF or
* PRIMITIVE_REF from the value's JS typeof before storing the payload. */
bool scr_caught_is_object(const ScrCaught *c);
/* `String(e)` / `${e}` on a catch binding: JS's String() over the snapshot
* — numbers/booleans/strings by value, Error payloads via
* scr_error_to_string ("name: message" — String(e) carries no stack in
@@ -3597,6 +3609,12 @@ double scr_dyn_number_coerce(const ScrDyn *d);
* back to its runtime error and the class's stamped preorder interval
* answers. A dyn value that never came from an error answers false. */
bool scr_dyn_err_instanceof(const ScrDyn *d, double kind);
/* Property read on a checked-dynamic object. Own fields take precedence;
* runtime Error instances inherit their exact builtin constructor token.
* The result is owned. */
ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len);
/* JS typeof comparison, including null's "object" result. */
bool scr_dyn_is_object(const ScrDyn *d);
/* structuredClone over the checked-dynamic tree: JSON-safe data + bytes deep-copy;
* functions/handles throw the spec's catchable DataCloneError; cycles
@@ -4821,6 +4839,7 @@ ScrJsval *scr_jsval_iter_new(ScrJsval *a);
ScrJsval *scr_jsval_plus(ScrJsval *a); /* unary + (ToNumber) */
int scr_jsval_truthy(ScrJsval *a);
ScrStr *scr_jsval_typeof(ScrJsval *a);
bool scr_jsval_is_object(ScrJsval *a);
ScrStr *scr_jsval_to_str(ScrJsval *a); /* String(v); NULL = bridged */
/* Property/element access and calls. Names are NUL-terminated ScrStr
+35
View File
@@ -0,0 +1,35 @@
try {
throw new TypeError("x");
} catch (error) {
console.log(typeof error === "object", typeof error !== "object");
}
try {
throw () => 1;
} catch (error) {
console.log(typeof error === "object", typeof error !== "object");
}
try {
throw 1;
} catch (error) {
console.log(typeof error === "object", typeof error !== "object");
}
try {
throw JSON.parse("null");
} catch (error) {
console.log(typeof error === "object", typeof error !== "object");
}
try {
throw Symbol("x");
} catch (error) {
console.log(typeof error === "object", typeof error !== "object");
}
try {
throw 1n;
} catch (error) {
console.log(typeof error === "object", typeof error !== "object");
}
+39
View File
@@ -0,0 +1,39 @@
function failing(): Promise<number> {
return new Promise<number>((_resolve, reject) => reject(new Error("receiver")));
}
async function main(): Promise<void> {
const fulfilled = await Promise.resolve(2).then((value) => value + 1, () => 0);
console.log("fulfilled", fulfilled);
const rejected = await failing().then(() => "unexpected", (error) => {
if (error instanceof Error) return error.message;
return "not an Error";
});
console.log("rejected", rejected);
const handlerThrow = await Promise.resolve("value")
.then((): string => { throw new Error("handler"); }, () => "wrong handler")
.catch((error) => {
if (error instanceof Error) return error.message;
return "not an Error";
});
console.log("handler throw", handlerThrow);
const flattened = await Promise.resolve(5).then(
(value) => Promise.resolve(value + 1),
() => Promise.resolve(0),
);
console.log("flattened", flattened);
const identity = await Promise.resolve(8).then(undefined, () => 0);
console.log("identity", identity);
const passedRejection = await failing().then(() => "wrong", undefined).catch((error) => {
if (error instanceof Error) return error.message;
return "not an Error";
});
console.log("passed rejection", passedRejection);
}
void main();
+3 -3
View File
@@ -1,6 +1,6 @@
// Catch bindings are deliberately NARROW: the binding is typed by what the
// exception cell can actually hold, so the supported uses are the narrowing
// tests (instanceof over hierarchy classes, typeof over primitives), reads
// tests (instanceof over hierarchy classes, typeof over selected types), reads
// under a proven narrow, rethrow, and the unknown-slot CONVERSION (an
// un-narrowed use typed `unknown` converts to a dynamic value — corpus
// 1554; `const copy = e` compiles now and is NOT in this fence corpus).
@@ -37,8 +37,8 @@ try {
try {
throw 6;
} catch (e) {
if (typeof e === "object") {
// only the primitive typeof tests narrow
if (typeof e === "function") {
// callable catch tests remain outside the supported narrowing set
console.log("no");
}
}
@@ -28,9 +28,9 @@ catch-bindings.ts:35:3 - error SC1090: assignments to catch bindings are not sup
| ^
36 | }
catch-bindings.ts:40:20 - error SC1090: 'typeof' catch-binding tests against "object" (only "string"/"number"/"boolean" narrow a catch binding; use 'instanceof' for objects) is not supported yet
catch-bindings.ts:40:20 - error SC1090: 'typeof' catch-binding tests against "function" (only "string"/"number"/"boolean"/"object" are supported) is not supported yet
39 | } catch (e) {
40 | if (typeof e === "object") {
| ^~~~~~~~
41 | // only the primitive typeof tests narrow
40 | if (typeof e === "function") {
| ^~~~~~~~~~
41 | // callable catch tests remain outside the supported narrowing set
@@ -258,15 +258,6 @@ stdlib-fence.ts:61:16 - error SC2020: 'JSON.stringify with replacer/space parame
hint: the serializer is type-directed — shape the value before stringifying
stdlib-fence.ts:66:16 - error SC2020: 'then with 2 arguments' is part of the standard library types but has no scriptc lowering yet
65 | }
66 | const thened = work().then((v) => v + 1, () => 0);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
67 | const caught = work().catch(() => 0);
hint: the supported form takes exactly one fulfillment handler — chain .catch(...) for the rejection half
stdlib-fence.ts:73:15 - error SC2020: 'Object.is over a dynamic operand' is part of the standard library types but has no scriptc lowering yet
72 | // unions needing narrowing.
@@ -32,9 +32,9 @@ Fetching aliases under mock-team
`;
exports[`vercel e2e (real CLI vs Node against the mock api) > alias ls > alias ls stdout 1`] = `
" source url age
mock-app-abc123defg-mock-team.vercel.app mock-app.vercel.app <T>
mock-app-abc123defg-mock-team.vercel.app mock-example.com <T>
" source url age
mock-app-abc123defg-mock-team.vercel.app mock-app.vercel.app <T>
mock-app-abc123defg-mock-team.vercel.app mock-example.com <T>
"
`;
@@ -95,9 +95,9 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > certs ls > certs l
`;
exports[`vercel e2e (real CLI vs Node against the mock api) > certs ls > certs ls stdout 1`] = `
" id cns expiration renew age
cert_mock000000000000000000001 - mock-example.com <T> ago yes <T>
- *.mock-example.com
" id cns expiration renew age
cert_mock000000000000000000001 - mock-example.com <T> ago yes <T>
- *.mock-example.com
"
`;
@@ -134,11 +134,11 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > dns ls > dns ls st
"Vercel CLI 56.2.0 (Node.js <V>)
Fetching domains under mock-team
> Records found under mock-team [<T>]
>
>
mock-example.com
id name type value created
A 76.76.21.21 default
rec_mock0000000000000000000002 www CNAME cname.vercel-dns.com <T> ago
id name type value created
A 76.76.21.21 default
rec_mock0000000000000000000002 www CNAME cname.vercel-dns.com <T> ago
"
`;
@@ -195,15 +195,15 @@ Fetching Domain mock-example.com under mock-team
Nameservers
Intended Nameservers Current Nameservers
ns1.vercel-dns.com ns1.example-dns.com ✘
ns2.vercel-dns.com ns2.example-dns.com ✘
Intended Nameservers Current Nameservers
ns1.vercel-dns.com ns1.example-dns.com ✘
ns2.vercel-dns.com ns2.example-dns.com ✘
Projects
Project Domains
mock-app mock-example.com
Project Domains
mock-app mock-example.com
"
`;
@@ -223,10 +223,10 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > domains ls > domai
"Vercel CLI 56.2.0 (Node.js <V>)
Fetching Domains under mock-team
> 1 Domain found under mock-team [<T>]
Domain Registrar Nameservers Expiration Date Creator Age
mock-example.com Third Party Third Party - mockuser <T>
Domain Registrar Nameservers Expiration Date Creator Age
mock-example.com Third Party Third Party - mockuser <T>
"
`;
@@ -252,9 +252,9 @@ Retrieving project…
Saving…
✓ Added MOCK_API_TOKEN
Project mock-team/mock-app
Environments Production
Type Sensitive
Project mock-team/mock-app
Environments Production
Type Sensitive
"
`;
@@ -281,10 +281,10 @@ Retrieving project…
exports[`vercel e2e (real CLI vs Node against the mock api) > env ls > env ls stdout 1`] = `
"
name value environments (git branch) created
DATABASE_URL Encrypted Production, Preview, Development <T> ago
NEXT_PUBLIC_BASE_URL https://mock-examp… Production <T> ago
PREVIEW_FLAG Encrypted Preview (staging) <T> ago
name value environments (git branch) created
DATABASE_URL Encrypted Production, Preview, Development <T> ago
NEXT_PUBLIC_BASE_URL https://mock-examp… Production <T> ago
PREVIEW_FLAG Encrypted Preview (staging) <T> ago
"
`;
@@ -370,9 +370,9 @@ Retrieving project…
exports[`vercel e2e (real CLI vs Node against the mock api) > env ls production > env ls production stdout 1`] = `
"
name value environments created
DATABASE_URL Encrypted Production, Preview, Development <T> ago
NEXT_PUBLIC_BASE_URL https://mock-examp… Production <T> ago
name value environments created
DATABASE_URL Encrypted Production, Preview, Development <T> ago
NEXT_PUBLIC_BASE_URL https://mock-examp… Production <T> ago
"
`;
@@ -431,88 +431,88 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > help > help stderr
Basic
deploy [path] Performs a deployment (default)
build Build the project locally into './vercel/output'
cache [cmd] Manages cache for your current Project
dev Start a local development server
env Manages the Environment Variables for your current Project
git Manage Git provider repository for your current Project
help [cmd] Displays complete help for [cmd]
init [example] Initialize an example project
inspect [id] Displays information related to a deployment
i | install [name] Install an integration from the Marketplace
integration [cmd] Manages your Marketplace integrations
ir | integration-resource [cmd] Manages your Marketplace integration resources
link [path] Link local directory to a Vercel Project
ls | list [app] Lists deployments
login [email] Logs into your account or creates a new one
logout Logs out of your account
open Opens the current project in the Vercel Dashboard
promote [url|id] Promote an existing deployment to current
pull [path] Pull your Project Settings from the cloud
redeploy [url|id] Rebuild and deploy a previous deployment.
rollback [url|id] Quickly revert back to a previous deployment
switch [scope] Switches between different scopes
deploy [path] Performs a deployment (default)
build Build the project locally into './vercel/output'
cache [cmd] Manages cache for your current Project
dev Start a local development server
env Manages the Environment Variables for your current Project
git Manage Git provider repository for your current Project
help [cmd] Displays complete help for [cmd]
init [example] Initialize an example project
inspect [id] Displays information related to a deployment
i | install [name] Install an integration from the Marketplace
integration [cmd] Manages your Marketplace integrations
ir | integration-resource [cmd] Manages your Marketplace integration resources
link [path] Link local directory to a Vercel Project
ls | list [app] Lists deployments
login [email] Logs into your account or creates a new one
logout Logs out of your account
open Opens the current project in the Vercel Dashboard
promote [url|id] Promote an existing deployment to current
pull [path] Pull your Project Settings from the cloud
redeploy [url|id] Rebuild and deploy a previous deployment.
rollback [url|id] Quickly revert back to a previous deployment
switch [scope] Switches between different scopes
Advanced
activity List user activity events
agent [init] Generate AGENTS.md with Vercel best practices
ai-gateway [cmd] Manage AI Gateway resources
agent-runs [cmd] Inspect Agent Runs observability data
alerts List alerts for a project or team
alias [cmd] Manages your domain aliases
api [endpoint] Make authenticated HTTP requests to the Vercel API [beta]
bisect Use binary search to find the deployment that introduced a bug
blob [cmd] Manages your Blob stores and files
buy [cmd] Purchase Vercel products for your team
certs [cmd] Manages your SSL certificates
connect [cmd] Manage connectors [beta]
contract Show contract information for billing periods
cron | crons [cmd] Manage cron jobs for a project [beta]
curl [path] cURL requests to your linked project's deployment [beta]
deploy-hooks [cmd] Manage deploy hooks for Git-triggered builds
dns [name] Manages your DNS records
domains [name] Manages your domain names
edge-config [cmd] Manage Edge Config stores
firewall [cmd] Manages Vercel Firewall configuration and custom rules
flags [cmd] Manage feature flags for a Vercel project
httpstat path Visualize HTTP timing statistics for deployments
logs [url] Displays the logs for a deployment
metrics <metric> Queries observability metrics for your project or team
mcp Set up MCP agents and configuration
microfrontends Manages your microfrontends
oauth-apps [cmd] Register Vercel Apps (OAuth) and manage team installations
projects Manages your Projects
redirects [cmd] Manages redirects for your current Project
rm | remove [id] Removes a deployment
routes [cmd] Manages routing rules for your current Project
rr | rolling-release [cmd] Manage rolling releases for gradual traffic shifting
sandbox Interact with Vercel Sandbox
skills [query] Discover agent skills relevant to your project
target [cmd] Manage custom environments for your Project
teams Manages your teams
telemetry [cmd] Enable or disable telemetry collection
tokens [cmd] Manage your personal Vercel authentication tokens
traces [cmd] Fetch and capture traces for your project's deployment
upgrade Upgrade the Vercel CLI to the latest version
usage Show billing usage for the current billing period
vcr [cmd] Manages your Container Registry repositories and images
webhooks [cmd] Manages webhooks [beta]
whoami Shows the username of the currently logged in user
activity List user activity events
agent [init] Generate AGENTS.md with Vercel best practices
ai-gateway [cmd] Manage AI Gateway resources
agent-runs [cmd] Inspect Agent Runs observability data
alerts List alerts for a project or team
alias [cmd] Manages your domain aliases
api [endpoint] Make authenticated HTTP requests to the Vercel API [beta]
bisect Use binary search to find the deployment that introduced a bug
blob [cmd] Manages your Blob stores and files
buy [cmd] Purchase Vercel products for your team
certs [cmd] Manages your SSL certificates
connect [cmd] Manage connectors [beta]
contract Show contract information for billing periods
cron | crons [cmd] Manage cron jobs for a project [beta]
curl [path] cURL requests to your linked project's deployment [beta]
deploy-hooks [cmd] Manage deploy hooks for Git-triggered builds
dns [name] Manages your DNS records
domains [name] Manages your domain names
edge-config [cmd] Manage Edge Config stores
firewall [cmd] Manages Vercel Firewall configuration and custom rules
flags [cmd] Manage feature flags for a Vercel project
httpstat path Visualize HTTP timing statistics for deployments
logs [url] Displays the logs for a deployment
metrics <metric> Queries observability metrics for your project or team
mcp Set up MCP agents and configuration
microfrontends Manages your microfrontends
oauth-apps [cmd] Register Vercel Apps (OAuth) and manage team installations
projects Manages your Projects
redirects [cmd] Manages redirects for your current Project
rm | remove [id] Removes a deployment
routes [cmd] Manages routing rules for your current Project
rr | rolling-release [cmd] Manage rolling releases for gradual traffic shifting
sandbox Interact with Vercel Sandbox
skills [query] Discover agent skills relevant to your project
target [cmd] Manage custom environments for your Project
teams Manages your teams
telemetry [cmd] Enable or disable telemetry collection
tokens [cmd] Manage your personal Vercel authentication tokens
traces [cmd] Fetch and capture traces for your project's deployment
upgrade Upgrade the Vercel CLI to the latest version
usage Show billing usage for the current billing period
vcr [cmd] Manages your Container Registry repositories and images
webhooks [cmd] Manages webhooks [beta]
whoami Shows the username of the currently logged in user
Global Options:
-h, --help Output usage information
-v, --version Output the version number
--cwd Current working directory
-A FILE, --local-config=FILE Path to the local \`vercel.json\` file
-Q DIR, --global-config=DIR Path to the global \`.vercel\` directory
-d, --debug Debug mode [off]
--no-color No color mode [off]
--non-interactive Run without interactive prompts (default when agent detected)
-S, --scope Set a custom scope
-t TOKEN, --token=TOKEN Login token
-h, --help Output usage information
-v, --version Output the version number
--cwd Current working directory
-A FILE, --local-config=FILE Path to the local \`vercel.json\` file
-Q DIR, --global-config=DIR Path to the global \`.vercel\` directory
-d, --debug Debug mode [off]
--no-color No color mode [off]
--non-interactive Run without interactive prompts (default when agent detected)
-S, --scope Set a custom scope
-t TOKEN, --token=TOKEN Login token
Examples:
@@ -549,29 +549,29 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > help alias > help
▲ vercel alias [command]
Interact with deployment aliases
Interact with deployment aliases
Commands:
list Show all aliases
remove alias Remove an alias using its hostname
set id-or-url alias Create a new alias (default)
list Show all aliases
remove alias Remove an alias using its hostname
set id-or-url alias Create a new alias (default)
Global Options:
--cwd <DIR> Sets the current working directory for a single
run of a command
-d, --debug Debug mode (default off)
-Q, --global-config <DIR> Path to the global \`.vercel\` directory
-h, --help Output usage information
-A, --local-config <FILE> Path to the local \`vercel.json\` file
--no-color No color mode (default off)
--non-interactive Run without interactive prompts; when an agent is
detected this is the default
-S, --scope Set a custom scope
-t, --token <TOKEN> Login token
-v, --version Output the version number
--cwd <DIR> Sets the current working directory for a single
run of a command
-d, --debug Debug mode (default off)
-Q, --global-config <DIR> Path to the global \`.vercel\` directory
-h, --help Output usage information
-A, --local-config <FILE> Path to the local \`vercel.json\` file
--no-color No color mode (default off)
--non-interactive Run without interactive prompts; when an agent is
detected this is the default
-S, --scope Set a custom scope
-t, --token <TOKEN> Login token
-v, --version Output the version number
Examples:
@@ -623,14 +623,14 @@ Fetching deployment "mock-app-abc123defg-mock-team.vercel.app" in mock-team
╶ https://mock-app.vercel.app
╶ https://mock-example.com
Builds
┌ package.json [<T>]
┌ package.json [<T>]
├── λ api/index (1MB) [iad1]
└── index.html (2KB)
"
`;
@@ -654,10 +654,10 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > logs for a deploym
Resolving deployment "mock-app-abc123defg-mock-team.vercel.app"
Fetching project "prj_mock000000000000000000000001"
Fetching logs...
TIME HOST LEVEL STATUS MESSAGE
00:01:40.00 mock-app.vercel.app info ◇ GET / 200 (no message)
TIME HOST LEVEL STATUS MESSAGE
00:01:40.00 mock-app.vercel.app info ◇ GET / 200 (no message)
00:01:50.00 mock-app.vercel.app error λ GET /api/data 500 TypeError: Cannot read properties of undefine…
00:02:00.00 mock-app.vercel.app warning λ POST /api/submit 202 payload missing optional field "tags"
00:02:00.00 mock-app.vercel.app warning λ POST /api/submit 202 payload missing optional field "tags"
Fetched 3 logs for mock-team/mock-app
"
`;
@@ -679,9 +679,9 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > ls (deployments) >
Fetching deployments in mock-team
> Deployments under mock-team [<T>]
Age Project Deployment Status Environment Duration Username
<T> mock-team/mock-app https://mock-app-abc123defg-mock-team.vercel.app ● Ready Production <T> mockuser
<T> mock-team/mock-app https://mock-app-hij456klmn-mock-team.vercel.app ● Error Preview ? mockuser
Age Project Deployment Status Environment Duration Username
<T> mock-team/mock-app https://mock-app-abc123defg-mock-team.vercel.app ● Ready Production <T> mockuser
<T> mock-team/mock-app https://mock-app-hij456klmn-mock-team.vercel.app ● Error Preview ? mockuser
"
`;
@@ -725,9 +725,9 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > project ls > proje
Fetching projects in mock-team
> Projects found under mock-team  [<T>]
Project Name Latest Production URL Updated Node Version
mock-app -- <T> 22.x
static-site -- <T> 22.x
Project Name Latest Production URL Updated Node Version
mock-app -- <T> 22.x
static-site -- <T> 22.x
"
`;
@@ -747,9 +747,9 @@ exports[`vercel e2e (real CLI vs Node against the mock api) > teams ls > teams l
"Vercel CLI 56.2.0 (Node.js <V>)
Fetching teams
Fetching user information
id Team name
second-team Second Team
mock-team Mock Team
id Team name
second-team Second Team
mock-team Mock Team
"
`;
+32 -4
View File
@@ -1,6 +1,6 @@
import { expect, test } from "vitest";
import { boundedRun } from "../test262/execute.js";
import { compileFailureStatus, exclusion, metadata, prepare, summarize, variants } from "../test262/support.mjs";
import { compileFailureStatus, exclusion, matchesParseNegative, metadata, prepare, summarize, variants } from "../test262/support.mjs";
const source = (yaml: string, body = "assert.sameValue(1, 1);") => `/*---\n${yaml}\n---*/\n${body}`;
@@ -33,14 +33,13 @@ test("metadata generates the upstream variants without rewriting execution goals
test("unsupported execution requirements and assertion reflection remain exclusions", () => {
for (const [head, body] of [
["negative: {phase: parse, type: SyntaxError}", "invalid syntax"],
["negative: {phase: parse, type: SyntaxError}", "assert.sameValue(1, 1);"],
["negative: {phase: runtime, type: TypeError}", "throw new TypeError();"],
["flags: [async]", "$DONE();"],
["includes: [propertyHelper.js]", "verifyProperty({}, 'x', {});"],
["description: global script", "assert.sameValue(this, globalThis);"],
["description: reflection", "assert.sameValue(typeof assert, 'function');"],
["description: mutation", "assert.sameValue = () => {};"],
["description: unsupported helper", "assert.throws(TypeError, () => {});"],
["description: unsupported helper", "assert.throws(CustomError, () => {});"],
]) {
const text = source(head!, body);
expect(exclusion(text, metadata(text), "strict")).toBeTypeOf("string");
@@ -48,6 +47,35 @@ test("unsupported execution requirements and assertion reflection remain exclusi
const text = source("description: scalar", "// globalThis and this in comments are harmless\nassert.sameValue('this', 'this');");
expect(exclusion(text, metadata(text), "strict")).toBeUndefined();
expect(exclusion(text, metadata(text), "sloppy")).toBe("execution:sloppy");
const asyncText = source("flags: [async]", "Promise.resolve().then(() => $DONE());");
expect(exclusion(asyncText, metadata(asyncText), "strict")).toBeUndefined();
});
test("negative parse cases require the compiler's matching source diagnostic", () => {
const text = source("negative: {phase: parse, type: SyntaxError}", "$DONOTEVALUATE();\nconst = ;");
expect(exclusion(text, metadata(text), "strict")).toBeUndefined();
expect(exclusion(text, metadata(text), "sloppy")).toBeUndefined();
const offset = prepare(text).indexOf("const = ;") + "const ".length;
const match = { status: "compile-refusal", diagnostics: [{
code: "SC0001", message: "Variable declaration expected.", loc: { file: "/tmp/main.js", start: offset },
}] };
expect(matchesParseNegative(match, text)).toBe(true);
expect(matchesParseNegative(match, text, "sloppy")).toBe(true);
expect(matchesParseNegative({ status: "compile-refusal", diagnostics: [{
code: "SC0001", message: "Cannot find name '$DONOTEVALUATE'.", loc: { file: "/tmp/main.js", start: 0 },
}] }, text)).toBe(false);
expect(matchesParseNegative({ status: "compiler-error", diagnostics: match.diagnostics }, text)).toBe(false);
const strictOnly = source("negative: {phase: parse, type: SyntaxError}", "with ({}) {}");
expect(exclusion(strictOnly, metadata(strictOnly), "sloppy")).toBe("negative-phase:parse");
const generatedOnly = source("negative: {phase: parse, type: SyntaxError}", "function f() {");
expect(exclusion(generatedOnly, metadata(generatedOnly), "strict")).toBe("negative-phase:parse");
});
test("built-in error assertions are admitted without permitting constructor aliases", () => {
const accepted = source("description: exact error", "assert.throws(TypeError, () => { throw new TypeError(); });");
expect(exclusion(accepted, metadata(accepted), "strict")).toBeUndefined();
const alias = source("description: custom error", "const Expected = TypeError; assert.throws(Expected, () => { throw new TypeError(); });");
expect(exclusion(alias, metadata(alias), "strict")).toBe("harness:assert-surface");
});
test("the compareArray include only admits the implemented assertion form", () => {
+39 -1
View File
@@ -3,7 +3,7 @@ import { join } from "node:path";
import { createContext, runInContext } from "node:vm";
import { describe, expect, test } from "vitest";
import { runSource } from "../test262/execute.js";
import { exclusion, matchesExpectation, metadata, pin, vendorRoot, verifyVendor } from "../test262/support.mjs";
import { exclusion, matchesExpectation, matchesParseNegative, metadata, pin, vendorRoot, verifyVendor } from "../test262/support.mjs";
import { shardSelect, shardSuffix } from "./shard.js";
const sanitize = process.env.SCRIPTC_SAN === "1";
@@ -20,6 +20,13 @@ test("Test262 regression inputs retain their pinned upstream bytes", () => {
verifyVendor();
});
test("negative parse cases require a matching compiler syntax diagnostic", async () => {
const source = "/*---\nnegative: {phase: parse, type: SyntaxError}\n---*/\n$DONOTEVALUATE();\nconst = ;";
expect(exclusion(source, metadata(source), "strict")).toBeUndefined();
const result = await runSource(source, { sanitize });
expect(matchesParseNegative(result, source), JSON.stringify(result, null, 2)).toBe(true);
});
const profileCases = shardSelect<string>(pin.tests, (path) => `${path}#strict`);
// The initial profile is small enough that a valid shard can own no cases.
// Vitest rejects an empty describe block, so register this suite only when
@@ -56,6 +63,11 @@ assert.notSameValue(1, "1");
{ name: "array element mismatch", status: "fail", source: "assert.compareArray([1, 2], [1, 3]);" },
{ name: "array signed zero mismatch", status: "fail", source: "assert.compareArray([0], [-0]);" },
{ name: "assert requires true, not truthiness", status: "fail", source: "assert(1);" },
{ name: "exact built-in exception", status: "pass", source: "assert.throws(TypeError, () => { throw new TypeError('x'); });" },
{ name: "wrong built-in exception", status: "fail", source: "assert.throws(TypeError, () => { throw new RangeError('x'); });" },
{ name: "subclass is not exact", status: "fail", source: "class E extends TypeError {} assert.throws(TypeError, () => { throw new E('x'); });" },
{ name: "missing exception", status: "fail", source: "assert.throws(TypeError, () => {});" },
{ name: "primitive exception", status: "fail", source: "assert.throws(TypeError, () => { throw 1; });" },
];
describe(`Test262 host assertion contract${shardSuffix()}`, () => {
@@ -96,4 +108,30 @@ describe(`Test262 host assertion contract${shardSuffix()}`, () => {
const result = await runSource(source, { sanitize });
expect(result, JSON.stringify(result)).toMatchObject({ status: "harness-refusal", reason: "reference-assertion" });
});
test("non-Error objects do not masquerade as Error assertions", async () => {
const source = "const value = JSON.parse('{\"constructor\":\"[builtin TypeError]\"}'); assert.throws(TypeError, () => { throw value; });";
expect(() => runUpstream(source)).toThrow();
const result = await runSource(source, { sanitize });
expect(result, JSON.stringify(result)).toMatchObject({ status: "harness-refusal", reason: "non-error-assertion" });
});
});
const asyncControls = shardSelect([
{ name: "promise completion", status: "pass", body: "Promise.resolve(1).then(value => { assert.sameValue(value, 1); $DONE(); });" },
{ name: "completion error", status: "fail", body: "Promise.resolve().then(() => { $DONE('failure'); });" },
{ name: "missing completion", status: "fail", body: "Promise.resolve().then(() => {});" },
{ name: "duplicate completion", status: "fail", body: "$DONE(); $DONE();" },
{ name: "completion marker spoof", status: "fail", body: "console.log('__scriptc_test262_complete__');" },
], (item) => `async:${item.name}`);
if (asyncControls.length > 0) describe(`Test262 async host contract${shardSuffix()}`, () => {
for (const control of asyncControls) {
test(control.name, async () => {
const source = `/*---\nflags: [async]\n---*/\n${control.body}`;
expect(exclusion(source, metadata(source), "strict")).toBeUndefined();
const result = await runSource(source, { sanitize, asyncTest: true });
expect(result, JSON.stringify(result, null, 2)).toMatchObject({ status: control.status });
});
}
});
+12 -3
View File
@@ -6,7 +6,7 @@
* the real CLI); stdout, stderr, and exit codes must agree. Mutations are
* safe BECAUSE of the jail: every write (env add/rm, dns add/rm, alias
* set/rm, project add) lands in the mock, whose endpoint trace pins the
* request body byte-for-byte. the gateway e2e suite's three-legged pattern, pointed at a CLI
* request body exactly. the gateway e2e suite's three-legged pattern, pointed at a CLI
* whose network stack rides the island's native fetch. The compiled leg's
* module-graph walls fell in sequence (node:https → smol-toml's dual
* package → node:domain); the "reaches today" pin below records the
@@ -49,12 +49,13 @@
* attributes requests by bearer token, so concurrent lanes stay separable
* and the per-command endpoint sequence is asserted per child.
*
* NORMALIZATIONS (both lanes, byte-exact otherwise): the lane cwd →
* NORMALIZATIONS (both lanes, other content exact): the lane cwd →
* "<CWD>", the banner's Node version → "(Node.js <V>)" (a build pin, not
* semantics), spinner elapsed "[123ms]" → "[<T>]", and relative-time
* tokens ("932d", "50s", "842d ago") → "<T>" — they are rendered against
* wall-clock "now" from the mock's fixed timestamps, so a day boundary
* mid-run would flip them. Absolute dates (inspect's created line, logs'
* mid-run would flip them. Table alignment spaces also vary with age and
* are collapsed while retaining cell boundaries. Absolute dates (inspect's created line, logs'
* TIME column) are NOT normalized: children run under TZ=UTC, so the
* mock's fixed epochs render machine-independently and any lane skew in
* Date formatting is a real divergence. */
@@ -245,6 +246,14 @@ function normalize(buf: Buffer, cwd: string): string {
.replace(/False positive error reports may follow\n/g, "")
.replace(/For details see https:\/\/github\.com\/google\/sanitizers\/issues\/189\n/g, "");
}
// Relative ages change table column widths as the fixed mock dates recede.
// Preserve cell boundaries and contents while discarding alignment padding.
text = text.split("\n").map((line) => {
const unpadded = line.trimEnd();
return /^ +\S.* {2,}\S/.test(unpadded)
? ` ${unpadded.trimStart().replace(/ {2,}/g, " ")}`
: unpadded;
}).join("\n");
return text;
}
+17
View File
@@ -0,0 +1,17 @@
/**
* @param {unknown} expectedErrorConstructor
* @param {() => unknown} func
* @returns {"pass" | "missing" | "primitive" | "wrong" | "invalid" | "unsupported"}
*/
export function throwsOutcome(expectedErrorConstructor, func) {
if (typeof func !== "function") return "invalid";
try {
func();
} catch (thrown) {
const value = thrown;
if (typeof thrown !== "object" || thrown === null) return "primitive";
if (!(thrown instanceof Error)) return "unsupported";
return value.constructor === expectedErrorConstructor ? "pass" : "wrong";
}
return "missing";
}
+11 -4
View File
@@ -1,10 +1,11 @@
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { compileFailureStatus, completion, harnessSource, prepare } from "./support.mjs";
import { assertThrowsSource, compileFailureStatus, completion, harnessSource, prepare } from "./support.mjs";
export interface Outcome {
status: string;
@@ -62,6 +63,7 @@ export function boundedRun(command: string, args: string[], timeoutMs: number):
export async function runSource(source: string, options: {
backend?: "default" | "llvm" | "c";
asyncTest?: boolean;
sanitize?: boolean;
compileTimeoutMs?: number;
runtimeTimeoutMs?: number;
@@ -76,8 +78,10 @@ export async function runSource(source: string, options: {
const requestFile = join(workDir, "request.json");
let phase = "compile";
try {
writeFileSync(entry, prepare(source));
writeFileSync(join(workDir, "harness.ts"), harnessSource);
const marker = `${completion}:${randomBytes(16).toString("hex")}`;
writeFileSync(entry, prepare(source, options.asyncTest ?? false, marker));
writeFileSync(join(workDir, "harness.ts"), harnessSource.replace(completion, marker));
writeFileSync(join(workDir, "assert-throws.js"), assertThrowsSource);
writeFileSync(requestFile, JSON.stringify({
entry, binary, workDir, result: resultFile,
backend: options.backend ?? "default", sanitize: options.sanitize ?? false,
@@ -106,7 +110,10 @@ export async function runSource(source: string, options: {
if (result.code === 86 && stderr === "SCRIPTC_TEST262_HARNESS: reference equality requires an identity-preserving adapter\n") {
return finish({ ...common, status: "harness-refusal", phase, reason: "reference-assertion", stderr });
}
if (result.code === 0 && !result.overflow && stderr === "" && result.stdout === `${completion}\n`) {
if (result.code === 86 && stderr === "SCRIPTC_TEST262_HARNESS: assert.throws needs an Error instance\n") {
return finish({ ...common, status: "harness-refusal", phase, reason: "non-error-assertion", stderr });
}
if (result.code === 0 && !result.overflow && stderr === "" && result.stdout === `${marker}\n`) {
return finish({ ...common, status: "pass" });
}
// Require the entire diagnostic, so a sanitizer report following a known
+22
View File
@@ -2,9 +2,21 @@
// adapter avoids JS function expandos, which currently hit a static fence.
// Keep the supported surface small and check it against the upstream helpers.
import nodeAssert from "node:assert/strict";
import { throwsOutcome } from "./assert-throws.js";
export class Test262Error extends Error {}
let asyncDone = false;
export function $DONE(error?: unknown): void {
if (asyncDone || error) {
console.error("SCRIPTC_TEST262_HARNESS: async completion failed");
process.exit(87);
}
asyncDone = true;
console.log("__scriptc_test262_complete__");
}
function scalar(value: unknown): void {
// Native-to-unknown conversion can copy references (notably arrays).
// Refuse these assertions before comparing altered identities. Terminate
@@ -40,6 +52,16 @@ export namespace assert {
}
}
export function throws(expectedErrorConstructor: unknown, func: () => unknown, message = "Expected exception"): void {
const outcome = throwsOutcome(expectedErrorConstructor, func);
if (outcome === "invalid") throw new Test262Error("assert.throws requires a function to run");
if (outcome === "unsupported") {
console.error("SCRIPTC_TEST262_HARNESS: assert.throws needs an Error instance");
process.exit(86);
}
if (outcome !== "pass") throw new Test262Error(message);
}
export function compareArray(actual: unknown, expected: unknown, message = "Expected matching array contents"): void {
if (!Array.isArray(actual) || !Array.isArray(expected)) {
scalar(actual);
+12 -8
View File
@@ -4,7 +4,7 @@ import { parseArgs } from "node:util";
import { shardSelect } from "../harness/shard.js";
import { runSource, type Outcome } from "./execute.js";
import {
directory, exclusion, matchesExpectation, metadata, pin, sha256, snapshotDigest, summarize,
assertThrowsSource, directory, exclusion, harnessSource, matchesExpectation, matchesParseNegative, metadata, pin, sha256, snapshotDigest, summarize,
testPaths, variants, vendorRoot, verifyVendor,
} from "./support.mjs";
@@ -29,8 +29,8 @@ async function main(): Promise<void> {
--list list variants and exclusions without compiling
--keep retain generated sources and native artifacts
SCRIPTC_SAN=1 enables sanitizers; SCRIPTC_TEST_SHARD=i/n partitions variants.
Only synchronous strict scripts in the documented adapted profile can pass.
All other variants remain visible as exclusions; negative errors never count as passes.`);
Adapted strict scripts and source-matched strict or sloppy syntax negatives can pass.
Other variants remain visible as exclusions.`);
return;
}
function positive(value: string | undefined, fallback: number, name: string): number {
@@ -56,7 +56,8 @@ All other variants remain visible as exclusions; negative errors never count as
const source = readFileSync(join(root, path), "utf8");
const meta = metadata(source, path);
return variants(meta).map((variant: string) => ({
id: `${path}#${variant}`, path, variant, source, features: meta.features,
id: `${path}#${variant}`, path, variant, source, features: meta.features, negative: meta.negative,
asyncTest: meta.flags.includes("async"),
exclusion: exclusion(source, meta, variant),
}));
});
@@ -82,11 +83,14 @@ All other variants remain visible as exclusions; negative errors never count as
for (;;) {
const item = shard[next++];
if (!item) return;
const outcome = item.exclusion
const raw = item.exclusion
? { status: "excluded", reason: item.exclusion }
: await runSource(item.source, {
backend, sanitize: process.env.SCRIPTC_SAN === "1", compileTimeoutMs, runtimeTimeoutMs, keep: values.keep,
backend, sanitize: process.env.SCRIPTC_SAN === "1", compileTimeoutMs, runtimeTimeoutMs, keep: values.keep, asyncTest: item.asyncTest,
});
const outcome = item.negative?.phase === "parse" && matchesParseNegative(raw, item.source, item.variant)
? { status: "pass", phase: "compile" }
: raw;
const result = { id: item.id, path: item.path, variant: item.variant, features: item.features, ...outcome };
if (journalPath) appendFileSync(journalPath, `${JSON.stringify(result)}\n`);
results.push(result);
@@ -99,8 +103,8 @@ All other variants remain visible as exclusions; negative errors never count as
const report = {
schema: "scriptc.test262.v1",
revision: pin.commit, snapshotSha256: pin.snapshotSha256,
profile: "static-strict-scalar-adapter-v1",
harnessSha256: sha256(readFileSync(join(directory, "harness.ts"))),
profile: "static-adapted-v2",
harnessSha256: sha256(harnessSource + assertThrowsSource),
dynamic: false, requestedBackend: backend, sanitize: process.env.SCRIPTC_SAN === "1",
host: `${process.platform}-${process.arch}`, node: process.version,
compilerVersion: JSON.parse(readFileSync(join(directory, "../../packages/compiler/package.json"), "utf8")).version,
+45 -6
View File
@@ -10,6 +10,7 @@ export const vendorRoot = join(directory, "vendor");
export const pin = JSON.parse(readFileSync(join(directory, "upstream.json"), "utf8"));
export const completion = "__scriptc_test262_complete__";
export const harnessSource = readFileSync(join(directory, "harness.ts"), "utf8");
export const assertThrowsSource = readFileSync(join(directory, "assert-throws.js"), "utf8");
export const expectations = JSON.parse(readFileSync(join(directory, "expectations.json"), "utf8"));
export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -87,15 +88,20 @@ export function variants(meta) {
// conservative about observable global-script semantics and helper reflection.
// Exclusions are runner limitations, never implementation support claims.
export function exclusion(source, meta, variant) {
if (meta.negative) {
if (meta.negative.phase !== "parse" || meta.negative.type !== "SyntaxError" || !parseDiagnostics(source, variant).length) {
return `negative-phase:${meta.negative.phase}`;
}
return undefined;
}
if (variant !== "strict") return `execution:${variant}`;
if (meta.negative) return `negative-phase:${meta.negative.phase}`;
if (meta.flags.includes("async")) return "execution:async";
if (meta.flags.some((flag) => flag.startsWith("CanBlock"))) return "host:agents";
const unsupportedIncludes = meta.includes.filter((name) => name !== "compareArray.js");
if (unsupportedIncludes.length) return `harness-includes:${unsupportedIncludes.join(",")}`;
const sf = ts.createSourceFile("test.js", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
let reason;
const forbidden = new Set(["$262", "$DONE", "$DONOTEVALUATE", "globalThis", "eval", "Function", "print", "process", "require", "arguments"]);
if (meta.flags.includes("async")) forbidden.delete("$DONE");
const visit = (node) => {
if (reason) return;
if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.ImportKeyword || ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
@@ -107,8 +113,13 @@ export function exclusion(source, meta, variant) {
if (ts.isCallExpression(parent) && parent.expression === node) {
// assert(value, message)
} else if (ts.isPropertyAccessExpression(parent) && parent.expression === node &&
["sameValue", "notSameValue", "compareArray"].includes(parent.name.text) &&
ts.isCallExpression(parent.parent) && parent.parent.expression === parent) {
ts.isCallExpression(parent.parent) && parent.parent.expression === parent &&
(["sameValue", "notSameValue", "compareArray"].includes(parent.name.text) ||
(parent.name.text === "throws" &&
parent.parent.arguments[0] !== undefined &&
ts.isIdentifier(parent.parent.arguments[0]) &&
["Error", "TypeError", "RangeError", "SyntaxError"].includes(parent.parent.arguments[0].text)))
) {
// Supported assertion calls; aliases, mutations, and reflection stay out.
} else reason = "harness:assert-surface";
} else if (meta.includes.includes("compareArray.js") && ts.isIdentifier(node) && node.text === "compareArray" &&
@@ -123,8 +134,36 @@ export function exclusion(source, meta, variant) {
return reason;
}
export function prepare(source) {
return `"use strict";\nimport { assert, Test262Error } from "./harness.ts";\n${source}\n;console.log(${JSON.stringify(completion)});\n`;
export function prepare(source, asyncTest = false, marker = completion) {
const names = asyncTest ? "assert, Test262Error, $DONE" : "assert, Test262Error";
const end = asyncTest ? "" : `;console.log(${JSON.stringify(marker)});\n`;
return `"use strict";\nimport { ${names} } from "./harness.ts";\n${source}\n${end}`;
}
function parseDiagnostics(source, variant) {
if (variant !== "strict" && variant !== "sloppy") return [];
const prepared = prepare(source);
const sourceStart = prepared.indexOf(source);
const sourceEnd = sourceStart + source.length;
const file = ts.createSourceFile("main.js", prepared, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
const raw = variant === "sloppy"
? ts.createSourceFile("test.js", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS).parseDiagnostics
: [];
return file.parseDiagnostics.filter((diagnostic) =>
diagnostic.start >= sourceStart && diagnostic.start < sourceEnd &&
(variant === "strict" || raw.some((item) =>
item.start === diagnostic.start - sourceStart &&
ts.flattenDiagnosticMessageText(item.messageText, "\n") ===
ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"))));
}
export function matchesParseNegative(outcome, source, variant = "strict") {
if (outcome.status !== "compile-refusal") return false;
return parseDiagnostics(source, variant).some((parsed) => outcome.diagnostics?.some((reported) =>
reported.code === "SC0001" &&
reported.loc?.file?.endsWith("/main.js") &&
reported.loc.start === parsed.start &&
reported.message === ts.flattenDiagnosticMessageText(parsed.messageText, "\n")));
}
export function summarize(results) {