@scriptc/compiler 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/ambient/package.json +3 -0
- package/ambient/scriptc-node-fallback.d.ts +3001 -0
- package/ambient/scriptc-overrides.d.ts +95 -0
- package/ambient/scriptc.d.ts +74 -0
- package/dist/backend/cc.d.ts +169 -0
- package/dist/backend/cc.js +897 -0
- package/dist/backend/cc.js.map +1 -0
- package/dist/backend/emission/emit-async.d.ts +145 -0
- package/dist/backend/emission/emit-async.js +996 -0
- package/dist/backend/emission/emit-async.js.map +1 -0
- package/dist/backend/emission/emit-exprs.d.ts +3 -0
- package/dist/backend/emission/emit-exprs.js +5949 -0
- package/dist/backend/emission/emit-exprs.js.map +1 -0
- package/dist/backend/emission/emit-island.d.ts +45 -0
- package/dist/backend/emission/emit-island.js +271 -0
- package/dist/backend/emission/emit-island.js.map +1 -0
- package/dist/backend/emission/emit-shapes.d.ts +142 -0
- package/dist/backend/emission/emit-shapes.js +575 -0
- package/dist/backend/emission/emit-shapes.js.map +1 -0
- package/dist/backend/emission/emit-stmts.d.ts +78 -0
- package/dist/backend/emission/emit-stmts.js +960 -0
- package/dist/backend/emission/emit-stmts.js.map +1 -0
- package/dist/backend/emission/emit-types.d.ts +65 -0
- package/dist/backend/emission/emit-types.js +652 -0
- package/dist/backend/emission/emit-types.js.map +1 -0
- package/dist/backend/emission/emit-walkers.d.ts +154 -0
- package/dist/backend/emission/emit-walkers.js +1587 -0
- package/dist/backend/emission/emit-walkers.js.map +1 -0
- package/dist/backend/emission/emitter.d.ts +452 -0
- package/dist/backend/emission/emitter.js +1249 -0
- package/dist/backend/emission/emitter.js.map +1 -0
- package/dist/backend/emission/may-throw.d.ts +15 -0
- package/dist/backend/emission/may-throw.js +250 -0
- package/dist/backend/emission/may-throw.js.map +1 -0
- package/dist/backend/llvm/blocks.d.ts +21 -0
- package/dist/backend/llvm/blocks.js +68 -0
- package/dist/backend/llvm/blocks.js.map +1 -0
- package/dist/backend/llvm/classes.d.ts +84 -0
- package/dist/backend/llvm/classes.js +438 -0
- package/dist/backend/llvm/classes.js.map +1 -0
- package/dist/backend/llvm/dyn.d.ts +123 -0
- package/dist/backend/llvm/dyn.js +2624 -0
- package/dist/backend/llvm/dyn.js.map +1 -0
- package/dist/backend/llvm/emitter.d.ts +3 -0
- package/dist/backend/llvm/emitter.js +10102 -0
- package/dist/backend/llvm/emitter.js.map +1 -0
- package/dist/backend/llvm/shapes.d.ts +78 -0
- package/dist/backend/llvm/shapes.js +754 -0
- package/dist/backend/llvm/shapes.js.map +1 -0
- package/dist/backend/llvm/unsupported.d.ts +6 -0
- package/dist/backend/llvm/unsupported.js +12 -0
- package/dist/backend/llvm/unsupported.js.map +1 -0
- package/dist/backend/llvm/walkers.d.ts +58 -0
- package/dist/backend/llvm/walkers.js +793 -0
- package/dist/backend/llvm/walkers.js.map +1 -0
- package/dist/backend/mangle.d.ts +124 -0
- package/dist/backend/mangle.js +232 -0
- package/dist/backend/mangle.js.map +1 -0
- package/dist/coverage/report.d.ts +65 -0
- package/dist/coverage/report.js +238 -0
- package/dist/coverage/report.js.map +1 -0
- package/dist/diagnostics/diagnostic.d.ts +140 -0
- package/dist/diagnostics/diagnostic.js +458 -0
- package/dist/diagnostics/diagnostic.js.map +1 -0
- package/dist/diagnostics/render.d.ts +11 -0
- package/dist/diagnostics/render.js +58 -0
- package/dist/diagnostics/render.js.map +1 -0
- package/dist/frontend/cjs-lexer.d.ts +20 -0
- package/dist/frontend/cjs-lexer.js +813 -0
- package/dist/frontend/cjs-lexer.js.map +1 -0
- package/dist/frontend/lowering/http2-constants.d.ts +1 -0
- package/dist/frontend/lowering/http2-constants.js +251 -0
- package/dist/frontend/lowering/http2-constants.js.map +1 -0
- package/dist/frontend/lowering/lib-boundary.d.ts +6 -0
- package/dist/frontend/lowering/lib-boundary.js +143 -0
- package/dist/frontend/lowering/lib-boundary.js.map +1 -0
- package/dist/frontend/lowering/lower-assert.d.ts +18 -0
- package/dist/frontend/lowering/lower-assert.js +1269 -0
- package/dist/frontend/lowering/lower-assert.js.map +1 -0
- package/dist/frontend/lowering/lower-builtins.d.ts +511 -0
- package/dist/frontend/lowering/lower-builtins.js +5249 -0
- package/dist/frontend/lowering/lower-builtins.js.map +1 -0
- package/dist/frontend/lowering/lower-calls.d.ts +581 -0
- package/dist/frontend/lowering/lower-calls.js +6286 -0
- package/dist/frontend/lowering/lower-calls.js.map +1 -0
- package/dist/frontend/lowering/lower-classes.d.ts +727 -0
- package/dist/frontend/lowering/lower-classes.js +4701 -0
- package/dist/frontend/lowering/lower-classes.js.map +1 -0
- package/dist/frontend/lowering/lower-comptime.d.ts +53 -0
- package/dist/frontend/lowering/lower-comptime.js +244 -0
- package/dist/frontend/lowering/lower-comptime.js.map +1 -0
- package/dist/frontend/lowering/lower-containers.d.ts +486 -0
- package/dist/frontend/lowering/lower-containers.js +6345 -0
- package/dist/frontend/lowering/lower-containers.js.map +1 -0
- package/dist/frontend/lowering/lower-dgram.d.ts +20 -0
- package/dist/frontend/lowering/lower-dgram.js +319 -0
- package/dist/frontend/lowering/lower-dgram.js.map +1 -0
- package/dist/frontend/lowering/lower-emitter.d.ts +10 -0
- package/dist/frontend/lowering/lower-emitter.js +688 -0
- package/dist/frontend/lowering/lower-emitter.js.map +1 -0
- package/dist/frontend/lowering/lower-enums.d.ts +22 -0
- package/dist/frontend/lowering/lower-enums.js +235 -0
- package/dist/frontend/lowering/lower-enums.js.map +1 -0
- package/dist/frontend/lowering/lower-expando.d.ts +35 -0
- package/dist/frontend/lowering/lower-expando.js +276 -0
- package/dist/frontend/lowering/lower-expando.js.map +1 -0
- package/dist/frontend/lowering/lower-exprs.d.ts +524 -0
- package/dist/frontend/lowering/lower-exprs.js +8111 -0
- package/dist/frontend/lowering/lower-exprs.js.map +1 -0
- package/dist/frontend/lowering/lower-generators.d.ts +50 -0
- package/dist/frontend/lowering/lower-generators.js +421 -0
- package/dist/frontend/lowering/lower-generators.js.map +1 -0
- package/dist/frontend/lowering/lower-inspect.d.ts +20 -0
- package/dist/frontend/lowering/lower-inspect.js +1074 -0
- package/dist/frontend/lowering/lower-inspect.js.map +1 -0
- package/dist/frontend/lowering/lower-island.d.ts +108 -0
- package/dist/frontend/lowering/lower-island.js +650 -0
- package/dist/frontend/lowering/lower-island.js.map +1 -0
- package/dist/frontend/lowering/lower-mixins.d.ts +96 -0
- package/dist/frontend/lowering/lower-mixins.js +534 -0
- package/dist/frontend/lowering/lower-mixins.js.map +1 -0
- package/dist/frontend/lowering/lower-modules.d.ts +135 -0
- package/dist/frontend/lowering/lower-modules.js +1613 -0
- package/dist/frontend/lowering/lower-modules.js.map +1 -0
- package/dist/frontend/lowering/lower-namespaces.d.ts +168 -0
- package/dist/frontend/lowering/lower-namespaces.js +706 -0
- package/dist/frontend/lowering/lower-namespaces.js.map +1 -0
- package/dist/frontend/lowering/lower-server.d.ts +90 -0
- package/dist/frontend/lowering/lower-server.js +3471 -0
- package/dist/frontend/lowering/lower-server.js.map +1 -0
- package/dist/frontend/lowering/lower-stmts.d.ts +361 -0
- package/dist/frontend/lowering/lower-stmts.js +5341 -0
- package/dist/frontend/lowering/lower-stmts.js.map +1 -0
- package/dist/frontend/lowering/lower-stream.d.ts +88 -0
- package/dist/frontend/lowering/lower-stream.js +1533 -0
- package/dist/frontend/lowering/lower-stream.js.map +1 -0
- package/dist/frontend/lowering/lower-test.d.ts +27 -0
- package/dist/frontend/lowering/lower-test.js +353 -0
- package/dist/frontend/lowering/lower-test.js.map +1 -0
- package/dist/frontend/lowering/lowerer.d.ts +1748 -0
- package/dist/frontend/lowering/lowerer.js +6257 -0
- package/dist/frontend/lowering/lowerer.js.map +1 -0
- package/dist/frontend/lowering/surfaces.d.ts +272 -0
- package/dist/frontend/lowering/surfaces.js +1096 -0
- package/dist/frontend/lowering/surfaces.js.map +1 -0
- package/dist/frontend/npm-static.d.ts +55 -0
- package/dist/frontend/npm-static.js +300 -0
- package/dist/frontend/npm-static.js.map +1 -0
- package/dist/frontend/npm.d.ts +330 -0
- package/dist/frontend/npm.js +1259 -0
- package/dist/frontend/npm.js.map +1 -0
- package/dist/frontend/program.d.ts +186 -0
- package/dist/frontend/program.js +2255 -0
- package/dist/frontend/program.js.map +1 -0
- package/dist/frontend/provenance-registry.d.ts +48 -0
- package/dist/frontend/provenance-registry.js +87 -0
- package/dist/frontend/provenance-registry.js.map +1 -0
- package/dist/frontend/provenance.d.ts +6 -0
- package/dist/frontend/provenance.js +459 -0
- package/dist/frontend/provenance.js.map +1 -0
- package/dist/frontend/resolve.d.ts +56 -0
- package/dist/frontend/resolve.js +682 -0
- package/dist/frontend/resolve.js.map +1 -0
- package/dist/frontend/shared.d.ts +67 -0
- package/dist/frontend/shared.js +241 -0
- package/dist/frontend/shared.js.map +1 -0
- package/dist/frontend/ts7/adapter.d.ts +7 -0
- package/dist/frontend/ts7/adapter.js +54 -0
- package/dist/frontend/ts7/adapter.js.map +1 -0
- package/dist/frontend/ts7/ast.d.ts +50 -0
- package/dist/frontend/ts7/ast.js +211 -0
- package/dist/frontend/ts7/ast.js.map +1 -0
- package/dist/frontend/ts7/census-check.d.ts +1 -0
- package/dist/frontend/ts7/census-check.js +12 -0
- package/dist/frontend/ts7/census-check.js.map +1 -0
- package/dist/frontend/ts7/checker.d.ts +140 -0
- package/dist/frontend/ts7/checker.js +544 -0
- package/dist/frontend/ts7/checker.js.map +1 -0
- package/dist/frontend/ts7/enums.d.ts +18 -0
- package/dist/frontend/ts7/enums.js +43 -0
- package/dist/frontend/ts7/enums.js.map +1 -0
- package/dist/frontend/ts7/program.d.ts +99 -0
- package/dist/frontend/ts7/program.js +278 -0
- package/dist/frontend/ts7/program.js.map +1 -0
- package/dist/frontend/ts7/world-check.d.ts +3 -0
- package/dist/frontend/ts7/world-check.js +34 -0
- package/dist/frontend/ts7/world-check.js.map +1 -0
- package/dist/frontend/types.d.ts +205 -0
- package/dist/frontend/types.js +2485 -0
- package/dist/frontend/types.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +429 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/nodes.d.ts +4334 -0
- package/dist/ir/nodes.js +1906 -0
- package/dist/ir/nodes.js.map +1 -0
- package/dist/ir/serialize.d.ts +4 -0
- package/dist/ir/serialize.js +34 -0
- package/dist/ir/serialize.js.map +1 -0
- package/dist/ir/validate.d.ts +36 -0
- package/dist/ir/validate.js +4885 -0
- package/dist/ir/validate.js.map +1 -0
- package/package.json +30 -6
- package/README.md +0 -3
- package/index.js +0 -2
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
/* The node:assert lowering (a spoke module like lower-dgram.ts): the
|
|
2
|
+
* callable module form (`assert(x)` through a default import or a CJS
|
|
3
|
+
* require binding), ok, the strict/deep equality quartet, fail, throws,
|
|
4
|
+
* and match/doesNotMatch — for BOTH the "assert" and "assert/strict"
|
|
5
|
+
* modules ("assert/strict" additionally binds the loose NAMES to the
|
|
6
|
+
* strict comparisons: equal IS strictEqual, exactly Node's aliasing).
|
|
7
|
+
*
|
|
8
|
+
* Failures throw Node's AssertionError (a runtime %Error whose name is
|
|
9
|
+
* "AssertionError" and whose code slot is "ERR_ASSERTION"), so the
|
|
10
|
+
* catch-side reads (`instanceof Error`, `.name`, `.message`, `.code`)
|
|
11
|
+
* answer exactly like Node's. Generated messages are Node-exact for the
|
|
12
|
+
* scalar comparisons (the runtime reproduces assertion_error.js's scalar
|
|
13
|
+
* forms; scr_assert.c's design note lists the divergences); assert.ok's
|
|
14
|
+
* source-text message bakes at compile time — the frontend HAS the source
|
|
15
|
+
* where Node re-reads the file at runtime.
|
|
16
|
+
*
|
|
17
|
+
* Deep equality is honest, never approximate: deepStrictEqual over
|
|
18
|
+
* composite values synthesizes a per-type structural comparison (Node's
|
|
19
|
+
* strict rules — Object.is for numbers, per-element arrays, per-field
|
|
20
|
+
* records, SameValueZero-keyed Maps/Sets with deep values), and any type
|
|
21
|
+
* it cannot compare faithfully (class instances, typed arrays, index-
|
|
22
|
+
* signature records, dyn/unknown) fences at the call site instead of
|
|
23
|
+
* miscomparing. */
|
|
24
|
+
import * as ts from "../ts7/adapter.js";
|
|
25
|
+
import { jsFuncNameOf, own } from "./lowerer.js";
|
|
26
|
+
import { NARROW_FIRST } from "./surfaces.js";
|
|
27
|
+
import { BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, REGEX, RUNTIME_ERROR_CLASSES, STRING, VOID, isUnitType, typeEquals, typeKey } from "../../ir/nodes.js";
|
|
28
|
+
/** node:assert/strict binds the loose NAMES to the strict comparisons —
|
|
29
|
+
* Node's own aliasing (`strict.equal === assert.strictEqual`). */
|
|
30
|
+
const STRICT_ALIASES = {
|
|
31
|
+
equal: "strictEqual",
|
|
32
|
+
notEqual: "notStrictEqual",
|
|
33
|
+
deepEqual: "deepStrictEqual",
|
|
34
|
+
notDeepEqual: "notDeepStrictEqual",
|
|
35
|
+
};
|
|
36
|
+
/** The canonical member the two modules share ("assert/strict".equal →
|
|
37
|
+
* strictEqual); "assert" members pass through unchanged. */
|
|
38
|
+
function canonicalAssertMember(module, member) {
|
|
39
|
+
if (module === "assert/strict")
|
|
40
|
+
return own(STRICT_ALIASES, member) ?? member;
|
|
41
|
+
return member;
|
|
42
|
+
}
|
|
43
|
+
/** VOID/never results are usable as statements and concise arrow bodies
|
|
44
|
+
* only (the lower-dgram stance): nothing downstream can consume them. */
|
|
45
|
+
function requireStatementPosition(L, call, what) {
|
|
46
|
+
if (ts.isExpressionStatement(call.parent) || ts.isArrowFunction(call.parent))
|
|
47
|
+
return;
|
|
48
|
+
L.noLowering(`using the result of ${what}`, call, "the result is void — call it as its own statement");
|
|
49
|
+
}
|
|
50
|
+
/** The optional trailing message argument: Node accepts a string or an
|
|
51
|
+
* Error (throwing the Error itself) — the STRING form lowers; a literal
|
|
52
|
+
* `undefined` counts as omitted (Node's `message == null` test). Returns
|
|
53
|
+
* the libCall's (msg, hasMsg) pair — a "" dummy when omitted, so the
|
|
54
|
+
* runtime can distinguish an omitted message from an empty one. */
|
|
55
|
+
function lowerMessageArg(L, node, loc) {
|
|
56
|
+
const flag = (value) => ({ kind: "boolLit", value, type: BOOL, loc });
|
|
57
|
+
if (!node || (ts.isIdentifier(node) && node.text === "undefined")) {
|
|
58
|
+
return { msg: { kind: "strLit", value: "", type: STRING, loc }, hasMsg: flag(false) };
|
|
59
|
+
}
|
|
60
|
+
const msg = L.lowerExpr(node);
|
|
61
|
+
if (msg.type.kind !== "string") {
|
|
62
|
+
L.noLowering(`assert messages of type '${L.fmt(msg.type)}'`, node, "only string messages lower (Node also accepts an Error to throw — construct and throw it directly)");
|
|
63
|
+
}
|
|
64
|
+
return { msg, hasMsg: flag(true) };
|
|
65
|
+
}
|
|
66
|
+
/** The module-function dispatch — the spoke's entry, called from both the
|
|
67
|
+
* named-import and namespace/default-import call paths. Null for other
|
|
68
|
+
* modules and for members with no lowering (the module tables' fence
|
|
69
|
+
* takes over, module-qualified with the surfaces.ts hints). */
|
|
70
|
+
export function lowerAssertModuleCall(L, expr, bi, loc) {
|
|
71
|
+
if (bi.module !== "assert" && bi.module !== "assert/strict")
|
|
72
|
+
return null;
|
|
73
|
+
switch (canonicalAssertMember(bi.module, bi.member)) {
|
|
74
|
+
case "ok":
|
|
75
|
+
return lowerAssertOk(L, expr, loc);
|
|
76
|
+
case "strictEqual":
|
|
77
|
+
return lowerAssertEqual(L, expr, loc, false, false);
|
|
78
|
+
case "notStrictEqual":
|
|
79
|
+
return lowerAssertEqual(L, expr, loc, true, false);
|
|
80
|
+
case "deepStrictEqual":
|
|
81
|
+
return lowerAssertEqual(L, expr, loc, false, true);
|
|
82
|
+
case "notDeepStrictEqual":
|
|
83
|
+
return lowerAssertEqual(L, expr, loc, true, true);
|
|
84
|
+
case "fail":
|
|
85
|
+
return lowerAssertFail(L, expr, loc);
|
|
86
|
+
case "match":
|
|
87
|
+
return lowerAssertMatch(L, expr, loc, false);
|
|
88
|
+
case "doesNotMatch":
|
|
89
|
+
return lowerAssertMatch(L, expr, loc, true);
|
|
90
|
+
case "throws":
|
|
91
|
+
return lowerAssertThrows(L, expr, loc);
|
|
92
|
+
case "rejects":
|
|
93
|
+
return lowerAssertRejects(L, expr, loc, false);
|
|
94
|
+
case "doesNotReject":
|
|
95
|
+
return lowerAssertRejects(L, expr, loc, true);
|
|
96
|
+
case "ifError":
|
|
97
|
+
return lowerAssertIfError(L, expr, loc);
|
|
98
|
+
default:
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** The callable-module form: `assert(x)` where the callee identifier IS
|
|
103
|
+
* the assert module binding — a default import (`import assert from
|
|
104
|
+
* "node:assert"`) or the CJS `const assert = require("assert")`. Node's
|
|
105
|
+
* module object is assert.ok itself. A NAMESPACE import binding is not
|
|
106
|
+
* callable in Node (ES namespace objects throw TypeError), so that form
|
|
107
|
+
* fences instead of compiling a crash. Null for non-assert callees. */
|
|
108
|
+
export function lowerAssertDirectCall(L, expr, loc) {
|
|
109
|
+
const callee = expr.expression;
|
|
110
|
+
if (!ts.isIdentifier(callee))
|
|
111
|
+
return null;
|
|
112
|
+
const module = L.builtinNamespaceModuleOf(callee);
|
|
113
|
+
if (module !== "assert" && module !== "assert/strict")
|
|
114
|
+
return null;
|
|
115
|
+
const calleeSym = L.checker.getSymbolAtLocation(callee);
|
|
116
|
+
const decl = calleeSym ? L.checker.declarationsOf(calleeSym)[0] : undefined;
|
|
117
|
+
if (decl && ts.isNamespaceImport(decl)) {
|
|
118
|
+
L.unsupported("SC1013", expr, "calling a module namespace object (Node throws TypeError there — " +
|
|
119
|
+
'use the default import: import assert from "node:assert")');
|
|
120
|
+
}
|
|
121
|
+
return lowerAssertOk(L, expr, loc);
|
|
122
|
+
}
|
|
123
|
+
/** assert(value[, message]) / assert.ok(value[, message]): the truthiness
|
|
124
|
+
* test over any condition-testable type; the generated message carries the
|
|
125
|
+
* call's SOURCE TEXT (Node re-reads the file at runtime — the frontend has
|
|
126
|
+
* the same bytes at compile time). */
|
|
127
|
+
function lowerAssertOk(L, expr, loc) {
|
|
128
|
+
requireStatementPosition(L, expr, expr.expression.getText());
|
|
129
|
+
if (expr.arguments.length < 1 || expr.arguments.length > 2) {
|
|
130
|
+
L.noLowering(`${expr.expression.getText()} with ${expr.arguments.length} arguments`, expr, "the supported forms are assert(value) and assert(value, message)");
|
|
131
|
+
}
|
|
132
|
+
const valueNode = expr.arguments[0];
|
|
133
|
+
const pass = L.ensureBool(L.lowerExpr(valueNode), valueNode);
|
|
134
|
+
const msgNode = expr.arguments[1];
|
|
135
|
+
if (!msgNode || (ts.isIdentifier(msgNode) && msgNode.text === "undefined")) {
|
|
136
|
+
// Node reads the CALLSITE LINE and slices the expression out of it
|
|
137
|
+
// (internal/errors/error_source.js) — a call spanning lines truncates
|
|
138
|
+
// at the first newline, so the compile-time text does too.
|
|
139
|
+
const source = expr.getText().split("\n", 1)[0];
|
|
140
|
+
const generated = `The expression evaluated to a falsy value:\n\n ${source}\n`;
|
|
141
|
+
const msg = { kind: "strLit", value: generated, type: STRING, loc };
|
|
142
|
+
return { kind: "libCall", fn: "assert.ok", args: [pass, msg], type: VOID, loc };
|
|
143
|
+
}
|
|
144
|
+
const { msg } = lowerMessageArg(L, msgNode, loc);
|
|
145
|
+
return { kind: "libCall", fn: "assert.ok", args: [pass, msg], type: VOID, loc };
|
|
146
|
+
}
|
|
147
|
+
/** assert.fail([message]): an unconditional AssertionError — "Failed"
|
|
148
|
+
* when the message is omitted (Node's internal default). Lowers as
|
|
149
|
+
* assert.ok over a false literal. */
|
|
150
|
+
function lowerAssertFail(L, expr, loc) {
|
|
151
|
+
requireStatementPosition(L, expr, "assert.fail");
|
|
152
|
+
if (expr.arguments.length > 1) {
|
|
153
|
+
L.noLowering(`assert.fail with ${expr.arguments.length} arguments`, expr, "the supported form is fail(message?) — the deprecated fail(actual, expected, ...) has no lowering");
|
|
154
|
+
}
|
|
155
|
+
let msg;
|
|
156
|
+
const msgNode = expr.arguments[0];
|
|
157
|
+
if (!msgNode || (ts.isIdentifier(msgNode) && msgNode.text === "undefined")) {
|
|
158
|
+
msg = { kind: "strLit", value: "Failed", type: STRING, loc };
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
msg = lowerMessageArg(L, msgNode, loc).msg;
|
|
162
|
+
}
|
|
163
|
+
const falseLit = { kind: "boolLit", value: false, type: BOOL, loc };
|
|
164
|
+
return { kind: "libCall", fn: "assert.ok", args: [falseLit, msg], type: VOID, loc };
|
|
165
|
+
}
|
|
166
|
+
/** The equality quartet. Scalars (number/string/boolean) compare with
|
|
167
|
+
* Object.is in the runtime (strictEqual and deepStrictEqual agree there —
|
|
168
|
+
* Node's own rule) and generate Node's exact messages; composite
|
|
169
|
+
* deepStrictEqual synthesizes the per-type structural comparison. Objects
|
|
170
|
+
* under strictEqual (reference equality), mixed static types, and unions
|
|
171
|
+
* fence — honestly, never a miscompare. */
|
|
172
|
+
function lowerAssertEqual(L, expr, loc, negated, deep) {
|
|
173
|
+
const surface = `assert.${deep ? (negated ? "notDeepStrictEqual" : "deepStrictEqual") : negated ? "notStrictEqual" : "strictEqual"}`;
|
|
174
|
+
requireStatementPosition(L, expr, surface);
|
|
175
|
+
if (expr.arguments.length < 2 || expr.arguments.length > 3) {
|
|
176
|
+
L.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr);
|
|
177
|
+
}
|
|
178
|
+
// A context-free EMPTY array literal on either side has no element type
|
|
179
|
+
// of its own (`deepStrictEqual(ee.listeners('x'), [])` — the checker
|
|
180
|
+
// says any[]): it adopts the OTHER side's array type. Literals have no
|
|
181
|
+
// side effects, so lowering order is unobservable.
|
|
182
|
+
const aNode = expr.arguments[0];
|
|
183
|
+
const bNode = expr.arguments[1];
|
|
184
|
+
const emptyArrayLit = (n) => ts.isArrayLiteralExpression(n) && n.elements.length === 0;
|
|
185
|
+
const emptyOf = (t) => ({ kind: "arrayLit", elems: [], type: t, loc });
|
|
186
|
+
// A dyn other side adopts number[] — the DOM array a boxed empty
|
|
187
|
+
// literal builds is elementless, so any elem type serves.
|
|
188
|
+
const adoptedEmpty = (other) => other.kind === "array" ? emptyOf(other) : other.kind === "dyn" ? emptyOf({ kind: "array", elem: F64 }) : null;
|
|
189
|
+
let a;
|
|
190
|
+
let b;
|
|
191
|
+
if (emptyArrayLit(aNode) && !emptyArrayLit(bNode)) {
|
|
192
|
+
b = L.lowerExpr(bNode);
|
|
193
|
+
a = adoptedEmpty(b.type) ?? L.lowerExpr(aNode);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
a = L.lowerExpr(aNode);
|
|
197
|
+
b = emptyArrayLit(bNode) ? (adoptedEmpty(a.type) ?? L.lowerExpr(bNode)) : L.lowerExpr(bNode);
|
|
198
|
+
}
|
|
199
|
+
const { msg, hasMsg } = lowerMessageArg(L, expr.arguments[2], loc);
|
|
200
|
+
const flag = (value) => ({ kind: "boolLit", value, type: BOOL, loc });
|
|
201
|
+
// CHECKED-DYNAMIC operands (dyn-vs-dyn, dyn-vs-static): both sides
|
|
202
|
+
// cross into the DOM and one runtime entry answers the whole quartet —
|
|
203
|
+
// SameValue for the strict pair (boxed-closure identity for
|
|
204
|
+
// functions), the structural DOM walk for the deep pair, with
|
|
205
|
+
// assertion_error.js's generated messages (scalar forms byte-exact;
|
|
206
|
+
// composites render compact:false/sorted through the DOM). The static
|
|
207
|
+
// side boxes only when the conversion is HONEST for the operator:
|
|
208
|
+
// scalars and unit literals always; boxable functions (the closure
|
|
209
|
+
// crosses by identity); JSON-safe composites under the deep pair only.
|
|
210
|
+
// Under strictEqual a composite fences instead: crossing the unknown
|
|
211
|
+
// boundary COPIES, so a strict compare against a static composite
|
|
212
|
+
// would answer false where Node can answer true (the value's own
|
|
213
|
+
// earlier crossing) — a miscompare, never lowered. Bytes fence both
|
|
214
|
+
// ways (the DOM copy cannot carry the Buffer/Uint8Array brand the
|
|
215
|
+
// checker type knows); class instances and everything non-convertible
|
|
216
|
+
// keep the fence too.
|
|
217
|
+
if (a.type.kind === "dyn" || b.type.kind === "dyn") {
|
|
218
|
+
const box = (e, node) => {
|
|
219
|
+
if (e.type.kind === "dyn")
|
|
220
|
+
return e;
|
|
221
|
+
const k = e.type.kind;
|
|
222
|
+
const ok = e.kind === "unitLit" ||
|
|
223
|
+
k === "f64" ||
|
|
224
|
+
k === "string" ||
|
|
225
|
+
k === "bool" ||
|
|
226
|
+
(k === "func" && L.dynConvertible(e.type)) ||
|
|
227
|
+
// Runtime handles cross by REFERENCE (identity preserved), so the
|
|
228
|
+
// strict compare is honest: assert.strictEqual(this, server) —
|
|
229
|
+
// the ambient-receiver suite shape. The runtime's deep arm
|
|
230
|
+
// answers same-handle only (its documented stance).
|
|
231
|
+
DYN_HANDLE_KINDS.has(k) ||
|
|
232
|
+
// %Error crosses as the DOM's error encoding ({%error, name,
|
|
233
|
+
// message, code?}) through scr_dyn_from_error's IDENTITY CACHE —
|
|
234
|
+
// one error instance is ONE DOM node however it crosses. Strict
|
|
235
|
+
// compares are therefore honest reference equality (a trace
|
|
236
|
+
// context's stamped .error, a caught dyn, and the static error's
|
|
237
|
+
// own boxing are the same node exactly when Node's objects are the
|
|
238
|
+
// same), and the deep walk compares the encoding's name/message/
|
|
239
|
+
// code — Node's deepStrictEqual over Errors. Island round trips
|
|
240
|
+
// re-encode and lose the identity (SEMANTICS.md).
|
|
241
|
+
(k === "object" && e.type.kind === "object" && e.type.className === "%Error") ||
|
|
242
|
+
(deep && k !== "bytes" && k !== "object" && L.dynConvertible(e.type));
|
|
243
|
+
if (!ok) {
|
|
244
|
+
const hint = !deep && (k === "record" || k === "array" || k === "map" || k === "set" || k === "union")
|
|
245
|
+
? "strictEqual on objects is reference equality, which does not survive the unknown boundary (values copy when they cross) — deepStrictEqual compares structure, or narrow the unknown side first"
|
|
246
|
+
: k === "bytes"
|
|
247
|
+
? "the unknown boundary cannot carry the Buffer-vs-Uint8Array brand Node compares — narrow the unknown side to a typed array and compare those"
|
|
248
|
+
: "the static side must convert into the checked-dynamic DOM (numbers, strings, booleans, null/undefined, boxable functions, or JSON-safe structures under the deep forms) — narrow the unknown side instead";
|
|
249
|
+
L.noLowering(`${surface} of 'unknown' and '${L.fmt(e.type)}' values`, node, hint);
|
|
250
|
+
}
|
|
251
|
+
// A boxed closure takes its best-effort JS name from the source
|
|
252
|
+
// node (the coerceInto convention) — the failure message renders
|
|
253
|
+
// [Function: name] like Node.
|
|
254
|
+
const fnName = k === "func" ? jsFuncNameOf(node) : null;
|
|
255
|
+
return { kind: "dynFrom", value: e, ...(fnName !== null ? { fnName } : {}), type: DYN, loc: e.loc };
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
kind: "libCall",
|
|
259
|
+
fn: "assert.eqDyn",
|
|
260
|
+
args: [box(a, aNode), box(b, bNode), flag(negated), flag(deep), msg, hasMsg],
|
|
261
|
+
type: VOID,
|
|
262
|
+
loc,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const scalarFn = a.type.kind === b.type.kind
|
|
266
|
+
? a.type.kind === "f64"
|
|
267
|
+
? "assert.eqF64"
|
|
268
|
+
: a.type.kind === "string"
|
|
269
|
+
? "assert.eqStr"
|
|
270
|
+
: a.type.kind === "bool"
|
|
271
|
+
? "assert.eqBool"
|
|
272
|
+
: a.type.kind === "symbol"
|
|
273
|
+
? // Symbols are primitives under SameValue: strictEqual IS
|
|
274
|
+
// deepStrictEqual (pointer identity), with v24's
|
|
275
|
+
// "Symbol(desc)" stacked-diff messages.
|
|
276
|
+
"assert.eqSym"
|
|
277
|
+
: null
|
|
278
|
+
: null;
|
|
279
|
+
if (scalarFn) {
|
|
280
|
+
return {
|
|
281
|
+
kind: "libCall",
|
|
282
|
+
fn: scalarFn,
|
|
283
|
+
args: [a, b, flag(negated), flag(deep), msg, hasMsg],
|
|
284
|
+
type: VOID,
|
|
285
|
+
loc,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Typed arrays / Buffers, both sides one static bytes type. strictEqual
|
|
289
|
+
// is reference identity (Object.is on objects — the runtime value IS its
|
|
290
|
+
// pointer); deepStrictEqual is brand + content: Node compares prototypes
|
|
291
|
+
// first (Buffer.from([1]) is never deep-equal to new Uint8Array([1])),
|
|
292
|
+
// and the brand lives in each argument's own checker type (the
|
|
293
|
+
// lower-inspect precedent: the runtime representation is one ScrBytes
|
|
294
|
+
// either way). A checker brand this code cannot classify fences — never
|
|
295
|
+
// a miscompare. The one accepted approximation: a bytes value reaching
|
|
296
|
+
// the call through a binding whose DECLARED constructor differs from the
|
|
297
|
+
// value's runtime one compares by the declared brand (SEMANTICS.md).
|
|
298
|
+
if (a.type.kind === "bytes" && typeEquals(a.type, b.type)) {
|
|
299
|
+
const brandA = bytesBrandOf(L, expr.arguments[0]);
|
|
300
|
+
const brandB = bytesBrandOf(L, expr.arguments[1]);
|
|
301
|
+
if (!deep) {
|
|
302
|
+
// Brands shape only the failure HEADER (same-structure vs
|
|
303
|
+
// reference-equal expectation); an unclassifiable side defers to the
|
|
304
|
+
// content probe.
|
|
305
|
+
const brandsEq = flag(brandA === null || brandB === null || brandA === brandB);
|
|
306
|
+
return {
|
|
307
|
+
kind: "libCall",
|
|
308
|
+
fn: "assert.refEqBytes",
|
|
309
|
+
args: [a, b, flag(negated), brandsEq, msg, hasMsg],
|
|
310
|
+
type: VOID,
|
|
311
|
+
loc,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (brandA === null || brandB === null) {
|
|
315
|
+
L.noLowering(`${surface} of typed-array values whose constructor is not in the static type`, brandA === null ? expr.arguments[0] : expr.arguments[1], "Node's deep equality compares prototypes (a Buffer is never deep-equal to a plain Uint8Array) — annotate the value so its constructor is visible");
|
|
316
|
+
}
|
|
317
|
+
const verdict = {
|
|
318
|
+
kind: "libCall",
|
|
319
|
+
fn: "assert.bytesDeepEq",
|
|
320
|
+
args: [a, b, flag(brandA === brandB)],
|
|
321
|
+
type: BOOL,
|
|
322
|
+
loc,
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
kind: "libCall",
|
|
326
|
+
fn: "assert.deepResult",
|
|
327
|
+
args: [verdict, flag(negated), msg, hasMsg],
|
|
328
|
+
type: VOID,
|
|
329
|
+
loc,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
// Function values (ANY two static signatures — a tuple-typed
|
|
333
|
+
// listeners() element against a prefix-declared handler): strictEqual
|
|
334
|
+
// is reference identity (functions are objects to Object.is), Node's
|
|
335
|
+
// object-comparison headers. The deep forms fall through to the generic
|
|
336
|
+
// deep path below — deep equality over functions IS reference identity
|
|
337
|
+
// (same-typed there; the per-type helper carries it).
|
|
338
|
+
if (!deep && a.type.kind === "func" && b.type.kind === "func") {
|
|
339
|
+
return {
|
|
340
|
+
kind: "libCall",
|
|
341
|
+
fn: "assert.refEqFn",
|
|
342
|
+
args: [a, b, flag(negated), msg, hasMsg],
|
|
343
|
+
type: VOID,
|
|
344
|
+
loc,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (!deep) {
|
|
348
|
+
const hint = a.type.kind === "union" || b.type.kind === "union"
|
|
349
|
+
? `union-typed comparisons need one arm (${NARROW_FIRST})`
|
|
350
|
+
: a.type.kind !== b.type.kind
|
|
351
|
+
? "both sides must share one static scalar type (number, string, or boolean)"
|
|
352
|
+
: "strictEqual on objects is reference equality — deepStrictEqual compares structure";
|
|
353
|
+
L.noLowering(`${surface} of '${L.fmt(a.type)}' and '${L.fmt(b.type)}' values`, expr, hint);
|
|
354
|
+
}
|
|
355
|
+
// Deep equality: identical static types, structurally comparable.
|
|
356
|
+
if (!typeEquals(a.type, b.type)) {
|
|
357
|
+
L.noLowering(`${surface} of '${L.fmt(a.type)}' and '${L.fmt(b.type)}' values`, expr, "deep equality lowers when both sides have the SAME static type — annotate or narrow one side");
|
|
358
|
+
}
|
|
359
|
+
if (isUnitType(a.type)) {
|
|
360
|
+
// undefined vs undefined / null vs null: trivially deep-equal.
|
|
361
|
+
return {
|
|
362
|
+
kind: "libCall",
|
|
363
|
+
fn: "assert.deepResult",
|
|
364
|
+
args: [flag(true), flag(negated), msg, hasMsg],
|
|
365
|
+
type: VOID,
|
|
366
|
+
loc,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const unsupported = deepUnsupportedReason(L, a.type, new Set());
|
|
370
|
+
if (unsupported !== null) {
|
|
371
|
+
L.noLowering(`${surface} of '${L.fmt(a.type)}' values`, expr, unsupported);
|
|
372
|
+
}
|
|
373
|
+
const helper = deepEqHelper(L, a.type, loc);
|
|
374
|
+
const verdict = { kind: "call", callee: helper, args: [a, b], type: BOOL, loc };
|
|
375
|
+
return {
|
|
376
|
+
kind: "libCall",
|
|
377
|
+
fn: "assert.deepResult",
|
|
378
|
+
args: [verdict, flag(negated), msg, hasMsg],
|
|
379
|
+
type: VOID,
|
|
380
|
+
loc,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/** assert.match / assert.doesNotMatch: the regex test with Node's
|
|
384
|
+
* generated message (regex source + getter-ordered flags, the inspected
|
|
385
|
+
* input). */
|
|
386
|
+
function lowerAssertMatch(L, expr, loc, negated) {
|
|
387
|
+
const surface = negated ? "assert.doesNotMatch" : "assert.match";
|
|
388
|
+
requireStatementPosition(L, expr, surface);
|
|
389
|
+
if (expr.arguments.length < 2 || expr.arguments.length > 3) {
|
|
390
|
+
L.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr);
|
|
391
|
+
}
|
|
392
|
+
const s = L.lowerExprExpecting(expr.arguments[0], STRING);
|
|
393
|
+
const re = L.lowerExpr(expr.arguments[1]);
|
|
394
|
+
if (re.type.kind !== "regex") {
|
|
395
|
+
L.noLowering(`${surface} with a non-regex pattern`, expr.arguments[1], "the second argument must be a regular expression");
|
|
396
|
+
}
|
|
397
|
+
const { msg, hasMsg } = lowerMessageArg(L, expr.arguments[2], loc);
|
|
398
|
+
const neg = { kind: "boolLit", value: negated, type: BOOL, loc };
|
|
399
|
+
return { kind: "libCall", fn: "assert.match", args: [s, re, neg, msg, hasMsg], type: VOID, loc };
|
|
400
|
+
}
|
|
401
|
+
const SHAPE_KEY_IDS = { code: 0, message: 1, name: 2 };
|
|
402
|
+
/** inspect renders regex flags in GETTER order (dgimsuvy), not source
|
|
403
|
+
* order — the same reordering scr_regex.c applies at runtime. */
|
|
404
|
+
function regexFlagsGetterOrder(flags) {
|
|
405
|
+
return [..."dgimsuvy"].filter((f) => flags.includes(f)).join("");
|
|
406
|
+
}
|
|
407
|
+
/** Is `className` the runtime %Error root or a class inside its
|
|
408
|
+
* hierarchy (builtin kinds and user subclasses alike)? */
|
|
409
|
+
function inErrorHierarchy(L, className) {
|
|
410
|
+
if (className === "%Error" || RUNTIME_ERROR_CLASSES.has(className))
|
|
411
|
+
return true;
|
|
412
|
+
for (let c = L.classes.get(className)?.base ?? null; c; c = c.base) {
|
|
413
|
+
if (c.def.name === "%Error")
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
/** Classify the expected argument: string → message, regex value → the
|
|
419
|
+
* regex test over String(error), an error-class identifier → instanceof,
|
|
420
|
+
* an object LITERAL over name/message/code (string or regex values) →
|
|
421
|
+
* the per-key shape comparison. Anything else fences honestly. */
|
|
422
|
+
function classifyThrowsExpected(L, surface, node, loc) {
|
|
423
|
+
const expectedT = L.mapTypeOf(L.typeOf(node));
|
|
424
|
+
if (expectedT?.kind === "string") {
|
|
425
|
+
const m = lowerMessageArg(L, node, loc);
|
|
426
|
+
return { form: "message", msg: m.msg, hasMsg: m.hasMsg };
|
|
427
|
+
}
|
|
428
|
+
if (expectedT?.kind === "regex") {
|
|
429
|
+
return { form: "regex", value: L.lowerExpr(node) };
|
|
430
|
+
}
|
|
431
|
+
const sym = ts.isIdentifier(node) ? L.resolveValueSymbol(node) : null;
|
|
432
|
+
const info = (sym && L.builtinErrorInfoOf(sym)) ?? (sym && L.classBySymbol.get(sym)) ?? null;
|
|
433
|
+
// A rebindable decorated name: the runtime expectation is the decoration
|
|
434
|
+
// result's interval, not the declaration's — no static class check.
|
|
435
|
+
if (info?.classDecorators?.valueGlobalId !== undefined) {
|
|
436
|
+
L.unsupported("SC1090", node, "assert.throws/rejects against a decorated class whose decorators may replace it");
|
|
437
|
+
}
|
|
438
|
+
if (info && L.inHierarchy(info)) {
|
|
439
|
+
const rec = RUNTIME_ERROR_CLASSES.get(info.def.name);
|
|
440
|
+
return {
|
|
441
|
+
form: "class",
|
|
442
|
+
className: info.def.name,
|
|
443
|
+
displayName: rec ? rec.lib : info.def.name.replace(/^%/, ""),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
let obj = node;
|
|
447
|
+
while (ts.isParenthesizedExpression(obj))
|
|
448
|
+
obj = obj.expression;
|
|
449
|
+
if (ts.isObjectLiteralExpression(obj)) {
|
|
450
|
+
if (obj.properties.length === 0) {
|
|
451
|
+
L.noLowering(`${surface} with an empty expected object`, obj, "Node rejects this (the error 'may not be an empty object') — name at least one of name/message/code");
|
|
452
|
+
}
|
|
453
|
+
// Duplicate keys keep the LAST value (JS object-literal semantics);
|
|
454
|
+
// property order is preserved for argument evaluation.
|
|
455
|
+
const byId = new Map();
|
|
456
|
+
for (const prop of obj.properties) {
|
|
457
|
+
const named = ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop) ? prop : null;
|
|
458
|
+
if (!named || !(ts.isIdentifier(named.name) || ts.isStringLiteral(named.name))) {
|
|
459
|
+
L.noLowering(`${surface} with this expected-object property form`, prop, "plain `key: value` properties are the lowered form");
|
|
460
|
+
}
|
|
461
|
+
const keyName = named.name.text;
|
|
462
|
+
const id = own(SHAPE_KEY_IDS, keyName);
|
|
463
|
+
if (id === undefined) {
|
|
464
|
+
L.noLowering(`${surface} comparing the '${keyName}' property of the thrown error`, prop, "the static error surface carries name, message, and code — compare other properties in a catch block");
|
|
465
|
+
}
|
|
466
|
+
const valueNode = ts.isPropertyAssignment(named) ? named.initializer : named.name;
|
|
467
|
+
const value = L.lowerExpr(valueNode);
|
|
468
|
+
if (value.type.kind === "string") {
|
|
469
|
+
byId.set(id, { id, kind: "str", value, enameBake: null });
|
|
470
|
+
}
|
|
471
|
+
else if (value.type.kind === "regex") {
|
|
472
|
+
let bake = null;
|
|
473
|
+
if (ts.isRegularExpressionLiteral(valueNode)) {
|
|
474
|
+
const text = valueNode.text;
|
|
475
|
+
const cut = text.lastIndexOf("/");
|
|
476
|
+
bake = `/${text.slice(1, cut)}/${regexFlagsGetterOrder(text.slice(cut + 1))}`;
|
|
477
|
+
}
|
|
478
|
+
byId.set(id, { id, kind: "re", value, enameBake: bake });
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
L.noLowering(`${surface} expected-object values of type '${L.fmt(value.type)}'`, valueNode, "string and regular-expression values are the lowered comparisons");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { form: "shape", keys: [...byId.values()] };
|
|
485
|
+
}
|
|
486
|
+
// An error INSTANCE (`assert.rejects(p, expectedError)` — the
|
|
487
|
+
// tracingChannel suite's shape): the value crosses as a DOM error and
|
|
488
|
+
// the runtime compares per Node's expectsError key walk. A
|
|
489
|
+
// checked-dynamic expected rides directly; a static %Error boxes.
|
|
490
|
+
if (expectedT?.kind === "dyn") {
|
|
491
|
+
const value = L.lowerExpr(node);
|
|
492
|
+
if (value.type.kind === "dyn")
|
|
493
|
+
return { form: "errValue", value };
|
|
494
|
+
}
|
|
495
|
+
if (expectedT?.kind === "object" && expectedT.className === "%Error") {
|
|
496
|
+
const value = L.lowerExpr(node);
|
|
497
|
+
if (value.type.kind === "object") {
|
|
498
|
+
return { form: "errValue", value: { kind: "dynFrom", value, type: DYN, loc } };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
L.noLowering(`${surface} with this expected-error shape`, node, "an error CLASS (Error/TypeError/... or a class extending them), an error instance, a message string, " +
|
|
502
|
+
"a regular expression, or an object literal over name/message/code are the lowered forms");
|
|
503
|
+
}
|
|
504
|
+
/** assert.throws(fn[, expected][, message]): the zero-parameter callback
|
|
505
|
+
* runs inside a synthesized try/catch helper. Bare form: any throw
|
|
506
|
+
* passes, a normal return throws "Missing expected exception." — a
|
|
507
|
+
* STRING second argument is Node's message form. An error-CLASS second
|
|
508
|
+
* argument checks `instanceof` (a non-matching Error throws Node's
|
|
509
|
+
* mismatch AssertionError); a REGEX tests String(error); an object
|
|
510
|
+
* LITERAL compares its name/message/code keys (strings deep, regex
|
|
511
|
+
* values by test) with Node's byte-exact Comparison-diff message. A
|
|
512
|
+
* non-Error thrown value under any expectation propagates
|
|
513
|
+
* (SEMANTICS.md 104 — Node builds an AssertionError from its
|
|
514
|
+
* inspection). */
|
|
515
|
+
function lowerAssertThrows(L, expr, loc) {
|
|
516
|
+
requireStatementPosition(L, expr, "assert.throws");
|
|
517
|
+
if (expr.arguments.length < 1 || expr.arguments.length > 3) {
|
|
518
|
+
L.noLowering(`assert.throws with ${expr.arguments.length} arguments`, expr, "the supported forms are throws(fn), throws(fn, expected), and throws(fn, expected, message)");
|
|
519
|
+
}
|
|
520
|
+
const fn = L.lowerExpr(expr.arguments[0]);
|
|
521
|
+
if (fn.type.kind !== "func" || fn.type.params.length !== 0) {
|
|
522
|
+
L.noLowering("assert.throws with this callback shape", expr.arguments[0], "the callback must be a zero-parameter function");
|
|
523
|
+
}
|
|
524
|
+
const { expected, msg, hasMsg } = throwsArguments(L, "assert.throws", expr, loc);
|
|
525
|
+
const helper = assertThrowsHelper(L, "throws", fn.type, false, expected, loc);
|
|
526
|
+
return { kind: "call", callee: helper, args: [fn, msg, hasMsg, ...expectedArgs(expected)], type: VOID, loc };
|
|
527
|
+
}
|
|
528
|
+
/** assert.rejects / assert.doesNotReject: the async twins. The first
|
|
529
|
+
* argument is a zero-parameter promise-returning function (an async
|
|
530
|
+
* function value — a synchronous throw from a non-async one propagates
|
|
531
|
+
* RAW through the helper's own promise, Node's waitForActual) or a
|
|
532
|
+
* promise value. The call answers Promise<void>: `await` it like Node;
|
|
533
|
+
* an un-awaited failing call enters the unhandled-rejection ledger,
|
|
534
|
+
* also like Node. rejects shares assert.throws's expected forms;
|
|
535
|
+
* doesNotReject rethrows a NON-matching rejection and throws Node's
|
|
536
|
+
* "Got unwanted rejection" AssertionError on a match (regex
|
|
537
|
+
* expectations fence — the runtime predicate is throw-shaped). */
|
|
538
|
+
function lowerAssertRejects(L, expr, loc, doesNot) {
|
|
539
|
+
const surface = doesNot ? "assert.doesNotReject" : "assert.rejects";
|
|
540
|
+
if (expr.arguments.length < 1 || expr.arguments.length > 3) {
|
|
541
|
+
L.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr, `the supported forms are ${doesNot ? "doesNotReject" : "rejects"}(fn), (fn, expected), and (fn, expected, message)`);
|
|
542
|
+
}
|
|
543
|
+
const recv = L.lowerExpr(expr.arguments[0]);
|
|
544
|
+
const recvIsPromise = recv.type.kind === "promise";
|
|
545
|
+
if (!recvIsPromise &&
|
|
546
|
+
(recv.type.kind !== "func" || recv.type.params.length !== 0 || recv.type.ret.kind !== "promise")) {
|
|
547
|
+
L.noLowering(`${surface} with this first argument`, expr.arguments[0], "an async (promise-returning) zero-parameter function or a promise are the lowered forms " +
|
|
548
|
+
"(a bare thenable — an object with a then method — has no lowering: wrap it, e.g. Promise.resolve().then(() => thenable))");
|
|
549
|
+
}
|
|
550
|
+
const { expected, msg, hasMsg } = throwsArguments(L, surface, expr, loc);
|
|
551
|
+
if (doesNot && (expected.form === "shape" || expected.form === "errValue")) {
|
|
552
|
+
// Node itself rejects this form (ERR_INVALID_ARG_TYPE: "expected"
|
|
553
|
+
// must be a function or RegExp) — the fence is the compile-time
|
|
554
|
+
// statement of the same rule.
|
|
555
|
+
L.noLowering(`${surface} with an object-shape expectation`, expr.arguments[1], "Node rejects this form (doesNotReject takes a class or RegExp) — use a class, a regex, or a bare call");
|
|
556
|
+
}
|
|
557
|
+
const helper = assertThrowsHelper(L, doesNot ? "dnr" : "rejects", recv.type, recvIsPromise, expected, loc);
|
|
558
|
+
return {
|
|
559
|
+
kind: "call",
|
|
560
|
+
callee: helper,
|
|
561
|
+
args: [recv, msg, hasMsg, ...expectedArgs(expected)],
|
|
562
|
+
type: { kind: "promise", inner: VOID },
|
|
563
|
+
loc,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/** The shared expected/message argument split (arguments 2 and 3): a
|
|
567
|
+
* string expected IS the message and excludes a third argument (Node
|
|
568
|
+
* throws ERR_INVALID_ARG_TYPE there). */
|
|
569
|
+
function throwsArguments(L, surface, expr, loc) {
|
|
570
|
+
const expectedNode = expr.arguments[1];
|
|
571
|
+
// A literal `undefined` expected counts as omitted (Node reads the
|
|
572
|
+
// message from the THIRD argument in that spelling).
|
|
573
|
+
const expected = expectedNode && !(ts.isIdentifier(expectedNode) && expectedNode.text === "undefined")
|
|
574
|
+
? classifyThrowsExpected(L, surface, expectedNode, loc)
|
|
575
|
+
: { form: "bare" };
|
|
576
|
+
if (expected.form === "message") {
|
|
577
|
+
if (expr.arguments.length === 3) {
|
|
578
|
+
L.noLowering(`${surface} with a string expected argument AND a message`, expr.arguments[2], "Node rejects this form (the string IS the message) — use an expected shape second");
|
|
579
|
+
}
|
|
580
|
+
return { expected, msg: expected.msg, hasMsg: expected.hasMsg };
|
|
581
|
+
}
|
|
582
|
+
const { msg, hasMsg } = lowerMessageArg(L, expr.arguments[2], loc);
|
|
583
|
+
return { expected, msg, hasMsg };
|
|
584
|
+
}
|
|
585
|
+
/** The extra call-site arguments a form carries into its helper, in the
|
|
586
|
+
* helper's own parameter order. */
|
|
587
|
+
function expectedArgs(expected) {
|
|
588
|
+
if (expected.form === "regex")
|
|
589
|
+
return [expected.value];
|
|
590
|
+
if (expected.form === "shape")
|
|
591
|
+
return expected.keys.map((k) => k.value);
|
|
592
|
+
if (expected.form === "errValue")
|
|
593
|
+
return [expected.value];
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
/** The interned per-(mode, receiver-type, expected-signature) helper:
|
|
597
|
+
*
|
|
598
|
+
* %assert.throws.<n>(f, m, hm[, re | k0..kN]) {
|
|
599
|
+
* try { f(); } catch (e) { <per-form checks> }
|
|
600
|
+
* <"Missing expected exception" throw>
|
|
601
|
+
* }
|
|
602
|
+
*
|
|
603
|
+
* rejects/doesNotReject synthesize the ASYNC twin — the callback's
|
|
604
|
+
* promise is created OUTSIDE the try (a sync throw from the callback
|
|
605
|
+
* rejects the helper's own promise raw, Node's waitForActual), then
|
|
606
|
+
* awaited inside it:
|
|
607
|
+
*
|
|
608
|
+
* async %assert.rejects.<n>(f, m, hm, ...) {
|
|
609
|
+
* const pr = f(); // or the promise param itself
|
|
610
|
+
* try { await pr; } catch (e) { <per-form checks, return> }
|
|
611
|
+
* <"Missing expected rejection" throw>
|
|
612
|
+
* }
|
|
613
|
+
*
|
|
614
|
+
* Per-form catch bodies (throws/rejects): bare returns; class returns on
|
|
615
|
+
* instanceof, throws the mismatch AssertionError for other Errors;
|
|
616
|
+
* regex/shape run their runtime checks (which throw on mismatch) and
|
|
617
|
+
* return. doesNotReject inverts: a MATCHING Error throws "Got unwanted
|
|
618
|
+
* rejection", everything else rethrows the original reason. A non-Error
|
|
619
|
+
* thrown value under any expectation propagates (SEMANTICS.md 104). */
|
|
620
|
+
function assertThrowsHelper(L, mode, recvType, recvIsPromise, expected, loc) {
|
|
621
|
+
const sig = expected.form === "class"
|
|
622
|
+
? `class:${expected.className}`
|
|
623
|
+
: expected.form === "regex"
|
|
624
|
+
? "regex"
|
|
625
|
+
: expected.form === "shape"
|
|
626
|
+
? "shape:" + expected.keys.map((k) => `${k.id}${k.kind}:${k.enameBake ?? ""}`).join(",")
|
|
627
|
+
: expected.form === "errValue"
|
|
628
|
+
? "errval"
|
|
629
|
+
: "bare";
|
|
630
|
+
const key = `assert.${mode}:${recvIsPromise ? "p" : "f"}:${typeKey(recvType)}:${sig}`;
|
|
631
|
+
const existing = L.assertHelpers.get(key);
|
|
632
|
+
if (existing)
|
|
633
|
+
return existing;
|
|
634
|
+
const name = `%assert.${mode}.${L.assertHelpers.size}`;
|
|
635
|
+
L.assertHelpers.set(key, name);
|
|
636
|
+
const ref = (localId, type) => ({ kind: "varRef", localId, type, loc });
|
|
637
|
+
const flag = (value) => ({ kind: "boolLit", value, type: BOOL, loc });
|
|
638
|
+
const str = (value) => ({ kind: "strLit", value, type: STRING, loc });
|
|
639
|
+
const num = (value) => ({ kind: "numLit", value, type: F64, loc });
|
|
640
|
+
const caughtRef = () => ref("e.0", CAUGHT);
|
|
641
|
+
const errT = { kind: "object", className: "%Error" };
|
|
642
|
+
const narrowed = () => ({ kind: "caughtNarrow", value: caughtRef(), type: errT, loc });
|
|
643
|
+
const isInstance = (className) => ({
|
|
644
|
+
kind: "caughtTest", value: caughtRef(), test: "instanceof", className, type: BOOL, loc,
|
|
645
|
+
});
|
|
646
|
+
const lib = (fn, args) => ({
|
|
647
|
+
kind: "exprStmt",
|
|
648
|
+
expr: { kind: "libCall", fn, args, type: VOID, loc },
|
|
649
|
+
loc,
|
|
650
|
+
});
|
|
651
|
+
const m = () => ref("m.0", STRING);
|
|
652
|
+
const hm = () => ref("hm.0", BOOL);
|
|
653
|
+
const doReturn = { kind: "return", value: null, loc };
|
|
654
|
+
const rethrow = { kind: "rethrow", localId: "e.0", loc };
|
|
655
|
+
const ifStmt = (cond, then) => ({ kind: "if", cond, then, else_: null, loc });
|
|
656
|
+
const params = [
|
|
657
|
+
{ localId: "f.0", name: recvIsPromise ? "p" : "f", type: recvType },
|
|
658
|
+
{ localId: "m.0", name: "m", type: STRING },
|
|
659
|
+
{ localId: "hm.0", name: "hm", type: BOOL },
|
|
660
|
+
];
|
|
661
|
+
const shapeParamIds = [];
|
|
662
|
+
if (expected.form === "regex")
|
|
663
|
+
params.push({ localId: "re.0", name: "re", type: REGEX });
|
|
664
|
+
if (expected.form === "errValue")
|
|
665
|
+
params.push({ localId: "ev.0", name: "ev", type: DYN });
|
|
666
|
+
if (expected.form === "shape") {
|
|
667
|
+
expected.keys.forEach((k, i) => {
|
|
668
|
+
const localId = `k${i}.0`;
|
|
669
|
+
shapeParamIds.push(localId);
|
|
670
|
+
params.push({ localId, name: `k${i}`, type: k.kind === "str" ? STRING : REGEX });
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const locals = params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false }));
|
|
674
|
+
locals.push({ id: "e.0", name: "e", type: CAUGHT, mutable: false });
|
|
675
|
+
/** shapeBegin + one slot call per key (never throw; shapeEnd consumes
|
|
676
|
+
* the accumulator). */
|
|
677
|
+
const shapeSlotStmts = () => {
|
|
678
|
+
if (expected.form !== "shape")
|
|
679
|
+
return [];
|
|
680
|
+
const stmts = [lib("assert.shapeBegin", [narrowed()])];
|
|
681
|
+
expected.keys.forEach((k, i) => {
|
|
682
|
+
stmts.push(lib(k.kind === "str" ? "assert.shapeStr" : "assert.shapeRe", [
|
|
683
|
+
num(k.id),
|
|
684
|
+
ref(shapeParamIds[i], k.kind === "str" ? STRING : REGEX),
|
|
685
|
+
]));
|
|
686
|
+
});
|
|
687
|
+
return stmts;
|
|
688
|
+
};
|
|
689
|
+
// The ` (${expected.name})` detail of the missing-exception message: a
|
|
690
|
+
// class's display name, a shape's name VALUE (its string param, or the
|
|
691
|
+
// compile-time rendering of a regex-literal name); a non-literal regex
|
|
692
|
+
// name drops the detail (a documented sliver).
|
|
693
|
+
const ename = (() => {
|
|
694
|
+
if (expected.form === "class")
|
|
695
|
+
return { e: str(expected.displayName), has: true };
|
|
696
|
+
if (expected.form === "shape") {
|
|
697
|
+
const i = expected.keys.findIndex((k) => k.id === 2);
|
|
698
|
+
if (i >= 0) {
|
|
699
|
+
const k = expected.keys[i];
|
|
700
|
+
if (k.kind === "str")
|
|
701
|
+
return { e: ref(shapeParamIds[i], STRING), has: true };
|
|
702
|
+
if (k.enameBake !== null)
|
|
703
|
+
return { e: str(k.enameBake), has: true };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return { e: str(""), has: false };
|
|
707
|
+
})();
|
|
708
|
+
let catchBody;
|
|
709
|
+
if (mode === "dnr") {
|
|
710
|
+
switch (expected.form) {
|
|
711
|
+
case "bare":
|
|
712
|
+
case "message":
|
|
713
|
+
catchBody = [
|
|
714
|
+
ifStmt(isInstance("%Error"), [lib("assert.unwantedRejection", [narrowed(), m(), hm()])]),
|
|
715
|
+
rethrow,
|
|
716
|
+
];
|
|
717
|
+
break;
|
|
718
|
+
case "class":
|
|
719
|
+
catchBody = [
|
|
720
|
+
ifStmt(isInstance(expected.className), [
|
|
721
|
+
lib("assert.unwantedRejection", [narrowed(), m(), hm()]),
|
|
722
|
+
]),
|
|
723
|
+
rethrow,
|
|
724
|
+
];
|
|
725
|
+
break;
|
|
726
|
+
case "regex":
|
|
727
|
+
catchBody = [
|
|
728
|
+
ifStmt(isInstance("%Error"), [
|
|
729
|
+
ifStmt({
|
|
730
|
+
kind: "libCall",
|
|
731
|
+
fn: "assert.regexErrTest",
|
|
732
|
+
args: [ref("re.0", REGEX), narrowed()],
|
|
733
|
+
type: BOOL,
|
|
734
|
+
loc,
|
|
735
|
+
}, [lib("assert.unwantedRejection", [narrowed(), m(), hm()])]),
|
|
736
|
+
]),
|
|
737
|
+
rethrow,
|
|
738
|
+
];
|
|
739
|
+
break;
|
|
740
|
+
default:
|
|
741
|
+
// Node itself rejects shape expectations here (ERR_INVALID_ARG_TYPE)
|
|
742
|
+
// — the call site fences before reaching this helper.
|
|
743
|
+
throw new Error("assert helper: doesNotReject shape forms fence at the call site");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
switch (expected.form) {
|
|
748
|
+
case "bare":
|
|
749
|
+
case "message":
|
|
750
|
+
catchBody = [doReturn];
|
|
751
|
+
break;
|
|
752
|
+
case "class":
|
|
753
|
+
catchBody = [
|
|
754
|
+
ifStmt(isInstance(expected.className), [doReturn]),
|
|
755
|
+
ifStmt(isInstance("%Error"), [
|
|
756
|
+
lib("assert.throwsMismatch", [str(expected.displayName), narrowed(), m(), hm()]),
|
|
757
|
+
]),
|
|
758
|
+
// A non-Error thrown value: propagate (SEMANTICS.md 104; the
|
|
759
|
+
// mismatch throw above never falls through — its pending
|
|
760
|
+
// exception unwinds first).
|
|
761
|
+
rethrow,
|
|
762
|
+
];
|
|
763
|
+
break;
|
|
764
|
+
case "regex":
|
|
765
|
+
catchBody = [
|
|
766
|
+
ifStmt(isInstance("%Error"), [
|
|
767
|
+
lib("assert.throwsRegex", [ref("re.0", REGEX), narrowed(), m(), hm()]),
|
|
768
|
+
doReturn,
|
|
769
|
+
]),
|
|
770
|
+
rethrow,
|
|
771
|
+
];
|
|
772
|
+
break;
|
|
773
|
+
case "shape":
|
|
774
|
+
catchBody = [
|
|
775
|
+
ifStmt(isInstance("%Error"), [
|
|
776
|
+
...shapeSlotStmts(),
|
|
777
|
+
lib("assert.shapeEnd", [m(), hm()]),
|
|
778
|
+
doReturn,
|
|
779
|
+
]),
|
|
780
|
+
rethrow,
|
|
781
|
+
];
|
|
782
|
+
break;
|
|
783
|
+
case "errValue":
|
|
784
|
+
// Node's expectsError over an error-instance expected: the
|
|
785
|
+
// caught value crosses as a DOM value (identity-preserving) and
|
|
786
|
+
// the runtime walks the expected's keys — a mismatch throws the
|
|
787
|
+
// deep-equal AssertionError.
|
|
788
|
+
catchBody = [
|
|
789
|
+
lib("assert.expectsErrDyn", [
|
|
790
|
+
{ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc },
|
|
791
|
+
ref("ev.0", DYN),
|
|
792
|
+
m(),
|
|
793
|
+
hm(),
|
|
794
|
+
]),
|
|
795
|
+
doReturn,
|
|
796
|
+
];
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const body = [];
|
|
801
|
+
let tryBody;
|
|
802
|
+
if (mode === "throws") {
|
|
803
|
+
const fnT = recvType;
|
|
804
|
+
tryBody = [
|
|
805
|
+
{
|
|
806
|
+
kind: "exprStmt",
|
|
807
|
+
expr: { kind: "callValue", callee: ref("f.0", fnT), args: [], type: fnT.ret, loc },
|
|
808
|
+
loc,
|
|
809
|
+
},
|
|
810
|
+
];
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
let awaited;
|
|
814
|
+
if (recvIsPromise) {
|
|
815
|
+
awaited = ref("f.0", recvType);
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
const fnT = recvType;
|
|
819
|
+
const promT = fnT.ret;
|
|
820
|
+
locals.push({ id: "pr.0", name: "pr", type: promT, mutable: false });
|
|
821
|
+
body.push({
|
|
822
|
+
kind: "varDecl",
|
|
823
|
+
localId: "pr.0",
|
|
824
|
+
init: { kind: "callValue", callee: ref("f.0", fnT), args: [], type: promT, loc },
|
|
825
|
+
loc,
|
|
826
|
+
});
|
|
827
|
+
awaited = ref("pr.0", promT);
|
|
828
|
+
}
|
|
829
|
+
const inner = awaited.type.inner;
|
|
830
|
+
tryBody = [
|
|
831
|
+
{ kind: "exprStmt", expr: { kind: "awaitExpr", value: awaited, type: inner, loc }, loc },
|
|
832
|
+
];
|
|
833
|
+
}
|
|
834
|
+
body.push({ kind: "tryCatch", tryBody, catchBody, catchLocalId: "e.0", finallyBody: null, loc });
|
|
835
|
+
if (mode !== "dnr") {
|
|
836
|
+
body.push(lib("assert.throwsNone", [flag(mode === "rejects"), ename.e, flag(ename.has), m(), hm()]));
|
|
837
|
+
}
|
|
838
|
+
body.push(doReturn);
|
|
839
|
+
L.liftedFns.push({
|
|
840
|
+
name,
|
|
841
|
+
params,
|
|
842
|
+
returnType: VOID,
|
|
843
|
+
locals,
|
|
844
|
+
body,
|
|
845
|
+
loc,
|
|
846
|
+
...(mode === "throws" ? {} : { async: true }),
|
|
847
|
+
});
|
|
848
|
+
return name;
|
|
849
|
+
}
|
|
850
|
+
/* ── assert.ifError ────────────────────────────────────────────────────── */
|
|
851
|
+
/** The typed runtime entry for one non-union ifError argument type:
|
|
852
|
+
* "unit" for null/undefined (can never throw — the evaluated expression
|
|
853
|
+
* alone suffices), a libCall name for scalars and %Error-hierarchy
|
|
854
|
+
* objects, null for everything else (the caller fences). */
|
|
855
|
+
function ifErrorLibFn(L, t) {
|
|
856
|
+
switch (t.kind) {
|
|
857
|
+
case "undefinedT":
|
|
858
|
+
case "nullT":
|
|
859
|
+
return "unit";
|
|
860
|
+
case "f64":
|
|
861
|
+
return "assert.ifErrorF64";
|
|
862
|
+
case "string":
|
|
863
|
+
return "assert.ifErrorStr";
|
|
864
|
+
case "bool":
|
|
865
|
+
return "assert.ifErrorBool";
|
|
866
|
+
case "object":
|
|
867
|
+
return inErrorHierarchy(L, t.className) ? "assert.ifErrorErr" : null;
|
|
868
|
+
default:
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/** assert.ifError(value): Node throws for ANY value except null and
|
|
873
|
+
* undefined — falsy scalars included — with "ifError got unwanted
|
|
874
|
+
* exception: " + the error's message (its name when the message is
|
|
875
|
+
* empty) or the value's inspection. Unit-typed arguments lower to the
|
|
876
|
+
* evaluated expression alone; scalars and %Error-hierarchy objects
|
|
877
|
+
* throw through the typed runtime entries; unions dispatch per arm in
|
|
878
|
+
* an interned helper. Anything else fences. */
|
|
879
|
+
function lowerAssertIfError(L, expr, loc) {
|
|
880
|
+
requireStatementPosition(L, expr, "assert.ifError");
|
|
881
|
+
if (expr.arguments.length !== 1) {
|
|
882
|
+
L.noLowering(`assert.ifError with ${expr.arguments.length} arguments`, expr, "the supported form is ifError(value)");
|
|
883
|
+
}
|
|
884
|
+
const argNode = expr.arguments[0];
|
|
885
|
+
const argT = L.mapTypeOf(L.typeOf(argNode));
|
|
886
|
+
if (argT && isUnitType(argT)) {
|
|
887
|
+
// null/undefined can never throw, and a unit-typed expression has no
|
|
888
|
+
// standalone runtime value to evaluate — a passing assert.ok is the
|
|
889
|
+
// void-typed no-op.
|
|
890
|
+
return {
|
|
891
|
+
kind: "libCall",
|
|
892
|
+
fn: "assert.ok",
|
|
893
|
+
args: [
|
|
894
|
+
{ kind: "boolLit", value: true, type: BOOL, loc },
|
|
895
|
+
{ kind: "strLit", value: "", type: STRING, loc },
|
|
896
|
+
],
|
|
897
|
+
type: VOID,
|
|
898
|
+
loc,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const v = L.lowerExpr(argNode);
|
|
902
|
+
const fence = (t) => L.noLowering(`assert.ifError over '${L.fmt(t)}' values`, argNode, "numbers, strings, booleans, Errors, null/undefined, and unions of those are the lowered forms");
|
|
903
|
+
const direct = ifErrorLibFn(L, v.type);
|
|
904
|
+
if (direct === "unit") {
|
|
905
|
+
// A unit-typed value that mapped through some other checker spelling:
|
|
906
|
+
// the same no-op (unreachable in practice — the mapTypeOf branch
|
|
907
|
+
// above catches unit checker types).
|
|
908
|
+
return {
|
|
909
|
+
kind: "libCall",
|
|
910
|
+
fn: "assert.ok",
|
|
911
|
+
args: [
|
|
912
|
+
{ kind: "boolLit", value: true, type: BOOL, loc },
|
|
913
|
+
{ kind: "strLit", value: "", type: STRING, loc },
|
|
914
|
+
],
|
|
915
|
+
type: VOID,
|
|
916
|
+
loc,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
if (direct !== null)
|
|
920
|
+
return { kind: "libCall", fn: direct, args: [v], type: VOID, loc };
|
|
921
|
+
// A checked-dynamic argument (test/common's mustSucceed wrapper — the
|
|
922
|
+
// callback's err parameter is untyped): the runtime dispatches over the
|
|
923
|
+
// DOM kind — units pass quietly, %error-marked objects throw with the
|
|
924
|
+
// error's message, everything else with the inspection.
|
|
925
|
+
if (v.type.kind === "dyn") {
|
|
926
|
+
return { kind: "libCall", fn: "assert.ifErrorDyn", args: [v], type: VOID, loc };
|
|
927
|
+
}
|
|
928
|
+
if (v.type.kind !== "union")
|
|
929
|
+
fence(v.type);
|
|
930
|
+
const unionT = v.type;
|
|
931
|
+
const def = L.unions.get(unionT.unionId);
|
|
932
|
+
if (!def)
|
|
933
|
+
fence(v.type);
|
|
934
|
+
for (const arm of def.arms) {
|
|
935
|
+
if (ifErrorLibFn(L, arm) === null)
|
|
936
|
+
fence(arm);
|
|
937
|
+
}
|
|
938
|
+
const helper = ifErrorHelper(L, unionT, loc);
|
|
939
|
+
return { kind: "call", callee: helper, args: [v], type: VOID, loc };
|
|
940
|
+
}
|
|
941
|
+
/** The interned per-union ifError helper `%assert.iferr.<n>(v): void` —
|
|
942
|
+
* one tag test per arm: unit arms return quietly, every other arm
|
|
943
|
+
* narrows and throws through its typed runtime entry. */
|
|
944
|
+
function ifErrorHelper(L, t, loc) {
|
|
945
|
+
const key = `assert.iferr:${typeKey(t)}`;
|
|
946
|
+
const existing = L.assertHelpers.get(key);
|
|
947
|
+
if (existing)
|
|
948
|
+
return existing;
|
|
949
|
+
const name = `%assert.iferr.${L.assertHelpers.size}`;
|
|
950
|
+
L.assertHelpers.set(key, name);
|
|
951
|
+
const def = L.unions.get(t.unionId);
|
|
952
|
+
if (!def)
|
|
953
|
+
throw new Error(`assert.ifError over unknown union ${t.unionId}`);
|
|
954
|
+
const v = () => ({ kind: "varRef", localId: "v.0", type: t, loc });
|
|
955
|
+
const doReturn = { kind: "return", value: null, loc };
|
|
956
|
+
const body = [];
|
|
957
|
+
def.arms.forEach((arm, tag) => {
|
|
958
|
+
const fn = ifErrorLibFn(L, arm);
|
|
959
|
+
if (fn === null)
|
|
960
|
+
throw new Error("assert.ifError helper over an unfenced arm");
|
|
961
|
+
const then = fn === "unit"
|
|
962
|
+
? [doReturn]
|
|
963
|
+
: [
|
|
964
|
+
{
|
|
965
|
+
kind: "exprStmt",
|
|
966
|
+
expr: {
|
|
967
|
+
kind: "libCall",
|
|
968
|
+
fn,
|
|
969
|
+
args: [{ kind: "unionNarrow", unionId: t.unionId, tag, value: v(), type: arm, loc }],
|
|
970
|
+
type: VOID,
|
|
971
|
+
loc,
|
|
972
|
+
},
|
|
973
|
+
loc,
|
|
974
|
+
},
|
|
975
|
+
doReturn,
|
|
976
|
+
];
|
|
977
|
+
body.push({
|
|
978
|
+
kind: "if",
|
|
979
|
+
cond: { kind: "unionIsTag", unionId: t.unionId, tag, negated: false, value: v(), type: BOOL, loc },
|
|
980
|
+
then,
|
|
981
|
+
else_: null,
|
|
982
|
+
loc,
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
body.push(doReturn); // unreachable: some arm always matches
|
|
986
|
+
L.liftedFns.push({
|
|
987
|
+
name,
|
|
988
|
+
params: [{ localId: "v.0", name: "v", type: t }],
|
|
989
|
+
returnType: VOID,
|
|
990
|
+
locals: [{ id: "v.0", name: "v", type: t, mutable: false }],
|
|
991
|
+
body,
|
|
992
|
+
loc,
|
|
993
|
+
});
|
|
994
|
+
return name;
|
|
995
|
+
}
|
|
996
|
+
/** The TOP-LEVEL checker brand of a bytes-typed argument: the constructor
|
|
997
|
+
* name Node's deep equality compares as the prototype. The runtime
|
|
998
|
+
* representation is one ScrBytes for Buffer AND the typed arrays, so the
|
|
999
|
+
* argument's own checker type is the only carrier (the lower-inspect
|
|
1000
|
+
* precedent). Null when the spelling is not a recognized constructor —
|
|
1001
|
+
* the caller fences. */
|
|
1002
|
+
const BYTES_BRANDS = new Set([
|
|
1003
|
+
"Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array",
|
|
1004
|
+
"Int8Array", "Int16Array", "Int32Array", "Float32Array", "Float64Array",
|
|
1005
|
+
"BigInt64Array", "BigUint64Array", "DataView",
|
|
1006
|
+
]);
|
|
1007
|
+
function bytesBrandOf(L, node) {
|
|
1008
|
+
const tname = L.checker.typeToString(L.checker.getBaseTypeOfLiteralType(L.typeOf(node)));
|
|
1009
|
+
const head = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(tname)?.[0] ?? "";
|
|
1010
|
+
if (head === "Buffer" || head === "NonSharedBuffer")
|
|
1011
|
+
return "Buffer";
|
|
1012
|
+
return BYTES_BRANDS.has(head) ? head : null;
|
|
1013
|
+
}
|
|
1014
|
+
/* ── deep equality (deepStrictEqual / notDeepStrictEqual) ──────────────── */
|
|
1015
|
+
/** Why `t` has no honest deep comparison, or null when it lowers. The
|
|
1016
|
+
* fence names the FIRST unsupported constituent — never a silent
|
|
1017
|
+
* miscompare. Recursive record shapes terminate through `visiting`
|
|
1018
|
+
* (a shape being checked is assumed fine; its own walk decides). */
|
|
1019
|
+
function deepUnsupportedReason(L, t, visiting) {
|
|
1020
|
+
switch (t.kind) {
|
|
1021
|
+
case "f64":
|
|
1022
|
+
case "string":
|
|
1023
|
+
case "bool":
|
|
1024
|
+
case "undefinedT":
|
|
1025
|
+
case "nullT":
|
|
1026
|
+
return null;
|
|
1027
|
+
case "array":
|
|
1028
|
+
return deepUnsupportedReason(L, t.elem, visiting);
|
|
1029
|
+
case "record": {
|
|
1030
|
+
if (visiting.has(t.shapeId))
|
|
1031
|
+
return null;
|
|
1032
|
+
visiting.add(t.shapeId);
|
|
1033
|
+
const shape = L.shapes.get(t.shapeId);
|
|
1034
|
+
if (!shape)
|
|
1035
|
+
return "this record shape has no deep comparison";
|
|
1036
|
+
if (shape.indexValue !== undefined) {
|
|
1037
|
+
return "index-signature records have no deep comparison (their key sets are dynamic)";
|
|
1038
|
+
}
|
|
1039
|
+
for (const f of shape.fields) {
|
|
1040
|
+
const why = deepUnsupportedReason(L, f.type, visiting);
|
|
1041
|
+
if (why !== null)
|
|
1042
|
+
return why;
|
|
1043
|
+
}
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
case "map": {
|
|
1047
|
+
const keyWhy = deepUnsupportedReason(L, t.key, visiting);
|
|
1048
|
+
if (keyWhy !== null)
|
|
1049
|
+
return keyWhy;
|
|
1050
|
+
return deepUnsupportedReason(L, t.value, visiting);
|
|
1051
|
+
}
|
|
1052
|
+
case "set":
|
|
1053
|
+
return t.elem.kind === "f64" || t.elem.kind === "string" || t.elem.kind === "bool"
|
|
1054
|
+
? null
|
|
1055
|
+
: "Sets of non-scalar elements have no deep comparison (Node matches object members structurally)";
|
|
1056
|
+
case "union": {
|
|
1057
|
+
const def = L.unions.get(t.unionId);
|
|
1058
|
+
if (!def)
|
|
1059
|
+
return "this union has no deep comparison";
|
|
1060
|
+
for (const arm of def.arms) {
|
|
1061
|
+
const why = deepUnsupportedReason(L, arm, visiting);
|
|
1062
|
+
if (why !== null)
|
|
1063
|
+
return why;
|
|
1064
|
+
}
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
case "func":
|
|
1068
|
+
// Node's deep equality over functions is reference identity (two
|
|
1069
|
+
// distinct functions are never deep-equal; the same reference is) —
|
|
1070
|
+
// a pointer compare carries it faithfully.
|
|
1071
|
+
return null;
|
|
1072
|
+
case "symbol":
|
|
1073
|
+
// Symbols deep-compare by identity (SameValue primitives) — the
|
|
1074
|
+
// pointer IS the identity.
|
|
1075
|
+
return null;
|
|
1076
|
+
case "bytes":
|
|
1077
|
+
// NESTED bytes (an array element, a record field): the Buffer-vs-
|
|
1078
|
+
// Uint8Array brand Node compares lives in the checker type of a
|
|
1079
|
+
// TOP-LEVEL argument only — no brand survives into shapes.
|
|
1080
|
+
return "typed arrays inside composites have no deep comparison (Node compares constructors, which the static element/field type does not carry)";
|
|
1081
|
+
case "object":
|
|
1082
|
+
return "class instances have no deep comparison (Node compares prototypes and own properties)";
|
|
1083
|
+
default:
|
|
1084
|
+
return `'${L.fmt(t)}' values have no deep comparison`;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/** The interned per-type deep-equality helper `%assert.deq.<n>(a, b):
|
|
1088
|
+
* boolean` — Node's strict deep rules over the static type: Object.is
|
|
1089
|
+
* numbers, byte-equal strings, per-element arrays, per-field records,
|
|
1090
|
+
* tag+payload unions, SameValueZero-keyed Maps with deep values, scalar
|
|
1091
|
+
* Sets by membership. Registered in the cache BEFORE the body builds, so
|
|
1092
|
+
* recursive record shapes call themselves by name. */
|
|
1093
|
+
function deepEqHelper(L, t, loc) {
|
|
1094
|
+
const key = `assert.deq:${typeKey(t)}`;
|
|
1095
|
+
const existing = L.assertHelpers.get(key);
|
|
1096
|
+
if (existing)
|
|
1097
|
+
return existing;
|
|
1098
|
+
const name = `%assert.deq.${L.assertHelpers.size}`;
|
|
1099
|
+
L.assertHelpers.set(key, name);
|
|
1100
|
+
const ref = (localId, type) => ({ kind: "varRef", localId, type, loc });
|
|
1101
|
+
const a = () => ref("a.0", t);
|
|
1102
|
+
const b = () => ref("b.0", t);
|
|
1103
|
+
const num = (value) => ({ kind: "numLit", value, type: F64, loc });
|
|
1104
|
+
const boolLit = (value) => ({ kind: "boolLit", value, type: BOOL, loc });
|
|
1105
|
+
const ret = (value) => ({ kind: "return", value, loc });
|
|
1106
|
+
const retFalse = ret(boolLit(false));
|
|
1107
|
+
const not = (operand) => ({ kind: "unary", op: "!", operand, type: BOOL, loc });
|
|
1108
|
+
const neq = (left, right) => ({ kind: "bin", op: "!==", left, right, type: BOOL, loc });
|
|
1109
|
+
/** if (cond) return false; */
|
|
1110
|
+
const bailIf = (cond) => ({ kind: "if", cond, then: [retFalse], else_: null, loc });
|
|
1111
|
+
/** Deep-compare two same-typed exprs through the per-type helper. */
|
|
1112
|
+
const deq = (elemT, x, y) => isUnitType(elemT)
|
|
1113
|
+
? boolLit(true)
|
|
1114
|
+
: { kind: "call", callee: deepEqHelper(L, elemT, loc), args: [x, y], type: BOOL, loc };
|
|
1115
|
+
const locals = [
|
|
1116
|
+
{ id: "a.0", name: "a", type: t, mutable: false },
|
|
1117
|
+
{ id: "b.0", name: "b", type: t, mutable: false },
|
|
1118
|
+
];
|
|
1119
|
+
let body;
|
|
1120
|
+
switch (t.kind) {
|
|
1121
|
+
case "f64":
|
|
1122
|
+
body = [ret({ kind: "libCall", fn: "assert.sameValue", args: [a(), b()], type: BOOL, loc })];
|
|
1123
|
+
break;
|
|
1124
|
+
case "string":
|
|
1125
|
+
body = [ret({ kind: "strEq", negated: false, left: a(), right: b(), type: BOOL, loc })];
|
|
1126
|
+
break;
|
|
1127
|
+
case "bool":
|
|
1128
|
+
body = [ret({ kind: "bin", op: "===", left: a(), right: b(), type: BOOL, loc })];
|
|
1129
|
+
break;
|
|
1130
|
+
case "func":
|
|
1131
|
+
case "symbol":
|
|
1132
|
+
// Reference identity — Node's deep equality over functions; a
|
|
1133
|
+
// symbol IS its pointer identity.
|
|
1134
|
+
body = [ret({ kind: "bin", op: "===", left: a(), right: b(), type: BOOL, loc })];
|
|
1135
|
+
break;
|
|
1136
|
+
case "array": {
|
|
1137
|
+
const len = (arr) => ({ kind: "arrIntrinsic", method: "length", receiver: arr, args: [], type: F64, loc });
|
|
1138
|
+
const at = (arr, i) => ({ kind: "arrayGet", arr, index: i, type: t.elem, loc });
|
|
1139
|
+
locals.push({ id: "i.0", name: "i", type: F64, mutable: true });
|
|
1140
|
+
body = [
|
|
1141
|
+
bailIf(neq(len(a()), len(b()))),
|
|
1142
|
+
{
|
|
1143
|
+
kind: "for",
|
|
1144
|
+
init: { kind: "varDecl", localId: "i.0", init: num(0), loc },
|
|
1145
|
+
cond: { kind: "bin", op: "<", left: ref("i.0", F64), right: len(a()), type: BOOL, loc },
|
|
1146
|
+
update: { kind: "assign", localId: "i.0", value: { kind: "bin", op: "+", left: ref("i.0", F64), right: num(1), type: F64, loc }, loc },
|
|
1147
|
+
body: [bailIf(not(deq(t.elem, at(a(), ref("i.0", F64)), at(b(), ref("i.0", F64)))))],
|
|
1148
|
+
loc,
|
|
1149
|
+
},
|
|
1150
|
+
ret(boolLit(true)),
|
|
1151
|
+
];
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
case "record": {
|
|
1155
|
+
const shape = L.shapes.get(t.shapeId);
|
|
1156
|
+
if (!shape)
|
|
1157
|
+
throw new Error(`assert deep-equal of unknown shape ${t.shapeId}`);
|
|
1158
|
+
const get = (obj, field, type) => ({ kind: "recordGet", obj, shapeId: t.shapeId, field, type, loc });
|
|
1159
|
+
body = [];
|
|
1160
|
+
for (const f of shape.fields) {
|
|
1161
|
+
if (isUnitType(f.type))
|
|
1162
|
+
continue; // a unit field is equal by type
|
|
1163
|
+
body.push(bailIf(not(deq(f.type, get(a(), f.name, f.type), get(b(), f.name, f.type)))));
|
|
1164
|
+
}
|
|
1165
|
+
body.push(ret(boolLit(true)));
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
case "union": {
|
|
1169
|
+
const def = L.unions.get(t.unionId);
|
|
1170
|
+
if (!def)
|
|
1171
|
+
throw new Error(`assert deep-equal of unknown union ${t.unionId}`);
|
|
1172
|
+
const isTag = (v, tag, negated) => ({ kind: "unionIsTag", unionId: t.unionId, tag, negated, value: v, type: BOOL, loc });
|
|
1173
|
+
const narrow = (v, tag, armT) => ({ kind: "unionNarrow", unionId: t.unionId, tag, value: v, type: armT, loc });
|
|
1174
|
+
body = [];
|
|
1175
|
+
def.arms.forEach((arm, tag) => {
|
|
1176
|
+
body.push({
|
|
1177
|
+
kind: "if",
|
|
1178
|
+
cond: isTag(a(), tag, false),
|
|
1179
|
+
then: [
|
|
1180
|
+
bailIf(isTag(b(), tag, true)),
|
|
1181
|
+
ret(deq(arm, narrow(a(), tag, arm), narrow(b(), tag, arm))),
|
|
1182
|
+
],
|
|
1183
|
+
else_: null,
|
|
1184
|
+
loc,
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
body.push(retFalse); // unreachable: some arm always matches
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
case "map": {
|
|
1191
|
+
const mi = (method, receiver, args, type) => ({ kind: "mapIntrinsic", method, receiver, args, type, loc });
|
|
1192
|
+
const valueT = t.value;
|
|
1193
|
+
const hasUndefArm = valueT.kind === "union" &&
|
|
1194
|
+
(L.unions.get(valueT.unionId)?.arms.some((arm) => arm.kind === "undefinedT") ?? false);
|
|
1195
|
+
const getT = hasUndefArm ? valueT : L.withUndefinedArm(valueT);
|
|
1196
|
+
locals.push({ id: "i.0", name: "i", type: F64, mutable: true }, { id: "k.0", name: "k", type: t.key, mutable: false }, { id: "av.0", name: "av", type: valueT, mutable: false }, { id: "bv.0", name: "bv", type: getT, mutable: false });
|
|
1197
|
+
const i = () => ref("i.0", F64);
|
|
1198
|
+
const k = () => ref("k.0", t.key);
|
|
1199
|
+
// The b-side value: get() answers `V | undefined`; has() already
|
|
1200
|
+
// proved presence, so a plain V narrows through the value arm.
|
|
1201
|
+
const bValue = getT === valueT
|
|
1202
|
+
? ref("bv.0", getT)
|
|
1203
|
+
: {
|
|
1204
|
+
kind: "unionNarrow",
|
|
1205
|
+
unionId: getT.unionId,
|
|
1206
|
+
tag: L.armTag(getT.unionId, valueT),
|
|
1207
|
+
value: ref("bv.0", getT),
|
|
1208
|
+
type: valueT,
|
|
1209
|
+
loc,
|
|
1210
|
+
};
|
|
1211
|
+
body = [
|
|
1212
|
+
bailIf(neq(mi("size", a(), [], F64), mi("size", b(), [], F64))),
|
|
1213
|
+
{
|
|
1214
|
+
kind: "for",
|
|
1215
|
+
init: { kind: "varDecl", localId: "i.0", init: num(0), loc },
|
|
1216
|
+
cond: { kind: "bin", op: "<", left: i(), right: mi("iterCount", a(), [], F64), type: BOOL, loc },
|
|
1217
|
+
update: { kind: "assign", localId: "i.0", value: { kind: "bin", op: "+", left: i(), right: num(1), type: F64, loc }, loc },
|
|
1218
|
+
body: [
|
|
1219
|
+
{ kind: "if", cond: not(mi("iterLive", a(), [i()], BOOL)), then: [{ kind: "continue", loc }], else_: null, loc },
|
|
1220
|
+
{ kind: "varDecl", localId: "k.0", init: mi("iterKey", a(), [i()], t.key), loc },
|
|
1221
|
+
bailIf(not(mi("has", b(), [k()], BOOL))),
|
|
1222
|
+
{ kind: "varDecl", localId: "av.0", init: mi("iterValue", a(), [i()], valueT), loc },
|
|
1223
|
+
{ kind: "varDecl", localId: "bv.0", init: mi("get", b(), [k()], getT), loc },
|
|
1224
|
+
bailIf(not(deq(valueT, ref("av.0", valueT), bValue))),
|
|
1225
|
+
],
|
|
1226
|
+
loc,
|
|
1227
|
+
},
|
|
1228
|
+
ret(boolLit(true)),
|
|
1229
|
+
];
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
case "set": {
|
|
1233
|
+
const si = (method, receiver, args, type) => ({ kind: "setIntrinsic", method, receiver, args, type, loc });
|
|
1234
|
+
locals.push({ id: "i.0", name: "i", type: F64, mutable: true });
|
|
1235
|
+
const i = () => ref("i.0", F64);
|
|
1236
|
+
body = [
|
|
1237
|
+
bailIf(neq(si("size", a(), [], F64), si("size", b(), [], F64))),
|
|
1238
|
+
{
|
|
1239
|
+
kind: "for",
|
|
1240
|
+
init: { kind: "varDecl", localId: "i.0", init: num(0), loc },
|
|
1241
|
+
cond: { kind: "bin", op: "<", left: i(), right: si("iterCount", a(), [], F64), type: BOOL, loc },
|
|
1242
|
+
update: { kind: "assign", localId: "i.0", value: { kind: "bin", op: "+", left: i(), right: num(1), type: F64, loc }, loc },
|
|
1243
|
+
body: [
|
|
1244
|
+
{ kind: "if", cond: not(si("iterLive", a(), [i()], BOOL)), then: [{ kind: "continue", loc }], else_: null, loc },
|
|
1245
|
+
bailIf(not(si("has", b(), [si("iterKey", a(), [i()], t.elem)], BOOL))),
|
|
1246
|
+
],
|
|
1247
|
+
loc,
|
|
1248
|
+
},
|
|
1249
|
+
ret(boolLit(true)),
|
|
1250
|
+
];
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
default:
|
|
1254
|
+
throw new Error(`assert deep-equal helper over unexpected type ${typeKey(t)}`);
|
|
1255
|
+
}
|
|
1256
|
+
L.liftedFns.push({
|
|
1257
|
+
name,
|
|
1258
|
+
params: [
|
|
1259
|
+
{ localId: "a.0", name: "a", type: t },
|
|
1260
|
+
{ localId: "b.0", name: "b", type: t },
|
|
1261
|
+
],
|
|
1262
|
+
returnType: BOOL,
|
|
1263
|
+
locals,
|
|
1264
|
+
body,
|
|
1265
|
+
loc,
|
|
1266
|
+
});
|
|
1267
|
+
return name;
|
|
1268
|
+
}
|
|
1269
|
+
//# sourceMappingURL=lower-assert.js.map
|