From 7c90c8e3a18a3abddc03beee61717f9b1b7b8a44 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 17 Oct 2026 18:23:08 -0500 Subject: [PATCH 001/120] Pool keep-alive sockets in http Agents and use globalAgent for default requests --- .../ambient/scriptc-node-fallback.d.ts | 12 +- .../compiler/src/backend/llvm/lib-shared.ts | 4 +- packages/compiler/src/backend/llvm/lib-tls.ts | 9 +- .../src/frontend/lowering/lower-exprs.ts | 9 + .../src/frontend/lowering/lower-server.ts | 88 ++-- .../compiler/src/frontend/lowering/lowerer.ts | 9 + .../src/frontend/lowering/surfaces.ts | 4 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 14 +- .../corpus/5596-http-agent-keep-alive.ts.json | 6 + .../5597-http-agent-idle-sockets.ts.json | 6 + .../corpus/5598-http-global-agent.ts.json | 6 + packages/runtime/src/scr_http.c | 404 ++++++++++++++++-- packages/runtime/src/scr_http.h | 12 +- packages/runtime/src/scr_http2.h | 6 +- packages/runtime/src/scr_net.c | 8 + packages/runtime/src/scr_net.h | 1 + packages/runtime/src/scr_tls.c | 86 +++- tests/corpus/5564-http-request-signal.ts | 11 +- tests/corpus/5596-http-agent-keep-alive.ts | 109 +++++ tests/corpus/5597-http-agent-idle-sockets.ts | 79 ++++ tests/corpus/5598-http-global-agent.ts | 30 ++ 22 files changed, 800 insertions(+), 114 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5596-http-agent-keep-alive.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5597-http-agent-idle-sockets.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5598-http-global-agent.ts.json create mode 100644 tests/corpus/5596-http-agent-keep-alive.ts create mode 100644 tests/corpus/5597-http-agent-idle-sockets.ts create mode 100644 tests/corpus/5598-http-global-agent.ts diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index d87172f61..3b033027a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -4716,17 +4716,16 @@ declare module "http" { /* The Agent lowers to a checked-dynamic HANDLE: option parsing at the * literal construction site, getName, destroy, the sockets/requests/ * freeSockets snapshots, and REAL maxSockets accounting (over-limit - * requests defer their dial and queue). keepAlive: true throws the - * named pooling fence — this client dials one connection per request - * and closes it with the response, so freeSockets is always - * empty. */ + * requests defer their dial and queue). keepAlive: true keeps finished + * connections in freeSockets for the next request to the same name, + * taken in `scheduling` order. */ export interface AgentOptions { keepAlive?: boolean; keepAliveMsecs?: number; maxSockets?: number; maxFreeSockets?: number; maxTotalSockets?: number; - scheduling?: string; + scheduling?: "fifo" | "lifo"; timeout?: number; port?: number; [option: string]: unknown; @@ -4756,6 +4755,7 @@ declare module "http" { readonly freeSockets: { [key: string]: unknown[] }; readonly requests: { [key: string]: unknown[] }; } + /* Node's keepAlive global agent for requests without an agent option. */ export const globalAgent: Agent; export interface ClientRequest { readonly destroyed: boolean; @@ -4959,6 +4959,8 @@ declare module "https" { import { Server } from "http"; import { Agent, ClientRequest, IncomingMessage, ServerResponse } from "http"; export { Agent, Server }; + /* Node's keepAlive global agent for requests without an agent option. */ + export const globalAgent: Agent; export interface ServerOptions { cert: string | Uint8Array; key: string | Uint8Array; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 4e1ba54ac..0be05ef5f 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1210,9 +1210,11 @@ export const LIB_FN_SYMS: Record = { "http.reqSetTimeout": "scr_http_req_set_timeout_plain", "http.clientDestroy": "scr_http_client_destroy", "http.clientDestroyError": "scr_http_client_destroy_error", + "http.globalAgent": "scr_http_global_agent_value", "http.serverMaxHeaderSize": "scr_http_server_max_header_size", "http.clientMaxHeaderSize": "scr_http_client_max_header_size", - "https.clientServername": "scr_https_client_servername", + "http.clientSignalCheck": "scr_http_client_signal_check", + "https.nextServername": "scr_https_set_next_servername", "http.clientAbort": "scr_http_client_abort", "http.clientAborted": "scr_http_client_aborted", "http.clientDestroyed": "scr_http_client_destroyed", diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index f174248c0..a993ec49a 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -164,13 +164,8 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu ]); B.br(done); B.startBlock(plain); - const insecure = callRuntime(host, "scr_http_request_ex", "ptr", [ - ...a.slice(1, 8), - ...handler, - { type: "i32", name: "80" }, - ptr(), - ptr(), - ]); + // The plain arm is http.request: Node's global agent serves it. + const insecure = callRuntime(host, "scr_http_request", "ptr", [...a.slice(1, 8), ...handler]); B.br(done); B.startBlock(done); const result = B.tmp(); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 08e12518b..1292a4ddb 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1158,6 +1158,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (bi.module === "http" && bi.member === "METHODS") { return { kind: "libCall", fn: "http.methods", args: [], type: arrayOf(STRING), loc }; } + if ((bi.module === "http" || bi.module === "https") && bi.member === "globalAgent") { + return { + kind: "libCall", + fn: "http.globalAgent", + args: [{ kind: "boolLit", value: bi.module === "https", type: BOOL, loc }], + type: DYN, + loc, + }; + } if (bi.module === "http" && bi.member === "STATUS_CODES") { return { kind: "libCall", fn: "http.statusCodes", args: [], type: DYN, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 8c8639d43..8f4793153 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -4228,12 +4228,12 @@ function lowerHttpCreateServerForms( * getName/destroy and the sockets/requests/freeSockets counters dispatch * through the dyn handle ops, and the request path threads it (the * requestAgent rows). Options parse at the LITERAL construction site: - * keepAlive (a boolean value — TRUE throws the runtime's named pooling - * fence: this client dials one connection per request), keepAliveMsecs / - * maxSockets / maxFreeSockets / timeout / port as numbers, scheduling - * DROPS (no free pool exists, so selection order cannot observe), other - * documented keys fence by name, and undocumented keys drop like Node - * drops them. Null when the callee isn't the http/https Agent. */ + * keepAlive (a boolean value: finished connections return to the free + * list), keepAliveMsecs / maxSockets / maxFreeSockets / timeout / port as + * numbers, scheduling as a value the runtime validates ('fifo' or + * 'lifo'), other documented keys fence by name, and undocumented keys + * drop like Node drops them. Null when the callee isn't the http/https + * Agent. */ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const callee = expr.expression; const bi = @@ -4252,6 +4252,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE let maxFree: IrExpr = numLit(-1, loc); let timeout: IrExpr = numLit(-1, loc); let port: IrExpr = numLit(-1, loc); + let scheduling: IrExpr = lowerer.coerceToExpected(strLit("lifo", loc), DYN); const args = expr.arguments ?? []; if (args.length > 1) { lowerer.noLowering( @@ -4334,11 +4335,15 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE // the settable defaultPort carries this one. port = lowerVal("f64"); break; - case "scheduling": - // Free-socket selection order: no free pool exists here (no - // keep-alive reuse), so the choice cannot observe — dropped. - if (initializer !== null) lowerer.lowerExpr(initializer); // evaluate, like Node + case "scheduling": { + // 'fifo' | 'lifo' (falsy = 'lifo'), validated by the runtime + const v = + initializer !== null + ? lowerer.lowerExpr(initializer) + : lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment); + scheduling = lowerer.coerceToExpected(v, DYN); break; + } default: fenceOrDropOptionKey( lowerer, @@ -4364,6 +4369,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE maxFree, timeout, port, + scheduling, ], type: DYN, loc, @@ -5960,6 +5966,13 @@ function lowerHttpClientCall( maxHeaderSize = lowerVal("f64"); break; case "servername": + if (binding !== null) { + lowerer.noLowering( + `${member} option 'servername' through a module-function binding`, + prop, + "call https.request directly when passing servername", + ); + } if (!secureish) { lowerer.noLowering( `${member} option 'servername'`, @@ -6196,14 +6209,48 @@ function lowerHttpClientCall( timeout ??= numLit(0, loc); headers ??= { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }; const autoEnd = boolLit(member === "get", loc); - // Options applied to the built request: servername before the TLS - // handshake starts, then options.signal (an already aborted signal - // destroys the request at once, like Node's constructor). + // options.signal is checked first: Node's constructor rejects a value + // that is not an AbortSignal before it dials. servername is stored just + // before construction (it selects the TLS name and which pooled sockets + // the request may reuse); maxHeaderSize and the signal then apply to the + // built request (an already aborted signal destroys it at once, like + // Node's constructor). const withSignal = (request: IrExpr): IrExpr => { if (signal === null && servername === null && maxHeaderSize === null) return request; const local = lowerer.declareHiddenLocal("%requestOptions", HTTPCLIENTREQ_T); const ref = (): IrExpr => varRef(local.id, HTTPCLIENTREQ_T, loc); - const steps: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: request, loc }]; + const steps: IrStmt[] = []; + let signalRef: (() => IrExpr) | null = null; + if (signal !== null) { + const signalLocal = lowerer.declareHiddenLocal("%requestSignal", DYN); + signalRef = () => varRef(signalLocal.id, DYN, loc); + steps.push({ kind: "varDecl", localId: signalLocal.id, init: signal, loc }); + steps.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "http.clientSignalCheck", + args: [signalRef()], + type: VOID, + loc, + }, + loc, + }); + } + if (servername !== null) { + steps.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "https.nextServername", + args: [servername], + type: VOID, + loc, + }, + loc, + }); + } + steps.push({ kind: "varDecl", localId: local.id, init: request, loc }); if (maxHeaderSize !== null) { steps.push({ kind: "exprStmt", @@ -6217,26 +6264,19 @@ function lowerHttpClientCall( loc, }); } - if (servername !== null) { + if (signalRef !== null) { steps.push({ kind: "exprStmt", expr: { kind: "libCall", - fn: "https.clientServername", - args: [ref(), servername], + fn: "http.clientSignal", + args: [ref(), signalRef()], type: VOID, loc, }, loc, }); } - if (signal !== null) { - steps.push({ - kind: "exprStmt", - expr: { kind: "libCall", fn: "http.clientSignal", args: [ref(), signal], type: VOID, loc }, - loc, - }); - } return { kind: "seqExpr", stmts: steps, result: ref(), type: HTTPCLIENTREQ_T, loc }; }; if (connCb !== null) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index d74cec0ae..9d2e8228f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -11338,6 +11338,15 @@ export class Lowerer { if (bi.module === "http" && bi.member === "METHODS") { return { kind: "libCall", fn: "http.methods", args: [], type: arrayOf(STRING), loc }; } + if ((bi.module === "http" || bi.module === "https") && bi.member === "globalAgent") { + return { + kind: "libCall", + fn: "http.globalAgent", + args: [{ kind: "boolLit", value: bi.module === "https", type: BOOL, loc }], + type: DYN, + loc, + }; + } if (bi.module === "http" && bi.member === "STATUS_CODES") { return { kind: "libCall", fn: "http.statusCodes", args: [], type: DYN, loc }; } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 4c74e8b6a..7fbf2258d 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -276,8 +276,8 @@ export const HTTP_SERVER_DOCUMENTED_OPTIONS: ReadonlySet = new Set([ /** new http.Agent(options)'s documented keys (Node v24 — the Agent * constructor table plus the socket.connect() options it forwards into * each dial). The lowered set is keepAlive/keepAliveMsecs/maxSockets/ - * maxFreeSockets/timeout/port (+ scheduling, dropped: no free pool - * exists); the rest fence by name and unknown keys drop like Node. */ + * maxFreeSockets/timeout/port/scheduling; the rest fence by name and + * unknown keys drop like Node. */ export const AGENT_DOCUMENTED_OPTIONS: ReadonlySet = new Set([ "family", "hints", diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index bd87a470d..f9b092e90 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -143,6 +143,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "http.resWriteHeadDyn", "http.resSetHeaderDyn", "http.clientSignal", + "http.clientSignalCheck", "http.serverMaxHeaderSize", "http.clientMaxHeaderSize", "http.resAppendHeader", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 4a7833053..0a2ecbaa3 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1719,15 +1719,21 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * socket POOLING is not modeled (one dial per request); maxSockets * accounting is real: over-limit requests defer their dial and queue. * MAY THROW. */ - "http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64], result: DYN }, + "http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64, DYN], result: DYN }, + /** http.globalAgent / https.globalAgent (true): Node's keepAlive global + * agent, the one requests without an agent option use. */ + "http.globalAgent": { argTypes: [BOOL], result: DYN }, /** http.request's `signal` option over the fresh request: an aborted * signal destroys it with Node's AbortError now, a later abort event * does the same. A falsy value is ignored; a non-signal throws * ERR_INVALID_ARG_TYPE. */ "http.clientSignal": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID }, - /** https.request's servername option: the TLS name sent and verified. - * The "https." prefix keeps the TLS unit linked. */ - "https.clientServername": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID }, + /** options.signal checked before the request is built (ERR_INVALID_ARG_TYPE). */ + "http.clientSignalCheck": { argTypes: [DYN], result: VOID }, + /** https.request's servername option, stored for the construction call + * that follows: the TLS name sent and verified, and part of the pooled + * socket key. The "https." prefix keeps the TLS unit linked. */ + "https.nextServername": { argTypes: [STRING], result: VOID }, /** The maxHeaderSize option of createServer / http.request: Node's * validateInteger (ERR_OUT_OF_RANGE) then the header size bound. * MAY THROW. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5596-http-agent-keep-alive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5596-http-agent-keep-alive.ts.json new file mode 100644 index 000000000..f09fa3087 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5596-http-agent-keep-alive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5596-http-agent-keep-alive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5597-http-agent-idle-sockets.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5597-http-agent-idle-sockets.ts.json new file mode 100644 index 000000000..91656c30a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5597-http-agent-idle-sockets.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5597-http-agent-idle-sockets.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5598-http-global-agent.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5598-http-global-agent.ts.json new file mode 100644 index 000000000..fdb1969f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5598-http-global-agent.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5598-http-global-agent.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index 7a8359032..af73d8e3f 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -2408,6 +2408,8 @@ typedef struct ScrHttpConn { bool pumping; /* inside scr_http_conn_pump */ bool keep_alive_timer; /* the socket's idle timer is the keep-alive timeout */ size_t max_header_size; /* maxHeaderSize option; 0 = SCR_HTTP_MAX_HEADER_SIZE */ + struct ScrHttpAgent *pool_agent; /* client mode: the agent whose free list holds + * this idle socket (borrowed), or NULL */ bool header_overflow; /* client mode: the response head exceeded a bound */ bool res_close_deferred; /* the response finished inside the pump while * the request body was still arriving */ @@ -3757,6 +3759,10 @@ struct ScrHttpClientReq { ScrError *destroy_error; /* destroy(err) or the signal's AbortError (+1): * fired in place of the teardown's own error */ ScrDyn *abort_dispose; /* options.signal: removes the abort listener (+1) */ + double timeout_ms; /* the timeout option (0 = none), reapplied on a pooled socket */ + bool reused; /* req.reusedSocket: the socket came from an agent's free list */ + bool sock_pooled; /* the socket went back to the agent after the response */ + ScrStr *pool_key; /* https: TLS options a pooled socket must match (+1), or NULL */ ScrClosure *abort_listener; /* its closure (borrowed); caps[0] points back here */ bool in_registry; struct ScrHttpClientReq *next; @@ -3796,6 +3802,7 @@ void scr_http_client_release(ScrHttpClientReq *c) { if (c->res) scr_http_req_release(c->res); if (c->sock) scr_net_sock_release(c->sock); if (c->destroy_error) scr_error_release(c->destroy_error); + scr_str_release(c->pool_key); if (c->abort_listener) scr_box_set_ref(c->abort_listener->caps[0], NULL); scr_dyn_release(c->abort_dispose); #ifdef SCR_RC_AUDIT @@ -3836,6 +3843,9 @@ static void scr_http_client_unregister(ScrHttpClientReq *c) { /* Agent bookkeeping (implementation below, with the Agent unit): the * settling client leaves its agent's lists and frees a slot. */ static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c); +static void scr_http_agent_free_remove(struct ScrHttpAgent *a, struct ScrHttpConn *conn); +static bool scr_http_client_pool(struct ScrHttpConn *conn, struct ScrHttpClientReq *c, double hint); +static double scr_http_response_keep_alive(ScrHttpReq *res, const struct ScrHttpClientReq *c); static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag); static void scr_http_client_agent_detach(struct ScrHttpClientReq *c); @@ -3994,7 +4004,7 @@ bool scr_http_client_headers_sent(ScrHttpClientReq *c) { return c->head_sent; } bool scr_http_client_writable_ended(ScrHttpClientReq *c) { return c->ended; } bool scr_http_client_writable_finished(ScrHttpClientReq *c) { return c->writable_finished; } ScrNetSocket *scr_http_client_socket(ScrHttpClientReq *c) { return scr_net_sock_retain(c->sock); } -bool scr_http_client_reused_socket(ScrHttpClientReq *c) { (void)c; return false; } +bool scr_http_client_reused_socket(ScrHttpClientReq *c) { return c->reused; } void scr_http_client_set_nodelay(ScrHttpClientReq *c, bool enable) { if (!c->sock) return; scr_net_sock_release(scr_net_sock_set_nodelay(c->sock, enable)); @@ -4295,7 +4305,7 @@ void scr_http_client_add_trailers(ScrHttpClientReq *c, ScrArr *pairs /*borrowed* void scr_http_client_set_timeout(ScrHttpClientReq *c, double ms) { if (!scr_http_timeout_valid(ms)) return; - if (c->sock) scr_net_sock_set_timeout(c->sock, ms); + if (c->sock && !c->sock_pooled) scr_net_sock_set_timeout(c->sock, ms); } void scr_http_client_set_timeout_cb(ScrHttpClientReq *c, double ms, ScrClosure *cb /*moves*/) { @@ -4314,7 +4324,8 @@ void scr_http_client_set_timeout_cb(ScrHttpClientReq *c, double ms, ScrClosure * void scr_http_client_destroy(ScrHttpClientReq *c) { if (c->destroyed) return; c->destroyed = true; - if (c->sock) scr_net_sock_destroy(c->sock); + /* a socket already back in the agent's pool belongs to the agent */ + if (c->sock && !c->sock_pooled) scr_net_sock_destroy(c->sock); } static void scr_http_client_emit_destroy_error(ScrHttpClientReq *c) { @@ -4387,14 +4398,10 @@ static ScrDyn *scr_http_client_abort_thunk(ScrClosure *clo, ScrDyn *const *args, static void *scr_http_dyn_retain_v(void *p) { return p ? scr_dyn_retain(p) : NULL; } static void scr_http_dyn_release_v(void *p) { scr_dyn_release(p); } -/* http.request's `signal` option: an aborted signal destroys the request - * with Node's AbortError now; otherwise the abort event does, until the - * request closes. A falsy signal is ignored; anything else that is not an - * AbortSignal throws ERR_INVALID_ARG_TYPE. Borrows the signal. */ -void scr_http_client_add_abort_signal(ScrHttpClientReq *c, ScrDyn *signal, - ScrDyn *(*add)(ScrDyn *signal, ScrDyn *listener)) { - if (signal == NULL || !scr_dyn_truthy(signal)) return; - /* Node's validateAbortSignal: an object with an `aborted` property */ +/* The signal's `aborted` value (+1), or NULL with Node's + * ERR_INVALID_ARG_TYPE pending when the value is not an AbortSignal + * (validateAbortSignal: an object with an `aborted` property). */ +static ScrDyn *scr_http_signal_aborted(const ScrDyn *signal) { ScrDyn *aborted_value = NULL; if (signal->kind == SCR_DYN_OBJ) { ScrDyn *v = scr_dyn_obj_get(signal, "aborted", 7); @@ -4408,10 +4415,29 @@ void scr_http_client_add_abort_signal(ScrHttpClientReq *c, ScrDyn *signal, aborted_value = NULL; } } - if (aborted_value == NULL) { - scr_dyn_arg_type_fail("options.signal", "an instance of AbortSignal", signal); - return; - } + if (aborted_value == NULL) scr_dyn_arg_type_fail("signal", "an instance of AbortSignal", signal); + return aborted_value; +} + +/* http.request's `signal` option before the request is built: Node's + * ClientRequest constructor validates a truthy signal before it dials, so + * a value that is not an AbortSignal throws ERR_INVALID_ARG_TYPE and no + * request exists. Borrows the signal. */ +void scr_http_client_signal_check(const ScrDyn *signal) { + if (signal == NULL || !scr_dyn_truthy(signal)) return; + ScrDyn *aborted_value = scr_http_signal_aborted(signal); + if (aborted_value != NULL) scr_dyn_release(aborted_value); +} + +/* http.request's `signal` option: an aborted signal destroys the request + * with Node's AbortError now; otherwise the abort event does, until the + * request closes. A falsy signal is ignored; anything else that is not an + * AbortSignal throws ERR_INVALID_ARG_TYPE. Borrows the signal. */ +void scr_http_client_add_abort_signal(ScrHttpClientReq *c, ScrDyn *signal, + ScrDyn *(*add)(ScrDyn *signal, ScrDyn *listener)) { + if (signal == NULL || !scr_dyn_truthy(signal)) return; + ScrDyn *aborted_value = scr_http_signal_aborted(signal); + if (aborted_value == NULL) return; bool aborted = scr_dyn_truthy(aborted_value); scr_dyn_release(aborted_value); if (aborted) { @@ -4733,6 +4759,10 @@ static void scr_http_client_response_done(ScrHttpConn *conn) { ScrHttpReq *res = conn->req; if (!c || c->response_done) return; c->response_done = true; + /* the response's reuse verdict, read before the response detaches: + * -2 = the connection closes, -1 = reusable with no Keep-Alive timeout + * hint, otherwise the hint in milliseconds */ + double keep_alive_hint = res != NULL ? scr_http_response_keep_alive(res, c) : -2; if (res) { scr_http_req_retain(res); scr_http_req_finish(res, true); /* fires 'end' */ @@ -4748,7 +4778,8 @@ static void scr_http_client_response_done(ScrHttpConn *conn) { conn->req = NULL; scr_http_req_release(res); } - if (c->sock) scr_net_sock_destroy(c->sock); /* quiet close, no pooling */ + /* a keepAlive agent takes the socket back; otherwise it closes quietly */ + if (c->sock && !scr_http_client_pool(conn, c, keep_alive_hint)) scr_net_sock_destroy(c->sock); } /* Premature teardown classification, shared by eof/closed/error: @@ -4792,6 +4823,11 @@ static void scr_http_client_premature(ScrHttpConn *conn) { static void scr_http_client_eof(ScrHttpConn *conn) { ScrHttpClientReq *c = conn->client; + if (conn->pool_agent != NULL) { + /* the server closed an idle pooled connection */ + scr_net_sock_destroy(conn->sock); + return; + } if (!c) return; if (conn->state == SCR_HTTP_BODY_EOF && c->response_started && !c->response_done) { /* EOF-delimited body: this IS completion */ @@ -4814,6 +4850,7 @@ static void scr_http_client_agent_detach(ScrHttpClientReq *c) { } static void scr_http_client_closed(ScrHttpConn *conn) { + if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, conn); ScrHttpClientReq *c = conn->client; if (!c) return; scr_http_client_agent_detach(c); @@ -4868,6 +4905,11 @@ static void scr_http_client_head_overflow(ScrHttpConn *conn) { * (never destroys anything — Node; the caller destroys). */ static void scr_http_client_sock_timeout(void *ctx) { ScrHttpConn *conn = (ScrHttpConn *)ctx; + if (conn->pool_agent != NULL) { + /* an idle pooled socket timed out: the agent destroys it */ + scr_net_sock_destroy(conn->sock); + return; + } ScrHttpClientReq *c = conn->client; if (!c || c->response_done || c->close_emitted) return; scr_net_fire0(&c->timeout_ls); @@ -4882,6 +4924,7 @@ static void scr_http_client_sock_established(void *ctx) { } static void scr_http_client_conn_free(ScrHttpConn *conn) { + if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, conn); if (conn->req) { scr_http_req_finish(conn->req, false); scr_http_req_release(conn->req); @@ -4998,6 +5041,7 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double if (wrap) wrap(c->sock, wrap_ctx); scr_net_sock_set_native_established(c->sock, &scr_http_client_sock_established); c->sock_ready = scr_net_sock_established(c->sock); + c->timeout_ms = timeout_ms > 0 ? timeout_ms : 0; if (timeout_ms > 0) scr_net_sock_set_timeout(c->sock, timeout_ms); scr_http_client_register(c); scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_SOCKET, scr_http_client_retain(c)); @@ -5041,13 +5085,15 @@ ScrHttpClientReq *scr_http_request_conn(ScrClosure *conn_cb /*moves*/, ScrStr *p return c; } +/* http.request without an agent option: the request goes through + * http.globalAgent, as in Node. */ ScrHttpClientReq *scr_http_request(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { - return scr_http_request_ex(host, port, path, method, timeout_ms, header_pairs, auto_end, cb, - fn, 80, NULL, NULL); + return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end, + NULL, cb, fn, 80, NULL, NULL, NULL); } /* ── the http Agent ────────────────────────────────────────────────────── @@ -5075,7 +5121,8 @@ typedef struct ScrHttpAgentEnt { typedef struct ScrHttpAgent { size_t rc; bool secure; /* https.Agent: protocol/defaultPort answers */ - bool keep_alive; /* always false here (true fences at construction) */ + bool keep_alive; /* keepAlive: finished connections return to free_socks */ + bool fifo; /* scheduling: 'fifo' takes the oldest free socket, 'lifo' the newest */ double ka_msecs; double max_sockets; /* INFINITY = Node's default */ double max_free; @@ -5083,10 +5130,20 @@ typedef struct ScrHttpAgent { double default_port; /* settable (agent.defaultPort = p) */ bool destroyed; ScrHttpAgentEnt *ents; /* append order — Node's FIFO queue */ + struct ScrHttpAgentFree *free_socks; /* keepAlive: idle sockets, most recent first */ bool in_registry; struct ScrHttpAgent *next; } ScrHttpAgent; +/* One idle socket in a keepAlive agent's free list, with its parser. */ +typedef struct ScrHttpAgentFree { + ScrStr *name; /* getName's key */ + ScrStr *pool_key; /* https: the TLS options the socket was verified with (+1), or NULL */ + ScrNetSocket *sock; /* +1 */ + struct ScrHttpConn *conn; /* the socket's parser (owned by the socket) */ + struct ScrHttpAgentFree *next; +} ScrHttpAgentFree; + static SCR_TL ScrHttpAgent *scr_http_agents = NULL; /* registry: +1 each, atexit-swept */ /* Node's ClientRequest: an agent without keepAlive and without a finite @@ -5102,6 +5159,15 @@ static ScrHttpAgent *scr_http_agent_retain(ScrHttpAgent *a) { static void scr_http_agent_release(ScrHttpAgent *a) { if (!a || --a->rc > 0) return; + while (a->free_socks) { + ScrHttpAgentFree *f = a->free_socks; + a->free_socks = f->next; + f->conn->pool_agent = NULL; + scr_str_release(f->name); + scr_str_release(f->pool_key); + scr_net_sock_release(f->sock); + free(f); + } ScrHttpAgentEnt *e = a->ents; while (e) { ScrHttpAgentEnt *next = e->next; @@ -5131,6 +5197,190 @@ static size_t scr_http_agent_active(const ScrHttpAgent *a, const ScrStr *name) { return n; } +static bool scr_http_name_same(const ScrStr *a, const ScrStr *b) { + return a->len == b->len && memcmp(a->data, b->data, a->len) == 0; +} + +static size_t scr_http_agent_free_count(const ScrHttpAgent *a, const ScrStr *name) { + size_t n = 0; + for (const ScrHttpAgentFree *f = a->free_socks; f; f = f->next) n += scr_http_name_same(f->name, name); + return n; +} + +static bool scr_http_key_same(const ScrStr *a, const ScrStr *b) { + return a == NULL ? b == NULL : b != NULL && scr_http_name_same(a, b); +} + +/* Takes a free socket for `name` off the list, or NULL: the most recently + * freed one under 'lifo' scheduling, the oldest under 'fifo'. */ +static ScrHttpAgentFree *scr_http_agent_free_take(ScrHttpAgent *a, const ScrStr *name, + const ScrStr *pool_key) { + ScrHttpAgentFree **found = NULL; + for (ScrHttpAgentFree **link = &a->free_socks; *link; link = &(*link)->next) { + if (!scr_http_name_same((*link)->name, name) || !scr_http_key_same((*link)->pool_key, pool_key)) + continue; + found = link; + if (!a->fifo) break; + } + if (found == NULL) return NULL; + ScrHttpAgentFree *f = *found; + *found = f->next; + return f; +} + +/* Puts a socket taken by scr_http_agent_free_take back where it was + * relative to the other sockets for its name. */ +static void scr_http_agent_free_untake(ScrHttpAgent *a, ScrHttpAgentFree *f) { + ScrHttpAgentFree **link = &a->free_socks; + if (a->fifo) { + while (*link) link = &(*link)->next; + } + f->next = *link; + *link = f; +} + +/* The idle socket closed (or its parser is going away): it leaves the + * free list. */ +static void scr_http_agent_free_remove(struct ScrHttpAgent *a, struct ScrHttpConn *conn) { + for (ScrHttpAgentFree **link = &a->free_socks; *link; link = &(*link)->next) { + if ((*link)->conn != conn) continue; + ScrHttpAgentFree *f = *link; + *link = f->next; + conn->pool_agent = NULL; + scr_str_release(f->name); + scr_str_release(f->pool_key); + scr_net_sock_release(f->sock); + free(f); + return; + } + conn->pool_agent = NULL; +} + +/* Binds request `c` to a pooled socket in place of its deferred, + * never-dialed one: the deferred socket's parser is dropped, its buffered + * bytes move to the pooled socket, and the pooled parser serves `c`. + * `reused` is req.reusedSocket: Node sets it when a new request takes a + * free socket, not when a queued request receives a freed one. */ +static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, bool reused) { + ScrNetSocket *old = c->sock; + ScrHttpConn *oldconn = c->conn; + scr_net_sock_set_native_reader(old, NULL, NULL, NULL, NULL, NULL); + scr_net_sock_set_native_events(old, NULL, NULL); + scr_net_sock_set_native_established(old, NULL); + scr_net_sock_move_writes(old, f->sock); + if (oldconn != NULL) { + if (oldconn->client) scr_http_client_release(oldconn->client); + free(oldconn->buf); + free(oldconn); + } + scr_net_sock_destroy(old); + scr_net_sock_release(old); + ScrHttpConn *conn = f->conn; + c->sock = f->sock; /* the free entry's +1 moves */ + c->conn = conn; + conn->client = scr_http_client_retain(c); + conn->pool_agent = NULL; + conn->state = SCR_HTTP_HEAD; + conn->len = 0; + conn->body_remaining = 0; + conn->client_resp_started = false; + conn->header_overflow = false; + conn->head_bytes = conn->trailer_bytes = conn->trailer_fields = 0; + scr_str_release(f->name); + scr_str_release(f->pool_key); + free(f); + c->reused = reused; + c->sock_ready = true; + scr_net_sock_set_ref(c->sock, true); + scr_net_sock_set_timeout(c->sock, c->timeout_ms); + scr_http_client_queue_finish(c); +} + +/* Whether the request's own header `name` carries `token`. */ +static bool scr_http_client_has_header_value(const ScrHttpClientReq *c, const char *name, + const char *token) { + for (size_t i = 0; i < c->nheaders; i++) { + if (scr_http_header_name_is(c->hnames[i], name) && scr_http_header_has_token(c->hvalues[i], token)) + return true; + } + return false; +} + +/* A client response's keep-alive verdict: -2 when the connection closes + * after it (Connection: close, HTTP/1.0 without keep-alive, or a body + * delimited by the connection closing), otherwise the Keep-Alive header's + * /^timeout=(\d+)/ hint in milliseconds, or -1 without one. */ +static double scr_http_response_keep_alive(ScrHttpReq *res, const struct ScrHttpClientReq *c) { + bool keep = !res->http10, framed = false; + double hint = -1; + for (size_t i = 0; i < res->nheaders; i++) { + const ScrStr *n = res->hnames[i]; + const ScrStr *v = res->hvalues[i]; + if (scr_http_header_name_is((ScrStr *)n, "connection")) { + if (scr_http_header_has_token(v, "close")) keep = false; + else if (scr_http_header_has_token(v, "keep-alive")) keep = true; + } else if (scr_http_header_name_is((ScrStr *)n, "content-length") || + scr_http_header_name_is((ScrStr *)n, "transfer-encoding")) { + framed = true; + } else if (scr_http_header_name_is((ScrStr *)n, "keep-alive") && v->len > 8 && + memcmp(v->data, "timeout=", 8) == 0 && isdigit((unsigned char)v->data[8])) { + double seconds = 0; + for (size_t k = 8; k < v->len && isdigit((unsigned char)v->data[k]); k++) seconds = seconds * 10 + (v->data[k] - '0'); + hint = seconds * 1000; + } + } + bool head = c->method->len == 4 && memcmp(c->method->data, "HEAD", 4) == 0; + bool bodiless = head || res->status == 204 || res->status == 304 || (res->status >= 100 && res->status < 200); + if (!keep || (!framed && !bodiless)) return -2; + return hint; +} + +/* Node's agent.keepSocketAlive after a response: the socket returns to the + * agent's free list (unref'd, with the agent's timeout lowered to the + * server's Keep-Alive hint less one second) unless the response or request + * asked to close, the request did not finish, or the free list for the + * name is full. Answers whether the socket was kept. */ +static bool scr_http_client_pool(struct ScrHttpConn *conn, struct ScrHttpClientReq *c, double hint) { + ScrHttpAgent *ag = c->agent; + if (ag == NULL || !ag->keep_alive || ag->destroyed || c->destroyed || c->aborted) return false; + if (hint == -2 || !c->ended || conn->len != 0 || scr_http_client_has_header_value(c, "connection", "close")) + return false; + ScrStr *name = NULL; + for (ScrHttpAgentEnt *e = ag->ents; e && name == NULL; e = e->next) + if (e->client == c) name = scr_str_retain(e->name); + if (name == NULL) return false; + if ((double)scr_http_agent_free_count(ag, name) >= ag->max_free) { + scr_str_release(name); + return false; + } + double timeout = ag->timeout_ms > 0 ? ag->timeout_ms : 0; + if (hint >= 0) { + double server = hint - 1000; + if (server < 0) { + scr_str_release(name); + return false; + } + if (server < timeout) timeout = server; + } + ScrHttpAgentFree *f = calloc(1, sizeof *f); + if (!f) scr_http_oom(); + f->name = name; + f->pool_key = c->pool_key ? scr_str_retain(c->pool_key) : NULL; + f->sock = scr_net_sock_retain(c->sock); + f->conn = conn; + /* the parser no longer serves this request */ + conn->client = NULL; + conn->pool_agent = ag; + f->next = ag->free_socks; + ag->free_socks = f; + c->sock_pooled = true; + scr_net_sock_set_ref(f->sock, false); + scr_net_sock_set_timeout(f->sock, timeout); + scr_http_client_agent_detach(c); /* frees the slot: a queued request takes the socket */ + scr_http_client_release(c); /* the parser's reference */ + return true; +} + /* The settling/dying client leaves the lists; a freed slot starts the * first queued dial for the same name (Node's FIFO). */ static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c) { @@ -5148,7 +5398,11 @@ static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpCl for (ScrHttpAgentEnt *e = ag->ents; e; e = e->next) { if (e->queued && e->name->len == name->len && memcmp(e->name->data, name->data, name->len) == 0) { - if (scr_http_agent_active(ag, name) < ag->max_sockets) { + ScrHttpAgentFree *f = scr_http_agent_free_take(ag, name, e->client->pool_key); + if (f != NULL) { + e->queued = false; + scr_http_client_rebind(e->client, f, false); + } else if (scr_http_agent_active(ag, name) < ag->max_sockets) { e->queued = false; if (e->client->sock) scr_net_sock_dial_start(e->client->sock); } @@ -5185,21 +5439,24 @@ static ScrStr *scr_http_agent_name(const char *host, size_t host_len, const char ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs, double max_sockets, double max_free, double timeout_ms, - double port /* < 0 = unset */) { - if (keep_alive) { - static const char msg[] = - "an http Agent with keepAlive: true (socket pooling and reuse — compiled clients " - "dial one connection per request and close it with the response) is not supported " - "yet — construct the Agent without keepAlive, or drop the agent option"; - scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); - return NULL; - } + double port /* < 0 = unset */, const ScrDyn *scheduling) { scr_http_install(); + /* Node: options.scheduling || 'lifo', then validateOneOf */ + bool fifo = false; + if (scheduling != NULL && scr_dyn_truthy(scheduling)) { + const ScrStr *s = scheduling->kind == SCR_DYN_STR ? scheduling->v.str : NULL; + if (s != NULL && s->len == 4 && memcmp(s->data, "fifo", 4) == 0) fifo = true; + else if (s == NULL || s->len != 4 || memcmp(s->data, "lifo", 4) != 0) { + scr_dyn_arg_value_fail("scheduling", "must be one of: 'fifo', 'lifo'", scheduling); + return NULL; + } + } ScrHttpAgent *a = calloc(1, sizeof *a); if (!a) scr_http_oom(); a->rc = 1; a->secure = secure; - a->keep_alive = false; + a->keep_alive = keep_alive; + a->fifo = fifo; a->ka_msecs = ka_msecs >= 0 ? ka_msecs : 1000; a->max_sockets = max_sockets >= 0 ? max_sockets : (double)INFINITY; a->max_free = max_free >= 0 ? max_free : 256; @@ -5218,11 +5475,35 @@ ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs, return d; } +/* http.globalAgent / https.globalAgent: Node's + * new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 }), + * created on first use. Borrowed (the thread keeps it until exit). */ +static SCR_TL ScrDyn *scr_http_global_agents[2]; + +const ScrDyn *scr_http_global_agent(bool secure) { + ScrDyn **slot = &scr_http_global_agents[secure ? 1 : 0]; + if (*slot == NULL) *slot = scr_http_agent_new(secure, true, -1, -1, -1, 5000, -1, NULL); + return *slot; +} + +/* http.globalAgent as a value (+1). */ +ScrDyn *scr_http_global_agent_value(bool secure) { + return scr_dyn_retain((ScrDyn *)scr_http_global_agent(secure)); +} + /* agent.destroy(): tears down every listed connection (actives destroy * their sockets, queued dials never start) — Node destroys in-use * sockets too; there is no free pool here. */ static void scr_http_agent_destroy(ScrHttpAgent *a) { a->destroyed = true; + /* idle pooled sockets close; their 'close' removes each from the list */ + while (a->free_socks) { + ScrNetSocket *sock = scr_net_sock_retain(a->free_socks->sock); + struct ScrHttpConn *conn = a->free_socks->conn; + scr_http_agent_free_remove(a, conn); + scr_net_sock_destroy(sock); + scr_net_sock_release(sock); + } /* destroying sockets detaches entries re-entrantly — walk a snapshot */ for (;;) { ScrHttpClientReq *victim = NULL; @@ -5244,13 +5525,11 @@ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double po bool auto_end, const ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn, int default_port, - void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx) { - /* the null/undefined arms: the default path (port < 0 = no port option) */ - if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL) { - double p = port >= 0 ? port : (double)default_port; - return scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs, auto_end, - cb, fn, default_port, wrap, wrap_ctx, NULL); - } + void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx, + ScrStr *pool_key /*borrowed, nullable*/) { + /* the null/undefined arms: the scheme's global agent, like Node */ + if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL) + agent = scr_http_global_agent(default_port == 443); /* agent: false — Node's one-shot Agent: exactly this client's model, * plus its Connection: close request header (unless the caller set one) */ if (agent->kind == SCR_DYN_BOOL && !agent->v.b) { @@ -5295,16 +5574,24 @@ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double po int portn = snprintf(portbuf, sizeof portbuf, "%d", (int)p); ScrStr *name = scr_http_agent_name(host->data, host->len, portbuf, (size_t)portn, NULL, 0, 0, NULL, 0); - bool queue = scr_http_agent_active(ag, name) >= ag->max_sockets; - ScrNetSocket *presock = queue ? scr_net_connect_deferred(p, host) : NULL; + /* a free pooled socket for the name serves the request right away */ + ScrHttpAgentFree *reuse = ag->keep_alive ? scr_http_agent_free_take(ag, name, pool_key) : NULL; + bool queue = reuse == NULL && scr_http_agent_active(ag, name) >= ag->max_sockets; + ScrNetSocket *presock = queue || reuse ? scr_net_connect_deferred(p, host) : NULL; ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs, auto_end, cb, fn, default_port, wrap, wrap_ctx, presock); if (c == NULL) { /* the method-token throw: nothing registered */ + if (reuse != NULL) scr_http_agent_free_untake(ag, reuse); /* back to the list it came from */ scr_str_release(name); return NULL; } - if (ag->timeout_ms >= 0 && timeout_ms <= 0) scr_net_sock_set_timeout(c->sock, ag->timeout_ms); + c->pool_key = pool_key ? scr_str_retain(pool_key) : NULL; + if (ag->timeout_ms >= 0 && timeout_ms <= 0) { + c->timeout_ms = ag->timeout_ms; + scr_net_sock_set_timeout(c->sock, ag->timeout_ms); + } + if (reuse != NULL) scr_http_client_rebind(c, reuse, true); c->agent = scr_http_agent_retain(ag); ScrHttpAgentEnt *e = calloc(1, sizeof *e); if (!e) scr_http_oom(); @@ -5323,12 +5610,16 @@ ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port, bool auto_end, const ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end, - agent, cb, fn, 80, NULL, NULL); + agent, cb, fn, 80, NULL, NULL, NULL); } /* The atexit sweep: break agent↔client edges a program leaves in flight * (queued dials that never ran) so the RC audit sees a clean heap. */ static void scr_http_agents_cleanup(void) { + for (size_t i = 0; i < 2; i++) { + scr_dyn_release(scr_http_global_agents[i]); + scr_http_global_agents[i] = NULL; + } while (scr_http_agents) { ScrHttpAgent *a = scr_http_agents; scr_http_agents = a->next; @@ -6434,6 +6725,12 @@ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) if (strcmp(key, "maxFreeSockets") == 0) return scr_dyn_new_num(a->max_free); if (strcmp(key, "keepAlive") == 0) return scr_dyn_new_bool(a->keep_alive); if (strcmp(key, "keepAliveMsecs") == 0) return scr_dyn_new_num(a->ka_msecs); + if (strcmp(key, "scheduling") == 0) { + ScrStr *s = scr_str_new(a->fifo ? "fifo" : "lifo", 4); + ScrDyn *d = scr_dyn_new_str(s); + scr_str_release(s); + return d; + } if (strcmp(key, "defaultPort") == 0) return scr_dyn_new_num(a->default_port); if (strcmp(key, "protocol") == 0) { ScrStr *s = scr_str_new(a->secure ? "https:" : "http:", a->secure ? 6 : 5); @@ -6444,8 +6741,27 @@ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) if (strcmp(key, "sockets") == 0) return scr_http_dynh_agent_table(a, false); if (strcmp(key, "requests") == 0) return scr_http_dynh_agent_table(a, true); if (strcmp(key, "freeSockets") == 0) { - /* Always empty: the runtime pools nothing (keepAlive fences). */ - return scr_dyn_new_obj(); + /* { name: [socket, ...] }, oldest first like Node's arrays */ + ScrDyn *obj = scr_dyn_new_obj(); + for (ScrHttpAgentFree *f = a->free_socks; f; f = f->next) { + ScrDyn *arr = scr_dyn_obj_get(obj, f->name->data, f->name->len); + if (arr == NULL) { + arr = scr_dyn_new_arr(); + scr_dyn_obj_set(obj, f->name->data, f->name->len, arr); + } + ScrDyn *item = scr_dyn_new_handle(f->sock, SCR_DYNH_NET_SOCKET); + scr_dyn_arr_push(arr, item); + } + /* the list is most recent first: reverse each array into Node's order */ + for (size_t i = 0; i < obj->v.obj.len; i++) { + ScrDyn *arr = obj->v.obj.entries[i].value; + for (size_t l = 0, r = arr->v.arr.len ? arr->v.arr.len - 1 : 0; l < r; l++, r--) { + ScrDyn *t = arr->v.arr.items[l]; + arr->v.arr.items[l] = arr->v.arr.items[r]; + arr->v.arr.items[r] = t; + } + } + return obj; } if (strcmp(key, "totalSocketCount") == 0) { size_t n = 0; @@ -6455,7 +6771,7 @@ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) return scr_dyn_new_num((double)n); } { - static const char *const known[] = { "options", "maxTotalSockets", "scheduling", NULL }; + static const char *const known[] = { "options", "maxTotalSockets", NULL }; if (scr_http_dynh_in(key, known)) { scr_http_dynh_unsupported("Agent", key, NULL); return NULL; diff --git a/packages/runtime/src/scr_http.h b/packages/runtime/src/scr_http.h index 33c24a0d8..a4dcc44ce 100644 --- a/packages/runtime/src/scr_http.h +++ b/packages/runtime/src/scr_http.h @@ -295,6 +295,9 @@ void scr_http_client_destroy(ScrHttpClientReq *c); /* req.destroy(err): err (borrowed) becomes the request's 'error'. */ void scr_http_client_destroy_error(ScrHttpClientReq *c, ScrError *err); void scr_http_client_max_header_size(ScrHttpClientReq *c, double value); /* see the server form */ +/* http.request's `signal` option (borrowed), checked before the request + * is built. May throw ERR_INVALID_ARG_TYPE. */ +void scr_http_client_signal_check(const ScrDyn *signal); /* http.request's `signal` option (borrowed); `add` is * events.addAbortListener. May throw ERR_INVALID_ARG_TYPE. */ void scr_http_client_add_abort_signal(ScrHttpClientReq *c, ScrDyn *signal, @@ -350,14 +353,19 @@ bool scr_http_url_parts(ScrStr *url /*borrowed*/, bool secure, ScrStr **host_out * option" — the agent's (settable) defaultPort, then the scheme's. */ ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs, double max_sockets, double max_free, double timeout_ms, - double port /* < 0 = unset: the option-merge default */); /* +1 */ + double port /* < 0 = unset: the option-merge default */, + const ScrDyn *scheduling /* borrowed; NULL = 'lifo' */); /* +1 */ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, const ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn, int default_port, - void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx); /* +1 */ + void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx, + ScrStr *pool_key /*borrowed, nullable: https TLS options*/); /* +1 */ +/* http.globalAgent / https.globalAgent: borrowed, and as a +1 value. */ +const ScrDyn *scr_http_global_agent(bool secure); +ScrDyn *scr_http_global_agent_value(bool secure); ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, diff --git a/packages/runtime/src/scr_http2.h b/packages/runtime/src/scr_http2.h index d5b1cae2d..839a7d98c 100644 --- a/packages/runtime/src/scr_http2.h +++ b/packages/runtime/src/scr_http2.h @@ -166,9 +166,9 @@ long scr_http2_live_count(void); ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves*/, ScrHttpReqFn fn); /* +1 */ /* The runtime-options-record twin (the tls block's divergence-66 walk). */ ScrNetServer *scr_https_create_server_dyn(const struct ScrDyn *opts /*borrowed*/, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn); /* +1 or NULL+pending */ -/* https.request's servername option (borrowed), applied before the - * handshake starts. */ -void scr_https_client_servername(ScrHttpClientReq *c, ScrStr *name); +/* https.request's servername option (borrowed), stored for the next + * https request construction call. */ +void scr_https_set_next_servername(ScrStr *name); ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 64f5dee02..53387f301 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -2395,6 +2395,14 @@ ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nul return s; } +/* Moves a deferred socket's buffered (never sent) bytes onto `to`, which + * sends them in order after its own: an http agent request that waited + * for a slot takes a pooled socket instead of dialing. */ +void scr_net_sock_move_writes(ScrNetSocket *from, ScrNetSocket *to) { + if (from->wlen > from->whead) scr_net_sock_write_native(to, from->wbuf + from->whead, from->wlen - from->whead); + from->whead = from->wlen = 0; +} + void scr_net_sock_dial_start(ScrNetSocket *s) { if (!s->dial_deferred || s->close_emitted || s->emit_close || s->fd >= 0) return; s->dial_deferred = false; diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 539c14cdb..05e969002 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -138,6 +138,7 @@ void scr_net_listen_opts_reuse_port(ScrNetServer *s, double port, ScrStr *host / double scr_net_server_port(ScrNetServer *s); /* address().port */ bool scr_net_server_listening(ScrNetServer *s); void scr_net_sock_set_ref(ScrNetSocket *s, bool ref); +void scr_net_sock_move_writes(ScrNetSocket *from, ScrNetSocket *to); void scr_net_server_set_ref(ScrNetServer *s, bool ref); bool scr_net_sock_has_ref(ScrNetSocket *s); double scr_net_server_keep_alive_ms(ScrNetServer *s); diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index 1f481c4fd..392aa261f 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -1089,15 +1089,68 @@ void scr_tls_fetch_client_wrap(ScrNetSocket *sock, void *ctx) { scr_tls_cli_wrap(sock, ctx); } +/* https.request's `servername` option: the request's lowering stores it + * just before the construction call, which takes it (+1, NULL = none). */ +static SCR_TL ScrStr *scr_https_next_servername = NULL; + +void scr_https_set_next_servername(ScrStr *name /*borrowed*/) { + scr_str_release(scr_https_next_servername); + scr_https_next_servername = scr_str_retain(name); +} + +static ScrStr *scr_https_take_servername(void) { + ScrStr *name = scr_https_next_servername; + scr_https_next_servername = NULL; + return name; +} + +/* The client config with the servername (when given) as the name the + * handshake sends and verifies, in place of the host. "" sends no SNI, + * and the certificate is still checked against the host, as Node's + * checkServerIdentity does. */ +static ScrTlsCli *scr_tls_cli_for(ScrStr *host, bool reject_unauthorized, const char *ca, + size_t ca_len, const ScrStr *servername) { + ScrTlsCli *cli = scr_tls_cli_new(host, reject_unauthorized, ca, ca_len); + if (servername != NULL) scr_tls_cli_set_servername(cli, servername); + return cli; +} + +/* The TLS options a pooled https socket was set up with: an agent reuses + * the socket only for a request with the same verification settings and + * servername, as Node's https getName keys them (a servername that is + * empty or equal to the host keys like none). +1. */ +static ScrStr *scr_tls_pool_key(ScrStr *host, bool reject_unauthorized, const char *ca, + size_t ca_len, ScrStr *servername) { + uint64_t h = 1469598103934665603ull; + for (size_t i = 0; i < ca_len; i++) h = (h ^ (unsigned char)ca[i]) * 1099511628211ull; + char buf[64]; + int n = snprintf(buf, sizeof buf, "%d:%zu:%016llx:", reject_unauthorized ? 1 : 0, ca_len, + (unsigned long long)h); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_write(&b, buf, (size_t)n); + if (servername != NULL && servername->len > 0 && !scr_str_eq(servername, host)) { + scr_jb_putc(&b, 's'); + scr_jb_write(&b, servername->data, servername->len); + } + return scr_jb_finish(&b); +} + ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, bool reject_unauthorized, const char *ca /*borrowed, len 0 = none*/, size_t ca_len, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { - ScrTlsCli *cli = scr_tls_cli_new(host, reject_unauthorized, ca, ca_len); - return scr_http_request_ex(host, port, path, method, timeout_ms, header_pairs, auto_end, cb, - fn, 443, &scr_tls_cli_wrap, cli); + ScrStr *servername = scr_https_take_servername(); + ScrTlsCli *cli = scr_tls_cli_for(host, reject_unauthorized, ca, ca_len, servername); + ScrStr *key = scr_tls_pool_key(host, reject_unauthorized, ca, ca_len, servername); + scr_str_release(servername); + ScrHttpClientReq *c = scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, + auto_end, NULL, cb, fn, 443, &scr_tls_cli_wrap, + cli, key); + scr_str_release(key); + return c; } /* https.get('https://host/path') — the URL-string spelling, sharing @@ -1131,9 +1184,15 @@ ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port const char *ca /*borrowed, len 0 = none*/, size_t ca_len, const ScrDyn *agent /*borrowed*/, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) { - ScrTlsCli *cli = scr_tls_cli_new(host, reject_unauthorized, ca, ca_len); - return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end, - agent, cb, fn, 443, &scr_tls_cli_wrap, cli); + ScrStr *servername = scr_https_take_servername(); + ScrTlsCli *cli = scr_tls_cli_for(host, reject_unauthorized, ca, ca_len, servername); + ScrStr *key = scr_tls_pool_key(host, reject_unauthorized, ca, ca_len, servername); + scr_str_release(servername); + ScrHttpClientReq *c = scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, + auto_end, agent, cb, fn, 443, &scr_tls_cli_wrap, + cli, key); + scr_str_release(key); + return c; } /* ── RUNTIME options records (the divergence-66 stance) ──────────────── @@ -1735,21 +1794,6 @@ static ScrTlsSock *scr_tls_sock_of(ScrNetSocket *s) { return (ScrTlsSock *)scr_net_sock_transport_ctx_for(s, &SCR_TLS_OPS); } -/* https.request's `servername` option: the TLS name the handshake sends - * (SNI) and verifies the certificate against, in place of the host. An - * empty servername sends no SNI, and the certificate is still checked - * against the host, as Node's checkServerIdentity does. The handshake - * starts once the dial connects, after this runs. */ -void scr_https_client_servername(ScrHttpClientReq *c, ScrStr *name /*borrowed*/) { - ScrNetSocket *sock = scr_http_client_socket(c); - if (sock == NULL) return; - ScrTlsSock *t = scr_tls_sock_of(sock); - if (t != NULL && t->cli != NULL) { - scr_tls_cli_set_servername(t->cli, name); - (void)mbedtls_ssl_set_hostname(&t->ssl, t->cli->hostname); - } - scr_net_sock_release(sock); -} /* The client 'session' event: fires ONCE per socket with the serialized * mbedTLS session — a real ticket where one arrived (the 1.3 receipt diff --git a/tests/corpus/5564-http-request-signal.ts b/tests/corpus/5564-http-request-signal.ts index 86dbc6b72..07cd0cdbd 100644 --- a/tests/corpus/5564-http-request-signal.ts +++ b/tests/corpus/5564-http-request-signal.ts @@ -1,7 +1,9 @@ // http.request's `signal` option: aborting destroys the request with an // AbortError (code ABORT_ERR, the signal's reason as `cause`), including a // signal that is already aborted, and an abort after the response has no -// effect. req.destroy(err) reports err as the request's 'error'. +// effect. req.destroy(err) reports err as the request's 'error'. A value +// that is not an AbortSignal throws from http.request before any request +// is made. import * as http from "node:http"; const server = http.createServer((req, res) => { @@ -70,6 +72,13 @@ server.listen(0, "127.0.0.1", async () => { }); }); }); + try { + http.request({ host: "127.0.0.1", port, path: "/", signal: {} as AbortSignal }); + console.log("no throw"); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log("not a signal", err.code, err.message); + } server.close(); server.closeAllConnections(); }); diff --git a/tests/corpus/5596-http-agent-keep-alive.ts b/tests/corpus/5596-http-agent-keep-alive.ts new file mode 100644 index 000000000..06e879920 --- /dev/null +++ b/tests/corpus/5596-http-agent-keep-alive.ts @@ -0,0 +1,109 @@ +// An Agent with keepAlive: true keeps each finished connection in its free +// list and hands it to the next request for the same host and port +// (req.reusedSocket is true), including requests that waited for a +// maxSockets slot. A response with Connection: close is not reused, and +// agent.destroy() closes the idle sockets. scheduling: 'lifo' (the +// default) reuses the most recently freed socket and 'fifo' the oldest; +// any other value throws. +import * as http from "node:http"; +import * as net from "node:net"; + +let connections = 0; +const server = http.createServer((req, res) => { + if (req.url === "/close") res.setHeader("Connection", "close"); + res.end(`${req.url} ${req.headers.connection}`); +}); +server.on("connection", () => connections++); + +function get(agent: http.Agent, path: string): Promise { + return new Promise((resolve) => { + const port = (server.address() as { port: number }).port; + const req = http.request( + { host: "127.0.0.1", port, path, agent: agent }, + (res) => { + let body = ""; + res.on("data", (c: Buffer) => (body += c.toString())); + res.on("end", () => resolve(`${body} reused=${req.reusedSocket}`)); + }, + ); + req.end(); + }); +} + +function tryScheduling(scheduling: any): void { + try { + new http.Agent({ scheduling }); + console.log("scheduling", JSON.stringify(scheduling)); + } catch (err) { + const e = err as NodeJS.ErrnoException; + console.log(e.name, e.code, e.message); + } +} + +function track( + agent: http.Agent, + freed: number[], + id: number, +): Promise { + return new Promise((resolve) => { + const port = (server.address() as { port: number }).port; + const req = http.request( + { host: "127.0.0.1", port, path: "/s", agent: agent }, + (res) => { + res.resume(); + res.on("end", () => { + freed.push(id); + resolve(req.socket); + }); + }, + ); + req.end(); + }); +} + +server.listen(0, "127.0.0.1", async () => { + const keep = new http.Agent({ keepAlive: true }); + console.log(await get(keep, "/a")); + console.log(await get(keep, "/b")); + await new Promise((r) => setTimeout(r, 10)); + console.log( + "free", + Object.keys(keep.freeSockets).length, + "connections", + connections, + ); + console.log(await get(keep, "/close")); + await new Promise((r) => setTimeout(r, 10)); + console.log(await get(keep, "/after-close")); + console.log("connections", connections); + const one = new http.Agent({ keepAlive: true, maxSockets: 1 }); + const pending: Promise[] = [ + get(one, "/q1"), + get(one, "/q2"), + get(one, "/q3"), + ]; + const results: string[] = await Promise.all(pending); + console.log(results.join(" | ")); + console.log("connections", connections); + for (const scheduling of ["lifo", "fifo", undefined] as const) { + // the requests in the order their sockets were freed, then which of + // those sockets the next request reuses + const agent = new http.Agent({ keepAlive: true, scheduling }); + const freed: number[] = []; + const p0 = track(agent, freed, 0); + const p1 = track(agent, freed, 1); + const p2 = track(agent, freed, 2); + const s0 = await p0; + const s1 = await p1; + const s2 = await p2; + await new Promise((r) => setTimeout(r, 10)); + const next = await track(agent, [], 3); + const id = next === s0 ? 0 : next === s1 ? 1 : next === s2 ? 2 : -1; + console.log(scheduling, freed.indexOf(id)); + agent.destroy(); + } + for (const scheduling of ["LIFO", "", 1]) tryScheduling(scheduling); + keep.destroy(); + one.destroy(); + server.close(() => console.log("closed")); +}); diff --git a/tests/corpus/5597-http-agent-idle-sockets.ts b/tests/corpus/5597-http-agent-idle-sockets.ts new file mode 100644 index 000000000..881993de2 --- /dev/null +++ b/tests/corpus/5597-http-agent-idle-sockets.ts @@ -0,0 +1,79 @@ +// A pooled socket the server closes while idle leaves the agent's free +// list, so the next request dials a new connection. An https keepAlive +// agent reuses its TLS connections the same way (a servername that is +// empty or equal to the host keys like none), and idle pooled sockets do +// not keep the process alive. +import * as http from "node:http"; +import * as https from "node:https"; +import { readFileSync } from "node:fs"; + +let connections = 0; +const server = http.createServer((req, res) => res.end(`ok ${req.url}`)); +server.keepAliveTimeout = 100; +server.keepAliveTimeoutBuffer = 0; +server.on("connection", () => connections++); + +const cert = readFileSync("tests/fixtures/server/certs/localhost.pem"); +const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const ca = readFileSync("tests/fixtures/server/certs/ca.pem"); +let tlsConnections = 0; +const secure = https.createServer({ key, cert }, (req, res) => res.end(`secure ${req.url}`)); +secure.on("connection", () => tlsConnections++); + +function get(port: number, agent: http.Agent, path: string): Promise { + return new Promise((resolve) => { + const req = http.request({ host: "127.0.0.1", port, path, agent: agent }, (res) => { + let body = ""; + res.on("data", (c: Buffer) => (body += c.toString())); + res.on("end", () => resolve(`${body} reused=${req.reusedSocket}`)); + }); + req.end(); + }); +} + +server.listen(0, "127.0.0.1", async () => { + const port = (server.address() as { port: number }).port; + const agent = new http.Agent({ keepAlive: true }); + console.log(await get(port, agent, "/1")); + await new Promise((r) => setTimeout(r, 300)); + console.log("free after server timeout", Object.keys(agent.freeSockets).length); + console.log(await get(port, agent, "/2")); + console.log("connections", connections); + server.close(); + secure.listen(0, "127.0.0.1", async () => { + const sport = (secure.address() as { port: number }).port; + const sagent = new https.Agent({ keepAlive: true }); + for (const path of ["/s1", "/s2"]) { + const out = await new Promise((resolve) => { + const req = https.request({ host: "localhost", port: sport, path, agent: sagent, ca: ca }, (res) => { + let body = ""; + res.on("data", (c: Buffer) => (body += c.toString())); + res.on("end", () => resolve(`${body} reused=${req.reusedSocket}`)); + }); + req.on("error", (err: Error) => resolve(`error ${err.message}`)); + req.end(); + }); + console.log(out); + } + for (const name of ["", "localhost"]) { + const out = await new Promise((resolve) => { + const req = https.request( + { host: "localhost", port: sport, path: "/sn", agent: sagent, ca: ca, servername: name }, + (res) => { + let body = ""; + res.on("data", (c: Buffer) => (body += c.toString())); + res.on("end", () => resolve(`${body} servername=${JSON.stringify(name)} reused=${req.reusedSocket}`)); + }, + ); + req.on("error", (err: Error) => resolve(`error ${err.message}`)); + req.end(); + }); + console.log(out); + } + console.log("tls connections", tlsConnections); + secure.close(); + secure.closeIdleConnections(); + // the pooled client sockets are unref'd: the process exits with them open + }); +}); +process.on("exit", () => console.log("exit")); diff --git a/tests/corpus/5598-http-global-agent.ts b/tests/corpus/5598-http-global-agent.ts new file mode 100644 index 000000000..2a2e3b83e --- /dev/null +++ b/tests/corpus/5598-http-global-agent.ts @@ -0,0 +1,30 @@ +// Requests without an agent option go through http.globalAgent, which in +// Node 24 has keepAlive: true, so sequential requests share one +// connection, and the process still exits with the idle socket open. +import * as http from "node:http"; +import * as https from "node:https"; + +let connections = 0; +const server = http.createServer((req, res) => res.end(`ok ${req.url}`)); +server.on("connection", () => connections++); + +function get(port: number, path: string): Promise { + return new Promise((resolve) => { + const req = http.get({ host: "127.0.0.1", port, path }, (res) => { + let body = ""; + res.on("data", (c: Buffer) => (body += c.toString())); + res.on("end", () => resolve(`${body} reused=${req.reusedSocket}`)); + }); + }); +} + +console.log("global", http.globalAgent.keepAlive, http.globalAgent.maxSockets, https.globalAgent.keepAlive); +server.listen(0, "127.0.0.1", async () => { + const port = (server.address() as { port: number }).port; + console.log(await get(port, "/1")); + console.log(await get(port, "/2")); + console.log(await get(port, "/3")); + console.log("connections", connections, "free", Object.keys(http.globalAgent.freeSockets).length); + server.close(() => console.log("server closed")); +}); +process.on("exit", () => console.log("exit")); From d1639298ff7d393fbf127fb07be294464736886a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 19:39:16 -0700 Subject: [PATCH 002/120] Erase only `import type` edges, following Node's type stripping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node runs TypeScript with verbatimModuleSyntax semantics: `import type` and `export type … from` vanish, while `import { type A }` and `export { type A } from` keep an empty clause and still load their module. - Package selection skipped nothing: `import type { X } from "type-fest"` selected type-fest and failed with SC2013, and type imports pulled whole packages into the compile. Erased edges no longer select packages. - Files reached only through erased edges are no longer program modules, so their imports (a `node:vm` import, say) no longer fence the build. - Inline type-only clauses are real module edges again (their modules' side effects ran under Node but not in the binary); a cycle closed by one binds nothing and is admitted like a side-effect import. - Interfaces, type aliases and type literals from an installed package's declarations map structurally, so values the program builds with a type-only package's types compile. Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/dependencies.mdx | 2 + .../compiler/src/frontend/npm-static-prune.ts | 11 +- packages/compiler/src/frontend/pipeline.ts | 5 + packages/compiler/src/frontend/program.ts | 83 +++++----- packages/compiler/src/frontend/type-mapper.ts | 60 ++++++-- .../src/frontend/type-only-imports.ts | 143 ++++++++++++++++++ .../6120-type-only-import-graph/flags.ts | 2 + .../6120-type-only-import-graph/main.ts | 21 +++ .../6120-type-only-import-graph/mixed.ts | 8 + .../6120-type-only-import-graph/models.ts | 4 + .../node_modules/types-only-pkg/index.d.ts | 6 + .../node_modules/types-only-pkg/package.json | 5 + .../6120-type-only-import-graph/payload.ts | 4 + .../6120-type-only-import-graph/reexports.ts | 2 + .../6120-type-only-import-graph/tsconfig.json | 5 + .../6120-type-only-import-graph/uses-vm.ts | 4 + tests/corpus/6121-type-specifier-edges/a.ts | 10 ++ tests/corpus/6121-type-specifier-edges/b.ts | 7 + .../config-barrel.ts | 2 + .../6121-type-specifier-edges/config.ts | 4 + .../corpus/6121-type-specifier-edges/main.ts | 10 ++ .../node_modules/@types/twinned/index.d.ts | 5 + .../node_modules/@types/twinned/package.json | 5 + .../npm-static/node_modules/twinned/index.js | 9 ++ .../node_modules/twinned/package.json | 5 + tests/fixtures/npm-static/twinned-cli.ts | 5 + tests/fixtures/npm-static/type-only-cli.ts | 7 + tests/harness/npm-static.test.ts | 15 ++ 28 files changed, 382 insertions(+), 67 deletions(-) create mode 100644 packages/compiler/src/frontend/type-only-imports.ts create mode 100644 tests/corpus/6120-type-only-import-graph/flags.ts create mode 100644 tests/corpus/6120-type-only-import-graph/main.ts create mode 100644 tests/corpus/6120-type-only-import-graph/mixed.ts create mode 100644 tests/corpus/6120-type-only-import-graph/models.ts create mode 100644 tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts create mode 100644 tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json create mode 100644 tests/corpus/6120-type-only-import-graph/payload.ts create mode 100644 tests/corpus/6120-type-only-import-graph/reexports.ts create mode 100644 tests/corpus/6120-type-only-import-graph/tsconfig.json create mode 100644 tests/corpus/6120-type-only-import-graph/uses-vm.ts create mode 100644 tests/corpus/6121-type-specifier-edges/a.ts create mode 100644 tests/corpus/6121-type-specifier-edges/b.ts create mode 100644 tests/corpus/6121-type-specifier-edges/config-barrel.ts create mode 100644 tests/corpus/6121-type-specifier-edges/config.ts create mode 100644 tests/corpus/6121-type-specifier-edges/main.ts create mode 100644 tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/@types/twinned/package.json create mode 100644 tests/fixtures/npm-static/node_modules/twinned/index.js create mode 100644 tests/fixtures/npm-static/node_modules/twinned/package.json create mode 100644 tests/fixtures/npm-static/twinned-cli.ts create mode 100644 tests/fixtures/npm-static/type-only-cli.ts diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index f93952221..80414ef2a 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -46,6 +46,8 @@ Your callback and the commander implementation both compile to native code. No e scriptc finds packages in `node_modules` with Node.js's resolution rules, including the export conditions in `package.json`, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including `.js` import specifiers that resolve to `.ts` sources. +Type-only imports follow Node.js's type stripping. `import type` and `export type … from` statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as `type-fest`. A clause whose specifiers are all marked `type`, such as `import { type Options } from "./config.ts"`, still loads its module, as it does under Node.js. + ## Which packages compile scriptc compiles a package automatically when it: diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts index c6c1f9caf..2627e69e2 100644 --- a/packages/compiler/src/frontend/npm-static-prune.ts +++ b/packages/compiler/src/frontend/npm-static-prune.ts @@ -10,6 +10,7 @@ import { nearestPackageType, resolveBareModule } from "./resolve.js"; import { npmStaticPackageOfPath } from "./npm-static.js"; import { trackedReadFile } from "./input-tracker.js"; import { npmPackageNameOf } from "./workspace-registry.js"; +import { typeOnlyNamedImport } from "./type-only-imports.js"; export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclaration): boolean { return program.analysis.prunedNpmReexports.has(stmt); @@ -232,7 +233,9 @@ export function planNpmStaticReexports( (clause.namedBindings !== undefined && (ts.isNamespaceImport(clause.namedBindings) || (ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.length === 0))) + clause.namedBindings.elements.length === 0))) || + // `import { type A }` still loads the module, like `import {}`. + typeOnlyNamedImport(stmt) ) { request(dep, null); } else { @@ -244,12 +247,10 @@ export function planNpmStaticReexports( } } } else if (ts.isExportDeclaration(stmt)) { + // `export { type A } from` still loads the module under Node's + // type stripping; only `export type … from` erases. if ( stmt.isTypeOnly || - (stmt.exportClause !== undefined && - ts.isNamedExports(stmt.exportClause) && - stmt.exportClause.elements.length > 0 && - stmt.exportClause.elements.every((element) => element.isTypeOnly)) || stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier) ) diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 2ec9553f6..944877d58 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -28,6 +28,7 @@ import type { FrontendServices } from "./services.js"; import { isImportDeclaration, isExportDeclaration, isStringLiteral } from "./ts7/adapter.js"; import { CONTRACT_TYPES_MODULE } from "./contract-types.js"; import { THREADS_MODULE } from "./threads-import.js"; +import { erasedTypeOnlyImport } from "./type-only-imports.js"; import { EXPERIMENTAL_MODULE } from "./experimental-import.js"; export interface ProgramLoadOptions { @@ -107,6 +108,10 @@ function detectAutoPackages( stmt.moduleSpecifier && isStringLiteral(stmt.moduleSpecifier) ) { + // `import type` and `export type … from` erase under Node's type + // stripping: Node never loads the package, so it is no dependency + // to compile (its declarations still type the program). + if (isImportDeclaration(stmt) ? erasedTypeOnlyImport(stmt) : stmt.isTypeOnly) continue; edges.push({ spec: stmt.moduleSpecifier.text, loc: locOf(stmt) }); } else if (mode === "lib") { // CJS packages spell their dep edges as top-level requires; the diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 3a4ec1647..c805d47a7 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -119,6 +119,11 @@ import { import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; import { forkTargetPaths } from "./fork-target.js"; import { workerTargetPaths } from "./worker-target.js"; +import { + erasedTypeOnlyImport, + typeOnlyNamedImport, + typeOnlyReachedFiles, +} from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; @@ -1642,7 +1647,7 @@ function insideFunctionLike7(node: ts.Node): boolean { function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null { for (const stmt of sf.statements) { if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt; - if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly && !erasedTypeOnlyReexport(stmt)) { + if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly) { return stmt; } if (ts.isExportAssignment(stmt)) return stmt; @@ -2314,6 +2319,7 @@ export function makeCycleAdmission( const clause = e.stmt.importClause; if ( clause === undefined || + typeOnlyNamedImport(e.stmt) || (clause.name === undefined && (clause.namedBindings === undefined || (ts.isNamespaceImport(clause.namedBindings) && @@ -3579,21 +3585,41 @@ function preflight7(load: LoadResult): { npmStaticPackageOfPath(sf.fileName) !== null) && !unselectedPackageJsFile(sf.fileName), ); + // Files reached only through `import type` / `export type … from` are + // checker input, never program modules: Node does not load them, so + // their imports and syntax must not fence the program. + const resolveRuntimeEdge = ( + sf: ts.SourceFile, + spec: string, + resolutionKind?: "import" | "require", + ): ts.SourceFile | null => + resolveImport7(program, sf, spec, resolutionKind) ?? + npmStaticDepSf7(program, sf, spec, resolutionKind); + const typeOnlyFiles = typeOnlyReachedFiles( + program, + entry, + programFiles, + () => [ + ...forkTargetPaths(program, program.getImplementationSourceFiles()), + ...workerTargetPaths(program, program.getImplementationSourceFiles()), + ], + resolveRuntimeEdge, + ); + const runtimeFiles = + typeOnlyFiles.size === 0 ? programFiles : programFiles.filter((sf) => !typeOnlyFiles.has(sf)); const userFiles = npmStaticActive() ? planNpmStaticReexports( program, entry, - programFiles, + runtimeFiles, [ ...createRequireProgramRoots7(program), ...forkTargetPaths(program, program.getImplementationSourceFiles()), ...workerTargetPaths(program, program.getImplementationSourceFiles()), ], - (sf, spec, resolutionKind) => - resolveImport7(program, sf, spec, resolutionKind) ?? - npmStaticDepSf7(program, sf, spec, resolutionKind), + resolveRuntimeEdge, ) - : programFiles; + : runtimeFiles; program.getTypeChecker().prefetchSourceFileStructures(userFiles); // Unselected node_modules JS is not native program source even when @@ -3718,8 +3744,7 @@ function preflight7(load: LoadResult): { continue; } if (ts.isExportDeclaration(stmt)) { - if (stmt.isTypeOnly || erasedTypeOnlyReexport(stmt) || isPrunedNpmReexport(program, stmt)) - continue; + if (stmt.isTypeOnly || isPrunedNpmReexport(program, stmt)) continue; if (!stmt.moduleSpecifier) continue; const fromSpec = ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : ""; if (load.externalTypes.has(fromSpec)) { @@ -4954,42 +4979,6 @@ export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean return checked; } -/** `import type …` declarations and named clauses whose every element is - * `type`-qualified are ERASED by tsc at emit — Node's module graph has no - * edge there, so module order, cycle detection, init calls, and the - * startup-refusal probe must not count them either (zod v4's core modules - * are "cyclic" only through `import type * as ns` namespaces — 27 phantom - * SC1016 sites against 1 real value-level cycle; and a type-only import - * of a module Node cannot resolve is NOT a startup crash — Node never - * resolves it). Originated as the provenance-sources pilot's flag-gated - * prototype (SCRIPTC_TYPE_ONLY_EDGES), promoted to always-on with the - * full-suite differential green. Side-effect imports (no clause) stay - * real edges. */ -function erasedTypeOnlyImport(stmt: ts.ImportDeclaration): boolean { - const clause = stmt.importClause; - if (clause === undefined) return false; // side-effect import: a real edge - if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return true; - return ( - clause.name === undefined && - clause.namedBindings !== undefined && - ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.length > 0 && - clause.namedBindings.elements.every((e) => e.isTypeOnly) - ); -} - -/** The re-export twin: `export { type X } from "./m"` with every element - * `type`-qualified erases at emit exactly like `export type { X } from` - * (which stmt.isTypeOnly already covers) — no module edge. */ -function erasedTypeOnlyReexport(stmt: ts.ExportDeclaration): boolean { - return ( - stmt.exportClause !== undefined && - ts.isNamedExports(stmt.exportClause) && - stmt.exportClause.elements.length > 0 && - stmt.exportClause.elements.every((e) => e.isTypeOnly) - ); -} - export function orderedImportsOf( program: ts.Program, sf: ts.SourceFile, @@ -4997,11 +4986,7 @@ export function orderedImportsOf( const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = []; for (const stmt of sf.statements) { if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; - if ( - ts.isExportDeclaration(stmt) && - (stmt.isTypeOnly || erasedTypeOnlyReexport(stmt) || !stmt.moduleSpecifier) - ) - continue; + if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue; if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue; if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue; if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 54fb8f70e..7570a6ce0 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1656,16 +1656,38 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Declaration-only package types have no compiled implementation. // Primitive and container types retain their structural mappings; types // owned by the standard library or explicit host mappings are handled - // by their dedicated native rules. - const npmSym = widened.getAliasSymbol() ?? widened.getSymbol(); - const npmDecls = npmSym ? checker.declarationsOf(npmSym) : undefined; + // by their dedicated native rules. Pure type shapes — interfaces, type + // aliases, and object or mapped type literals with no value declaration + // merged in — describe data, not an implementation: a program value of + // such a type is one the program built itself (`import type` from a + // types-only package), so it maps structurally like a local declaration. + // Values the package itself would produce stay refused at their import. + const packageOnly = (sym: ts.Symbol | undefined): readonly ts.Node[] | null => { + const decls = sym ? checker.declarationsOf(sym) : undefined; + return decls && + decls.length > 0 && + decls.every((d) => { + const sf = d.getSourceFile(); + return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf); + }) + ? decls + : null; + }; + const pureTypeShape = (decls: readonly ts.Node[] | null): boolean => + decls === null || + decls.every( + (d) => + packageTypeFile(d.getSourceFile(), ctx) && + (ts.isInterfaceDeclaration(d) || + ts.isTypeAliasDeclaration(d) || + ts.isTypeLiteralNode(d) || + ts.isMappedTypeNode(d)), + ); + const aliasDecls = packageOnly(widened.getAliasSymbol()); + const ownDecls = packageOnly(widened.getSymbol()); if ( - npmDecls && - npmDecls.length > 0 && - npmDecls.every((d) => { - const sf = d.getSourceFile(); - return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf); - }) + (aliasDecls !== null || (widened.getAliasSymbol() === undefined && ownDecls !== null)) && + !(pureTypeShape(aliasDecls) && pureTypeShape(ownDecls)) ) { return null; } @@ -5167,6 +5189,17 @@ function isMappedShape(t: ts.Type): boolean { ); } +/** A declaration file an installed package ships (its own or an @types + * twin), outside the standard library and @types/node: the type surface + * of a package the program does not compile. Its pure type shapes — + * interfaces, aliases, type literals — describe data a program can build + * itself (`import type` from a types-only package such as type-fest). */ +function packageTypeFile(sf: ts.SourceFile, ctx: Pick): boolean { + return ( + sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf) + ); +} + /** The record path's provenance fence. Declared shapes (object literals, * interfaces, type literals) must come from user code or an explicitly * mapped external type surface, never an arbitrary .d.ts — the empty @@ -5205,13 +5238,14 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean { .every( (decl) => !decl.getSourceFile().isDeclarationFile || - ctx.isExternalTypeFile(decl.getSourceFile()), + ctx.isExternalTypeFile(decl.getSourceFile()) || + packageTypeFile(decl.getSourceFile(), ctx), ), ); } return !decls.some((d) => { const sf = d.getSourceFile(); - return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf); + return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }); } @@ -5668,7 +5702,7 @@ function mapRecordTypeInner( computed && checker.declarationsOf(p).some((d) => { const sf = d.getSourceFile(); - return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf); + return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) ) { return null; @@ -5995,7 +6029,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx computed && checker.declarationsOf(p).some((d) => { const sf = d.getSourceFile(); - return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf); + return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) ) { return null; diff --git a/packages/compiler/src/frontend/type-only-imports.ts b/packages/compiler/src/frontend/type-only-imports.ts new file mode 100644 index 000000000..003d4ccf7 --- /dev/null +++ b/packages/compiler/src/frontend/type-only-imports.ts @@ -0,0 +1,143 @@ +/** Type-only module edges under Node's type stripping. + * + * Node runs TypeScript by erasing types in place, with the semantics of + * `verbatimModuleSyntax`: `import type …` and `export type … from` vanish, + * but `import { type A } from "./m"` and `export { type A } from "./m"` + * keep an empty clause (`import {} from "./m"`) and still load and + * evaluate `./m`. Only the erased forms are absent from Node's module + * graph. */ +import * as ts from "./ts7/adapter.js"; +import { moduleSourceCandidates } from "./module-source-candidates.js"; + +/** `import type …` (default, named, or namespace): erased by Node, so + * module order, cycle detection, init calls, package selection, and the + * startup-refusal probe must not count it (zod v4's core modules are + * "cyclic" only through `import type * as ns` namespaces, and a + * type-only import of a module Node cannot resolve is not a startup + * crash). Side-effect imports and inline `type` specifiers stay real + * edges. */ +export function erasedTypeOnlyImport(stmt: ts.ImportDeclaration): boolean { + return stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword; +} + +/** `import { type A, type B } from "./m"`: a real module edge that binds + * nothing at runtime, exactly like a side-effect import. */ +export function typeOnlyNamedImport(stmt: ts.ImportDeclaration): boolean { + const clause = stmt.importClause; + return ( + clause !== undefined && + clause.phaseModifier !== ts.SyntaxKind.TypeKeyword && + clause.name === undefined && + clause.namedBindings !== undefined && + ts.isNamedImports(clause.namedBindings) && + clause.namedBindings.elements.length > 0 && + clause.namedBindings.elements.every((e) => e.isTypeOnly) + ); +} + +/** One module reference of a program file: its specifier and whether + * Node erases it. */ +interface ModuleReference { + spec: string; + erased: boolean; + kind?: "import" | "require"; +} + +function declarationReferencesOf(sf: ts.SourceFile): ModuleReference[] { + const refs: ModuleReference[] = []; + for (const stmt of sf.statements) { + if (ts.isImportDeclaration(stmt)) { + if (ts.isStringLiteral(stmt.moduleSpecifier)) + refs.push({ spec: stmt.moduleSpecifier.text, erased: erasedTypeOnlyImport(stmt) }); + } else if (ts.isExportDeclaration(stmt)) { + if (stmt.moduleSpecifier !== undefined && ts.isStringLiteral(stmt.moduleSpecifier)) + refs.push({ spec: stmt.moduleSpecifier.text, erased: stmt.isTypeOnly }); + } else if ( + ts.isImportEqualsDeclaration(stmt) && + ts.isExternalModuleReference(stmt.moduleReference) && + ts.isStringLiteralLike(stmt.moduleReference.expression) + ) { + refs.push({ + spec: stmt.moduleReference.expression.text, + erased: stmt.isTypeOnly, + kind: "require", + }); + } + } + return refs; +} + +/** Every call with a leading string literal counts as a loading edge — + * require(), import(), createRequire'd require functions, and loader + * helpers alike. Over-counting only keeps a file in the graph. */ +function callReferencesOf(program: ts.Program, sf: ts.SourceFile): ModuleReference[] { + const refs: ModuleReference[] = []; + for (const node of moduleSourceCandidates(program, sf).calls) { + const arg = node.arguments[0]; + if (arg === undefined || !ts.isStringLiteralLike(arg)) continue; + refs.push({ + spec: arg.text, + erased: false, + kind: node.expression.kind === ts.SyntaxKind.ImportKeyword ? "import" : "require", + }); + } + return refs; +} + +/** The program files Node never loads because the only references that + * reach them are erased type-only forms (directly or through other such + * files). A file referenced by nothing at all (a program root, or a file + * reached through a reference this walk does not model, such as an + * `import("./m")` type query) keeps its place, as do `extraRoots` (fork + * and worker targets). Empty when no erased reference resolves to a + * program file — the common case pays one statement scan per file. */ +export function typeOnlyReachedFiles( + program: ts.Program, + entry: ts.SourceFile, + files: readonly ts.SourceFile[], + extraRoots: () => readonly string[], + resolveEdge: ( + from: ts.SourceFile, + spec: string, + resolutionKind?: "import" | "require", + ) => ts.SourceFile | null, +): Set { + const available = new Set(files); + const declarationRefs = new Map(files.map((sf) => [sf, declarationReferencesOf(sf)] as const)); + const target = (sf: ts.SourceFile, ref: ModuleReference): ts.SourceFile | null => { + const dep = resolveEdge(sf, ref.spec, ref.kind); + return dep === null || dep === sf || !available.has(dep) ? null : dep; + }; + if ( + !files.some((sf) => + declarationRefs.get(sf)!.some((ref) => ref.erased && target(sf, ref) !== null), + ) + ) + return new Set(); + const valueEdges = new Map(); + const referenced = new Set(); + for (const sf of files) { + const deps: ts.SourceFile[] = []; + valueEdges.set(sf, deps); + for (const ref of [...declarationRefs.get(sf)!, ...callReferencesOf(program, sf)]) { + const dep = target(sf, ref); + if (dep === null) continue; + referenced.add(dep); + if (!ref.erased) deps.push(dep); + } + } + const roots = files.filter((sf) => sf === entry || !referenced.has(sf)); + for (const path of extraRoots()) { + const sf = program.getSourceFile(path); + if (sf !== undefined && available.has(sf)) roots.push(sf); + } + const reached = new Set(); + const queue = [...roots]; + while (queue.length > 0) { + const sf = queue.pop()!; + if (reached.has(sf)) continue; + reached.add(sf); + for (const dep of valueEdges.get(sf) ?? []) if (!reached.has(dep)) queue.push(dep); + } + return new Set(files.filter((sf) => !reached.has(sf))); +} diff --git a/tests/corpus/6120-type-only-import-graph/flags.ts b/tests/corpus/6120-type-only-import-graph/flags.ts new file mode 100644 index 000000000..5af639c0a --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/flags.ts @@ -0,0 +1,2 @@ +export type Flag = boolean; +console.log("flags evaluated"); diff --git a/tests/corpus/6120-type-only-import-graph/main.ts b/tests/corpus/6120-type-only-import-graph/main.ts new file mode 100644 index 000000000..251ce6809 --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/main.ts @@ -0,0 +1,21 @@ +// Node's type stripping erases `import type` and `export type … from`, so +// the modules and packages they name are never loaded: a package with no +// runtime entry, a module importing a builtin scriptc does not compile, and +// modules with load-time output. An inline `type` specifier keeps an empty +// import clause and still evaluates its module, as does a mixed clause. +import type { Frozen, Shape } from "types-only-pkg"; +import type { Label } from "./uses-vm.ts"; +import type * as Models from "./models.ts"; +import type { Reexported } from "./reexports.ts"; +import { type Mode, describe } from "./mixed.ts"; +import { type Flag } from "./flags.ts"; + +const shape: Shape = { width: 3, height: 4 }; +const frozen: Frozen = { width: 5, height: 6 }; +const label: Label = "area"; +const model: Models.Model = { id: 1 }; +const payload: Reexported = { ok: true }; +const mode: Mode = "fast"; +const flag: Flag = true; +console.log(label, shape.width * shape.height, model.id, payload.ok); +console.log(describe(mode), flag, frozen.width + frozen.height); diff --git a/tests/corpus/6120-type-only-import-graph/mixed.ts b/tests/corpus/6120-type-only-import-graph/mixed.ts new file mode 100644 index 000000000..ec5a08bbb --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/mixed.ts @@ -0,0 +1,8 @@ +export type { Payload } from "./payload.ts"; + +export type Mode = "fast" | "slow"; + +export function describe(mode: Mode): string { + return `mode=${mode}`; +} +console.log("mixed evaluated"); diff --git a/tests/corpus/6120-type-only-import-graph/models.ts b/tests/corpus/6120-type-only-import-graph/models.ts new file mode 100644 index 000000000..195cf9a17 --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/models.ts @@ -0,0 +1,4 @@ +export interface Model { + id: number; +} +console.log("models evaluated"); diff --git a/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts new file mode 100644 index 000000000..e9c1b5497 --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts @@ -0,0 +1,6 @@ +export interface Shape { + width: number; + height: number; +} + +export type Frozen = { readonly [K in keyof T]: T[K] }; diff --git a/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json new file mode 100644 index 000000000..c2be952b9 --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json @@ -0,0 +1,5 @@ +{ + "name": "types-only-pkg", + "version": "1.0.0", + "types": "./index.d.ts" +} diff --git a/tests/corpus/6120-type-only-import-graph/payload.ts b/tests/corpus/6120-type-only-import-graph/payload.ts new file mode 100644 index 000000000..beb97c5ac --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/payload.ts @@ -0,0 +1,4 @@ +export interface Payload { + ok: boolean; +} +console.log("payload evaluated"); diff --git a/tests/corpus/6120-type-only-import-graph/reexports.ts b/tests/corpus/6120-type-only-import-graph/reexports.ts new file mode 100644 index 000000000..52e86f294 --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/reexports.ts @@ -0,0 +1,2 @@ +export type { Payload as Reexported } from "./payload.ts"; +console.log("reexports evaluated"); diff --git a/tests/corpus/6120-type-only-import-graph/tsconfig.json b/tests/corpus/6120-type-only-import-graph/tsconfig.json new file mode 100644 index 000000000..aee0ec940 --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strict": true + } +} diff --git a/tests/corpus/6120-type-only-import-graph/uses-vm.ts b/tests/corpus/6120-type-only-import-graph/uses-vm.ts new file mode 100644 index 000000000..bed6646bf --- /dev/null +++ b/tests/corpus/6120-type-only-import-graph/uses-vm.ts @@ -0,0 +1,4 @@ +import * as vm from "node:vm"; + +export type Label = string; +console.log("uses-vm evaluated", vm.runInThisContext("1 + 1")); diff --git a/tests/corpus/6121-type-specifier-edges/a.ts b/tests/corpus/6121-type-specifier-edges/a.ts new file mode 100644 index 000000000..59d6385da --- /dev/null +++ b/tests/corpus/6121-type-specifier-edges/a.ts @@ -0,0 +1,10 @@ +import { b } from "./b.ts"; + +export interface AInfo { + tag: string; +} + +export function start(): string { + return `a+${b()}`; +} +console.log("a evaluated"); diff --git a/tests/corpus/6121-type-specifier-edges/b.ts b/tests/corpus/6121-type-specifier-edges/b.ts new file mode 100644 index 000000000..a71f62def --- /dev/null +++ b/tests/corpus/6121-type-specifier-edges/b.ts @@ -0,0 +1,7 @@ +import { type AInfo } from "./a.ts"; + +export function b(): string { + const info: AInfo = { tag: "b" }; + return info.tag; +} +console.log("b evaluated"); diff --git a/tests/corpus/6121-type-specifier-edges/config-barrel.ts b/tests/corpus/6121-type-specifier-edges/config-barrel.ts new file mode 100644 index 000000000..dc461f654 --- /dev/null +++ b/tests/corpus/6121-type-specifier-edges/config-barrel.ts @@ -0,0 +1,2 @@ +export { type Config } from "./config.ts"; +console.log("barrel evaluated"); diff --git a/tests/corpus/6121-type-specifier-edges/config.ts b/tests/corpus/6121-type-specifier-edges/config.ts new file mode 100644 index 000000000..1a42a9093 --- /dev/null +++ b/tests/corpus/6121-type-specifier-edges/config.ts @@ -0,0 +1,4 @@ +export interface Config { + name: string; +} +console.log("config evaluated"); diff --git a/tests/corpus/6121-type-specifier-edges/main.ts b/tests/corpus/6121-type-specifier-edges/main.ts new file mode 100644 index 000000000..23855b470 --- /dev/null +++ b/tests/corpus/6121-type-specifier-edges/main.ts @@ -0,0 +1,10 @@ +// An inline `type` specifier keeps its module request under Node's type +// stripping: `export { type X } from` and `import { type X } from` both +// evaluate the named module, once, in source order. A cycle closed only by +// such an import binds nothing at runtime and runs like a side-effect +// import. +import { type Config } from "./config-barrel.ts"; +import { start } from "./a.ts"; + +const config: Config = { name: "demo" }; +console.log("main", config.name, start()); diff --git a/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts b/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts new file mode 100644 index 000000000..ef7c0d6c6 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts @@ -0,0 +1,5 @@ +export interface Greeting { + text: string; +} + +export default function wait(text: string): string; diff --git a/tests/fixtures/npm-static/node_modules/@types/twinned/package.json b/tests/fixtures/npm-static/node_modules/@types/twinned/package.json new file mode 100644 index 000000000..2bdbbfa6e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/@types/twinned/package.json @@ -0,0 +1,5 @@ +{ + "name": "@types/twinned", + "version": "1.0.0", + "types": "index.d.ts" +} diff --git a/tests/fixtures/npm-static/node_modules/twinned/index.js b/tests/fixtures/npm-static/node_modules/twinned/index.js new file mode 100644 index 000000000..0aa0a79f0 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/twinned/index.js @@ -0,0 +1,9 @@ +// A CommonJS package without its own declarations: its types live in the +// separate @types/twinned package, and it depends on the untyped ms. +'use strict'; + +const ms = require('ms'); + +module.exports = function wait(text) { + return 'twinned:' + text + ':' + ms(90000); +}; diff --git a/tests/fixtures/npm-static/node_modules/twinned/package.json b/tests/fixtures/npm-static/node_modules/twinned/package.json new file mode 100644 index 000000000..14cce5ab2 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/twinned/package.json @@ -0,0 +1,5 @@ +{ + "name": "twinned", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/twinned-cli.ts b/tests/fixtures/npm-static/twinned-cli.ts new file mode 100644 index 000000000..e24383184 --- /dev/null +++ b/tests/fixtures/npm-static/twinned-cli.ts @@ -0,0 +1,5 @@ +// twinned's declarations come from @types/twinned; the package itself and +// its untyped ms dependency ship JavaScript only. +import wait from "twinned"; + +console.log(wait("ready")); diff --git a/tests/fixtures/npm-static/type-only-cli.ts b/tests/fixtures/npm-static/type-only-cli.ts new file mode 100644 index 000000000..9fd332331 --- /dev/null +++ b/tests/fixtures/npm-static/type-only-cli.ts @@ -0,0 +1,7 @@ +// `import type` erases under Node's type stripping: twinned is never +// loaded, so no package is selected or refused, and its declared +// interface types a value the program builds itself. +import type { Greeting } from "twinned"; + +const greeting: Greeting = { text: "hello" }; +console.log(greeting.text); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 9324d1c7f..398c35b8a 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -791,6 +791,21 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 120_000); + test("type-only imports select no package", async () => { + const entry = join(pilotRoot, "type-only-cli.ts"); + const { coverage } = analyze(entry); + expect(coverage.npmStatic ?? []).toEqual([]); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toEqual([]); + const binary = await buildStatic(entry, []); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, 180_000); + test("ms supports invalid duration strings without runtime fences", () => { const { coverage } = analyze(join(pilotRoot, "ms-invalid-cli.ts"), { npmStatic: ["ms"] }); expect(coverage.npmStatic).toEqual([{ package: "ms", status: "static" }]); From e79380ce17172766b583dc616c43f98d52bd7492 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 19:39:21 -0700 Subject: [PATCH 003/120] Name the runtime package in npm refusals and add transitive --npm-static - A package typed only by an @types twin was judged under the twin's name and refused with "its import resolves to '@types/x/index.d.ts', not to runtime JavaScript". Selection now resolves the runtime package, and the refusal names it with the --npm-static option that compiles it anyway. - Refusals the declaration and shipped-JS heuristics make carry the same remedy as a diagnostic hint; a dependency of a named package suggests the transitive form. - `--npm-static=pkg...` (and `"pkg..."` in a library profile) selects the package and every package its files import, nested require() calls included, without the eligibility heuristics. Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/cli.mdx | 2 +- docs/content/docs/dependencies.mdx | 4 +- packages/compiler/src/cli/usage.ts | 3 +- packages/compiler/src/coverage/report.ts | 2 + .../compiler/src/diagnostics/diagnostic.ts | 11 +- packages/compiler/src/executable/prepare.ts | 1 + packages/compiler/src/frontend/analysis.ts | 1 + packages/compiler/src/frontend/pipeline.ts | 134 +++++++++++++++--- tests/harness/npm-static.test.ts | 47 ++++++ 9 files changed, 179 insertions(+), 26 deletions(-) diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index a6f75c6d3..3180d1fa4 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -119,7 +119,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
On Linux, when the program calls process.exit(), hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.
--npm-static <pkg[,pkg…]|auto>
-
Name npm packages to compile even if they don't ship their own type declarations (repeatable). auto uses the default automatic selection. Packages that can't compile produce diagnostics. See npm Dependencies.
+
Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing ..., as in fs-extra..., also names every package that package depends on. auto uses the default automatic selection. Packages that can't compile produce diagnostics. See npm Dependencies.
--provenance-sources
Experimental. Compile npm dependencies from the source at the commit their provenance attestation names. Packages without a usable attestation compile from their installed files.
diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 80414ef2a..c611fa1d5 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -55,7 +55,7 @@ scriptc compiles a package automatically when it: - ships its own type declarations, and - ships readable, unminified JavaScript. -A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. +A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway. @@ -69,7 +69,7 @@ Some packages rely on separately installed declarations, such as an `@types/` pa $ scriptc build app.ts --npm-static=three -o app >/dev/null ``` -Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated. +Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated. ## Unsupported code inside a package diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index a0388374b..cf099b5fd 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -57,7 +57,8 @@ Options: compile the named npm packages' shipped JS statically as program modules (repeatable; "auto" opts in every eligible direct import: own .d.ts, unminified JS, no build-transform - markers). Packages that fail preflight are refused with a + markers; "pkg..." also names every package pkg depends + on). Packages that fail preflight are refused with a diagnostic. Eligible dependencies compile automatically. --provenance-sources EXPERIMENTAL: compile npm dependencies from their diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 9d0776d9a..e8d81ac01 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -61,6 +61,8 @@ export interface NpmStaticStatus { status: "static" | "refused"; /** The first refusal reason for a refused package. */ detail?: string; + /** How to act on the refusal, when there is a specific remedy. */ + hint?: string; } const GREEN = "\x1b[32m"; diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 9a7b4a37a..1902f6b18 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -877,13 +877,20 @@ export function unsupportedPackageValueDiag(pkg: string, loc: SrcLoc): ScrDiagno ); } -export function npmStaticRefusalDiag(pkg: string, detail: string, loc: SrcLoc): ScrDiagnostic { +export function npmStaticRefusalDiag( + pkg: string, + detail: string, + loc: SrcLoc, + hint?: string, +): ScrDiagnostic { return { code: "SC2013", message: `cannot compile npm package '${pkg}' statically: ${detail}`, loc, milestone: "later", - hint: "use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback", + hint: + hint ?? + "use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback", }; } diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts index f6012dd2a..ead13afb8 100644 --- a/packages/compiler/src/executable/prepare.ts +++ b/packages/compiler/src/executable/prepare.ts @@ -59,6 +59,7 @@ export function prepareExecutableModule( entry.package, entry.detail ?? "its static compilation was refused", fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 }, + entry.hint, ), ), ); diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index b38a267fa..8435710c2 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -58,6 +58,7 @@ export function analyzeWithFrontend( entry.package, entry.detail ?? "its static compilation was refused", fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 }, + entry.hint, ), ), ]; diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 944877d58..20df75c9b 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -25,7 +25,15 @@ import { isJsSourceFileName } from "./tsc-codes.js"; import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js"; import { lowerToIr, type LowerOptions, type LowerResult } from "./lowering/lowerer.js"; import type { FrontendServices } from "./services.js"; -import { isImportDeclaration, isExportDeclaration, isStringLiteral } from "./ts7/adapter.js"; +import { + isIdentifier, + isImportDeclaration, + isExportDeclaration, + isStringLiteral, + isStringLiteralLike, + SyntaxKind, +} from "./ts7/adapter.js"; +import { moduleSourceCandidates } from "./module-source-candidates.js"; import { CONTRACT_TYPES_MODULE } from "./contract-types.js"; import { THREADS_MODULE } from "./threads-import.js"; import { erasedTypeOnlyImport } from "./type-only-imports.js"; @@ -92,15 +100,36 @@ function detectAutoPackages( mode: "auto" | "lib" = "auto", judged?: Set, sites?: Map, + transitive: Set = new Set(), ): string[] { + const chosen: string[] = []; // package → the resolved types file AND the file whose import found it: // the runtime-JS probe below must resolve from the SAME importing file, // or a package visible only to a nested package.json realm (a pnpm // monorepo's packages/*/node_modules, unreachable from the entry's own // walk-up) answers "no runtime JS" for perfectly ordinary installs. - const seen = new Map(); - for (const sf of [...load.moduleOrder, load.entry]) { + const seen = new Map< + string, + { typesFile: string; fromFile: string; spec: string; owner: string | null } + >(); + // A transitive package's files outside the module order (reached through + // nested require() calls the graph walk does not model) still name its + // dependencies. + const scanned = new Set([...load.moduleOrder, load.entry]); + const transitiveFiles = + transitive.size === 0 + ? [] + : load.program.getImplementationSourceFiles().filter((sf) => { + const pkg = npmStaticPackageOfPath(sf.fileName); + return !scanned.has(sf) && pkg !== null && transitive.has(pkg); + }); + for (const sf of [...scanned, ...transitiveFiles]) { if (mode === "auto" && sf.fileName.includes("/node_modules/")) continue; + // `--npm-static=pkg...` opts pkg's whole dependency closure in: every + // package its compiled files import is selected the same explicit way, + // without the declaration eligibility bar. + const owner = npmStaticPackageOfPath(sf.fileName); + const viaTransitive = owner !== null && transitive.has(owner); const edges: { spec: string; loc: SrcLoc }[] = []; for (const stmt of sf.statements) { if ( @@ -113,12 +142,26 @@ function detectAutoPackages( // to compile (its declarations still type the program). if (isImportDeclaration(stmt) ? erasedTypeOnlyImport(stmt) : stmt.isTypeOnly) continue; edges.push({ spec: stmt.moduleSpecifier.text, loc: locOf(stmt) }); - } else if (mode === "lib") { + } else if (mode === "lib" && !viaTransitive) { // CJS packages spell their dep edges as top-level requires; the // import-statement scan alone would miss every one of them. for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) }); } } + // Transitive selection counts every literal require() and import() + // anywhere in the package, nested ones included. + if (viaTransitive) { + for (const call of moduleSourceCandidates(load.program, sf).calls) { + const arg = call.arguments[0]; + if ( + arg !== undefined && + isStringLiteralLike(arg) && + (call.expression.kind === SyntaxKind.ImportKeyword || + (isIdentifier(call.expression) && call.expression.text === "require")) + ) + edges.push({ spec: arg.text, loc: locOf(call) }); + } + } for (const { spec, loc } of edges) { if (load.externalTypes?.has(spec)) continue; // scriptc's own packages are not npm dependencies: contract types are @@ -142,28 +185,49 @@ function detectAutoPackages( if (judged?.has(packageNameOfSpecifier(spec))) continue; const npm = resolveNpmImport(sf.fileName, spec); if (npm !== null && isNodeTypesPath(npm.typesFile)) continue; - if (npm === null) { - if (mode !== "lib") continue; - const js = resolveBareModule(sf.fileName, spec, "js-only"); - if (js === null || judged!.has(js.packageName)) continue; - judged!.add(js.packageName); - sites!.set(js.packageName, loc); + // The runtime package Node loads. Types-first resolution answers an + // @types twin's declarations for a package that ships none, but + // selection and every refusal name the package that has the code. + const js = + mode === "lib" && (npm === null || npm.packageName.startsWith("@types/")) + ? resolveBareModule(sf.fileName, spec, "js-only") + : null; + const runtimePkg = js !== null && js.packageName !== npm?.packageName ? js.packageName : null; + if (npm === null || runtimePkg !== null) { + if (mode !== "lib" || runtimePkg === null || judged!.has(runtimePkg)) continue; + judged!.add(runtimePkg); + sites!.set(runtimePkg, loc); + if (viaTransitive) { + transitive.add(runtimePkg); + chosen.push(runtimePkg); + continue; + } statuses.push({ - package: js.packageName, + package: runtimePkg, status: "refused", - detail: "it ships no own .d.ts declaration surface", + detail: + npm === null + ? "it ships no own .d.ts declaration surface" + : `its declared types come from the separate ${npm.packageName} package, not the package itself`, + hint: npmStaticOptInHint(runtimePkg, owner), }); continue; } if (judged?.has(npm.packageName)) continue; + if (viaTransitive) { + judged!.add(npm.packageName); + sites!.set(npm.packageName, loc); + transitive.add(npm.packageName); + chosen.push(npm.packageName); + continue; + } if (!seen.has(npm.packageName)) { - seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec }); + seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec, owner }); sites?.set(npm.packageName, loc); } } } - const chosen: string[] = []; - for (const [pkg, { typesFile, fromFile, spec }] of seen) { + for (const [pkg, { typesFile, fromFile, spec, owner }] of seen) { judged?.add(pkg); // A package without a root entry (no "main", "exports" or index file) // can still serve the subpath the program imports. @@ -180,11 +244,23 @@ function detectAutoPackages( package: pkg, status: "refused", detail: mode === "lib" ? reason : `auto: ${reason}`, + // Naming a package bypasses the declaration and shipped-JS + // heuristics; it cannot supply JavaScript that does not resolve. + ...(mode === "lib" && jsEntry !== null ? { hint: npmStaticOptInHint(pkg, owner) } : {}), }); } return chosen; } +/** How to compile a package the declaration bar refused anyway: name it, + * or (for a dependency of an opted-in package) opt in that package's + * whole dependency closure. */ +function npmStaticOptInHint(pkg: string, importer: string | null): string { + return importer === null + ? `to compile it anyway, name it with --npm-static=${pkg} (or --npm-static=${pkg}... to include its dependencies)` + : `to compile it anyway, name it with --npm-static=${pkg}, or use --npm-static=${importer}... to include all of ${importer}'s dependencies`; +} + /** The opted-in packages a consumer-anchored tsc message NAMES: module * specifiers in `Module '"spec"'` phrasings, and resolved file paths in * `import("…")` type spellings — the two ways the checker points at an @@ -232,9 +308,23 @@ export function runFrontend( // selections bypass eligibility heuristics, but never compilation checks. const explicit: readonly string[] = npmStatic !== undefined && typeof npmStatic !== "string" ? npmStatic : []; - return loadFrontend(entryPath, trackedLoader, "lib", externalTypes, [ - ...new Set([...libraryNpmStatic, ...explicit]), - ]); + // A trailing "..." (`--npm-static=fs-extra...`) selects the package and + // its whole dependency closure, which the library fixpoint discovers. + const transitive = new Set(); + const names = [...libraryNpmStatic, ...explicit].map((name) => { + if (!name.endsWith("...") || name.length === 3) return name; + const base = name.slice(0, -3); + transitive.add(base); + return base; + }); + return loadFrontend( + entryPath, + trackedLoader, + "lib", + externalTypes, + [...new Set(names)], + transitive, + ); } catch (error) { for (const load of active) { // Preserve the original failure and attempt every remaining cleanup. @@ -254,6 +344,7 @@ function loadFrontend( npmStatic?: readonly string[] | "auto" | "lib", externalTypes?: Readonly>, libraryNpmStatic: readonly string[] = [], + transitive: Set = new Set(), ): Frontend { // Resolver package/workspace metadata is intentionally shared across the // several load attempts of ONE auto-detection fixpoint, but never across @@ -275,7 +366,10 @@ function loadFrontend( const scoutPreflight = checkPreflight(scout); requested = npmStatic === "lib" - ? [...libraryNpmStatic, ...detectAutoPackages(scout, statuses, "lib", judged, npmSites)] + ? [ + ...libraryNpmStatic, + ...detectAutoPackages(scout, statuses, "lib", judged, npmSites, transitive), + ] : detectAutoPackages(scout, statuses); // With no package to opt in, the scout already IS the final frontend: // same roots, resolution posture, preflight, and module order. Retain it @@ -333,7 +427,7 @@ function loadFrontend( // at least one new package for good). if (npmStatic === "lib") { for (;;) { - const grown = detectAutoPackages(load, statuses, "lib", judged, npmSites); + const grown = detectAutoPackages(load, statuses, "lib", judged, npmSites, transitive); if (grown.length === 0) break; requested = [...requested, ...grown]; load.dispose(); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 398c35b8a..e144f428b 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -787,10 +787,56 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { package: "ms", status: "refused", detail: "it ships no own .d.ts declaration surface", + hint: "to compile it anyway, name it with --npm-static=ms (or --npm-static=ms... to include its dependencies)", }, ]); }, 120_000); + // Types-first resolution answers @types/twinned, but the refusal names the + // package that has the code and the flag that compiles it. + test("an @types-typed package is refused under its runtime name with the opt-in remedy", () => { + const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts")); + expect(coverage.npmStatic).toEqual([ + { + package: "twinned", + status: "refused", + detail: + "its declared types come from the separate @types/twinned package, not the package itself", + hint: "to compile it anyway, name it with --npm-static=twinned (or --npm-static=twinned... to include its dependencies)", + }, + ]); + const diagnostic = coverage.diagnostics.find((d) => d.code === "SC2013"); + expect(diagnostic?.message).toContain("'twinned'"); + expect(diagnostic?.hint).toContain("--npm-static=twinned"); + }, 120_000); + + test("a named package's untyped dependency points at the transitive opt-in", () => { + const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts"), { npmStatic: ["twinned"] }); + expect(coverage.npmStatic).toContainEqual({ + package: "ms", + status: "refused", + detail: "it ships no own .d.ts declaration surface", + hint: "to compile it anyway, name it with --npm-static=ms, or use --npm-static=twinned... to include all of twinned's dependencies", + }); + }, 120_000); + + test("--npm-static=pkg... compiles the package's dependency closure", async () => { + const entry = join(pilotRoot, "twinned-cli.ts"); + const { coverage } = analyze(entry, { npmStatic: ["twinned..."] }); + expect(coverage.npmStatic).toEqual([ + { package: "twinned", status: "static" }, + { package: "ms", status: "static" }, + ]); + expect(coverage.preflightFailed).toBe(false); + const binary = await buildStatic(entry, ["twinned..."]); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8")); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, 180_000); + test("type-only imports select no package", async () => { const entry = join(pilotRoot, "type-only-cli.ts"); const { coverage } = analyze(entry); @@ -937,6 +983,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { package: "typegapped", status: "refused", detail: "it ships no own .d.ts declaration surface", + hint: "to compile it anyway, name it with --npm-static=typegapped (or --npm-static=typegapped... to include its dependencies)", }, { package: "escape-string-regexp", status: "static" }, ]); From 27f06f41dbf3c3d4042944f2c6fdd06ea12833d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 00:22:29 -0500 Subject: [PATCH 004/120] Carry Hash and Hmac values through unknown and any slots A Hash or Hmac that flows into an unknown or any slot, or into untyped JavaScript code, is now a native handle (Hash or Hmac) instead of a compile error. The handle keeps the value's identity, so a cast back to Hash or Hmac answers the same value. update(), digest() and Hash's copy() dispatch through it, called directly or read as values: update is one function for Hash and Hmac and digest is one per class, as in Node 24.22, and update() answers the receiver. The stream and event members throw a coded SC2020 error there, as do the methods called with another receiver; other names read as undefined. The to-dyn conversion boxes a Hash or Hmac through scr_crypto_hash_to_dyn, which installs the handle ops on first use, so programs that never box one link none of the handle code. A native handle read through `as any` now dispatches its method calls through the handle. Calling copy() on an Hmac throws Node's TypeError ("mac.copy is not a function") after the arguments are evaluated, instead of the method-call fence. Corpus 5768 and 5769 cover the TypeScript and JavaScript forms against Node 24.22, and errors.test.ts covers the SC2020 cases. corpus-costs.json records their plain and sanitized costs. --- .../limitations/builtin-apis/crypto.mdx | 2 +- .../generated/node-v24-backlog.json | 24 +++ .../generated/node-v24-internal.json | 32 +++ internal/compatibility/static-support.json | 2 +- packages/compiler/src/backend/llvm/dyn.ts | 22 ++- .../src/frontend/lowering/builtins/crypto.ts | 22 ++- .../src/frontend/lowering/lower-calls.ts | 6 + packages/compiler/src/ir/ir.ts | 2 + .../corpus/5768-crypto-hash-unknown.ts.json | 6 + .../corpus/5769-crypto-hash-unknown.cjs.json | 6 + packages/runtime/src/scr_crypto.h | 4 + packages/runtime/src/scr_dyn.h | 2 + packages/runtime/src/scr_lib.c | 187 ++++++++++++++++++ tests/corpus/5768-crypto-hash-unknown.ts | 68 +++++++ tests/corpus/5769-crypto-hash-unknown.cjs | 77 ++++++++ tests/harness/corpus-costs.json | 4 + tests/harness/errors.test.ts | 37 ++++ 17 files changed, 497 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json create mode 100644 tests/corpus/5768-crypto-hash-unknown.ts create mode 100644 tests/corpus/5769-crypto-hash-unknown.cjs diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 586e0b1fd..988228aff 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -1 +1 @@ -- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller. +- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 0046a5acc..40f2f1ca2 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -6729,6 +6729,8 @@ "source": "compiler-dedicated:crypto.createHash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -6767,6 +6769,8 @@ "source": "compiler-dedicated:crypto.createHmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -7389,6 +7393,8 @@ "source": "compiler-dedicated:crypto.hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -7512,6 +7518,8 @@ "source": "compiler-dedicated:crypto.pbkdf2", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -7550,6 +7558,8 @@ "source": "compiler-dedicated:crypto.pbkdf2Sync", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9030,6 +9040,8 @@ "source": "compiler-dedicated:Hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9068,6 +9080,8 @@ "source": "compiler-dedicated:hash.copy", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9106,6 +9120,8 @@ "source": "compiler-dedicated:hash.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9144,6 +9160,8 @@ "source": "compiler-dedicated:hash.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9182,6 +9200,8 @@ "source": "compiler-dedicated:Hmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9220,6 +9240,8 @@ "source": "compiler-dedicated:hmac.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -9258,6 +9280,8 @@ "source": "compiler-dedicated:hmac.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 67d79376e..d0313ae14 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19849,6 +19849,8 @@ "tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -20390,6 +20392,8 @@ "evidence": "compiler-dedicated:crypto.createHash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -20427,6 +20431,8 @@ "evidence": "compiler-dedicated:crypto.createHmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -21028,6 +21034,8 @@ "evidence": "compiler-dedicated:crypto.hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -21146,6 +21154,8 @@ "evidence": "compiler-dedicated:crypto.pbkdf2", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -21183,6 +21193,8 @@ "evidence": "compiler-dedicated:crypto.pbkdf2Sync", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23083,6 +23095,8 @@ "evidence": "compiler-dedicated:Hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23120,6 +23134,8 @@ "evidence": "compiler-dedicated:hash.copy", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23157,6 +23173,8 @@ "evidence": "compiler-dedicated:hash.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23194,6 +23212,8 @@ "evidence": "compiler-dedicated:hash.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23231,6 +23251,8 @@ "evidence": "compiler-dedicated:Hmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23268,6 +23290,8 @@ "evidence": "compiler-dedicated:hmac.digest", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -23305,6 +23329,8 @@ "evidence": "compiler-dedicated:hmac.update", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -25019,6 +25045,8 @@ "evidence": "compiler-dedicated:crypto.pbkdf2", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -28102,6 +28130,8 @@ "evidence": "compiler-dedicated:Hash", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", @@ -28152,6 +28182,8 @@ "evidence": "compiler-dedicated:Hmac", "tests": [ "tests/corpus/5760-crypto-hash-stream.ts", + "tests/corpus/5768-crypto-hash-unknown.ts", + "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 252c19d71..9791b6594 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -370,7 +370,7 @@ { "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] }, - { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, + { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, { "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] }, { "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] }, { "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] }, diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86a8a945e..0ea840254 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -84,6 +84,16 @@ const DYN_HANDLE_TAG_NUM: Record = { child: 16, fileHandle: 17, dir: 44, + cryptoHash: 49, + cryptoHmac: 50, +}; + +/** Handle kinds whose runtime unit boxes them through its own function, + * which installs the tag's ops on first use (scr_crypto.h). Programs that + * never box one of these values do not link its dynamic members. */ +const DYN_HANDLE_BOX_FN: Record = { + cryptoHash: "scr_crypto_hash_to_dyn", + cryptoHmac: "scr_crypto_hash_to_dyn", }; export const DYN_KIND = { @@ -2932,9 +2942,17 @@ export class LlDyn { // tag's installed ops. const h = DYN_HANDLE_KINDS.get(t.kind); if (h) { - host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`); + const box = DYN_HANDLE_BOX_FN[t.kind]; const r = B.tmp(); - B.line(`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`); + if (box) { + host.declare(`declare ptr @${box}(ptr)`); + B.line(`${r} = call ptr @${box}(ptr %v)`); + } else { + host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`); + B.line( + `${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`, + ); + } B.terminate(`ret ptr ${r}`); break; } diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 2202088fa..ae6ddc951 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1,6 +1,6 @@ import { lowerBuiltinByteInput, errorFirstBytesCallback, errorFirstCallback } from "./arguments.js"; import { isKeyObjectValue } from "./key-object.js"; -import { boolLit, dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { boolLit, dynUndefinedExpr, nodeThrowExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -940,9 +940,27 @@ export function lowerCryptoHashMethodCall( if (call.questionDotToken || access.questionDotToken) return null; const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (receiverType?.kind !== "cryptoHash" && receiverType?.kind !== "cryptoHmac") return null; - if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); + // Hmac has no copy(): Node reads the member as undefined and throws a + // TypeError once the arguments are evaluated. + if (receiverType.kind === "cryptoHmac" && name === "copy") { + if (call.arguments.some(ts.isSpreadElement)) + lowerer.noLowering("Hmac.copy with spread arguments", call); + const evaluated = [access.expression, ...call.arguments].map((node): IrStmt => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(node), + loc, + })); + return { + kind: "seqExpr", + stmts: evaluated, + result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc), + type: DYN, + loc, + }; + } + if (!lowerer.isStdlibMember(access)) return null; const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, receiverType); const hash = receiverType.kind === "cryptoHash"; const label = hash ? "Hash" : "Hmac"; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2ba83a3d5..27abe2115 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -33,6 +33,7 @@ import { BOOL, CAUGHT, DYN, + DYN_HANDLE_KINDS, F64, type IrExpr, type IrFunction, @@ -3252,6 +3253,11 @@ function lowerDynReceiverMethodCall( lowerStringMethodCall(lowerer, call, access, () => recv) ); } + // A native handle read through `as any` (`(hash as any).update(x)`) + // dispatches through the handle, as the same value held in an + // 'unknown' slot does. + if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind)) + recv = lowerer.coerceToExpected(recv, DYN); if (recv.type.kind !== "dyn") return null; if ( access.name.text === "normalize" && diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index eaee503fa..d99c77da1 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3879,6 +3879,8 @@ export const DYN_HANDLE_KINDS: ReadonlyMap ["http2Stream", { tag: "SCR_DYNH_H2_STREAM", cls: "Http2Stream" }], ["httpClientReq", { tag: "SCR_DYNH_HTTP_CLIENT", cls: "ClientRequest" }], ["dir", { tag: "SCR_DYNH_DIR", cls: "Dir" }], + ["cryptoHash", { tag: "SCR_DYNH_HASH", cls: "Hash" }], + ["cryptoHmac", { tag: "SCR_DYNH_HMAC", cls: "Hmac" }], ]); /** A class value that crosses an `unknown` slot as a compiler-owned typed diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json new file mode 100644 index 000000000..ba6370e68 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5768-crypto-hash-unknown.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5768-crypto-hash-unknown.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json new file mode 100644 index 000000000..60e1632e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5769-crypto-hash-unknown.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5769-crypto-hash-unknown.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 6b70a7fab..e106a5ab4 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -25,6 +25,10 @@ ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h); void scr_crypto_hash_release(ScrCryptoHash *h); void *scr_crypto_hash_retain_v(void *p); void scr_crypto_hash_release_v(void *p); +/* A Hash or Hmac boxed as a checked-dynamic handle (+1; retains h). The + * emitted to-dyn conversion calls it, and it installs the handle ops on + * first use, so programs that never box a Hash do not link them. */ +ScrDyn *scr_crypto_hash_to_dyn(ScrCryptoHash *h); ScrCryptoHash *scr_crypto_hash_update_str(ScrCryptoHash *h, ScrStr *data); ScrCryptoHash *scr_crypto_hash_update_bytes(ScrCryptoHash *h, ScrBytes *data); ScrCryptoHash *scr_crypto_hash_copy(ScrCryptoHash *h); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 375ced573..f0b0538bf 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -168,6 +168,8 @@ typedef enum { SCR_DYNH_DECIPHER, /* node:crypto Decipheriv (scr_cipher.c) */ SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */ SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */ + SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */ + SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 5f595f263..21534d36a 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -8031,6 +8031,193 @@ void scr_crypto_hash_release(ScrCryptoHash *hash) { void *scr_crypto_hash_retain_v(void *p) { return scr_crypto_hash_retain(p); } void scr_crypto_hash_release_v(void *p) { scr_crypto_hash_release(p); } +/* Hash and Hmac values seen as 'unknown' are native handles. update(), + * digest() and Hash's copy() dispatch through the handle, called or read + * as values; the other members Node defines on a Hash or Hmac (the + * Transform and emitter surface) throw a coded SC2020 error, and other + * names read as undefined, as they do in Node. The member names are one + * NUL-separated char array, which the linker drops from programs that + * never box a Hash (an array of string pointers keeps the strings in the + * unit's shared string section). */ +static const char scr_crypto_hash_dyn_members[] = + "_destroy\0_events\0_eventsCount\0_final\0_flush\0_maxListeners\0_options\0_read\0" + "_readableState\0_transform\0_undestroy\0_writableState\0_write\0_writev\0addListener\0" + "allowHalfOpen\0closed\0compose\0constructor\0cork\0destroy\0destroyed\0digest\0drop\0" + "emit\0end\0errored\0eventNames\0every\0filter\0find\0flatMap\0forEach\0getMaxListeners\0" + "isPaused\0iterator\0listenerCount\0listeners\0map\0off\0on\0once\0pause\0pipe\0" + "prependListener\0prependOnceListener\0push\0rawListeners\0read\0readable\0" + "readableAborted\0readableBuffer\0readableDidRead\0readableEncoding\0readableEnded\0" + "readableFlowing\0readableHighWaterMark\0readableLength\0readableObjectMode\0reduce\0" + "removeAllListeners\0removeListener\0resume\0setDefaultEncoding\0setEncoding\0" + "setMaxListeners\0some\0take\0toArray\0uncork\0unpipe\0unshift\0update\0wrap\0writable\0" + "writableBuffer\0writableCorked\0writableEnded\0writableFinished\0writableHighWaterMark\0" + "writableLength\0writableNeedDrain\0writableObjectMode\0write\0"; + +static bool scr_crypto_hash_dyn_member(const ScrCryptoHash *hash, const char *key, size_t len) { + if (len == 4 && memcmp(key, "copy", 4) == 0) return !hash->hmac; + for (const char *name = scr_crypto_hash_dyn_members; *name; name += strlen(name) + 1) + if (strlen(name) == len && memcmp(name, key, len) == 0) return true; + return false; +} + +static void scr_crypto_hash_dyn_unsupported(const ScrCryptoHash *hash) { + static const char hash_msg[] = + "Hash members other than update(), digest() and copy() have no lowering on an 'unknown' " + "value; cast the value to Hash first"; + static const char hmac_msg[] = + "Hmac members other than update() and digest() have no lowering on an 'unknown' value; " + "cast the value to Hmac first"; + if (hash->hmac) scr_throw_error_msg_code(SCR_ERR_ERROR, hmac_msg, sizeof hmac_msg - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, hash_msg, sizeof hash_msg - 1, "SC2020"); +} + +/* The method values: update() is one function for Hash and Hmac, as in + * Node; digest() is one per class, and copy() is Hash's. */ +enum { SCR_HASH_DYN_UPDATE, SCR_HASH_DYN_HASH_DIGEST, SCR_HASH_DYN_HMAC_DIGEST, SCR_HASH_DYN_COPY }; +static const char *const scr_crypto_hash_dyn_method_names[] = {"update", "digest", "digest", "copy"}; +static SCR_TL ScrDyn *scr_crypto_hash_dyn_methods[4]; +static SCR_TL bool scr_crypto_hash_dyn_methods_registered; + +static void scr_crypto_hash_dyn_methods_cleanup(void) { + for (size_t i = 0; i < 4; i++) { + ScrDyn *method = scr_crypto_hash_dyn_methods[i]; + scr_crypto_hash_dyn_methods[i] = NULL; + scr_dyn_release(method); + } +} + +static int scr_crypto_hash_dyn_method_index(const ScrCryptoHash *hash, const char *key, size_t len) { + if (len == 6 && memcmp(key, "update", 6) == 0) return SCR_HASH_DYN_UPDATE; + if (len == 6 && memcmp(key, "digest", 6) == 0) + return hash->hmac ? SCR_HASH_DYN_HMAC_DIGEST : SCR_HASH_DYN_HASH_DIGEST; + if (len == 4 && memcmp(key, "copy", 4) == 0 && !hash->hmac) return SCR_HASH_DYN_COPY; + return -1; +} + +/* Runs method `index` on `hash`; `self` is the receiving box, which + * update() answers. */ +static ScrDyn *scr_crypto_hash_dyn_run(ScrCryptoHash *hash, ScrDyn *self, int index, + ScrDyn *const *args, size_t argc) { + ScrDyn *undefined = scr_dyn_undefined(); + ScrDyn *first = argc > 0 ? args[0] : undefined; + ScrDyn *second = argc > 1 ? args[1] : undefined; + ScrDyn *result = NULL; + if (index == SCR_HASH_DYN_UPDATE) { + ScrCryptoHash *updated = scr_crypto_hash_update_dyn(hash, first, second); + if (updated) { + scr_crypto_hash_release(updated); + result = scr_dyn_retain(self); + } + } else if (index == SCR_HASH_DYN_COPY) { + ScrCryptoHash *copy = scr_crypto_hash_copy_opts(hash, first); + if (copy) { + result = scr_dyn_new_handle(copy, SCR_DYNH_HASH); + scr_crypto_hash_release(copy); + } + } else { + result = scr_crypto_hash_digest_dyn(hash, first); + } + scr_dyn_release(undefined); + return result; +} + +static ScrDyn *scr_crypto_hash_dyn_method_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + int index = (int)scr_box_get_f64(clo->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + bool hash_this = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_HASH; + bool hmac_this = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_HMAC; + bool valid = index == SCR_HASH_DYN_UPDATE ? hash_this || hmac_this + : index == SCR_HASH_DYN_HMAC_DIGEST ? hmac_this + : hash_this; + ScrDyn *result = NULL; + if (valid) { + result = scr_crypto_hash_dyn_run(self->v.handle.ptr, self, index, args, argc); + } else { + static const char msg[] = + "Hash and Hmac methods called with a receiver other than their own Hash or Hmac have no " + "lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020"); + } + scr_dyn_release(self); + return result; +} + +static ScrDyn *scr_crypto_hash_dyn_method_value(int index) { + if (!scr_crypto_hash_dyn_methods_registered) { + scr_crypto_hash_dyn_methods_registered = true; + scr_atexit(scr_crypto_hash_dyn_methods_cleanup); + } + if (!scr_crypto_hash_dyn_methods[index]) { + ScrClosure *clo = scr_closure_new(NULL, 1); + clo->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(clo->caps[0], (double)index); + uint32_t arity = index == SCR_HASH_DYN_UPDATE ? 2 : 1; + scr_crypto_hash_dyn_methods[index] = scr_dyn_new_func( + clo, scr_crypto_hash_dyn_method_call, arity, "", scr_crypto_hash_dyn_method_names[index]); + } + return scr_dyn_retain(scr_crypto_hash_dyn_methods[index]); +} + +static ScrDyn *scr_crypto_hash_dyn_invoke(void *h, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + ScrCryptoHash *hash = h; + size_t len = strlen(method); + int index = scr_crypto_hash_dyn_method_index(hash, method, len); + if (index >= 0) return scr_crypto_hash_dyn_run(hash, self, index, args, argc); + if (scr_crypto_hash_dyn_member(hash, method, len)) { + scr_crypto_hash_dyn_unsupported(hash); + return NULL; + } + char message[160]; + int n = snprintf(message, sizeof message, "%s is not a function", what); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return NULL; +} + +static ScrDyn *scr_crypto_hash_dyn_get(void *h, const char *key, size_t len) { + int index = scr_crypto_hash_dyn_method_index(h, key, len); + if (index >= 0) return scr_crypto_hash_dyn_method_value(index); + if (scr_crypto_hash_dyn_member(h, key, len)) scr_crypto_hash_dyn_unsupported(h); + return NULL; +} + +static bool scr_crypto_hash_dyn_set(void *h, const char *key, size_t len, const ScrDyn *value) { + (void)h; + (void)key; + (void)len; + (void)value; + return false; +} + +static void scr_crypto_hash_dyn_install(void) { + static SCR_TL bool installed; + if (installed) return; + installed = true; + static const ScrDynHandleOps hash_ops = { + .cls = "Hash", + .retain = scr_crypto_hash_retain_v, + .release = scr_crypto_hash_release_v, + .invoke = scr_crypto_hash_dyn_invoke, + .get = scr_crypto_hash_dyn_get, + .set = scr_crypto_hash_dyn_set, + }; + static const ScrDynHandleOps hmac_ops = { + .cls = "Hmac", + .retain = scr_crypto_hash_retain_v, + .release = scr_crypto_hash_release_v, + .invoke = scr_crypto_hash_dyn_invoke, + .get = scr_crypto_hash_dyn_get, + .set = scr_crypto_hash_dyn_set, + }; + scr_dyn_handle_install(SCR_DYNH_HASH, &hash_ops); + scr_dyn_handle_install(SCR_DYNH_HMAC, &hmac_ops); +} + +ScrDyn *scr_crypto_hash_to_dyn(ScrCryptoHash *hash) { + scr_crypto_hash_dyn_install(); + return scr_dyn_new_handle(hash, hash->hmac ? SCR_DYNH_HMAC : SCR_DYNH_HASH); +} + static ScrCryptoHash *scr_crypto_hash_update_raw(ScrCryptoHash *hash, const unsigned char *data, size_t len) { if (!scr_crypto_hash_updatable(hash)) return NULL; diff --git a/tests/corpus/5768-crypto-hash-unknown.ts b/tests/corpus/5768-crypto-hash-unknown.ts new file mode 100644 index 000000000..7948dc758 --- /dev/null +++ b/tests/corpus/5768-crypto-hash-unknown.ts @@ -0,0 +1,68 @@ +// Hash and Hmac values held in 'unknown' and 'any' slots: update(), +// digest() and Hash's copy() dispatch through the value, update() answers +// the same value, a cast back to Hash or Hmac keeps its identity, and the +// errors after digest() are Node's. An Hmac has no copy(). +import { createHash, createHmac, type Hash, type Hmac } from "node:crypto"; + +function code(run: () => unknown): string { + try { + const value = run(); + return typeof value === "string" ? `ok ${value.slice(0, 16)}` : `ok ${typeof value}`; + } catch (error) { + const e = error as Error & { code?: string }; + return `${e.name} ${e.code ?? ""} ${e.message}`; + } +} + +function hexOf(value: any): string { + return value.update("abc").digest("hex"); +} + +console.log("any param", hexOf(createHash("sha256"))); +console.log("any param hmac", hexOf(createHmac("sha256", "key"))); + +const hash = createHash("sha1"); +const held: unknown = hash; +console.log("typeof", typeof held, String(held), Object.prototype.toString.call(held)); +console.log("cast keeps identity", (held as Hash) === hash); +const chained: any = held; +console.log("update answers the value", chained.update("a") === chained); +console.log("typed digest after untyped update", hash.digest("hex")); +console.log("digest again", code(() => chained.digest("hex"))); +console.log("update after digest", code(() => chained.update("x"))); + +const values: unknown[] = [createHash("md5"), createHmac("sha512", "k"), createHash("sha256")]; +for (const value of values) { + const v = value as any; + v.update("one"); + v.update(Buffer.from("two")); + v.update("74687265", "hex"); + console.log("array", v.digest("base64")); +} + +const record: { label: string; hash: unknown } = { label: "rec", hash: createHash("sha384") }; +const fromRecord = record.hash as Hash; +fromRecord.update("record"); +console.log(record.label, fromRecord.digest("hex").slice(0, 32)); + +const original: any = createHash("sha256").update("prefix"); +const copy = original.copy(); +copy.update("-copy"); +original.update("-original"); +console.log("copy", copy.digest("hex").slice(0, 16), original.digest("hex").slice(0, 16)); +console.log("copy is a new value", copy === original); +const shake: any = createHash("shake256"); +shake.update("x"); +console.log("copy outputLength", shake.copy({ outputLength: 8 }).digest("hex")); +console.log("copy bad outputLength", code(() => shake.copy({ outputLength: -1 }))); + +const mac: unknown = createHmac("sha256", "secret"); +const macAny: any = mac; +console.log("hmac copy", typeof macAny.copy, macAny.copy === undefined); +console.log("hmac copy call", code(() => macAny.copy())); +console.log("hmac digest", (mac as Hmac).update("data").digest("hex").slice(0, 16)); +console.log("hmac digest again", JSON.stringify(macAny.digest("hex"))); + +console.log("missing member", macAny.nope, typeof chained.missing); +console.log("bad data", code(() => (createHash("sha256") as any).update(42))); +console.log("default digest", (createHash("md5") as any).update("z").digest().length); diff --git a/tests/corpus/5769-crypto-hash-unknown.cjs b/tests/corpus/5769-crypto-hash-unknown.cjs new file mode 100644 index 000000000..90782757c --- /dev/null +++ b/tests/corpus/5769-crypto-hash-unknown.cjs @@ -0,0 +1,77 @@ +// Hash and Hmac values passed through untyped JavaScript code: function +// parameters, object fields, arrays and Map values. update() answers the +// same value, digest() and copy() follow Node, and Hmac has no copy(). +const crypto = require("node:crypto"); + +function code(run) { + try { + const value = run(); + return typeof value === "string" ? `ok ${value.slice(0, 16)}` : `ok ${typeof value}`; + } catch (error) { + return `${error.name} ${error.code || ""} ${error.message}`; + } +} + +function digestAll(items, encoding) { + return items.map((item) => item.update("payload").digest(encoding)); +} + +console.log(digestAll([crypto.createHash("sha256"), crypto.createHmac("sha1", "k")], "hex")); + +const box = { hash: crypto.createHash("md5"), mac: crypto.createHmac("sha256", Buffer.from("key")) }; +box.hash.update("a").update("b"); +console.log("field", box.hash.digest("hex"), box.mac.update("c").digest("base64url")); + +const byName = new Map(); +byName.set("sha512", crypto.createHash("sha512")); +byName.set("hmac", crypto.createHmac("md5", "x")); +for (const [name, value] of byName) { + const same = value.update(name); + console.log("map", name, same === value, value.digest("hex").slice(0, 20)); +} + +function copyTwice(h) { + const a = h.copy(); + const b = h.copy(); + a.update("1"); + b.update("2"); + return [a.digest("hex").slice(0, 12), b.digest("hex").slice(0, 12), h.digest("hex").slice(0, 12)]; +} +console.log("copies", copyTwice(crypto.createHash("sha256").update("base"))); + +function describe(value) { + return [typeof value, String(value), Object.prototype.toString.call(value), value.notAMember]; +} +console.log("hash", describe(crypto.createHash("sha256"))); +console.log("hmac", describe(crypto.createHmac("sha256", "k"))); +const plainMac = [crypto.createHmac("sha256", "k")][0]; +console.log("hmac copy read", typeof plainMac.copy, plainMac.copy === undefined); + +const mac = crypto.createHmac("sha256", "k"); +const viaObject = { mac }; +console.log("hmac copy", code(() => viaObject.mac.copy())); +console.log("digest", viaObject.mac.digest("hex").slice(0, 16), JSON.stringify(viaObject.mac.digest("hex"))); +console.log("update after digest", code(() => viaObject.mac.update("x"))); + +const used = [crypto.createHash("sha1")]; +used[0].digest(); +console.log("digest twice", code(() => used[0].digest("hex"))); +console.log("copy after digest", code(() => used[0].copy())); +console.log("update bad", code(() => [crypto.createHash("sha1")][0].update(null))); + +function methods(h, m) { + return [h.update === m.update, h.digest === m.digest, typeof h.copy, typeof m.copy, + h.update.name, h.update.length, h.digest.name, h.digest.length, h.copy.length]; +} +console.log("methods", methods(crypto.createHash("sha256"), crypto.createHmac("sha1", "k"))); +function viaCall(h) { + const update = h.update; + const digest = h.digest; + return [update.call(h, "abc") === h, digest.call(h, "hex").slice(0, 16)]; +} +console.log("call", viaCall(crypto.createHash("sha256")), viaCall(crypto.createHmac("sha256", "k"))); +function sharedUpdate(h, m) { + h.update.call(m, "shared"); + return m.digest("hex").slice(0, 16); +} +console.log("shared update", sharedUpdate(crypto.createHash("md5"), crypto.createHmac("md5", "k"))); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 8ffc03b9a..2825d99ac 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1841,6 +1841,8 @@ "5648-dns-lookup-forms.ts": 2992, "5649-dns-service-servers.ts": 2712, "5650-dns-lookup-errors-untyped.cjs": 2822, + "5768-crypto-hash-unknown.ts": 3470, + "5769-crypto-hash-unknown.cjs": 3490, "5833-url-search-params-iterators.ts": 2678, "5834-url-search-params-iterators-dynamic.mjs": 2696, "5836-url-search-params-tuple-pairs.ts": 2118, @@ -4482,6 +4484,8 @@ "5648-dns-lookup-forms.ts": 3416, "5649-dns-service-servers.ts": 4916, "5650-dns-lookup-errors-untyped.cjs": 3889, + "5768-crypto-hash-unknown.ts": 3960, + "5769-crypto-hash-unknown.cjs": 4090, "5833-url-search-params-iterators.ts": 5339, "5834-url-search-params-iterators-dynamic.mjs": 3797, "5836-url-search-params-tuple-pairs.ts": 3193, diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 28daa88d3..53f09192d 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -928,6 +928,43 @@ hash.end("abc"); }); }); +// A Hash or Hmac held in an 'unknown' slot answers update(), digest() and +// Hash's copy(). Its other members throw a coded SC2020 error, and so do +// those methods called with another receiver. +describe("Hash and Hmac members through 'unknown'", () => { + test("throw SC2020 other than update, digest and copy", async () => { + const r = await compileAndRun( + "hash-unknown-members", + `const crypto = require("node:crypto"); +function attempt(value, run) { + try { + run(value); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +const hash = [crypto.createHash("sha256")][0]; +attempt(hash, (h) => h.on("data", () => {})); +attempt(hash, (h) => h.readable); +attempt([crypto.createHmac("sha256", "k")][0], (m) => m.write("x")); +attempt(hash, (h) => h.digest.call({})); +console.log(hash.update("abc").digest("hex").slice(0, 8)); +`, + "cjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "Error SC2020 Hash members other than update(), digest() and copy() have no lowering on an 'unknown' value; cast the value to Hash first\n".repeat( + 2, + ) + + "Error SC2020 Hmac members other than update() and digest() have no lowering on an 'unknown' value; cast the value to Hmac first\n" + + "Error SC2020 Hash and Hmac methods called with a receiver other than their own Hash or Hmac have no lowering\n" + + "ba7816bf\n", + ); + expect(r.stderr).toBe(""); + }); +}); + // Node 24.22 skips the chacha20-poly1305 tag check when neither // setAuthTag() nor authTagLength was used, and returns unverified // plaintext. scriptc keeps Node 24.15's behavior: final() fails. From a3feec9b3554299a7672498189817c965e11da89 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 04:11:02 -0500 Subject: [PATCH 005/120] Add ThreadSlots, per-thread values keyed by objects, to @scriptc/experimental ThreadSlots holds one value per key and thread, like a WeakMap that each thread has its own copy of; under Node.js it is a WeakMap per name and thread. Setting a value never writes the key, so values can be set on published keys. Classes used as keys carry a 32-bit slot word after their header words, assigned on the first set for a key (with a compare-and-swap for a published key) and never reused. Each family has one block per thread that maps slot indices to values through pages of 1024 entries. get and has are inline reads of the slot word, the page and the entry; set and delete call scr_slots.c, which holds one reference to each value. A key class's free paths release the key's values, and thread exit releases the rest. --- benchmarks/runtime/README.md | 1 + benchmarks/runtime/workloads.json | 20 ++ benchmarks/runtime/workloads/thread-slots.ts | 122 +++++++++ docs/content/docs/experimental.mdx | 93 ++++++- docs/test/fixtures/migration-baseline.json | 16 ++ .../compiler/src/backend/cycle-mutability.ts | 12 +- packages/compiler/src/backend/llvm/classes.ts | 64 ++++- packages/compiler/src/backend/llvm/emitter.ts | 15 ++ .../src/backend/llvm/reference-writes.ts | 7 +- packages/compiler/src/backend/llvm/regions.ts | 4 +- .../src/backend/llvm/stack-objects.ts | 4 + .../src/backend/llvm/thread-slots.test.ts | 142 ++++++++++ .../compiler/src/backend/llvm/thread-slots.ts | 170 ++++++++++++ .../src/backend/native/runtime-inputs.ts | 1 + .../src/frontend/experimental-import.ts | 33 ++- .../src/frontend/lowering/lower-classes.ts | 3 + .../src/frontend/lowering/lower-exprs.ts | 8 + .../compiler/src/frontend/lowering/lowerer.ts | 21 +- .../src/frontend/lowering/thread-slots.ts | 254 ++++++++++++++++++ packages/compiler/src/frontend/type-mapper.ts | 16 +- .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 18 +- packages/compiler/src/ir/validate.ts | 40 +++ .../corpus/5727-thread-slots-basics.ts.json | 6 + .../corpus/5728-thread-slots-workers.ts.json | 6 + .../corpus/5729-thread-slots-values.ts.json | 6 + .../diagnostics/thread-slots-fences.ts.json | 6 + packages/experimental/README.md | 45 +++- packages/experimental/index.d.ts | 21 ++ packages/experimental/index.js | 38 +++ packages/experimental/package.json | 2 +- .../runtime-pack-matrix.mjs | 1 + .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_runtime.h | 1 + packages/runtime/src/scr_slots.c | 145 ++++++++++ packages/runtime/src/scr_slots.h | 73 +++++ packages/runtime/src/scr_slots.test.c | 113 ++++++++ packages/runtime/src/scr_slots.test.ts | 52 ++++ tests/corpus/5727-thread-slots-basics.ts | 85 ++++++ tests/corpus/5728-thread-slots-workers.ts | 107 ++++++++ tests/corpus/5729-thread-slots-values.ts | 89 ++++++ tests/diagnostics/thread-slots-fences.ts | 29 ++ .../__snapshots__/thread-slots-fences.ts.txt | 62 +++++ tests/harness/corpus-costs.json | 6 + tests/harness/wasi-differential.test.ts | 3 + 45 files changed, 1934 insertions(+), 28 deletions(-) create mode 100644 benchmarks/runtime/workloads/thread-slots.ts create mode 100644 packages/compiler/src/backend/llvm/thread-slots.test.ts create mode 100644 packages/compiler/src/backend/llvm/thread-slots.ts create mode 100644 packages/compiler/src/frontend/lowering/thread-slots.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json create mode 100644 packages/runtime/src/scr_slots.c create mode 100644 packages/runtime/src/scr_slots.h create mode 100644 packages/runtime/src/scr_slots.test.c create mode 100644 packages/runtime/src/scr_slots.test.ts create mode 100644 tests/corpus/5727-thread-slots-basics.ts create mode 100644 tests/corpus/5728-thread-slots-workers.ts create mode 100644 tests/corpus/5729-thread-slots-values.ts create mode 100644 tests/diagnostics/thread-slots-fences.ts create mode 100644 tests/harness/__snapshots__/thread-slots-fences.ts.txt diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index ed9ee0532..a3c8c73b9 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -17,6 +17,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | +| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | | `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs | | `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index c0d643a58..1526b7ec9 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -127,6 +127,26 @@ "100000" ] }, + { + "name": "thread-slots", + "entry": "workloads/thread-slots.ts", + "args": [ + "slots", + "8", + "50000", + "20" + ] + }, + { + "name": "thread-slots-cache", + "entry": "workloads/thread-slots.ts", + "args": [ + "cache", + "8", + "50000", + "20" + ] + }, { "name": "atomics-contended", "entry": "workloads/atomics-contended.ts", diff --git a/benchmarks/runtime/workloads/thread-slots.ts b/benchmarks/runtime/workloads/thread-slots.ts new file mode 100644 index 000000000..9791b5a1a --- /dev/null +++ b/benchmarks/runtime/workloads/thread-slots.ts @@ -0,0 +1,122 @@ +// Per-thread state for shared objects: worker threads read the same +// published type graph from an InternTable and keep per-thread state for +// each type they visit (a resolved flag and a visit count), then walk the +// graph many times. Arguments: the store ("slots" for a ThreadSlots family, +// "cache" for a direct-mapped cache by id in front of a Map by id, the +// layout programs use without slots), the thread count, the type count and +// the number of walks. Under Node.js both stores are per-thread. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; +import { InternTable, ThreadSlots } from "@scriptc/experimental"; + +class Type { + readonly id: number; + readonly members: readonly Type[]; + constructor(id: number, members: readonly Type[]) { + this.id = id; + this.members = members; + } +} + +class TypeState { + resolved = false; + visits = 0; +} + +const types = new InternTable("bench.slot-types"); +const states = new ThreadSlots("bench.type-states"); + +const CACHE = 1024; +const cacheIds: number[] = new Array(CACHE).fill(-1); +const cacheStates: (TypeState | undefined)[] = new Array(CACHE).fill( + undefined, +); +const byId = new Map(); + +function slotState(t: Type): TypeState { + let state = states.get(t); + if (state === undefined) { + state = new TypeState(); + states.set(t, state); + } + return state; +} + +function cachedState(t: Type): TypeState { + const slot = t.id & (CACHE - 1); + if (cacheIds[slot] === t.id) return cacheStates[slot]!; + let state = byId.get(t.id); + if (state === undefined) { + state = new TypeState(); + byId.set(t.id, state); + } + cacheIds[slot] = t.id; + cacheStates[slot] = state; + return state; +} + +function typeOf(id: number): Type { + return types.getOrInsertComputed(id, () => { + const members: Type[] = []; + if (id >= 3) members.push(typeOf(Math.floor(id / 3))); + if (id >= 7) members.push(typeOf(id - 7)); + return new Type(id, members); + }); +} + +function run(useSlots: boolean, count: number, walks: number): string { + const all: Type[] = []; + for (let id = 0; id < count; id++) all.push(typeOf(id)); + // Visits follow member edges from a pseudo-random start, as a checker's + // relation checks do: neighbours are often visited together. + let seed = 1; + let resolved = 0; + let visits = 0; + for (let w = 0; w < walks; w++) { + for (let i = 0; i < count; i++) { + seed = (seed * 48271) % 2147483647; + let t = all[seed % count]!; + for (let depth = 0; depth < 4; depth++) { + const state = useSlots ? slotState(t) : cachedState(t); + if (!state.resolved) { + state.resolved = true; + resolved++; + } + state.visits++; + visits++; + if (t.members.length === 0) break; + t = t.members[depth % t.members.length]!; + } + } + } + return `${resolved} resolved, ${visits} visits`; +} + +if (isMainThread) { + const store = process.argv[2] ?? "slots"; + const threads = Number(process.argv[3] ?? "8"); + const count = Number(process.argv[4] ?? "50000"); + const walks = Number(process.argv[5] ?? "20"); + const results: string[] = []; + let pending = threads; + for (let i = 0; i < threads; i++) { + const worker = new Worker(new URL(import.meta.url), { + workerData: { useSlots: store === "slots", count, walks }, + }); + worker.on("message", (text: string) => { + results[i] = text; + }); + worker.on("exit", () => { + if (--pending === 0) { + const same = results.every((r) => r === results[0]); + console.log(`${threads} threads: ${results[0]}, same in every thread ${same}`); + } + }); + } +} else { + const { useSlots, count, walks } = workerData as { + useSlots: boolean; + count: number; + walks: number; + }; + parentPort!.postMessage(run(useSlots, count, walks)); +} diff --git a/docs/content/docs/experimental.mdx b/docs/content/docs/experimental.mdx index ac5891ec9..e7ab2d194 100644 --- a/docs/content/docs/experimental.mdx +++ b/docs/content/docs/experimental.mdx @@ -183,8 +183,99 @@ No lock is held while a callback runs. Two threads that miss the same key at the 2. It does not compare identities or ids across threads. 3. A thread does not depend on another thread's inserts: `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js. -State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. +State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a [`ThreadSlots`](#threadslots) family, a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. ### Memory Lookups take no lock and write nothing. Inserts lock one of 64 segments of the table for a probe and a few stores. Stored values are immortal in scriptc builds: they stay allocated until the process exits, including a value a callback computed for a key that another thread stored first. Programs that never construct an `InternTable` do not run any of its code. + +## ThreadSlots + +Threads that share values through an `InternTable` often compute more about each shared value later: resolved members, cached results, flags. That state differs per thread, so it cannot live in the shared value, which is immutable. A `ThreadSlots` family holds one value per key and thread, like a `WeakMap` that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares. + +```ts:slots.ts +import { Worker, isMainThread, parentPort, threadId } from "node:worker_threads"; +import { InternTable, ThreadSlots } from "@scriptc/experimental"; + +class Shape { + readonly name: string; + constructor(name: string) { + this.name = name; + } +} + +class ShapeState { + uses = 0; + readonly thread: number; + constructor(thread: number) { + this.thread = thread; + } +} + +const shapes = new InternTable("example.slot-shapes"); +// Module scope: every thread that evaluates this line opens the same family. +const states = new ThreadSlots("example.shape-states"); + +function use(name: string): ShapeState { + const shape = shapes.getOrInsertComputed(name, () => new Shape(name)); + let state = states.get(shape); + if (state === undefined) { + state = new ShapeState(threadId); + states.set(shape, state); + } + state.uses++; + return state; +} + +if (isMainThread) { + for (let i = 0; i < 2; i++) { + new Worker(new URL(import.meta.url)).on("message", (text: string) => console.log(text)); + } +} else { + use("square"); + const state = use("square"); + parentPort!.postMessage(`worker: ${state.uses} uses, own state ${state.thread === threadId}`); +} +``` + +In the scriptc executable both workers read the same `Shape` object from the table, and each keeps its own `ShapeState` for it. Both runtimes print the same: + +```console +$ node slots.ts +worker: 2 uses, own state true +worker: 2 uses, own state true +$ scriptc build slots.ts -o slots >/dev/null +$ ./slots +worker: 2 uses, own state true +worker: 2 uses, own state true +``` + +### Slot methods + +
+
new ThreadSlots<K, V>(name)
+
Opens the family called name. Within one thread every construction with the same name refers to the same family.
+
slots.get(key), slots.has(key)
+
Read the calling thread's value for key.
+
slots.set(key, value)
+
Sets the calling thread's value for key and returns the slots. It never writes the key, so it works on published keys.
+
slots.delete(key)
+
Removes the calling thread's value for key and returns whether there was one.
+
+ +Under Node.js each family is a `WeakMap` per thread. + +### Keys and values + +scriptc requires: + +- the name to be a string literal, and every construction of one name to use the same type arguments; +- the key type to be a class declared in the program; keys may be instances of its subclasses; +- the value type to be a class, an object type, an array, a `Map` or a `Set`; +- the methods to be called through a module-level `const` initialized with `new ThreadSlots("name")`, as in the example. A family's storage is found by its name when the program is compiled. + +### Slot cost + +Each instance of a key class, and of every class in its hierarchy, carries a 4-byte slot index, 0 until a value is set for it. The first `set` for a key assigns the index; indices are never reused. `get` and `has` read the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family stores values in pages of 1024 entries, allocated as the thread sets values in them. + +A family holds a reference to each of the thread's values. A value is released when it is replaced or deleted, when its key is freed, and when the thread exits. Keys that are freed with a [`LifetimeRegion`](#lifetimeregion) keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. Programs that never construct a `ThreadSlots` do not run any of its code, and their classes have no slot index. diff --git a/docs/test/fixtures/migration-baseline.json b/docs/test/fixtures/migration-baseline.json index 6aaab5f70..1b99faf9a 100644 --- a/docs/test/fixtures/migration-baseline.json +++ b/docs/test/fixtures/migration-baseline.json @@ -296,6 +296,22 @@ { "text": "Memory", "id": "memory" + }, + { + "text": "ThreadSlots", + "id": "threadslots" + }, + { + "text": "Slot methods", + "id": "slot-methods" + }, + { + "text": "Keys and values", + "id": "keys-and-values" + }, + { + "text": "Slot cost", + "id": "slot-cost" } ] }, diff --git a/packages/compiler/src/backend/cycle-mutability.ts b/packages/compiler/src/backend/cycle-mutability.ts index c7d476527..79993fdff 100644 --- a/packages/compiler/src/backend/cycle-mutability.ts +++ b/packages/compiler/src/backend/cycle-mutability.ts @@ -97,16 +97,20 @@ export function computeCycleMutability( // can store; a commit rewrites every field, reference fields possibly // with fresh conversions). Without any such store in the program, // crossing into checked-dynamic code writes nothing. FFI calls and - // intrinsics other than publish (which only marks objects immortal) and - // the intern-table operations (which store published values) stay - // conservative. + // intrinsics other than publish (which only marks objects immortal), the + // intern-table operations (which store published values) and ThreadSlots + // reads stay conservative. const dynStores = dynamicFieldStores(mod); const dynamicStores = dynStores.all || dynStores.names.size > 0; result.emptyPropertyBags = !dynamicStores; const boundaryWrites = (e: IrExpr): boolean => dynamicStores || e.kind === "ffiCall" || - (e.kind === "intrinsic" && e.name !== "threads.publish" && !e.name.startsWith("itable.")); + (e.kind === "intrinsic" && + e.name !== "threads.publish" && + !e.name.startsWith("itable.") && + e.name !== "slots.get" && + e.name !== "slots.has"); // Types that can become checked-dynamic values: mark their whole closure. const exposedTypes = new Set(); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 8a13d83ad..02036e4d9 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -75,6 +75,10 @@ export class LlClassMeta { membership: { pre: number; post: number }[] = []; /** Type-only references retain layouts without retaining construction. */ constructorRetained = false; + /** The hierarchy holds ThreadSlots keys (IrModule.threadSlotKeys): every + * member's struct carries the i32 slot word before its fields + * (classSlotWordIndex). */ + threadSlot = false; constructor(readonly def: IrClassDef) { this.root = this; @@ -116,6 +120,12 @@ export function buildClassGraph( if (meta.base === null) number(meta, meta); meta.hierarchy = meta.base !== null || meta.children.length > 0; } + const slotRoots = new Set(); + for (const key of mod.threadSlotKeys ?? []) { + const meta = metaMap.get(key); + if (meta !== undefined) slotRoots.add(meta.root); + } + for (const meta of metaMap.values()) meta.threadSlot = slotRoots.has(meta.root); const declares = (m: LlClassMeta, method: string): boolean => m.def.methods?.includes(method) ?? false; const declaredBelow = (m: LlClassMeta, method: string): boolean => @@ -323,16 +333,31 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null { return RUNTIME_PREFIXES.get(meta.root.def.name) ?? null; } -/** The GEP index where a class's own fields start: rc at 0, the vtable - * word at 1 on hierarchy members, then a runtime prefix (state + display - * name) on classes rooted at a runtime base, then the stream-state slot - * on stream-rooted ones. */ -function fieldBase(meta: LlClassMeta): number { +/** The GEP index after a class's header words: rc at 0, the vtable word + * at 1 on hierarchy members, then a runtime prefix (state + display name) + * on classes rooted at a runtime base, then the stream-state slot on + * stream-rooted ones. */ +function headerEnd(meta: LlClassMeta): number { if (!meta.hierarchy) return 1; if (streamRooted(meta)) return 5; return runtimePrefixOf(meta) ? 4 : 2; } +/** The GEP index of the ThreadSlots slot word (an i32) in a class whose + * hierarchy holds ThreadSlots keys: right after the header words, so every + * member of the hierarchy has it at the same offset. */ +export function classSlotWordIndex(meta: LlClassMeta): number { + if (!meta.threadSlot) + throw new InternalCompilerError(`llvm emitter bug: class ${meta.def.name} has no slot word`); + return headerEnd(meta); +} + +/** The GEP index where a class's own fields start: after the header words + * and the slot word. */ +export function classFieldBase(meta: LlClassMeta): number { + return headerEnd(meta) + (meta.threadSlot ? 1 : 0); +} + /** Append the lexical environment after inherited fields to preserve base layouts. */ export function classEnvironmentIndex(meta: LlClassMeta): number { return classFieldIndex(meta, `%classEnvironment:${meta.def.name}`).index; @@ -346,7 +371,28 @@ export function classFieldIndex(meta: LlClassMeta, field: string): { index: numb throw new InternalCompilerError( `llvm emitter bug: unknown field ${field} on class ${meta.def.name}`, ); - return { index: fieldBase(meta) + idx, type: meta.def.fields[idx]!.type }; + return { index: classFieldBase(meta) + idx, type: meta.def.fields[idx]!.type }; +} + +/** Lines that release the ThreadSlots values of the object `%o` before it + * is freed: its slot word is 0 unless a value was ever set for it. */ +function slotForgetLines( + host: ClassHost, + struct: string, + meta: LlClassMeta, + tag: string, +): string[] { + host.declare(`declare void @scr_slots_forget(i32)`); + return [ + ` %${tag}p = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${classSlotWordIndex(meta)}`, + ` %${tag} = load i32, ptr %${tag}p, align 4`, + ` %${tag}.set = icmp ne i32 %${tag}, 0`, + ` br i1 %${tag}.set, label %${tag}.forget, label %${tag}.done`, + `${tag}.forget: ; release its ThreadSlots values`, + ` call void @scr_slots_forget(i32 %${tag})`, + ` br label %${tag}.done`, + `${tag}.done:`, + ]; } /** What class emission needs beyond ShapeHost: interned unit instances for @@ -483,7 +529,7 @@ export function emitClassShapes( ? ["ptr", "ptr", "ptr"] : ["ptr"] : []; - const members = [...prefix, ...fieldTys]; + const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys]; typeDefs.push( `%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` + `; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`, @@ -569,7 +615,7 @@ export function emitClassShapes( const traced = host.tracedShapes.has(`object:${cls.name}`); const runtimePrefix = runtimePrefixOf(meta); const isStreamRooted = streamRooted(meta); - const fieldIndex = (i: number): number => fieldBase(meta) + i; + const fieldIndex = (i: number): number => classFieldBase(meta) + i; // Teardown, trace and gcFree see each slot's storage type: a nullable // class field holds the instance pointer (NULL for the unit arm), and // every class retain/release/visit entry point is NULL-tolerant. @@ -640,6 +686,7 @@ export function emitClassShapes( }); if (runtimePrefix) lines.push(...regCall("td", runtimePrefix.drop, "")); if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", "")); + if (meta.threadSlot) lines.push(...slotForgetLines(host, struct, meta, "tdw")); lines.push(...emitObjectFree(host, traced)); }; @@ -841,6 +888,7 @@ export function emitClassShapes( ` call void ${releaseSym(host, f.type)}(ptr %v${i}) ; ${llvmCommentText(f.name)} (acyclic)`, ); }); + if (meta.threadSlot) gf.push(...slotForgetLines(host, struct, meta, "gfw")); gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``); defs.push(...gf); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bd9a4b883..6adaa4678 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1,6 +1,7 @@ import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { emitLlvmLayouts } from "./layouts.js"; +import { emitThreadSlotsIntrinsic, threadSlotsBlockDefinitions } from "./thread-slots.js"; import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js"; import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js"; import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; @@ -834,6 +835,8 @@ export class LlEmitter { /** Constant-key reads on checked-dynamic objects, each with its own * zero-initialized ScrDynReadIc (scr_runtime.h). */ private dynReadIcSites = 0; + /** ThreadSlots blocks by family name (thread-slots.ts). */ + private readonly threadSlotsBlocks = new Map(); /** May-throw analysis (computeMayThrow): pending * checks are emitted only after calls that can actually raise. */ readonly mayThrow: Set; @@ -2369,6 +2372,7 @@ export class LlEmitter { { const tl = this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""; + out.push(...threadSlotsBlockDefinitions(this.threadSlotsBlocks, tl !== "")); for (let site = 0; site < this.dynReadIcSites; site++) out.push( `@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${this.sizeType}, ${this.sizeType}, ptr } zeroinitializer`, @@ -9816,9 +9820,20 @@ export class LlEmitter { ) return this.emitRegionIntrinsic(e); if (e.name.startsWith("itable.")) return this.emitInternTableIntrinsic(e); + if (e.name.startsWith("slots.")) return emitThreadSlotsIntrinsic(this, e); return emitIntrinsicExpr(this, e); } + /** The ScrSlots block of the ThreadSlots family `name`. */ + threadSlotsBlock(name: string): string { + let sym = this.threadSlotsBlocks.get(name); + if (sym === undefined) { + sym = `@sc_slots_${this.threadSlotsBlocks.size}`; + this.threadSlotsBlocks.set(name, sym); + } + return sym; + } + /** InternTable's runtime calls (scr_itable.c). Table and entry handles * are f64 values carrying pointer bits; entry values are 64-bit slots * (double bits, a bool, or a pointer to an immortal value) that the diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 33110eaa4..317104400 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -468,11 +468,14 @@ export class ReferenceWrites { return; case "intrinsic": // A TDZ test reads; publication only makes objects immortal; intern - // tables store published values and write no script object. + // tables store published values and write no script object; + // ThreadSlots reads write nothing. if ( e.name !== "module.tdzCheck" && e.name !== "threads.publish" && - !e.name.startsWith("itable.") + !e.name.startsWith("itable.") && + e.name !== "slots.get" && + e.name !== "slots.has" ) sink.all(); return; diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts index 41f67c0cb..fbd41a3f7 100644 --- a/packages/compiler/src/backend/llvm/regions.ts +++ b/packages/compiler/src/backend/llvm/regions.ts @@ -36,7 +36,7 @@ import { mangleRecordStruct, } from "../mangle.js"; import { threadHotAddressLines } from "./alloc.js"; -import type { LlClassMeta } from "./classes.js"; +import { classFieldBase, type LlClassMeta } from "./classes.js"; import { llvmCommentText } from "./common.js"; import type { NullableRefFields } from "./nullable-fields.js"; import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js"; @@ -84,7 +84,7 @@ export class LifetimeRegions { /** The reference-holding fields; inline scalar unions own nothing. */ private classFields(def: IrClassDef, meta: LlClassMeta): Field[] { - const base = meta.hierarchy ? 2 : 1; + const base = classFieldBase(meta); return def.fields.flatMap((f, i) => this.host.inlineUnions.classField(def.name, f.name) ? [] diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index c03c7fb44..8183ce920 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -23,6 +23,10 @@ export type StackObjectSource = IrExpr & { kind: "recordLit" | "new" | "call" }; * (record-results.ts). The caller proves that every use of the value only * projects it. */ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource { + // A ThreadSlots key keeps its slot index for its lifetime and has its + // values released when it is freed: it lives on the heap. + if (e.type.kind === "object" && host.classMeta.get(e.type.className)?.threadSlot === true) + return false; // A producer with a result-slot entry builds its fresh result in the // caller's storage. if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee)); diff --git a/packages/compiler/src/backend/llvm/thread-slots.test.ts b/packages/compiler/src/backend/llvm/thread-slots.test.ts new file mode 100644 index 000000000..c7260b9fe --- /dev/null +++ b/packages/compiler/src/backend/llvm/thread-slots.test.ts @@ -0,0 +1,142 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, STRING, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; +import { mangleClassStruct } from "../mangle.js"; + +const loc = { file: "thread-slots.ts", start: 0, end: 1 }; +const slotsType: IrType = { kind: "object", className: "%ThreadSlots" }; +const node: IrType = { kind: "object", className: "Node" }; +const leaf: IrType = { kind: "object", className: "Leaf" }; +const plain: IrType = { kind: "object", className: "Plain" }; +const state: IrType = { kind: "object", className: "State" }; +const maybeState: IrType = { kind: "union", unionId: "u0" }; + +/** A key hierarchy (Node, Leaf), a class that is not a key (Plain), and the + * value class (State), with `slots` a module global of the slots class. */ +function module(body: IrExpr[]): IrModule { + const cls = (name: string, fields: [string, IrType][], base?: string) => ({ + name, + ...(base !== undefined ? { base } : {}), + fields: fields.map(([field, type]) => ({ name: field, type })), + methods: [], + loc, + }); + return { + irVersion: 16, + sourceFile: loc.file, + entry: "main", + threadSlotKeys: ["Node"], + globals: [ + ["slots", slotsType], + ["node", node], + ["leaf", leaf], + ["plain", plain], + ["state", state], + ].map(([name, type]) => ({ id: `%g.${name as string}`, name, type, mutable: false })), + functions: [ + { + name: "main", + params: [], + returnType: VOID, + locals: [], + body: body.map((expr) => ({ kind: "exprStmt" as const, expr, loc })), + loc, + }, + ], + classes: [ + cls("%ThreadSlots", []), + cls("Node", [["name", STRING]]), + cls( + "Leaf", + [ + ["name", STRING], + ["weight", F64], + ], + "Node", + ), + cls("Plain", [["name", STRING]]), + cls("State", [["count", F64]]), + ], + unions: [{ id: "u0", arms: [state, { kind: "undefinedT" }] }], + } as unknown as IrModule; +} + +function body(llvm: string, name: string): string { + const start = llvm.indexOf(`@${name}(`); + return start < 0 ? "" : llvm.slice(start, llvm.indexOf("\n}", start)); +} + +const intrinsic = (name: string, args: IrExpr[], type: IrType): IrExpr => + ({ kind: "intrinsic", name, args, type, loc }) as IrExpr; +const str = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc }); +const global = (id: string, type: IrType): IrExpr => + ({ kind: "varRef", localId: `%g.${id}`, type, loc }) as IrExpr; +const slots = global("slots", slotsType); +const fresh = (type: IrType & { kind: "object" }): IrExpr => + global(type.className.toLowerCase(), type); + +test("slots.get reads the slot word, the page and the entry inline", () => { + const calls = [ + intrinsic("slots.get", [str("s"), slots, fresh(leaf)], maybeState), + intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL), + intrinsic("slots.set", [str("s"), slots, fresh(leaf), fresh(state)], VOID), + intrinsic("slots.delete", [str("t"), slots, fresh(node)], BOOL), + ]; + expect(validateModule(module(calls))).toEqual([]); + const llvm = emitLlvmModule(module(calls), { pointerBits: 64 }); + // The slot word sits after the header words of every hierarchy member. + expect(llvm).toMatch( + new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`), + ); + expect(llvm).toMatch( + new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`), + ); + expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`)); + // One block per family name. + expect(llvm).toMatch( + /@sc_slots_0 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/, + ); + expect(llvm).toMatch( + /@sc_slots_1 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/, + ); + const main = body(llvm, "sc_f_main"); + expect(main).toMatch( + /getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/, + ); + expect(main).not.toMatch(/call ptr @scr_slots_get/); + expect(main).toMatch( + /call void @scr_slots_set\(ptr @sc_slots_0, ptr %\S+, ptr %\S+, ptr %\S+, ptr @\S+\)/, + ); + expect(main).toMatch(/call zeroext i1 @scr_slots_delete\(ptr @sc_slots_1, ptr %\S+\)/); +}); + +test("freeing a key releases its values when its slot word is set", () => { + const llvm = emitLlvmModule( + module([intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL)]), + { pointerBits: 64 }, + ); + expect(llvm).toMatch(/icmp ne i32 %tdw, 0\n {2}br i1 %tdw\.set, label %tdw\.forget/); + expect(llvm).toMatch(/call void @scr_slots_forget\(i32 %tdw\)/); +}); + +test("the validator checks ThreadSlots intrinsic operands", () => { + const bad = [ + intrinsic("slots.get", [str("s"), slots], maybeState), + intrinsic("slots.has", [slots, slots, fresh(node)], BOOL), + intrinsic("slots.has", [str("s"), slots, fresh(plain)], BOOL), + intrinsic( + "slots.set", + [str("s"), slots, fresh(node), { kind: "numLit", value: 1, type: F64, loc }], + VOID, + ), + intrinsic("slots.get", [str("s"), slots, fresh(node)], state), + ]; + expect(validateModule(module(bad)).map((e) => e.message)).toEqual([ + "in main: slots.get takes 3 arguments", + "in main: slots.has takes a name literal and a %ThreadSlots value", + "in main: slots.has key object:Plain is not a ThreadSlots key class", + "in main: slots.set value f64 is not a ThreadSlots value type", + "in main: slots.get has a V | undefined union type", + ]); +}); diff --git a/packages/compiler/src/backend/llvm/thread-slots.ts b/packages/compiler/src/backend/llvm/thread-slots.ts new file mode 100644 index 000000000..da182bde2 --- /dev/null +++ b/packages/compiler/src/backend/llvm/thread-slots.ts @@ -0,0 +1,170 @@ +/* ThreadSlots (@scriptc/experimental): the slots.* intrinsics (scr_slots.c, + * frontend/lowering/thread-slots.ts). + * + * Each family name has one `{ ptr, i32, i32, ptr, ptr }` ScrSlots block, + * an internal global that is thread-local where module globals are. A key + * class's hierarchy carries an i32 slot word after its header words + * (classSlotWordIndex). A read is inline: the slot word, then the page + * and the entry, + * + * page = word >> 10 < block.npages ? block.pages[word >> 10] : none + * value = page[word & 1023] + * + * and absent pages are a shared page of NULL entries (scr_slots_none), so + * a key without a value reads NULL, the undefined of `V | undefined`. Set + * and delete call the runtime, which holds one reference to each value and + * releases the value it replaces or removes. */ +import { InternalCompilerError } from "../../errors.js"; +import { isUnitType, typeKey, type IrType } from "../../ir/ir.js"; +import { classSlotWordIndex, classStructSym } from "./classes.js"; +import { llvmCommentText } from "./common.js"; +import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { releaseSym } from "./shapes.js"; + +const BLOCK = "{ ptr, i32, i32, ptr, ptr }"; + +/** The slots.* intrinsics. */ +export function emitThreadSlotsIntrinsic( + host: LlvmEmitterContext, + e: ExprOf<"intrinsic">, +): LlValue { + const B = host.B; + const [nameArg, slotsArg, keyArg, valueArg] = e.args; + if (nameArg?.kind !== "strLit" || slotsArg === undefined || keyArg === undefined) + throw new InternalCompilerError(`llvm emitter bug: ${e.name} operands`); + const block = host.threadSlotsBlock(nameArg.value); + // The slots value is read for its effects (a module const's TDZ check). + const slots = host.emitReadReceiver(slotsArg); + const key = host.emitReadReceiver(keyArg); + const value = valueArg !== undefined ? host.emitExpr(valueArg) : null; + if (B.isTerminated()) return { name: "", type: e.type }; + if (keyArg.type.kind !== "object") + throw new InternalCompilerError(`llvm emitter bug: ${e.name} key of kind ${keyArg.type.kind}`); + const className = keyArg.type.className; + const word = B.tmp(); + B.line( + `${word} = getelementptr inbounds %${classStructSym(className)}, ptr ${key.name}, i64 0, i32 ${classSlotWordIndex(host.classMetaOf(className))}`, + ); + switch (e.name) { + case "slots.get": { + const read = readSlot(host, block, word); + if (host.nullableUnions.of(e.type) !== null) + return host.own({ name: host.retainValue(read, e.type), type: e.type }); + return boxedRead(host, read, e.type); + } + case "slots.has": { + const read = readSlot(host, block, word); + const has = B.tmp(); + B.line(`${has} = icmp ne ptr ${read}, null`); + return { name: has, type: e.type }; + } + case "slots.delete": { + host.declare(`declare zeroext i1 @scr_slots_delete(ptr, ptr)`); + const had = B.tmp(); + B.line(`${had} = call zeroext i1 @scr_slots_delete(ptr ${block}, ptr ${word})`); + return { name: had, type: e.type }; + } + case "slots.set": { + if (value === null) throw new InternalCompilerError("llvm emitter bug: slots.set value"); + // The value moves into the block. + host.moveTemp(value); + host.declare(`declare void @scr_slots_set(ptr, ptr, ptr, ptr, ptr)`); + B.line( + `call void @scr_slots_set(ptr ${block}, ptr ${key.name}, ptr ${word}, ptr ${value.name}, ptr ${releaseSym(host.shapeHost, value.type)})`, + ); + if (e.type.kind === "void") return { name: "", type: e.type }; + return host.own({ name: host.retainValue(slots.name, e.type), type: e.type }); + } + default: + throw new InternalCompilerError(`llvm emitter bug: unknown intrinsic ${e.name}`); + } +} + +/** A read as a tagged union box, for `V | undefined` types that the module + * keeps boxed (Map keys and Set elements of that type): the value's arm, or + * the interned undefined instance for NULL. */ +function boxedRead(host: LlvmEmitterContext, read: string, type: IrType): LlValue { + const B = host.B; + const def = type.kind === "union" ? host.unionsById.get(type.unionId) : undefined; + const refTag = def?.arms.findIndex((a) => !isUnitType(a)) ?? -1; + const unitTag = def?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1; + if (def === undefined || refTag < 0 || unitTag < 0 || def.arms.length !== 2) + throw new InternalCompilerError(`llvm emitter bug: slots.get of type ${typeKey(type)}`); + const result = B.slot(); + B.entryAllocas.push(`${result} = alloca ptr`); + const has = B.tmp(); + B.line(`${has} = icmp ne ptr ${read}, null`); + const some = B.newLabel("slots.some"), + none = B.newLabel("slots.none"), + done = B.newLabel("slots.box"); + B.condBr(has, some, none); + B.startBlock(some); + const arm = def.arms[refTag]!; + const box = host.unionNewOwned(def.id, refTag, { name: host.retainValue(read, arm), type: arm }); + B.line(`store ptr ${box}, ptr ${result}`); + B.br(done); + B.startBlock(none); + B.line(`store ptr ${host.unitInstanceRef(def.id, unitTag)}, ptr ${result}`); + B.br(done); + B.startBlock(done); + const out = B.tmp(); + B.line(`${out} = load ptr, ptr ${result}`); + return host.own({ name: out, type }); +} + +/** This thread's value for the slot word at `word`, borrowed, or NULL. */ +function readSlot(host: LlvmEmitterContext, block: string, word: string): string { + const B = host.B; + host.declare(`@scr_slots_none = external constant [1024 x ptr]`); + const index = B.tmp(), + page = B.tmp(), + pages = B.tmp(), + count = B.tmp(), + inRange = B.tmp(); + B.line(`${index} = load atomic i32, ptr ${word} monotonic, align 4`); + B.line(`${page} = lshr i32 ${index}, 10`); + B.line(`${count} = getelementptr inbounds ${BLOCK}, ptr ${block}, i64 0, i32 1`); + const npages = B.tmp(); + B.line(`${npages} = load i32, ptr ${count}`); + B.line(`${inRange} = icmp ult i32 ${page}, ${npages}`); + const result = B.slot(); + B.entryAllocas.push(`${result} = alloca ptr`); + B.line(`store ptr null, ptr ${result}`); + const found = B.newLabel("slots.page"), + done = B.newLabel("slots.j"); + B.condBr(inRange, found, done); + B.startBlock(found); + B.line(`${pages} = load ptr, ptr ${block}`); + const pageIndex = B.tmp(), + pagePtr = B.tmp(), + entries = B.tmp(), + entryIndex = B.tmp(), + entryWide = B.tmp(), + entry = B.tmp(), + v = B.tmp(); + B.line(`${pageIndex} = zext i32 ${page} to i64`); + B.line(`${pagePtr} = getelementptr inbounds ptr, ptr ${pages}, i64 ${pageIndex}`); + B.line(`${entries} = load ptr, ptr ${pagePtr}`); + B.line(`${entryIndex} = and i32 ${index}, 1023`); + B.line(`${entryWide} = zext i32 ${entryIndex} to i64`); + B.line(`${entry} = getelementptr inbounds ptr, ptr ${entries}, i64 ${entryWide}`); + B.line(`${v} = load ptr, ptr ${entry}`); + B.line(`store ptr ${v}, ptr ${result}`); + B.br(done); + B.startBlock(done); + const out = B.tmp(); + B.line(`${out} = load ptr, ptr ${result}`); + return out; +} + +/** The ScrSlots block definitions of the families a module uses. */ +export function threadSlotsBlockDefinitions( + blocks: ReadonlyMap, + threadLocal: boolean, +): string[] { + const tl = threadLocal ? "thread_local " : ""; + return [...blocks].map( + ([name, sym]) => + `${sym} = internal ${tl}global ${BLOCK} zeroinitializer ; ThreadSlots ${llvmCommentText(JSON.stringify(name))}`, + ); +} diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index 88f049b7c..bc9643670 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -70,6 +70,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_publish.c", "scr_region.c", "scr_itable.c", + "scr_slots.c", "scr_alloc.c", ] as const; diff --git a/packages/compiler/src/frontend/experimental-import.ts b/packages/compiler/src/frontend/experimental-import.ts index 3b15bfeb9..86aa10933 100644 --- a/packages/compiler/src/frontend/experimental-import.ts +++ b/packages/compiler/src/frontend/experimental-import.ts @@ -1,14 +1,15 @@ /* Provenance of the @scriptc/experimental exports. The package is scriptc's * own (packages/experimental): Node.js runs its JavaScript implementation, * scriptc builds lower its exports (lowering/lifetime-regions.ts, - * lowering/intern-tables.ts) and never compile its body. */ + * lowering/intern-tables.ts, lowering/thread-slots.ts) and never compile + * its body. */ import { existsSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import * as ts from "./ts7/adapter.js"; export const EXPERIMENTAL_MODULE = "@scriptc/experimental"; -export type ExperimentalMember = "LifetimeRegion" | "InternTable"; +export type ExperimentalMember = "LifetimeRegion" | "InternTable" | "ThreadSlots"; /** The @scriptc/experimental export an identifier is a named-import binding of. */ export function experimentalImportOf( @@ -27,7 +28,9 @@ export function experimentalImportOf( ) return null; const member = decl.propertyName?.text ?? decl.name.text; - return member === "LifetimeRegion" || member === "InternTable" ? member : null; + return member === "LifetimeRegion" || member === "InternTable" || member === "ThreadSlots" + ? member + : null; } /** Whether `sf` is the package's own declaration file: its directory's @@ -114,3 +117,27 @@ export function internTableMemberOf( ? name : null; } + +export type ThreadSlotsMember = "get" | "has" | "set" | "delete"; + +/** The ThreadSlots member a property access names (`slots.get`), by the + * declaration it resolves to. */ +export function threadSlotsMemberOf( + program: ts.Program, + access: ts.PropertyAccessExpression, + known: Map, +): ThreadSlotsMember | null { + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(access.name); + const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; + const owner = decl.parent; + if ( + !ts.isClassDeclaration(owner) || + owner.name?.text !== "ThreadSlots" || + !isExperimentalDeclarationFile(owner.getSourceFile(), known) + ) + return null; + const name = access.name.text; + return name === "get" || name === "has" || name === "set" || name === "delete" ? name : null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 8856ae9b5..e3f82048e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -504,6 +504,9 @@ export interface ClassInfo { /** The synthetic InternTable class (@scriptc/experimental, * intern-tables.ts): synthesized like lifetimeRegion. */ internTable?: true; + /** The synthetic ThreadSlots class (@scriptc/experimental, + * thread-slots.ts): synthesized like lifetimeRegion. */ + threadSlots?: true; /** This class's own `emit` override in the FORWARDING SHAPE (the one * EventEmitter member a subclass may re-declare): never in `methods` — * emit calls keep routing through the emitter spoke, which lowers the diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 391dd7c88..d51d34a6e 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -16,6 +16,7 @@ import { STATS_TYPE_TESTS } from "./builtins/filesystem.js"; import { threadsImportOf } from "../threads-import.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; import { lowerInternTableProperty } from "./intern-tables.js"; +import { lowerThreadSlotsProperty } from "./thread-slots.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import { SYMBOL_T } from "../../ir/ir.js"; @@ -1142,6 +1143,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { expr, 'open tables with new InternTable("name")', ); + if (experimentalImportOf(lowerer.program, expr) === "ThreadSlots") + lowerer.noLowering( + "ThreadSlots (@scriptc/experimental) as a value", + expr, + 'open slots with new ThreadSlots("name")', + ); } { const bi = lowerer.builtinImportOf(expr); @@ -1759,6 +1766,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const table = lowerInternTableProperty(lowerer, expr); if (table) return table; } + lowerThreadSlotsProperty(lowerer, expr); // Expando statics may still be absent even when JS inference sees // the eventual assignment's scalar type. Preserve the checked value. const staticField = lowerStaticFieldRead(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 12deec7ad..01328d526 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4,6 +4,7 @@ import { lowerLifetimeRegionNew, } from "./lifetime-regions.js"; import { internTableType, lowerInternTableCall, lowerInternTableNew } from "./intern-tables.js"; +import { lowerThreadSlotsCall, lowerThreadSlotsNew, threadSlotsType } from "./thread-slots.js"; import { isExperimentalDeclarationFile } from "../experimental-import.js"; import { checkCycleBindingAccesses, @@ -2488,6 +2489,15 @@ export class Lowerer { readonly internTableHelpers = new Map(); /** Each InternTable name's type arguments and first construction site. */ readonly internTableNames = new Map(); + /** Each ThreadSlots name's type arguments (thread-slots.ts). */ + readonly threadSlotNames = new Map(); + /** The family each `new ThreadSlots(...)` opens. */ + readonly threadSlotFamilies = new Map< + ts.NewExpression, + { name: string; key: IrType; value: IrType } + >(); + /** Classes used as ThreadSlots keys: their hierarchies carry a slot word. */ + readonly threadSlotKeys = new Set(); readonly forkTargetIdByPath = new Map(); /** Namespace ModuleBlocks this program lowers, filled by splitFiles: * "flattened" — an instantiated namespace whose body joined the file's @@ -2849,6 +2859,10 @@ export class Lowerer { isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) ? internTableType(this) : null, + threadSlotsClass: (decl) => + isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + ? threadSlotsType(this) + : null, moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type), }; // Modules reachable only through literal import() joined @@ -6302,6 +6316,9 @@ export class Lowerer { entry: ENTRY_NAME, ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), + ...(this.threadSlotKeys.size > 0 + ? { threadSlotKeys: [...this.threadSlotKeys].sort() } + : {}), ...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }), ...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}), }; @@ -6500,7 +6517,7 @@ export class Lowerer { } for (const info of this.classes.values()) { if (info.builtinError) continue; // runtime-provided; nothing lowers - if (info.lifetimeRegion || info.internTable) continue; // synthesized with the class + if (info.lifetimeRegion || info.internTable || info.threadSlots) continue; // synthesized with the class // Generic-class INSTANTIATIONS (and mixin instantiations) are // demand-driven, not units: their members lower unconditionally in // the instance drain below — the generic-fn instance rule. @@ -10916,6 +10933,7 @@ export class Lowerer { return ( lowerLifetimeRegionCall(this, expr) ?? lowerInternTableCall(this, expr) ?? + lowerThreadSlotsCall(this, expr) ?? lowerFfiCall(this, expr) ?? lowerFetchCall(this, expr) ?? lowerStaticFetchCompanionCall(this, expr) ?? @@ -11141,6 +11159,7 @@ export class Lowerer { const builtin = lowerLifetimeRegionNew(this, expr) ?? lowerInternTableNew(this, expr) ?? + lowerThreadSlotsNew(this, expr) ?? lowerAbortControllerNew(this, expr) ?? lowerDisposableStackNew(this, expr) ?? lowerEventTargetNew(this, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts new file mode 100644 index 000000000..9247703f5 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -0,0 +1,254 @@ +/* ThreadSlots (@scriptc/experimental): named families of per-thread values + * keyed by class instances (scr_slots.c, backend/llvm/thread-slots.ts). + * + * A ThreadSlots value is an instance of the synthetic class `%ThreadSlots` + * with no fields, so it has the identity, typeof and inspection of the + * Node.js class (whose state is private). A family's per-thread storage is + * found by its name at compile time, so its methods are called through a + * module-level const initialized with `new ThreadSlots("name")`, and + * lower at their call sites to slots.get, slots.has, slots.set and + * slots.delete, with the family name, the const's value and the key: + * - the name must be a string literal, and every construction of one name + * must use the same type arguments; + * - the key type must be a class (its hierarchy carries the slot word); + * - the value type must be a class, an object type, an array, a Map or a + * Set, so `V | undefined` is a nullable reference. + * Any other use of the class or its members as values is refused. */ +import { STRING, VOID, typeKey } from "../../ir/ir.js"; +import type { IrExpr, IrType, SrcLoc } from "../../ir/ir.js"; +import { + type ThreadSlotsMember, + experimentalImportOf, + threadSlotsMemberOf, +} from "../experimental-import.js"; +import * as ts from "../ts7/adapter.js"; +import type { ClassInfo } from "./lower-classes.js"; +import type { Lowerer } from "./lowerer.js"; +import { locOf } from "../program.js"; + +export const THREAD_SLOTS_CLASS = "%ThreadSlots"; + +const BUILTIN_LOC: SrcLoc = { file: "", start: 0, end: 0 }; + +/** Value types a family stores: references that `V | undefined` holds as a + * nullable pointer. */ +const VALUE_KINDS = new Set(["object", "record", "array", "map", "set"]); + +/** The ThreadSlots instance type, registering the class (and its + * constructor) on first use. */ +export function threadSlotsType(lowerer: Lowerer): IrType { + const type: IrType = { kind: "object", className: THREAD_SLOTS_CLASS }; + if (lowerer.classes.has(THREAD_SLOTS_CLASS)) return type; + const info: ClassInfo = { + def: { name: THREAD_SLOTS_CLASS, fields: [], loc: BUILTIN_LOC }, + fields: new Map(), + fieldOrder: [], + methods: new Map(), + decl: null, + threadSlots: true, + ctor: null, + ctorParams: [], + base: null, + subclasses: [], + throwingSetters: [], + staticFields: [], + }; + lowerer.classes.set(THREAD_SLOTS_CLASS, info); + lowerer.liftedFns.push({ + name: `%${THREAD_SLOTS_CLASS}.constructor`, + params: [{ localId: "this.0", name: "this", type }], + returnType: VOID, + locals: [{ id: "this.0", name: "this", type, mutable: false }], + body: [], + loc: BUILTIN_LOC, + }); + return type; +} + +interface Family { + name: string; + key: IrType; + value: IrType; +} + +/** The root of a class's hierarchy. */ +function rootOf(info: ClassInfo): ClassInfo { + let root = info; + while (root.base !== null) root = root.base; + return root; +} + +/** The family a `new ThreadSlots("name")` expression opens, + * validated and registered once per expression. */ +function familyOf(lowerer: Lowerer, expr: ts.NewExpression): Family { + const known = lowerer.threadSlotFamilies.get(expr); + if (known !== undefined) return known; + const args = expr.arguments ?? []; + const arg = args[0]; + if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) + lowerer.noLowering(`new ThreadSlots with ${args.length} arguments`, expr); + if (!ts.isStringLiteral(arg!) && !ts.isNoSubstitutionTemplateLiteral(arg!)) + lowerer.unsupported( + "SC1090", + arg!, + "ThreadSlots names other than string literals", + "name the slots with a string literal: every construction of the name opens the same slots", + ); + const name = (arg as ts.StringLiteral).text; + const instance = lowerer.typeOf(expr); + const typeArgs = lowerer.checker.getTypeArguments(instance as ts.TypeReference); + if (typeArgs.length !== 2) lowerer.noLowering("ThreadSlots without type arguments", expr); + const key = lowerer.mapTypeOf(typeArgs[0]!); + const keyInfo = key?.kind === "object" ? lowerer.classes.get(key.className) : undefined; + if ( + key?.kind !== "object" || + keyInfo === undefined || + keyInfo.decl === null || + rootOf(keyInfo).def.runtime === true + ) + lowerer.unsupported( + "SC1090", + expr, + `'${lowerer.checker.typeToString(typeArgs[0]!)}' as a ThreadSlots key type`, + "key the slots by instances of a class declared in the program", + ); + const value = lowerer.mapTypeOf(typeArgs[1]!); + if (value === null || !VALUE_KINDS.has(value.kind)) + lowerer.unsupported( + "SC1090", + expr, + `'${lowerer.checker.typeToString(typeArgs[1]!)}' as a ThreadSlots value type`, + "store class instances, objects, arrays, Maps or Sets", + ); + const signature = `${typeKey(key!)}|${typeKey(value!)}`; + const first = lowerer.threadSlotNames.get(name); + if (first === undefined) lowerer.threadSlotNames.set(name, signature); + else if (first !== signature) + lowerer.unsupported( + "SC1090", + expr, + `opening ThreadSlots ${JSON.stringify(name)} with different type arguments than its first construction`, + "every construction of one slots name must use the same key and value types", + ); + lowerer.threadSlotKeys.add((key as IrType & { kind: "object" }).className); + const family = { name, key: key!, value: value! }; + lowerer.threadSlotFamilies.set(expr, family); + return family; +} + +/** `new ThreadSlots("name")`. */ +export function lowerThreadSlotsNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + if (!ts.isIdentifier(expr.expression)) return null; + if (experimentalImportOf(lowerer.program, expr.expression) !== "ThreadSlots") return null; + familyOf(lowerer, expr); + return { + kind: "new", + className: THREAD_SLOTS_CLASS, + args: [], + type: threadSlotsType(lowerer), + loc: locOf(expr), + }; +} + +/** The `new ThreadSlots(...)` initializer of the module-level const that + * `receiver` names (through imports and namespace imports), or of the + * slots whose `set` call + * `receiver` is (`slots.set(a, x).set(b, y)`), or null. */ +function declaringNew(lowerer: Lowerer, receiver: ts.Expression): ts.NewExpression | null { + if ( + ts.isCallExpression(receiver) && + ts.isPropertyAccessExpression(receiver.expression) && + threadSlotsMemberOf(lowerer.program, receiver.expression, lowerer.experimentalFiles) === "set" + ) + return declaringNew(lowerer, receiver.expression.expression); + // `slots`, or `ns.slots` through a namespace import. + const ref = ts.isPropertyAccessExpression(receiver) ? receiver.name : receiver; + if (!ts.isIdentifier(ref)) return null; + const checker = lowerer.checker; + let symbol = checker.getSymbolAtLocation(ref); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = checker.getAliasedSymbol(symbol); + const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; + const list = decl.parent; + if ( + !ts.isVariableDeclarationList(list) || + (list.flags & ts.NodeFlags.Const) === 0 || + !ts.isVariableStatement(list.parent) || + !ts.isSourceFile(list.parent.parent) + ) + return null; + const init = decl.initializer; + if (init === undefined || !ts.isNewExpression(init) || !ts.isIdentifier(init.expression)) + return null; + return experimentalImportOf(lowerer.program, init.expression) === "ThreadSlots" ? init : null; +} + +/** The refusal of ThreadSlots members used as values. */ +export function lowerThreadSlotsProperty( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + const member = threadSlotsMemberOf(lowerer.program, expr, lowerer.experimentalFiles); + if (member === null) return null; + return lowerer.noLowering( + `ThreadSlots.${member} (@scriptc/experimental) as a value`, + expr, + `call slots.${member}(...) directly`, + ); +} + +/** `slots.get(k)`, `has`, `set` and `delete`. */ +export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr | null { + const callee = expr.expression; + if (!ts.isPropertyAccessExpression(callee)) return null; + const member: ThreadSlotsMember | null = threadSlotsMemberOf( + lowerer.program, + callee, + lowerer.experimentalFiles, + ); + if (member === null) return null; + if (callee.questionDotToken !== undefined || expr.questionDotToken !== undefined) + lowerer.noLowering(`ThreadSlots.${member} through ?.`, expr, `call slots.${member}() directly`); + const arity = member === "set" ? 2 : 1; + if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a))) + lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.length} arguments`, expr); + const init = declaringNew(lowerer, callee.expression); + if (init === null) + lowerer.unsupported( + "SC1090", + callee.expression, + "ThreadSlots calls through a value other than a module-level const", + 'call the methods through `const slots = new ThreadSlots("name")` declared at module scope', + ); + const family = familyOf(lowerer, init!); + const loc = locOf(expr); + const slotsType = threadSlotsType(lowerer); + const receiver = lowerer.coerceInto( + callee.expression, + lowerer.lowerExpr(callee.expression), + slotsType, + ); + const keyNode = expr.arguments[0]!; + const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), family.key); + const name: IrExpr = { kind: "strLit", value: family.name, type: STRING, loc }; + const args = [name, receiver, key]; + switch (member) { + case "get": { + const resultTs = lowerer.typeOf(expr); + const result = lowerer.mapTypeOf(resultTs); + if (result === null) lowerer.badType(expr, resultTs); + return { kind: "intrinsic", name: "slots.get", args, type: result!, loc }; + } + case "has": + case "delete": + return { kind: "intrinsic", name: `slots.${member}`, args, type: { kind: "bool" }, loc }; + case "set": { + const valueNode = expr.arguments[1]!; + const value = lowerer.coerceInto(valueNode, lowerer.lowerExpr(valueNode), family.value); + // `set` returns the slots; a statement discards them. + const type = ts.isExpressionStatement(expr.parent) ? VOID : slotsType; + return { kind: "intrinsic", name: "slots.set", args: [...args, value], type, loc }; + } + } +} diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 52964e1bd..eab7fd56d 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1075,6 +1075,9 @@ export interface TypeMapperCtx { /** The InternTable instance type (@scriptc/experimental), when `decl` is * that package's class declaration (lowering/intern-tables.ts). */ internTableClass?: (decl: ts.ClassDeclaration) => IrType | null; + /** The ThreadSlots instance type (@scriptc/experimental), when `decl` is + * that package's class declaration (lowering/thread-slots.ts). */ + threadSlotsClass?: (decl: ts.ClassDeclaration) => IrType | null; } /** A generator type's normalized value channels (Generator and the IteratorResult alias share this): @@ -1653,16 +1656,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return hasStr ? STRING : F64; } } - // @scriptc/experimental's LifetimeRegion and InternTable: synthetic - // program classes, provenance-checked by the package's declaration file. + // @scriptc/experimental's LifetimeRegion, InternTable and ThreadSlots: + // synthetic program classes, provenance-checked by the package's + // declaration file. { const classSym = widened.getSymbol(); const name = classSym?.name; - if (name === "LifetimeRegion" || name === "InternTable") { + if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") { for (const d of checker.declarationsOf(classSym!)) { if (!ts.isClassDeclaration(d)) continue; const synthetic = - name === "LifetimeRegion" ? ctx.lifetimeRegionClass?.(d) : ctx.internTableClass?.(d); + name === "LifetimeRegion" + ? ctx.lifetimeRegionClass?.(d) + : name === "InternTable" + ? ctx.internTableClass?.(d) + : ctx.threadSlotsClass?.(d); if (synthetic) return synthetic; } } diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 0b1f9fb49..ab4b05c8b 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -209,6 +209,7 @@ const MODULE_FIELDS: Record>, Role> = { "contracts.acyclic[]": "other", "contracts.immortalGlobals[]": "other", "contracts.programLifetime[]": "other", + "threadSlotKeys[]": "other", }; const loc = { file: "references.ts", start: 0, end: 0 }; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4041cd265..0f3d0811a 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -955,6 +955,10 @@ export interface IrModule { * ProgramLifetime instance lives in a counted region (the region.* * intrinsics; backend/llvm/regions.ts). */ lifetimeRegions?: true; + /** Classes used as ThreadSlots keys (@scriptc/experimental): the root of + * each one's hierarchy carries a slot word (the slots.* intrinsics; + * backend/llvm/thread-slots.ts). */ + threadSlotKeys?: string[]; /** The entry module is CommonJS under Node's classification. Node runs a * CommonJS main synchronously and drains process.nextTick callbacks * before promise jobs at the first checkpoint; an ES-module main's @@ -3563,7 +3567,19 @@ export type IrExpr = | "itable.find" | "itable.insert" | "itable.value" - | "itable.size"; + | "itable.size" + /* ThreadSlots (@scriptc/experimental; scr_slots.c). Every one takes + * the family name (a string literal), the family's %ThreadSlots + * value (read for its effects only) and the key, a class instance + * whose hierarchy is in IrModule.threadSlotKeys: + * slots.get → V | undefined, a nullable union, owned (+1); + * slots.has → bool; slots.delete → bool, releasing the value; + * slots.set (…, V value) → void, or the %ThreadSlots value when the + * result is used. Nothing throws. */ + | "slots.get" + | "slots.has" + | "slots.set" + | "slots.delete"; args: IrExpr[]; type: IrType; loc: SrcLoc; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index dec6ab201..9a6294f5a 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -985,6 +985,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { errors, mod.workers === true, mod.lifetimeRegions === true, + new Set(mod.threadSlotKeys ?? []), ); } return errors; @@ -1003,6 +1004,7 @@ function validateFunction( errors: IrValidationError[], workers: boolean, lifetimeRegions: boolean, + threadSlotKeys: ReadonlySet, ): void { const locals = new Map(fn.locals.map((l) => [l.id, l])); const err = (message: string, loc: SrcLoc) => @@ -3870,6 +3872,44 @@ function validateFunction( } else if (e.type.kind !== "f64") err(`${e.name} has type f64`, e.loc); return; } + if ( + e.name === "slots.get" || + e.name === "slots.has" || + e.name === "slots.set" || + e.name === "slots.delete" + ) { + for (const a of e.args) checkExpr(a); + const [name, slots, key, value] = e.args; + const arity = e.name === "slots.set" ? 4 : 3; + const isSlots = (t: IrType): boolean => t.kind === "object" && t.className === "%ThreadSlots"; + // The key's class or one of its bases is a declared key. + const keyed = (className: string): boolean => { + for (let c: string | undefined = className; c !== undefined; c = classes.get(c)?.base) + if (threadSlotKeys.has(c)) return true; + return false; + }; + if (e.args.length !== arity) err(`${e.name} takes ${arity} arguments`, e.loc); + else if (name!.kind !== "strLit" || !isSlots(slots!.type)) + err(`${e.name} takes a name literal and a %ThreadSlots value`, e.loc); + else if (key!.type.kind !== "object" || !keyed(key!.type.className)) + err(`${e.name} key ${typeKey(key!.type)} is not a ThreadSlots key class`, e.loc); + else if (e.name === "slots.set") { + const kind = value!.type.kind; + if ( + kind !== "object" && + kind !== "record" && + kind !== "array" && + kind !== "map" && + kind !== "set" + ) + err(`slots.set value ${typeKey(value!.type)} is not a ThreadSlots value type`, e.loc); + if (e.type.kind !== "void" && !isSlots(e.type)) + err("slots.set has type void or %ThreadSlots", e.loc); + } else if (e.name === "slots.get") { + if (e.type.kind !== "union") err("slots.get has a V | undefined union type", e.loc); + } else if (e.type.kind !== "bool") err(`${e.name} has type bool`, e.loc); + return; + } if (e.name === "threads.publish") { if (e.args.length !== 1) err("threads.publish takes exactly one argument", e.loc); for (const a of e.args) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json new file mode 100644 index 000000000..574f51b5d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5727-thread-slots-basics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json new file mode 100644 index 000000000..1fb5e7a76 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5728-thread-slots-workers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json new file mode 100644 index 000000000..186761e38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5729-thread-slots-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json new file mode 100644 index 000000000..f278d25e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/thread-slots-fences.ts" + ], + "diags": [] +} diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 413deeff2..8f52f9a5b 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -70,6 +70,49 @@ Programs that behave identically under scriptc and Node.js follow the `@scriptc/ 2. Identity and ids are not compared across threads. Ids assigned inside callbacks are unique in both runtimes but differ between them. 3. A thread does not depend on another thread's inserts. `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js. -Per-thread state about a shared value (lazily computed results, caches) belongs in thread-local state, for example a module-level `Map` keyed by the value, or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. +Per-thread state about a shared value (lazily computed results, caches) belongs in thread-local state, for example a `ThreadSlots` family (below), a module-level `Map` keyed by the value, or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. In scriptc builds stored values are immortal: they stay allocated until the process exits, including values a callback computed for a key that another thread stored first. + +## ThreadSlots + +A `ThreadSlots` family holds one value per key and thread, like a `WeakMap` that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares, such as values read from an `InternTable`, and setting a value never writes the key. + +```ts +import { InternTable, ThreadSlots } from "@scriptc/experimental"; + +class Type { + readonly id: number; + constructor(id: number) { + this.id = id; + } +} + +class TypeState { + resolved = false; +} + +const types = new InternTable("checker.types"); +// Module scope: every thread that evaluates this line opens the same family. +const states = new ThreadSlots("checker.typeStates"); + +function stateOf(t: Type): TypeState { + let state = states.get(t); + if (state === undefined) { + state = new TypeState(); + states.set(t, state); + } + return state; +} +``` + +- `new ThreadSlots(name)` opens the family called `name`. Within one thread, every construction with the same name refers to the same family. +- `get(key)` and `has(key)` read the calling thread's value for `key`. +- `set(key, value)` sets the calling thread's value and returns the slots. +- `delete(key)` removes the calling thread's value and returns whether there was one. + +scriptc requires the name to be a string literal, every construction of one name to use the same type arguments, the key type to be a class declared in the program (keys may be instances of its subclasses), and the value type to be a class, an object type, an array, a `Map` or a `Set`. The methods must be called through a module-level `const` initialized with `new ThreadSlots("name")`, since a family's storage is found by its name when the program is compiled. + +In scriptc builds every instance of a key class's hierarchy carries a 4-byte slot index, assigned by the first `set` for it and never reused. `get` reads the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family holds a reference to each of the thread's values and releases it when it is replaced or deleted, when its key is freed, and when the thread exits. Keys freed with a `LifetimeRegion` keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. + +Under Node.js each family is a `WeakMap` per thread. diff --git a/packages/experimental/index.d.ts b/packages/experimental/index.d.ts index d831cbff9..77377e25f 100644 --- a/packages/experimental/index.d.ts +++ b/packages/experimental/index.d.ts @@ -52,3 +52,24 @@ export declare class InternTable< * call it. */ getOrInsertComputed(key: K, callback: (key: K) => V): V; } + +/** A named family of per-thread values keyed by objects, like a WeakMap + * that each thread has its own copy of. Every construction with one name + * opens the same family. A value set on one thread is visible only to that + * thread, including for keys that every thread shares (published objects, + * @scriptc/threads), and setting one never writes the key. scriptc requires + * the name to be a string literal, every construction of one name to use + * the same type arguments, the key type to be a class, and the slots to be + * called through a module-level const initialized with `new ThreadSlots`. */ +export declare class ThreadSlots { + /** Opens the family called `name`. */ + constructor(name: string); + /** This thread's value for `key`, or undefined. */ + get(key: K): V | undefined; + /** Whether this thread has a value for `key`. */ + has(key: K): boolean; + /** Sets this thread's value for `key`. Returns the slots. */ + set(key: K, value: V): this; + /** Removes this thread's value for `key`. Returns whether there was one. */ + delete(key: K): boolean; +} diff --git a/packages/experimental/index.js b/packages/experimental/index.js index 8a1b28c26..e5b824ed0 100644 --- a/packages/experimental/index.js +++ b/packages/experimental/index.js @@ -6,6 +6,8 @@ // program no longer references. An InternTable is a Map per name and // thread: Node.js threads share no objects, so every thread has its own // tables, and stored values are published (deep-frozen) as in scriptc. +// ThreadSlots is a WeakMap per name and thread: module state is per thread +// under Node.js, so each thread sees only the values it set. import { publish } from "@scriptc/threads"; @@ -90,3 +92,39 @@ export class InternTable { return value; } } + +/** This thread's slot families, by name. */ +const families = new Map(); + +export class ThreadSlots { + #values; + + /** Opens the family called `name`. */ + constructor(name) { + if (typeof name !== "string") throw new TypeError("The slots name must be a string"); + let values = families.get(name); + if (values === undefined) families.set(name, (values = new WeakMap())); + this.#values = values; + } + + /** This thread's value for `key`, or undefined. */ + get(key) { + return this.#values.get(key); + } + + /** Whether this thread has a value for `key`. */ + has(key) { + return this.#values.has(key); + } + + /** Sets this thread's value for `key`. Returns the slots. */ + set(key, value) { + this.#values.set(key, value); + return this; + } + + /** Removes this thread's value for `key`. Returns whether there was one. */ + delete(key) { + return this.#values.delete(key); + } +} diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 114650325..327f6fc97 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,7 +1,7 @@ { "name": "@scriptc/experimental", "version": "0.3.6", - "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects and intern tables shared between threads", + "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", "repository": { diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 54e789624..d4607f694 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -45,6 +45,7 @@ const BASE_RUNTIME_SOURCES = [ "scr_publish.c", "scr_region.c", "scr_itable.c", + "scr_slots.c", "scr_alloc.c", ]; diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6b5052482..96add68b6 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -43,6 +43,7 @@ const BASE_RUNTIME_SOURCES = [ "scr_publish.c", "scr_region.c", "scr_itable.c", + "scr_slots.c", "scr_alloc.c", ]; diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index a66d1c554..59cafe208 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -56,6 +56,7 @@ #include "scr_object.h" #include "scr_region.h" #include "scr_itable.h" +#include "scr_slots.h" #include "scr_async.h" #include "scr_events.h" #include "scr_readline.h" diff --git a/packages/runtime/src/scr_slots.c b/packages/runtime/src/scr_slots.c new file mode 100644 index 000000000..7c5bc7f88 --- /dev/null +++ b/packages/runtime/src/scr_slots.c @@ -0,0 +1,145 @@ +/* Per-thread slots (ThreadSlots, @scriptc/experimental): see scr_slots.h. */ + +#include "scr_runtime.h" + +#include +#include + +void *const scr_slots_none[SCR_SLOTS_PAGE] = {0}; + +/* The next slot index to assign. 0 means "no index". */ +static _Atomic uint32_t scr_slots_next = 1; + +/* This thread's blocks that hold or held values, and whether their release + * at thread exit is registered. */ +static SCR_TL ScrSlots *scr_slots_blocks; +static SCR_TL bool scr_slots_exit_registered; + +static _Noreturn void scr_slots_oom(void) { scr_trap("scriptc: out of memory\n"); } + +/* A page's entries follow its count of non-NULL entries. */ +static inline uintptr_t *scr_slots_count(void **page) { return (uintptr_t *)page - 1; } + +/* The slot index of the key `obj` with slot word `word`, assigned on first + * use. A published key may be read by every thread, so its index is set + * once with a compare-and-swap and every thread uses the winner's. An + * unpublished key is reachable from this thread only. */ +static uint32_t scr_slots_index(void *obj, uint32_t *word) { + uint32_t index = __atomic_load_n(word, __ATOMIC_RELAXED); + if (index != 0) return index; + index = atomic_fetch_add_explicit(&scr_slots_next, 1, memory_order_relaxed); + if (index == 0) scr_trap("scriptc: ThreadSlots ran out of slot indices\n"); + if (*(size_t *)obj != SIZE_MAX) { + __atomic_store_n(word, index, __ATOMIC_RELAXED); + return index; + } + uint32_t expected = 0; + if (__atomic_compare_exchange_n(word, &expected, index, false, __ATOMIC_RELAXED, + __ATOMIC_RELAXED)) + return index; + return expected; +} + +/* Releases the values of every block of this thread and frees their + * pages, leaving the blocks as a later first set finds them. A released + * value can free a key, whose scr_slots_forget then finds an already + * emptied block. */ +static void scr_slots_thread_exit(void) { + if (!scr_exit_frees()) return; + ScrSlots *blocks = scr_slots_blocks; + scr_slots_blocks = NULL; + scr_slots_exit_registered = false; + for (ScrSlots *s = blocks, *next; s != NULL; s = next) { + next = s->next; + void ***pages = s->pages; + uint32_t npages = s->npages; + s->pages = NULL; + s->npages = 0; + s->registered = 0; + s->next = NULL; + for (uint32_t p = 0; p < npages; p++) { + void **page = pages[p]; + if (page == (void **)scr_slots_none) continue; + for (uint32_t i = 0; i < SCR_SLOTS_PAGE; i++) + if (page[i] != NULL) s->release(page[i]); + free(scr_slots_count(page)); + } + free(pages); + } +} + +static void scr_slots_register(ScrSlots *s, void (*release)(void *)) { + s->release = release; + s->registered = 1; + s->next = scr_slots_blocks; + scr_slots_blocks = s; + if (!scr_slots_exit_registered) { + scr_slots_exit_registered = true; + scr_atexit(scr_slots_thread_exit); + } +} + +/* The page holding `index`, created when absent. */ +static void **scr_slots_page(ScrSlots *s, uint32_t index) { + uint32_t p = index >> SCR_SLOTS_PAGE_BITS; + if (p >= s->npages) { + uint32_t n = s->npages ? s->npages : 4; + while (n <= p) n = n > UINT32_MAX / 2 ? (UINT32_MAX >> SCR_SLOTS_PAGE_BITS) + 1 : n * 2; + void ***pages = realloc(s->pages, (size_t)n * sizeof *pages); + if (!pages) scr_slots_oom(); + for (uint32_t i = s->npages; i < n; i++) pages[i] = (void **)scr_slots_none; + s->pages = pages; + s->npages = n; + } + void **page = s->pages[p]; + if (page == (void **)scr_slots_none) { + uintptr_t *block = calloc(SCR_SLOTS_PAGE + 1, sizeof(void *)); + if (!block) scr_slots_oom(); + page = (void **)(block + 1); + s->pages[p] = page; + } + return page; +} + +/* Takes the entry `index` out of `s`; returns its value, or NULL. The + * page goes when its last entry does. */ +static void *scr_slots_take(ScrSlots *s, uint32_t index) { + uint32_t p = index >> SCR_SLOTS_PAGE_BITS; + if (p >= s->npages) return NULL; + void **page = s->pages[p]; + void **entry = &page[index & (SCR_SLOTS_PAGE - 1)]; + void *value = *entry; + if (value == NULL) return NULL; + *entry = NULL; + if (--*scr_slots_count(page) == 0) { + free(scr_slots_count(page)); + s->pages[p] = (void **)scr_slots_none; + } + return value; +} + +void scr_slots_set(ScrSlots *s, void *obj, uint32_t *word, void *value, void (*release)(void *)) { + if (!s->registered) scr_slots_register(s, release); + uint32_t index = scr_slots_index(obj, word); + void **page = scr_slots_page(s, index); + void **entry = &page[index & (SCR_SLOTS_PAGE - 1)]; + void *old = *entry; + *entry = value; + if (old == NULL) ++*scr_slots_count(page); + else s->release(old); +} + +bool scr_slots_delete(ScrSlots *s, const uint32_t *word) { + uint32_t index = __atomic_load_n(word, __ATOMIC_RELAXED); + void *old = index ? scr_slots_take(s, index) : NULL; + if (old == NULL) return false; + s->release(old); + return true; +} + +void scr_slots_forget(uint32_t index) { + for (ScrSlots *s = scr_slots_blocks; s != NULL; s = s->next) { + void *old = scr_slots_take(s, index); + if (old != NULL) s->release(old); + } +} diff --git a/packages/runtime/src/scr_slots.h b/packages/runtime/src/scr_slots.h new file mode 100644 index 000000000..5e0307b00 --- /dev/null +++ b/packages/runtime/src/scr_slots.h @@ -0,0 +1,73 @@ +/* scriptc runtime — per-thread slots (scr_slots.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_SLOTS_H +#define SCR_SLOTS_H + +#include +#include +#include + +/* ── per-thread slots (scr_slots.c) ─────────────────────────────────── + * ThreadSlots (@scriptc/experimental): a family of per-thread values keyed + * by class instances. Each instance of a class used as a key carries a + * 32-bit slot word, 0 until a value is first set for it on any thread. + * Setting assigns the word an index that is unique in the process and + * never reused; a published instance, which every thread may read, gets + * its index with a compare-and-swap, so all threads agree on it. + * + * Each family has one ScrSlots block per thread (an emitted thread-local + * global). The block maps indices to that thread's values through pages + * of SCR_SLOTS_PAGE entries, so a read is the slot word, the page and the + * entry, with no hashing, lock or atomic read-modify-write: + * + * page = word >> SCR_SLOTS_PAGE_BITS < npages ? pages[word >> BITS] : none + * value = page[word & (SCR_SLOTS_PAGE - 1)] + * + * Absent pages point at a shared page of NULL entries, and index 0 is + * never assigned, so a word of 0 reads NULL. The emitted read inlines + * this (llvm/thread-slots.ts); scr_slots_get is the same read in C. + * + * A block holds one reference to each value it stores. Values belong to + * the thread: the block releases them when they are replaced or deleted, + * when their key is freed (scr_slots_forget, called by the key class's + * free path), and when the thread exits. */ + +#define SCR_SLOTS_PAGE_BITS 10 +#define SCR_SLOTS_PAGE (1u << SCR_SLOTS_PAGE_BITS) + +/* The emitted layout is `{ ptr, i32, i32, ptr, ptr }`. */ +typedef struct ScrSlots { + void ***pages; /* npages entries; scr_slots_none where unset */ + uint32_t npages; + uint32_t registered; /* on this thread's list of blocks */ + void (*release)(void *); + struct ScrSlots *next; +} ScrSlots; + +/* The page that absent pages point at. */ +extern void *const scr_slots_none[SCR_SLOTS_PAGE]; + +/* This thread's value for the key whose slot word is `word`, borrowed, or + * NULL. */ +static inline void *scr_slots_get(const ScrSlots *s, const uint32_t *word) { + uint32_t index = __atomic_load_n(word, __ATOMIC_RELAXED); + uint32_t page = index >> SCR_SLOTS_PAGE_BITS; + if (page >= s->npages) return NULL; + return s->pages[page][index & (SCR_SLOTS_PAGE - 1)]; +} + +/* Stores `value` (one reference, moved in) for the key `obj`, whose slot + * word is `word`, and releases the value it replaces. `release` releases a + * value of the family's type. */ +void scr_slots_set(ScrSlots *s, void *obj, uint32_t *word, void *value, void (*release)(void *)); + +/* Removes and releases this thread's value for the key; false when there + * was none. */ +bool scr_slots_delete(ScrSlots *s, const uint32_t *word); + +/* The key with slot index `index` is being freed: releases its values in + * every block of the calling thread. Only the thread that owns an + * unpublished key can hold values for it. */ +void scr_slots_forget(uint32_t index); + +#endif /* SCR_SLOTS_H */ diff --git a/packages/runtime/src/scr_slots.test.c b/packages/runtime/src/scr_slots.test.c new file mode 100644 index 000000000..5aba5b266 --- /dev/null +++ b/packages/runtime/src/scr_slots.test.c @@ -0,0 +1,113 @@ +#undef NDEBUG +#include "scr_runtime.h" +#include +#include +#include +#include +#include + +/* The slots unit needs scr_trap, scr_exit_frees and the context's exit + * registration from the rest of the runtime; the exit callback is run + * here at the end of each thread. */ +_Noreturn void scr_trap(const char *msg) { + fputs(msg, stderr); + abort(); +} +bool scr_exit_frees(void) { return true; } +static _Thread_local void (*exit_fn)(void); +int scr_context_atexit(void (*fn)(void)) { + assert(exit_fn == NULL || exit_fn == fn); + exit_fn = fn; + return 0; +} + +enum { THREADS = 8, KEYS = 5000 }; + +/* A stand-in for a key object: the count word first, the slot word after. */ +typedef struct { + size_t rc; + uint32_t word; +} Key; + +/* A stand-in for a value: its owner thread and key. */ +typedef struct { + unsigned thread; + unsigned key; +} Value; + +static atomic_size_t released; +static void release(void *v) { + free(v); + atomic_fetch_add(&released, 1); +} + +static Value *value(unsigned thread, unsigned key) { + Value *v = malloc(sizeof *v); + assert(v); + v->thread = thread; + v->key = key; + return v; +} + +static Key shared[KEYS]; +static _Thread_local ScrSlots states; +static _Thread_local ScrSlots others; + +/* Every thread sets its own value for every shared key, in its own order, + * so threads race to assign each key's index, then reads its values back. */ +static void *run(void *arg) { + unsigned t = (unsigned)(uintptr_t)arg; + for (unsigned i = 0; i < KEYS; i++) { + unsigned k = (i * 7919u + t * 104729u) % KEYS; + scr_slots_set(&states, &shared[k], &shared[k].word, value(t, k), release); + } + for (unsigned k = 0; k < KEYS; k++) { + Value *v = scr_slots_get(&states, &shared[k].word); + assert(v && v->thread == t && v->key == k); + assert(scr_slots_get(&others, &shared[k].word) == NULL); + } + /* Replacing releases the old value; deleting releases and reports. */ + scr_slots_set(&states, &shared[0], &shared[0].word, value(t, 0), release); + assert(scr_slots_delete(&states, &shared[1].word)); + assert(!scr_slots_delete(&states, &shared[1].word)); + assert(scr_slots_get(&states, &shared[1].word) == NULL); + + /* Unpublished keys: forget releases a freed key's values in every block + * of the thread. */ + Key *local = calloc(KEYS, sizeof *local); + assert(local); + for (unsigned k = 0; k < KEYS; k++) { + local[k].rc = 1; + scr_slots_set(&states, &local[k], &local[k].word, value(t, k), release); + if (k % 2 == 0) scr_slots_set(&others, &local[k], &local[k].word, value(t, k), release); + } + for (unsigned k = 0; k < KEYS; k += 3) scr_slots_forget(local[k].word); + for (unsigned k = 0; k < KEYS; k++) { + Value *v = scr_slots_get(&states, &local[k].word); + assert(k % 3 == 0 ? v == NULL : v != NULL && v->key == k); + Value *o = scr_slots_get(&others, &local[k].word); + assert(k % 3 == 0 || k % 2 != 0 ? o == NULL : o != NULL && o->key == k); + } + free(local); + + /* Thread exit releases the rest and leaves the blocks empty. */ + assert(exit_fn != NULL); + exit_fn(); + assert(states.npages == 0 && others.npages == 0); + assert(scr_slots_get(&states, &shared[2].word) == NULL); + return NULL; +} + +int main(void) { + for (unsigned k = 0; k < KEYS; k++) shared[k].rc = SIZE_MAX; + pthread_t threads[THREADS]; + for (unsigned t = 0; t < THREADS; t++) + assert(pthread_create(&threads[t], NULL, run, (void *)(uintptr_t)t) == 0); + for (unsigned t = 0; t < THREADS; t++) assert(pthread_join(threads[t], NULL) == 0); + /* Every key got one index, and every value set was released. */ + for (unsigned k = 0; k < KEYS; k++) assert(shared[k].word != 0); + size_t sets = THREADS * ((size_t)KEYS + 1 + KEYS + (KEYS + 1) / 2); + assert(atomic_load(&released) == sets); + printf("thread slots keep one value per thread and key\n"); + return 0; +} diff --git a/packages/runtime/src/scr_slots.test.ts b/packages/runtime/src/scr_slots.test.ts new file mode 100644 index 000000000..80eb8e68b --- /dev/null +++ b/packages/runtime/src/scr_slots.test.ts @@ -0,0 +1,52 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const exec = promisify(execFile); + +/* The slots unit links alone (the test supplies what it calls), so the + * test of racing index assignment runs under ThreadSanitizer as well as + * AddressSanitizer. */ +const lanes = [ + { name: "address", flags: ["-fsanitize=address,undefined"], run: process.platform !== "win32" }, + { name: "thread", flags: ["-fsanitize=thread"], run: process.platform === "linux" }, +]; + +for (const lane of lanes) + test.skipIf(!lane.run)( + `thread slots keep one value per thread and key (${lane.name})`, + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-slots-")); + try { + const bin = join(dir, "slots"); + await exec("clang", [ + "-std=c11", + "-O1", + "-g", + "-pthread", + "-DSCR_WORKERS", + ...lane.flags, + ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []), + join(import.meta.dirname, "scr_slots.test.c"), + join(import.meta.dirname, "scr_slots.c"), + "-o", + bin, + ]); + const result = await exec(bin, [], { + timeout: 60000, + env: { + ...process.env, + UBSAN_OPTIONS: "halt_on_error=1", + TSAN_OPTIONS: "halt_on_error=1", + }, + }); + expect(result.stdout).toBe("thread slots keep one value per thread and key\n"); + expect(result.stderr).toBe(""); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + ); diff --git a/tests/corpus/5727-thread-slots-basics.ts b/tests/corpus/5727-thread-slots-basics.ts new file mode 100644 index 000000000..b8a1c43b5 --- /dev/null +++ b/tests/corpus/5727-thread-slots-basics.ts @@ -0,0 +1,85 @@ +import { ThreadSlots } from "@scriptc/experimental"; + +// ThreadSlots (@scriptc/experimental): per-thread values keyed by class +// instances, like a WeakMap. Keys of a base class and its subclasses share +// one family; constructions with one name open the same family. Node.js +// prints the same. + +class Node { + readonly name: string; + constructor(name: string) { + this.name = name; + } +} + +class Leaf extends Node { + readonly weight: number; + constructor(name: string, weight: number) { + super(name); + this.weight = weight; + } +} + +class Info { + count = 0; + readonly label: string; + constructor(label: string) { + this.label = label; + } +} + +const info = new ThreadSlots("basics.info"); +const sameInfo = new ThreadSlots("basics.info"); +const other = new ThreadSlots("basics.other"); + +function infoOf(n: Node): Info { + let found = info.get(n); + if (found === undefined) { + found = new Info(`info of ${n.name}`); + info.set(n, found); + } + found.count++; + return found; +} + +const a = new Node("a"); +const b = new Leaf("b", 2); +console.log(info.get(a), info.has(a), info.delete(a)); +console.log(infoOf(a).label, infoOf(a).count, infoOf(b).label, infoOf(b).count); +console.log(info.has(a), info.has(b), sameInfo.get(a)?.count, other.has(a)); + +// set returns the slots, so calls chain. +const c = new Leaf("c", 3); +const chained = other.set(a, new Info("other a")).set(c, new Info("other c")); +console.log(chained === other, other.get(a)?.label, other.get(c)?.label, other.has(b)); + +// Replacing and deleting. +other.set(a, new Info("other a again")); +console.log(other.get(a)?.label, other.delete(a), other.delete(a), other.get(a)); +console.log(sameInfo.delete(b), info.has(b), info.get(b)); + +// Many keys: values come back for each, after keys around them go away. +const kept: Node[] = []; +for (let i = 0; i < 5000; i++) { + const n = i % 2 === 0 ? new Node(`n${i}`) : new Leaf(`l${i}`, i); + info.set(n, new Info(`#${i}`)); + if (i % 3 === 0) kept.push(n); +} +let sum = 0; +for (const n of kept) sum += Number(info.get(n)!.label.slice(1)); +console.log(kept.length, sum, info.get(kept[kept.length - 1]!)?.label); + +// A key whose value refers back to the key. +class Holder { + readonly node: Node; + constructor(node: Node) { + this.node = node; + } +} +const backs = new ThreadSlots("basics.back"); +for (let i = 0; i < 100; i++) { + const n = new Node(`back${i}`); + backs.set(n, new Holder(n)); + if (i === 99) console.log(backs.get(n)?.node === n, backs.get(n)?.node.name); +} +console.log(typeof info); diff --git a/tests/corpus/5728-thread-slots-workers.ts b/tests/corpus/5728-thread-slots-workers.ts new file mode 100644 index 000000000..70bee484f --- /dev/null +++ b/tests/corpus/5728-thread-slots-workers.ts @@ -0,0 +1,107 @@ +import { Worker, isMainThread, parentPort, threadId, workerData } from "node:worker_threads"; +import { InternTable, ThreadSlots } from "@scriptc/experimental"; +import type { ProgramLifetime } from "@scriptc/types"; + +// ThreadSlots on shared keys: workers intern types in an InternTable, so in +// scriptc builds every thread reads the same published Type objects, and +// each thread keeps its own derived state for them in one ThreadSlots +// family. A thread sees only the values it set. Keys are ProgramLifetime +// instances, and some get a value before they are published. Node.js +// prints the same. + +class Type implements ProgramLifetime { + readonly id: number; + readonly name: string; + readonly members: readonly Type[]; + constructor(id: number, name: string, members: readonly Type[]) { + this.id = id; + this.name = name; + this.members = members; + } +} + +class TypeState { + readonly thread: number; + printed: string; + reads = 0; + constructor(thread: number, printed: string) { + this.thread = thread; + this.printed = printed; + } +} + +let nextLocalId = 0; +function nextId(): number { + return threadId * 1000000 + nextLocalId++; +} + +const interned = new InternTable("slots.types"); +const states = new ThreadSlots("slots.states"); + +function named(name: string): Type { + return interned.getOrInsertComputed(name, () => new Type(nextId(), name, [])); +} + +function listOf(element: Type): Type { + const key = `list:${print(element)}`; + const found = interned.get(key); + if (found !== undefined) return found; + // A value set before the type is published stays with it. + const fresh = new Type(nextId(), "list", [element]); + states.set(fresh, new TypeState(threadId, `${print(element)}[]`)); + return interned.getOrInsert(key, fresh); +} + +function print(t: Type): string { + let state = states.get(t); + if (state === undefined) { + const text = t.name === "list" ? `${print(t.members[0]!)}[]` : t.name; + state = new TypeState(threadId, text); + states.set(t, state); + } + state.reads++; + return state.printed; +} + +function run(seed: number): string { + const base = ["string", "number", "boolean", "bigint"].map(named); + const made: Type[] = [...base]; + let state = seed; + for (let r = 0; r < 2000; r++) { + state = (state * 1103515245 + 12345) % 2147483648; + const t = made[state % made.length]!; + made.push(r % 4 === 0 ? listOf(t) : t); + } + let reads = 0; + let own = 0; + const forms = new Set(); + for (const t of made) { + forms.add(print(t)); + const s = states.get(t)!; + reads += s.reads; + if (s.thread === threadId) own++; + } + let removed = 0; + for (const t of base) if (states.delete(t)) removed++; + return `${made.length} types, ${forms.size} forms, own state ${own === made.length}, reads ${reads > made.length}, removed ${removed}, after ${states.has(base[0]!)}`; +} + +if (isMainThread) { + const lines: string[] = []; + let pending = 3; + for (let w = 0; w < 3; w++) { + const worker = new Worker(new URL(import.meta.url), { workerData: { seed: w * 31 + 7 } }); + worker.on("message", (message: unknown) => { + lines[w] = `worker ${w}: ${message as string}`; + }); + worker.on("exit", () => { + if (--pending === 0) { + for (const line of lines) console.log(line); + console.log(`main: ${run(3)}`); + } + }); + } +} else { + const { seed } = workerData as { seed: number }; + parentPort!.postMessage(run(seed)); +} diff --git a/tests/corpus/5729-thread-slots-values.ts b/tests/corpus/5729-thread-slots-values.ts new file mode 100644 index 000000000..dbe905a06 --- /dev/null +++ b/tests/corpus/5729-thread-slots-values.ts @@ -0,0 +1,89 @@ +import { ThreadSlots } from "@scriptc/experimental"; +import { publish } from "@scriptc/threads"; + +// ThreadSlots value types (class instances, objects, arrays, Maps and +// Sets), families keyed by a base class and by one of its subclasses, and +// published keys. Node.js prints the same. + +class Shape { + readonly sides: number; + constructor(sides: number) { + this.sides = sides; + } +} + +class Square extends Shape { + readonly size: number; + constructor(size: number) { + super(4); + this.size = size; + } +} + +class Point { + readonly x: number; + readonly y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +interface Note { + text: string; + seen: number; +} + +const notes = new ThreadSlots("values.notes"); +const lists = new ThreadSlots("values.lists"); +const maps = new ThreadSlots>("values.maps"); +const sets = new ThreadSlots>("values.sets"); +const points = new ThreadSlots("values.points"); + +const triangle = new Shape(3); +const square = new Square(2); +notes.set(triangle, { text: "three", seen: 0 }); +notes.set(square, { text: "four", seen: 0 }); +notes.get(square)!.seen++; +lists.set(square, ["a"]); +lists.get(square)!.push("b"); +maps.set(square, new Map([["area", 4]])); +maps.get(square)!.set("perimeter", 8); +console.log(notes.get(triangle), notes.get(square), lists.get(square), lists.has(triangle)); +console.log([...maps.get(square)!], maps.has(new Square(2))); + +const origin = new Point(0, 0); +sets.set(origin, new Set([1, 2])); +sets.get(origin)!.add(3); +points.set(origin, new Point(1, 1)); +points.set(points.get(origin)!, origin); +console.log([...sets.get(origin)!], points.get(points.get(origin)!) === origin); + +// Published keys: the slots never write the key. +const frozen = publish(new Square(5)); +notes.set(frozen, { text: "published", seen: 1 }); +maps.set(frozen, new Map()); +console.log(notes.get(frozen)?.text, maps.get(frozen)?.size, frozen.size); +console.log(notes.delete(frozen), notes.has(frozen), maps.has(frozen)); + +// Replaced values and freed keys. +let total = 0; +for (let round = 0; round < 3; round++) { + const shapes: Shape[] = []; + for (let i = 0; i < 2000; i++) { + const s = i % 2 === 0 ? new Shape(i) : new Square(i); + shapes.push(s); + notes.set(s, { text: `first ${i}`, seen: 0 }); + notes.set(s, { text: `second ${i}`, seen: round }); + lists.set(s, [String(i)]); + } + for (const s of shapes) total += notes.get(s)!.seen + lists.get(s)!.length; +} +console.log(total, notes.get(triangle)?.text); + +// `Note | undefined` as Set elements: reads of that type are tagged boxes. +const seen = new Set(); +seen.add(notes.get(triangle)); +seen.add(notes.get(new Shape(9))); +seen.add(notes.get(triangle)); +console.log(seen.size, [...seen].map((n) => n?.text ?? "none").join(",")); diff --git a/tests/diagnostics/thread-slots-fences.ts b/tests/diagnostics/thread-slots-fences.ts new file mode 100644 index 000000000..7ec71db34 --- /dev/null +++ b/tests/diagnostics/thread-slots-fences.ts @@ -0,0 +1,29 @@ +import { ThreadSlots } from "@scriptc/experimental"; + +// ThreadSlots (@scriptc/experimental) forms scriptc refuses: names that are +// not string literals, one name opened with two value types, key and value +// types the slots cannot hold, calls through values other than a +// module-level const, and the class or its methods as values. + +class Key { + readonly id = 0; +} +class State { + count = 0; +} +interface Shape { + readonly id: number; +} + +const name = "dynamic"; +const named = new ThreadSlots(name); +const first = new ThreadSlots("fences.shared"); +const second = new ThreadSlots("fences.shared"); +const shaped = new ThreadSlots("fences.shaped"); +const callbacks = new ThreadSlots number>("fences.callbacks"); +function local(slots: ThreadSlots, key: Key): State | undefined { + return slots.get(key); +} +const ctor = ThreadSlots; +const getter = first.get; +console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); diff --git a/tests/harness/__snapshots__/thread-slots-fences.ts.txt b/tests/harness/__snapshots__/thread-slots-fences.ts.txt new file mode 100644 index 000000000..566ddfba6 --- /dev/null +++ b/tests/harness/__snapshots__/thread-slots-fences.ts.txt @@ -0,0 +1,62 @@ +thread-slots-fences.ts:19:43 - error SC1090: ThreadSlots names other than string literals are not supported yet + + 18 | const name = "dynamic"; + 19 | const named = new ThreadSlots(name); + | ^~~~ + 20 | const first = new ThreadSlots("fences.shared"); + + hint: name the slots with a string literal: every construction of the name opens the same slots + +thread-slots-fences.ts:21:16 - error SC1090: opening ThreadSlots "fences.shared" with different type arguments than its first construction is not supported yet + + 20 | const first = new ThreadSlots("fences.shared"); + 21 | const second = new ThreadSlots("fences.shared"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 22 | const shaped = new ThreadSlots("fences.shaped"); + + hint: every construction of one slots name must use the same key and value types + +thread-slots-fences.ts:22:16 - error SC1090: 'Shape' as a ThreadSlots key type is not supported yet + + 21 | const second = new ThreadSlots("fences.shared"); + 22 | const shaped = new ThreadSlots("fences.shaped"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 23 | const callbacks = new ThreadSlots number>("fences.callbacks"); + + hint: key the slots by instances of a class declared in the program + +thread-slots-fences.ts:23:19 - error SC1090: '() => number' as a ThreadSlots value type is not supported yet + + 22 | const shaped = new ThreadSlots("fences.shaped"); + 23 | const callbacks = new ThreadSlots number>("fences.callbacks"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 24 | function local(slots: ThreadSlots, key: Key): State | undefined { + + hint: store class instances, objects, arrays, Maps or Sets + +thread-slots-fences.ts:25:10 - error SC1090: ThreadSlots calls through a value other than a module-level const is not supported yet + + 24 | function local(slots: ThreadSlots, key: Key): State | undefined { + 25 | return slots.get(key); + | ^~~~~ + 26 | } + + hint: call the methods through `const slots = new ThreadSlots("name")` declared at module scope + +thread-slots-fences.ts:27:14 - error SC2020: 'ThreadSlots (@scriptc/experimental) as a value' is part of the standard library types but has no scriptc lowering yet + + 26 | } + 27 | const ctor = ThreadSlots; + | ^~~~~~~~~~~ + 28 | const getter = first.get; + + hint: open slots with new ThreadSlots("name") + +thread-slots-fences.ts:28:16 - error SC2020: 'ThreadSlots.get (@scriptc/experimental) as a value' is part of the standard library types but has no scriptc lowering yet + + 27 | const ctor = ThreadSlots; + 28 | const getter = first.get; + | ^~~~~~~~~ + 29 | console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); + + hint: call slots.get(...) directly \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 7f89295df..e66295a04 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1841,6 +1841,9 @@ "5648-dns-lookup-forms.ts": 2992, "5649-dns-service-servers.ts": 2712, "5650-dns-lookup-errors-untyped.cjs": 2822, + "5727-thread-slots-basics.ts": 3100, + "5728-thread-slots-workers.ts": 2500, + "5729-thread-slots-values.ts": 2550, "5800-broadcast-channel.ts": 2899, "5801-broadcast-channel.cjs": 2727, "5802-broadcast-channel-workers.ts": 2809, @@ -4525,6 +4528,9 @@ "5648-dns-lookup-forms.ts": 3416, "5649-dns-service-servers.ts": 4916, "5650-dns-lookup-errors-untyped.cjs": 3889, + "5727-thread-slots-basics.ts": 4500, + "5728-thread-slots-workers.ts": 5000, + "5729-thread-slots-values.ts": 4400, "5800-broadcast-channel.ts": 28374, "5801-broadcast-channel.cjs": 27251, "5802-broadcast-channel-workers.ts": 27251, diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 0cb55f690..5d814b384 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -310,6 +310,9 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { "5721-intern-table-values.ts", // Array key sizes are checked in 32-bit size_t arithmetic. "5726-intern-table-key-limits.ts", + // ThreadSlots blocks and pages hold 32-bit pointers. + "5727-thread-slots-basics.ts", + "5729-thread-slots-values.ts", ])("the LLVM target runs %s byte-identically to Node", async (fixture) => { const entry = join(repoRoot, "tests/corpus", fixture); const outDir = await mkdtemp("/tmp/scriptc-wasi-"); From 5d7234a61c9067aeb9180f99594ecc4ceb9fff7a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 05:12:27 -0500 Subject: [PATCH 006/120] Concatenate untyped Buffer lists as Node does Buffer.concat lowered only a statically typed Buffer[]/Uint8Array[] list or an array literal. Any other list, such as JavaScript's `const chunks = []`, reached an SC2020 fence at run time. Such lists now go to the runtime, which checks them as Node does (an Array whose elements are Uint8Arrays, ERR_INVALID_ARG_TYPE with Node's messages otherwise) before concatenating; the length form is covered as well. --- .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../src/frontend/lowering/containers/bytes.ts | 22 +++++++-- packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 2 + .../corpus/5973-buffer-concat-untyped.js.json | 6 +++ packages/runtime/src/scr_bytes.h | 4 ++ packages/runtime/src/scr_bytes_io.c | 46 +++++++++++++++++++ tests/corpus/5973-buffer-concat-untyped.js | 20 ++++++++ tests/harness/corpus-costs.json | 2 + 9 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-buffer-concat-untyped.js.json create mode 100644 tests/corpus/5973-buffer-concat-untyped.js diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 6516756e9..343393c2a 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -404,6 +404,8 @@ export const LIB_FN_SYMS: Record = { "buffer.fromDyn": "scr_buffer_from_dyn", "buffer.concat": "scr_buffer_concat", "buffer.concatLen": "scr_buffer_concat_len", + "buffer.concatDyn": "scr_buffer_concat_dyn", + "buffer.concatLenDyn": "scr_buffer_concat_len_dyn", "buffer.alloc": "scr_buffer_alloc", "buffer.allocUnsafe": "scr_buffer_alloc_unsafe", "buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 7a19927de..c41bf5163 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1511,11 +1511,23 @@ function lowerBufferStaticValue( } else { list = lowerer.lowerExpr(argNode); if (!typeEquals(list.type, arrayOf(BYTES_U8))) { - lowerer.noLowering( - `Buffer.concat of '${lowerer.fmt(list.type)}' values`, - argNode, - "one Uint8Array[]/Buffer[] value (or literal) is the lowered list shape", - ); + // Any other list (JavaScript's untyped `const chunks = []`, or a + // union of arrays): the runtime checks it as Node does (an Array of + // Uint8Arrays, the ERR_INVALID_ARG_TYPE errors otherwise) before + // concatenating. + const value: IrExpr = + list.type.kind === "dyn" ? list : { kind: "dynFrom", value: list, type: DYN, loc }; + if (args.length === 2) { + const total = lowerer.lowerExprExpecting(args[1]!, F64); + return { + kind: "libCall", + fn: "buffer.concatLenDyn", + args: [value, total], + type: BYTES_U8, + loc, + }; + } + return { kind: "libCall", fn: "buffer.concatDyn", args: [value], type: BYTES_U8, loc }; } } if (args.length === 2) { diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 96bd381f7..a872f3f42 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -670,6 +670,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.scryptCb", "buffer.concat", "buffer.concatLen", + "buffer.concatDyn", + "buffer.concatLenDyn", "buffer.fromDyn", "buffer.fromStrChecked", "buffer.alloc", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6c09eeb01..9eb0948e0 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2575,6 +2575,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * opaque reference inputs retain explicit runtime refusals. */ "buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 }, + "buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 }, /** The pooled Buffer factories (Node's allocate/fromStringFast/ * fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays, * number[] and checked values, and concat return views into the @@ -2601,6 +2602,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * zero-padded to the total. THROWS Node's 'length' RangeError on a * negative/non-integer total (may-throw seed). */ "buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 }, + "buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 }, // The checked-dynamic compare/equals validators (Node's argument // ladders over dyn-boxed invalid-input probes). /** The checked-dynamic compare/equals validators (scr_bytes_io.c) — diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-buffer-concat-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-buffer-concat-untyped.js.json new file mode 100644 index 000000000..3fffd75c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-buffer-concat-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5973-buffer-concat-untyped.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 4309154c9..993921043 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -389,6 +389,10 @@ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); +/* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list + * and each element, then the typed concat. NULL with the exception pending. */ +ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list); +ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total); /* Drops the pool's slab reference and restores Buffer.poolSize (process * exit audits and library session resets). */ void scr_buffer_pool_reset(void); diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 6fe829eeb..fc7b3bbb8 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1249,6 +1249,52 @@ static ScrBytes *scr_buffer_from_array_like(const ScrDyn *value, double length) return out; } +/* Buffer.concat over an untyped list: Node's checks (an Array whose every + * element is a Uint8Array, ERR_INVALID_ARG_TYPE otherwise), then the typed + * concat over the elements. total < 0 selects the one-argument form. NULL + * with the exception pending. */ +static ScrBytes *scr_buffer_concat_dyn_impl(const ScrDyn *list, double total, bool has_total) { + ScrDyn *materialized = NULL; + if (list->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(list) != NULL) { + materialized = scr_dyn_typed_ref_materialize(list); + list = materialized; + } + if (list->kind != SCR_DYN_ARR) { + scr_dyn_arg_type_fail("list", "an instance of Array", list); + scr_dyn_release(materialized); + return NULL; + } + size_t n = list->v.arr.len; + ScrArr *parts = scr_arr_new(SCR_ELEM_BYTES, (double)n); + for (size_t i = 0; i < n; i++) { + ScrDyn *item = scr_dyn_index_get(list, (double)i); + const ScrDyn *shown = item != NULL ? item : scr_dyn_undefined(); + if (!scr_dyn_bytes_is(shown, SCR_BYTES_U8) || shown->v.bytes->is_data_view) { + char name[40]; + snprintf(name, sizeof name, "list[%zu]", i); + scr_dyn_arg_type_fail(name, "an instance of Buffer or Uint8Array", shown); + scr_dyn_release(item); + scr_arr_release(parts); + scr_dyn_release(materialized); + return NULL; + } + scr_arr_push_ref(parts, scr_bytes_retain(shown->v.bytes)); + scr_dyn_release(item); + } + ScrBytes *out = has_total ? scr_buffer_concat_len(parts, total) : scr_buffer_concat(parts); + scr_arr_release(parts); + scr_dyn_release(materialized); + return out; +} + +ScrBytes *scr_buffer_concat_dyn(const ScrDyn *list) { + return scr_buffer_concat_dyn_impl(list, 0, false); +} + +ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { + return scr_buffer_concat_dyn_impl(list, total, true); +} + ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); diff --git a/tests/corpus/5973-buffer-concat-untyped.js b/tests/corpus/5973-buffer-concat-untyped.js new file mode 100644 index 000000000..54256c935 --- /dev/null +++ b/tests/corpus/5973-buffer-concat-untyped.js @@ -0,0 +1,20 @@ +// Buffer.concat over an untyped list (JavaScript's `const chunks = []`): +// Uint8Array elements concatenate, the length form pads or truncates, and +// a non-array list or a non-Uint8Array element throws Node's TypeError. +const chunks = []; +chunks.push(Buffer.from("ab")); +chunks.push(new Uint8Array([99, 100])); +console.log(Buffer.concat(chunks).toString()); +console.log(Buffer.concat(chunks, 6)); +console.log(Buffer.concat(chunks, 3).toString()); +console.log(Buffer.concat([]).length); + +const lists = [[1], ["a"], [Buffer.from("x"), null], "x", 5, [new Uint16Array(1)]]; +for (const list of lists) { + try { + Buffer.concat(list); + console.log("no error"); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 24eca7d43..9551e9ad0 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1901,6 +1901,7 @@ "5970-net-connect-host-name.ts": 2400, "5971-buffer-chunks-to-string.ts": 2700, "5972-net-set-encoding-listeners.ts": 2550, + "5973-buffer-concat-untyped.js": 1900, "5990-worker-uncloneable.mjs": 3283, "5991-worker-async-dispose.mjs": 5699, "5992-worker-thread-name-lifetime.mjs": 2687, @@ -4588,6 +4589,7 @@ "5970-net-connect-host-name.ts": 2800, "5971-buffer-chunks-to-string.ts": 3100, "5972-net-set-encoding-listeners.ts": 3000, + "5973-buffer-concat-untyped.js": 2900, "5990-worker-uncloneable.mjs": 22432, "5991-worker-async-dispose.mjs": 25768, "5992-worker-thread-name-lifetime.mjs": 28932, From 0e67cd1e3bad393b6cc067852c05d367b82ed44c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 08:02:34 -0500 Subject: [PATCH 007/120] Place functions too large to inline by their own name The helper keeps an internal function that exactly one other function references in its referrer's partition, so the partition can inline it. A program's module initializers are each called only from the entry point, so all of them, with every single-use helper they call, shared the entry point's partition. In the three.js nodes fixture that partition held 1.1 million lines of LLVM IR, almost five times the average, and set the native build's longest compile. A function whose body is larger than 256 KiB of LLVM text is past LLVM's inline cost limit even for the only call to a local function, so it is now placed by the hash of its own name. Its own single-use callees still stay with it. --- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 10 +++++----- .../generated/node-v24-backlog.json | 19 +++++++++++-------- .../generated/node-v24-internal.json | 12 ++++++++---- native/llvm-codegen/src/partition.cpp | 10 ++++++++-- native/llvm-codegen/src/partition.h | 4 ++-- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 500876593..db36cdb52 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "56f660f5e2eb7391a048", + "artifactVersion": "cade1b8e6e0c5c42fcc9", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 568cdb4c9..dadc9e836 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -569,9 +569,9 @@ "apiEntries": 120, "static": { "supported": 0, - "partial": 50, + "partial": 51, "refused": 0, - "not-implemented": 70, + "not-implemented": 69, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -57593,9 +57593,9 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 683d477a5..12b12d63b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1142, - "verify-gap": 2187, + "audit-partial": 1143, + "verify-gap": 2186, "replace-refusal": 40, "classify": 2, "implement": 85 @@ -57507,12 +57507,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.process.execArgv", + "tests": [ + "tests/corpus/990-process-basics.ts", + "tests/corpus/2314-process-introspection.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 393955bc0..2b97f675e 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -596,9 +596,9 @@ "apiEntries": 120, "static": { "supported": 0, - "partial": 50, + "partial": 51, "refused": 0, - "not-implemented": 70, + "not-implemented": 69, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -80949,8 +80949,12 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "surface-manifest:node-builtin.process.execArgv", + "tests": [ + "tests/corpus/990-process-basics.ts", + "tests/corpus/2314-process-introspection.ts" + ] }, "anchorSource": "chapter" }, diff --git a/native/llvm-codegen/src/partition.cpp b/native/llvm-codegen/src/partition.cpp index 066ba614c..8ad696d33 100644 --- a/native/llvm-codegen/src/partition.cpp +++ b/native/llvm-codegen/src/partition.cpp @@ -418,7 +418,12 @@ std::optional partitionProgramText(StringRef Text, // An internal function referenced by exactly one other function, and by // no global, is a whole-program inlining candidate; keep it with that - // function so its partition can inline it without importing. + // function so its partition can inline it without importing. A function + // body larger than this is past LLVM's inline cost limit even for the only + // call to a local function, so it is placed by its own name. Otherwise a + // program's module initializers, which only the entry point calls, would + // all share the entry point's partition. + constexpr size_t MaxInlinedBodyBytes = 256 * 1024; constexpr unsigned Shared = ~1u; std::vector Referrer(Symbols.size(), NoPartition); for (unsigned I = 0; I < Symbols.size(); ++I) { @@ -441,7 +446,8 @@ std::optional partitionProgramText(StringRef Text, for (unsigned Steps = 0; Steps < 1024; ++Steps) { const Symbol &S = Symbols[Root]; unsigned Parent = Referrer[Root]; - if (!S.Local || Parent == NoPartition || Parent == Shared || + if (!S.Local || S.Body.size() > MaxInlinedBodyBytes || + Parent == NoPartition || Parent == Shared || Symbols[Parent].Kind != SymbolKind::Function || Parent == I) break; Root = Parent; diff --git a/native/llvm-codegen/src/partition.h b/native/llvm-codegen/src/partition.h index 9d48548fb..838919443 100644 --- a/native/llvm-codegen/src/partition.h +++ b/native/llvm-codegen/src/partition.h @@ -22,8 +22,8 @@ struct ProgramPartitions { // Divides the LLVM assembly scriptc emits into Count modules without parsing // it, so the partitions can be parsed concurrently. Functions are placed by a // stable hash of their name; an internal function referenced by exactly one -// other function stays with that function, and a global stays with the first -// function that references it. Returns nullopt for any construct outside the +// other function stays with that function unless its body is too large to +// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the // emitter's narrow top-level grammar; callers then split a parsed module. std::optional partitionProgramText(llvm::StringRef Text, unsigned Count); From 57b6a4460c37590a541b6821534369c3c6f7d8d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 08:03:20 -0500 Subject: [PATCH 008/120] Compare the zlib-shims npm case with Node With untyped Buffer.concat lists and typeof on builtin module exports supported, the fixture runs to completion and matches Node, so its expected deferred SC2020 refusal is removed. --- tests/harness/npm.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index a6cdee2b5..71b8a61d1 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -109,7 +109,6 @@ const KNOWN: Record = { "web-streams": { build: "SC2013" }, "workspace-copied": { build: "SC2013" }, "workspace-unclean": { build: "SC2013" }, - "zlib-shims": { run: "SC2020" }, }; const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name); From e2c7eac01db6b7b3ae00f54b7bd9776748d4879e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 21:01:15 -0700 Subject: [PATCH 009/120] Record order baselines for the type-only import fixtures Co-Authored-By: Claude Opus 5.5 --- .../corpus/6120-type-only-import-graph/main.ts.json | 8 ++++++++ .../corpus/6121-type-specifier-edges/main.ts.json | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json new file mode 100644 index 000000000..979391c1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6120-type-only-import-graph/mixed.ts", + "/tests/corpus/6120-type-only-import-graph/flags.ts", + "/tests/corpus/6120-type-only-import-graph/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json new file mode 100644 index 000000000..c0f3f653c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/6121-type-specifier-edges/config.ts", + "/tests/corpus/6121-type-specifier-edges/config-barrel.ts", + "/tests/corpus/6121-type-specifier-edges/b.ts", + "/tests/corpus/6121-type-specifier-edges/a.ts", + "/tests/corpus/6121-type-specifier-edges/main.ts" + ], + "diags": [] +} From b4e25cdba0708b1b2a4aacbd499ec3ff21871c8c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 21:01:15 -0700 Subject: [PATCH 010/120] Leave an @types twin with no runtime package to the declaration bar in transitive selection Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/pipeline.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 20df75c9b..c769ed99e 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -214,7 +214,9 @@ function detectAutoPackages( continue; } if (judged?.has(npm.packageName)) continue; - if (viaTransitive) { + // An @types twin with no runtime package installed has nothing to + // compile (Node cannot load it either). + if (viaTransitive && !npm.packageName.startsWith("@types/")) { judged!.add(npm.packageName); sites!.set(npm.packageName, loc); transitive.add(npm.packageName); From 1c19d6613df8a52d64e4536724d84f6d7eaeec1f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 21:01:16 -0700 Subject: [PATCH 011/120] Compile the deprecated punycode and constants builtin modules - node:punycode: encode, decode, toASCII, toUnicode, ucs2.decode, ucs2.encode and version, ported from the punycode.js 2.1.0 Node bundles (scr_url.c), with its RangeErrors. DEP0040 follows Node: a static ES import reports before any module evaluates and before the program can register a 'warning' listener; require() reports on the next tick, once for both spellings, not when a dependency under node_modules loads the module first, and not under process.noDeprecation. - node:constants: the frozen object Node builds from os.constants' dlopen, errno, priority and signals groups, fs.constants (node_constants.cc order and guards) and crypto.constants (scr_os.c); its bindings read it as a checked-dynamic value. These unblock tr46 (whatwg-url, node-fetch@2) and graceful-fs (fs-extra). Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/limitations.mdx | 2 + .../builtin-apis/deprecated-modules.mdx | 1 + .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 48 +-- .../generated/node-v24-backlog.json | 116 +++--- .../generated/node-v24-internal.json | 76 +++- internal/compatibility/static-support.json | 1 + .../ambient/scriptc-node-fallback.d.ts | 33 ++ .../compiler/src/backend/llvm/lib-dispatch.ts | 2 + .../compiler/src/backend/llvm/lib-shared.ts | 8 + .../compiler/src/frontend/builtin-modules.ts | 2 + .../frontend/lowering/lower-builtin-values.ts | 32 ++ .../src/frontend/lowering/lower-exprs.ts | 14 + .../src/frontend/lowering/lower-modules.ts | 1 + .../src/frontend/lowering/lower-stmts.ts | 4 +- .../compiler/src/frontend/lowering/lowerer.ts | 77 ++++ .../src/frontend/lowering/surfaces.ts | 17 + packages/compiler/src/ir/builtin-effects.ts | 7 + .../compiler/src/ir/builtin-signatures.ts | 17 + packages/compiler/src/ir/runtime-features.ts | 2 + packages/compiler/surface-manifest.json | 57 +++ .../corpus/6122-punycode-conversions.ts.json | 6 + .../6124-punycode-require-warnings.cjs.json | 6 + .../corpus/6125-constants-module.cjs.json | 6 + .../6126-punycode-dependency/main.cjs.json | 18 + packages/runtime/src/scr_async.h | 5 + packages/runtime/src/scr_async_dyn.c | 21 ++ packages/runtime/src/scr_os.c | 194 ++++++++++ packages/runtime/src/scr_os.h | 4 + packages/runtime/src/scr_url.c | 339 ++++++++++++++++++ packages/runtime/src/scr_url.h | 9 + tests/corpus/6122-punycode-conversions.ts | 45 +++ .../corpus/6124-punycode-require-warnings.cjs | 32 ++ tests/corpus/6125-constants-module.cjs | 26 ++ .../corpus/6126-punycode-dependency/main.cjs | 12 + .../node_modules/puny-dep/index.d.ts | 2 + .../node_modules/puny-dep/index.js | 8 + .../node_modules/puny-dep/package.json | 5 + 38 files changed, 1169 insertions(+), 88 deletions(-) create mode 100644 docs/content/limitations/builtin-apis/deprecated-modules.mdx create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json create mode 100644 tests/corpus/6122-punycode-conversions.ts create mode 100644 tests/corpus/6124-punycode-require-warnings.cjs create mode 100644 tests/corpus/6125-constants-module.cjs create mode 100644 tests/corpus/6126-punycode-dependency/main.cjs create mode 100644 tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts create mode 100644 tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js create mode 100644 tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index ed2c5b1ec..a66b2ffd6 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -74,6 +74,8 @@ title: "Limitations" ../limitations/builtin-apis/legacy-url-api.mdx +../limitations/builtin-apis/deprecated-modules.mdx + ../limitations/builtin-apis/filesystem-flags.mdx ../limitations/builtin-apis/copy.mdx diff --git a/docs/content/limitations/builtin-apis/deprecated-modules.mdx b/docs/content/limitations/builtin-apis/deprecated-modules.mdx new file mode 100644 index 000000000..bb630b8c3 --- /dev/null +++ b/docs/content/limitations/builtin-apis/deprecated-modules.mdx @@ -0,0 +1 @@ +- **Deprecated `punycode` and `constants` modules:** `punycode.encode()`, `decode()`, `toASCII()`, `toUnicode()`, `ucs2.decode()`, `ucs2.encode()`, and `version` compile, with the `DEP0040` warning Node.js reports when the module loads. A `require("punycode")` made from inside `node_modules` doesn't warn; a `require()` used inside a larger expression, such as `require("punycode").toASCII(name)`, doesn't warn either. The `punycode.ucs2` object itself is unsupported as a value. The `constants` module is the frozen object Node.js builds from `os.constants` and `fs.constants`, plus the `crypto.constants` of Node.js 24.22.0. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 93149c51c..a063052f3 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "a1ff929d49885bd8c92e", + "artifactVersion": "7b345b86e2e5f616a7a0", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 352c211b5..62eb8283e 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -584,9 +584,9 @@ "apiEntries": 9, "static": { "supported": 0, - "partial": 0, + "partial": 8, "refused": 0, - "not-implemented": 9, + "not-implemented": 1, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -58489,7 +58489,7 @@ "inherited": false }, "static": { - "status": "not-implemented", + "status": "partial", "detail": "Derived from the exact API rows in this chapter.", "verification": "derived" } @@ -58512,9 +58512,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -58535,9 +58535,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -58558,9 +58558,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -58581,9 +58581,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -58627,9 +58627,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -58650,9 +58650,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -58673,9 +58673,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index fff784adc..c4ce78314 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1091, - "verify-gap": 2238, + "audit-partial": 1098, + "verify-gap": 2231, "replace-refusal": 40, "classify": 2, "implement": 85 @@ -58534,12 +58534,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:punycode", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.punycode.decode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] } } }, @@ -58559,12 +58563,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:punycode", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.punycode.encode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] } } }, @@ -58584,12 +58592,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:punycode", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.punycode.toASCII", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] } } }, @@ -58609,12 +58621,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:punycode", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.punycode.toUnicode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] } } }, @@ -58659,12 +58675,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:punycode", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.punycode.ucs2.decode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] } } }, @@ -58684,12 +58704,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:punycode", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.punycode.ucs2.encode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] } } }, @@ -58709,12 +58733,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:punycode", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.punycode.version", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 559bd48ca..58ffc3399 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -611,9 +611,9 @@ "apiEntries": 9, "static": { "supported": 0, - "partial": 0, + "partial": 8, "refused": 0, - "not-implemented": 9, + "not-implemented": 1, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -81908,8 +81908,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "derived:descendants" + "status": "partial", + "evidence": "derived:descendants", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "chapter" }, @@ -81931,8 +81936,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:punycode" + "status": "partial", + "evidence": "surface-manifest:node-builtin.punycode.decode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "exact" }, @@ -81954,8 +81964,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:punycode" + "status": "partial", + "evidence": "surface-manifest:node-builtin.punycode.encode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "exact" }, @@ -81977,8 +81992,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:punycode" + "status": "partial", + "evidence": "surface-manifest:node-builtin.punycode.toASCII", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "exact" }, @@ -82000,8 +82020,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:punycode" + "status": "partial", + "evidence": "surface-manifest:node-builtin.punycode.toUnicode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "exact" }, @@ -82046,8 +82071,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:punycode" + "status": "partial", + "evidence": "surface-manifest:node-builtin.punycode.ucs2.decode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "exact" }, @@ -82069,8 +82099,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:punycode" + "status": "partial", + "evidence": "surface-manifest:node-builtin.punycode.ucs2.encode", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "exact" }, @@ -82092,8 +82127,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:punycode" + "status": "partial", + "evidence": "surface-manifest:node-builtin.punycode.version", + "tests": [ + "tests/corpus/6122-punycode-conversions.ts", + "tests/corpus/6124-punycode-require-warnings.cjs", + "tests/corpus/6126-punycode-dependency/main.cjs" + ] }, "anchorSource": "chapter" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index d68011393..5332ddd4c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -146,6 +146,7 @@ "node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"], "node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"], "node-builtin.process.exitCode": ["tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"], + "node-builtin.punycode": ["tests/corpus/6122-punycode-conversions.ts", "tests/corpus/6124-punycode-require-warnings.cjs", "tests/corpus/6126-punycode-dependency/main.cjs"], "node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"], "node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"], "node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"], diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index d87172f61..2c53bb523 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3643,6 +3643,39 @@ declare module "node:querystring" { export * from "querystring"; } +/* node:punycode (deprecated, DEP0040) — punycode.js 2.1.0: decode and + * toUnicode throw its RangeErrors on malformed input; ucs2 converts + * between strings and UTF-16 code point arrays. */ +declare module "punycode" { + export function decode(string: string): string; + export function encode(string: string): string; + export function toUnicode(domain: string): string; + export function toASCII(domain: string): string; + export namespace ucs2 { + function decode(string: string): number[]; + function encode(codePoints: readonly number[]): string; + } + export const version: string; +} +declare module "node:punycode" { + export * from "punycode"; +} + +/* node:constants (deprecated, DEP0008) — one frozen object: os.constants' + * dlopen, errno, priority and signals groups, fs.constants, and + * crypto.constants. A platform's missing constant reads as undefined. */ +declare module "constants" { + const constants: { readonly [name: string]: number } & { + readonly defaultCoreCipherList: string; + readonly defaultCipherList: string; + }; + export = constants; +} +declare module "node:constants" { + import constants = require("constants"); + export = constants; +} + /* node:readline — the question/close slice: createInterface over exactly * { input: process.stdin, output: process.stdout }, question(query, cb) * (the query writes to stdout, the callback gets the next line's text), diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 50804bf5a..92dbaea76 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -680,6 +680,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "events": case "blob": case "record": + case "punycode": + case "constants": return host.emitGenericLibCall(e); case "test": return emitTestLibCall(host, e); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9232cbd03..700044da2 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -192,6 +192,7 @@ export const LIB_FN_SYMS: Record = { "os.cpusModel": "scr_os_cpus_model", "os.cpusField": "scr_os_cpus_field", "os.constants": "scr_os_constants", + "constants.object": "scr_node_constants", "os.release": "scr_os_release", "os.userName": "scr_os_user_name", "os.userShell": "scr_os_user_shell", @@ -631,6 +632,13 @@ export const LIB_FN_SYMS: Record = { "url.legacyFormat": "scr_url_legacy_format", "url.legacyResolve": "scr_url_legacy_resolve", "url.domainToASCII": "scr_url_domain_to_ascii", + "punycode.decode": "scr_punycode_decode", + "punycode.encode": "scr_punycode_encode", + "punycode.toASCII": "scr_punycode_to_ascii", + "punycode.toUnicode": "scr_punycode_to_unicode", + "punycode.ucs2Decode": "scr_punycode_ucs2_decode", + "punycode.ucs2Encode": "scr_str_from_code_point", + "punycode.warn": "scr_punycode_warn", "url.domainToUnicode": "scr_url_domain_to_unicode", "url.pathToFileURL": "scr_url_from_path", "url.pathToFileURLPlatform": "scr_url_from_path_platform", diff --git a/packages/compiler/src/frontend/builtin-modules.ts b/packages/compiler/src/frontend/builtin-modules.ts index b45dce6d7..3a1acb934 100644 --- a/packages/compiler/src/frontend/builtin-modules.ts +++ b/packages/compiler/src/frontend/builtin-modules.ts @@ -42,6 +42,8 @@ export const SUPPORTED_BUILTIN_MODULES = [ "module", "console", "process", + "punycode", + "constants", ] as const; /** Builtins Node itself serves ONLY under the node: prefix — diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index a9df9e662..c05430ad3 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -23,6 +23,7 @@ import { builtinModulesArrayLit, stdlibGlobalNameOf, } from "./surfaces.js"; +import { CRYPTO_CONSTANTS } from "./crypto-tables.js"; const PROTOTYPE_METHODS: Record = { Date: ["getTime", "valueOf", "toISOString", "toJSON"], @@ -680,3 +681,34 @@ export function lowerBuiltinLoaderValue(lowerer: Lowerer, loc: SrcLoc): IrExpr { loc, }; } + +/** The deprecated node:constants module object: the runtime merges + * os.constants' groups and fs.constants with crypto.constants, which a + * lifted function answers from the baked table the first time the object + * is built. */ +export function builtinConstantsObject(lowerer: Lowerer, loc: SrcLoc): IrExpr { + const name = "%builtin.constants.crypto"; + if (!lowerer.builtinCallableValueFns.has(name)) { + lowerer.builtinCallableValueFns.set(name, name); + const fields = Object.entries(CRYPTO_CONSTANTS).map(([key, value]) => ({ + key: strLit(key, loc), + value: box(builtinConstLit(value, loc)), + })); + lowerer.liftedFns.push({ + name, + params: [], + returnType: DYN, + locals: [], + body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }], + loc, + }); + } + const table: IrExpr = { + kind: "closure", + fnName: name, + captures: [], + type: { kind: "func", params: [], ret: DYN }, + loc, + }; + return { kind: "libCall", fn: "constants.object", args: [box(table)], type: DYN, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 08e12518b..88a4918d8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -216,6 +216,7 @@ import { } from "./class-prototypes.js"; import { isClassCallback } from "./class-callbacks.js"; import { + builtinConstantsObject, builtinPrototypeMethod, lowerArrayIsArrayValue, lowerCheckedPredicateValue, @@ -1164,6 +1165,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (bi.module === "os" && bi.member === "constants") { return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc }; } + // `import { O_RDONLY } from "constants"`: the member of the frozen + // module object. + if (bi.module === "constants") { + return { + kind: "dynKeyGet", + value: builtinConstantsObject(lowerer, loc), + key: { kind: "strLit", value: bi.member, type: STRING, loc }, + type: DYN, + loc, + }; + } // tls.rootCertificates: a runtime-valued module constant (the // cached bundled-CA array) — the one member read that lowers // to a libCall instead of a baked literal. @@ -1206,6 +1218,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // their access sites only. The CommonJS namespace binding // (`const lib = require("./lib.js")`) gets the same fence. const builtinNamespace = lowerer.builtinNamespaceModuleOf(expr); + if (builtinNamespace === "constants") return builtinConstantsObject(lowerer, loc); if (builtinNamespace !== null) { const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNamespace}` }; return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc }; @@ -1495,6 +1508,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc }; } const builtinNs = lowerer.builtinNamespaceModuleOf(expr); + if (builtinNs === "constants") return builtinConstantsObject(lowerer, loc); if (builtinNs !== null) { const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNs}` }; return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d18c4f547..b2f76cd24 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2656,6 +2656,7 @@ export function lowerFileInit( loc: loc0, }); } + if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning()); for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) { const npm = lowerer.npmInitActions.get(sf)?.get(stmt); if (npm !== undefined && npm.length > 0) { diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 367c147f8..27f4169c1 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1557,7 +1557,9 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement): // Relative requires keep the fence: their module-init-at-position // semantics needs storage this model doesn't represent. if (canonicalBuiltinModule(spec) !== null) { - return []; + return canonicalBuiltinModule(spec) === "punycode" + ? [lowerer.punycodeRequireWarning(decl)] + : []; } lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3217802ad..8e6f64143 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -128,6 +128,7 @@ import { resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; import { tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; +import { erasedTypeOnlyImport } from "../type-only-imports.js"; import { literalValues } from "../literal-values.js"; import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js"; import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; @@ -3745,6 +3746,65 @@ export class Lowerer { for (const fp of parts) if (!indexOf.has(fp.sf)) visit(fp.sf); } + /** node:punycode's load at a require() call: the module body reports + * DEP0040 when it first runs, unless isInsideNodeModules() finds the + * requiring frame in a dependency, and process.emitWarning delivers the + * report on the next tick. Later loads are cache hits either way. */ + punycodeRequireWarning(node: ts.Node): IrStmt { + const loc = locOf(node); + const fromDependency = /[\\/]node_modules[\\/]/.test(node.getSourceFile().fileName); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "punycode.warn", + args: [ + { kind: "boolLit", value: false, type: BOOL, loc }, + { kind: "boolLit", value: !fromDependency, type: BOOL, loc }, + ], + type: VOID, + loc, + }, + loc, + }; + } + + /** node:punycode's DEP0040 for a static ES import anywhere in the module + * graph: Node links builtins before any module evaluates, so the report + * comes first, before the program can register a 'warning' listener — + * whichever file imports it (a dependency's ES import has no requiring + * frame either). Prepended to the entry's %init. */ + punycodeLinkWarning(): IrStmt[] { + const imports = this.moduleOrder.some((sf) => + sf.statements.some( + (stmt) => + ((ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) || + (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) && + stmt.moduleSpecifier !== undefined && + ts.isStringLiteral(stmt.moduleSpecifier) && + canonicalBuiltinModule(stmt.moduleSpecifier.text) === "punycode", + ), + ); + if (!imports) return []; + const loc: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 }; + return [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "punycode.warn", + args: [ + { kind: "boolLit", value: true, type: BOOL, loc }, + { kind: "boolLit", value: true, type: BOOL, loc }, + ], + type: VOID, + loc, + }, + loc, + }, + ]; + } + /** The lowering of a CommonJS `require("./local")` occurrence: a call of * the required module's run-once %init at exactly this statement's * position — Node's inline evaluation, with the guard supplying the @@ -3754,6 +3814,7 @@ export class Lowerer { * their external dependencies by name). Null for everything else * (builtins load nothing; the rest kept its preflight fence). */ requireInitStmt(spec: string, node: ts.Node): IrStmt | null { + if (canonicalBuiltinModule(spec) === "punycode") return this.punycodeRequireWarning(node); // Project requires resolve within the program; another bare require can // be a program-module edge when it names an opted-in --npm-static package // (one package requiring another — the resolution answered its shipped @@ -11286,7 +11347,23 @@ export class Lowerer { return { module: "timers/promises", member: `scheduler.${access.name.text}` }; } } + // punycode.ucs2.decode / encode: methods of the module's ucs2 object, + // dispatched as module members. + if (access.name.text === "decode" || access.name.text === "encode") { + const owner = access.expression; + const imported = ts.isIdentifier(owner) + ? this.builtinImportOf(owner) + : ts.isPropertyAccessExpression(owner) + ? this.builtinMemberOf(owner) + : null; + if (imported?.module === "punycode" && imported.member === "ucs2") { + return { module: "punycode", member: `ucs2.${access.name.text}` }; + } + } const module = this.builtinNamespaceModuleOf(access.expression); + // node:constants is one checked-dynamic object: its members read + // through the object itself (builtinConstantsObject), not the tables. + if (module === "constants") return null; if (module !== null) return { module, member: access.name.text }; // The events module object IS the EventEmitter class, so the class's // defaultMaxListeners is the module property. diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 4c74e8b6a..72d6c09f8 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1801,6 +1801,22 @@ export const BUILTIN_MODULE_FNS: Record< valueParams: [{ mode: "required", type: STRING }], }, }, + // node:punycode (deprecated): punycode.js 2.1.0's conversions over + // strings (scr_url.c). ucs2.decode/encode are members of the module's + // ucs2 object, dispatched as module members (builtinMemberOf). The + // DEP0040 report rides the module's load sites (punycodeLoadWarning). + punycode: { + decode: { fn: "punycode.decode", params: [STRING], result: STRING }, + encode: { fn: "punycode.encode", params: [STRING], result: STRING }, + toASCII: { fn: "punycode.toASCII", params: [STRING], result: STRING }, + toUnicode: { fn: "punycode.toUnicode", params: [STRING], result: STRING }, + "ucs2.decode": { fn: "punycode.ucs2Decode", params: [STRING], result: arrayOf(F64) }, + "ucs2.encode": { fn: "punycode.ucs2Encode", params: [arrayOf(F64)], result: STRING }, + }, + // node:constants (deprecated) is one frozen object (Object.assign of + // os.constants' groups, fs.constants and crypto.constants): bindings of + // the module read it as a checked-dynamic value (builtinConstantsObject). + constants: {}, // node:readline: createInterface's options are entirely special-cased // (exactly { input: process.stdin, output?: process.stdout } — see // lowerBuiltinModuleCall); the entry carries the canonical shape. The @@ -1930,6 +1946,7 @@ export const BUILTIN_MODULE_CONSTS: Record< os: { EOL: "\n", devNull: "/dev/null" }, worker_threads: { isMainThread: true, threadId: 0 }, cluster: { isPrimary: true, isMaster: true, isWorker: false }, + punycode: { version: "2.1.0" }, // zlib's module-level copies of its constants (every member outside // BROTLI_*, Node's own loop over zlib.constants). zlib: Object.fromEntries( diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 2381413f8..ee5ba8c7c 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -466,6 +466,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "str.encodeUri", "qs.escape", "qs.stringify", + // node:punycode's RangeErrors (encode/toASCII only past 32-bit deltas) + // and String.fromCodePoint's for ucs2.encode. + "punycode.decode", + "punycode.encode", + "punycode.toASCII", + "punycode.toUnicode", + "punycode.ucs2Encode", // The base64 globals: atob/btoa throw the catchable DOMException // InvalidCharacterError on malformed input; the zero-argument form // always throws Node's TypeError [ERR_MISSING_ARGS]. diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 5e219ecf6..e9f7778bc 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -559,6 +559,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** os.constants: the per-thread null-prototype constants object (dlopen, * errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */ "os.constants": { argTypes: [], result: DYN }, + /** The deprecated node:constants object (scr_os.c): the os groups and + * fs.constants merged with the borrowed baked crypto.constants table, + * frozen, one per thread. */ + "constants.object": { argTypes: [DYN], result: DYN }, /** umask(2): arg < 0 reads without setting (umask has no read-only form * — set 0, restore); otherwise sets and answers the PREVIOUS mask. * Never throws. */ @@ -856,6 +860,19 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING }, "url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING }, "url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING }, + /** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions; + * decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the + * module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the + * module's first load (scr_async_dyn.c) and reports DEP0040 then unless + * the second argument is false: at once for a static ES import, on the + * next tick for a require() call. */ + "punycode.decode": { argTypes: [STRING], result: STRING }, + "punycode.encode": { argTypes: [STRING], result: STRING }, + "punycode.toASCII": { argTypes: [STRING], result: STRING }, + "punycode.toUnicode": { argTypes: [STRING], result: STRING }, + "punycode.ucs2Decode": { argTypes: [STRING], result: arrayOf(F64) }, + "punycode.ucs2Encode": { argTypes: [arrayOf(F64)], result: STRING }, + "punycode.warn": { argTypes: [BOOL, BOOL], result: VOID }, "url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 }, "url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T }, "url.fileURLToPathStr": { argTypes: [STRING], result: STRING }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 80a9c9b57..ba97670ce 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -43,6 +43,8 @@ export interface RuntimeFeatures { } const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ + // DEP0040 rides the dyn-async unit's warning machinery. + "punycode.warn", "fs.callbackValue", "fs.callbackCall", "fs.callbackGlob", diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 63efde378..3ee171669 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -466,6 +466,13 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.constants", + "kind": "node-builtin", + "name": "constants", + "status": "static", + "note": "recognized module (bare and node:-prefixed specifiers)" + }, { "id": "node-builtin.crypto", "kind": "node-builtin", @@ -2095,6 +2102,56 @@ "status": "static", "note": "shared version dictionary containing node and openssl; other components are absent unless defined by the program" }, + { + "id": "node-builtin.punycode", + "kind": "node-builtin", + "name": "punycode", + "status": "static", + "note": "recognized module (bare and node:-prefixed specifiers)" + }, + { + "id": "node-builtin.punycode.decode", + "kind": "node-builtin", + "name": "punycode.decode", + "status": "static" + }, + { + "id": "node-builtin.punycode.encode", + "kind": "node-builtin", + "name": "punycode.encode", + "status": "static" + }, + { + "id": "node-builtin.punycode.toASCII", + "kind": "node-builtin", + "name": "punycode.toASCII", + "status": "static" + }, + { + "id": "node-builtin.punycode.toUnicode", + "kind": "node-builtin", + "name": "punycode.toUnicode", + "status": "static" + }, + { + "id": "node-builtin.punycode.ucs2.decode", + "kind": "node-builtin", + "name": "punycode.ucs2.decode", + "status": "static" + }, + { + "id": "node-builtin.punycode.ucs2.encode", + "kind": "node-builtin", + "name": "punycode.ucs2.encode", + "status": "static" + }, + { + "id": "node-builtin.punycode.version", + "kind": "node-builtin", + "name": "punycode.version", + "status": "static", + "note": "constant value read" + }, { "id": "node-builtin.querystring", "kind": "node-builtin", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json new file mode 100644 index 000000000..b15e4fe13 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6122-punycode-conversions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json new file mode 100644 index 000000000..dfd4aa912 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6124-punycode-require-warnings.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json new file mode 100644 index 000000000..ef8c7f2bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6125-constants-module.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json new file mode 100644 index 000000000..5de83361a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/6126-punycode-dependency/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'puny-dep' module is not supported yet", + "loc": { + "file": "/tests/corpus/6126-punycode-dependency/main.cjs", + "start": 347, + "end": 376 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index a391d2a26..f41949f6e 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -212,6 +212,11 @@ void scr_process_on_warning(ScrDyn *fn); void scr_process_off_warning(ScrDyn *fn); void scr_process_emit_warning(ScrDyn *args); void scr_emit_warning(const char *name, const char *code, ScrStr *message); +/* node:punycode's first load (scr_async_dyn.c): later calls are module + * cache hits. The first reports DEP0040 when `report` (false for a require + * made from node_modules) — at once for a static ES import, which Node + * links before any module evaluates, otherwise on the next tick. */ +void scr_punycode_warn(bool immediate, bool report); /* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the * listener registration): units that cannot link the dyn-async unit route * their warnings through it, and print the default report otherwise. */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index a14ae6e6a..7a27f16f3 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -813,6 +813,27 @@ void scr_warning_emit_deferred(ScrDyn *w) { scr_warning_defer(w, scr_warning_tick); } +static SCR_TL bool scr_punycode_loaded = false; + +void scr_punycode_warn(bool immediate, bool report) { + if (scr_punycode_loaded) return; + scr_punycode_loaded = true; + if (!report) return; + static const char text[] = + "The `punycode` module is deprecated. Please use a userland alternative instead."; + ScrStr *message = scr_str_new(text, sizeof text - 1); + ScrStr *code = scr_str_new("DEP0040", 7); + ScrDyn *w = scr_warning_new(message, "DeprecationWarning", code); + if (immediate) { + if (!scr_process_deprecation_flag_on(0)) scr_warning_dispatch(w); + } else { + scr_warning_emit_deferred(w); + } + scr_dyn_release(w); + scr_str_release(code); + scr_str_release(message); +} + /* The console members' warnings. Node's emitWarning emits on * process.nextTick, so these queue in order and each rides one raw tick * on the shared tick queue; any left at exit are dropped with it. */ diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c index 0bc50fb3f..0df57d2e1 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -27,6 +27,8 @@ #include #include #endif +#include +#include #ifdef __linux__ #include #endif @@ -1286,3 +1288,195 @@ ScrDyn *scr_os_constants(void) { scr_atexit(scr_os_constants_clear); return scr_dyn_retain(os); } + +/* ── node:constants (deprecated, DEP0008) ────────────────────────────── + * Node's lib/constants.js: Object.assign({}, os.constants.dlopen, errno, + * priority, signals, fs.constants, crypto.constants), frozen, with an + * ordinary prototype. The fs group follows node_constants.cc's + * DefineFsConstants order and guards (libuv's UV_* values are fixed on + * POSIX); crypto_table (borrowed) is a compiler-emitted function that + * answers the baked crypto.constants table, called only on first use. One + * object per thread. */ +#define SCR_FS_CONSTANT(o, name) scr_os_constant((o), #name, (double)(name)) + +static void scr_fs_constants(ScrDyn *o) { + scr_os_constant(o, "UV_FS_SYMLINK_DIR", 1); + scr_os_constant(o, "UV_FS_SYMLINK_JUNCTION", 2); +#ifdef O_RDONLY + SCR_FS_CONSTANT(o, O_RDONLY); +#endif +#ifdef O_WRONLY + SCR_FS_CONSTANT(o, O_WRONLY); +#endif +#ifdef O_RDWR + SCR_FS_CONSTANT(o, O_RDWR); +#endif + scr_os_constant(o, "UV_DIRENT_UNKNOWN", 0); + scr_os_constant(o, "UV_DIRENT_FILE", 1); + scr_os_constant(o, "UV_DIRENT_DIR", 2); + scr_os_constant(o, "UV_DIRENT_LINK", 3); + scr_os_constant(o, "UV_DIRENT_FIFO", 4); + scr_os_constant(o, "UV_DIRENT_SOCKET", 5); + scr_os_constant(o, "UV_DIRENT_CHAR", 6); + scr_os_constant(o, "UV_DIRENT_BLOCK", 7); +#ifdef S_IFMT + SCR_FS_CONSTANT(o, S_IFMT); +#endif +#ifdef S_IFREG + SCR_FS_CONSTANT(o, S_IFREG); +#endif +#ifdef S_IFDIR + SCR_FS_CONSTANT(o, S_IFDIR); +#endif +#ifdef S_IFCHR + SCR_FS_CONSTANT(o, S_IFCHR); +#endif +#ifdef S_IFBLK + SCR_FS_CONSTANT(o, S_IFBLK); +#endif +#ifdef S_IFIFO + SCR_FS_CONSTANT(o, S_IFIFO); +#endif +#ifdef S_IFLNK + SCR_FS_CONSTANT(o, S_IFLNK); +#endif +#ifdef S_IFSOCK + SCR_FS_CONSTANT(o, S_IFSOCK); +#endif +#ifdef O_CREAT + SCR_FS_CONSTANT(o, O_CREAT); +#endif +#ifdef O_EXCL + SCR_FS_CONSTANT(o, O_EXCL); +#endif + /* libuv's Windows-only open flags are 0 everywhere else. */ + scr_os_constant(o, "UV_FS_O_FILEMAP", 0); + scr_os_constant(o, "UV_FS_O_TEMPORARY", 0); + scr_os_constant(o, "UV_FS_O_SHORT_LIVED", 0); + scr_os_constant(o, "UV_FS_O_SEQUENTIAL", 0); + scr_os_constant(o, "UV_FS_O_RANDOM", 0); +#ifdef O_NOCTTY + SCR_FS_CONSTANT(o, O_NOCTTY); +#endif +#ifdef O_TRUNC + SCR_FS_CONSTANT(o, O_TRUNC); +#endif +#ifdef O_APPEND + SCR_FS_CONSTANT(o, O_APPEND); +#endif +#ifdef O_DIRECTORY + SCR_FS_CONSTANT(o, O_DIRECTORY); +#endif +#ifdef O_NOATIME + SCR_FS_CONSTANT(o, O_NOATIME); +#endif +#ifdef O_NOFOLLOW + SCR_FS_CONSTANT(o, O_NOFOLLOW); +#endif +#ifdef O_SYNC + SCR_FS_CONSTANT(o, O_SYNC); +#endif +#ifdef O_DSYNC + SCR_FS_CONSTANT(o, O_DSYNC); +#endif +#ifdef O_SYMLINK + SCR_FS_CONSTANT(o, O_SYMLINK); +#endif +#ifdef O_DIRECT + SCR_FS_CONSTANT(o, O_DIRECT); +#endif +#ifdef O_NONBLOCK + SCR_FS_CONSTANT(o, O_NONBLOCK); +#endif +#ifdef S_IRWXU + SCR_FS_CONSTANT(o, S_IRWXU); +#endif +#ifdef S_IRUSR + SCR_FS_CONSTANT(o, S_IRUSR); +#endif +#ifdef S_IWUSR + SCR_FS_CONSTANT(o, S_IWUSR); +#endif +#ifdef S_IXUSR + SCR_FS_CONSTANT(o, S_IXUSR); +#endif +#ifdef S_IRWXG + SCR_FS_CONSTANT(o, S_IRWXG); +#endif +#ifdef S_IRGRP + SCR_FS_CONSTANT(o, S_IRGRP); +#endif +#ifdef S_IWGRP + SCR_FS_CONSTANT(o, S_IWGRP); +#endif +#ifdef S_IXGRP + SCR_FS_CONSTANT(o, S_IXGRP); +#endif +#ifdef S_IRWXO + SCR_FS_CONSTANT(o, S_IRWXO); +#endif +#ifdef S_IROTH + SCR_FS_CONSTANT(o, S_IROTH); +#endif +#ifdef S_IWOTH + SCR_FS_CONSTANT(o, S_IWOTH); +#endif +#ifdef S_IXOTH + SCR_FS_CONSTANT(o, S_IXOTH); +#endif +#ifdef F_OK + SCR_FS_CONSTANT(o, F_OK); +#endif +#ifdef R_OK + SCR_FS_CONSTANT(o, R_OK); +#endif +#ifdef W_OK + SCR_FS_CONSTANT(o, W_OK); +#endif +#ifdef X_OK + SCR_FS_CONSTANT(o, X_OK); +#endif + scr_os_constant(o, "UV_FS_COPYFILE_EXCL", 1); + scr_os_constant(o, "COPYFILE_EXCL", 1); + scr_os_constant(o, "UV_FS_COPYFILE_FICLONE", 2); + scr_os_constant(o, "COPYFILE_FICLONE", 2); + scr_os_constant(o, "UV_FS_COPYFILE_FICLONE_FORCE", 4); + scr_os_constant(o, "COPYFILE_FICLONE_FORCE", 4); +} +#undef SCR_FS_CONSTANT + +static SCR_TL ScrDyn *scr_node_constants_value; +static void scr_node_constants_clear(void) { + ScrDyn *value = scr_node_constants_value; + scr_node_constants_value = NULL; + scr_dyn_release(value); +} + +ScrDyn *scr_node_constants(const ScrDyn *crypto_table) { + if (scr_node_constants_value) return scr_dyn_retain(scr_node_constants_value); + ScrDyn *crypto = scr_dyn_call(crypto_table, NULL, 0, "constants"); + if (!crypto) return NULL; + ScrDyn *o = scr_dyn_new_obj(); + scr_os_dlopen_constants(o); + scr_os_errno_constants(o); + scr_os_constant(o, "PRIORITY_LOW", SCR_OS_PRIORITY_LOW); + scr_os_constant(o, "PRIORITY_BELOW_NORMAL", SCR_OS_PRIORITY_BELOW_NORMAL); + scr_os_constant(o, "PRIORITY_NORMAL", SCR_OS_PRIORITY_NORMAL); + scr_os_constant(o, "PRIORITY_ABOVE_NORMAL", SCR_OS_PRIORITY_ABOVE_NORMAL); + scr_os_constant(o, "PRIORITY_HIGH", SCR_OS_PRIORITY_HIGH); + scr_os_constant(o, "PRIORITY_HIGHEST", SCR_OS_PRIORITY_HIGHEST); + scr_os_signal_constants(o); + scr_fs_constants(o); + if (crypto && crypto->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < crypto->v.obj.len; i++) { + const ScrDynEntry *entry = &crypto->v.obj.entries[i]; + if (entry->accessor || !entry->value) continue; + scr_dyn_obj_set(o, entry->key, entry->key_len, scr_dyn_retain(entry->value)); + } + } + scr_dyn_release(crypto); + scr_dyn_release(scr_dyn_freeze(o)); + scr_node_constants_value = o; + scr_atexit(scr_node_constants_clear); + return scr_dyn_retain(o); +} diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index b40a8027a..6f8d9b616 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -34,6 +34,10 @@ ScrStr *scr_os_cpus_model(double i); double scr_os_cpus_field(double i, double field); /* os.constants: the per-thread null-prototype constants object (+1). */ ScrDyn *scr_os_constants(void); +/* The deprecated node:constants module object (scr_os.c): the os groups, + * fs.constants and the crypto table the borrowed function answers, merged + * and frozen (+1). */ +ScrDyn *scr_node_constants(const ScrDyn *crypto_table); double scr_os_available_parallelism(void); /* uv_available_parallelism: CPUs this process may use, >= 1 */ /* os.userInfo()'s field trio (pw_name / pw_shell / pw_dir — the passwd * home, not the $HOME cascade). +1 fresh; abort on lookup failure. */ diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c index b6fdb9cd6..42a7ef607 100644 --- a/packages/runtime/src/scr_url.c +++ b/packages/runtime/src/scr_url.c @@ -1988,3 +1988,342 @@ ScrBytes *scr_url_checked_to_path_buffer(const ScrDyn *value, const ScrDyn *opti scr_url_release(url); return result; } + +/* ── node:punycode (deprecated, DEP0040) ─────────────────────────────── + * A port of the punycode.js 2.1.0 that Node 24 bundles as lib/punycode.js. + * It works on UTF-16 code units: ucs2.decode pairs surrogates and keeps + * lone ones, which is exactly a code-point walk over WTF-8 storage. + * Errors throw the module's RangeErrors ("Overflow: ...", "Illegal input + * >= 0x80 ...", "Invalid input"); decoded code points past U+10FFFF throw + * String.fromCodePoint's "Invalid code point N". All inputs are borrowed; + * results are +1, or NULL with the error pending. The DEP0040 report is + * scr_punycode_warn (scr_async_dyn.c). */ + +enum { + PC_MAX_INT = 2147483647, + PC_BASE = 36, + PC_TMIN = 1, + PC_TMAX = 26, + PC_SKEW = 38, + PC_DAMP = 700, + PC_INITIAL_BIAS = 72, + PC_INITIAL_N = 128 +}; + +static void pc_error(const char *message) { + scr_throw_error_msg(SCR_ERR_RANGE, message, strlen(message)); +} +#define PC_OVERFLOW "Overflow: input needs wider integers to process" +#define PC_NOT_BASIC "Illegal input >= 0x80 (not a basic code point)" +#define PC_INVALID "Invalid input" + +typedef struct { + uint32_t *data; + size_t len, cap; +} PcPoints; + +static void pc_push(PcPoints *p, uint32_t cp) { + if (p->len == p->cap) { + size_t cap = p->cap ? p->cap * 2 : 16; + uint32_t *grown = realloc(p->data, cap * sizeof *grown); + if (!grown) scr_trap("scriptc: out of memory\n"); + p->data = grown; + p->cap = cap; + } + p->data[p->len++] = cp; +} + +/* ucs2decode over WTF-8 storage: a pair is one 4-byte sequence, a lone + * surrogate one 3-byte sequence carrying its own value. */ +static void pc_code_points(const char *s, size_t n, PcPoints *out) { + const unsigned char *b = (const unsigned char *)s; + for (size_t i = 0; i < n;) { + uint32_t c = b[i]; + if (c < 0x80) { + i += 1; + } else if (c < 0xE0 && i + 1 < n) { + c = ((c & 0x1F) << 6) | (b[i + 1] & 0x3F); + i += 2; + } else if (c < 0xF0 && i + 2 < n) { + c = ((c & 0x0F) << 12) | ((uint32_t)(b[i + 1] & 0x3F) << 6) | (b[i + 2] & 0x3F); + i += 3; + } else if (i + 3 < n) { + c = ((c & 0x07) << 18) | ((uint32_t)(b[i + 1] & 0x3F) << 12) | + ((uint32_t)(b[i + 2] & 0x3F) << 6) | (b[i + 3] & 0x3F); + i += 4; + } else { + i += 1; + } + pc_push(out, c); + } +} + +static int64_t pc_adapt(int64_t delta, int64_t points, bool first) { + int64_t k = 0; + delta = first ? delta / PC_DAMP : delta >> 1; + delta += delta / points; + for (; delta > ((PC_BASE - PC_TMIN) * PC_TMAX) >> 1; k += PC_BASE) delta /= PC_BASE - PC_TMIN; + return k + (PC_BASE - PC_TMIN + 1) * delta / (delta + PC_SKEW); +} + +static int64_t pc_basic_to_digit(unsigned char c) { + if (c >= 0x30 && c < 0x3A) return 26 + (c - 0x30); + if (c >= 0x41 && c < 0x5B) return c - 0x41; + if (c >= 0x61 && c < 0x7B) return c - 0x61; + return PC_BASE; +} + +static char pc_digit_to_basic(int64_t digit) { + return (char)(digit + 22 + 75 * (digit < 26)); +} + +/* String.fromCodePoint(...points). */ +static ScrStr *pc_from_points(const PcPoints *p) { + for (size_t i = 0; i < p->len; i++) { + if (p->data[i] > 0x10FFFF) { + char message[48]; + int size = snprintf(message, sizeof message, "Invalid code point %u", (unsigned)p->data[i]); + scr_throw_error_msg(SCR_ERR_RANGE, message, (size_t)size); + return NULL; + } + } + char *out = malloc(p->len * 4 + 1); + if (!out) scr_trap("scriptc: out of memory\n"); + size_t offset = 0; + for (size_t i = 0; i < p->len; i++) { + uint32_t point = p->data[i]; + if (point >= 0xD800 && point <= 0xDBFF && i + 1 < p->len && p->data[i + 1] >= 0xDC00 && + p->data[i + 1] <= 0xDFFF) { + point = 0x10000 + ((point - 0xD800) << 10) + (p->data[i + 1] - 0xDC00); + i++; + } + offset += scr_wtf8_put(out + offset, point); + } + ScrStr *result = scr_str_new(out, offset); + free(out); + return result; +} + +/* punycode.decode over the bytes of one label. Any non-ASCII byte fails + * where its first code unit would: before the last delimiter it is not + * basic, after it it is no digit — so byte positions stand in for code + * unit positions until the first error. */ +static ScrStr *pc_decode(const char *s, size_t n) { + PcPoints output = {0}; + size_t basic = 0; + for (size_t k = n; k > 0; k--) { + if (s[k - 1] == '-') { + basic = k - 1; + break; + } + } + ScrStr *result = NULL; + for (size_t j = 0; j < basic; j++) { + if ((unsigned char)s[j] >= 0x80) { + pc_error(PC_NOT_BASIC); + goto done; + } + pc_push(&output, (unsigned char)s[j]); + } + int64_t i = 0, cp = PC_INITIAL_N, bias = PC_INITIAL_BIAS; + for (size_t index = basic > 0 ? basic + 1 : 0; index < n;) { + int64_t old = i; + for (int64_t w = 1, k = PC_BASE;; k += PC_BASE) { + if (index >= n) { + pc_error(PC_INVALID); + goto done; + } + int64_t digit = pc_basic_to_digit((unsigned char)s[index++]); + if (digit >= PC_BASE) { + pc_error(PC_INVALID); + goto done; + } + if (digit > (PC_MAX_INT - i) / w) { + pc_error(PC_OVERFLOW); + goto done; + } + i += digit * w; + int64_t t = k <= bias ? PC_TMIN : (k >= bias + PC_TMAX ? PC_TMAX : k - bias); + if (digit < t) break; + if (w > PC_MAX_INT / (PC_BASE - t)) { + pc_error(PC_OVERFLOW); + goto done; + } + w *= PC_BASE - t; + } + int64_t out = (int64_t)output.len + 1; + bias = pc_adapt(i - old, out, old == 0); + if (i / out > PC_MAX_INT - cp) { + pc_error(PC_OVERFLOW); + goto done; + } + cp += i / out; + i %= out; + pc_push(&output, 0); + memmove(output.data + i + 1, output.data + i, (output.len - 1 - (size_t)i) * sizeof *output.data); + output.data[i++] = (uint32_t)cp; + } + result = pc_from_points(&output); +done: + free(output.data); + return result; +} + +/* punycode.encode of one label's code points, appended to buf. */ +static bool pc_encode(const PcPoints *input, ScrJsonBuf *buf) { + int64_t cp = PC_INITIAL_N, delta = 0, bias = PC_INITIAL_BIAS; + size_t basic_length = 0; + for (size_t k = 0; k < input->len; k++) { + if (input->data[k] < 0x80) { + scr_jb_putc(buf, (char)input->data[k]); + basic_length++; + } + } + size_t handled = basic_length; + if (basic_length) scr_jb_putc(buf, '-'); + while (handled < input->len) { + int64_t m = PC_MAX_INT; + for (size_t k = 0; k < input->len; k++) + if (input->data[k] >= cp && input->data[k] < m) m = input->data[k]; + int64_t handled_plus_one = (int64_t)handled + 1; + if (m - cp > (PC_MAX_INT - delta) / handled_plus_one) { + pc_error(PC_OVERFLOW); + return false; + } + delta += (m - cp) * handled_plus_one; + cp = m; + for (size_t k = 0; k < input->len; k++) { + int64_t current = input->data[k]; + if (current < cp && ++delta > PC_MAX_INT) { + pc_error(PC_OVERFLOW); + return false; + } + if (current == cp) { + int64_t q = delta; + for (int64_t j = PC_BASE;; j += PC_BASE) { + int64_t t = j <= bias ? PC_TMIN : (j >= bias + PC_TMAX ? PC_TMAX : j - bias); + if (q < t) break; + scr_jb_putc(buf, pc_digit_to_basic(t + (q - t) % (PC_BASE - t))); + q = (q - t) / (PC_BASE - t); + } + scr_jb_putc(buf, pc_digit_to_basic(q)); + bias = pc_adapt(delta, handled_plus_one, handled == basic_length); + delta = 0; + handled++; + } + } + delta++; + cp++; + } + return true; +} + +ScrStr *scr_punycode_decode(const ScrStr *input) { return pc_decode(input->data, input->len); } + +ScrStr *scr_punycode_encode(const ScrStr *input) { + PcPoints points = {0}; + pc_code_points(input->data, input->len, &points); + ScrJsonBuf buf; + scr_jb_init(&buf); + bool ok = pc_encode(&points, &buf); + free(points.data); + ScrStr *result = scr_jb_finish(&buf); + if (ok) return result; + scr_str_release(result); + return NULL; +} + +ScrArr *scr_punycode_ucs2_decode(const ScrStr *input) { + PcPoints points = {0}; + pc_code_points(input->data, input->len, &points); + ScrArr *out = scr_arr_new(SCR_ELEM_F64, points.len); + for (size_t i = 0; i < points.len; i++) scr_arr_push_f64(out, (double)points.data[i]); + free(points.data); + return out; +} + +/* mapDomain: only the part after the first '@' maps (and only up to a + * second one — punycode.js keeps parts[0] and parts[1]); the RFC 3490 + * separators U+002E, U+3002, U+FF0E and U+FF61 all split labels, and the + * labels rejoin with '.'. */ +static ScrStr *pc_map_domain(const ScrStr *input, bool to_ascii) { + const char *s = input->data; + size_t n = input->len; + ScrJsonBuf buf; + scr_jb_init(&buf); + const char *at = memchr(s, '@', n); + if (at) { + scr_jb_write(&buf, s, (size_t)(at - s) + 1); + const char *rest = at + 1; + const char *second = memchr(rest, '@', n - (size_t)(rest - s)); + n = (second ? (size_t)(second - rest) : n - (size_t)(rest - s)); + s = rest; + } + size_t start = 0; + bool ok = true; + for (size_t i = 0; ok && i <= n;) { + size_t sep = 0; + if (i == n) sep = 1; + else if (s[i] == '.') sep = 1; + else if (i + 2 < n && (unsigned char)s[i] == 0xE3 && (unsigned char)s[i + 1] == 0x80 && + (unsigned char)s[i + 2] == 0x82) + sep = 3; + else if (i + 2 < n && (unsigned char)s[i] == 0xEF && + (((unsigned char)s[i + 1] == 0xBC && (unsigned char)s[i + 2] == 0x8E) || + ((unsigned char)s[i + 1] == 0xBD && (unsigned char)s[i + 2] == 0xA1))) + sep = 3; + if (!sep) { + i++; + continue; + } + const char *label = s + start; + size_t len = i - start; + bool ascii = true; + for (size_t k = 0; k < len; k++) ascii = ascii && (unsigned char)label[k] < 0x80; + if (to_ascii && !ascii) { + PcPoints points = {0}; + pc_code_points(label, len, &points); + scr_jb_puts(&buf, "xn--"); + ok = pc_encode(&points, &buf); + free(points.data); + } else if (!to_ascii && len >= 4 && memcmp(label, "xn--", 4) == 0) { + /* string.slice(4).toLowerCase(): ASCII letters fold; the one other + * code point whose lowercase is ASCII is KELVIN SIGN (U+212A → k). + * Any other non-ASCII stays non-ASCII and fails in decode either + * way, at the same position. */ + ScrJsonBuf lower; + scr_jb_init(&lower); + for (size_t k = 4; k < len; k++) { + unsigned char c = (unsigned char)label[k]; + if (c == 0xE2 && k + 2 < len && (unsigned char)label[k + 1] == 0x84 && + (unsigned char)label[k + 2] == 0xAA) { + scr_jb_putc(&lower, 'k'); + k += 2; + } else { + scr_jb_putc(&lower, (char)(c >= 'A' && c <= 'Z' ? c + 32 : c)); + } + } + ScrStr *folded = scr_jb_finish(&lower); + ScrStr *decoded = pc_decode(folded->data, folded->len); + scr_str_release(folded); + if (decoded) { + scr_jb_put_str(&buf, decoded); + scr_str_release(decoded); + } else { + ok = false; + } + } else { + scr_jb_write(&buf, label, len); + } + if (ok && i < n) scr_jb_putc(&buf, '.'); + i += sep; + start = i; + } + ScrStr *result = scr_jb_finish(&buf); + if (ok) return result; + scr_str_release(result); + return NULL; +} + +ScrStr *scr_punycode_to_ascii(const ScrStr *input) { return pc_map_domain(input, true); } +ScrStr *scr_punycode_to_unicode(const ScrStr *input) { return pc_map_domain(input, false); } diff --git a/packages/runtime/src/scr_url.h b/packages/runtime/src/scr_url.h index 134893054..05e5702bb 100644 --- a/packages/runtime/src/scr_url.h +++ b/packages/runtime/src/scr_url.h @@ -53,6 +53,15 @@ ScrDyn *scr_url_legacy_parse(const ScrDyn *url, const ScrDyn *parse_query, const ScrStr *scr_url_legacy_format(const ScrDyn *object, const ScrDyn *options, bool warn); ScrStr *scr_url_legacy_resolve(const ScrDyn *from, const ScrDyn *to, bool warn); ScrStr *scr_url_domain_to_ascii(const ScrDyn *domain, bool missing); +/* node:punycode (scr_url.c): punycode.js 2.1.0's encode/decode, toASCII/ + * toUnicode (mapDomain over RFC 3490 separators) and ucs2.decode (UTF-16 + * code points as an f64 array). Borrowed input; +1, or NULL with the + * module's RangeError pending. */ +ScrStr *scr_punycode_decode(const ScrStr *input); +ScrStr *scr_punycode_encode(const ScrStr *input); +ScrStr *scr_punycode_to_ascii(const ScrStr *input); +ScrStr *scr_punycode_to_unicode(const ScrStr *input); +ScrArr *scr_punycode_ucs2_decode(const ScrStr *input); ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing); ScrStr *scr_url_to_path(ScrUrl *u); /* +1, or throws */ ScrStr *scr_url_str_to_path(ScrStr *s); /* +1, or throws */ diff --git a/tests/corpus/6122-punycode-conversions.ts b/tests/corpus/6122-punycode-conversions.ts new file mode 100644 index 000000000..061987a78 --- /dev/null +++ b/tests/corpus/6122-punycode-conversions.ts @@ -0,0 +1,45 @@ +// @exit: 1 +// @no-node-shims +// node:punycode's conversions (punycode.js 2.1.0): encode/decode of bare +// labels, toASCII/toUnicode over domains and email addresses (only the part +// after '@' maps; every RFC 3490 separator splits labels), ucs2 code point +// arrays with astral pairs and lone surrogates, and the RangeErrors of +// malformed input. A static import links the module before anything runs, +// so its DEP0040 report precedes the program and no 'warning' listener the +// program registers sees it (the Node side runs without the preload shim, +// which would delay that report past the program's first turn). The report +// carries the process id, so the @exit directive keeps stderr out of the +// comparison. +import * as punycode from "node:punycode"; + +process.on("warning", (w) => console.log("listener", w.name)); + +const show = (label: string, f: () => unknown): void => { + try { + console.log(label, JSON.stringify(f())); + } catch (e) { + const err = e as Error; + console.log(label, "threw", err instanceof RangeError, err.name, err.message); + } +}; + +console.log(punycode.version); +for (const s of ["mañana", "bücher", "ü", "abc", "", "-> $1.00 <-", "ليهمابتكلموشعربي؟", "他们为什么不说中文", "😀x", "MaJiでKoiする5秒前"]) { + show(`encode ${s}`, () => punycode.encode(s)); + show(`roundtrip ${s}`, () => punycode.decode(punycode.encode(s))); +} +for (const s of ["maana-pta", "bcher-kva", "tda", "abc-", "-abc", "ABC-kva", "a-b-c-kva", "ü-kva", "kva-ü", "99999999999", "zzzzzzzzzzzzz", "a-", "-"]) { + show(`decode ${s}`, () => punycode.decode(s)); +} +for (const d of ["mañana.com", "bücher.de", "example.com", "", "a..b", "ex@mple@bücher.de", "user@bücher.de", "日本。jp", "a.b。c", "ÄÖÜ.test", "a.b."]) { + show(`toASCII ${d}`, () => punycode.toASCII(d)); +} +for (const d of ["xn--maana-pta.com", "xn--bcher-kva.de", "XN--bcher-kva.de", "xn--BCHER-KVA.de", "example.com", "user@xn--bcher-kva.de", "xn--.x", "xn--ü.x", "xn--zzzzzzzzzzzzz.x", "xn--a-Kva.x"]) { + show(`toUnicode ${d}`, () => punycode.toUnicode(d)); +} +show("ucs2.decode", () => punycode.ucs2.decode("aé😀\ud800b\udc00")); +show("ucs2.encode", () => punycode.ucs2.encode([97, 0xe9, 0x1f600, 0xd800, 0xdc00, 98])); +show("ucs2.encode join", () => punycode.ucs2.encode([0xd83d, 0xde00]).length); +show("ucs2.encode range", () => punycode.ucs2.encode([0x110000])); +show("ucs2 roundtrip", () => punycode.ucs2.encode(punycode.ucs2.decode("x😀\udfffy")) === "x😀\udfffy"); +process.exitCode = 1; diff --git a/tests/corpus/6124-punycode-require-warnings.cjs b/tests/corpus/6124-punycode-require-warnings.cjs new file mode 100644 index 000000000..c3255e6b7 --- /dev/null +++ b/tests/corpus/6124-punycode-require-warnings.cjs @@ -0,0 +1,32 @@ +// DEP0040 for require("punycode"): reported once, on the next tick after +// the requiring turn, to the stderr report and the 'warning' listeners; +// require("node:punycode") is the same module and does not report again. +// process.noDeprecation drops the report and the listener call. The program +// re-runs itself per case and prints each child's output with process ids +// normalized. +"use strict"; +const { spawnSync } = require("child_process"); + +const mode = process.argv[2]; +if (mode === "direct") { + process.on("warning", (w) => console.log("listener", w.name, w.code, w.message)); + process.nextTick(() => console.log("earlier tick")); + console.log("before require"); + const direct = require("punycode"); + console.log("after require", direct.toASCII("bücher.de")); + const prefixed = require("node:punycode"); + console.log("prefixed", prefixed.toUnicode("xn--bcher-kva.de")); + setTimeout(() => console.log("timer"), 0); +} else if (mode === "silenced") { + process.on("warning", (w) => console.log("listener", w.code)); + process.noDeprecation = true; + const silencedModule = require("punycode"); + console.log("silenced", silencedModule.encode("ü")); +} else { + for (const child of ["direct", "silenced"]) { + const run = spawnSync(process.execPath, [process.argv[1], child], { encoding: "utf8" }); + console.log(`== ${child}: status ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); + } +} diff --git a/tests/corpus/6125-constants-module.cjs b/tests/corpus/6125-constants-module.cjs new file mode 100644 index 000000000..d7a547ac5 --- /dev/null +++ b/tests/corpus/6125-constants-module.cjs @@ -0,0 +1,26 @@ +// node:constants (deprecated in documentation only, so no warning): one +// frozen object merging os.constants' dlopen, errno, priority and signals +// groups with fs.constants and crypto.constants, in that order. Members +// read like the owning modules' constants, and a constant the platform +// lacks is absent (graceful-fs probes O_SYMLINK with hasOwnProperty). +const constants = require("constants"); +const nodeConstants = require("node:constants"); +const os = require("os"); + +console.log(typeof constants, Object.isFrozen(constants), constants === nodeConstants); +console.log(constants.O_RDONLY, constants.O_WRONLY, constants.O_RDWR); +console.log(constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC); +console.log(constants.S_IFMT, constants.S_IFREG, constants.S_IFDIR, constants.S_IRWXU); +console.log(constants.F_OK, constants.R_OK, constants.W_OK, constants.X_OK); +console.log(constants.COPYFILE_EXCL, constants.UV_FS_COPYFILE_FICLONE, constants.UV_DIRENT_DIR); +console.log(constants.SIGTERM === os.constants.signals.SIGTERM, constants.SIGINT); +console.log(constants.ENOENT === os.constants.errno.ENOENT, constants.EACCES); +console.log(constants.PRIORITY_HIGH, constants.PRIORITY_LOW, constants.RTLD_LAZY); +console.log(constants.TLS1_2_VERSION, constants.RSA_PKCS1_PADDING, typeof constants.defaultCoreCipherList); +console.log(constants.hasOwnProperty("O_SYMLINK") === ("O_SYMLINK" in constants)); +console.log(constants.hasOwnProperty("O_RDWR"), "SIGKILL" in constants, "UV_UDP_REUSEADDR" in constants); +console.log(constants.hasOwnProperty("NOT_A_CONSTANT"), "dlopen" in constants, "errno" in constants); +const keys = Object.keys(constants); +console.log(keys.indexOf("RTLD_LAZY") < keys.indexOf("E2BIG"), keys.indexOf("E2BIG") < keys.indexOf("PRIORITY_LOW")); +console.log(keys.indexOf("PRIORITY_LOW") < keys.indexOf("SIGHUP"), keys.indexOf("SIGHUP") < keys.indexOf("O_RDONLY")); +console.log(keys.indexOf("O_RDONLY") < keys.indexOf("OPENSSL_VERSION_NUMBER"), keys.includes("POINT_CONVERSION_HYBRID")); diff --git a/tests/corpus/6126-punycode-dependency/main.cjs b/tests/corpus/6126-punycode-dependency/main.cjs new file mode 100644 index 000000000..530503a71 --- /dev/null +++ b/tests/corpus/6126-punycode-dependency/main.cjs @@ -0,0 +1,12 @@ +// A dependency under node_modules that requires node:punycode: Node's +// isInsideNodeModules() rule skips DEP0040 for it, so nothing reaches stderr +// or the 'warning' listeners, and the program's own later require of the +// cached module does not report either. +"use strict"; +process.on("warning", (w) => console.log("listener", w.code)); +const toAscii = require("puny-dep"); +const punycode = require("punycode"); + +console.log(toAscii("mañana.com"), toAscii("example.org")); +console.log(punycode.toUnicode("xn--maana-pta.com")); +setTimeout(() => console.log("timer"), 0); diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts new file mode 100644 index 000000000..89b712210 --- /dev/null +++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts @@ -0,0 +1,2 @@ +declare function toAscii(domain: string): string; +export = toAscii; diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js new file mode 100644 index 000000000..417c44889 --- /dev/null +++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js @@ -0,0 +1,8 @@ +// A dependency requiring node:punycode: Node's isInsideNodeModules() rule +// skips DEP0040 for it. +"use strict"; +const punycode = require("punycode"); + +module.exports = function toAscii(domain) { + return punycode.toASCII(domain); +}; diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json new file mode 100644 index 000000000..71edeb25c --- /dev/null +++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json @@ -0,0 +1,5 @@ +{ + "name": "puny-dep", + "version": "1.0.0", + "main": "index.js" +} From fd6145f6270bb4852eff7ce34a01b46860f3729a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:17:24 -0700 Subject: [PATCH 012/120] Test the dispose hook only in programs that can install it Every emitted free loaded the runtime's dispose hook and branched on it, and every runtime array, map and byte-store free made an out-of-line call to scr_weak_dispose to do the same. The hook has two clients, weak collections and the kept sources of checked records, and a program that has neither never installs it. The emitted frees now test two module constants the emitter defines after every helper is emitted: whether the program constructs a WeakMap or WeakSet, and whether some helper can keep a record source (a checked record builder that notes one, or a JSON schema of a closed record). Class instances test the first, closed record layouts both; LLVM folds the test away, so a program without either frees with no hook load at all. The runtime's hot frees test the hook inline instead of calling. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/alloc.test.ts | 31 +++++++++- packages/compiler/src/backend/llvm/alloc.ts | 58 ++++++++++++++++--- packages/compiler/src/backend/llvm/dyn.ts | 6 ++ packages/compiler/src/backend/llvm/emitter.ts | 27 ++++++++- packages/compiler/src/backend/llvm/shapes.ts | 7 ++- packages/runtime/src/scr_array.c | 2 +- packages/runtime/src/scr_blob.c | 2 +- packages/runtime/src/scr_bytes.c | 2 +- packages/runtime/src/scr_events.c | 2 +- packages/runtime/src/scr_fetch.c | 4 +- packages/runtime/src/scr_map.c | 4 +- packages/runtime/src/scr_rc.h | 9 ++- .../corpus/6162-dispose-observer-weak-keys.ts | 41 +++++++++++++ 13 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 tests/corpus/6162-dispose-observer-weak-keys.ts diff --git a/packages/compiler/src/backend/llvm/alloc.test.ts b/packages/compiler/src/backend/llvm/alloc.test.ts index 5ba7ff3d1..980c7ab7a 100644 --- a/packages/compiler/src/backend/llvm/alloc.test.ts +++ b/packages/compiler/src/backend/llvm/alloc.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vitest"; -import { type AllocHost, emitObjectAlloc, emitObjectFree, inlineAllocSupported } from "./alloc.js"; +import { + type AllocHost, + emitObjectAlloc, + emitObjectFree, + inlineAllocSupported, +} from "./alloc.js"; function host(inlineAlloc: boolean): AllocHost & { decls: Set; oom: boolean } { const h = { @@ -63,7 +68,12 @@ describe("emitted object allocation", () => { ]); expect(emitObjectFree(h, false)).toEqual([ " call void @scr_obj_free_note()", + " %sf.obs = load i1, ptr @sc_weak_keys", + " br i1 %sf.obs, label %sf.observe, label %sf.block", + "sf.observe:", " call void @scr_weak_dispose(ptr %o)", + " br label %sf.block", + "sf.block:", " call void @scr_rt_free(ptr %o)", ]); expect([...h.decls].some((d) => d.includes("@scr_sa"))).toBe(false); @@ -167,4 +177,23 @@ describe("emitted object allocation", () => { expect(lines.at(-1)).toBe("sf.done:"); } }); + + test("frees test the dispose hook only where the program can observe them", () => { + const h = host(true); + const weak = emitObjectFree(h, false); + expectSingleDefinitions(weak); + expect(weak.slice(0, 2)).toEqual([ + " %sf.obs = load i1, ptr @sc_weak_keys", + " br i1 %sf.obs, label %sf.hooked, label %sf.block", + ]); + const records = emitObjectFree(h, true, "weak-or-source"); + expectSingleDefinitions(records); + expect(records.slice(0, 4)).toEqual([ + " %sf.obsw = load i1, ptr @sc_weak_keys", + " %sf.obss = load i1, ptr @sc_record_sources", + " %sf.obs = or i1 %sf.obsw, %sf.obss", + " br i1 %sf.obs, label %sf.hooked, label %sf.block", + ]); + expect(records).toContain(" call void %sf.hook(ptr %o)"); + }); }); diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index 7dfc385aa..ceec8a54d 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -325,20 +325,56 @@ function allocLines( return lines; } +/* ── dispose observers ──────────────────────────────────────────────── + * The runtime's dispose hook (scr_weak_dispose_hook) has two clients: weak + * collections, whose keys can be any object, and the kept sources of + * checked records (scr_record_source_note and the JSON schema parser), + * keyed by closed records. A program without either can never install the + * hook, so its frees need not test it. Whether the program has them is + * known only once every helper is emitted (dynCheck builders and JSON + * schemas are requested lazily), so the emitted frees and record writers + * test these two module constants, which the emitter defines last + * (LlvmEmitter.emitParts) and LLVM folds away. */ + +/** True when the program constructs a WeakMap or WeakSet. */ +export const WEAK_KEYS_GATE = "@sc_weak_keys"; +/** True when a checked conversion can keep a record's undeclared source. */ +export const RECORD_SOURCES_GATE = "@sc_record_sources"; + +/** Which dispose observers an object type can have: weak-collection keys + * only, or (closed record layouts) kept record sources too. */ +export type FreeObservers = "weak" | "weak-or-source"; + /** * Lines that free the object `%o` after its members were released: the - * RC-audit free note, the weak-collection dispose hook, and the block — - * `scr_cyc_free` for collector-headered objects, `scr_rt_free` otherwise. - * Labels use the `sf.` prefix; the lines end in an open block. + * RC-audit free note, the dispose hook (when the program can observe this + * type's disposals), and the block — `scr_cyc_free` for collector-headered + * objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines + * end in an open block. */ -export function emitObjectFree(host: AllocHost, cyc: boolean): string[] { - const lines = threadLocalState(host, "sf", freeLines(host, cyc)); +export function emitObjectFree( + host: AllocHost, + cyc: boolean, + observers: FreeObservers = "weak", +): string[] { + const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers)); if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE); host.declare(`declare void @scr_obj_free_note()`); return lines; } -function freeLines(host: AllocHost, cyc: boolean): string[] { +/** Lines that leave in `%.obs` whether the program can observe a + * disposal of this type (folds to a constant). */ +function observedLines(prefix: string, observers: FreeObservers): string[] { + if (observers === "weak") return [` %${prefix}.obs = load i1, ptr ${WEAK_KEYS_GATE}`]; + return [ + ` %${prefix}.obsw = load i1, ptr ${WEAK_KEYS_GATE}`, + ` %${prefix}.obss = load i1, ptr ${RECORD_SOURCES_GATE}`, + ` %${prefix}.obs = or i1 %${prefix}.obsw, %${prefix}.obss`, + ]; +} + +function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] { if (host.inlineAlloc !== true) { if (cyc) { host.declare(`declare void @scr_cyc_free(ptr)`); @@ -348,7 +384,12 @@ function freeLines(host: AllocHost, cyc: boolean): string[] { host.declare(`declare void @scr_weak_dispose(ptr)`); return [ ` call void @scr_obj_free_note()`, + ...observedLines("sf", observers), + ` br i1 %sf.obs, label %sf.observe, label %sf.block`, + `sf.observe:`, ` call void @scr_weak_dispose(ptr %o)`, + ` br label %sf.block`, + `sf.block:`, ` call void @scr_rt_free(ptr %o)`, ]; } @@ -356,11 +397,14 @@ function freeLines(host: AllocHost, cyc: boolean): string[] { host.declare(`declare void @scr_rt_free(ptr)`); declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`); const lines = [ + ...observedLines("sf", observers), + ` br i1 %sf.obs, label %sf.hooked, label %sf.block`, + `sf.hooked:`, ` %sf.hook = load ptr, ptr @scr_weak_dispose_hook`, ` %sf.has = icmp ne ptr %sf.hook, null`, ` %sf.hasx = call i1 @llvm.expect.i1(i1 %sf.has, i1 false)`, ` br i1 %sf.hasx, label %sf.observe, label %sf.block`, - `sf.observe: ; weak collections observe the disposal`, + `sf.observe: ; a weak collection or a kept record source observes the disposal`, ` call void %sf.hook(ptr %o)`, ` br label %sf.block`, `sf.block:`, diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb35e70c2..2e8c0968f 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1106,6 +1106,10 @@ export class LlDyn { private readonly jsonSchemas = new Map(); private readonly jsonParsers = new Map(); private readonly recordNameTables = new Map(); + /** Some emitted helper can keep a record's undeclared source: a checked + * record builder that notes it, or a JSON schema of a closed record + * (alloc.ts RECORD_SOURCES_GATE). */ + keepsRecordSources = false; /** Targets the one-pass parser (scr_json_parse_schema) can build * directly: number, boolean and string leaves inside arrays and records @@ -1220,6 +1224,7 @@ export class LlDyn { const index = new Map(shape.fields.map((f, i) => [f.name, i + 1])); // The runtime names kept-source keys by this index (record-sources.ts). const order = recordSourceFieldOrder(shape); + if (recordMayKeepSource(shape)) this.keepsRecordSources = true; const fieldTy = `{ ptr, ${S}, ${S}, ptr }`; const entries = order.map((fieldName) => { const field = shape.fields[index.get(fieldName)! - 1]!; @@ -1978,6 +1983,7 @@ export class LlDyn { // that has them stays beside the record so its reflective // surfaces still observe them (scr_record_source_note). if (recordMayKeepSource(shape)) { + this.keepsRecordSources = true; const names = this.recordNameTable(t.shapeId, shape); host.declare( `declare void @scr_record_source_note(ptr, ptr, ptr, ptr, ${host.sizeType})`, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2cccda19c..c6ae04917 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -2,7 +2,14 @@ import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js" import { emitStringInputs } from "./string-lifetimes.js"; import { emitLlvmLayouts } from "./layouts.js"; import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js"; -import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js"; +import { + CYC_BUF, + CYC_HDR, + FIRST_EMITTED_CYC_KIND, + inlineAllocSupported, + RECORD_SOURCES_GATE, + WEAK_KEYS_GATE, +} from "./alloc.js"; import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; import { InternalCompilerError } from "../../errors.js"; import { createHash } from "node:crypto"; @@ -1899,7 +1906,10 @@ export class LlEmitter { } emitParts(): string[] { - const parts = limitInlineDynReleases(this.emitModuleParts()); + const parts = [ + ...limitInlineDynReleases(this.emitModuleParts()), + ...this.disposeObserverGates(), + ]; if (!this.executableTls) return parts; // A worker executable's thread-locals all live in the executable's own // TLS block: the program's internal ones, and the runtime's external @@ -1922,6 +1932,19 @@ export class LlEmitter { ); } + /** The dispose-observer gates the emitted frees and record writers test + * (alloc.ts): defined after every helper is emitted, because dynCheck + * builders, JSON schemas and weak collection constructors are requested + * lazily. */ + private disposeObserverGates(): string[] { + const weak = [...this.decls].some((d) => /@scr_weak_(?:map|set)_new\(/.test(d)); + return [ + `${WEAK_KEYS_GATE} = internal constant i1 ${weak}`, + `${RECORD_SOURCES_GATE} = internal constant i1 ${this.dyn.keepsRecordSources}`, + ``, + ]; + } + private emitModuleParts(): string[] { // Function bodies first (the literal/unit/fn-value tables fill as they // emit), then the file assembles around them — the runtime ABI’s order. diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 23565eff4..5f916dc6a 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -38,6 +38,7 @@ import { stackUnionBox, type InlineUnionFields, } from "./inline-unions.js"; +import { recordMayKeepSource } from "./record-sources.js"; import { LlvmUnsupportedError } from "./unsupported.js"; /** What the tables need from the emitter: the extern-declaration ledger @@ -1404,7 +1405,9 @@ export function emitRecordShapes( } t++; } - freeBody.push(...emitObjectFree(host, traced)); + // Closed layouts can carry a kept record source (record-sources.ts). + const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak"; + freeBody.push(...emitObjectFree(host, traced, observers)); defs.push( ...releaseBody( host, @@ -1514,7 +1517,7 @@ export function emitRecordShapes( ` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)} (acyclic)`, ); }); - gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``); + gf.push(...emitObjectFree(host, true, observers), ` ret void`, `}`, ``); defs.push(...gf); } } diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index ef64be56d..4ef24e906 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -1179,7 +1179,7 @@ static void scr_arr_destroy(void *object) { #ifdef SCR_RC_AUDIT scr_live_arrays--; #endif - scr_weak_dispose(a); + scr_weak_dispose_inline(a); scr_mem_free(a); } } diff --git a/packages/runtime/src/scr_blob.c b/packages/runtime/src/scr_blob.c index 5c4f9ba68..817a85e2d 100644 --- a/packages/runtime/src/scr_blob.c +++ b/packages/runtime/src/scr_blob.c @@ -44,7 +44,7 @@ static void *scr_blob_retain_v(void *h) { static void scr_blob_release_v(void *h) { ScrBlob *b = h; if (--b->rc) return; - scr_weak_dispose(b); + scr_weak_dispose_inline(b); scr_blob_store_release(b->store); free(b->parts); scr_str_release(b->type); diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 28258a29c..d33f32a97 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -296,7 +296,7 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) { void scr_bytes_release(ScrBytes *b) { if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--b->rc == 0) { - scr_weak_dispose(b); + scr_weak_dispose_inline(b); if (b->backing) { scr_bytes_release(b->backing); /* a view: data points into the owner */ } else if (b->shared) { diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 670ea10d6..54eb87a0d 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -1061,7 +1061,7 @@ static void scr_stdio_cleanup(void) { scr_stdio_write_hook = NULL; for (int i = 0; i < 3; i++) { ScrStdio *stream = &scr_stdio_streams[i]; - scr_weak_dispose(stream); + scr_weak_dispose_inline(stream); scr_dyn_release(stream->write); stream->write = NULL; scr_dyn_release(stream->write_error); diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index f586b96f8..eff2ae6bc 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -2522,7 +2522,7 @@ static SfResponse *sf_response_retain(SfResponse *r) { static void sf_response_release(SfResponse *r) { if (!r || --r->rc > 0) return; - scr_weak_dispose(r); + scr_weak_dispose_inline(r); sf_stream_release(r->body); sf_headers_release(r->headers); scr_str_release(r->url); @@ -4760,7 +4760,7 @@ static void *sf_request_retain_v(void *ptr) { static void sf_request_release_v(void *ptr) { SfRequest *r = ptr; if (--r->rc) return; - scr_weak_dispose(r); + scr_weak_dispose_inline(r); sf_response_release(r->payload); scr_str_release(r->url); scr_str_release(r->method); scr_str_release(r->redirect); sf_signal_release(r->signal); free(r); diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 8e34533ad..a0af45b39 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -696,7 +696,7 @@ static void scr_map_trace(void *o, ScrTraceVisit visit, void *ctx) { static void scr_map_gcfree(void *o) { ScrMap *m = (ScrMap *)o; bool keys = m->traced & SCR_MAP_TRACED_KEY, vals = m->traced & SCR_MAP_TRACED_VAL; - scr_weak_dispose(m); + scr_weak_dispose_inline(m); for (size_t e = 0; e < m->nentries; e++) { if (!m->entries[e].hash) continue; if (!keys) scr_map_release_key(m, m->entries[e].key); @@ -751,7 +751,7 @@ ScrMap *scr_map_retain(ScrMap *m) { static void scr_map_destroy(void *object) { ScrMap *m = object; - scr_weak_dispose(m); + scr_weak_dispose_inline(m); for (size_t e = 0; e < m->nentries; e++) { if (!m->entries[e].hash) continue; scr_map_release_key(m, m->entries[e].key); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index cdc03c966..4d6cc68b5 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -420,8 +420,15 @@ static inline void *scr_cyc_alloc_inline(size_t size, ScrCycType *type) { scr_cyc_live++; return h + 1; } -static inline void scr_cyc_free_inline(void *obj) { +/* scr_weak_dispose's NULL test inline, for the runtime's hot frees: the + * hook stays NULL unless the program creates a weak collection or keeps a + * checked record's source, so the free costs one predictable branch, not a + * call. */ +static inline void scr_weak_dispose_inline(void *obj) { if (scr_weak_dispose_hook) scr_weak_dispose_hook(obj); +} +static inline void scr_cyc_free_inline(void *obj) { + scr_weak_dispose_inline(obj); scr_cyc_live--; if (scr_cyc_gen(scr_cyc_hdr(obj)) == SCR_CYC_OLD) scr_cyc_old_freed++; scr_mem_free(scr_cyc_hdr(obj)); diff --git a/tests/corpus/6162-dispose-observer-weak-keys.ts b/tests/corpus/6162-dispose-observer-weak-keys.ts new file mode 100644 index 000000000..f9c5738fa --- /dev/null +++ b/tests/corpus/6162-dispose-observer-weak-keys.ts @@ -0,0 +1,41 @@ +// With a weak collection in the program, every object type's free tells +// the collection to forget the key: objects built later at a freed key's +// address (the allocator reuses blocks of the same size) are not members. +class Node { + id: number; + constructor(id: number) { + this.id = id; + } +} +interface Tag { + label: string; +} + +const seenNodes = new WeakSet(); +const seenTags = new WeakSet(); +const seenArrays = new WeakMap(); +for (let i = 0; i < 64; i++) { + seenNodes.add(new Node(i)); + seenTags.add({ label: "t" + i }); + seenArrays.set([i], "a" + i); +} +let hits = 0; +for (let i = 0; i < 64; i++) { + if (seenNodes.has(new Node(i))) hits++; + if (seenTags.has({ label: "t" + i })) hits++; + if (seenArrays.has([i])) hits++; +} +console.log("stale hits", hits); + +const kept = new Node(1); +const tag: Tag = { label: "kept" }; +seenNodes.add(kept); +seenTags.add(tag); +console.log(seenNodes.has(kept), seenTags.has(tag)); +seenNodes.delete(kept); +console.log(seenNodes.has(kept), seenTags.has(tag)); + +// Checked records with undeclared keys in the same program. +const parsed = JSON.parse('{"label":"x","more":1}') as Tag; +seenTags.add(parsed); +console.log(JSON.stringify(parsed), seenTags.has(parsed)); From 06e987b8484e7bb947a052e721779b64a3329a92 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:17:30 -0700 Subject: [PATCH 013/120] Skip the record-source lookup in typed JSON.stringify unless one was kept Every closed record written by a typed JSON.stringify called scr_jb_try_record_source first, an out-of-line call and table probe for records that almost never keep a source (2% of json-records samples). The record writer now tests the record-sources constant first, so a program without a checked conversion that can keep a source emits no lookup. Where one can, the writer tests the dispose hook inline before calling: keeping a source installs the hook, so a NULL hook means the table is empty, and only a program that has kept a source pays the call. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/alloc.test.ts | 8 ++++ packages/compiler/src/backend/llvm/alloc.ts | 11 +++++ packages/compiler/src/backend/llvm/walkers.ts | 19 ++++++++ tests/corpus/6161-dispose-observer-gates.ts | 47 +++++++++++++++++++ tests/corpus/6163-dispose-observer-workers.ts | 43 +++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 tests/corpus/6161-dispose-observer-gates.ts create mode 100644 tests/corpus/6163-dispose-observer-workers.ts diff --git a/packages/compiler/src/backend/llvm/alloc.test.ts b/packages/compiler/src/backend/llvm/alloc.test.ts index 980c7ab7a..49971c3ce 100644 --- a/packages/compiler/src/backend/llvm/alloc.test.ts +++ b/packages/compiler/src/backend/llvm/alloc.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "vitest"; import { type AllocHost, + disposeHookLoadLines, emitObjectAlloc, emitObjectFree, inlineAllocSupported, @@ -196,4 +197,11 @@ describe("emitted object allocation", () => { ]); expect(records).toContain(" call void %sf.hook(ptr %o)"); }); + + test("loads the dispose hook from emitted code only with the inline allocator", () => { + expect(disposeHookLoadLines(host(false), "%t1")).toBeNull(); + expect(disposeHookLoadLines(host(true), "%t1")).toEqual([ + " %t1 = load ptr, ptr @scr_weak_dispose_hook", + ]); + }); }); diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index ceec8a54d..7fdc4b683 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -79,6 +79,17 @@ function threadLocalState(host: AllocHost, prefix: string, lines: string[]): str return [...preamble, ...text.split("\n")]; } +/** Lines that load the runtime's dispose hook into `dest`, or null on + * targets without the inline allocator, where the hook's storage is not + * addressed from emitted code. The hook is NULL until a weak collection + * or a kept record source installs it, and then never NULL again. */ +export function disposeHookLoadLines(host: AllocHost, dest: string): string[] | null { + if (host.inlineAlloc !== true) return null; + declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`); + const prefix = dest.replace(/^%/, "").replace(/[^-$._A-Za-z0-9]/g, "_"); + return threadLocalState(host, prefix, [` ${dest} = load ptr, ptr @scr_weak_dispose_hook`]); +} + /** Worker executables: the calling script thread's ScrThreadHot block into * `dest`. Its address also identifies the thread (the owner word of a * lifetime-region chunk; llvm/regions.ts). */ diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 9b8d80b87..6ab7205e9 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -28,6 +28,7 @@ import { traceAdapter, type ShapeHost, } from "./shapes.js"; +import { disposeHookLoadLines, RECORD_SOURCES_GATE } from "./alloc.js"; import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { emitUnionPeek, emitUnionTag } from "./union-repr.js"; @@ -396,6 +397,24 @@ export class LlWalkers { const writer = this.recordFieldWriter(shapeId); const lSource = B.newLabel("jw.source"); const lNative = B.newLabel("jw.native"); + // Only a program that can keep record sources looks one up (the gate + // folds to a constant), and only once one was kept: keeping a source + // installs the dispose hook, so a NULL hook means the table is empty. + const lHooked = B.newLabel("jw.hooked"); + const kept = B.tmp(); + B.line(`${kept} = load i1, ptr ${RECORD_SOURCES_GATE}`); + B.condBr(kept, lHooked, lNative); + B.startBlock(lHooked); + const hook = B.tmp(); + const hookLines = disposeHookLoadLines(host, hook); + if (hookLines !== null) { + const lProbe = B.newLabel("jw.probe"); + for (const line of hookLines) B.line(line.trimStart()); + const installed = B.tmp(); + B.line(`${installed} = icmp ne ptr ${hook}, null`); + B.condBr(installed, lProbe, lNative); + B.startBlock(lProbe); + } if (traceAdapter(host, { kind: "record", shapeId }) === null) { // One lookup: the runtime writes the record when it kept a source. host.declare(`declare zeroext i1 @scr_jb_try_record_source(ptr, ptr, ptr)`); diff --git a/tests/corpus/6161-dispose-observer-gates.ts b/tests/corpus/6161-dispose-observer-gates.ts new file mode 100644 index 000000000..aa53c9c85 --- /dev/null +++ b/tests/corpus/6161-dispose-observer-gates.ts @@ -0,0 +1,47 @@ +// A program without weak collections still keeps the undeclared keys of +// checked records, and forgets them when the record is freed: a record +// built later at the same address (the allocator reuses freed blocks of +// its size) serializes only its own fields. Class instances and records +// of shapes that never keep a source free without the dispose hook. +interface Row { + id: number; + name: string; +} +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +function parsedRows(round: number): Row[] { + const text = + `[{"id":${round},"extra":"kept-${round}","name":"a"},` + + `{"name":"b","id":${round + 1}},` + + `{"id":${round + 2},"name":"c","tags":[${round}]}]`; + return JSON.parse(text) as Row[]; +} + +let checksum = 0; +for (let round = 0; round < 3; round++) { + const rows = parsedRows(round); + console.log(JSON.stringify(rows)); + // Drop the parsed rows, then build fresh rows of the same shape. + rows.length = 0; + const fresh: Row[] = []; + for (let i = 0; i < 64; i++) fresh.push({ id: i, name: "n" + i }); + const text = JSON.stringify(fresh); + checksum += text.length; + console.log(text.includes("extra"), text.includes("tags"), Object.keys(fresh[0]!).join(",")); +} +console.log(checksum); + +let sum = 0; +for (let i = 0; i < 10000; i++) { + const p = new Point(i, -i); + const r = { a: p.x, b: [p.y] }; + sum += r.a + r.b[0]!; +} +console.log(sum); diff --git a/tests/corpus/6163-dispose-observer-workers.ts b/tests/corpus/6163-dispose-observer-workers.ts new file mode 100644 index 000000000..aa5781752 --- /dev/null +++ b/tests/corpus/6163-dispose-observer-workers.ts @@ -0,0 +1,43 @@ +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +// Each script thread has its own dispose hook: a worker that keeps checked +// record sources and weak keys serializes and forgets them on its own, +// while the main thread runs the same code without sharing its state. +interface Row { + id: number; + name: string; +} +class Key { + id: number; + constructor(id: number) { + this.id = id; + } +} + +function run(who: string, base: number): string[] { + const out: string[] = []; + const text = `[{"id":${base},"extra":"${who}","name":"a"},{"name":"b","id":${base + 1}}]`; + const rows = JSON.parse(text) as Row[]; + out.push(JSON.stringify(rows)); + rows.length = 0; + const fresh: Row[] = []; + for (let i = 0; i < 32; i++) fresh.push({ id: base + i, name: who }); + out.push(String(JSON.stringify(fresh).includes("extra"))); + const seen = new WeakSet(); + for (let i = 0; i < 32; i++) seen.add(new Key(i)); + let stale = 0; + for (let i = 0; i < 32; i++) if (seen.has(new Key(i))) stale++; + out.push(`${who} stale ${stale}`); + return out; +} + +if (isMainThread) { + for (const line of run("main", 0)) console.log(line); + const worker = new Worker(new URL(import.meta.url)); + worker.on("message", (lines: string[]) => { + for (const line of lines) console.log(line); + }); + worker.on("exit", (code) => console.log("worker exit", code)); +} else { + parentPort!.postMessage(run("worker", 100)); +} From 4b23d34efe1007a4ee7132e33d45e5a02f9932ba Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:17:31 -0700 Subject: [PATCH 014/120] Format toString(radix) with a number radix without a dyn box n.toString(radix) passed the radix as a dyn value, so every call boxed a number and converted it back, and kept scr_json.c's dyn machinery linked into programs that use nothing else of it. A number-typed radix now calls scr_num_to_string_radix_f64, which truncates it as ToIntegerOrInfinity does and holds the formatting that moved from scr_json.c to scr_lib.c. Other radix types keep the generic entry point, which converts and then calls the same function. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-calls.ts | 24 +++++++- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/runtime/src/scr_json.c | 56 +----------------- packages/runtime/src/scr_lib.c | 58 +++++++++++++++++++ packages/runtime/src/scr_number.h | 1 + tests/corpus/6160-number-radix-typed.ts | 36 ++++++++++++ 8 files changed, 126 insertions(+), 54 deletions(-) create mode 100644 tests/corpus/6160-number-radix-typed.ts diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index f69c96e6e..a72a5f4b6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -144,6 +144,7 @@ export const LIB_FN_SYMS: Record = { "num.toFixed0": "scr_num_to_fixed0", "num.toFixed": "scr_num_to_fixed", "num.toStringRadix": "scr_num_to_string_radix", + "num.toStringRadixF64": "scr_num_to_string_radix_f64", "num.sameValue": "scr_num_same_value", "intl.numFormatEnUs": "scr_intl_num_format_en_us", "number.isFinite": "scr_num_is_finite", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b3094d5c7..f3b344a44 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4105,10 +4105,32 @@ function lowerNumberToStringCall( } if (call.arguments.length === 1) { if (operand.type.kind !== "f64") return null; + const arg = call.arguments[0]!; + // A number radix needs no box: ToIntegerOrInfinity of a number is a + // truncation, which the typed entry point applies itself. + if (!ts.isSpreadElement(arg) && lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "f64") { + const radix = lowerer.lowerExpr(arg); + if (radix.type.kind === "f64") { + return { + kind: "libCall", + fn: "num.toStringRadixF64", + args: [operand, radix], + type: STRING, + loc: locOf(call), + }; + } + return { + kind: "libCall", + fn: "num.toStringRadix", + args: [operand, lowerer.coerceToExpected(radix, DYN)], + type: STRING, + loc: locOf(call), + }; + } return { kind: "libCall", fn: "num.toStringRadix", - args: [operand, lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [operand, lowerer.lowerExprExpecting(arg, DYN)], type: STRING, loc: locOf(call), }; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 1bb9cbd14..2e844fc26 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -137,6 +137,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fetch.streamFrom", "num.toFixed", "num.toStringRadix", + "num.toStringRadixF64", "insp.jsonDyn", // diagnostics_channel: publish runs subscribers synchronously (a throw // propagates — the documented divergence from triggerUncaughtException); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index ca2749591..9cdad7366 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -751,6 +751,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "num.toFixed0": { argTypes: [F64], result: STRING }, "num.toFixed": { argTypes: [F64, F64], result: STRING }, "num.toStringRadix": { argTypes: [F64, DYN], result: STRING }, + /** toString(radix) with a number radix (scr_lib.c): truncates it as + * ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */ + "num.toStringRadixF64": { argTypes: [F64, F64], result: STRING }, /** Object.is over two numbers — the spec's SameValue on doubles: NaN * equals NaN, +0 differs from -0, everything else is `===`. Plain bool * result; never throws. (Union-armed operands take unionEq's sameValue diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b23e78241..9a3f315d2 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -13045,63 +13045,13 @@ double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *encoding) { return 0; } -/* Radix digit generation follows V8's DoubleToRadixStringView algorithm. - * Copyright 2012 the V8 project authors. BSD license in - * ../vendor/v8-number-radix/LICENSE. The decimal path retains Ryū. */ +/* toString(radix) with any radix value: undefined means 10, anything else + * converts with ToNumber (which can run program code) first. */ ScrStr *scr_num_to_string_radix(double value, const ScrDyn *argument) { double requested = 10; if (argument && argument->kind != SCR_DYN_UNDEF && !scr_dyn_number_coerce_js(argument, &requested)) return NULL; - requested = isnan(requested) ? 0 : trunc(requested); - if (requested < 2 || requested > 36 || !isfinite(requested)) { - static const char message[] = "toString() radix argument must be between 2 and 36"; - scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1); - return NULL; - } - int radix = (int)requested; - if (radix == 10 || !isfinite(value) || value == 0) return scr_f64_to_scrstr(value); - static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; - char buffer[2200]; - const size_t middle = sizeof buffer / 2; - size_t begin = middle, end = middle; - bool negative = value < 0; - value = fabs(value); - double integer = floor(value), fraction = value - integer; - double tolerance = 0.5 * (nextafter(value, INFINITY) - value); - if (tolerance <= 0) tolerance = nextafter(0, INFINITY); - if (fraction >= tolerance) { - buffer[end++] = '.'; - do { - fraction *= radix; - tolerance *= radix; - int digit = (int)fraction; - buffer[end++] = digits[digit]; - fraction -= digit; - if ((fraction > 0.5 || (fraction == 0.5 && (digit & 1))) && fraction + tolerance > 1) { - for (;;) { - end--; - if (end == middle) { integer += 1; break; } - char last = buffer[end]; - digit = last > '9' ? last - 'a' + 10 : last - '0'; - if (digit + 1 < radix) { buffer[end++] = digits[digit + 1]; break; } - } - break; - } - } while (fraction >= tolerance); - } - /* V8's Double::Exponent is the unbiased exponent minus 52. Beyond - * that precision boundary, trailing digits are unrepresented zeros. */ - while (integer / radix >= 0x1p53) { - integer /= radix; - buffer[--begin] = '0'; - } - do { - double remainder = fmod(integer, radix); - buffer[--begin] = digits[(int)remainder]; - integer = (integer - remainder) / radix; - } while (integer > 0); - if (negative) buffer[--begin] = '-'; - return scr_str_new(buffer + begin, end - begin); + return scr_num_to_string_radix_f64(value, requested); } ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 9307d9b01..c7ef8632b 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10660,6 +10660,64 @@ ScrStr *scr_num_to_fixed0(double x) { * the receiver's non-finite arm, as ECMA-262 requires. Invalid precision * raises V8's catchable RangeError text; otherwise the result is +1. The * exact-binary-value digits come from scr_f64_to_fixed (scr_number.c). */ +/* Number.prototype.toString(radix) with a number radix, which + * ToIntegerOrInfinity truncates (NaN to 0). Radix digit generation follows + * V8's DoubleToRadixStringView algorithm. Copyright 2012 the V8 project + * authors. BSD license in ../vendor/v8-number-radix/LICENSE. The decimal + * path retains Ryū. */ +ScrStr *scr_num_to_string_radix_f64(double value, double requested) { + requested = isnan(requested) ? 0 : trunc(requested); + if (requested < 2 || requested > 36 || !isfinite(requested)) { + static const char message[] = "toString() radix argument must be between 2 and 36"; + scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1); + return NULL; + } + int radix = (int)requested; + if (radix == 10 || !isfinite(value) || value == 0) return scr_f64_to_scrstr(value); + static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; + char buffer[2200]; + const size_t middle = sizeof buffer / 2; + size_t begin = middle, end = middle; + bool negative = value < 0; + value = fabs(value); + double integer = floor(value), fraction = value - integer; + double tolerance = 0.5 * (nextafter(value, INFINITY) - value); + if (tolerance <= 0) tolerance = nextafter(0, INFINITY); + if (fraction >= tolerance) { + buffer[end++] = '.'; + do { + fraction *= radix; + tolerance *= radix; + int digit = (int)fraction; + buffer[end++] = digits[digit]; + fraction -= digit; + if ((fraction > 0.5 || (fraction == 0.5 && (digit & 1))) && fraction + tolerance > 1) { + for (;;) { + end--; + if (end == middle) { integer += 1; break; } + char last = buffer[end]; + digit = last > '9' ? last - 'a' + 10 : last - '0'; + if (digit + 1 < radix) { buffer[end++] = digits[digit + 1]; break; } + } + break; + } + } while (fraction >= tolerance); + } + /* V8's Double::Exponent is the unbiased exponent minus 52. Beyond + * that precision boundary, trailing digits are unrepresented zeros. */ + while (integer / radix >= 0x1p53) { + integer /= radix; + buffer[--begin] = '0'; + } + do { + double remainder = fmod(integer, radix); + buffer[--begin] = digits[(int)remainder]; + integer = (integer - remainder) / radix; + } while (integer > 0); + if (negative) buffer[--begin] = '-'; + return scr_str_new(buffer + begin, end - begin); +} + ScrStr *scr_num_to_fixed(double x, double fraction_digits) { double fd = isnan(fraction_digits) ? 0 : trunc(fraction_digits); if (!(fd >= 0 && fd <= 100)) { diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 84db27075..f980daeb5 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -73,6 +73,7 @@ bool scr_num_is_safe_integer(double x); ScrStr *scr_num_to_exponential(double x); ScrStr *scr_num_to_fixed0(double x); ScrStr *scr_num_to_fixed(double x, double fraction_digits); +ScrStr *scr_num_to_string_radix_f64(double value, double radix); /* Object.is over two numbers — the spec's SameValue on doubles: NaN * equals NaN, +0 differs from -0, everything else is ==. Never throws. */ diff --git a/tests/corpus/6160-number-radix-typed.ts b/tests/corpus/6160-number-radix-typed.ts new file mode 100644 index 000000000..4e11e06b2 --- /dev/null +++ b/tests/corpus/6160-number-radix-typed.ts @@ -0,0 +1,36 @@ +// Number.prototype.toString(radix) with a number-typed radix takes the +// typed entry point: ToIntegerOrInfinity truncates the radix (NaN to 0), +// out-of-range radixes throw the same RangeError, and the argument is +// evaluated once, before formatting. An optional radix keeps the generic +// conversion, where undefined means 10. +const radixes = [2, 2.9, 7, 10, 16, 16.5, 35.99, 36]; +const values = [0, -0, 1, -1, 255, -255.5, 0.1, 1 / 3, 2 ** 53, -(2 ** 60), 1e21, 123.456, NaN, Infinity, -Infinity]; +for (const radix of radixes) { + const row: string[] = []; + for (const value of values) row.push(value.toString(radix)); + console.log(radix, row.join(" ")); +} + +let calls = 0; +function nextRadix(): number { + calls++; + return 8 + calls; +} +console.log((1234).toString(nextRadix()), (1234).toString(nextRadix()), calls); + +for (const bad of [-0, 0.5, 1, 1.99, 36.5, 37, -2, NaN, Infinity, -Infinity]) { + try { + console.log((42).toString(bad)); + } catch (error) { + console.log(String(bad), error instanceof RangeError, (error as Error).message); + } +} + +function maybeRadix(i: number): number | undefined { + return i % 2 === 0 ? undefined : 2 + i; +} +for (let i = 0; i < 4; i++) console.log((255).toString(maybeRadix(i))); + +let total = 0; +for (let i = 0; i < 2000; i++) total += (i * 7919).toString(2 + (i % 35)).length; +console.log(total); From 56e472a225ebe8888c2e4d802a29d57705d69176 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 11:04:03 -0500 Subject: [PATCH 015/120] Return undefined from generic functions that read a missing element A generic function, arrow or method whose body returns an array element or an index-signature entry now returns undefined for a missing one, as in Node, instead of throwing. Only declarations whose bodies return such a read change their instance result type. Fixes #964 Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com> --- .../src/frontend/lowering/function-returns.ts | 7 ++ .../frontend/lowering/generic-functions.ts | 9 +- .../compiler/src/frontend/lowering/lowerer.ts | 92 +++++++++++++++++++ .../5954-generic-optional-returns.ts.json | 6 ++ tests/corpus/5954-generic-optional-returns.ts | 51 ++++++++++ 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json create mode 100644 tests/corpus/5954-generic-optional-returns.ts diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index d3c5eb1ba..4f519691f 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -157,6 +157,13 @@ export function declaredReturnType( } if (mappedReturn && isJsSourceFile(decl.getSourceFile()) && functionCanReturnUndefined(decl)) mappedReturn = lowerer.withUndefinedArmOf(mappedReturn) ?? mappedReturn; + // A generic-class method body that can return an indexed read that + // misses returns undefined there, whatever the declared result says. + if (mappedReturn && lowerer.runtimeOptionalGenericReturns.has(decl)) + mappedReturn = + mappedReturn.kind === "promise" + ? { kind: "promise", inner: lowerer.runtimeOptionalType(mappedReturn.inner) } + : lowerer.runtimeOptionalType(mappedReturn); if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "void") { let returnsValue = false; const scan = (node: ts.Node): void => { diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index bd785a256..90f83ec2c 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -292,11 +292,16 @@ export function genericCallInstance( (_param, i) => expr.arguments[i] ?? expr, ); const retTs = lowerer.checker.getReturnTypeOfSignature(rsig); - const returnType = lowerer.mapTypeOf(retTs); - if (!returnType) { + const mappedReturn = lowerer.mapTypeOf(retTs); + if (!mappedReturn) { fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); } + // A body that can return an indexed read that misses returns undefined + // there, as in JavaScript, whatever the declared result says. + const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl) + ? lowerer.runtimeOptionalType(mappedReturn) + : mappedReturn; // keyof-constrained type parameters (`K extends keyof T`): the bound // LITERAL is semantic — the instance body reads the named field — so diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b77768c28..40cf40de9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1880,6 +1880,10 @@ export class Lowerer { * declared optional binding to that read's bare type, so presence tests * on these bindings consult the stored tag instead. */ readonly runtimeOptionalAssignedSymbols = new Set(); + /** Generic function declarations whose body can return an indexed read + * that misses. + * Each instance's result admits undefined (generic-functions.ts). */ + readonly runtimeOptionalGenericReturns = new Set(); /** Concise callback/lambda returns promoted by the HOF callback prepass; * arrows have no declaration symbol to key in fnSigsBySymbol. */ readonly runtimeOptionalFunctionReturns = new Map(); @@ -4289,6 +4293,79 @@ export class Lowerer { const read = this.mapTypeOf(this.typeOf(e)); return !(read?.kind === "union" && this.armTag(read.unionId, UNDEFINED_T) >= 0); }; + /** An optional value in a generic body, where element types are type + * parameters with no IR mapping yet: an element read of an array or a + * read of a string index signature, judged on checker types. */ + const genericClassMethods: ts.MethodDeclaration[] = []; + for (const sf of sourceFiles) { + if (isJsSourceFile(sf) || sf.isDeclarationFile) continue; + ts.walkPreorder(sf, (node) => { + if (!ts.isClassDeclaration(node)) return undefined; + const genericClass = (node.typeParameters?.length ?? 0) > 0; + for (const member of node.members) + if ( + ts.isMethodDeclaration(member) && + member.body && + (genericClass || (member.typeParameters?.length ?? 0) > 0) + ) + genericClassMethods.push(member); + return undefined; + }); + } + const genericMayBeOptional = (node: ts.Expression): boolean => { + if (explicitlyNonNull(node)) return false; + if (mayBeOptional(node)) return true; + const e = peel(node); + if (ts.isConditionalExpression(e)) + return genericMayBeOptional(e.whenTrue) || genericMayBeOptional(e.whenFalse); + if (ts.isBinaryExpression(e)) { + const op = e.operatorToken.kind; + if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) + return genericMayBeOptional(e.right); + if (op === ts.SyntaxKind.AmpersandAmpersandToken) + return genericMayBeOptional(e.left) || genericMayBeOptional(e.right); + return false; + } + if (ts.isIdentifier(e)) { + // A binding initialized from such a read and never reassigned. + const symbol = symbolOf(e); + const decl = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + return ( + symbol !== null && + !reassigned.has(symbol) && + decl !== undefined && + ts.isVariableDeclaration(decl) && + decl.initializer !== undefined && + ts.isIdentifier(decl.name) && + !provenPresent(e, symbol) && + genericIndexedRead(decl.initializer) + ); + } + return genericIndexedRead(e); + }; + const genericIndexedRead = (node: ts.Expression): boolean => { + const e = peel(node); + if (!ts.isElementAccessExpression(e) || explicitlyNonNull(node)) return false; + const read = typeOrNull(e); + if (!read || (read.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any)) !== 0) return false; + if ( + read.isUnionType() && + ts.constituentTypes(read).some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0) + ) + return false; + const receiver = typeOrNull(e.expression); + if (!receiver || this.checker.isTupleType(receiver)) return false; + if (this.checker.isArrayType(receiver)) return true; + const key = e.argumentExpression; + if ( + (ts.isStringLiteral(key) || ts.isNumericLiteral(key)) && + this.checker.getPropertyOfType(receiver, key.text) !== undefined + ) + return false; + return this.checker + .getIndexInfosOfType(receiver) + .some((info) => (info.keyType.flags & ts.TypeFlags.StringLike) !== 0); + }; const isDynamicObjectEntryRead = (node: ts.Expression): boolean => { const read = peel(node); if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false; @@ -5815,6 +5892,21 @@ export class Lowerer { for (const sf of sourceFiles) if (scanFile(sf)) changed = true; for (const [symbol, decl] of functionDeclBySymbol) if (scanReturns(symbol, decl)) changed = true; + const noteGenericReturn = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): void => { + if (optionalReturns.has(symbol) || isJsSourceFile(decl.getSourceFile())) return; + if (!returnsOf(decl).some(genericMayBeOptional)) return; + optionalReturns.add(symbol); + this.runtimeOptionalGenericReturns.add(decl); + provenance?.note("return", decl.name ?? decl, decl); + changed = true; + }; + for (const [symbol, info] of this.genericFnsBySymbol) noteGenericReturn(symbol, info.decl); + // Generic methods and methods of a generic class collect with their + // class, after this analysis; their result admits undefined there. + for (const method of genericClassMethods) { + const symbol = method.name ? symbolOf(method.name) : null; + if (symbol) noteGenericReturn(symbol, method); + } for (const fn of tsLambdas) { // An annotated return is the author's contract, as for parameters, // except that a var read before its first store is undefined diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json new file mode 100644 index 000000000..a57275c12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5954-generic-optional-returns.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5954-generic-optional-returns.ts b/tests/corpus/5954-generic-optional-returns.ts new file mode 100644 index 000000000..9aaec5889 --- /dev/null +++ b/tests/corpus/5954-generic-optional-returns.ts @@ -0,0 +1,51 @@ +// A generic function, method or arrow that returns an element read or an +// index-signature read answers undefined for a missing element or key, as +// in Node, for every instantiation. +export {}; + +function at(items: T[], index: number): T { + return items[index]; +} +function lookup(table: Record, key: string): T { + return table[key]; +} +function viaBinding(items: T[], index: number): T { + const item = items[index]; + return item; +} +function either(items: T[], index: number, fallback: T): T { + return index >= 0 ? items[index] : fallback; +} +const first = (items: T[]): T => items[0]; + +console.log(at([1, 2], 1), at([1, 2], 5), at(["a"], 0), at(["a"], 3)); +console.log(lookup({ a: 1 }, "a"), lookup({ a: 1 }, "b"), lookup({ s: "x" }, "t")); +console.log(viaBinding([true], 0), viaBinding([true], 2)); +console.log(either([1], 4, 9), either([1], -1, 9)); +console.log(first([7]), first([]), first(["only"])); + +const missing = at([10, 20], 7); +console.log(missing === undefined, typeof missing); +const label = lookup({ a: "A" }, "z"); +console.log(label ?? "none", label === undefined); + +class Store { + items: Record; + constructor(items: Record) { + this.items = items; + } + get(key: string): T { + return this.items[key]; + } + pick(keys: K[], index: number): K { + return keys[index]; + } +} +const store = new Store({ a: 1 }); +console.log(store.get("a"), store.get("b"), store.pick(["x", "y"], 1), store.pick(["x"], 4)); + +const pairs: [string, number][] = [["a", 1]]; +function second(rows: T[][], row: number, column: number): T { + return rows[row][column]; +} +console.log(second([[1, 2]], 0, 1), second([[1, 2]], 0, 9), second(pairs, 0, 1)); From 1b8b44dd333ebc12baf020377b55512730f4fdc8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:48:56 -0700 Subject: [PATCH 016/120] Release the replacement buffer when a replace callback throws scr_regex_replace_callback freed its output buffer with free() when the replacer threw after an earlier match had been written, but the buffer is the body of a runtime string, so the process aborted with "free(): invalid pointer" instead of propagating the exception. It now abandons the buffer through scr_jb_abandon, which releases the string. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_json.c | 2 ++ packages/runtime/src/scr_json.h | 2 ++ packages/runtime/src/scr_regex.c | 2 +- .../6165-regex-replace-callback-throws.ts | 26 +++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/6165-regex-replace-callback-throws.ts diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 9a3f315d2..b96a19757 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -212,6 +212,8 @@ static void scr_jb_dispose(ScrJsonBuf *b) { scr_jb_init(b); } +void scr_jb_abandon(ScrJsonBuf *b) { scr_jb_dispose(b); } + void scr_jb_putc(ScrJsonBuf *b, char c) { scr_jb_grow(b, 1); b->data[b->len++] = c; diff --git a/packages/runtime/src/scr_json.h b/packages/runtime/src/scr_json.h index 8a353f40a..e3077703e 100644 --- a/packages/runtime/src/scr_json.h +++ b/packages/runtime/src/scr_json.h @@ -119,6 +119,8 @@ _Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == s "LLVM backend expects ScrJsonBuf to start with { data, len, cap }"); void scr_jb_init(ScrJsonBuf *b); +/* Release a buffer's text without finishing it (error paths). */ +void scr_jb_abandon(ScrJsonBuf *b); /* Append a borrowed byte span verbatim, including embedded NUL bytes. */ void scr_jb_write(ScrJsonBuf *b, const char *s, size_t n); /* scr_jb_write for text that holds no lone surrogate (fixed text, JSON diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index b24261a4d..4a6b88e1e 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -1358,7 +1358,7 @@ ScrStr *scr_regex_replace_callback(ScrStr *s, ScrRegex *re, ScrDyn *callback, bo for (size_t i = 0; i < count; i++) scr_dyn_release(matches[i].arguments); free(matches); if (scr_exc_pending()) { - free(output.data); + scr_jb_abandon(&output); scr_regex_input_dispose(&input); return NULL; } diff --git a/tests/corpus/6165-regex-replace-callback-throws.ts b/tests/corpus/6165-regex-replace-callback-throws.ts new file mode 100644 index 000000000..57cb8af18 --- /dev/null +++ b/tests/corpus/6165-regex-replace-callback-throws.ts @@ -0,0 +1,26 @@ +// A replace callback that throws after earlier matches were replaced: the +// partial result is discarded and the exception propagates, with the +// callbacks that ran counted exactly as in Node. +let calls = 0; +function replacer(m: string, digit?: string): string { + calls++; + if (digit === "3") throw new RangeError("stop at " + m); + return "<" + m + ">"; +} +for (const subject of ["a1b2c3d4", "x3", "no digits"]) { + calls = 0; + try { + console.log(subject.replace(/(\d)|z/g, replacer), calls); + } catch (error) { + console.log(error instanceof RangeError, (error as Error).message, calls); + } +} +const parts: string[] = []; +for (let i = 0; i < 50; i++) { + try { + parts.push(("k" + i + "-3").replace(/(\d)/g, replacer)); + } catch { + parts.push("thrown"); + } +} +console.log(parts.filter((p) => p === "thrown").length, calls); From dfacb791a1b1cf81fb05508c9feb4817bc91088b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:48:56 -0700 Subject: [PATCH 017/120] Call typed replace callbacks without a dyn box s.replace(re, fn) and replaceAll boxed a typed callback into a dyn function and called it through the dyn ABI: per match, a dyn argument array, a dyn node per capture and for the offset, the box's argument checks back to typed values, and a dyn result converted to a string. When every parameter the callback declares receives a value of its declared kind in each match, the box is unobservable (the runtime only calls it, with `this` undefined), so the backend passes the closure to scr_regex_replace_fn, which collects the matches as before and calls the closure through its own ABI. That holds for callbacks returning a string that take the match string alone, or, with a regex literal whose captures the compiled-matcher parser reads, captures every match sets (strings), then the offset (a number) and the subject (a string). Callbacks with optional, union or rest parameters, `arguments`, or captures some match leaves unset keep the dyn path. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-dispatch.ts | 60 +++++++++ .../src/backend/llvm/regex-matchers.test.ts | 25 ++++ .../src/backend/llvm/regex-matchers.ts | 34 +++++ packages/runtime/src/scr_regex.c | 122 ++++++++++++++++++ packages/runtime/src/scr_regex.h | 10 ++ .../6164-regex-replace-typed-callback.ts | 67 ++++++++++ 6 files changed, 318 insertions(+) create mode 100644 tests/corpus/6164-regex-replace-typed-callback.ts diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 4a9f4017a..ab195f5c0 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -9,6 +9,7 @@ import { emitTestLibCall } from "./lib-test.js"; import { InternalCompilerError } from "../../errors.js"; import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; +import type { IrExpr, IrType } from "../../ir/ir.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js"; import { @@ -21,6 +22,7 @@ import { isDynUndefined } from "../../ir/runtime-features.js"; import { DYN_KIND } from "./dyn.js"; import { rawBytes } from "./lib-abi.js"; import { emitInlineAtomicsOp } from "./expr-bytes.js"; +import { regexLiteralCaptures } from "./regex-matchers.js"; export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; @@ -554,8 +556,66 @@ function emitAssertFailing( * `a.name.localeCompare(b.name)` reads both fields without retaining). */ const READ_ONLY_STRING_LIB_FNS: ReadonlySet = new Set(["str.localeCompare"]); +/** The parameter count of a replace callback the runtime can call through + * the closure ABI (scr_regex_replace_fn), or null when it needs the dyn + * box. Each declared parameter must receive a value of its declared kind + * in every match: the match string at 0; with more than one parameter the + * pattern must be a literal whose captures are known, so position i is a + * capture that every match sets (a string), the offset (a number) right + * after the captures, or the subject (a string) after that. */ +function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null { + if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null; + if (callback.ret.kind !== "string") return null; + const params = callback.params; + if (params.length > 4) return null; + if (params.length === 0) return 0; + if (params[0]!.kind !== "string") return null; + if (params.length === 1) return 1; + if (pattern.kind !== "regexLit") return null; + const captures = regexLiteralCaptures(pattern.pattern, pattern.flags); + if (!captures) return null; + for (let i = 1; i < params.length; i++) { + const kind = params[i]!.kind; + const ok = + i < captures.count + ? kind === "string" && captures.always[i] === true + : i === captures.count + ? kind === "f64" + : i === captures.count + 1 && kind === "string"; + if (!ok) return null; + } + return params.length; +} + +/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can + * call directly: the dyn box the IR wraps it in is never observable (the + * runtime only calls it, with `this` undefined), so the closure goes to + * scr_regex_replace_fn as it is. Null keeps the generic path. */ +function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null { + const [subject, pattern, callback, all] = e.args; + if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null; + const arity = typedReplacerArity(callback.value.type, pattern); + if (arity === null) return null; + const B = host.B; + const s = host.emitExpr(subject); + const re = host.emitExpr(pattern); + const fn = host.emitExpr(callback.value); + host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`); + const result = B.tmp(); + B.line( + `${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`, + ); + const output = host.own({ name: result, type: e.type }); + host.emitPendingCheck(); + return output; +} + export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { if (USES_TIMERS_LIB_FNS.has(e.fn)) host.usesTimers = true; + if (e.fn === "regex.replaceCallback") { + const typed = emitTypedRegexReplace(host, e); + if (typed) return typed; + } if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e); if ( e.fn === "sp.pipeline" || diff --git a/packages/compiler/src/backend/llvm/regex-matchers.test.ts b/packages/compiler/src/backend/llvm/regex-matchers.test.ts index a3627cb7f..1acd9c465 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.test.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest"; import { compileRegexMatcher, regexLiteralBailReason, + regexLiteralCaptures, regexLiteralCompiles, } from "./regex-matchers.js"; @@ -11,6 +12,30 @@ function body(pattern: string, flags = "", vector = true): string { return code.lines.join("\n"); } +describe("regex literal captures", () => { + test("counts captures with the whole match and marks the ones every match sets", () => { + expect(regexLiteralCaptures("[&<>]", "g")).toEqual({ count: 1, always: [true] }); + expect(regexLiteralCaptures("(\\w+)=(\\d)+", "")).toEqual({ + count: 3, + always: [true, true, true], + }); + expect(regexLiteralCaptures("(?a)(b)?|(c)", "")).toEqual({ + count: 4, + always: [true, false, false, false], + }); + expect(regexLiteralCaptures("x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}", "")).toEqual({ + count: 6, + always: [true, false, false, false, false, true], + }); + }); + + test("answers null outside the parsed subset", () => { + expect(regexLiteralCaptures("(?=a)(b)", "")).toBeNull(); + expect(regexLiteralCaptures("(a)\\1", "")).toBeNull(); + expect(regexLiteralCaptures("a", "v")).toBeNull(); + }); +}); + describe("regex literal matcher subset", () => { test.each([ ['^(\\d+\\.\\d+\\.\\d+\\.\\d+) - - \\[([^\\]]+)\\] "(\\w+) ([^ ?"]+)(?:\\?([^ "]*))?$', ""], diff --git a/packages/compiler/src/backend/llvm/regex-matchers.ts b/packages/compiler/src/backend/llvm/regex-matchers.ts index ad5c47c86..001f081e5 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.ts @@ -1639,6 +1639,40 @@ function parseSubset(pattern: string, flags: string): { root: RxNode; ncap: numb } } +/** The capture count of a regex literal (the whole match included) and, + * per capture, whether every match sets it: no alternation of several + * branches and no repetition that can run zero times encloses it. Null + * when the pattern is outside the parsed subset. */ +export function regexLiteralCaptures( + pattern: string, + flags: string, +): { count: number; always: boolean[] } | null { + const parsed = parseSubset(pattern, flags); + if (!parsed) return null; + const always = Array.from({ length: parsed.ncap }, (_, i) => i === 0); + const walk = (node: RxNode, sure: boolean): void => { + switch (node.kind) { + case "cap": + always[node.index] = sure; + walk(node.body, sure); + return; + case "seq": + for (const item of node.items) walk(item, sure); + return; + case "alt": + for (const alt of node.alts) walk(alt, sure && node.alts.length === 1); + return; + case "rep": + walk(node.body, sure && node.min > 0); + return; + default: + return; + } + }; + walk(parsed.root, true); + return { count: parsed.ncap, always }; +} + /** Whether a literal compiles to a native matcher (tests and size caps). */ export function regexLiteralCompiles(pattern: string, flags: string): boolean { return parseSubset(pattern, flags) !== null; diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 4a6b88e1e..a2e591b0e 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -1367,6 +1367,128 @@ ScrStr *scr_regex_replace_callback(ScrStr *s, ScrRegex *re, ScrDyn *callback, bo return scr_jb_finish(&output); } +/* One call of a typed replacer (scr_regex_replace_fn's contract): string + * arguments are passed +1 (the closure ABI's owned parameters), the offset + * as a number, and the result is the closure's +1 string. */ +static ScrStr *scr_regex_call_replacer(ScrClosure *f, uint32_t arity, int ncap, + ScrStr *const *caps, double offset, ScrStr *subject) { + void *p[4] = { NULL, NULL, NULL, NULL }; + for (uint32_t i = 0; i < arity && i < 4; i++) { + if ((int)i < ncap) { + if (!caps[i]) scr_trap("scriptc: internal error: typed replacer capture is unset\n"); + p[i] = scr_str_retain(caps[i]); + } else if ((int)i == ncap + 1) { + p[i] = scr_str_retain(subject); + } + } + void *fn = f->fn; + typedef ScrStr *R; + switch (arity) { + case 0: return ((R (*)(ScrClosure *))fn)(f); + case 1: return ((R (*)(ScrClosure *, void *))fn)(f, p[0]); + case 2: + if (ncap == 1) return ((R (*)(ScrClosure *, void *, double))fn)(f, p[0], offset); + return ((R (*)(ScrClosure *, void *, void *))fn)(f, p[0], p[1]); + case 3: + if (ncap == 1) return ((R (*)(ScrClosure *, void *, double, void *))fn)(f, p[0], offset, p[2]); + if (ncap == 2) return ((R (*)(ScrClosure *, void *, void *, double))fn)(f, p[0], p[1], offset); + return ((R (*)(ScrClosure *, void *, void *, void *))fn)(f, p[0], p[1], p[2]); + case 4: + if (ncap == 2) + return ((R (*)(ScrClosure *, void *, void *, double, void *))fn)(f, p[0], p[1], offset, p[3]); + if (ncap == 3) + return ((R (*)(ScrClosure *, void *, void *, void *, double))fn)(f, p[0], p[1], p[2], offset); + if (ncap >= 4) + return ((R (*)(ScrClosure *, void *, void *, void *, void *))fn)(f, p[0], p[1], p[2], p[3]); + break; + default: break; + } + scr_trap("scriptc: internal error: typed replacer signature\n"); +} + +/* scr_regex_replace_callback's algorithm for a typed replacer: the matches + * are collected first, keeping only the captures the replacer reads, then + * the replacer runs once per match in order. */ +ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, bool all) { + uint8_t *bc = scr_regex_bc(re); + int flags = lre_get_flags(bc); + bool global = (flags & LRE_FLAG_GLOBAL) != 0; + if (all && !global) { + static const char message[] = "String.prototype.replaceAll called with a non-global RegExp argument"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + bool sticky = (flags & LRE_FLAG_STICKY) != 0; + bool unicode = (flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; + int capture_count = lre_get_capture_count(bc); + size_t keep = arity < (uint32_t)capture_count ? arity : (size_t)capture_count; + ScrRegexInput input; + scr_regex_input_init(&input, re, bc, s); + int len = input.len; + struct { int start, end; } *matches = NULL; + ScrStr **caps = NULL; + size_t count = 0, capacity = 0; + if (global) re->last_index = 0; + int position = sticky ? scr_regex_start(re, len) : 0; + if (position < 0) position = len + 1; + if (sticky && !global) re->last_index = 0; + while (position <= len && scr_exec(&input, bc, position) == 1) { + uint8_t **capture = input.capture; + int start = scr_capture_index(&input, capture[0]); + int end = scr_capture_index(&input, capture[1]); + if (sticky && !global) re->last_index = end; + if (count == capacity) { + size_t next = capacity ? capacity * 2 : 8; + if (next < capacity || next > SIZE_MAX / sizeof(*matches) / (keep + 1)) scr_regex_oom(); + void *grown = realloc(matches, next * sizeof(*matches)); + if (!grown) scr_regex_oom(); + matches = grown; + if (keep) { + grown = realloc(caps, next * keep * sizeof(*caps)); + if (!grown) scr_regex_oom(); + caps = grown; + } + capacity = next; + } + for (size_t group = 0; group < keep; group++) { + const uint8_t *cs = capture[group * 2], *ce = capture[group * 2 + 1]; + caps[count * keep + group] = + cs && ce ? scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)) + : NULL; + } + matches[count].start = start; + matches[count].end = end; + count++; + if (!global) break; + position = start == end ? scr_advance(&input, end, unicode) : end; + } + ScrJsonBuf output; + scr_jb_init(&output); + int next = 0; + scr_dyn_this_push_dyn(NULL); + for (size_t i = 0; i < count; i++) { + ScrStr *replacement = scr_regex_call_replacer(f, arity, capture_count, caps + i * keep, + (double)matches[i].start, s); + if (scr_exc_pending()) break; + scr_regex_put_span(&output, &input, next, matches[i].start); + scr_jb_put_str(&output, replacement); + scr_str_release(replacement); + next = matches[i].end; + } + scr_dyn_this_pop(); + for (size_t i = 0; i < count * keep; i++) scr_str_release(caps[i]); + free(caps); + free(matches); + if (scr_exc_pending()) { + scr_jb_abandon(&output); + scr_regex_input_dispose(&input); + return NULL; + } + scr_regex_put_span(&output, &input, next, len); + scr_regex_input_dispose(&input); + return scr_jb_finish(&output); +} + /* The pinned Node ANSI matcher derives from ansi-regex, copyright (c) * Sindre Sorhus , used under the MIT license: * Permission is hereby granted, free of charge, to any person obtaining a diff --git a/packages/runtime/src/scr_regex.h b/packages/runtime/src/scr_regex.h index e34e877de..014e5aa83 100644 --- a/packages/runtime/src/scr_regex.h +++ b/packages/runtime/src/scr_regex.h @@ -80,6 +80,16 @@ ScrStr *scr_regex_flags(ScrRegex *re); /* +1 */ ScrStr *scr_regex_to_string(ScrRegex *re); /* +1 */ /* replace: first match without /g, every match with it. */ ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep); +/* replace/replaceAll with a typed replacer closure of `arity` (at most 4) + * parameters returning a string. Parameter i receives what Node passes at + * position i: the match and captures (strings) below the capture count n, + * the match offset (a number) at n, the subject (a string) at n + 1. The + * compiler admits a closure only when every position it declares is one of + * those with the declared kind, and every capture it reads is set in every + * match (scr_regex_replace_callback handles the rest). Borrows all. */ +struct ScrClosure; +ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, struct ScrClosure *f, uint32_t arity, + bool all); /* replaceAll: throws Node's TypeError when /g is missing (may-throw). */ ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep); /* split: captured substrings are interleaved with the split pieces; diff --git a/tests/corpus/6164-regex-replace-typed-callback.ts b/tests/corpus/6164-regex-replace-typed-callback.ts new file mode 100644 index 000000000..8945235cd --- /dev/null +++ b/tests/corpus/6164-regex-replace-typed-callback.ts @@ -0,0 +1,67 @@ +// String replace/replaceAll with typed function replacers. Callbacks whose +// every parameter receives its declared kind in each match (the match, the +// captures every match sets, the offset, the subject) are called through +// the closure directly; the rest keep the generic path. Both must match +// Node: all matches are collected before the first call, calls run in +// order with the offset in UTF-16 code units, and a throw stops the +// replacement. +const entities: Record = { "&": "&", "<": "<", ">": ">", '"': """ }; +const escapeHtml = (text: string): string => text.replace(/[&<>"]/g, (c) => entities[c] ?? c); +console.log(escapeHtml('a & "c"'), escapeHtml("plain")); +console.log("hello world".replace(/o/, (m) => m.toUpperCase())); +console.log("hello world".replaceAll(/o/g, (m) => `[${m}]`)); +console.log("abc".replace(/x/g, () => "never")); + +console.log("a-b-c".replace(/-/g, (m: string, offset: number) => `${m}${offset}`)); +console.log("ab".replace(/b/, (m: string, offset: number, input: string) => `${m}@${offset}/${input}`)); +console.log("id-12 id-345".replace(/(\d+)/g, (_m: string, digits: string) => String(Number(digits) * 2))); +console.log("k1=v1;k2=v2".replace(/(\w+)=(\w+)/g, (_m: string, k: string, v: string) => `${v}=${k}`)); +console.log("x1y22".replace(/(\d)+/g, (m: string, last: string, at: number) => `<${m}:${last}:${at}>`)); +console.log("a=1".replace(/(\w)=(\d)/, (m: string, k: string, v: string, at: number) => `${m}|${k}|${v}|${at}`)); +console.log("ab ab".replace(/(?a)(b)/g, (m: string, a: string, b: string, at: number, all: string) => `${b}${a}${at}${all.length}`)); + +// Offsets count UTF-16 code units; empty matches advance by code points under /u. +console.log("héllo wörld 😀!".replace(/o/g, (m: string, at: number) => `${m}${at}`)); +console.log("😀a".replace(/(?:)/gu, (m: string, at: number) => `[${at}]`)); +console.log("😀a".replace(/(?:)/g, (m: string, at: number) => `[${at}]`)); + +// Captures that some match leaves unset keep the generic path. +console.log("ab".replace(/(a)|(b)/g, (m: string, a?: string, b?: string) => `${m}:${a}:${b};`)); +console.log("ab".replace(/(x)?b/, (m: string, x?: string) => `${m}:${x}`)); +const dynamicPattern = /(\d)/g; +console.log("1-2".replace(dynamicPattern, (m: string, d: string) => d + d)); + +// The callback runs after every match is collected: it can reuse the regex. +const word = /\w+/g; +console.log("one two three".replace(word, (m: string) => `${m.length}${word.lastIndex}`), word.lastIndex); +const sticky = /a/y; +sticky.lastIndex = 1; +console.log("baaa".replace(sticky, (m: string) => m.toUpperCase()), sticky.lastIndex); + +let calls = 0; +try { + "a1b2c3".replace(/\d/g, (m: string) => { + calls++; + if (m === "2") throw new Error("stop at " + m); + return "#"; + }); +} catch (error) { + console.log((error as Error).message, calls); +} +try { + "a1b2".replace(/(\d)|z/g, (m: string, d?: string) => { + if (d === "2") throw new Error("generic stop at " + d); + return "#" + m; + }); +} catch (error) { + console.log((error as Error).message); +} +try { + "aaa".replaceAll(/a/, (m: string) => m); +} catch (error) { + console.log(error instanceof TypeError, (error as Error).message); +} + +let total = 0; +for (let i = 0; i < 3000; i++) total += escapeHtml(`<${i}> & "${i}"`).length; +console.log(total); From 023d6201bc16dab915fdbdb25520107f9fe3b69e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 09:15:16 -0700 Subject: [PATCH 018/120] Record TypeScript order baselines for the new corpus programs Co-Authored-By: Claude Opus 5.5 --- .../tests/corpus/6160-number-radix-typed.ts.json | 6 ++++++ .../tests/corpus/6161-dispose-observer-gates.ts.json | 6 ++++++ .../tests/corpus/6162-dispose-observer-weak-keys.ts.json | 6 ++++++ .../tests/corpus/6163-dispose-observer-workers.ts.json | 6 ++++++ .../tests/corpus/6164-regex-replace-typed-callback.ts.json | 6 ++++++ .../tests/corpus/6165-regex-replace-callback-throws.ts.json | 6 ++++++ 6 files changed, 36 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6160-number-radix-typed.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6161-dispose-observer-gates.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6162-dispose-observer-weak-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6163-dispose-observer-workers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6164-regex-replace-typed-callback.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6165-regex-replace-callback-throws.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6160-number-radix-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6160-number-radix-typed.ts.json new file mode 100644 index 000000000..6df464b2b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6160-number-radix-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6160-number-radix-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6161-dispose-observer-gates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6161-dispose-observer-gates.ts.json new file mode 100644 index 000000000..1387cc325 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6161-dispose-observer-gates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6161-dispose-observer-gates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6162-dispose-observer-weak-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6162-dispose-observer-weak-keys.ts.json new file mode 100644 index 000000000..ab4838284 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6162-dispose-observer-weak-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6162-dispose-observer-weak-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6163-dispose-observer-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6163-dispose-observer-workers.ts.json new file mode 100644 index 000000000..327aa105f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6163-dispose-observer-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6163-dispose-observer-workers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6164-regex-replace-typed-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6164-regex-replace-typed-callback.ts.json new file mode 100644 index 000000000..8ca65275f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6164-regex-replace-typed-callback.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6164-regex-replace-typed-callback.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6165-regex-replace-callback-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6165-regex-replace-callback-throws.ts.json new file mode 100644 index 000000000..0f4b99484 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6165-regex-replace-callback-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6165-regex-replace-callback-throws.ts" + ], + "diags": [] +} From 19c742834e7eedf2f258fd72ee6820ab3a8d0abc Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:20:18 -0700 Subject: [PATCH 019/120] Add an experimental young-in nursery for compiled classes and records SCRIPTC_YOUNG_NURSERY=1 at compile time makes an executable allocate traced, program-owned class instances and records young and link the collector unit built with SCR_YIN (a runtime pack variant of scr_cycle.c; libraries never get it). The emitted constructor lists the object in a per-thread nursery and marks its header: generation SCR_CYC_YOUNG and its nursery index under the buffered bit, so every existing release path treats it as a buffered candidate (no call while it stays alive; scr_cyc_on_dead takes it out of the nursery at its death). A nursery collection (whenever 4096 young objects are live, at collection points, before full sweeps and limited passes) counts each young object's references from other young objects, marks from the objects whose count is below their reference count (referenced from outside the nursery: stacks, globals, older objects, runtime structures, FFI, fibers, coroutine frames), frees what stays unmarked, and gives up the dead set's outgoing edges after the teardowns. The counts are recomputed at every collection instead of maintained by a store barrier, so soundness rests only on the invariants trial deletion already needs. Survivors join trial deletion's nursery and outside roots become candidates; trial deletion walks young objects it reaches as nursery-level objects. Per-type admission statistics send types that do not die in young cycles back to trial deletion for a while. Publication takes young objects out of the nursery. The RC audit build checks nursery membership and counts at every collection. With the flag off, the compiler emits and links exactly what it did before (249 programs and the self-hosted compiler compared byte-identical). Measured with the flag on: cg-rings -38%, cg-pairs -27%, the runtime suite geomean -0.3% (published-graphs +3.4%), the self-hosted compiler neutral; shapes whose cycles go through arrays or closures (phase 2) pay without gaining. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 2 + .../compiler/src/backend/llvm/classes.test.ts | 51 +- packages/compiler/src/backend/llvm/classes.ts | 6 + packages/compiler/src/backend/llvm/emitter.ts | 2 + packages/compiler/src/backend/llvm/shapes.ts | 68 ++- .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 2 + .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../compiler/src/backend/young-nursery.ts | 9 + .../src/executable/executable-cache.ts | 8 +- packages/compiler/src/index.ts | 3 + packages/compiler/src/native/prepare.ts | 2 + .../compiler/src/native/sanitizer.test.ts | 1 + .../corpus/5981-young-nursery-cycles.ts.json | 6 + .../corpus/5982-young-nursery-workers.ts.json | 6 + .../runtime-pack-matrix.mjs | 9 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/library-matrix.mjs | 6 +- .../runtime-pack-matrix.mjs | 9 + .../runtime-pack-common/scripts/build.mjs | 2 +- .../test/library-matrix.test.ts | 17 + packages/runtime/src/scr_cycle.c | 417 ++++++++++++++++- packages/runtime/src/scr_rc.h | 32 ++ packages/runtime/test/test_yin.c | 442 ++++++++++++++++++ packages/runtime/test/yin.test.ts | 80 ++++ tests/corpus/5981-young-nursery-cycles.ts | 178 +++++++ tests/corpus/5982-young-nursery-workers.ts | 81 ++++ 28 files changed, 1439 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/src/backend/young-nursery.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json create mode 100644 packages/runtime/test/test_yin.c create mode 100644 packages/runtime/test/yin.test.ts create mode 100644 tests/corpus/5981-young-nursery-cycles.ts create mode 100644 tests/corpus/5982-young-nursery-workers.ts diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 553643470..a565965f1 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -3,11 +3,13 @@ import { type IrModule } from "../ir/ir.js"; import { moduleRuntimeFeatures } from "../ir/runtime-features.js"; import { hasForeignFfiCallback } from "./ffi-callbacks.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; +import { youngNurseryRequested } from "./young-nursery.js"; export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { const features = moduleRuntimeFeatures(mod); return { ...(mod.workers ? { workers: true } : {}), + ...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}), regex: features.regex, copying: features.copying, textDecoderLegacy: features.legacyTextDecoder, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 309d8f071..d1dec3f97 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; +import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper } from "./classes.js"; import { emitLlvmModule } from "./emitter.js"; import { @@ -316,3 +316,52 @@ test("unconstructed narrowing layouts may omit abstract slots while live classes }); expect(() => emitLlvmModule(mod)).toThrow("no implementation of run for Dormant"); }); + +test("the young-in nursery flag adds the nursery entry to traced constructors only", () => { + const linked = { kind: "object" as const, className: "Linked" }; + const mod: IrModule = { + irVersion: 16, + sourceFile: loc.file, + entry: "main", + functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], + classes: [ + { name: "Scalar", fields: [{ name: "value", type: F64 }], methods: [], loc }, + // A dyn field can reach anything: the class carries a collector header. + { + name: "Linked", + fields: [ + { name: "next", type: linked }, + { name: "hook", type: DYN }, + ], + methods: [], + loc, + }, + ], + }; + const helper = (llvm: string, name: string): string => { + const start = llvm.indexOf(`define internal ptr @${name}(`); + expect(start).toBeGreaterThanOrEqual(0); + return llvm.slice(start, llvm.indexOf("\n}\n", start)); + }; + const saved = process.env["SCRIPTC_YOUNG_NURSERY"]; + try { + delete process.env["SCRIPTC_YOUNG_NURSERY"]; + const off = emitLlvmModule(mod, { pointerBits: 64 }); + expect(off).not.toContain("scr_yin"); + + process.env["SCRIPTC_YOUNG_NURSERY"] = "1"; + const on = emitLlvmModule(mod, { pointerBits: 64 }); + const linkedNew = helper(on, mangleClassNew("Linked")); + expect(linkedNew).toContain("@scr_yin_skip"); + expect(linkedNew).toContain("call void @scr_yin_enter_slow(ptr %o)"); + expect(linkedNew).toContain("store i8 3, ptr %ye.genp"); + // An acyclic class has no collector header and never enters. + expect(helper(on, mangleClassNew("Scalar"))).not.toContain("scr_yin"); + // Targets without the inline allocator call the whole entry. + const narrow = emitLlvmModule(mod, { pointerBits: 32 }); + expect(helper(narrow, mangleClassNew("Linked"))).toContain("call void @scr_yin_enter(ptr %o)"); + } finally { + if (saved === undefined) delete process.env["SCRIPTC_YOUNG_NURSERY"]; + else process.env["SCRIPTC_YOUNG_NURSERY"] = saved; + } +}); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 8a13d83ad..456b4b187 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -51,6 +51,7 @@ import { retainBody, traceAdapter, type ShapeHost, + youngEntryLines, } from "./shapes.js"; /** One virtual method slot of a hierarchy: the ROOT-MOST declaring class @@ -803,6 +804,11 @@ export function emitClassShapes( ); } nw.push(...undefFieldInits(host, meta, nullable)); + // Young instances: program-owned traced classes. Classes rooted at a + // runtime base (errors, emitters, events, streams) carry runtime state + // and stay with trial deletion, like every runtime family. + if (traced && !lifetime && (!meta.hierarchy || meta.root.def.runtime !== true)) + nw.push(...youngEntryLines(host)); nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``); defs.push(...nw); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2c3a71457..6d0973a05 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3,6 +3,7 @@ import { emitStringInputs } from "./string-lifetimes.js"; import { emitLlvmLayouts } from "./layouts.js"; import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js"; import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js"; +import { youngNurseryRequested } from "../young-nursery.js"; import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; import { InternalCompilerError } from "../../errors.js"; import { createHash } from "node:crypto"; @@ -1334,6 +1335,7 @@ export class LlEmitter { ...(this.rcMode !== undefined ? { rcMode: this.rcMode } : {}), ...(this.rcHelpersNoinline ? { rcHelpersNoinline: true } : {}), nextCycKind: () => FIRST_EMITTED_CYC_KIND + this.cycKinds++, + ...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}), unionsById: this.unionsById, declare: (decl) => this.declare(decl), needOom: () => this.needOom(), diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 23565eff4..32a0ca006 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -28,7 +28,7 @@ import { mangleRecordStruct, mangleRecordTrace, } from "../mangle.js"; -import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js"; +import { CYC_HDR, cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js"; import type { BlockBuilder } from "./blocks.js"; import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import type { NullableUnions } from "./nullable-unions.js"; @@ -40,6 +40,10 @@ import { } from "./inline-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; +/** SCR_CYC_MAX_TYPES (scr_rc.h): the registry's size, and the length of + * the young-in nursery's per-id skip countdown. */ +const CYC_MAX_TYPES = 32768; + /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { @@ -101,6 +105,67 @@ export interface ShapeHost { * in emission order. * Absent: every descriptor gets 0, which keeps no statistics. */ nextCycKind?(): number; + /** The young-in nursery (backend/young-nursery.ts): `new` hands each + * traced program-owned instance to scr_yin_enter. Absent: off. */ + readonly youngNursery?: boolean; +} + +/** The nursery entry of a new traced instance in `%o`, after its count is + * stored; nothing when the nursery is off. Where the allocator is inlined + * (64-bit, process-global state) the entry is inline too: a type that is + * skipping the nursery (its scr_yin_skip countdown is nonzero) counts down + * and stays out; otherwise, while the nursery has room (n < lim in ScrYin, + * scr_rc.h), list the object at index n and mark its header young + * (generation SCR_CYC_YOUNG, `buf` the buffered bit and the index), and + * call scr_yin_enter_slow when it has none (it collects the nursery + * first). Other targets call scr_yin_enter, which does all of it. + * Labels use the `ye.` prefix; the lines end in an open block. */ +export function youngEntryLines(host: ShapeHost): string[] { + if (host.youngNursery !== true) return []; + if (host.inlineAlloc !== true || host.threadLocalAlloc === true) { + host.declare(`declare void @scr_yin_enter(ptr)`); + return [` call void @scr_yin_enter(ptr %o) ; the young-in nursery`]; + } + host.declare(`@scr_yin = external global { ptr, i64, i64, i64 }`); + host.declare(`@scr_yin_skip = external global [${CYC_MAX_TYPES} x i32]`); + host.declare(`declare void @scr_yin_enter_slow(ptr)`); + return [ + ` %ye.idp = getelementptr inbounds i8, ptr %o, i64 -${CYC_HDR} ; the young-in nursery`, + ` %ye.id16 = load i16, ptr %ye.idp ; ScrCycHdr.id`, + ` %ye.id = zext i16 %ye.id16 to i64`, + ` %ye.sp = getelementptr inbounds [${CYC_MAX_TYPES} x i32], ptr @scr_yin_skip, i64 0, i64 %ye.id`, + ` %ye.s = load i32, ptr %ye.sp`, + ` %ye.skip = icmp ne i32 %ye.s, 0`, + ` br i1 %ye.skip, label %ye.skipped, label %ye.enter`, + `ye.skipped: ; a type that does not die young: counted down, stays out`, + ` %ye.s1 = sub i32 %ye.s, 1`, + ` store i32 %ye.s1, ptr %ye.sp`, + ` br label %ye.done`, + `ye.enter:`, + ` %ye.v = load ptr, ptr @scr_yin`, + ` %ye.np = getelementptr inbounds i8, ptr @scr_yin, i64 8`, + ` %ye.n = load i64, ptr %ye.np`, + ` %ye.lp = getelementptr inbounds i8, ptr @scr_yin, i64 16`, + ` %ye.l = load i64, ptr %ye.lp`, + ` %ye.room = icmp ult i64 %ye.n, %ye.l`, + ` br i1 %ye.room, label %ye.fast, label %ye.slow`, + `ye.fast:`, + ` %ye.slot = getelementptr inbounds ptr, ptr %ye.v, i64 %ye.n`, + ` store ptr %o, ptr %ye.slot`, + ` %ye.n1 = add i64 %ye.n, 1`, + ` store i64 %ye.n1, ptr %ye.np`, + ` %ye.genp = getelementptr inbounds i8, ptr %o, i64 -5`, + ` store i8 3, ptr %ye.genp ; ScrCycHdr.gen = SCR_CYC_YOUNG`, + ` %ye.bufp = getelementptr inbounds i8, ptr %o, i64 -4`, + ` %ye.i = trunc i64 %ye.n to i32`, + ` %ye.buf = or i32 %ye.i, -2147483648`, + ` store i32 %ye.buf, ptr %ye.bufp ; ScrCycHdr.buf = buffered bit | index`, + ` br label %ye.done`, + `ye.slow:`, + ` call void @scr_yin_enter_slow(ptr %o)`, + ` br label %ye.done`, + `ye.done:`, + ]; } /** What class emission asks of lifetime regions (LifetimeRegions in @@ -1453,6 +1518,7 @@ export function emitRecordShapes( ` store ptr %ovf, ptr %ovfp`, ); } + if (traced) nw.push(...youngEntryLines(host)); nw.push(` ret ptr %o`, `}`, ``); defs.push(...nw); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 8f69cea57..3beb84a20 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -16,6 +16,9 @@ import type { NativeTargetSpec } from "./targets.js"; export interface NativeLinkFeatures { workers?: boolean; + /** The young-in nursery (backend/young-nursery.ts): the SCR_YIN + * variant of the collector unit. Absent when off. */ + youngNursery?: boolean; regex: boolean; copying: boolean; textDecoderLegacy: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 5e981fe44..875a723bc 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -46,6 +46,8 @@ export interface CcOptions { frameworks?: readonly string[]; /** Isolated native Worker contexts; every runtime object must use TLS. */ workers?: boolean; + /** Compile the runtime with SCR_YIN (backend/young-nursery.ts). */ + youngNursery?: boolean; regex?: boolean; /** The program uses one of the copying/typed-array bridge intrinsics diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 549e9ebbf..6c8673fe4 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -445,6 +445,7 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", // The runtime object model uses type-punned C: a hierarchy // upcast is a raw pointer cast, so one object's header (rc, vt) and @@ -643,6 +644,7 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", "-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs "-Wno-deprecated-declarations", diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 039ecc209..16fbc2a36 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -80,6 +80,7 @@ export interface RuntimePackManifest { export interface RuntimeFeatureSet extends NativeLinkFeatures { workers: boolean; + youngNursery: boolean; eventTarget: boolean; nativeFetch: boolean; netEffective: boolean; @@ -266,6 +267,7 @@ export function effectiveRuntimeFeatures( return { ...features, workers: features.workers === true, + youngNursery: features.youngNursery === true, eventTarget: features.eventTarget === true, nativeFetch, netEffective, diff --git a/packages/compiler/src/backend/young-nursery.ts b/packages/compiler/src/backend/young-nursery.ts new file mode 100644 index 000000000..090a4a6b9 --- /dev/null +++ b/packages/compiler/src/backend/young-nursery.ts @@ -0,0 +1,9 @@ +/** The young-in nursery (phase 1, experimental): compile-time opt-in with + * SCRIPTC_YOUNG_NURSERY=1. Executables then allocate compiled class and + * record instances young (the emitted constructors call scr_yin_enter) and + * link the collector unit built with SCR_YIN, which collects young cycles + * in nursery collections (scr_cycle.c, "young-in nursery"). Unset, nothing + * the compiler emits or links changes. */ +export function youngNurseryRequested(env: NodeJS.ProcessEnv = process.env): boolean { + return env["SCRIPTC_YOUNG_NURSERY"] === "1"; +} diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index fbd7e4edd..ee9d84fcf 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -49,6 +49,7 @@ export interface EarlyExecutableNativeFeatures { /** Omitted is the historical release posture. */ optimization?: "dev" | "speed"; workers?: boolean; + youngNursery?: boolean; regex: boolean; copying: boolean; textDecoderLegacy: boolean; @@ -111,6 +112,8 @@ export interface EarlyExecutableCacheOptions { windowsSubsystem?: "gui"; /** Omitted unless --detached-exit is set. */ detachedExit?: true; + /** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */ + youngNursery?: true; npmStatic: readonly string[] | "auto" | null; /** Raw manifest identity: path and bytes. Native archives remain under the * stricter native cache's independent dependency validation. */ @@ -200,7 +203,8 @@ function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeat (native.optimization === undefined || native.optimization === "dev" || native.optimization === "speed") && - (native.workers === undefined || typeof native.workers === "boolean"), + (native.workers === undefined || typeof native.workers === "boolean") && + (native.youngNursery === undefined || typeof native.youngNursery === "boolean"), ); } @@ -221,6 +225,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string { ...(options.strip ? ["strip"] : []), ...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []), ...(options.detachedExit ? ["detached-exit"] : []), + ...(options.youngNursery ? ["young-nursery"] : []), options.npmStatic === null ? "" : options.npmStatic === "auto" @@ -260,6 +265,7 @@ function routeKey(options: Omit; if (features["workers"] !== undefined && typeof features["workers"] !== "boolean") return false; + if (features["youngNursery"] !== undefined && typeof features["youngNursery"] !== "boolean") + return false; return [ "regex", "copying", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 9047f3376..79984d6c5 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -73,6 +73,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( systemLibraries: [], features: { workers: false, + youngNursery: false, eventTarget: false, regex: true, copying: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json new file mode 100644 index 000000000..b5e72c495 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5981-young-nursery-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json new file mode 100644 index 000000000..77892fe94 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5982-young-nursery-workers.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 54e789624..858324434 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -124,6 +124,15 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + if (source === "scr_cycle.c") { + // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with + // nursery collections; programs built without it link the default. + variants.push({ + id: "young-nursery", + when: { youngNursery: true }, + defines: ["SCR_YIN"], + }); + } return variants; } diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index d5c8c3720..18ee441e6 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -343,7 +343,7 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY"], + executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs index 4f14d959b..106950941 100644 --- a/packages/runtime-pack-common/library-matrix.mjs +++ b/packages/runtime-pack-common/library-matrix.mjs @@ -27,7 +27,11 @@ export function withLibraryRuntimeFlavors(matrix) { }, ].map((unit) => ({ ...unit, - variants: unit.variants.filter((variant) => !variant.defines.includes("SCR_WORKERS")), + // Libraries run neither workers nor the young-in nursery (an + // executable-only experiment). + variants: unit.variants.filter( + (variant) => !variant.defines.includes("SCR_WORKERS") && !variant.defines.includes("SCR_YIN"), + ), })); const flavors = { ...matrix.flavors }; for (const [flavor, spec] of Object.entries(matrix.flavors)) { diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6b5052482..91470b760 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -114,6 +114,15 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + if (source === "scr_cycle.c") { + // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with + // nursery collections; programs built without it link the default. + variants.push({ + id: "young-nursery", + when: { youngNursery: true }, + defines: ["SCR_YIN"], + }); + } return variants; } diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index d1de57418..1bcd7455d 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -440,7 +440,7 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY"], + executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime-pack-common/test/library-matrix.test.ts b/packages/runtime-pack-common/test/library-matrix.test.ts index e78a2260c..1bebe159a 100644 --- a/packages/runtime-pack-common/test/library-matrix.test.ts +++ b/packages/runtime-pack-common/test/library-matrix.test.ts @@ -61,6 +61,23 @@ test("library pack modes preserve pure runtime features and exclude ambient even expect(matrix.runtime_units.some((unit) => unit.source === "scr_library.c")).toBe(false); }); +test("the young-in nursery collector is an executable-only variant of scr_cycle.c", () => { + const ids = (units: { source: string; variants: { id: string }[] }[]) => + units.find((unit) => unit.source === "scr_cycle.c")!.variants.map((variant) => variant.id); + expect(ids(matrix.runtime_units)).toEqual([ + "default", + "default-workers", + "young-nursery", + "young-nursery-workers", + ]); + const young = matrix.runtime_units + .find((unit) => unit.source === "scr_cycle.c") + .variants.find((variant) => variant.id === "young-nursery"); + expect(young).toMatchObject({ when: { youngNursery: true, workers: false }, defines: ["SCR_YIN"] }); + for (const mode of ["library-release", "library-thread-release"]) + expect(ids(matrix.flavors[mode].runtime_units)).toEqual(["default"]); +}); + test("mobile packs ship only library modes and their permitted vendor archives", () => { const mobile = createRuntimePackMatrix({ target: { object_format: "elf" }, diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index c2be13a03..853545114 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -409,6 +409,22 @@ static SCR_TL unsigned scr_gen_limit = SCR_CYC_OLD; #endif static size_t scr_cyc_pass(unsigned gen_limit); +#ifdef SCR_YIN +/* The young-in nursery's state (see "young-in nursery" below): the nursery + * itself (scr_rc.h), every live young object at the index its header + * records; a collection's mark worklist, dead set and deferred releases; + * per-object counts of references from other young objects, valid during a + * collection. */ +SCR_TL ScrYin scr_yin = {0}; +static SCR_TL ScrVec scr_yin_stack; +static SCR_TL ScrVec scr_yin_dead; +static SCR_TL ScrVec scr_yin_xrel; +static SCR_TL uint32_t *scr_yin_counts; +static SCR_TL size_t scr_yin_counts_cap; +static SCR_TL bool scr_yin_busy; +static void scr_yin_leave(ScrCycHdr *h, void *obj); +static size_t scr_yin_collect(void); +#endif /* A limited pass (scr_cyc_collect_set) takes only the candidates in this * set from the buffers up to `scr_cyc_take_gen`; NULL takes them all. */ static SCR_TL const ScrCycSet *scr_cyc_take = NULL; @@ -699,6 +715,12 @@ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) { * per turn would walk the whole live heap every turn — which is exactly the * cost the generations exist to avoid. */ void scr_cyc_collect_scheduled(void) { +#ifdef SCR_YIN + /* A collection point also collects the nursery when anything entered it + * since its last collection, so young garbage does not wait for the + * allocation trigger in a program that has gone idle. */ + if (scr_yin.n != 0) scr_yin_collect(); +#endif if (scr_cyc_buffered() == 0) { scr_cyc_scheduled_mature_age = 0; return; @@ -834,6 +856,13 @@ static inline void scr_cyc_freed(const ScrCycType *type, unsigned gen) { void scr_cyc_on_dead(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); if (!scr_cyc_is_buffered(h)) return; +#ifdef SCR_YIN + /* A young object leaves the nursery; nothing else lists it. */ + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) { + scr_yin_leave(h, obj); + return; + } +#endif /* O(1) removal: swap the last entry into the hole. A buffered object's * generation never changes (promotion runs only on objects the pass has * already drained from the buffers), so `gen` names the right one. */ @@ -863,6 +892,18 @@ static SCR_TL ScrVec scr_pub_holes[SCR_CYC_NGENS]; uint32_t scr_cyc_pub_take(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); +#ifdef SCR_YIN + /* A young object (its header reads as buffered) leaves the nursery, + * unbuffered: young objects never become immortal in the nursery, and so + * never reach another thread from it. The answer UINT32_MAX makes + * scr_pub_mark's recorded position 0 ("not taken"), so a refused publish + * leaves it out of every buffer, as it was. It is reachable from the + * graph being published, so it needs no candidate. */ + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) { + scr_yin_leave(h, obj); + return UINT32_MAX; + } +#endif unsigned gen = scr_cyc_gen(h); uint32_t index = scr_cyc_buf_index(h); scr_roots[gen].v[index] = NULL; @@ -931,9 +972,19 @@ void scr_cyc_on_release(void *obj) { * two must agree on which edges are internal. Nothing to do for NULL * (unassigned slots), immortal (interned statics have no header at all), or * a generation this pass does not collect. */ +#ifdef SCR_YIN +/* Trial deletion walks a young object as a nursery-level one: structures + * that mix young objects with families that are not young (arrays, + * closures) are found by the candidates of the latter, exactly as before. + * A young survivor stays young (scan_black promotes only below OLD) and a + * young member of the dead set leaves the nursery before its teardown. */ +#define SCR_CYC_WALK_GEN(h) (scr_cyc_gen(h) == SCR_CYC_YOUNG ? SCR_CYC_NURSERY : scr_cyc_gen(h)) +#else +#define SCR_CYC_WALK_GEN(h) scr_cyc_gen(h) +#endif #define SCR_CYC_SKIP(child) \ ((child) == NULL || SCR_RC(child) == SIZE_MAX \ - || scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit) + || SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit) /* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where * an object is queued instead (scr_pending, or scr_restore_pending for the @@ -976,7 +1027,7 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp static void scr_mg_visit(void *child, void *ctx) { if (child == NULL || SCR_RC(child) == SIZE_MAX) return; ScrCycHdr *h = scr_cyc_hdr(child); - if (scr_cyc_gen(h) > scr_gen_limit) { + if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) { scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder); scr_cyc_push_work(&scr_xgen_edges, child); return; @@ -1148,6 +1199,333 @@ static SCR_CYC_COMPACT void scr_xg_release(void *obj) { scr_rc_destroy(obj, scr_xg_destroy); } +#ifdef SCR_YIN +/* ── young-in nursery ─────────────────────────────────────────────────── */ + +/* Programs built with the young-in nursery (SCRIPTC_YOUNG_NURSERY=1, which + * links this unit built with SCR_YIN) allocate compiled class and record + * instances YOUNG: the emitted constructor hands each new object to + * scr_yin_enter, which lists it in the nursery and marks its header + * SCR_CYC_YOUNG with its nursery index in `buf`, under the buffered bit. + * The bit makes every release path treat the object as an existing + * candidate: a release that leaves it alive makes no call, and its death + * calls scr_cyc_on_dead, which takes it out of the nursery + * (scr_yin_leave) before the teardown runs. Young objects are never + * candidates, but trial deletion walks them as nursery-level objects when + * a candidate reaches them (SCR_CYC_WALK_GEN). + * + * Whenever the nursery holds SCR_YOUNG_NURSERY_SIZE live objects (4096 by + * default), at collection points and before every full sweep, the nursery + * is collected on its own, in time proportional to its size and without + * walking anything older: + * count for every young object, the references to it from traced + * fields of other young objects (one trace per young object); + * mark an object whose count is below its reference count is + * referenced from outside the nursery (a stack, a global, an + * older object, a runtime structure, FFI, a fiber or a coroutine + * frame: every one of them holds a counted reference); mark + * from each such object through young edges; + * sweep what is unmarked is referenced only from unmarked young + * objects, which is garbage. Marked objects join trial + * deletion's nursery generation, unbuffered; dead ones are torn + * down, giving up only the edges that leave the dead set (like + * the white set's cross-generation edges, after every teardown). + * Survivors go to trial deletion's nursery rather than past it because the + * structures that survive are mostly held by families that are not young + * (arrays, closures, maps): a dead tree whose nodes are young and whose + * child arrays are not survives the nursery collection through the + * arrays, and the arrays' own candidates reach it there. + * Soundness rests on the two invariants trial deletion already needs: every + * strong reference is counted, and a trace visits exactly the counted + * references to headered children. The counts are recomputed at every + * collection, so no store barrier is needed and none can be missed. + * + * Candidates. A release of a young object never buffers it, so a structure + * whose last outside reference was dropped while its root was young has no + * candidate, and when the structure reaches beyond the nursery (a young + * node in an array that is not young, say), the nursery collection sees + * the outside edge as a root and keeps it. So every outside root a + * collection finds (count below the reference count) becomes a candidate + * as it is promoted. That suffices: every survivor is reachable from such + * a root, so if a survivor was already garbage, so was its root, and trial + * deletion reaches the survivor from the root's candidate; garbage formed + * after the promotion has a candidate from the release that formed it, + * since survivors are no longer young. */ +static void scr_cyc_trim(ScrVec *vec); +#define SCR_YIN_DEFAULT_SIZE 4096 +#define SCR_CYC_YIN_PROMOTED SCR_CYC_NURSERY + +/* Pushes onto the collection's work vectors, inline. */ +static inline __attribute__((always_inline)) void scr_yin_push(ScrVec *vec, void *obj) { + if (__builtin_expect(vec->n == vec->cap, 0)) scr_cyc_grow(vec); + vec->v[vec->n++] = obj; + if (vec->n > vec->used) vec->used = vec->n; +} +#define SCR_YIN_MAX_SIZE ((size_t)1 << 30) + +static size_t scr_yin_size(void) { + static SCR_TL size_t cached = 0; + if (cached == 0) { + const char *env = scr_getenv("SCR_YOUNG_NURSERY_SIZE"); + long v = env ? strtol(env, NULL, 10) : 0; + cached = v > 0 ? ((size_t)v < SCR_YIN_MAX_SIZE ? (size_t)v : SCR_YIN_MAX_SIZE) + : SCR_YIN_DEFAULT_SIZE; + } + return cached; +} + +/* Collections, survivors and dead objects, printed at exit with + * SCR_YOUNG_NURSERY_STATS=1 (measurement). */ +static SCR_TL size_t scr_yin_stat[3]; +static void scr_yin_stats_print(void) { + fprintf(stderr, "scriptc young nursery: %zu collections, %zu survivors, %zu dead\n", + scr_yin_stat[0], scr_yin_stat[1], scr_yin_stat[2]); +} + +/* Types that do not die in young cycles. Only a young object that dies in + * a nursery collection gains anything there; every other one costs: one + * that dies by reference counting makes a call to leave the nursery (where + * it made none before), one that is published is moved out of it, and one + * that survives a collection is kept by it, and becomes a trial-deletion + * candidate when an outside reference holds it. Structures held by + * families that are not young (tree nodes in child arrays, objects + * captured by closures) survive, and plain data dies by reference counting. + * So each thread keeps, per registry id, how many of the type's young + * objects left the nursery (by a collection or otherwise) and how many of + * them a collection freed, and after SCR_YIN_WINDOW of them a type freed + * for fewer than one in SCR_YIN_RATIO is allocated as before (trial + * deletion's nursery) for its next SCR_YIN_PROBE allocations, then + * enters again for a fresh window: the same bypass-and-probe scheme as the + * levels' bypass above. The countdown + * (scr_yin_skip, an ABI: emitted constructors test and decrement it + * inline) only steers which objects enter the nursery. */ +#define SCR_YIN_WINDOW 1024 +#define SCR_YIN_RATIO 4 +#define SCR_YIN_PROBE (64 * SCR_YIN_WINDOW) +#ifdef SCR_WORKERS +static _Thread_local uint32_t *scr_yin_skip_tl; +static uint32_t *scr_yin_skip_table(void) { + if (!scr_yin_skip_tl) { + scr_yin_skip_tl = calloc(SCR_CYC_MAX_TYPES, sizeof *scr_yin_skip_tl); + if (!scr_yin_skip_tl) scr_cyc_oom(); + } + return scr_yin_skip_tl; +} +#else +uint32_t scr_yin_skip[SCR_CYC_MAX_TYPES] = {0}; +static uint32_t *scr_yin_skip_table(void) { return scr_yin_skip; } +#endif +typedef struct { + uint32_t met, freed; +} ScrYinType; +/* SCR_YOUNG_NURSERY_ALL=1 keeps every type young (test lanes). */ +static bool scr_yin_all(void) { + static SCR_TL int cached = -1; + if (cached < 0) { + const char *env = scr_getenv("SCR_YOUNG_NURSERY_ALL"); + cached = env != NULL && env[0] == '1'; + } + return cached != 0; +} +static SCR_TL ScrYinType *scr_yin_types; + +static __attribute__((noinline, cold)) void scr_yin_decide(uint16_t id, ScrYinType *t) { + if (t->freed * SCR_YIN_RATIO < t->met && !scr_yin_all()) scr_yin_skip_table()[id] = SCR_YIN_PROBE; + t->met = t->freed = 0; +} +static inline __attribute__((always_inline)) void scr_yin_met(uint16_t id, bool dead) { + ScrYinType *t = &scr_yin_types[id]; + t->freed += dead; + if (__builtin_expect(++t->met >= SCR_YIN_WINDOW, 0)) scr_yin_decide(id, t); +} + +void scr_yin_enter_slow(void *obj); + +/* The whole nursery entry, for targets whose constructors call it. */ +void scr_yin_enter(void *obj) { + uint32_t *skip = &scr_yin_skip_table()[scr_cyc_hdr(obj)->id]; + if (*skip != 0) { + --*skip; + return; + } + scr_yin_enter_slow(obj); +} + +/* The slow half of the nursery entry (the emitted constructors inline the + * rest: skip, or append while n < lim). The nursery holds at most + * SCR_YOUNG_NURSERY_SIZE live young objects: the object that would exceed + * that collects the nursery first, then enters. */ +void scr_yin_enter_slow(void *obj) { + size_t size = scr_yin_size(); + if (!scr_yin_types) { + scr_yin_types = calloc(SCR_CYC_MAX_TYPES, sizeof *scr_yin_types); + if (!scr_yin_types) scr_cyc_oom(); + } + if (scr_yin.n >= size) { + static SCR_TL bool stats_checked = false; + if (!stats_checked) { + stats_checked = true; + const char *env = scr_getenv("SCR_YOUNG_NURSERY_STATS"); + if (env && env[0] == '1') atexit(scr_yin_stats_print); + } + scr_yin_collect(); + } + if (scr_yin.n == scr_yin.cap) { + /* Positions are 31-bit (ScrCycHdr.buf); the size is capped below. */ + size_t cap = scr_yin.cap ? scr_yin.cap * 2 : 1024; + void **grown = realloc(scr_yin.v, cap * sizeof *grown); + if (!grown) scr_cyc_oom(); + scr_yin.v = grown; + scr_yin.cap = cap; + } + scr_yin.lim = scr_yin.cap < size ? scr_yin.cap : size; + ScrCycHdr *h = scr_cyc_hdr(obj); + scr_cyc_set_gen(h, SCR_CYC_YOUNG); + scr_cyc_set_buffered(h, (uint32_t)scr_yin.n); + scr_yin.v[scr_yin.n++] = obj; +} + +/* O(1) removal: the last young object takes the hole. */ +static void scr_yin_leave(ScrCycHdr *h, void *obj) { + size_t i = scr_cyc_buf_index(h); +#ifdef SCR_RC_AUDIT + if (i >= scr_yin.n || scr_yin.v[i] != obj) scr_trap("scriptc: young object outside the nursery\n"); +#endif + void *last = scr_yin.v[--scr_yin.n]; + scr_yin.v[i] = last; + if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i); + scr_cyc_clear_buffered(h); + scr_cyc_set_gen(h, SCR_CYC_NURSERY); + scr_yin_met(h->id, false); +} + +static void scr_yin_count_visit(void *child, void *ctx) { + (void)ctx; + if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + ScrCycHdr *h = scr_cyc_hdr(child); + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_counts[scr_cyc_buf_index(h)]++; +} + +/* Marked young objects are GRAY (young objects are otherwise BLACK). */ +static void scr_yin_mark_visit(void *child, void *ctx) { + (void)ctx; + if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + ScrCycHdr *h = scr_cyc_hdr(child); + if (scr_cyc_gen(h) != SCR_CYC_YOUNG || scr_cyc_color(h) == SCR_CYC_GRAY) return; + scr_cyc_set_color(h, SCR_CYC_GRAY); + scr_yin_push(&scr_yin_stack, child); +} + +/* A dead object's edge: young and unmarked is dead too (nothing to give + * up); a marked young object (promoted now or soon) or an older one keeps + * the count until every teardown has run. */ +static void scr_yin_dead_visit(void *child, void *ctx) { + (void)ctx; + if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + ScrCycHdr *h = scr_cyc_hdr(child); + if (scr_cyc_gen(h) == SCR_CYC_YOUNG && scr_cyc_color(h) != SCR_CYC_GRAY) return; + scr_yin_push(&scr_yin_xrel, child); +} + +/* Fully optimized, unlike trial deletion's per-pass bookkeeping: its loops + * run once per young object. */ +static __attribute__((noinline)) size_t scr_yin_collect(void) { + if (scr_yin_busy || scr_collecting) return 0; + size_t n = scr_yin.n; + if (n == 0) return 0; + scr_yin_busy = true; + /* Releases made by teardowns below buffer candidates but must not start + * a trial-deletion pass while the dead set is half torn down. */ + scr_collecting = true; + if (n > scr_yin_counts_cap) { + size_t cap = scr_yin_counts_cap ? scr_yin_counts_cap * 2 : 1024; + while (cap < n) cap *= 2; + uint32_t *counts = realloc(scr_yin_counts, cap * sizeof *counts); + if (!counts) scr_cyc_oom(); + scr_yin_counts = counts; + scr_yin_counts_cap = cap; + } + memset(scr_yin_counts, 0, n * sizeof *scr_yin_counts); + void **v = scr_yin.v; + for (size_t k = 0; k < n; k++) + scr_cyc_type(scr_cyc_hdr(v[k]))->trace(v[k], scr_yin_count_visit, NULL); +#ifdef SCR_RC_AUDIT + /* Membership and counts: every listed object is young at its index and + * alive, and holds at least the references young objects make to it + * (fewer would mean an uncounted reference). */ + for (size_t k = 0; k < n; k++) { + ScrCycHdr *h = scr_cyc_hdr(v[k]); + if (scr_cyc_gen(h) != SCR_CYC_YOUNG || !scr_cyc_is_buffered(h) || scr_cyc_buf_index(h) != k || + scr_cyc_color(h) != SCR_CYC_BLACK) + scr_trap("scriptc: corrupt young nursery entry\n"); + if (SCR_RC(v[k]) == 0 || SCR_RC(v[k]) < scr_yin_counts[k]) + scr_trap("scriptc: young object with uncounted references\n"); + } +#endif + for (size_t k = 0; k < n; k++) { + void *obj = v[k]; + ScrCycHdr *h = scr_cyc_hdr(obj); + if (scr_cyc_color(h) == SCR_CYC_GRAY || SCR_RC(obj) <= scr_yin_counts[k]) continue; + scr_cyc_set_color(h, SCR_CYC_GRAY); + scr_yin_push(&scr_yin_stack, obj); + while (scr_yin_stack.n) { + void *x = scr_yin_stack.v[--scr_yin_stack.n]; + scr_cyc_type(scr_cyc_hdr(x))->trace(x, scr_yin_mark_visit, NULL); + } + } + /* Split: promote the marked objects; record the dead ones and their + * edges to survivors and older objects, which are given up after every + * teardown (until then each holds the count it was made with, so no + * teardown's releases can free a target early). Edges between dead + * objects need nothing: both go. No block is freed before every dead + * object has been traced. */ + scr_yin_dead.n = 0; + scr_yin_xrel.n = 0; + for (size_t k = 0; k < n; k++) { + void *obj = v[k]; + ScrCycHdr *h = scr_cyc_hdr(obj); + if (scr_cyc_color(h) == SCR_CYC_GRAY) { + scr_cyc_set_color(h, SCR_CYC_BLACK); + scr_cyc_set_gen(h, SCR_CYC_YIN_PROMOTED); + scr_cyc_clear_buffered(h); + /* An outside root becomes a candidate (see "candidates" above). */ + if (SCR_RC(obj) > scr_yin_counts[k]) scr_cyc_on_release(obj); + scr_yin_met(h->id, false); + } else { + scr_yin_met(h->id, true); + scr_cyc_set_color(h, SCR_CYC_DOOMED); + scr_yin_push(&scr_yin_dead, obj); + scr_cyc_type(h)->trace(obj, scr_yin_dead_visit, NULL); + } + } + scr_yin.n = 0; + scr_yin_stat[0]++; + scr_yin_stat[1] += n - scr_yin_dead.n; + scr_yin_stat[2] += scr_yin_dead.n; + size_t freed = scr_yin_dead.n; + for (size_t i = 0; i < scr_yin_dead.n; i++) { + void *obj = scr_yin_dead.v[i]; + scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj); + } + scr_yin_dead.n = 0; + scr_xg_freed = 0; + for (size_t i = 0; i < scr_yin_xrel.n; i++) scr_xg_release(scr_yin_xrel.v[i]); + freed += scr_xg_freed; + scr_yin_xrel.n = 0; + scr_cyc_trim(&scr_yin_stack); + scr_cyc_trim(&scr_yin_dead); + scr_cyc_trim(&scr_yin_xrel); + scr_collecting = false; + scr_yin_busy = false; + /* The candidates buffered above count toward trial deletion's trigger, + * which only a release checks; a program whose releases are all of + * young objects would otherwise never reach it. */ + if (scr_cyc_nbuffered >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger) + scr_cyc_collect_due(); + return freed; +} +#endif + /* One pass over every candidate at or below `gen_limit`; returns how many * objects it freed. */ /* The second half of a pass: settle the white set, promote, re-buffer, @@ -1242,6 +1620,9 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { const ScrCycType *type = scr_cyc_type(h); if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(type, gen_limit); else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE; +#ifdef SCR_YIN + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_leave(h, obj); +#endif type->free_fn(obj); } scr_white.n = 0; @@ -1492,9 +1873,16 @@ size_t scr_cyc_pass_at(unsigned gen) { return scr_cyc_pass(gen); } size_t scr_cyc_collect_set(const ScrCycSet *set, unsigned gen) { if (scr_collecting) return 0; +#ifdef SCR_YIN + /* Young objects are invisible to trial deletion: collect them first, so + * survivors are promoted where the limited pass can walk them. */ + size_t young = scr_yin_collect(); +#else + size_t young = 0; +#endif scr_cyc_take = set; scr_cyc_take_gen = gen; - size_t freed = scr_cyc_pass(SCR_CYC_OLD); + size_t freed = young + scr_cyc_pass(SCR_CYC_OLD); scr_cyc_take = NULL; return freed; } @@ -1508,6 +1896,11 @@ void scr_collect_cycles(void) { * can have buffered anything new, so a pass that comes up empty ends * this; and since each repeat needs a strictly smaller live heap to * continue, it terminates. */ +#ifdef SCR_YIN + /* The nursery first: afterwards no young object is left (a teardown + * never allocates one), so the passes see every object. */ + scr_yin_collect(); +#endif while (scr_cyc_pass(SCR_CYC_OLD) && scr_cyc_buffered()) { } } @@ -1526,4 +1919,22 @@ void scr_cyc_context_cleanup(void) { free(scr_cyc_kinds); scr_cyc_kinds = NULL; scr_cyc_kinds_cap = 0; +#ifdef SCR_YIN + ScrVec *young[] = {&scr_yin_stack, &scr_yin_dead, &scr_yin_xrel}; + for (size_t i = 0; i < sizeof young / sizeof young[0]; i++) { + free(young[i]->v); + *young[i] = (ScrVec){0}; + } + free(scr_yin.v); + scr_yin = (ScrYin){0}; + free(scr_yin_types); + scr_yin_types = NULL; +#ifdef SCR_WORKERS + free(scr_yin_skip_tl); + scr_yin_skip_tl = NULL; +#endif + free(scr_yin_counts); + scr_yin_counts = NULL; + scr_yin_counts_cap = 0; +#endif } diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index d5d83a72a..8de5fa49d 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -524,4 +524,36 @@ SCR_COLD void scr_throw_published_field(const char *prop, const char *owner); bool scr_throw_published_array(int op, const struct ScrArr *a, double index, double n); SCR_COLD void scr_throw_published_collection(bool set); +/* Young-in nursery (scr_cycle.c built with SCR_YIN, linked by programs + * compiled with SCRIPTC_YOUNG_NURSERY=1). A young object's header has + * generation SCR_CYC_YOUNG, above every trial-deletion level, and its + * nursery index in `buf` under SCR_CYC_BUFFERED_BIT, so release paths treat + * it as a buffered candidate (no call on a release that leaves it alive, + * scr_cyc_on_dead at its death). Emitted constructors of compiled classes + * and records call scr_yin_enter on each new object, after storing its + * count; the call may collect the nursery first. */ +#define SCR_CYC_YOUNG 3 +void scr_yin_enter(void *obj); +/* The inline entry's halves: the per-registry-id countdown of allocations + * that skip the nursery (process-wide in executables without workers, + * which are the ones that inline the entry), and the append when the + * nursery has no room. */ +extern uint32_t scr_yin_skip[SCR_CYC_MAX_TYPES]; +void scr_yin_enter_slow(void *obj); +/* The nursery: live young objects v[0..n), capacity cap. Its layout is an + * ABI: emitted constructors (llvm/shapes.ts youngEntryLines) append inline + * while n < lim, writing the header themselves, and call scr_yin_enter + * otherwise; lim stays at most the capacity and the collection size. */ +typedef struct ScrYin { + void **v; + size_t n; + size_t lim; + size_t cap; +} ScrYin; +extern SCR_TL ScrYin scr_yin; +#if UINTPTR_MAX == UINT64_MAX +_Static_assert(offsetof(ScrYin, n) == 8 && offsetof(ScrYin, lim) == 16, + "llvm/shapes.ts appends to the nursery at these offsets"); +#endif + #endif /* SCR_RC_H */ diff --git a/packages/runtime/test/test_yin.c b/packages/runtime/test/test_yin.c new file mode 100644 index 000000000..3b746a435 --- /dev/null +++ b/packages/runtime/test/test_yin.c @@ -0,0 +1,442 @@ +/* The young-in nursery (scr_cycle.c built with SCR_YIN): young objects + * allocated as emitted constructors allocate them, released as emitted + * release paths release them, and collected by nursery collections, trial + * deletion and the full sweep. Every check must hold for any nursery size + * (SCR_YOUNG_NURSERY_SIZE), including a one-object nursery that collects at + * every allocation. */ +#include "../src/scr_runtime.h" + +#include +#include +#include + +typedef struct Plain { + size_t rc; +} Plain; + +typedef struct Obj Obj; +struct Obj { + size_t rc; + Obj *a; + Obj *b; + Plain *plain; /* untraced: released by the teardown, never visited */ +}; + +static size_t freed, plain_freed, disposed; + +static void check(bool condition, const char *message) { + if (condition) return; + fprintf(stderr, "young nursery test failed: %s\n", message); + exit(EXIT_FAILURE); +} + +_Noreturn void scr_trap(const char *msg) { + fputs(msg, stderr); + exit(EXIT_FAILURE); +} + +size_t scr_cyc_pass_at(unsigned gen); + +static void plain_release(Plain *p) { + if (p && --p->rc == 0) { + plain_freed++; + free(p); + } +} + +static void obj_trace(void *o, ScrTraceVisit visit, void *ctx) { + Obj *x = o; + if (x->a) visit(x->a, ctx); + if (x->b) visit(x->b, ctx); +} + +/* The collector's teardown: untraced children only, then the block. */ +static void obj_gcfree(void *o) { + plain_release(((Obj *)o)->plain); + freed++; + scr_cyc_free(o); +} + +static ScrCycType young_type = {&obj_trace, &obj_gcfree, 0, 0}; +static ScrCycType mature_type = {&obj_trace, &obj_gcfree, 0, 0}; + +/* An emitted constructor: zeroed headered block, rc = 1, then the nursery. */ +static Obj *young_new_of(ScrCycType *type) { + Obj *o = scr_cyc_alloc(sizeof(Obj), type); + o->rc = 1; + scr_yin_enter(o); + return o; +} +static Obj *young_new(void) { return young_new_of(&young_type); } + +/* A runtime family that is not young (gen 0, trial deletion only). */ +static Obj *mature_new(void) { + Obj *o = scr_cyc_alloc(sizeof(Obj), &mature_type); + o->rc = 1; + return o; +} + +static void release(Obj *o); +static void destroy(void *p) { + Obj *o = p; + release(o->a); + release(o->b); + plain_release(o->plain); + freed++; + scr_cyc_free(o); +} + +/* An emitted release: the buffered-bit tests are what young objects rely on. */ +static void release(Obj *o) { + if (!o || o->rc == SIZE_MAX) return; + if (--o->rc == 0) { + scr_cyc_dead(o); + scr_rc_destroy(o, destroy); + return; + } + scr_cyc_released(o); +} + +static Obj *retain(Obj *o) { + if (o && o->rc != SIZE_MAX) o->rc++; + return o; +} + +/* h->a or h->b = v (v borrowed): unlink, then release the old value. */ +static void store(Obj *h, int slot, Obj *v) { + retain(v); + Obj **f = slot == 0 ? &h->a : &h->b; + Obj *old = *f; + *f = v; + release(old); +} + +static unsigned gen_of(Obj *o) { return scr_cyc_gen(scr_cyc_hdr(o)); } +static bool buffered(Obj *o) { return scr_cyc_is_buffered(scr_cyc_hdr(o)); } + +static void count_dispose(void *obj) { + (void)obj; + disposed++; +} + +/* Everything collectable is collected and the live count is back. */ +static void settle(size_t live, const char *what) { + scr_collect_cycles(); + if (scr_cyc_live != live) { + fprintf(stderr, "live %zu, expected %zu\n", scr_cyc_live, live); + check(false, what); + } +} + +/* With a nursery of a few objects, a pair's first object is promoted before + * the pair is linked, and the pair then waits for an older level's pass: + * the exact checks need room for the pair. */ +static bool small_nursery(void) { + const char *size = getenv("SCR_YOUNG_NURSERY_SIZE"); + return size != NULL && atol(size) < 4; +} + +static void check_dead_pair_and_live_ring(void) { + size_t live = scr_cyc_live, before = freed; + Obj *a = young_new(), *b = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + check(freed == before, "a young cycle died by reference counting"); + + enum { RING = 10 }; + Obj *head = young_new(), *last = retain(head); + for (int i = 1; i < RING; i++) { + Obj *n = young_new(); + store(last, 0, n); + release(last); + last = n; + } + store(last, 0, head); + release(last); + settle(live + RING, "dead young pair not collected or live ring freed"); + check(freed == before + 2, "dead young pair not freed exactly"); + check(gen_of(head) != SCR_CYC_YOUNG && !buffered(head), "survivor not promoted unbuffered"); + release(head); /* an older release buffers the ring (and may collect it) */ + settle(live, "promoted ring not collected after its last reference"); + check(freed == before + 2 + RING, "promoted ring not freed exactly"); +} + +static void check_rc_deaths_leave_the_nursery(void) { + enum { N = 300 }; + size_t live = scr_cyc_live, before = freed; + Obj *objs[N]; + for (int i = 0; i < N; i++) objs[i] = young_new(); + /* Pairs (i, i+1) for even i; every third object dies by RC first. */ + for (int i = 0; i + 1 < N; i += 2) { + store(objs[i], 0, objs[i + 1]); + store(objs[i + 1], 0, objs[i]); + } + size_t rc_dead = 0; + for (int i = 0; i < N; i += 6) { + /* break the pair, then drop both: plain RC deaths of young objects */ + store(objs[i], 0, NULL); + store(objs[i + 1], 0, NULL); + release(objs[i]); + release(objs[i + 1]); + objs[i] = objs[i + 1] = NULL; + rc_dead += 2; + } + check(freed == before + rc_dead, "young RC deaths not freed at once"); + /* Keep every fourth surviving pair alive from outside; drop the rest. */ + Obj *kept[N]; + size_t nkept = 0, dropped = 0; + for (int i = 0; i < N; i += 2) { + if (!objs[i]) continue; + if ((i / 2) % 4 == 0) kept[nkept++] = objs[i]; + else { + release(objs[i]); + dropped += 2; + } + release(objs[i + 1]); + } + settle(live + 2 * nkept, "young pairs not collected after RC deaths"); + check(freed == before + rc_dead + dropped, "dead pairs not freed exactly"); + for (size_t k = 0; k < nkept; k++) { + check(gen_of(kept[k]) != SCR_CYC_YOUNG, "kept pair not promoted"); + release(kept[k]); + } + settle(live, "kept pairs not collected after release"); +} + +static void check_dead_set_gives_up_outgoing_edges(void) { + size_t live = scr_cyc_live, before = freed, plain_before = plain_freed; + Obj *m = mature_new(); /* held from outside, and by the dead set */ + Obj *s = young_new(); /* survivor held from outside, and by the dead set */ + Obj *d1 = young_new(), *d2 = young_new(); + Plain *p = calloc(1, sizeof *p); + p->rc = 1; + d1->plain = p; + store(d1, 0, d2); + store(d2, 0, d1); + store(d1, 1, m); + store(d2, 1, s); + release(d1); + release(d2); + check(m->rc == 2 && s->rc == 2, "setup counts"); + settle(live + 2, "dead set with outgoing edges not collected"); + check(freed == before + 2, "only the dead pair is freed"); + check(plain_freed == plain_before + 1, "untraced child of the dead set not released"); + check(m->rc == 1 && s->rc == 1, "dead set's outgoing edges not given up exactly once"); + check(gen_of(s) != SCR_CYC_YOUNG, "survivor not promoted"); + release(m); + release(s); + settle(live, "outside objects not freed at their last release"); +} + +static void check_older_holders_keep_young_alive(void) { + size_t live = scr_cyc_live, before = freed; + Obj *m = mature_new(); + Obj *y1 = young_new(), *y2 = young_new(); + store(y1, 0, y2); + store(y2, 0, y1); + store(m, 0, y1); + release(y1); + release(y2); + settle(live + 3, "young cycle held by an older object was freed"); + check(freed == before, "nothing may be freed while the holder lives"); + release(m); /* m dies by RC; its release of y1 buffers the promoted cycle */ + settle(live, "cycle not collected after its older holder died"); + check(freed == before + 3, "holder and cycle not freed exactly"); +} + +static void check_trial_deletion_releases_into_the_nursery(void) { + size_t live = scr_cyc_live, before = freed; + Obj *m1 = mature_new(), *m2 = mature_new(); + Obj *y = young_new(), *z = young_new(); + store(m1, 0, m2); + store(m2, 0, m1); + store(m1, 1, y); /* y's only reference: dies when the white set goes */ + store(m2, 1, z); /* z keeps an outside reference */ + release(y); + release(m1); + release(m2); + /* A trial-deletion pass alone walks the young objects at the nursery + * level: y joins the white set (and leaves the nursery), z survives. */ + scr_cyc_pass_at(SCR_CYC_OLD); + check(freed == before + 3, "white set and its only-held young child not freed"); + check(z->rc == 1, "outside-held young child damaged"); + check(small_nursery() || gen_of(z) == SCR_CYC_YOUNG, "trial deletion promoted a young survivor"); + release(z); + settle(live, "nursery inconsistent after trial deletion freed young objects"); + check(freed == before + 4, "last young object not freed"); + + /* A young cycle held only by a dead older cycle: one pass frees all. */ + before = freed; + m1 = mature_new(); + m2 = mature_new(); + Obj *y1 = young_new(), *y2 = young_new(); + store(y1, 0, y2); + store(y2, 0, y1); + store(m1, 0, m2); + store(m2, 0, m1); + store(m1, 1, y1); + release(y1); + release(y2); + release(m1); + release(m2); + scr_cyc_pass_at(SCR_CYC_OLD); + check(freed == before + 4, "mixed dead structure not freed by one pass"); + settle(live, "nursery inconsistent after a mixed dead structure"); +} + +/* A dead structure whose root died young and that reaches beyond the + * nursery: the nursery collection keeps it (an older object holds a young + * member) and must leave a candidate, or nothing short of a full sweep + * would ever reach it. */ +static void check_dead_structures_beyond_the_nursery(void) { + size_t live = scr_cyc_live, before = freed; + Obj *r = young_new(); + Obj *a = mature_new(); /* a family that is not young: an array, say */ + Obj *n = young_new(); + r->a = a; /* moved in, never released: a is no candidate */ + store(a, 0, n); + store(n, 0, r); + release(n); + release(r); /* the last outside reference, dropped while r is young */ + for (int i = 0; i < 1024 && freed == before; i++) scr_cyc_collect_scheduled(); + check(freed == before + 3, "dead structure reaching beyond the nursery never collected"); + settle(live, "structure beyond the nursery"); +} + +/* A type whose objects keep surviving stops entering the nursery for a + * while, then is met again. */ +static void check_types_that_do_not_die_young(void) { + enum { KEPT = 5000, PROBE = 64 * 1024 }; + static ScrCycType kept_type = {&obj_trace, &obj_gcfree, 0, 0}; + size_t live = scr_cyc_live; + Obj **kept = malloc(KEPT * sizeof *kept); + for (int i = 0; i < KEPT; i++) kept[i] = young_new_of(&kept_type); + scr_collect_cycles(); /* meets them all: none dies */ + Obj *out = young_new_of(&kept_type); + check(gen_of(out) != SCR_CYC_YOUNG && !buffered(out), "surviving type still enters the nursery"); + release(out); + bool met_again = false; + for (int i = 0; i < 2 * PROBE && !met_again; i++) { + Obj *probe = young_new_of(&kept_type); + met_again = gen_of(probe) == SCR_CYC_YOUNG; + release(probe); + } + check(met_again, "surviving type never met again"); + for (int i = 0; i < KEPT; i++) release(kept[i]); + free(kept); + settle(live, "surviving type left objects behind"); +} + +static void check_publication_takes_young_objects(void) { + size_t live = scr_cyc_live; + Obj *y1 = young_new(), *y2 = young_new(), *y3 = young_new(); + /* scr_pub_mark takes an object whose header reads as buffered, and + * records the answer plus one as its position (0: nothing to restore). */ + if (buffered(y1)) { + check((uint32_t)(scr_cyc_pub_take(y1) + 1) == 0 || small_nursery(), "young take recorded"); + scr_cyc_pub_fill(); + } + check(gen_of(y1) != SCR_CYC_YOUNG && !buffered(y1), "published young object not taken"); + y1->rc = SIZE_MAX; /* published: immortal from here on */ + /* y3 took y1's nursery slot: its death and y2's must still find theirs. */ + release(y3); + release(y2); + settle(live + 1, "nursery inconsistent after a publish took a young object"); + /* A refused publish restores nothing for a young object: it stays out of + * the nursery and every buffer, reachable from the refused graph. */ + Obj *y4 = young_new(); + uint32_t recorded = scr_cyc_pub_take(y4) + 1; + check(recorded == 0, "young take recorded a position"); + scr_cyc_pub_fill(); + check(gen_of(y4) != SCR_CYC_YOUNG && !buffered(y4), "refused publish left the object young"); + release(y4); + settle(live + 1, "refused young object not freed"); +} + +static void check_deep_young_structures(void) { + enum { DEPTH = 200000 }; + static ScrCycType deep_type = {&obj_trace, &obj_gcfree, 0, 0}; + size_t live = scr_cyc_live, before = freed; + /* A long live chain (deep marking), and a long dead chain closing a cycle. */ + Obj *keep = young_new_of(&deep_type), *at = retain(keep); + for (int i = 1; i < DEPTH; i++) { + Obj *n = young_new_of(&deep_type); + store(at, 0, n); + release(at); + at = n; + } + release(at); + Obj *head = young_new_of(&deep_type); + at = retain(head); + for (int i = 1; i < DEPTH; i++) { + Obj *n = young_new_of(&deep_type); + store(at, 0, n); + release(at); + at = n; + } + store(at, 0, head); + release(at); + release(head); + settle(live + DEPTH, "deep young structures"); + check(freed == before + DEPTH, "dead chain not freed exactly"); + release(keep); /* an acyclic chain: freed by reference counting */ + check(freed == before + 2 * DEPTH, "live chain not freed at its release"); + settle(live, "deep young structures left objects behind"); +} + +static void check_collection_points_and_limited_passes(void) { + size_t live = scr_cyc_live, before = freed; + if (small_nursery()) return; + Obj *a = young_new(), *b = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + scr_cyc_collect_scheduled(); /* a collection point collects the nursery */ + check(freed == before + 2, "collection point left young garbage"); + a = young_new(); + b = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + void *slots[4] = {0}; + ScrCycSet none = {slots, 3}; + check(scr_cyc_collect_set(&none, SCR_CYC_OLD) >= 2, "limited pass skipped the nursery"); + check(freed == before + 4, "limited pass left young garbage"); + check(scr_cyc_live == live, "limited pass live count"); +} + +static void check_weak_observers_see_young_deaths(void) { + size_t live = scr_cyc_live, before = freed; + disposed = 0; + scr_weak_dispose_hook = count_dispose; + Obj *a = young_new(), *b = young_new(), *c = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + release(c); /* RC death */ + settle(live, "weak observer case"); + scr_weak_dispose_hook = NULL; + check(freed == before + 3 && disposed == 3, "weak dispose hook missed a young death"); +} + +int main(void) { + check_dead_pair_and_live_ring(); + check_rc_deaths_leave_the_nursery(); + check_dead_set_gives_up_outgoing_edges(); + check_older_holders_keep_young_alive(); + check_trial_deletion_releases_into_the_nursery(); + check_dead_structures_beyond_the_nursery(); + check_types_that_do_not_die_young(); + check_publication_takes_young_objects(); + check_deep_young_structures(); + check_collection_points_and_limited_passes(); + check_weak_observers_see_young_deaths(); + const char *size = getenv("SCR_YOUNG_NURSERY_SIZE"); + printf("young nursery checks passed: size=%s\n", size ? size : "default"); + return 0; +} diff --git a/packages/runtime/test/yin.test.ts b/packages/runtime/test/yin.test.ts new file mode 100644 index 000000000..d36e3f087 --- /dev/null +++ b/packages/runtime/test/yin.test.ts @@ -0,0 +1,80 @@ +import { execFile } from "node:child_process"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const testDir = import.meta.dirname; + +// The young-in nursery variant of the collector (SCR_YIN), with the RC +// audit's membership and count checks, at nursery sizes from one object +// (a collection at every allocation) to the default. +test("young-in nursery collects young cycles at every nursery size", async () => { + const buildDir = join(testDir, "build"); + await mkdir(buildDir, { recursive: true }); + const bin = join(buildDir, "test_yin"); + await execFileAsync("clang", [ + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-fsanitize=address,undefined", + "-DSCR_CYC_TESTING", + "-DSCR_YIN", + "-DSCR_RC_AUDIT", + "-o", + bin, + join(testDir, "test_yin.c"), + join(testDir, "../src/scr_cycle.c"), + join(testDir, "../src/scr_alloc.c"), + ]); + const baseEnv = { + ...process.env, + ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1", + UBSAN_OPTIONS: "halt_on_error=1", + }; + delete baseEnv.SCR_YOUNG_NURSERY_SIZE; + delete baseEnv.SCR_CYCLE_THRESHOLD; + for (const [size, threshold] of [ + [undefined, undefined], + ["1", undefined], + ["2", undefined], + ["7", undefined], + ["64", "1"], + ["1", "1"], + ] as const) { + const env = { ...baseEnv }; + if (size !== undefined) env.SCR_YOUNG_NURSERY_SIZE = size; + if (threshold !== undefined) env.SCR_CYCLE_THRESHOLD = threshold; + const run = await execFileAsync(bin, [], { env }); + expect(run.stdout).toBe(`young nursery checks passed: size=${size ?? "default"}\n`); + } +}); + +// Trial deletion is unchanged in the young-nursery build: the whole cycle +// suite passes against it (it allocates no young objects). +test("trial deletion checks pass against the young-nursery collector", async () => { + const buildDir = join(testDir, "build"); + await mkdir(buildDir, { recursive: true }); + const bin = join(buildDir, "test_cycle_yin"); + await execFileAsync("clang", [ + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-fsanitize=address,undefined", + "-DSCR_CYC_TESTING", + "-DSCR_YIN", + "-o", + bin, + join(testDir, "test_cycle.c"), + join(testDir, "../src/scr_cycle.c"), + join(testDir, "../src/scr_alloc.c"), + ]); + const env = { ...process.env, ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1" }; + delete env.SCR_CYCLE_THRESHOLD; + delete env.SCR_CYCLE_GROWTH_CAP; + const run = await execFileAsync(bin, [], { env }); + expect(run.stdout).toBe("cycle collection checks passed: threshold=256 growth_cap=16\n"); +}); diff --git a/tests/corpus/5981-young-nursery-cycles.ts b/tests/corpus/5981-young-nursery-cycles.ts new file mode 100644 index 000000000..caa59b2a4 --- /dev/null +++ b/tests/corpus/5981-young-nursery-cycles.ts @@ -0,0 +1,178 @@ +// Young cycles of classes and records, dropped at once or kept from every +// kind of outside holder: globals, older objects, arrays, maps, closures, +// weak-map values, error objects, generator and async frames. Constructors +// that throw after linking `this` into a cycle leave a partly built object +// reachable from its peer. Churn between the steps runs collections (with +// the young-in nursery, nursery collections; with SCR_YOUNG_NURSERY_SIZE=1, +// one per allocation). Whatever is still reachable keeps its contents; the +// sanitized lane's RC audit fails if anything leaks or is freed early. + +class Pair { + peer: Pair | null = null; + label: string; + n: number; + constructor(label: string, n: number) { + this.label = label; + this.n = n; + } +} + +interface Cell { + value: number; + next: Cell | null; + owner: Pair | null; +} + +class Fragile { + other: Fragile | null = null; + stage = 0; + name: string; + constructor(name: string, peer: Fragile | null, fail: boolean) { + this.name = name; + this.other = peer; + if (peer) peer.other = this; + this.stage = 1; + if (fail) throw new Error(`${name} failed at stage ${this.stage}`); + this.stage = 2; + } +} + +class Holder extends Error { + held: Pair | null = null; +} + +function pair(label: string, n: number): Pair { + const a = new Pair(`${label}.a`, n); + const b = new Pair(`${label}.b`, n + 1); + a.peer = b; + b.peer = a; + return a; +} + +function ring(size: number, base: number): Cell { + const first: Cell = { value: base, next: null, owner: null }; + let last = first; + for (let i = 1; i < size; i++) { + const cell: Cell = { value: base + i, next: null, owner: null }; + last.next = cell; + last = cell; + } + last.next = first; + return first; +} + +function ringSum(start: Cell): number { + let total = start.value; + for (let c = start.next; c !== null && c !== start; c = c.next) total += c.value; + return total; +} + +function churn(rounds: number): number { + let acc = 0; + for (let i = 0; i < rounds; i++) { + acc = (acc + pair("t", i).peer!.n) % 1000003; + acc = (acc + ringSum(ring(5, i))) % 1000003; + } + return acc; +} + +function show(p: Pair | null): string { + return p === null ? "null" : `${p.label}:${p.n}<->${p.peer?.label ?? "none"}`; +} + +// Dropped at once, in bulk. +console.log("churn", churn(20000)); + +// Kept from a global, an older object, an array, a map and a closure. +const kept = pair("global", 1); +const old = new Pair("old", 0); +console.log("churn", churn(5000)); +old.peer = pair("from-old", 2); +const list: Pair[] = []; +const byName = new Map(); +for (let i = 0; i < 50; i++) { + list.push(pair(`list${i}`, i)); + byName.set(`map${i}`, pair(`map${i}`, i)); +} +const captured = pair("closure", 3); +const read = (): string => show(captured.peer); +const cells: Cell[] = []; +for (let i = 0; i < 20; i++) { + const c = ring(4, i * 10); + c.owner = pair(`cell${i}`, i); + cells.push(c); +} +console.log("churn", churn(30000)); +console.log(show(kept), show(kept.peer)); +console.log(show(old.peer), show(old.peer!.peer)); +console.log(show(list[17]!), show(byName.get("map33")!.peer)); +console.log(read()); +console.log(ringSum(cells[7]!), show(cells[7]!.owner), ringSum(cells[19]!.next!)); + +// A weak map whose value refers back to its young key keeps the key while +// the key is held; dropping the key leaves the pair to the collector. +const weak = new WeakMap(); +const key = pair("key", 7); +weak.set(key, key.peer!); +for (let i = 0; i < 200; i++) { + const k = pair(`weak${i}`, i); + weak.set(k, k.peer!); +} +console.log("churn", churn(20000)); +console.log(weak.has(key), show(weak.get(key) ?? null)); + +// Error objects (runtime-rooted classes) holding young cycles. +const errors: Holder[] = []; +for (let i = 0; i < 10; i++) { + const e = new Holder(`holder ${i}`); + e.held = pair(`err${i}`, i); + errors.push(e); +} +console.log("churn", churn(10000)); +console.log(errors[4]!.message, show(errors[4]!.held)); + +// Constructors that throw after linking into a cycle. +const survivors: Fragile[] = []; +for (let i = 0; i < 100; i++) { + const peer = new Fragile(`peer${i}`, null, false); + try { + new Fragile(`victim${i}`, peer, i % 3 === 0); + } catch (e) { + if (i < 6) console.log((e as Error).message); + } + if (i % 10 === 0) survivors.push(peer); +} +console.log("churn", churn(10000)); +for (const s of survivors.slice(0, 4)) + console.log(s.name, s.stage, s.other?.name, s.other?.stage, s.other?.other === s); + +// Generator frames hold young objects across collections. +function* pairs(count: number): Generator { + for (let i = 0; i < count; i++) { + const p = pair(`gen${i}`, i); + churn(500); + yield p; + } +} +let genTotal = 0; +for (const p of pairs(30)) genTotal += p.peer!.n; +console.log("generator", genTotal); + +// Async frames hold young objects across awaits. +async function hold(n: number): Promise { + const a = pair(`async${n}`, n); + const cell = ring(3, n); + cell.owner = a; + await null; + churn(3000); + await new Promise((resolve) => setTimeout(resolve, 1)); + churn(3000); + return `${show(a)} ${show(cell.owner)} ${ringSum(cell)}`; +} +async function main(): Promise { + const pending: Promise[] = [hold(1), hold(2), hold(3)]; + for (const p of pending) console.log(await p); + console.log("churn", churn(10000)); + console.log(show(kept), show(list[49]!), read()); +} +void main(); diff --git a/tests/corpus/5982-young-nursery-workers.ts b/tests/corpus/5982-young-nursery-workers.ts new file mode 100644 index 000000000..64c3b30f5 --- /dev/null +++ b/tests/corpus/5982-young-nursery-workers.ts @@ -0,0 +1,81 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; +import { publish } from "@scriptc/threads"; + +// Every thread builds and drops young cycles of its own while it publishes +// graphs of freshly built objects and posts them. Publishing takes young +// objects out of the thread's nursery before they become shared, so the +// receiving thread reads them intact while both threads keep collecting. +// Receivers publish() what they receive (a no-op in scriptc) and read only +// data, so Node.js prints the same. + +class Node { + parent: Node | null = null; + next: Node | null = null; + children: Node[] = []; + label: string; + constructor(label: string) { + this.label = label; + } +} + +function graph(prefix: string, width: number): Node { + const root = new Node(`${prefix}root`); + let last: Node | null = null; + for (let i = 0; i < width; i++) { + const child = new Node(`${prefix}${i}`); + child.parent = root; + if (last !== null) last.next = child; + last = child; + root.children.push(child); + } + if (last !== null) last.next = root.children[0]!; + return root; +} + +function churn(rounds: number): number { + let acc = 0; + for (let i = 0; i < rounds; i++) { + const a = new Node(`a${i}`); + const b = new Node(`b${i}`); + a.next = b; + b.next = a; + acc = (acc + a.next.label.length + b.next.label.length) % 1000003; + } + return acc; +} + +function describe(root: Node): string { + let out = root.label; + for (const c of root.children) out += ` ${c.label}^${c.parent?.label}>${c.next?.label}`; + return out; +} + +if (isMainThread) { + const replies: string[] = []; + let pending = 3; + for (let w = 0; w < 3; w++) { + const shared = publish(graph(`m${w}.`, 3)); + const worker = new Worker(new URL(import.meta.url), { workerData: { id: w, shared } }); + worker.on("message", (message: unknown) => { + const received = message as { id: number; graph: Node; churned: number }; + publish(received.graph); + replies[w] = `worker ${received.id}: ${describe(received.graph)} churn=${received.churned}`; + }); + worker.on("exit", () => { + if (--pending === 0) { + for (const line of replies) console.log(line); + console.log(`main churn ${churn(20000)}`); + } + }); + } + console.log(`main churn ${churn(20000)}`); +} else { + const { id, shared } = workerData as { id: number; shared: Node }; + publish(shared); + const churned = churn(30000); + const fresh = graph(`w${id}.`, 4); + fresh.children[1]!.children.push(new Node(`w${id}.leaf`)); + publish(fresh); + churn(10000); + parentPort!.postMessage({ id, graph: fresh, churned: (churned + describe(shared).length) % 1000003 }); +} From 2ae1f72d0bc9613d30325db8f7ecf7e8e4b69b97 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:13:57 -0700 Subject: [PATCH 020/120] Read self-capturing closures through selfRef instead of their box `const visit = (node) => { ... visit(child) ... }` lowered to a closure over the box of `visit`, and the box then held the closure: a two-object reference cycle that only the cycle collector could free, along with everything the closure captured. Nested function declarations already read their own name through selfRef; arrows and function expressions stored into a binding now do too, when the IR proves the binding holds the closure itself whenever the closure can run. resolveSelfCaptures runs with the other backend IR passes, before copyStableCaptures. A capture qualifies when every creation site stores the new closure, unadapted and with the binding's own type, into a boxed local of the creating function (its only declaration's initializer, or the only assignment after an empty declaration in the same statement list), nothing else writes the binding in any body that shares its box, and the closure body is synchronous, not a generator, and does not hand the binding to a nested closure. The capture then leaves the body and every creation site; its reads become selfRef. A creating binding that no other closure captures loses its box, except dead-zone bindings. Self-hosted compiler (native, compiling its CLI to IR): the cycle collector frees 14.5M objects instead of 22.1M; closures that held a box containing themselves drop from 3.5M to 54k. Of the runtime suite and the cg-* cycle workloads, only published-graphs compiles differently (-1.3%, 31 interleaved runs). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/emitter.ts | 9 +- .../compiler/src/ir/self-captures.test.ts | 206 ++++++++++++++++ packages/compiler/src/ir/self-captures.ts | 229 ++++++++++++++++++ .../5990-self-capturing-closures.ts.json | 6 + tests/corpus/5990-self-capturing-closures.ts | 124 ++++++++++ 5 files changed, 572 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/src/ir/self-captures.test.ts create mode 100644 packages/compiler/src/ir/self-captures.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-self-capturing-closures.ts.json create mode 100644 tests/corpus/5990-self-capturing-closures.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2c3a71457..126ff36e3 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -100,6 +100,7 @@ import { } from "../../ir/integer-ranges.js"; import { unshareNodes } from "../../ir/unshare.js"; import { copyStableCaptures } from "../../ir/capture-values.js"; +import { resolveSelfCaptures } from "../../ir/self-captures.js"; import { analyzeInt32Slots, dynamicFieldStores, type Int32Slots } from "../../ir/int32-slots.js"; import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js"; import { foldConstantGlobals } from "../../ir/constant-globals.js"; @@ -486,7 +487,9 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}) specializeNumericCalls( foldConstantGlobals( copyStableCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), mangleFunction, @@ -514,7 +517,9 @@ export function emitLlvmModuleSource( specializeNumericCalls( foldConstantGlobals( copyStableCaptures( - hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + resolveSelfCaptures( + hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))), + ), ), ), mangleFunction, diff --git a/packages/compiler/src/ir/self-captures.test.ts b/packages/compiler/src/ir/self-captures.test.ts new file mode 100644 index 000000000..bd03725aa --- /dev/null +++ b/packages/compiler/src/ir/self-captures.test.ts @@ -0,0 +1,206 @@ +import { expect, test } from "vitest"; +import { + F64, + funcOf, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { resolveSelfCaptures } from "./self-captures.js"; + +const loc = { file: "self.ts", start: 0, end: 0 }; +const FN = funcOf([], F64); +const ref = (localId: string, type = FN): IrExpr => ({ kind: "varRef", localId, type, loc }); +const call = (localId: string): IrExpr => ({ + kind: "callValue", + callee: ref(localId), + args: [], + type: F64, + loc, +}); +const self: IrExpr = { kind: "selfRef", type: FN, loc }; +const closure = (fnName: string, captures: string[], extra: object = {}): IrExpr => ({ + kind: "closure", + fnName, + captures, + type: FN, + loc, + ...extra, +}); +const decl = (localId: string, init: IrExpr | null): IrStmt => ({ + kind: "varDecl", + localId, + init, + loc, +}); +const assign = (localId: string, value: IrExpr, initializes = false): IrStmt => ({ + kind: "assign", + localId, + value, + ...(initializes ? { initializes: true as const } : {}), + loc, +}); +const ret = (e: IrExpr): IrStmt => ({ kind: "return", value: e, loc }); +const use = (e: IrExpr): IrStmt => ({ kind: "exprStmt", expr: e, loc }); +const local = (id: string, extra: Partial = {}): IrLocal => ({ + id, + name: id.split(".")[0]!, + type: FN, + mutable: false, + boxed: true, + ...extra, +}); + +function outer(body: IrStmt[], locals: IrLocal[]): IrFunction { + return { name: "outer", params: [], returnType: F64, locals, body, loc }; +} +function lifted(name: string, body: IrStmt[], captured: IrLocal[]): IrFunction { + return { + name, + params: [], + returnType: F64, + captures: captured.map((l) => ({ localId: l.id, name: l.name, type: l.type })), + locals: captured, + body, + loc, + }; +} +function run(...functions: IrFunction[]): Map { + const mod = { functions } as unknown as IrModule; + return new Map(resolveSelfCaptures(mod).functions.map((fn) => [fn.name, fn])); +} + +test("a closure stored in the dead-zone binding it captures reads itself", () => { + const fns = run( + outer( + [decl("f.0", null), assign("f.0", closure("walk", ["f.0"]), true), ret(call("f.0"))], + [local("f.0", { mutable: true, tdz: true })], + ), + lifted("walk", [ret(call("f.0"))], [local("f.0", { tdz: true })]), + ); + const walk = fns.get("walk")!; + expect(walk.captures).toEqual([]); + expect(walk.locals).toEqual([]); + expect(walk.body).toEqual([ret({ kind: "callValue", callee: self, args: [], type: F64, loc })]); + const origin = fns.get("outer")!; + expect(origin.body[1]).toEqual(assign("f.0", closure("walk", []), true)); + // The dead-zone box still represents the binding's uninitialized state. + expect(origin.locals[0]).toMatchObject({ boxed: true, tdz: true }); +}); + +test("a binding that only held the closure loses its box", () => { + const n = local("n.0", { type: F64 }); + const fns = run( + outer( + [ + decl("n.0", { kind: "numLit", value: 1, type: F64, loc }), + decl("f.0", closure("walk", ["n.0", "f.0"])), + ret(call("f.0")), + ], + [n, local("f.0")], + ), + lifted("walk", [use(ref("n.0", F64)), ret(call("f.0"))], [n, local("f.0")]), + ); + expect(fns.get("walk")!.captures!.map((c) => c.localId)).toEqual(["n.0"]); + const origin = fns.get("outer")!; + expect(origin.body[1]).toEqual(decl("f.0", closure("walk", ["n.0"]))); + expect(origin.locals.find((l) => l.id === "f.0")!.boxed).toBeUndefined(); + expect(origin.locals.find((l) => l.id === "n.0")!.boxed).toBe(true); +}); + +test("hoisted declarations qualify; later writes and second sites do not", () => { + const body = (extra: IrStmt[]): IrStmt[] => [ + decl("f.0", null), + assign("f.0", closure("walk", ["f.0"])), + ...extra, + ret(call("f.0")), + ]; + const walk = lifted("walk", [ret(call("f.0"))], [local("f.0")]); + const hoisted = run(outer(body([]), [local("f.0", { mutable: true })]), walk); + expect(hoisted.get("walk")!.captures).toEqual([]); + expect(hoisted.get("outer")!.locals[0]!.boxed).toBeUndefined(); + + const rewritten = run( + outer(body([assign("f.0", closure("other", []))]), [local("f.0", { mutable: true })]), + walk, + lifted("other", [ret({ kind: "numLit", value: 0, type: F64, loc })], []), + ); + expect(rewritten.get("walk")).toBe(walk); + + // The same body stored somewhere else: its environment must keep the box. + const elsewhere = run( + outer( + [...body([]).slice(0, 2), use(closure("walk", ["f.0"])), ret(call("f.0"))], + [local("f.0", { mutable: true })], + ), + walk, + ); + expect(elsewhere.get("walk")).toBe(walk); +}); + +test("an assignment in another statement list, an adapter or a nested capture keeps the box", () => { + const walk = lifted("walk", [ret(call("f.0"))], [local("f.0")]); + const branch = run( + outer( + [ + decl("f.0", null), + { kind: "block", body: [assign("f.0", closure("walk", ["f.0"]))], loc }, + ret(call("f.0")), + ], + [local("f.0", { mutable: true })], + ), + walk, + ); + expect(branch.get("walk")).toBe(walk); + + const adapter = run( + outer( + [decl("f.0", closure("walk", ["f.0"], { adapts: true })), ret(call("f.0"))], + [local("f.0")], + ), + walk, + ); + expect(adapter.get("walk")).toBe(walk); + + const nested = lifted("walk", [use(closure("inner", ["f.0"])), ret(call("f.0"))], [local("f.0")]); + const recaptured = run( + outer([decl("f.0", closure("walk", ["f.0"])), ret(call("f.0"))], [local("f.0")]), + nested, + lifted("inner", [ret(call("f.1"))], [local("f.1")]), + ); + expect(recaptured.get("walk")).toBe(nested); +}); + +test("mutually recursive closures only drop their own capture", () => { + const fns = run( + outer( + [ + decl("a.0", null), + decl("b.0", null), + assign("a.0", closure("fa", ["a.0", "b.0"]), true), + assign("b.0", closure("fb", ["a.0", "b.0"]), true), + ret(call("a.0")), + ], + [local("a.0", { mutable: true, tdz: true }), local("b.0", { mutable: true, tdz: true })], + ), + lifted( + "fa", + [use(call("b.0")), ret(call("a.0"))], + [local("a.0", { tdz: true }), local("b.0", { tdz: true })], + ), + lifted( + "fb", + [use(call("a.0")), ret(call("b.0"))], + [local("a.0", { tdz: true }), local("b.0", { tdz: true })], + ), + ); + expect(fns.get("fa")!.captures!.map((c) => c.localId)).toEqual(["b.0"]); + expect(fns.get("fb")!.captures!.map((c) => c.localId)).toEqual(["a.0"]); + expect(fns.get("outer")!.body.slice(2)).toEqual([ + assign("a.0", closure("fa", ["b.0"]), true), + assign("b.0", closure("fb", ["a.0"]), true), + ret(call("a.0")), + ]); +}); diff --git a/packages/compiler/src/ir/self-captures.ts b/packages/compiler/src/ir/self-captures.ts new file mode 100644 index 000000000..e009e9a11 --- /dev/null +++ b/packages/compiler/src/ir/self-captures.ts @@ -0,0 +1,229 @@ +import { CaptureFlow } from "./capture-values.js"; +import { + typeEquals, + type IrExpr, + type IrFunction, + type IrLocal, + type IrModule, + type IrStmt, +} from "./ir.js"; +import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js"; + +/** Closures that capture the binding they are stored in read themselves + * through `selfRef` instead of through that binding's box. + * + * `const visit = (node) => { ... visit(child) ... }` lowers to a closure + * over the box of `visit`, and the box then holds the closure: a two-object + * reference cycle that reference counting cannot free. Each closure made + * this way is buffered as a cycle candidate and reclaimed only when a + * collector pass walks it. Nested function declarations already read their + * own name through `selfRef`; this pass gives arrows and function + * expressions bound to a binding the same form once the IR proves that the + * binding always holds the closure itself while the closure can run: + * + * - every creation site of the closure body stores the new closure, as is, + * into a boxed local of the creating function: either the initializer of + * that local's only declaration, or the only assignment of a local whose + * only declaration is empty and precedes the assignment in the same + * statement list (so each box is filled once, and a closure can only run + * after the statement that stores it); + * - nothing else writes the binding, in the creating function or in any + * body that shares its box (CaptureFlow.unchanged), and no local class + * captures it; + * - the closure body is synchronous, is not a generator, and does not pass + * the binding on to a nested closure (its environment would need a box of + * its own, allocated on every call). + * + * The capture then disappears from the body and from every creation site, + * and each read of it becomes `selfRef` (the running closure, retained), + * which is the same pointer the box would have produced. The creating + * function keeps its binding; it loses its box when no other closure + * captures it, except for temporal-dead-zone bindings, whose box still + * carries the empty state its own reads test. */ +export function resolveSelfCaptures(mod: IrModule): IrModule { + const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); + const sites = new Map(); + /** Closure nodes stored into a local by a qualifying definition, with that + * local and the number of writes the definition itself makes. */ + const definitions = new Map(); + const declarations = new Map>(); + for (const fn of mod.functions) { + const counts = new Map(); + declarations.set(fn.name, counts); + const expr = (e: IrExpr): boolean => { + if (e.kind === "closure") { + let list = sites.get(e.fnName); + if (list === undefined) sites.set(e.fnName, (list = [])); + list.push({ owner: fn.name, closure: e }); + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if (s.kind === "varDecl") counts.set(s.localId, (counts.get(s.localId) ?? 0) + 1); + return everyStmtChild(s, expr, stmt); + }; + fn.body.every(stmt); + for (const list of statementLists(fn.body)) { + list.forEach((s, i) => { + if (s.kind !== "varDecl") return; + if (s.init !== null) { + if (s.init.kind === "closure") definitions.set(s.init, { local: s.localId, writes: 0 }); + return; + } + for (let j = i + 1; j < list.length; j++) { + const next = list[j]!; + if (next.kind === "assign" && next.localId === s.localId) { + if (next.value.kind === "closure") + definitions.set(next.value, { local: s.localId, writes: 1 }); + return; + } + } + }); + } + } + + let flow: CaptureFlow | null = null; + /** Per closure body, the capture index that names its own closure. */ + const resolved = new Map(); + for (const fn of mod.functions) { + const captures = fn.captures; + if (captures === undefined || captures.length === 0) continue; + if (fn.async === true || fn.generator !== undefined || fn.classCaptures !== undefined) continue; + const list = sites.get(fn.name); + if (list === undefined || list.length === 0) continue; + const recaptured = capturedIds(fn.body); + for (let index = 0; index < captures.length; index++) { + const capture = captures[index]!; + if (capture.type.kind !== "func" || recaptured.has(capture.localId)) continue; + const qualifies = list.every(({ owner, closure }) => { + if (closure.adapts === true || !typeEquals(closure.type, capture.type)) return false; + const definition = definitions.get(closure); + const source = closure.captures[index]; + if (definition === undefined || source !== definition.local) return false; + const creator = functions.get(owner)!; + const local = creator.locals.find((l) => l.id === source); + if (local?.boxed !== true || !typeEquals(local.type, closure.type)) return false; + if (creator.params.some((p) => p.localId === source)) return false; + if (creator.captures?.some((c) => c.localId === source)) return false; + if (creator.classCaptures?.some((c) => c.localId === source)) return false; + if (declarations.get(owner)!.get(source) !== 1) return false; + flow ??= new CaptureFlow(mod.functions); + return flow.unchanged(owner, source, definition.writes); + }); + if (qualifies) { + resolved.set(fn.name, index); + break; + } + } + } + if (resolved.size === 0) return mod; + + const rewritten = mod.functions.map((fn) => { + const own = resolved.get(fn.name); + const self = own === undefined ? null : fn.captures![own]!; + let changed = false; + const expr = (e: IrExpr): IrExpr => { + if (self !== null && e.kind === "varRef" && e.localId === self.localId) { + changed = true; + return { kind: "selfRef", type: self.type, loc: e.loc }; + } + if (e.kind === "closure") { + const drop = resolved.get(e.fnName); + if (drop !== undefined) { + changed = true; + return { ...e, captures: e.captures.filter((_, i) => i !== drop) }; + } + } + return mapExprChildren(e, expr, stmt); + }; + const stmt = (s: IrStmt): IrStmt => mapStmtChildren(s, expr, stmt); + const body = fn.body.map(stmt); + if (!changed && self === null) return fn; + // A binding no environment captures any more needs no box. A dead-zone + // binding keeps the box that represents its uninitialized state. + const stillCaptured = capturedIds(body); + // The callback returns IrLocal explicitly: inferred from both returns, + // its type would be the unboxed copy's, and the self-hosted compiler + // would narrow every kept local to that shape, dropping `boxed`. + const locals = fn.locals + .filter((local) => self === null || local.id !== self.localId) + .map((local): IrLocal => { + if (local.boxed !== true || local.tdz === true || stillCaptured.has(local.id)) return local; + if (fn.captures?.some((c) => c.localId === local.id)) return local; + if (fn.classCaptures?.some((c) => c.localId === local.id)) return local; + if (!wasSource(fn, local.id, resolved)) return local; + const { boxed: _boxed, ...plain } = local; + return plain; + }); + const next: IrFunction = { ...fn, locals, body }; + if (self !== null) next.captures = fn.captures!.filter((_, i) => i !== own); + return next; + }); + return { ...mod, functions: rewritten }; +} + +/** Whether `fn` stored a resolved closure into `id`, the only case in which + * this pass may unbox a binding (others stay exactly as lowered). */ +function wasSource(fn: IrFunction, id: string, resolved: ReadonlyMap): boolean { + let found = false; + const expr = (e: IrExpr): boolean => { + if (e.kind === "closure") { + const index = resolved.get(e.fnName); + if (index !== undefined && e.captures[index] === id) found = true; + } + return !found && everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt); + fn.body.every(stmt); + return found; +} + +/** Locals some closure or class created in `body` captures. */ +function capturedIds(body: readonly IrStmt[]): Set { + const ids = new Set(); + const expr = (e: IrExpr): boolean => { + if (e.kind === "closure" || e.kind === "classRef") + for (const id of e.captures ?? []) ids.add(id); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt); + body.every(stmt); + return ids; +} + +/** `body` and every statement list nested in its statements (not the ones + * inside expressions). Statements of one list run in order, each at most + * once per execution of the list. */ +function statementLists(body: readonly IrStmt[]): (readonly IrStmt[])[] { + const lists: (readonly IrStmt[])[] = []; + const visit = (list: readonly IrStmt[]): void => { + lists.push(list); + for (const s of list) { + switch (s.kind) { + case "block": + case "while": + case "doWhile": + case "for": + case "forOf": + visit(s.body); + break; + case "if": + visit(s.then); + if (s.else_ !== null) visit(s.else_); + break; + case "switch": + for (const c of s.cases) visit(c.body); + break; + case "tryCatch": + visit(s.tryBody); + if (s.catchBody !== null) visit(s.catchBody); + if (s.finallyBody !== null) visit(s.finallyBody); + break; + default: + break; + } + } + }; + visit(body); + return lists; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-self-capturing-closures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-self-capturing-closures.ts.json new file mode 100644 index 000000000..5f707aaf2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-self-capturing-closures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5990-self-capturing-closures.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5990-self-capturing-closures.ts b/tests/corpus/5990-self-capturing-closures.ts new file mode 100644 index 000000000..274e7b8d7 --- /dev/null +++ b/tests/corpus/5990-self-capturing-closures.ts @@ -0,0 +1,124 @@ +// Closures that call themselves through the binding they are stored in: +// const arrows, function expressions, closures made in loops, closures that +// also hand themselves to nested callbacks, and a binding that is later +// rebound (its closure must see the new value). Each read of the binding +// inside the body is the same function object the binding holds. The +// sanitized lane's RC audit fails if any of these closures leaks or is +// freed while reachable. + +const fact = (n: number): number => (n <= 1 ? 1 : n * fact(n - 1)); +console.log(fact(10)); + +const pick = (n: number): (() => number) | null => (n === 0 ? null : () => n); +const identity = (depth: number): unknown => (depth === 0 ? identity : identity(depth - 1)); +console.log(identity(3) === identity, pick(0) === null, pick(2)!()); + +const fib = function (n: number): number { + return n < 2 ? n : fib(n - 1) + fib(n - 2); +}; +console.log(fib(15)); + +const outerName = function innerName(n: number): number { + return n <= 0 ? 0 : 1 + outerName(n - 1) + innerName(0); +}; +console.log(outerName(5)); + +// One closure per iteration, each recursing into itself and reading its +// own iteration's value. +const perIteration: ((n: number) => number)[] = []; +for (let i = 0; i < 4; i++) { + const step = (n: number): number => (n === 0 ? i : step(n - 1) + 10); + perIteration.push(step); +} +console.log(perIteration.map((f) => f(2)).join(" ")); +console.log(perIteration[0] === perIteration[1], perIteration[2] === perIteration[2]); + +// The closure also passes itself to nested callbacks. +interface Tree { + value: number; + kids: Tree[]; +} +const leaf = (value: number): Tree => ({ value, kids: [] }); +const tree: Tree = { + value: 1, + kids: [{ value: 2, kids: [leaf(3), leaf(4)] }, leaf(5)], +}; +const sum = (node: Tree): number => node.value + node.kids.map((kid) => sum(kid)).reduce((a, b) => a + b, 0); +console.log(sum(tree)); +const visited: number[] = []; +const walk = (node: Tree): void => { + visited.push(node.value); + node.kids.forEach(walk); +}; +walk(tree); +console.log(visited.join(",")); + +// Captured state next to the self reference. +function counterTo(limit: number): () => number { + let calls = 0; + const tick = (): number => { + calls++; + return calls < limit ? tick() : calls; + }; + return tick; +} +const tick = counterTo(5); +console.log(tick(), tick()); + +// A rebound binding: the first closure calls whatever the binding holds now. +let rebound = (n: number): number => (n <= 0 ? 0 : 1 + rebound(n - 1)); +const first = rebound; +console.log(first(3)); +rebound = (n: number): number => n * 100; +console.log(first(3), rebound(3)); + +// Mutual recursion keeps both bindings live. +const isEven = (n: number): boolean => (n === 0 ? true : isOdd(n - 1)); +const isOdd = (n: number): boolean => (n === 0 ? false : isEven(n - 1)); +console.log(isEven(10), isOdd(7), isEven(3)); + +// Identity as a map key from inside the body. +const seen = new Map(); +const keyed = (n: number): number => { + seen.set(keyed, (seen.get(keyed) ?? 0) + 1); + return n === 0 ? seen.size : keyed(n - 1); +}; +console.log(keyed(4), seen.get(keyed)); + +// An object that holds a closure capturing the object (a real cycle). +class Widget { + label: string; + onChange: (() => string) | null = null; + constructor(label: string) { + this.label = label; + } +} +function widgets(count: number): number { + let total = 0; + for (let i = 0; i < count; i++) { + const widget = new Widget(`w${i}`); + const describe = (depth: number): string => (depth === 0 ? widget.label : describe(depth - 1)); + widget.onChange = () => describe(2); + total += widget.onChange().length; + } + return total; +} +console.log(widgets(1000)); + +// Many short-lived recursive closures. +let checksum = 0; +for (let round = 0; round < 20000; round++) { + const down = (n: number): number => (n === 0 ? round % 7 : down(n - 1)); + checksum += down(3); +} +console.log(checksum); + +// Generators and async functions that name themselves. +const countdown = function* (n: number): Generator { + if (n < 0) return; + yield n; + yield* countdown(n - 1); +}; +console.log([...countdown(3)].join("")); +const later = async (n: number): Promise => (n === 0 ? 0 : 1 + (await later(n - 1))); +later(3).then((value) => console.log("async", value)); From 989d0d6bc2d03ad2202c974143533b3d033c8bb6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 09:12:14 -0700 Subject: [PATCH 021/120] Buffer nursery cycle candidates on the release fast paths Every release that left a headered object alive and unbuffered called scr_cyc_on_release, which looked up the type's bypass statistics, pushed the object, kept a running total and compared it with two triggers. The nursery case now has a fast path that appends the object to the nursery buffer itself, while the runtime-maintained window says the append can neither make a collection due nor grow the buffer, and the live count is below the allocation trigger: - scr_rc.h exports the candidate buffers (scr_cyc_buffers: the three buffers, the window and the allocation trigger) where the collector state is ordinary globals, and scr_cyc_append. The runtime's own release paths (scr_cyc_released) append inline, and scr_cyc_on_release tries the append first. - Optimized emitted code buffers through one sc_cyc_append helper per module, called with preserve_mostcc, so release fast paths keep the size of the plain call they had (inlining the append into every release fast path grew the self-hosted compiler's text by 4.6%) and their callers keep their registers. Its runtime call goes through a second preserve_mostcc function, which keeps the register saves off the append path. - scr_cycle.c keeps the window: recomputed where buffers, capacity or the trigger change in bulk, lowered by one on older buffering and nursery deaths, 0 during passes. The running total is gone; the slow path sums the buffers. - An appended candidate whose type bypasses the nursery would have been buffered past it. scr_cyc_route_bypassed moves such candidates before any scheduling decision or pass reads the buffers, consuming the bypass counts as scr_cyc_on_release does (skipped while no type bypasses). test_cycle checks the window (the release that reaches the trigger still collects) and the routing of inline-buffered candidates of a bypassed type; its mature-bypass probe routes before reading generations. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/alloc.ts | 91 ++++++++++ .../compiler/src/backend/llvm/shapes.test.ts | 37 ++++ packages/compiler/src/backend/llvm/shapes.ts | 57 +++++- packages/runtime/src/scr_cycle.c | 167 ++++++++++++++---- packages/runtime/src/scr_rc.h | 68 ++++++- packages/runtime/test/test_cycle.c | 74 +++++++- 6 files changed, 450 insertions(+), 44 deletions(-) diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index 7dfc385aa..72631ba1c 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -141,6 +141,97 @@ function declareGlobal(host: AllocHost, decl: string): void { if (host.threadLocalAlloc !== true) host.declare(decl); } +/** `scr_cyc_buffers` (scr_rc.h ScrCycBuffers): the three candidate buffers + * `{ v, n, cap, used }`, the inline buffering window and the allocation + * trigger. */ +const CYC_BUFFERS_TYPE = "{ [3 x { ptr, i64, i64, i64 }], i64, i64 }"; +/** Byte offsets in it (asserted in scr_rc.h). */ +const CYC_NURSERY_COUNT = 8; +const CYC_WINDOW = 96; +const CYC_ALLOC_TRIGGER = 104; + +/** Whether emitted release paths can append nursery candidates to the + * collector's buffer themselves (through {@link cycleAppendHelper}): where + * the runtime keeps that state in ordinary globals (executables without + * workers and classic libraries, on the 64-bit targets with inline + * allocation). */ +export function inlineBufferingSupported(host: AllocHost): boolean { + return host.inlineAlloc === true && host.threadLocalAlloc !== true && host.sizeType === "i64"; +} + +/** The emitted symbol of {@link cycleAppendHelper}. */ +export const CYC_APPEND = "sc_cyc_append"; + +/** The runtime symbols {@link cycleAppendHelper} uses. The helper is + * defined after the module's extern block, so its caller declares them. */ +export function cycleAppendDecls(): string[] { + return [ + `@scr_cyc_buffers = external global ${CYC_BUFFERS_TYPE}`, + `@scr_cyc_live = external global i64`, + `declare void @scr_cyc_on_release(ptr)`, + ]; +} + +/** The candidate-buffering step of every emitted release that leaves a + * headered object alive and unbuffered: scr_rc.h's scr_cyc_append, then + * scr_cyc_on_release when it declines. A nursery object goes onto the + * nursery buffer while the buffer holds fewer entries than the runtime's + * window and the live count is below the allocation trigger. + * + * One out-of-line definition per module keeps the release fast paths, which + * LLVM inlines into their callers, as small as the plain call they had. It + * uses preserve_mostcc, so the call clobbers almost no caller registers: + * the callers' code around a release stays what it was, and the append + * itself saves only the few registers it uses. Its slow path enters the + * runtime through a second preserve_mostcc function, which saves the rest. */ +export function cycleAppendHelper(): string[] { + return [ + `define internal preserve_mostcc void @${CYC_APPEND}(ptr %o) noinline #0 { ; scr_cyc_append, else scr_cyc_on_release`, + `entry:`, + ` %genp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_GEN}`, + ` %gen = load i8, ptr %genp ; ScrCycHdr.gen`, + ` %young = icmp eq i8 %gen, 0`, + ` br i1 %young, label %window, label %call`, + `window:`, + ` %np = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_NURSERY_COUNT}`, + ` %n = load i64, ptr %np ; nursery buffer entries`, + ` %wp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_WINDOW}`, + ` %w = load i64, ptr %wp ; window`, + ` %room = icmp ult i64 %n, %w`, + ` br i1 %room, label %live, label %call`, + `live:`, + ` %lv = load i64, ptr @scr_cyc_live`, + ` %tp = getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 ${CYC_ALLOC_TRIGGER}`, + ` %t = load i64, ptr %tp ; allocation trigger`, + ` %due = icmp uge i64 %lv, %t`, + ` br i1 %due, label %call, label %append`, + `append:`, + ` %v = load ptr, ptr @scr_cyc_buffers`, + ` %slot = getelementptr inbounds ptr, ptr %v, i64 %n`, + ` store ptr %o, ptr %slot`, + ` %n1 = add i64 %n, 1`, + ` store i64 %n1, ptr %np`, + ` %i = trunc i64 %n to i32`, + ` %b = or i32 %i, -2147483648 ; SCR_CYC_BUFFERED_BIT | position`, + ` %bufp = getelementptr i8, ptr %o, i64 -${CYC_HDR - CYC_BUF}`, + ` store i32 %b, ptr %bufp`, + ` ret void`, + `call:`, + ` tail call preserve_mostcc void @${CYC_APPEND}_call(ptr %o)`, + ` ret void`, + `}`, + // The runtime call clobbers every caller-saved register, which this + // preserve_mostcc function must restore: saving them here keeps those + // saves out of the append path above. + `define internal preserve_mostcc void @${CYC_APPEND}_call(ptr %o) noinline #0 {`, + `entry:`, + ` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`, + ` ret void`, + `}`, + ``, + ]; +} + /** A collector header to install: the object type's constant ScrCycType * descriptor (its trace and teardown; see cycTypeDescriptor). */ export interface CycHeader { diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index 8c2c46541..f25dca3a3 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -205,6 +205,43 @@ describe("LLVM runtime RC symbols", () => { expect(fn("sc_rc_retain_promise")).not.toContain("store i32"); }); + test("optimized builds buffer candidates through the module's append helper", () => { + const emit = (inlineRc: true | "release", extra: Partial) => { + const { host } = declarationHost(64, inlineRc); + const configured: ShapeHost = { ...host, inlineAlloc: true, ...extra }; + releaseSym(configured, { kind: "dyn" }); + return { + ir: emitInlineRcHelpers(configured).join("\n"), + decls: inlineRcDecls(configured), + }; + }; + for (const mode of [true, "release"] as const) { + const { ir, decls } = emit(mode, {}); + expect(ir).toContain("call preserve_mostcc void @sc_cyc_append(ptr %o)"); + expect(ir).toContain("define internal preserve_mostcc void @sc_cyc_append(ptr %o) noinline"); + // The young test reads the generation byte at obj-5; the append fills + // scr_cyc_buffers' nursery buffer and the header's position at obj-4. + expect(ir).toContain("getelementptr i8, ptr %o, i64 -5"); + expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 96"); + expect(ir).toContain("getelementptr inbounds i8, ptr @scr_cyc_buffers, i64 104"); + expect(ir).toContain("load i64, ptr @scr_cyc_live"); + expect(ir).toContain("or i32 %i, -2147483648"); + expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); + expect(decls).toContain( + "@scr_cyc_buffers = external global { [3 x { ptr, i64, i64, i64 }], i64, i64 }", + ); + expect(decls).toContain("@scr_cyc_live = external global i64"); + } + // Worker executables and targets without inline allocation call + // scr_cyc_on_release, which tries the same append first. + for (const extra of [{ threadLocalAlloc: true }, { inlineAlloc: false }]) { + const { ir, decls } = emit(true, extra); + expect(ir).not.toContain("sc_cyc_append"); + expect(ir).toContain("call void @scr_cyc_on_release(ptr %o)"); + expect(decls.join("\n")).not.toContain("scr_cyc_buffers"); + } + }); + test("uses the wasm32 header offsets", () => { const { host } = declarationHost(32, true); releaseSym(host, { kind: "union", unionId: "u0" } as never); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 23565eff4..b62e7d034 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -28,7 +28,15 @@ import { mangleRecordStruct, mangleRecordTrace, } from "../mangle.js"; -import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js"; +import { + cycTypeDescriptor, + emitObjectAlloc, + emitObjectFree, + CYC_APPEND, + cycleAppendDecls, + cycleAppendHelper, + inlineBufferingSupported, +} from "./alloc.js"; import type { BlockBuilder } from "./blocks.js"; import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import type { NullableUnions } from "./nullable-unions.js"; @@ -308,6 +316,10 @@ export const INLINE_RC_FAMILIES: Readonly> = { type RcOp = "retain" | "release"; +/** The helper key (with the "family:op" keys) of the module's candidate + * append helper (alloc.ts cycleAppendHelper). */ +const CYC_APPEND_KEY = "cyc:append"; + /** The mirrored family for an RC operation on a runtime stem (`scr_str`), * or null when it stays a call. */ export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineRcFamily | null { @@ -369,6 +381,10 @@ function requestRcHelper(host: ShapeHost, family: string, op: RcOp): string { } host.rcHelpers.add(`${family}:${op}`); for (const decl of rcHelperDecls(family, op)) host.declare(decl); + // Its body is written last (emitInlineRcHelpers), after the requests are + // collected: a release that can buffer a candidate requests the append + // helper now. + if (op === "release" && INLINE_RC_FAMILIES[family]!.cycle === "always") requestCycleAppend(host); return `@${inlineRcName(family, op)}`; } @@ -466,6 +482,10 @@ function rcHelperKeys(host: ShapeHost): string[] { export function inlineRcDecls(host: ShapeHost): string[] { const decls = new Set(); for (const key of rcHelperKeys(host)) { + if (key === CYC_APPEND_KEY) { + for (const decl of cycleAppendDecls()) decls.add(decl); + continue; + } const [family, op] = key.split(":") as [string, RcOp]; for (const decl of rcHelperDecls(family, op)) decls.add(decl); } @@ -611,6 +631,10 @@ function releaseHelper(host: ShapeHost, family: string, row: InlineRcFamily): st export function emitInlineRcHelpers(host: ShapeHost): string[] { const out: string[] = []; for (const key of rcHelperKeys(host).sort()) { + if (key === CYC_APPEND_KEY) { + out.push(...cycleAppendHelper()); + continue; + } const [family, op] = key.split(":") as [string, RcOp]; const row = INLINE_RC_FAMILIES[family]!; out.push( @@ -1254,12 +1278,6 @@ export function releaseFastPath( ]; } -/** The possible-cycle-root step of a release that left a headered object - * alive: scr_cyc_on_release with its early return inlined. Test the sign - * of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured - * object) and call the runtime only to enqueue a new candidate. - * Every emitted release of a headered object shares this sequence - * (scr_runtime.h asserts the offset); it ends by branching to `done`. */ /** One traced field of an emitted trace function: load the slot and visit * it unless it is null. Every collector visitor ignores null children, and * most traced slots of a typical heap are empty (optional links, unset @@ -1283,16 +1301,39 @@ export function traceFieldVisitLines( ]; } +/** Whether candidate buffering goes through the module's append helper + * (alloc.ts cycleAppendHelper), requesting its definition if so: in + * optimized builds (those with inline RC helpers) that support it. */ +function requestCycleAppend(host: ShapeHost): boolean { + if (host.rcHelpers === null || !inlineBufferingSupported(host)) return false; + if (!host.rcHelpers.has(CYC_APPEND_KEY)) { + host.rcHelpers.add(CYC_APPEND_KEY); + for (const decl of cycleAppendDecls()) host.declare(decl); + } + return true; +} + +/** The possible-cycle-root step of a release that left a headered object + * alive: scr_cyc_on_release with its early return inlined. Test the sign + * of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured + * object), and buffer a new candidate: through the module's + * cycleAppendHelper in optimized builds that support it, through + * scr_cyc_on_release otherwise. Every emitted release of a headered object + * shares this sequence (scr_runtime.h asserts the offsets); it ends by + * branching to `done`. */ export function cycleRootLines(host: ShapeHost, done: string): string[] { const S = host.sizeType; host.declare(`declare void @scr_cyc_on_release(ptr)`); + const append = requestCycleAppend(host); return [ ` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`, ` %buf = load i32, ptr %bufp`, ` %queued = icmp slt i32 %buf, 0`, ` br i1 %queued, label %${done}, label %enqueue`, `enqueue:`, - ` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`, + append + ? ` call preserve_mostcc void @${CYC_APPEND}(ptr %o) ; buffer the candidate; may collect` + : ` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`, ` br label %${done}`, ]; } diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index c2be13a03..974c2e66d 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -156,14 +156,11 @@ static void scr_cyc_oom(void) { * struct on purpose: buffering a candidate is the hot path, and splitting * them across three arrays indexed by generation would touch three cache * lines per release where the original single buffer touched one. */ -typedef struct { - void **v; - size_t n, cap; - /* The most entries a pass held, for the work vectors a pass fills - * (scr_cyc_push_work); scr_cyc_pass_done frees a large vector that the - * pass used little of. */ - size_t used; -} ScrVec; +/* Its layout is scr_rc.h's ScrCycBuffer (the candidate buffers are part of + * the inline buffering ABI). `used`: the most entries a pass held, for the + * work vectors a pass fills (scr_cyc_push_work); scr_cyc_pass_done frees a + * large vector that the pass used little of. */ +typedef ScrCycBuffer ScrVec; typedef struct { void *object; @@ -201,7 +198,6 @@ typedef struct { unsigned char growth_shift[SCR_CYC_NGENS]; unsigned char growth_streak[SCR_CYC_NGENS]; unsigned backlog_shift[SCR_CYC_NGENS]; - size_t nbuffered; size_t trigger; size_t alloc_trigger; size_t gray_referenced; @@ -235,7 +231,6 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD}; #define scr_cyc_growth_shift (scr_cyc_thread.growth_shift) #define scr_cyc_growth_streak (scr_cyc_thread.growth_streak) #define scr_cyc_backlog_shift (scr_cyc_thread.backlog_shift) -#define scr_cyc_nbuffered (scr_cyc_thread.nbuffered) #define scr_cyc_trigger (scr_cyc_thread.trigger) #define scr_cyc_alloc_trigger (scr_cyc_thread.alloc_trigger) #define scr_gray_referenced (scr_cyc_thread.gray_referenced) @@ -361,7 +356,16 @@ static inline void scr_cyc_note_used(ScrVec *vec) { /* ── the candidate-root buffers (one per generation) ──────────────────── */ #ifndef SCR_WORKERS -static SCR_TL ScrVec scr_roots[SCR_CYC_NGENS]; +/* Emitted and inline releases append nursery candidates to these buffers + * directly where the build allows it (scr_rc.h). Explicitly initialized so + * library localization can demote it (see scr_cyc_types). */ +#if SCR_CYC_INLINE_BUFFERING +ScrCycBuffers scr_cyc_buffers = {0}; +#else +static SCR_TL ScrCycBuffers scr_cyc_buffers; +#endif +#define scr_roots (scr_cyc_buffers.roots) +#define scr_cyc_alloc_trigger (scr_cyc_buffers.alloc_trigger) static SCR_TL bool scr_collecting = false; #endif @@ -409,6 +413,7 @@ static SCR_TL unsigned scr_gen_limit = SCR_CYC_OLD; #endif static size_t scr_cyc_pass(unsigned gen_limit); +static void scr_cyc_route_bypassed(void); /* A limited pass (scr_cyc_collect_set) takes only the candidates in this * set from the buffers up to `scr_cyc_take_gen`; NULL takes them all. */ static SCR_TL const ScrCycSet *scr_cyc_take = NULL; @@ -505,18 +510,18 @@ static size_t scr_cyc_nursery_threshold(void) { return cached; } -/* Buffered candidates across every generation, and the count at which the - * release path next collects. Keeping a running total (rather than summing - * the buffers) is what holds the hot path to one load and one compare, as - * it was when there was a single buffer. The trigger is re-armed at the end - * of every pass to "whatever survived, plus a nursery's worth": without that +/* The number of buffered candidates (across every generation) at which the + * release path next collects, and the live count at which it collects + * however few candidates there are. The trigger is re-armed at the end of + * every pass to "whatever survived, plus a nursery's worth": without that * hysteresis a large older buffer — which a nursery pass cannot drain — * would re-arm on every single release. Both start at zero, so the first - * release runs one trivial pass that arms them properly. */ + * release runs one trivial pass that arms them properly. The inline + * appends (scr_rc.h) compare against the window scr_cyc_window_update + * derives from these, so the slow path sums the buffers when it needs the + * total. */ #ifndef SCR_WORKERS -static SCR_TL size_t scr_cyc_nbuffered = 0; static SCR_TL size_t scr_cyc_trigger = 0; -static SCR_TL size_t scr_cyc_alloc_trigger = 0; #endif /* Gray objects whose count is above zero after the trial decrements so far * (see scr_mg_visit). markGray records the gray objects in scr_white while @@ -549,6 +554,34 @@ static size_t scr_cyc_buffered(void) { + scr_roots[SCR_CYC_OLD].n; } +/* The inline buffering window (scr_rc.h): how many entries the nursery + * buffer may hold before an append would bring the buffered total to the + * trigger or would need the buffer to grow. While a pass runs, every + * release takes the call. Recomputed wherever the buffers, the nursery + * buffer's capacity or the trigger change in bulk; the release and death + * paths adjust it by one (scr_cyc_window_shrink). */ +static inline void scr_cyc_window_update(void) { +#if SCR_CYC_INLINE_BUFFERING + size_t window = 0; + if (!scr_collecting) { + size_t older = scr_roots[SCR_CYC_MATURE].n + scr_roots[SCR_CYC_OLD].n; + if (scr_cyc_trigger > older + 1) window = scr_cyc_trigger - older - 1; + if (window > scr_roots[SCR_CYC_NURSERY].cap) window = scr_roots[SCR_CYC_NURSERY].cap; + } + scr_cyc_buffers.window = window; +#endif +} + +/* One step of the window's formula going down by one: an older candidate + * was buffered, or a nursery death lowered the trigger. A window below the + * formula only sends more releases through the call, so the clamp to the + * capacity can stay where it is. */ +static inline void scr_cyc_window_shrink(void) { +#if SCR_CYC_INLINE_BUFFERING + if (scr_cyc_buffers.window) scr_cyc_buffers.window--; +#endif +} + static bool scr_cyc_older_backlog(void) { return scr_roots[SCR_CYC_MATURE].n != 0 || scr_roots[SCR_CYC_OLD].n != 0; } @@ -682,6 +715,7 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow * per pass, so it is kept out of line and built for size, which leaves the * hot buffering path in scr_cyc_on_release small. */ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) { + scr_cyc_route_bypassed(); unsigned gen = SCR_CYC_OLD; bool by_growth = scr_cyc_grown(SCR_CYC_OLD); if (!by_growth && scr_roots[SCR_CYC_OLD].n < scr_cyc_backlog_threshold(SCR_CYC_OLD)) { @@ -699,6 +733,7 @@ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) { * per turn would walk the whole live heap every turn — which is exactly the * cost the generations exist to avoid. */ void scr_cyc_collect_scheduled(void) { + scr_cyc_route_bypassed(); if (scr_cyc_buffered() == 0) { scr_cyc_scheduled_mature_age = 0; return; @@ -778,6 +813,10 @@ typedef struct ScrCycKind { static SCR_TL ScrCycKind *scr_cyc_kinds; static SCR_TL size_t scr_cyc_kinds_cap; #endif +#if SCR_CYC_INLINE_BUFFERING +/* Types that bypass the nursery right now (scr_cyc_route_bypassed). */ +static size_t scr_cyc_nursery_bypassing = 0; +#endif /* The statistics of `kind` (nonzero), growing the table to cover it. */ static SCR_CYC_COMPACT ScrCycKind *scr_cyc_kind_grow(uint32_t kind) { @@ -802,7 +841,12 @@ static inline bool scr_cyc_bypassed(const ScrCycType *type, unsigned gen) { if (kind >= scr_cyc_kinds_cap) return false; ScrCycLevelStat *k = &scr_cyc_kinds[kind].level[gen]; if (k->bypass == 0) return false; - if (--k->bypass == 0) k->walked = k->freed = 0; + if (--k->bypass == 0) { + k->walked = k->freed = 0; +#if SCR_CYC_INLINE_BUFFERING + if (gen == SCR_CYC_NURSERY) scr_cyc_nursery_bypassing--; +#endif + } return true; } @@ -820,9 +864,16 @@ static inline void scr_cyc_walked(const ScrCycType *type, unsigned gen) { if (!type->kind) return; ScrCycLevelStat *k = &scr_cyc_kind(type->kind)->level[gen]; if (++k->walked == SCR_CYC_BYPASS_WINDOW) { +#if SCR_CYC_INLINE_BUFFERING + bool before = k->bypass != 0; +#endif k->bypass = k->freed * SCR_CYC_BYPASS_RATIO < SCR_CYC_BYPASS_WINDOW ? SCR_CYC_BYPASS_WINDOW * SCR_CYC_BYPASS_PROBE : 0; +#if SCR_CYC_INLINE_BUFFERING + if (gen == SCR_CYC_NURSERY && before != (k->bypass != 0)) + scr_cyc_nursery_bypassing += before ? (size_t)-1 : 1; +#endif k->walked = k->freed = 0; } } @@ -831,6 +882,40 @@ static inline void scr_cyc_freed(const ScrCycType *type, unsigned gen) { if (type->kind) scr_cyc_kind(type->kind)->level[gen].freed++; } +/* An inline append (scr_rc.h) leaves every nursery candidate in the nursery + * buffer, including one whose type bypasses the nursery, which + * scr_cyc_on_release would have buffered past it. Where a candidate waits + * matters only once something reads the buffers, so every scheduling + * decision and every pass first moves such candidates to the generation + * scr_cyc_on_release would have chosen, consuming the bypass counts as it + * would have, in buffer order. Candidates that scr_cyc_on_release buffered + * already passed the same test with the counts unchanged since (they only + * change during passes), so they stay. */ +static SCR_CYC_COMPACT void scr_cyc_route_bypassed(void) { +#if SCR_CYC_INLINE_BUFFERING + if (scr_cyc_nursery_bypassing == 0) return; + ScrVec *nursery = &scr_roots[SCR_CYC_NURSERY]; + for (size_t i = 0; i < nursery->n;) { + void *obj = nursery->v[i]; + ScrCycHdr *h = scr_cyc_hdr(obj); + unsigned gen = scr_cyc_buffer_gen(scr_cyc_type(h), SCR_CYC_NURSERY); + if (gen == SCR_CYC_NURSERY) { + i++; + continue; + } + void *last = nursery->v[--nursery->n]; + nursery->v[i] = last; + if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i); + scr_cyc_set_gen(h, gen); + ScrVec *b = &scr_roots[gen]; + if (b->n == b->cap) scr_cyc_grow(b); + scr_cyc_set_buffered(h, (uint32_t)b->n); + b->v[b->n++] = obj; + } + scr_cyc_window_update(); +#endif +} + void scr_cyc_on_dead(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); if (!scr_cyc_is_buffered(h)) return; @@ -843,11 +928,12 @@ void scr_cyc_on_dead(void *obj) { b->v[i] = last; if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i); scr_cyc_clear_buffered(h); - scr_cyc_nbuffered--; /* A death must not consume room reserved for a future candidate. */ scr_cyc_trigger--; - if (scr_cyc_gen(h) != SCR_CYC_NURSERY && !scr_cyc_older_backlog()) - scr_cyc_scheduled_mature_age = 0; + /* An older death lowers the older count with the trigger: the window + * stays. A nursery death lowers only the trigger. */ + if (scr_cyc_gen(h) == SCR_CYC_NURSERY) scr_cyc_window_shrink(); + else if (!scr_cyc_older_backlog()) scr_cyc_scheduled_mature_age = 0; } /* Publication (scr_publish.c) takes buffered objects out of their candidate @@ -896,31 +982,39 @@ void scr_cyc_pub_fill(void) { } holes->n = 0; size_t closed = before - b->n; - scr_cyc_nbuffered -= closed; /* A death must not consume room reserved for a future candidate. */ scr_cyc_trigger -= closed; if (closed && gen != SCR_CYC_NURSERY) older = true; } if (older && !scr_cyc_older_backlog()) scr_cyc_scheduled_mature_age = 0; + scr_cyc_window_update(); } /* Every release that leaves an unbuffered object alive lands here (the - * emitted and inline release paths test `buffered` first), so it stays what - * it has always been: buffer inline, then one compare. */ + * emitted and inline release paths test `buffered` first) unless the window + * let it append a nursery candidate on its own fast path (scr_rc.h), so it + * stays what it has always been: the same append, then buffer and compare. */ void scr_cyc_on_release(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); /* Releasing an existing candidate cannot advance the trigger. A pass * always re-arms after its teardowns, including any candidates they add. */ if (scr_cyc_is_buffered(h)) return; +#if SCR_CYC_INLINE_BUFFERING + if (scr_cyc_append(h, obj)) return; +#endif if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_set_gen(h, scr_cyc_buffer_gen(scr_cyc_type(h), scr_cyc_gen(h))); - ScrVec *b = &scr_roots[scr_cyc_gen(h)]; - if (b->n == b->cap) scr_cyc_grow(b); + unsigned gen = scr_cyc_gen(h); + ScrVec *b = &scr_roots[gen]; + if (b->n == b->cap) { + scr_cyc_grow(b); + scr_cyc_window_update(); + } scr_cyc_set_buffered(h, (uint32_t)b->n); b->v[b->n++] = obj; - scr_cyc_nbuffered++; + if (gen != SCR_CYC_NURSERY) scr_cyc_window_shrink(); /* Never re-entered: a teardown's releases of untraced children can buffer * new candidates mid-collection, but they only wait for the next pass. */ - if (!scr_collecting && (scr_cyc_nbuffered >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger)) + if (!scr_collecting && (scr_cyc_buffered() >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger)) scr_cyc_collect_due(); } @@ -1272,10 +1366,10 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { } /* markRoots drained buffers in bulk and teardowns moved the count around; * resync from the buffers themselves and re-arm. */ - scr_cyc_nbuffered = scr_cyc_buffered(); - scr_cyc_trigger = scr_cyc_nbuffered + scr_cyc_nursery_threshold(); + scr_cyc_trigger = scr_cyc_buffered() + scr_cyc_nursery_threshold(); scr_cyc_alloc_trigger = scr_cyc_live + SCR_CYC_NURSERY_ALLOCATIONS; scr_collecting = false; + scr_cyc_window_update(); return freed; } @@ -1315,7 +1409,9 @@ static size_t scr_cyc_pass_done(size_t freed) { static size_t scr_cyc_pass(unsigned gen_limit) { if (scr_collecting) return 0; + scr_cyc_route_bypassed(); scr_collecting = true; + scr_cyc_window_update(); scr_gen_limit = gen_limit; /* markRoots: every buffered entry is a candidate (retains do not mark @@ -1488,6 +1584,9 @@ static size_t scr_cyc_pass(unsigned gen_limit) { #ifdef SCR_CYC_TESTING /* Tests only: one pass at exactly `gen`, outside the schedule. */ size_t scr_cyc_pass_at(unsigned gen) { return scr_cyc_pass(gen); } +/* Tests only: route appended candidates of bypassing types, as the next + * scheduling decision or pass would. */ +void scr_cyc_route_bypassed_at(void) { scr_cyc_route_bypassed(); } #endif size_t scr_cyc_collect_set(const ScrCycSet *set, unsigned gen) { @@ -1526,4 +1625,8 @@ void scr_cyc_context_cleanup(void) { free(scr_cyc_kinds); scr_cyc_kinds = NULL; scr_cyc_kinds_cap = 0; +#if SCR_CYC_INLINE_BUFFERING + scr_cyc_nursery_bypassing = 0; +#endif + scr_cyc_window_update(); } diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index d5d83a72a..b4c61a6ff 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -463,12 +463,76 @@ uint32_t scr_cyc_pub_take(void *obj); void scr_cyc_pub_restore(void *obj, uint32_t index); void scr_cyc_pub_fill(void); +/* The candidate buffers, one per generation (scr_cycle.c), and the window + * through which a release buffers a nursery candidate on its fast path. A + * release that leaves an unbuffered nursery object alive appends it to the + * nursery buffer while that buffer holds fewer than `window` entries and the + * live count is below `alloc_trigger` (scr_cyc_append); anything else takes + * the full scr_cyc_on_release. scr_cycle.c keeps `window` at most at the + * number of nursery entries the buffers can hold before a collection falls + * due or the buffer must grow, and at 0 while a pass runs, so the append is + * exactly what scr_cyc_on_release would do with a nursery candidate. A + * candidate whose type bypasses the nursery is moved past it before the + * buffers are next read (scr_cycle.c, "level bypass"). + * + * The runtime's own release paths append inline (scr_cyc_released), and + * scr_cyc_on_release tries the append first. The layout is an ABI: + * optimized emitted code appends through a per-module helper + * (llvm/alloc.ts cycleAppendHelper) that addresses roots[0].v, roots[0].n, + * window and alloc_trigger at fixed offsets. Builds whose collector state + * is thread-local (worker executables, thread-instanced libraries) never + * append on a fast path. */ +typedef struct ScrCycBuffer { + void **v; + size_t n, cap; + /* The most entries a pass held, for the work vectors a pass fills + * (scr_cycle.c ScrVec); unused by the candidate buffers. */ + size_t used; +} ScrCycBuffer; +typedef struct ScrCycBuffers { + ScrCycBuffer roots[SCR_CYC_NGENS]; + size_t window; + size_t alloc_trigger; +} ScrCycBuffers; +#if !defined(SCR_WORKERS) && !(defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +#define SCR_CYC_INLINE_BUFFERING 1 +extern ScrCycBuffers scr_cyc_buffers; +#if UINTPTR_MAX == UINT64_MAX +_Static_assert(offsetof(ScrCycBuffers, roots) == 0 && offsetof(ScrCycBuffer, v) == 0 && + offsetof(ScrCycBuffer, n) == 8 && offsetof(ScrCycBuffers, window) == 96 && + offsetof(ScrCycBuffers, alloc_trigger) == 104, + "llvm/alloc.ts appends nursery candidates against this layout"); +#endif +/* Append the unbuffered object `obj` (header `h`) to the nursery buffer if + * it is a nursery object and the window allows; false leaves it to the rest + * of scr_cyc_on_release. */ +static inline bool scr_cyc_append(ScrCycHdr *h, void *obj) { + ScrCycBuffer *nursery = &scr_cyc_buffers.roots[SCR_CYC_NURSERY]; + size_t n = nursery->n; + if (scr_cyc_gen(h) != SCR_CYC_NURSERY || n >= scr_cyc_buffers.window || + scr_cyc_live >= scr_cyc_buffers.alloc_trigger) + return false; + nursery->v[n] = obj; + nursery->n = n + 1; + scr_cyc_set_buffered(h, (uint32_t)n); + return true; +} +#else +#define SCR_CYC_INLINE_BUFFERING 0 +#endif + /* Inline halves of the hooks for runtime release paths, mirroring the * emitted ones (llvm/shapes.ts): a release of an already-buffered - * object, and the death of an object that was never buffered, need no call. + * object, and the death of an object that was never buffered, need no call, + * and a nursery candidate is appended inline while the window allows. * Most releases of long-lived containers take the first early return. */ static inline void scr_cyc_released(void *obj) { - if (!scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_release(obj); + ScrCycHdr *h = scr_cyc_hdr(obj); + if (scr_cyc_is_buffered(h)) return; +#if SCR_CYC_INLINE_BUFFERING + if (scr_cyc_append(h, obj)) return; +#endif + scr_cyc_on_release(obj); } static inline void scr_cyc_dead(void *obj) { if (scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_dead(obj); diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index ba0bc0819..c50f0d5c5 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -712,8 +712,11 @@ static void check_deferred_white_restoration(void) { * once the type's objects die in nursery cycles again, the next probe window * finds that and it is walked again. Window, ratio and probe interval mirror * SCR_CYC_BYPASS_*. */ -/* Tests only (SCR_CYC_TESTING): one pass at exactly `gen`. */ +/* Tests only (SCR_CYC_TESTING): one pass at exactly `gen`, and the routing + * of appended candidates whose type bypasses a level, which the next + * scheduling decision or pass would do. */ size_t scr_cyc_pass_at(unsigned gen); +void scr_cyc_route_bypassed_at(void); static size_t bypass_traced; /* trace calls on bypass-kind nodes */ static size_t bypass_freed; /* bypass-kind nodes freed */ static void bypass_trace(void *obj, ScrTraceVisit visit, void *ctx) { @@ -785,6 +788,22 @@ static void check_nursery_bypass(void) { scr_collect_cycles(); check(bypass_freed - before == 2 * RINGS, "an older pass missed dead rings of a bypassed type"); + /* The same through the inline release path, which leaves the candidates + * in the nursery buffer: the next pass routes them past the nursery. */ + before = bypass_freed; + for (size_t r = 0; r < RINGS; r++) { + Node *a = make_bypass_node(), *b = make_bypass_node(); + a->next = b; + b->next = a; + a->rc++; + a->rc--; + scr_cyc_released(a); + } + run_release_passes(); + check(bypass_freed - before < 2 * RINGS, "inline-buffered bypassed rings were walked by the nursery"); + scr_collect_cycles(); + check(bypass_freed - before == 2 * RINGS, "an older pass missed inline-buffered rings"); + /* Now the type's objects die in nursery cycles. Once INTERVAL windows' * worth of candidates went past the nursery, a probe window walks them * there again, and its yield turns the bypass off. A nursery pass after @@ -832,13 +851,16 @@ static Node *make_mature_kind_node(void) { } /* How many of PROBE fresh, externally owned candidates a release buffers - * straight into the old generation. */ + * straight into the old generation, by the time anything reads the buffers + * (a release may append the candidate to the nursery buffer, which the next + * decision routes past the levels its type bypasses). */ static size_t probe_old_on_release(Node **owned, size_t probe) { scr_collect_cycles(); /* reset the growth baselines: no pass runs below */ size_t old = 0; for (size_t i = 0; i < probe; i++) { owned[i] = make_mature_kind_node(); read_live(owned[i]); + scr_cyc_route_bypassed_at(); old += scr_cyc_gen(scr_cyc_hdr(owned[i])) == SCR_CYC_OLD; } return old; @@ -1187,7 +1209,55 @@ static void check_large_old_buffer_with_nursery_candidates(void) { } } +/* Releases through scr_cyc_released append nursery candidates inline while + * the window allows, and fall back to scr_cyc_on_release exactly when a + * collection is due: the candidate that brings the buffered total to the + * trigger runs the pass, as it would through the call. */ +static void check_inline_buffering(void) { + enum { MAX = 4096 }; + static Node *nodes[MAX]; + size_t threshold = configured_nursery_threshold(); + if (threshold > MAX) return; + scr_collect_cycles(); + size_t before = traced; + for (size_t i = 0; i < threshold; i++) { + nodes[i] = make_ring(1); + nodes[i]->rc--; /* drop the self edge: a live leaf held by its owner */ + nodes[i]->next = NULL; + nodes[i]->rc++; + nodes[i]->rc--; + scr_cyc_released(nodes[i]); + ScrCycHdr *h = scr_cyc_hdr(nodes[i]); + if (i + 1 < threshold) { + check(scr_cyc_is_buffered(h) && scr_cyc_gen(h) == SCR_CYC_NURSERY, + "a nursery release did not buffer its candidate"); +#if SCR_CYC_INLINE_BUFFERING + check(scr_cyc_buffers.roots[SCR_CYC_NURSERY].v[scr_cyc_buf_index(h)] == nodes[i], + "an inline append is not where the header says"); +#endif + check(traced == before, "a pass ran before the trigger"); + } + } + /* The last release reached the trigger: one nursery pass walked them all + * and promoted the survivors, which wait in the mature buffer. */ + check(traced > before, "the release that reached the trigger did not collect"); + for (size_t i = 0; i < threshold; i++) { + ScrCycHdr *h = scr_cyc_hdr(nodes[i]); + check(scr_cyc_gen(h) == SCR_CYC_MATURE, "a nursery survivor was not promoted"); + check(nodes[i]->rc == 1, "a survivor lost its count"); + } + size_t freed_before = freed; + for (size_t i = 0; i < threshold; i++) { + nodes[i]->rc = 0; + scr_cyc_on_dead(nodes[i]); + node_free(nodes[i]); + } + check(freed - freed_before == threshold, "the leaves were not freed"); + scr_collect_cycles(); +} + int main(void) { + check_inline_buffering(); check_old_garbage_reclaimed_while_growing(); check_older_rings_into_old(); check_cycle_spanning_mature_and_old(); From 526415a91ceaffd0d28fd2cdfbf680b803a1d8e1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 14:43:40 -0500 Subject: [PATCH 022/120] Give matchAll rows and exec results their index, input and groups Match results record their metadata lazily. The results of one exec or matchAll call share a source (the subject and the regex), and each result keeps its start index. index, input and a null-prototype groups object are built when a result is converted to a dynamic value; .index and .input on a match slice read the source directly. matchAll rows are allocated inline, like exec results. A drain whose rows are read only through their elements, .length, the .groups projection and the companion index uses matchAllCaptures or matchAllCapturesInto, whose rows carry no metadata. --- .../compiler/src/backend/llvm/expr-records.ts | 12 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/reference-effects.ts | 1 + .../src/backend/llvm/reference-writes.ts | 1 + packages/compiler/src/backend/may-throw.ts | 4 +- .../lowering/containers/match-row-uses.ts | 299 ++++++++++++++++++ .../lowering/containers/string-and-regexp.ts | 3 +- .../src/frontend/lowering/lower-exprs.ts | 12 +- .../src/frontend/lowering/lower-stmts.ts | 12 +- .../compiler/src/ir/builtin-signatures.ts | 2 + .../compiler/src/ir/intrinsic-signatures.ts | 6 + packages/compiler/src/ir/ir.ts | 10 +- .../compiler/test/match-row-emission.test.ts | 69 ++++ .../tests/corpus/6010-match-metadata.ts.json | 6 + .../corpus/6011-match-metadata-js.js.json | 6 + packages/runtime/src/scr_array.c | 4 + packages/runtime/src/scr_array.h | 14 + packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_json.c | 8 + packages/runtime/src/scr_publish.c | 2 +- packages/runtime/src/scr_regex.c | 170 +++++++++- packages/runtime/src/scr_regex.h | 14 +- tests/corpus/6010-match-metadata.ts | 40 +++ tests/corpus/6011-match-metadata-js.js | 31 ++ tests/harness/corpus-costs.json | 4 + 25 files changed, 705 insertions(+), 29 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/containers/match-row-uses.ts create mode 100644 packages/compiler/test/match-row-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json create mode 100644 tests/corpus/6010-match-metadata.ts create mode 100644 tests/corpus/6011-match-metadata-js.js diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 3318ae774..0cbfe15dd 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -147,7 +147,9 @@ function directRegexOperandMethod(method: string): boolean { case "match": case "search": case "matchAll": + case "matchAllCaptures": case "matchAllInto": + case "matchAllCapturesInto": case "replace": case "replaceAll": case "split": @@ -192,7 +194,7 @@ export function emitRegexIntrinsic( } else { values = preservesRegexInputs(method) ? emitBorrowedInputs(host, inputs) - : method === "matchAllInto" + : method === "matchAllInto" || method === "matchAllCapturesInto" ? inputs.map((a) => emitBorrowedInput(host, a)) : inputs.map((a) => host.emitExpr(a)); } @@ -216,6 +218,9 @@ export function emitRegexIntrinsic( // Every match drained eagerly into a fresh +1 string[][]; throws // Node's TypeError on a non-global regex (catchable). return fallible("scr_regex_match_all", `ptr ${r.name}, ptr ${args[0]!.name}`); + case "matchAllCaptures": + // matchAll whose rows carry no index, input or groups properties. + return fallible("scr_regex_match_all_captures", `ptr ${r.name}, ptr ${args[0]!.name}`); case "matchAllInto": // matchAll's companion-index form: args[1] (a number[]) also // receives each match's UTF-16 start index. @@ -223,6 +228,11 @@ export function emitRegexIntrinsic( "scr_regex_match_all_into", `ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`, ); + case "matchAllCapturesInto": + return fallible( + "scr_regex_match_all_captures_into", + `ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`, + ); case "replaceAll": return fallible( "scr_regex_replace_all", diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02bf37192..1334642a2 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1394,6 +1394,7 @@ export const LIB_FN_SYMS: Record = { "regex.groupEquals": "scr_regex_group_eq", "regex.groupNumber": "scr_regex_group_number", "regex.groupParseInt": "scr_regex_group_parse_int", + "regex.resultProp": "scr_regex_result_prop", // queueMicrotask's checked-dynamic form (borrowed dyn; a non-function // throws synchronously), the minted setImmediate value, and the // timers/promises immediate — all mark the loop live. diff --git a/packages/compiler/src/backend/llvm/reference-effects.ts b/packages/compiler/src/backend/llvm/reference-effects.ts index 88875fce4..e7b044187 100644 --- a/packages/compiler/src/backend/llvm/reference-effects.ts +++ b/packages/compiler/src/backend/llvm/reference-effects.ts @@ -29,6 +29,7 @@ export function preservesRegexInputs(method: string): boolean { case "match": case "search": case "matchAll": + case "matchAllCaptures": case "replace": case "replaceAll": case "split": diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 2371147a9..dcd6058d8 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -104,6 +104,7 @@ const INERT_LIB_CALLS = new Set([ "regex.groupEquals", "regex.groupNumber", "regex.groupParseInt", + "regex.resultProp", "process.cwd", "process.argv", "process.execArgv", diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index ac45129f0..2881fcbeb 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -297,7 +297,9 @@ export function computeMayThrow( rec.method === "replaceAll" || rec.method === "split" || rec.method === "matchAll" || - rec.method === "matchAllInto" + rec.method === "matchAllCaptures" || + rec.method === "matchAllInto" || + rec.method === "matchAllCapturesInto" ) { f.throws = true; } diff --git a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts new file mode 100644 index 000000000..387ff81d3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts @@ -0,0 +1,299 @@ +import * as ts from "../../ts7/adapter.js"; +import type { Lowerer } from "../lowerer.js"; +import { identifiersNamed } from "../identifier-uses.js"; +import { bindingNeverReassigned } from "../binding-analysis.js"; +import { matchCallbackParamGroupsOf, matchResultNamedGroupsOf } from "../lower-exprs.js"; + +/** Whether the rows a matchAll drain produces are read only through their + * elements: element reads and `in` tests with number keys, `.length`, the compile-time + * `.groups` projection, `{ groups }` destructuring and, on a for-of binding + * over the drain itself (`companion`), the companion-index `.index`. Rows + * reach these reads through for-of loops, spreads, `Array.from`, stored + * const bindings, `filter`/`slice`/`toReversed` copies, `find`/`findLast`/ + * `at` picks and inline callbacks of the array methods below. No such read + * can observe a row's `index`, `input` or `groups` properties, so the + * drain leaves them out. False for any other use. */ +export function matchAllRowsReadByElements( + lowerer: Lowerer, + drain: ts.Expression, + companion: boolean, +): boolean { + const parent = outerValue(drain).parent; + if ( + ts.isCallExpression(parent) && + parent.arguments[0] === outerValue(drain) && + ts.isPropertyAccessExpression(parent.expression) && + parent.expression.name.text === "from" && + lowerer.isStdlibGlobal(parent.expression.expression, "Array") + ) { + const mapper = parent.arguments[1]; + if (mapper === undefined) return rowsReadByElements(lowerer, parent, 0); + return parent.arguments.length === 2 && callbackReadsByElements(lowerer, mapper, 2); + } + if (ts.isForOfStatement(parent) && parent.expression === outerValue(drain)) + return forOfReadsByElements(lowerer, parent, companion); + return rowsReadByElements(lowerer, drain, 0); +} + +/** matchAllRowsReadByElements for a stored drain, `const rows = + * s.matchAll(re)`: a const-binding for-of loop over `rows` in the same + * function walks the companion indices, so its binding may also read + * `.index`. */ +export function storedMatchAllRowsReadByElements( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): boolean { + const scope = enclosingFunction(decl); + return constBindingReadsByElements(lowerer, decl, (use) => { + const parent = outerValue(use).parent; + if ( + ts.isForOfStatement(parent) && + parent.expression === outerValue(use) && + ts.isVariableDeclarationList(parent.initializer) && + (parent.initializer.flags & ts.NodeFlags.Const) !== 0 && + enclosingFunction(parent) === scope + ) + return forOfReadsByElements(lowerer, parent, true); + return rowsReadByElements(lowerer, use, 1); + }); +} + +function enclosingFunction(node: ts.Node): ts.Node | undefined { + let at = node.parent; + while (at !== undefined && !ts.isFunctionLike(at)) at = at.parent; + return at; +} + +/** Array methods whose first argument is a callback over the rows. */ +const ROW_CALLBACK_METHODS = new Set([ + "map", + "forEach", + "filter", + "some", + "every", + "find", + "findLast", + "findIndex", + "findLastIndex", + "flatMap", +]); + +/** Array methods whose result holds rows of the receiver. */ +const ROW_SUBSET_METHODS = new Set(["filter", "slice", "toReversed"]); + +/** Array methods that return one row of the receiver. */ +const ROW_PICK_METHODS = new Set(["find", "findLast", "at"]); + +/** Bounds the walk through stored bindings and copies. */ +const MAX_DEPTH = 8; + +function outerValue(node: ts.Node): ts.Node { + let ref = node; + while (ts.isParenthesizedExpression(ref.parent) || ts.isNonNullExpression(ref.parent)) + ref = ref.parent; + return ref; +} + +/** `node` evaluates to an array of rows. */ +function rowsReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boolean { + if (depth > MAX_DEPTH) return false; + const ref = outerValue(node); + const parent = ref.parent; + if (ts.isSpreadElement(parent)) { + const literal = parent.parent; + return ( + ts.isArrayLiteralExpression(literal) && + literal.elements.length === 1 && + rowsReadByElements(lowerer, literal, depth + 1) + ); + } + if (ts.isForOfStatement(parent) && parent.expression === ref) + return forOfReadsByElements(lowerer, parent, false); + if (ts.isVariableDeclaration(parent) && parent.initializer === ref) + return constBindingReadsByElements(lowerer, parent, (use) => + rowsReadByElements(lowerer, use, depth + 1), + ); + if (ts.isElementAccessExpression(parent) && parent.expression === ref) + return numberKeyRead(lowerer, parent) && rowReadByElements(lowerer, parent, depth + 1); + if (!ts.isPropertyAccessExpression(parent) || parent.expression !== ref) return false; + if (parent.questionDotToken !== undefined || !lowerer.isStdlibMember(parent)) return false; + const method = parent.name.text; + const call = parent.parent; + if (!ts.isCallExpression(call) || call.expression !== parent) + return method === "length" && isValueRead(parent); + if (ROW_CALLBACK_METHODS.has(method)) { + if (call.arguments.length !== 1 || !callbackReadsByElements(lowerer, call.arguments[0]!, 2)) + return false; + } else if (ROW_SUBSET_METHODS.has(method) || ROW_PICK_METHODS.has(method)) { + if (call.arguments.some((a) => ts.isSpreadElement(a))) return false; + } else { + return false; + } + if (ROW_SUBSET_METHODS.has(method)) return rowsReadByElements(lowerer, call, depth + 1); + if (ROW_PICK_METHODS.has(method)) return rowReadByElements(lowerer, call, depth + 1); + return true; +} + +/** `node` evaluates to one row. */ +function rowReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boolean { + if (depth > MAX_DEPTH) return false; + const ref = outerValue(node); + const parent = ref.parent; + if (ts.isExpressionStatement(parent)) return true; + if (ts.isElementAccessExpression(parent) && parent.expression === ref) + return numberKeyRead(lowerer, parent); + // `1 in m`: a number key tests an element. + if ( + ts.isBinaryExpression(parent) && + parent.operatorToken.kind === ts.SyntaxKind.InKeyword && + parent.right === ref + ) + return lowerer.mapTypeOf(lowerer.typeOf(parent.left))?.kind === "f64"; + if (ts.isPropertyAccessExpression(parent) && parent.expression === ref) { + if (parent.questionDotToken !== undefined || !isValueRead(parent)) return false; + if (parent.name.text === "length") return true; + return parent.name.text === "groups" && groupsProjected(lowerer, ref as ts.Expression); + } + if (ts.isVariableDeclaration(parent) && parent.initializer === ref) { + if (ts.isObjectBindingPattern(parent.name)) + return groupsPattern(parent.name) && groupsProjected(lowerer, ref as ts.Expression); + return constBindingReadsByElements(lowerer, parent, (use) => + rowReadByElements(lowerer, use, depth + 1), + ); + } + return false; +} + +/** A for-of loop over rows: its binding is read by elements, or by its + * companion `.index` when the loop walks the drain itself. */ +function forOfReadsByElements( + lowerer: Lowerer, + loop: ts.ForOfStatement, + companion: boolean, +): boolean { + const list = loop.initializer; + if (!ts.isVariableDeclarationList(list) || list.declarations.length !== 1) return false; + const decl = list.declarations[0]!; + if (!ts.isIdentifier(decl.name)) return false; + const symbol = lowerer.resolveValueSymbol(decl.name); + if (!symbol || !bindingNeverReassigned(lowerer, symbol, decl)) return false; + return usesOf(lowerer, decl.name, symbol).every((use) => { + const parent = outerValue(use).parent; + if ( + companion && + ts.isPropertyAccessExpression(parent) && + parent.name.text === "index" && + parent.questionDotToken === undefined && + isValueRead(parent) + ) + return true; + return rowReadByElements(lowerer, use, 1); + }); +} + +/** An inline callback whose first parameter (a row) is read by elements. + * The callback may name at most `maxParams` parameters, which keeps the + * array argument (the rows themselves) out of reach. */ +function callbackReadsByElements( + lowerer: Lowerer, + node: ts.Expression, + maxParams: number, +): boolean { + let fn: ts.Node = node; + while (ts.isParenthesizedExpression(fn)) fn = fn.expression; + if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false; + if (fn.parameters.length > maxParams) return false; + const param = fn.parameters[0]; + if (param === undefined) return true; + if (param.dotDotDotToken !== undefined || param.initializer !== undefined) return false; + if (ts.isObjectBindingPattern(param.name)) + return groupsPattern(param.name) && groupsProjectedParam(lowerer, param); + if (!ts.isIdentifier(param.name)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (!symbol || !bindingNeverReassigned(lowerer, symbol, param)) return false; + return usesOf(lowerer, param.name, symbol).every((use) => rowReadByElements(lowerer, use, 1)); +} + +/** Every use of a non-exported const binding satisfies `read`. */ +function constBindingReadsByElements( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + read: (use: ts.Identifier) => boolean, +): boolean { + if ( + !ts.isIdentifier(decl.name) || + decl.type !== undefined || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & ts.NodeFlags.Const) === 0 + ) + return false; + const statement = decl.parent.parent; + if ( + ts.isVariableStatement(statement) && + statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + ) + return false; + const symbol = lowerer.resolveValueSymbol(decl.name); + if (!symbol) return false; + return usesOf(lowerer, decl.name, symbol).every(read); +} + +function usesOf(lowerer: Lowerer, name: ts.Identifier, symbol: ts.Symbol): ts.Identifier[] { + return identifiersNamed(lowerer, name.getSourceFile(), name.text).filter( + (use) => use !== name && lowerer.resolveValueSymbol(use) === symbol, + ); +} + +/** `rows[i]` or `rows?.[i]` with a number key, read and not written. */ +function numberKeyRead(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean { + if (!isValueRead(access)) return false; + const key = lowerer.mapTypeOf(lowerer.typeOf(access.argumentExpression)); + return key?.kind === "f64"; +} + +/** `m.groups` lowers to the compile-time projection over the row's + * elements (lowerMatchGroupsRead). */ +function groupsProjected(lowerer: Lowerer, row: ts.Expression): boolean { + const groups = matchResultNamedGroupsOf(lowerer, row); + return groups !== null && groups.length > 0; +} + +/** `({ groups }) => ...` lowers through the same projection. */ +function groupsProjectedParam(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean { + const groups = matchCallbackParamGroupsOf(lowerer, param); + return groups !== null && groups.length > 0; +} + +/** An object pattern that binds only `groups`. */ +function groupsPattern(pattern: ts.ObjectBindingPattern): boolean { + return ( + pattern.elements.length > 0 && + pattern.elements.every((el) => { + if (el.dotDotDotToken !== undefined) return false; + const prop = el.propertyName; + if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; + return (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) && prop.text === "groups"; + }) + ); +} + +/** A read whose value is only consumed: not an assignment, update or + * delete target. */ +function isValueRead(access: ts.Expression): boolean { + const ref = outerValue(access); + const parent = ref.parent; + if ( + ts.isBinaryExpression(parent) && + parent.left === ref && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + return false; + return !( + (ts.isPrefixUnaryExpression(parent) && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken)) || + ts.isPostfixUnaryExpression(parent) || + ts.isDeleteExpression(parent) + ); +} diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 361441636..dd70eb197 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,3 +1,4 @@ +import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { @@ -869,7 +870,7 @@ export function lowerRegexMethodCall( const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); return { kind: "regexIntrinsic", - method: "matchAll", + method: matchAllRowsReadByElements(lowerer, call, false) ? "matchAllCaptures" : "matchAll", receiver, args: [re], type: arrayOf(arrayOf(STRING)), diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4d03591d..48d9d68fe 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -2053,13 +2053,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { (lowerer.isStdlibMember(expr) || isJsSourceFile(expr.getSourceFile())) ) { const recv = represented; - const read: IrExpr = { - kind: "dynKeyGet", - value: lowerer.coerceToExpected(recv, DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, - type: DYN, - loc, - }; + const key: IrExpr = { kind: "strLit", value: expr.name.text, type: STRING, loc }; + // A plain slice answers from its match source without a dynamic copy. + const read: IrExpr = typeEquals(recv.type, arrayOf(STRING)) + ? { kind: "libCall", fn: "regex.resultProp", args: [recv, key], type: DYN, loc } + : { kind: "dynKeyGet", value: lowerer.coerceToExpected(recv, DYN), key, type: DYN, loc }; return lowerer.maybeNarrow(read, expr); } } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 0f1b606ef..99e768e32 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,3 +1,7 @@ +import { + matchAllRowsReadByElements, + storedMatchAllRowsReadByElements, +} from "./containers/match-row-uses.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; @@ -1746,7 +1750,9 @@ function lowerMatchAllDrainDecl( localId: local.id, init: { kind: "regexIntrinsic", - method: "matchAllInto", + method: storedMatchAllRowsReadByElements(lowerer, decl) + ? "matchAllCapturesInto" + : "matchAllInto", receiver, args: [re, { kind: "varRef", localId: idxs.id, type: idxsT, loc }], type: rowsT, @@ -9289,7 +9295,9 @@ function lowerForOfMatchAll( }); rowsInit = { kind: "regexIntrinsic", - method: "matchAllInto", + method: matchAllRowsReadByElements(lowerer, call, true) + ? "matchAllCapturesInto" + : "matchAllInto", receiver, args: [re, { kind: "varRef", localId: idxs.id, type: idxsT, loc }], type: rowsT, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 08c493ae4..5657546de 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3377,6 +3377,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "regex.groupEquals": { argTypes: [arrayOf(STRING), F64, STRING], result: BOOL }, "regex.groupNumber": { argTypes: [arrayOf(STRING), F64], result: F64 }, "regex.groupParseInt": { argTypes: [arrayOf(STRING), F64, F64], result: F64 }, + // `.index` / `.input` (args[1]) of a match result. Borrowed; never throws. + "regex.resultProp": { argTypes: [arrayOf(STRING), STRING], result: DYN }, // node:events EventEmitter: receivers are emitter-hierarchy objects and // the chaining forms (on/off/removeAll/setMax) return the receiver's // own class — program-dependent object types, checked in the libCall diff --git a/packages/compiler/src/ir/intrinsic-signatures.ts b/packages/compiler/src/ir/intrinsic-signatures.ts index 3c224f8bc..eda569883 100644 --- a/packages/compiler/src/ir/intrinsic-signatures.ts +++ b/packages/compiler/src/ir/intrinsic-signatures.ts @@ -53,11 +53,17 @@ export const REGEX_INTRINSIC_SIGS: Record< matchGroups: { receiver: STRING, argTypes: [REGEX, F64, F64], result: VOID }, lastIndex: { receiver: REGEX, argTypes: [], result: F64 }, matchAll: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) }, + matchAllCaptures: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) }, matchAllInto: { receiver: STRING, argTypes: [REGEX, arrayOf(F64)], result: arrayOf(arrayOf(STRING)), }, + matchAllCapturesInto: { + receiver: STRING, + argTypes: [REGEX, arrayOf(F64)], + result: arrayOf(arrayOf(STRING)), + }, search: { receiver: STRING, argTypes: [REGEX], result: F64 }, source: { receiver: REGEX, argTypes: [], result: STRING }, flags: { receiver: REGEX, argTypes: [], result: STRING }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..74f5197ac 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2365,13 +2365,21 @@ export type IrRegexIntrinsicMethod = * unobservable across the lowered surface (strings are immutable; the * spec clones the regex at the call, so lastIndex games can't reach the * drain either). Non-global regexes THROW Node's exact TypeError - * (catchable — replaceAll's stance). */ + * (catchable — replaceAll's stance). Each row carries its index, input + * and groups properties. */ | "matchAll" + /** matchAll whose rows are read only through their elements + * (matchAllRowsReadByElements): the rows carry no index, input or + * groups properties. Same result and throw contract as matchAll. */ + | "matchAllCaptures" /** matchAll's companion-index form (the for-of-over-matchAll desugar): * args[1] is a number[] the drain ALSO fills with each match's UTF-16 * start index — the row's `.index`, always present (every drained row * matched). Same result and throw contract as matchAll. */ | "matchAllInto" + /** matchAllInto whose rows are read only through their elements and the + * companion index: the rows carry no index, input or groups properties. */ + | "matchAllCapturesInto" /** `s.search(re)` — the first match's UTF-16 index, or -1. Symbol.search * neither reads nor writes lastIndex (a fresh exec from position 0), so * no g/y fence applies: /g is irrelevant and /y anchors at 0 — exactly diff --git a/packages/compiler/test/match-row-emission.test.ts b/packages/compiler/test/match-row-emission.test.ts new file mode 100644 index 000000000..39e07bc1c --- /dev/null +++ b/packages/compiler/test/match-row-emission.test.ts @@ -0,0 +1,69 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-match-rows-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const calls = (llvm: string, symbol: string): number => + llvm.split("\n").filter((line) => line.includes("call ") && line.includes(`@${symbol}(`)).length; + +test("rows read only through their elements carry no match metadata", async () => { + const llvm = await emit(` +const text = "k1=v1; k2=v2;"; +const re = /(?k\\d)=(?v\\d);/g; +for (const m of text.matchAll(re)) console.log(m.index, m[1], m.groups!.val, 1 in m); +console.log([...text.matchAll(re)].map((m) => m[2]).join(",")); +console.log([...text.matchAll(re)].filter((m) => m.length > 2).find((m) => m[1] === "k2")?.[2]); +console.log(Array.from(text.matchAll(re), ({ groups }) => groups!.key).join(",")); +const rows = [...text.matchAll(re)]; +for (const row of rows.slice(1)) console.log(row[0]); +`); + expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(1); + expect(calls(llvm, "scr_regex_match_all_captures")).toBe(4); + expect(calls(llvm, "scr_regex_match_all")).toBe(0); + expect(calls(llvm, "scr_regex_match_all_into")).toBe(0); +}); + +test("rows whose properties are read or that escape keep match metadata", async () => { + const llvm = await emit(` +const text = "k1=v1; k2=v2;"; +const re = /(?k\\d)=(?v\\d);/g; +for (const m of text.matchAll(re)) console.log(m.input.length); +console.log([...text.matchAll(re)].map((m) => m.index).join(",")); +function first(m: RegExpExecArray): string { + return m[1]!; +} +console.log([...text.matchAll(re)].map(first).join(",")); +console.log([...text.matchAll(re)].map((m) => m)[0]![1]); +`); + expect(calls(llvm, "scr_regex_match_all_into")).toBe(1); + expect(calls(llvm, "scr_regex_match_all")).toBe(3); + expect(calls(llvm, "scr_regex_match_all_captures")).toBe(0); + expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(0); +}); + +test(".index and .input on a match slice read its match source", async () => { + const llvm = await emit(` +const rows = [..."a1b2".matchAll(/[a-z](\\d)/g)]; +console.log(rows[1]!.index, rows[0]!.input); +const m = /b/.exec("ab")!; +console.log(m.index); +`); + expect(calls(llvm, "scr_regex_result_prop")).toBe(3); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json new file mode 100644 index 000000000..d2b199768 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6010-match-metadata.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json new file mode 100644 index 000000000..437b28911 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6011-match-metadata-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index ef64be56d..fa494d1e5 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -40,6 +40,8 @@ static void scr_arr_oom(void) { /* Readers of the out-of-line storage see an all-empty one until the first * sparse entry, noncanonical property or metadata allocates it. */ static const ScrArrExt scr_arr_no_ext; + +SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks = NULL; #define SCR_AX(a) ((const ScrArrExt *)((a)->ext ? (a)->ext : &scr_arr_no_ext)) ScrArrExt *scr_arr_ext(ScrArr *a) { @@ -1068,6 +1070,7 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { * arrays have none. */ static void scr_arr_ext_free(ScrArr *a) { ScrArrExt *x = a->ext; + if (x->match_source) scr_arr_match_hooks->release(x->match_source); scr_arr_sparse_free(x->sparse); for (size_t i = 0; i < x->prop_len; i++) free(x->props[i].key); free(x->props); @@ -4423,6 +4426,7 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { } void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { + if (SCR_AX(a)->match_source && scr_arr_match_hooks->copy_metadata(a, target)) return; if (!SCR_AX(a)->metadata) return; for (size_t i = 0; i < SCR_AX(a)->metadata->v.obj.len; i++) { ScrDynEntry *entry = &SCR_AX(a)->metadata->v.obj.entries[i]; diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index d02c2097e..616aa4c75 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -106,6 +106,11 @@ typedef struct ScrArrExt { size_t prop_len; size_t prop_cap; struct ScrDyn *metadata; /* heterogeneous named properties of match results */ + /* A match result's metadata before its first use: the match's shared + * subject and regex (scr_regex.c builds `metadata` from them) and its + * UTF-16 start index. */ + struct ScrRegexMatchSource *match_source; + double match_index; } ScrArrExt; /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like @@ -207,6 +212,15 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { * the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); +/* The match-source operations, installed by scr_regex.c when it records a + * pending match source (ScrArrExt.match_source); this unit links without + * the regex runtime. copy_metadata writes the pending metadata into the + * dynamic copy or materializes it, answering whether it wrote. */ +typedef struct ScrArrMatchHooks { + void (*release)(struct ScrRegexMatchSource *src); + bool (*copy_metadata)(ScrArr *a, struct ScrDyn *target); +} ScrArrMatchHooks; +extern SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks; /* The array's out-of-line storage, allocated (zeroed) on first use. */ ScrArrExt *scr_arr_ext(ScrArr *a); /* Moves inline dense slots (kept in the header block of a small array) to diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 667ceced0..8ebe11265 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -862,6 +862,9 @@ ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity * non-object kinds throw Node's catchable TypeErrors (strict-mode * wording). All three operands BORROWED (the value is retained in). */ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); +/* A named (non-index) property of an extensible, unfrozen ARR, stored as + * scr_dyn_key_set stores it. Takes ownership of value. */ +void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value); void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b23e78241..1eca0c9ec 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -7059,6 +7059,14 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con return; } +void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value) { + if (!arr->v.arr.properties) { + scr_dyn_region_store(arr, NULL); /* a table that later holds anything */ + arr->v.arr.properties = scr_dyn_new_obj(); + } + scr_dyn_obj_set(arr->v.arr.properties, key, key_len, value); +} + void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (scr_dyn_class_reflection_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 9dedc6844..a974e82ff 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -188,7 +188,7 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; - if (a->ext && (a->ext->props || a->ext->metadata)) { + if (a->ext && (a->ext->props || a->ext->metadata || a->ext->match_source)) { scr_pub_refuse(ctx, "Cannot publish an array with non-index properties"); return; } diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index b24261a4d..40e843cac 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -610,6 +610,124 @@ static ScrStr *scr_regex_span(const ScrRegexInput *input, int start, int end) { return scr_str_new((const char *)input->data + start, (size_t)(end - start)); } +/* ── match metadata ───────────────────────────────────────────────────── */ + +/* Results of one exec or matchAll call share their subject and regex. + * Each result records the source and its start index; the metadata + * object (index, input, groups) is built when a result is first + * converted to a dynamic value, so results read only through their + * elements never allocate it. */ +typedef struct ScrRegexMatchSource { + size_t rc; + ScrStr *input; + ScrRegex *re; + ScrDyn *input_value; /* `input` as a dynamic value, built on first use */ +} ScrRegexMatchSource; + +static void scr_regex_materialize_metadata(ScrArr *a); +static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target); + +static ScrRegexMatchSource *scr_regex_source_new(ScrStr *s, ScrRegex *re) { + ScrRegexMatchSource *src = malloc(sizeof *src); + if (!src) scr_regex_oom(); + src->rc = 1; + src->input = scr_str_retain(s); + src->re = scr_regex_retain(re); + src->input_value = NULL; + return src; +} + +static void scr_regex_source_release(ScrRegexMatchSource *src) { + if (!src || --src->rc != 0) return; + scr_str_release(src->input); + scr_regex_release(src->re); + scr_dyn_release(src->input_value); + free(src); +} + +/* Records the match at UTF-16 `index` of `src` as the row's pending + * metadata. */ +static void scr_regex_defer_metadata(ScrArr *row, ScrRegexMatchSource *src, int index) { + static const ScrArrMatchHooks hooks = { scr_regex_source_release, scr_regex_copy_pending_metadata }; + scr_arr_match_hooks = &hooks; + ScrArrExt *x = scr_arr_ext(row); + src->rc++; + x->match_source = src; + x->match_index = (double)index; +} + +/* The subject as a dynamic value, shared by the source's results. +1. */ +static ScrDyn *scr_regex_source_input(ScrRegexMatchSource *src) { + if (!src->input_value) src->input_value = scr_dyn_new_str(src->input); + return scr_dyn_retain(src->input_value); +} + +ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key) { + ScrArrExt *x = a->ext; + if (x && x->match_source && !x->metadata) { + if (key->len == 5 && memcmp(key->data, "index", 5) == 0) + return scr_dyn_new_num(x->match_index); + if (key->len == 5 && memcmp(key->data, "input", 5) == 0) + return scr_regex_source_input(x->match_source); + scr_regex_materialize_metadata(a); + } + ScrDyn *value = x && x->metadata ? scr_dyn_obj_get(x->metadata, key->data, key->len) : NULL; + return scr_dyn_retain(value ? value : scr_dyn_undefined()); +} + +/* groups is a null-prototype object mapping each group name to its first + * participating capture (the row's own string, shared) or undefined, and + * undefined itself for a pattern without named groups. The row holds each + * capture as a string or a present undefined. +1. */ +static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { + const char *groupnames = lre_get_groupnames(bc); + if (!groupnames) return scr_dyn_retain(scr_dyn_undefined()); + int capture_count = lre_get_capture_count(bc); + ScrDyn *groups = scr_dyn_new_obj_null_proto(); + const char *name = groupnames; + for (int k = 1; k < capture_count; k++) { + size_t length = strlen(name); + bool matched = (size_t)k < a->len && (size_t)k < a->cap && a->present[k] == SCR_ARR_VALUE; + /* ES2025 duplicates sit in distinct alternatives: at most one + * participates, and it wins over an earlier undefined. */ + if (length && (matched || !scr_dyn_obj_get(groups, name, length))) + scr_dyn_obj_set(groups, name, length, + matched ? scr_dyn_new_str((ScrStr *)(uintptr_t)a->data[k]) + : scr_dyn_retain(scr_dyn_undefined())); + name += length + LRE_GROUP_NAME_TRAILER_LEN; + } + return groups; +} + +static void scr_regex_materialize_metadata(ScrArr *a) { + ScrArrExt *x = scr_arr_ext(a); + ScrRegexMatchSource *src = x->match_source; + if (!src) return; + x->match_source = NULL; + if (!x->metadata) { + ScrDyn *props = scr_dyn_new_obj(); + x->metadata = props; + scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_obj_set(props, "input", 5, scr_regex_source_input(src)); + scr_dyn_obj_set(props, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); + } + scr_regex_source_release(src); +} + +static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target) { + ScrArrExt *x = scr_arr_ext(a); + ScrRegexMatchSource *src = x->match_source; + if (target->kind != SCR_DYN_ARR || target->v.arr.properties || target->non_extensible || + target->v.arr.frozen) { + scr_regex_materialize_metadata(a); + return false; + } + scr_dyn_arr_named_put(target, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_arr_named_put(target, "input", 5, scr_regex_source_input(src)); + scr_dyn_arr_named_put(target, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); + return true; +} + /* ECMA-262 AdvanceStringIndex: past a whole surrogate pair under /u, one * code unit otherwise. ASCII cannot contain a surrogate pair. */ static int scr_advance(const ScrRegexInput *input, int i, bool unicode) { @@ -713,12 +831,6 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool int count = lre_get_capture_count(bc); bool spans = lazy != 0 && !input.wide && (count >= 64 || ((mask | lazy) >> count) == 0); out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans); - if (metadata) { - ScrDyn *props = scr_dyn_new_obj(); - scr_arr_ext(out)->metadata = props; - scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num((double)scr_capture_index(&input, capture[0]))); - scr_dyn_obj_set(props, "input", 5, scr_dyn_new_str(s)); - } /* A fresh dense array: every slot is written directly, then the * hole-free prefix is the run of materialized captures from 0. Its * storage pointers are fixed (the inline block never moves here), so @@ -750,6 +862,11 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool while (filled < n && present[filled] == SCR_ARR_VALUE) filled++; out->filled = (uint32_t)filled; out->len = n; + if (metadata) { + ScrRegexMatchSource *src = scr_regex_source_new(s, re); + scr_regex_defer_metadata(out, src, scr_capture_index(&input, capture[0])); + scr_regex_source_release(src); + } } scr_regex_input_dispose(&input); return out; /* +1, or NULL (no match) */ @@ -883,8 +1000,11 @@ double scr_regex_search(ScrStr *s, ScrRegex *re) { * Node's exact TypeError (catchable — replaceAll's stance). Empty matches * advance one unit, unicode-aware (AdvanceStringIndex). `indices`, when * non-NULL, receives each match's UTF-16 start index (the row's .index — - * the companion array the for-of-over-matchAll desugar reads). */ -static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices) { + * the companion array the for-of-over-matchAll desugar reads). With + * `metadata`, each row records the match source for its index, input and + * groups properties. */ +static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices, + bool metadata) { uint8_t *bc = scr_regex_bc(re); int re_flags = lre_get_flags(bc); if (!(re_flags & LRE_FLAG_GLOBAL)) { @@ -895,6 +1015,7 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices } bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; int capture_count = lre_get_capture_count(bc); + ScrRegexMatchSource *src = metadata ? scr_regex_source_new(s, re) : NULL; ScrRegexInput input; scr_regex_input_init(&input, re, bc, s); int len = input.len; @@ -906,30 +1027,51 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices if (scr_exec(&input, bc, pos) != 1) break; int start = scr_capture_index(&input, capture[0]); int end = scr_capture_index(&input, capture[1]); - ScrArr *row = scr_arr_new(SCR_ELEM_STR, (size_t)capture_count); + /* One allocation per row, like an exec result: every slot is written + * directly, then the hole-free prefix is the run of captures from 0. */ + ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count); + uint64_t *data = row->data; + uint8_t *present = row->present; for (int k = 0; k < capture_count; k++) { const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1]; if (cs == NULL || ce == NULL) { - scr_arr_set_undefined(row, (double)row->len); + data[k] = 0; + present[k] = SCR_ARR_UNDEFINED; } else { - scr_arr_push_ref( - row, scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce))); + ScrStr *text = + scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)); + data[k] = (uint64_t)(uintptr_t)text; + present[k] = SCR_ARR_VALUE; } } + size_t filled = 0; + while (filled < (size_t)capture_count && present[filled] == SCR_ARR_VALUE) filled++; + row->filled = (uint32_t)filled; + row->len = (size_t)capture_count; + if (src) scr_regex_defer_metadata(row, src, start); scr_arr_push_ref(out, row); if (indices != NULL) scr_arr_push_f64(indices, (double)start); pos = end == start ? scr_advance(&input, end, unicode) : end; } + scr_regex_source_release(src); scr_regex_input_dispose(&input); return out; /* +1 (possibly empty — Node's no-match drain is []) */ } ScrArr *scr_regex_match_all(ScrStr *s, ScrRegex *re) { - return scr_regex_match_all_core(s, re, NULL); + return scr_regex_match_all_core(s, re, NULL, true); +} + +ScrArr *scr_regex_match_all_captures(ScrStr *s, ScrRegex *re) { + return scr_regex_match_all_core(s, re, NULL, false); } ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices) { - return scr_regex_match_all_core(s, re, indices); + return scr_regex_match_all_core(s, re, indices, true); +} + +ScrArr *scr_regex_match_all_captures_into(ScrStr *s, ScrRegex *re, ScrArr *indices) { + return scr_regex_match_all_core(s, re, indices, false); } /* ── replace / replaceAll ─────────────────────────────────────────────── */ diff --git a/packages/runtime/src/scr_regex.h b/packages/runtime/src/scr_regex.h index e34e877de..af8ef4150 100644 --- a/packages/runtime/src/scr_regex.h +++ b/packages/runtime/src/scr_regex.h @@ -88,11 +88,23 @@ ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep); ScrArr *scr_regex_split(ScrStr *s, ScrRegex *re); ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit); /* matchAll drained eagerly: +1 string[][] of honest match slices; throws - * Node's TypeError on a non-global regex (catchable). */ + * Node's TypeError on a non-global regex (catchable). Each row carries its + * index, input and groups properties. */ ScrArr *scr_regex_match_all(ScrStr *s, ScrRegex *re); +/* scr_regex_match_all for rows read only through their elements: the rows + * carry no index, input or groups properties. */ +ScrArr *scr_regex_match_all_captures(ScrStr *s, ScrRegex *re); /* matchAll + the companion-index drain: also pushes each match's UTF-16 * start index (f64) onto `indices` — the .index the for-of-over-matchAll * desugar reads. Same throw/result contract as scr_regex_match_all. */ ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices); +/* scr_regex_match_all_into for rows read only through their elements and + * the companion index: the rows carry no index, input or groups + * properties. */ +ScrArr *scr_regex_match_all_captures_into(ScrStr *s, ScrRegex *re, ScrArr *indices); +/* `.index` / `.input` of a match result: its pending match source or its + * metadata answers, without building the metadata object; undefined when + * neither has the key. Borrows both; +1. */ +struct ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key); #endif /* SCR_REGEX_H */ diff --git a/tests/corpus/6010-match-metadata.ts b/tests/corpus/6010-match-metadata.ts new file mode 100644 index 000000000..8de5f6c4a --- /dev/null +++ b/tests/corpus/6010-match-metadata.ts @@ -0,0 +1,40 @@ +// index and input on matchAll rows and exec results: for-of bindings that +// read only captures and the index, bindings that escape, spreads, +// callbacks, stored drains and closures. +const text = "k1=v10; k22=v3; k3=v400;"; +const re = /(?k\d+)=(?v\d+);/g; + +for (const m of text.matchAll(re)) console.log(m.index, m[1], m.groups!.val); +const kept: RegExpExecArray[] = []; +for (const m of text.matchAll(re)) { + kept.push(m); + console.log(m.index, m.input === text); +} +console.log(kept.map((m) => `${m.index}:${m[0]}`).join(" ")); + +const rows = [...text.matchAll(re)]; +console.log(rows.map((m) => m.index).join(","), rows[1]!.index, rows[2]!.input.length); +console.log(rows.filter((m) => m.index > 0).map((m) => m.groups!.key).join(",")); +console.log(rows.find((m) => m.index === 8)?.[2], rows.at(-1)!.index); +console.log(Array.from(text.matchAll(re), (m) => m.index + m[0].length).join(",")); + +const drained = text.matchAll(/v(\d+)/g); +for (const m of drained) console.log(m.index, m[1]); +const stored = text.matchAll(/k(\d)/g); +const later = (): string => [...stored].map((m) => m.index).join("|"); +console.log(later()); + +function startOf(m: RegExpExecArray): number { + return m.index; +} +console.log([...text.matchAll(/=/g)].map(startOf).join(",")); + +const e = /(?\d+);/.exec(text)!; +console.log(e.index, e.input === text, e[1], e.groups!.n); +const g = text.match(/k\d+/g)!; +console.log(g.join(","), g.length); +const plain = [..."aXbX".matchAll(/X/g)]; +console.log(plain.map((m) => m.index).join(","), plain.length); +console.log([...text.matchAll(re)].map((m) => m[1]).join(","), [...text.matchAll(re)].length); +console.log(Array.from(text.matchAll(re), (m) => m.groups!.key + m.length).join(",")); +for (const m of [...text.matchAll(re)].slice(1)) console.log(m[2], m.groups!.val); diff --git a/tests/corpus/6011-match-metadata-js.js b/tests/corpus/6011-match-metadata-js.js new file mode 100644 index 000000000..b67e11069 --- /dev/null +++ b/tests/corpus/6011-match-metadata-js.js @@ -0,0 +1,31 @@ +// Match metadata on matchAll rows and exec results in JavaScript: index, +// input and groups through callbacks, untyped functions, destructuring, +// Object.keys and spread copies. +const text = "k1=v10; k22=v3; k3=v400;"; +const re = /(?k\d+)=(?v\d+)(?!)?;/g; +const rows = [...text.matchAll(re)]; +console.log(rows.map((m) => m.index).join(","), rows.map((m) => m.input.length).join(",")); +console.log(rows.map(({ index, groups }) => `${index}=${groups.key}`).join(" ")); + +function describe(m) { + return [m.index, m.input === text, m.groups.key, m.groups.bang, Object.keys(m).join("|")]; +} +for (const m of text.matchAll(re)) console.log(describe(m)); +console.log(rows.map((m) => describe(m)[0]).join(",")); + +const first = rows[0]; +const copy = { ...first }; +console.log(Object.keys(copy).join(","), copy.index, copy.groups.val, Object.getPrototypeOf(first.groups) === null); +console.log(JSON.stringify(first), first.length); + +const dup = [..."a1-b".matchAll(/(?[a-z])|(?\d)|-/g)]; +console.log(dup.map((m) => describe2(m)).join(" ")); +function describe2(m) { + return `${m.index}:${m.groups.v}`; +} + +const pick = (m) => m && [m.index, m.groups]; +console.log(pick(/(?\d)(?x)?/.exec("ab3")), pick(/b/.exec("ab")), pick(/z/.exec("ab"))); +console.log(pick("a1".match(/(?\d)/)), Object.keys("a1a".match(/a/g)).join(",")); +const none = [..."ab".matchAll(/[a-z]/g)]; +console.log(none.map((m) => m.groups === undefined && m.index >= 0).join(",")); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index e211a263b..cda819b0e 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2199,6 +2199,8 @@ "5996-rejections-between-macrotasks.cjs": 3034, "600-closures-basic.ts": 2349, "601-closures-loops.ts": 1788, + "6010-match-metadata.ts": 3990, + "6011-match-metadata-js.js": 3660, "602-closures-identity-recursion.ts": 2372, "6020-class-extends-function-constructor.js": 4300, "6021-asserted-literal-union-arm.ts": 3400, @@ -5236,6 +5238,8 @@ "5996-rejections-between-macrotasks.cjs": 22172, "600-closures-basic.ts": 2534, "601-closures-loops.ts": 2297, + "6010-match-metadata.ts": 5450, + "6011-match-metadata-js.js": 4890, "602-closures-identity-recursion.ts": 2427, "6020-class-extends-function-constructor.js": 7500, "6021-asserted-literal-union-arm.ts": 7200, From 26b703a821cd46fe4262fa3a3d0bfacd7bb99d86 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:44:31 -0700 Subject: [PATCH 023/120] Keep React Flight's development-only stack parsing out of production builds React Flight's production server never runs its V8 structured-stack parser (ReactFlightStackConfigV8): every use is behind __DEV__. Builds compiled from its sources keep that code behind `throw new Error('... should never be called in production mode')` stubs, so scriptc lowered it, failed on Error.prepareStackTrace, and a dead `.stack` read turned on stack-trace mode (a frame push in every function) for the whole program. - Statements after return/throw/break/continue in a function body are not lowered. Declarations keep their hoisting through the existing forward machinery (later function declarations, `var` reads undefined, `let`/ `const` TDZ); class, enum, module, import/export and generic function declarations still lower. Functions referenced only from such code are unreached and dead-stripped. - Stack-trace mode requires a stack read reachable through live code (observesStacks). - Synchronous functions in executables push and pop their source frame inline instead of calling scr_stack_enter/leave. - Functions that only perform operations which never create or throw an Error, and only call such functions, keep no frame (captureFreeFunctions): no capture can include them. Error.prepareStackTrace and CallSite stay refused: only development Flight runs them. Good code is byte-identical (zero-cost gate: bench and self-hosted compiler IR, LLVM and executables). Co-Authored-By: Claude Opus 5.5 --- .oxlintrc.jsonc | 1 + packages/compiler/src/backend/llvm/emitter.ts | 52 +++- packages/compiler/src/backend/may-throw.ts | 2 +- .../src/backend/stack-observations.test.ts | 137 ++++++++++ .../src/backend/stack-observations.ts | 237 ++++++++++++++++++ .../src/frontend/lowering/lower-stmts.ts | 52 ++++ .../6170-flight-dev-stacks-dead.ts.json | 6 + .../6171-error-stack-capture-sites.ts.json | 6 + .../6172-unreachable-statements.ts.json | 6 + packages/runtime/src/scr_exception.h | 4 + tests/corpus/6170-flight-dev-stacks-dead.ts | 173 +++++++++++++ .../corpus/6171-error-stack-capture-sites.ts | 79 ++++++ tests/corpus/6172-unreachable-statements.ts | 85 +++++++ tests/harness/deadstrip.test.ts | 33 ++- 14 files changed, 858 insertions(+), 15 deletions(-) create mode 100644 packages/compiler/src/backend/stack-observations.test.ts create mode 100644 packages/compiler/src/backend/stack-observations.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6170-flight-dev-stacks-dead.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6171-error-stack-capture-sites.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6172-unreachable-statements.ts.json create mode 100644 tests/corpus/6170-flight-dev-stacks-dead.ts create mode 100644 tests/corpus/6171-error-stack-capture-sites.ts create mode 100644 tests/corpus/6172-unreachable-statements.ts diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 52cbf5026..0c5cad25f 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -270,6 +270,7 @@ "packages/compiler/src/backend/llvm/**", "packages/compiler/src/backend/loop-polls.test.ts", "packages/compiler/src/backend/may-throw.test.ts", + "packages/compiler/src/backend/stack-observations.test.ts", "packages/compiler/src/ir/**", "packages/compiler/src/library/*.test.ts", "packages/compiler/test/**", diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 698a48aaf..d5d52926a 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -50,6 +50,7 @@ import { VOID, } from "../../ir/ir.js"; import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js"; +import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, @@ -1134,18 +1135,14 @@ export class LlEmitter { readonly chainSlots = new Map(); logArgSlots = 0; private readonly stackTraces: boolean; + /** Stack-trace mode: functions no capture can include (no frame push). */ + private readonly unframedFunctions: ReadonlySet; constructor( readonly mod: IrModule, options: LlvmTargetOptions, ) { - this.stackTraces = mod.functions.some( - (fn) => - !everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"), - }), - ); + this.stackTraces = observesStacks(mod); this.nullableUnions = new NullableUnions(mod); this.mockTimers = moduleUsesMockTimers(mod); this.debug = @@ -1227,6 +1224,9 @@ export class LlEmitter { // which Windows lacks. this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows; this.publishedTypes = computePublishedTypes(mod); + this.unframedFunctions = this.stackTraces + ? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes) + : new Set(); this.immutableFields = computeImmutableFields(mod); this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`)); this.indirectMayThrow = mt.indirect || mod.workers === true; @@ -7040,13 +7040,39 @@ export class LlEmitter { // Stackful native fibers and native coroutine frames retain these frames // while suspended. The active exception context owns the chain, so // concurrent fibers isolate it. - if (this.stackTraces && !this.wasi && fn.sourceName !== undefined) { - this.declare(`declare void @scr_stack_enter(ptr, ptr)`); - this.declare(`declare void @scr_stack_leave(ptr)`); + if ( + this.stackTraces && + !this.wasi && + fn.sourceName !== undefined && + !this.unframedFunctions.has(fn.name) + ) { B.entryAllocas.push(`%source_frame = alloca { ptr, ptr }`); - const frame = ` at ${fn.sourceName} (${fn.loc.file})`; - B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`); - B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`; + const frame = this.cstr(` at ${fn.sourceName} (${fn.loc.file})`); + if ( + B.excCellInvariant && + this.mod.lib === undefined && + this.mod.workers !== true && + this.sizeType === "i64" + ) { + // Executables link the active-cell pointer as a plain global, and a + // synchronous body sees one cell from entry to every return: push + // and pop the frame inline (ScrExcCell.stack is at byte 56 on + // 64-bit targets) instead of calling the runtime twice per call. + this.declare(`@scr_exc_active = external global ptr`); + B.line(`%source_frame.cell = load ptr, ptr @scr_exc_active`); + B.line(`%source_frame.chain = getelementptr inbounds i8, ptr %source_frame.cell, i64 56`); + B.line(`%source_frame.previous = load ptr, ptr %source_frame.chain`); + B.line(`store ptr %source_frame.previous, ptr %source_frame`); + B.line(`%source_frame.text = getelementptr inbounds i8, ptr %source_frame, i64 8`); + B.line(`store ptr ${frame}, ptr %source_frame.text`); + B.line(`store ptr %source_frame, ptr %source_frame.chain`); + B.returnEpilogue = `store ptr %source_frame.previous, ptr %source_frame.chain`; + } else { + this.declare(`declare void @scr_stack_enter(ptr, ptr)`); + this.declare(`declare void @scr_stack_leave(ptr)`); + B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${frame})`); + B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`; + } } if (this.stackChecks?.has(fn.name)) this.emitStackCheck(); if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true); diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index ac45129f0..f07bbfa78 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -580,7 +580,7 @@ function breaksTo( /** Statements no execution reaches: those after a statement that cannot * complete normally in the same statement list. */ -function unreachableStatements(body: readonly IrStmt[]): Set { +export function unreachableStatements(body: readonly IrStmt[]): Set { const dead = new Set(); const { statement } = completion(); const list = (stmts: readonly IrStmt[]): void => { diff --git a/packages/compiler/src/backend/stack-observations.test.ts b/packages/compiler/src/backend/stack-observations.test.ts new file mode 100644 index 000000000..f58c89ad8 --- /dev/null +++ b/packages/compiler/src/backend/stack-observations.test.ts @@ -0,0 +1,137 @@ +import { expect, test } from "vitest"; +import { + F64, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrModule, + type IrStmt, +} from "../ir/ir.js"; +import { captureFreeFunctions, observesStacks } from "./stack-observations.js"; + +const loc = { file: "stack-observations.ts", start: 0, end: 0 }; +const ERROR = { kind: "object", className: "%Error" } as const; +const err: IrExpr = { loc, kind: "varRef", localId: "e", type: ERROR }; +const stackRead: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "libCall", fn: "error.stack", args: [err], type: STRING }, +}; +const call = (callee: string): IrStmt => ({ + loc, + kind: "exprStmt", + expr: { loc, kind: "call", callee, args: [], type: VOID }, +}); +const throwStmt: IrStmt = { loc, kind: "throw", value: err }; +const ret: IrStmt = { loc, kind: "return", value: null }; +const fn = (name: string, body: IrStmt[]): IrFunction => ({ + loc, + name, + params: [], + locals: [], + returnType: VOID, + body, +}); +const mod = (functions: IrFunction[], extra: Partial = {}): IrModule => ({ + irVersion: 16, + sourceFile: "stack-observations.ts", + entry: "%main", + functions, + classes: [], + records: [], + unions: [], + globals: [], + ...extra, +}); + +test("a reachable stack read is observed", () => { + expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true); +}); + +test("programs without stack reads are not", () => { + expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false); +}); + +test("reads after an unconditional throw or return never run", () => { + expect(observesStacks(mod([fn("%main", [throwStmt, stackRead])]))).toBe(false); + expect(observesStacks(mod([fn("%main", [ret, call("parse")]), fn("parse", [stackRead])]))).toBe( + false, + ); + // A terminator ends only its own list: the outer list continues. + const cond: IrExpr = { loc, kind: "numLit", value: 1, type: F64 }; + expect( + observesStacks( + mod([fn("%main", [{ kind: "if", cond, then: [ret], else_: null, loc }, stackRead])]), + ), + ).toBe(true); +}); + +test("functions referenced only from dead code are unreached", () => { + // The production React shape: a DEV-only body behind a throw stub calls + // the stack parser, and a closure created after the stub reaches it too. + const closure: IrStmt = { + loc, + kind: "assign", + localId: "replacer", + value: { + loc, + kind: "closure", + fnName: "replacer", + captures: [], + type: { kind: "func", params: [], ret: VOID }, + }, + }; + const program = mod([ + fn("%main", [call("outline")]), + fn("outline", [throwStmt, closure, call("parse")]), + fn("replacer", [call("parse")]), + fn("parse", [stackRead]), + ]); + expect(observesStacks(program)).toBe(false); + // The same closure created before the stub keeps the read reachable. + const live = mod([ + fn("%main", [call("outline")]), + fn("outline", [closure, throwStmt]), + fn("replacer", [call("parse")]), + fn("parse", [stackRead]), + ]); + expect(observesStacks(live)).toBe(true); +}); + +test("class methods are roots", () => { + const program = mod([fn("%main", [ret]), fn("%Parser.read", [stackRead])], { + classes: [ + { name: "Parser", fields: [], methods: ["read"] } as unknown as NonNullable< + IrModule["classes"] + >[number], + ], + }); + expect(observesStacks(program)).toBe(true); +}); + +test("leaf functions that cannot create an Error need no frame", () => { + const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 }); + const add: IrStmt = { + loc, + kind: "return", + value: { loc, kind: "bin", op: "+", left: num(1), right: num(2), type: F64 }, + }; + const construct: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "libCall", fn: "error.new", args: [], type: ERROR }, + }; + const program = mod([ + fn("%main", [call("leaf"), call("caller"), call("creates"), call("viaCreates")]), + fn("leaf", [add]), + fn("caller", [call("leaf"), add]), + fn("creates", [construct]), + fn("viaCreates", [call("creates")]), + fn("recursive", [call("recursive")]), + ]); + const free = captureFreeFunctions(program, new Set(["recursive"]), null); + expect([...free].sort()).toEqual(["caller", "leaf"]); + // A worker program keeps every frame. + expect(captureFreeFunctions({ ...program, workers: true }, null, null).size).toBe(0); +}); diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts new file mode 100644 index 000000000..7debf0751 --- /dev/null +++ b/packages/compiler/src/backend/stack-observations.ts @@ -0,0 +1,237 @@ +/* Whole-program stack-trace gate (see observesStacks). Pure function of the + * IR module; the emitter keeps a source frame in every function only when + * it answers true. */ +import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js"; +import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js"; +import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js"; +import { MAY_THROW_ARR_METHODS } from "../ir/ir.js"; +import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js"; +import { unreachableStatements } from "./may-throw.js"; +import type { PublishedTypes } from "./publication.js"; +import { writesPublished } from "./publication.js"; + +/** Lib calls that read a captured stack trace. */ +const STACK_READS: ReadonlySet = new Set(["error.stack"]); + +function readsStack(expr: IrExpr): boolean { + return expr.kind === "libCall" && STACK_READS.has(expr.fn); +} + +function containsStackRead(fn: IrFunction): boolean { + return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) }); +} + +/** Visit every expression of `body` that execution can reach: statements + * after one that cannot complete normally in the same list are skipped. */ +function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void { + const dead = unreachableStatements(body); + const expr = (node: IrExpr): boolean => { + visit(node); + return everyExprChild(node, expr, stmt); + }; + const stmt = (node: IrStmt): boolean => dead.has(node) || everyStmtChild(node, expr, stmt); + body.every(stmt); +} + +/** Whether the program can read a captured stack trace, which is what makes + * every function keep a source frame. A read counts when it can execute: + * it sits in live code of a function reachable from the module's roots (its + * entry, library exports, class prototype helpers and every class's + * methods) through live code. Code after a statement that cannot complete + * normally is not live: production builds of libraries such as React's + * Flight server keep development-only stack parsing behind `throw` stubs, + * and those reads must not cost every call of the program a frame. + * Programs without any stack read answer false without the walk. */ +export function observesStacks(mod: IrModule): boolean { + if (!mod.functions.some(containsStackRead)) return false; + const byName = new Map(mod.functions.map((fn) => [fn.name, fn])); + const classNames = new Set((mod.classes ?? []).map((cls) => cls.name)); + const pending: string[] = [...moduleFunctionReferences(mod)]; + // Class methods, accessors and constructors are reachable through + // dispatch the IR does not name per site: all of them are roots. + for (const fn of mod.functions) { + if (!fn.name.startsWith("%")) continue; + for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) { + if (classNames.has(fn.name.slice(1, dot))) { + pending.push(fn.name); + break; + } + } + } + const reached = new Set(); + let observed = false; + while (pending.length > 0 && !observed) { + const name = pending.pop()!; + if (reached.has(name)) continue; + reached.add(name); + const fn = byName.get(name); + if (fn === undefined) continue; + visitLiveExprs(fn.body, (expr) => { + if (readsStack(expr)) observed = true; + if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee); + const referenced = exprFunctionReference(expr); + if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced); + }); + } + return observed; +} + +/** Expressions that neither create an Error nor throw one by themselves. + * Calls, `new` and virtual calls are capture-free when their callees are. */ +const CAPTURE_FREE_EXPRS: ReadonlySet = new Set([ + "numLit", + "boolLit", + "strLit", + "unitLit", + "varRef", + "incDec", + "assignExpr", + "bin", + "unary", + "logical", + "ternary", + "nullish", + "toBool", + "fieldGet", + "recordGet", + "arrayGet", + "arrayHas", + "arrayState", + "strCmp", + "strEq", + "strConcat", + "upcast", + "unionDisc", + "unionIsTag", + "unionEq", + "instanceOf", + "recordLit", + "arrayLit", + "unionWrap", + "unionNarrow", + "libCall", + "arrIntrinsic", + "call", + "new", + "virtualCall", +]); + +/** Statements that neither create an Error nor throw one by themselves. */ +const CAPTURE_FREE_STMTS: ReadonlySet = new Set([ + "varDecl", + "assign", + "exprStmt", + "if", + "while", + "doWhile", + "for", + "switch", + "block", + "return", + "break", + "continue", + "fieldSet", + "recordSet", + "arraySet", +]); + +/** Functions whose frame no stack capture can ever include: a capture runs + * where an Error is created (by the program or by a throwing runtime + * operation), and these functions only perform operations from a fixed + * list that never create one and only call functions of the same kind. + * Their frames are therefore never observable and the emitter omits them, + * which keeps small leaf methods and helpers free of the per-call frame + * push in programs that read stacks. Recursive functions (the stack guard + * throws RangeError), async and generator bodies, and every function of a + * worker program keep their frames. */ +export function captureFreeFunctions( + mod: IrModule, + stackChecks: ReadonlySet | null, + published: PublishedTypes | null, +): Set { + const free = new Set(); + if (mod.workers === true) return free; + const methodImpls = new Map(); + for (const cls of mod.classes ?? []) { + for (const m of cls.methods ?? []) { + let list = methodImpls.get(m); + if (!list) methodImpls.set(m, (list = [])); + list.push(`%${cls.name}.${m}`); + } + } + const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id)); + const callees = new Map(); + for (const fn of mod.functions) { + if (fn.async === true || fn.generator !== undefined || stackChecks?.has(fn.name)) continue; + const tdz = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]); + const calls: string[] = []; + const bigint = (e: IrExpr): boolean => e.type.kind === "bigint"; + const expr = (e: IrExpr): boolean => { + if (!CAPTURE_FREE_EXPRS.has(e.kind)) return false; + switch (e.kind) { + case "varRef": + case "incDec": + case "assignExpr": + if (tdz.has(e.localId)) return false; + break; + case "bin": + // BigInt division and remainder by zero throw RangeError. + if (bigint(e) || bigint(e.left) || bigint(e.right)) return false; + break; + case "unary": + if (bigint(e) || bigint(e.operand)) return false; + break; + case "libCall": + // Math functions only: they neither throw nor call back. + if (!e.fn.startsWith("math.") || MAY_THROW_LIB_FNS.has(e.fn)) return false; + break; + case "arrIntrinsic": { + // Methods that cannot throw, over numbers, booleans or strings + // (no element conversion can run program code) and without a + // callback argument. + const elem = e.receiver.type.kind === "array" ? e.receiver.type.elem.kind : null; + if ( + MAY_THROW_ARR_METHODS.has(e.method) || + (elem !== "f64" && elem !== "bool" && elem !== "string") || + e.args.some((arg) => arg.type.kind === "func") + ) + return false; + break; + } + case "call": + calls.push(e.callee); + break; + case "new": + calls.push(`%${e.className}.constructor`); + break; + case "virtualCall": { + const impls = methodImpls.get(e.method); + if (impls === undefined) return false; + calls.push(...impls); + break; + } + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if (!CAPTURE_FREE_STMTS.has(s.kind)) return false; + if (s.kind === "assign" && tdz.has(s.localId)) return false; + if (published !== null && writesPublished(published, s)) return false; + return everyStmtChild(s, expr, stmt); + }; + if (fn.body.every(stmt)) callees.set(fn.name, calls); + } + // Drop candidates that call a function outside the set until none do. + for (const name of callees.keys()) free.add(name); + let changed = true; + while (changed) { + changed = false; + for (const [name, calls] of callees) { + if (free.has(name) && !calls.every((callee) => free.has(callee))) { + free.delete(name); + changed = true; + } + } + } + return free; +} diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 93f00596c..9fd770a23 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -662,10 +662,14 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir out, }; lowerer.activeStmtLists.push(entry); + const inFunction = stmts.length > 0 && enclosedByFunction(stmts[0]!); + let unreachable = false; try { for (let i = 0; i < stmts.length; i++) { const stmt = stmts[i]!; entry.index = i; + if (unreachable && skipsWhenUnreachable(stmt)) continue; + if (inFunction && isAbruptStatement(stmt)) unreachable = true; if (!lowerer.suppressStats) { lowerer.stats.statementsTotal++; if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total"); @@ -781,6 +785,54 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir return out; } +/** Whether a statement list belongs to a function body (at any block + * depth) rather than to a module's top level. */ +function enclosedByFunction(stmt: ts.Statement): boolean { + let owner: ts.Node | undefined = stmt.parent; + while (owner !== undefined && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner)) + owner = owner.parent; + return owner !== undefined && ts.isFunctionLike(owner); +} + +/** `return`, `throw`, `break` and `continue`: the rest of the enclosing + * statement list never runs. */ +function isAbruptStatement(stmt: ts.Statement): boolean { + return ( + ts.isReturnStatement(stmt) || + ts.isThrowStatement(stmt) || + ts.isBreakStatement(stmt) || + ts.isContinueStatement(stmt) + ); +} + +/** Statements after an abrupt statement in a function body that lowering + * skips: they never execute, so neither their unsupported constructs nor the + * functions they reference become part of the program (production builds of + * libraries keep development-only code behind `throw` stubs this way). + * Hoisting keeps its meaning: a reference above the abrupt statement to a + * later function declaration lowers that declaration eagerly + * (predeclareForwardFnDecl), and references to a later `var`, `let` or + * `const` binding resolve through the forward-hoisting machinery to its + * undefined or TDZ state, which skipping its initializer preserves. Class, + * enum, module and import/export declarations and generic function + * declarations still lower. */ +function skipsWhenUnreachable(stmt: ts.Statement): boolean { + if (ts.isFunctionDeclaration(stmt)) return stmt.typeParameters === undefined; + if (ts.isVariableStatement(stmt)) + return !(stmt.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword); + return !( + ts.isClassDeclaration(stmt) || + ts.isEnumDeclaration(stmt) || + ts.isModuleDeclaration(stmt) || + ts.isImportDeclaration(stmt) || + ts.isImportEqualsDeclaration(stmt) || + ts.isExportDeclaration(stmt) || + ts.isExportAssignment(stmt) || + ts.isInterfaceDeclaration(stmt) || + ts.isTypeAliasDeclaration(stmt) + ); +} + const TDZ_KINDS = new Set([ "func", "string", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6170-flight-dev-stacks-dead.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6170-flight-dev-stacks-dead.ts.json new file mode 100644 index 000000000..f78bd38f5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6170-flight-dev-stacks-dead.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6170-flight-dev-stacks-dead.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6171-error-stack-capture-sites.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6171-error-stack-capture-sites.ts.json new file mode 100644 index 000000000..b4cdd2191 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6171-error-stack-capture-sites.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6171-error-stack-capture-sites.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6172-unreachable-statements.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6172-unreachable-statements.ts.json new file mode 100644 index 000000000..3ecbd301c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6172-unreachable-statements.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6172-unreachable-statements.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index 80fb98957..eaef90f3d 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -84,6 +84,10 @@ typedef struct ScrExcCell { ScrTraceFn trace_fn; ScrStackFrame *stack; /* execution state; never moves with an exception */ } ScrExcCell; +/* Executables push and pop source frames inline (emitter.ts, stack-trace + * mode) at this offset on 64-bit targets. */ +_Static_assert(sizeof(void *) != 8 || offsetof(ScrExcCell, stack) == 56, + "ScrExcCell.stack moved: update the emitter's inline frame push"); /* Runtime-internal (fiber machinery in scr_async.c) — never emitted. */ ScrExcCell *scr_exc_swap_cell(ScrExcCell *cell); /* NULL = main's cell */ diff --git a/tests/corpus/6170-flight-dev-stacks-dead.ts b/tests/corpus/6170-flight-dev-stacks-dead.ts new file mode 100644 index 000000000..bddf70f6a --- /dev/null +++ b/tests/corpus/6170-flight-dev-stacks-dead.ts @@ -0,0 +1,173 @@ +// React Flight's production server as compiled from its sources: the V8 +// structured-stack parser (ReactFlightStackConfigV8) is development-only and +// is referenced only after the production `throw` stubs, so a production +// render never parses a stack. Statements after an abrupt statement never +// run: they may use APIs with no static lowering (Error.prepareStackTrace, +// CallSite) and must not make the program keep stack frames. +// V8's structured stack API, declared here as React's Flow build declares +// it (the program's own types, not Node's). +interface CallSite { + getTypeName(): string | null; + getMethodName(): string | null; + getFunctionName(): string | null; + getScriptNameOrSourceURL(): string | null; + getEvalOrigin(): string | undefined; + getLineNumber(): number | null; + getColumnNumber(): number | null; + isNative(): boolean; + isAsync(): boolean; + isConstructor(): boolean; + isToplevel(): boolean; + isEval(): boolean; + toString(): string; +} +declare global { + interface ErrorConstructor { + prepareStackTrace?: (error: Error, structuredStackTrace: CallSite[]) => string; + } +} +type ReactStackTrace = Array<[string, string, number, number, number, number, boolean]>; + +let framesToSkip: number = 0; +let collectedStackTrace: null | ReactStackTrace = null; +const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + +function getMethodCallName(callSite: CallSite): string { + const typeName = callSite.getTypeName(); + const methodName = callSite.getMethodName(); + const functionName = callSite.getFunctionName(); + let result = ''; + if (functionName) { + if (typeName && identifierRegExp.test(functionName) && functionName !== typeName) { + result += typeName + '.'; + } + result += functionName; + if (methodName && functionName !== methodName && !functionName.endsWith('.' + methodName) && !functionName.endsWith(' ' + methodName)) { + result += ' [as ' + methodName + ']'; + } + } else { + if (typeName) { + result += typeName + '.'; + } + result += methodName ? methodName : ''; + } + return result; +} + +function collectStackTracePrivate(error: Error, structuredStackTrace: CallSite[]): string { + const result: ReactStackTrace = []; + for (let i = framesToSkip; i < structuredStackTrace.length; i++) { + const callSite = structuredStackTrace[i]; + let name = callSite.getFunctionName() || ''; + if (name.includes('react_stack_bottom_frame')) { + break; + } else if (callSite.isNative()) { + result.push([name, '', 0, 0, 0, 0, callSite.isAsync()]); + } else { + if (callSite.isConstructor()) { + name = 'new ' + name; + } else if (!callSite.isToplevel()) { + name = getMethodCallName(callSite); + } + if (name === '') { + name = ''; + } + let filename = callSite.getScriptNameOrSourceURL() || ''; + if (filename === '') { + filename = ''; + if (callSite.isEval()) { + const origin = callSite.getEvalOrigin(); + if (origin) { + filename = origin.toString() + ', '; + } + } + } + const line = callSite.getLineNumber() || 0; + const col = callSite.getColumnNumber() || 0; + result.push([name, filename, line, col, 0, 0, callSite.isAsync()]); + } + } + collectedStackTrace = result; + return ''; +} + +function collectStackTrace(error: Error, structuredStackTrace: CallSite[]): string { + collectStackTracePrivate(error, structuredStackTrace); + const name = error.name || 'Error'; + const message = error.message || ''; + let stack = name + ': ' + message; + for (let i = 0; i < structuredStackTrace.length; i++) { + stack += '\n at ' + structuredStackTrace[i].toString(); + } + return stack; +} + +function parseStackTrace(error: Error, skipFrames: number): ReactStackTrace { + collectedStackTrace = null; + framesToSkip = skipFrames; + const previousPrepare = Error.prepareStackTrace; + Error.prepareStackTrace = collectStackTrace; + let stack; + try { + stack = String(error.stack); + } finally { + Error.prepareStackTrace = previousPrepare; + } + if (collectedStackTrace !== null) { + const result = collectedStackTrace; + collectedStackTrace = null; + return result; + } + return stack.split('\n').length > 0 ? [] : []; +} + +function renderDebugModel(value: any): any { + if (value !== null && typeof value === 'object' && value._debugStack != null) { + return parseStackTrace(value._debugStack, 1); + } + return value; +} + +function emitOutlinedDebugModelChunk(model: any): string { + throw new Error('emitOutlinedDebugModel should never be called in production mode. This is a bug in React.'); + function replacer(this: any, parentPropertyName: string, value: any): any { + return renderDebugModel(this[parentPropertyName]); + } + const json: string = JSON.stringify(model, replacer); + return json; +} + +function outlineComponentInfo(componentInfo: any): string { + throw new Error('outlineComponentInfo should never be called in production mode. This is a bug in React.'); + const counter = { objectLimit: 10 }; + if (componentInfo.debugStack != null) { + counter.objectLimit += parseStackTrace(componentInfo.debugStack, 1).length; + } + return emitOutlinedDebugModelChunk(componentInfo) + counter.objectLimit; +} + +// The production render: elements become `id:json` rows. +type Element = { type: string; props: { [key: string]: string }; children: Element[] }; +function el(type: string, props: { [key: string]: string }, children: Element[]): Element { + return { type, props, children }; +} +function renderModel(node: Element): string { + const children = node.children.map(renderModel); + return '["$","' + node.type + '",null,' + JSON.stringify(node.props).slice(0, -1) + (children.length > 0 ? (Object.keys(node.props).length > 0 ? ',' : '') + '"children":[' + children.join(',') + ']' : '') + '}]'; +} +const rows: string[] = []; +const items: Element[] = []; +for (let i = 0; i < 3; i++) items.push(el('li', { key: 'k' + i }, [])); +rows.push('0:' + renderModel(el('main', {}, [el('ul', { className: 'list' }, items)]))); +console.log(rows.join('\n')); + +// The production stubs themselves run and throw; their DEV bodies never do. +for (const stub of [() => outlineComponentInfo({ debugStack: new Error('owner') }), () => emitOutlinedDebugModelChunk({})]) { + try { + stub(); + } catch (error) { + console.log((error as Error).message); + } +} + +export {}; diff --git a/tests/corpus/6171-error-stack-capture-sites.ts b/tests/corpus/6171-error-stack-capture-sites.ts new file mode 100644 index 000000000..1a8c8494d --- /dev/null +++ b/tests/corpus/6171-error-stack-capture-sites.ts @@ -0,0 +1,79 @@ +// Stack captures keep the frames of every function active when the Error is +// created: callers of constructors and closures, recursion, and errors a +// runtime operation creates and a local catch takes. Leaf functions and +// accessors that cannot create an Error run in between. Captures record +// frames when the Error is created and format them on the first `.stack` +// read. +let lastCaught: Error | null = null; + +function parseNumber(text: string): number { + try { + return JSON.parse(text) as number; + } catch (error) { + lastCaught = error as Error; + return -1; + } +} + +class Failure { + readonly error: Error; + constructor(reason: string) { + this.error = new Error(reason); + } +} +function buildFailure(reason: string): Failure { + return new Failure(reason); +} + +class Counter { + count: number = 0; + get next(): number { + return ++this.count; + } +} + +function leaf(n: number): number { + return n * 2 + 1; +} +function sumLeaves(n: number): number { + let total = 0; + for (let i = 0; i < n; i++) total += leaf(i); + return total; +} +function viaClosure(make: () => Error): Error { + return make(); +} +function recurse(n: number): Error { + return n === 0 ? new Error('deep') : recurse(n - 1); +} +function hasFrame(error: Error, name: string): boolean { + return error.stack!.split('\n').some((line) => line.includes(' ' + name + ' ') || line.endsWith(' ' + name)); +} + +const counter = new Counter(); +console.log(parseNumber('12'), parseNumber('{'), counter.next, counter.next, sumLeaves(4)); +const caught = lastCaught!; +console.log(caught.name, caught.stack!.startsWith('SyntaxError: '), hasFrame(caught, 'parseNumber')); + +const constructed = buildFailure('in constructor').error; +console.log(constructed.stack!.split('\n')[0], hasFrame(constructed, 'buildFailure')); + +const closure = viaClosure(() => new Error('in closure')); +console.log(closure.stack!.split('\n')[0], hasFrame(closure, 'viaClosure')); + +// Frames are recorded at creation: a later limit and later calls do not +// change them, and the first read fixes the text. +Error.stackTraceLimit = 3; +const deep = recurse(10); +Error.stackTraceLimit = 10; +sumLeaves(100); +const first = deep.stack!; +console.log(first.split('\n').length, hasFrame(deep, 'recurse'), first === deep.stack); +deep.message = 'changed'; +console.log(deep.stack === first); + +// An Error created with the limit at zero has no frames. +Error.stackTraceLimit = 0; +const bare = recurse(2); +Error.stackTraceLimit = 10; +console.log(JSON.stringify(bare.stack)); diff --git a/tests/corpus/6172-unreachable-statements.ts b/tests/corpus/6172-unreachable-statements.ts new file mode 100644 index 000000000..53704f694 --- /dev/null +++ b/tests/corpus/6172-unreachable-statements.ts @@ -0,0 +1,85 @@ +// Statements after return, throw, break or continue never run, while the +// declarations among them keep their hoisting: a later function declaration +// is callable from above, a later `var` reads undefined, and a later +// `let`/`const` stays in its temporal dead zone. +function hoistedCall(): number { + return twice(21); + console.log('never'); + function twice(n: number): number { + return n * 2; + } +} + +function hoistedVar(): string { + const before = typeof late; + return before; + var late: string | undefined = 'assigned'; + console.log(late); +} + +function deadZone(): string { + const read = (): number => counted; + try { + return String(read()); + } catch (error) { + return (error as Error).name + ': ' + (error as Error).message; + } + const counted = 1; +} + +function closureReference(): number { + const call = (): number => helper(); + return call(); + function helper(): number { + return 7; + } +} + +function loops(): string { + const seen: string[] = []; + for (let i = 0; i < 4; i++) { + if (i === 1) { + seen.push('skip'); + continue; + seen.push('never'); + } + if (i === 3) { + seen.push('stop'); + break; + seen.push('never'); + } + seen.push('i' + i); + } + return seen.join(','); +} + +function cases(n: number): string { + switch (n) { + case 0: + return 'zero'; + console.log('never'); + case 1: + n += 10; + case 2: + return 'fell to ' + n; + default: + throw new RangeError('n=' + n); + console.log('never'); + } +} + +function stub(): string { + throw new Error('stub'); + const unused = [1, 2, 3].map((x) => x * 2); + return unused.join(); +} + +console.log(hoistedCall(), hoistedVar(), deadZone(), closureReference(), loops()); +console.log(cases(0), cases(1), cases(2)); +for (const run of [() => cases(5), stub]) { + try { + run(); + } catch (error) { + console.log((error as Error).name, (error as Error).message); + } +} diff --git a/tests/harness/deadstrip.test.ts b/tests/harness/deadstrip.test.ts index 7db7ec404..1baa92ac9 100644 --- a/tests/harness/deadstrip.test.ts +++ b/tests/harness/deadstrip.test.ts @@ -1,5 +1,6 @@ /* Reachability's build contract — scriptc-only, deliberately NOT - * differential: + * differential (tests/corpus/6170-flight-dev-stacks-dead pins statement-level + * reachability differentially; here its emitted LLVM is grepped): * * - The dead-strip side (a program importing a module whose UNUSED exports * hold rejected constructs must build and run) is covered differentially @@ -49,6 +50,36 @@ test("unreached bodies leave no trace in the emitted LLVM", async () => { } }); +test("statements after an abrupt statement leave no trace and no stack frames", async () => { + const outDir = join(cacheDir, `deadstrip-abrupt${sanitize ? "-san" : ""}`); + const result = await compile(join(repoRoot, "tests/corpus/6170-flight-dev-stacks-dead.ts"), { + outPath: join(outDir, "program"), + outDir, + sanitize, + backend: "llvm", + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + const c = readFileSync(result.llvmPath, "utf8"); + for (const reached of ["outlineComponentInfo", "emitOutlinedDebugModelChunk", "renderModel"]) { + expect(c, `expected reached '${reached}' in the emitted LLVM`).toContain(reached); + } + // The development-only stack parser is referenced only after the + // production stubs' throw: none of it is emitted, and the program keeps + // no source frames (its only stack read never runs). + for (const gone of [ + "parseStackTrace", + "collectStackTrace", + "getMethodCallName", + "renderDebugModel", + ]) { + expect(c, `unreached '${gone}' must leave no trace in the emitted LLVM`).not.toContain(gone); + } + expect(c).not.toContain("source_frame"); + expect(c).not.toContain("scr_stack_enter"); +}); + /** Compiles an in-repo fixture and returns its diagnostics codes+messages. * Deliberately NO backend pin: every program here fails during lowering, * before any backend runs — the default is irrelevant by construction, and From 8aa69ad16912c7494fcf98a4589d935c879ea1e5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:12:28 -0700 Subject: [PATCH 024/120] Stop checking the storage fill of early-reachable let globals A top-level `let x;` in a JavaScript module holds a checked dynamic value that the module %init fills with undefined at entry. When the module's early-running code (a published function, a function declaration called above the binding) can reach the binding, it also gets an initialization flag, and every write before the flag is set is TDZ-checked. The entry fill was rewritten like a source write, so the module threw "Cannot access 'x' before initialization" before its first statement ran. That broke json5, cliui and cosmiconfig on load. Mark the fill as initializing when the global has a flag: it is storage setup, not the source write, so it neither runs the check nor sets the flag. Accesses that really happen before the declaration still throw. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-modules.ts | 11 ++++++++- .../6180-cjs-let-early-reachable/main.js.json | 7 ++++++ .../6181-esm-let-early-reachable/main.js.json | 7 ++++++ .../6180-cjs-let-early-reachable/lib.js | 24 +++++++++++++++++++ .../6180-cjs-let-early-reachable/main.js | 11 +++++++++ .../6180-cjs-let-early-reachable/package.json | 1 + .../6181-esm-let-early-reachable/lib.js | 19 +++++++++++++++ .../6181-esm-let-early-reachable/main.js | 8 +++++++ .../6181-esm-let-early-reachable/package.json | 1 + 9 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json create mode 100644 tests/corpus/6180-cjs-let-early-reachable/lib.js create mode 100644 tests/corpus/6180-cjs-let-early-reachable/main.js create mode 100644 tests/corpus/6180-cjs-let-early-reachable/package.json create mode 100644 tests/corpus/6181-esm-let-early-reachable/lib.js create mode 100644 tests/corpus/6181-esm-let-early-reachable/main.js create mode 100644 tests/corpus/6181-esm-let-early-reachable/package.json diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 3baab9dd0..df6677d5b 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2791,7 +2791,16 @@ export function lowerFileInit( const wrapped = g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0); if (wrapped) { - header.push({ kind: "assign", localId: g.id, value: wrapped, loc: loc0 }); + // A let global whose early reads are checked (initFlag) only + // gets its storage filled here: this is not the source write, so + // it must neither run the TDZ check nor set the flag. + header.push({ + kind: "assign", + localId: g.id, + value: wrapped, + ...(g.initFlag !== undefined ? { initializes: true as const } : {}), + loc: loc0, + }); } } const prelude = lowerer.jsonInitActions.get(sf) ?? []; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json new file mode 100644 index 000000000..c83e326cd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6180-cjs-let-early-reachable/lib.js", + "/tests/corpus/6180-cjs-let-early-reachable/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json new file mode 100644 index 000000000..cc09432a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6181-esm-let-early-reachable/lib.js", + "/tests/corpus/6181-esm-let-early-reachable/main.js" + ], + "diags": [] +} diff --git a/tests/corpus/6180-cjs-let-early-reachable/lib.js b/tests/corpus/6180-cjs-let-early-reachable/lib.js new file mode 100644 index 000000000..ce4300b1f --- /dev/null +++ b/tests/corpus/6180-cjs-let-early-reachable/lib.js @@ -0,0 +1,24 @@ +'use strict'; + +let source; +const parse = function parse(text) { + source = text; + return lex(); +}; +module.exports = parse; + +try { + console.log('early', parse('early')); +} catch (e) { + console.log('early', e instanceof ReferenceError, e.message); +} + +let state; +let count; +function lex() { + state = state === undefined ? 'fresh' : 'seen'; + count = count === undefined ? 1 : count + 1; + return source + ':' + state + ':' + count; +} + +console.log('loaded', state, count); diff --git a/tests/corpus/6180-cjs-let-early-reachable/main.js b/tests/corpus/6180-cjs-let-early-reachable/main.js new file mode 100644 index 000000000..58c0e5044 --- /dev/null +++ b/tests/corpus/6180-cjs-let-early-reachable/main.js @@ -0,0 +1,11 @@ +// A CommonJS module's top-level `let x;` bindings that a published +// function can reach before their declarations run. Node only throws +// for an access that really happens before the declaration; once the +// module body has run, every call sees the bindings (undefined until +// assigned). +'use strict'; + +const parse = require('./lib.js'); + +console.log(parse('a'), parse('b')); +console.log(parse('c')); diff --git a/tests/corpus/6180-cjs-let-early-reachable/package.json b/tests/corpus/6180-cjs-let-early-reachable/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6180-cjs-let-early-reachable/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6181-esm-let-early-reachable/lib.js b/tests/corpus/6181-esm-let-early-reachable/lib.js new file mode 100644 index 000000000..47a688101 --- /dev/null +++ b/tests/corpus/6181-esm-let-early-reachable/lib.js @@ -0,0 +1,19 @@ +let mixin; +export function cliui(opts) { + if (opts !== undefined) mixin = opts; + return describe(); +} + +let earlyResult; +try { + earlyResult = cliui({ width: 1 }); +} catch (e) { + earlyResult = `${e.name}: ${e.message}`; +} +export const early = earlyResult; + +let calls; +function describe() { + calls = calls === undefined ? 1 : calls + 1; + return `${calls} ${mixin === undefined ? 'none' : mixin.width}`; +} diff --git a/tests/corpus/6181-esm-let-early-reachable/main.js b/tests/corpus/6181-esm-let-early-reachable/main.js new file mode 100644 index 000000000..a27a418e2 --- /dev/null +++ b/tests/corpus/6181-esm-let-early-reachable/main.js @@ -0,0 +1,8 @@ +// An ES module's top-level `let x;` bindings that an exported function +// can reach before their declarations run. Only an access that really +// happens before the declaration throws; after the module body ran, +// calls see the bindings (undefined until assigned). +import { cliui, early } from './lib.js'; + +console.log(early); +console.log(cliui({ width: 3 }), cliui(undefined)); diff --git a/tests/corpus/6181-esm-let-early-reachable/package.json b/tests/corpus/6181-esm-let-early-reachable/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6181-esm-let-early-reachable/package.json @@ -0,0 +1 @@ +{ "type": "module" } From 6d6a140064aef2537e6a35ca598dca279dc73e36 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:16:48 -0700 Subject: [PATCH 025/120] Lower a JavaScript class method that reads itself as a value once A reflected JavaScript class method lowers its lambda the first time its value is read, and caches the closure only after the body lowered. A method whose own body reads the method as a value, like i18next's `instance.createInstance = I18n.createInstance`, reentered the lowering for the same method until the compiler overflowed its stack. Record the method while its lambda lowers; lowerLambda names the closure it will produce, and a read of the method inside its own body takes that closure, so every read sees the same function. When the body later changes the closure (its type is settled by an inferred return, or it captures), the self read is refused with a named diagnostic instead. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/class-method-values.ts | 37 +++++++++++++++++++ .../src/frontend/lowering/lower-functions.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 9 +++++ .../6182-class-method-self-value.js.json | 6 +++ tests/corpus/6182-class-method-self-value.js | 28 ++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json create mode 100644 tests/corpus/6182-class-method-self-value.js diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 7c8770021..50278c3b7 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -379,12 +379,48 @@ function reflectedMethodValue( needsNativeReceiver = true; }); if (needsNativeReceiver) return null; + // The method reads itself as a value inside its own body: it is the + // closure its lowering in progress produces. + if (lowerer.methodLambdasInProgress.has(member)) { + const pending = lowerer.methodLambdasInProgress.get(member); + if (!pending) { + lowerer.unsupported( + "SC1090", + member, + "class methods that read their own value while their signature is computed", + ); + } + pending.selfReads++; + return { + kind: "closure", + fnName: pending.fnName, + captures: [], + type: pending.type, + loc: locOf(member), + }; + } const context = newFnCtx(false, null, null, DYN); const previousClass = lowerer.currentClass; lowerer.currentClass = info; lowerer.fnStack.push(context); + lowerer.methodLambdasInProgress.set(member, null); try { const value = lowerer.lowerLambda(member); + const pending = lowerer.methodLambdasInProgress.get(member); + if ( + pending && + pending.selfReads > 0 && + (value.kind !== "closure" || + value.fnName !== pending.fnName || + value.captures.length > 0 || + !typeEquals(value.type, pending.type)) + ) { + lowerer.unsupported( + "SC1090", + member, + "class methods that read their own value inside their body when the method's value type is only settled by its body", + ); + } if (value.kind === "closure" && value.captures.length === 0) { const body = lowerer.liftedFns.find((fn) => fn.name === value.fnName); if (body?.captures?.length === 0) delete body.captures; @@ -395,6 +431,7 @@ function reflectedMethodValue( cache.set(method, value); return value; } finally { + lowerer.methodLambdasInProgress.delete(member); lowerer.fnStack.pop(); lowerer.currentClass = previousClass; } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 2991c0d39..d1707101e 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,6 +270,8 @@ export function lowerLambda( : lowerer.bodyReturnType(isAsync, funcType.ret); const fnCtx = newFnCtx(true, selfSymbol, funcType, bodyReturn); + if (lowerer.methodLambdasInProgress.has(node)) + lowerer.methodLambdasInProgress.set(node, { fnName, type: funcType, selfReads: 0 }); const inferredSignature = isJsSourceFile(node.getSourceFile()) && !node.type ? lowerer.checker.getSignatureFromDeclaration(node) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index bf4ed5464..adca3dfc5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2332,6 +2332,15 @@ export class Lowerer { /** Method adapter names are owned by class-method-values. Looking them * up must not scan and retain every unrelated lifted function. */ readonly classMethodValueHelpers = new Set(); + /** Reflected class methods whose lambda is lowering right now (null + * until lowerLambda named it), with the closure it will produce: a + * method that reads itself as a value inside its own body + * (`o.create = K.create`) takes this closure instead of lowering the + * method again. */ + readonly methodLambdasInProgress = new Map< + ts.Node, + { fnName: string; type: IrType; selfReads: number } | null + >(); readonly classMethodValueSelections = new Map< string, import("./class-method-values.js").ClassMethodValueSelection diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json new file mode 100644 index 000000000..7c8016a0e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6182-class-method-self-value.js" + ], + "diags": [] +} diff --git a/tests/corpus/6182-class-method-self-value.js b/tests/corpus/6182-class-method-self-value.js new file mode 100644 index 000000000..a5527dbe7 --- /dev/null +++ b/tests/corpus/6182-class-method-self-value.js @@ -0,0 +1,28 @@ +// JavaScript class methods that read themselves as a value inside their +// own body: i18next's `instance.createInstance = I18n.createInstance`. +// The value is the same function every other read of the method sees. + +class Registry { + /** @param {string} name */ + constructor(name) { + this.name = name; + /** @type {((name: string) => Registry) | undefined} */ + this.create = undefined; + } + + /** @param {string} name */ + static create(name) { + const registry = new Registry(name); + registry.create = Registry.create; + return registry; + } + + describe() { + return `${this.name} ${typeof this.create}`; + } +} + +const a = Registry.create('a'); +const b = a.create?.('b') ?? a; +console.log(a.describe(), b.describe()); +console.log(a.create === Registry.create, b.create === a.create); From 7ab6e8c6262e1d392aca4b518932743409794856 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:11 -0700 Subject: [PATCH 026/120] Store typed-array elements through storage wider than a typed array `b[i] = v` lowered to bytesSet whenever the checker's flow type of `b` was a typed array, even when the binding's storage was wider. A hoisted JavaScript `var` holds `bytes | undefined`, so the plain `var dst = Buffer.alloc(n); dst[i] = x` (ecdsa-sig-formatter, dotenv) and a field assigned after construction (@noble/hashes' HashMD buffer) failed IR validation with SC9001 "bytesSet on non-bytes union". For `bytes | undefined` storage with a number index, evaluate the receiver, index and value in order, then throw Node's TypeError for an undefined receiver or take the typed store. Any other storage takes the checked-dynamic store, which applies the value the receiver holds; a type with no checked-dynamic form is refused with a named diagnostic. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-exprs.ts | 85 +++++++++++++++++++ .../6184-js-bytes-write-wider-storage.js.json | 6 ++ .../6184-js-bytes-write-wider-storage.js | 57 +++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json create mode 100644 tests/corpus/6184-js-bytes-write-wider-storage.js diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4d03591d..d8d969973 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -7328,6 +7328,89 @@ function lowerGuardedRecordWrite( }; } +/** `b[i] = v` where the checker's flow type of `b` is a typed array but + * its storage is wider. A hoisted JavaScript `var` (or a field written + * after construction) holds `bytes | undefined`: the receiver, index and + * value evaluate in order, then an undefined receiver throws Node's + * TypeError and a present one takes the typed store. Any other storage (a + * `var` later reassigned a string, a checked-dynamic value) takes the + * checked-dynamic store, which applies whatever value the receiver holds. */ +function lowerWiderBytesWrite( + lowerer: Lowerer, + expr: ts.BinaryExpression, + target: ts.ElementAccessExpression, + recv: IrExpr, +): IrStmt { + const loc = locOf(expr); + if (recv.type.kind === "union") { + const unionId = recv.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + const undefTag = lowerer.armTag(unionId, UNDEFINED_T); + const bytes = arms?.length === 2 && undefTag >= 0 ? arms[1 - undefTag]! : null; + const keyType = lowerer.mapTypeOf(lowerer.typeOf(target.argumentExpression)); + const index = + bytes?.kind === "bytes" && keyType?.kind === "f64" + ? lowerer.lowerExpr(target.argumentExpression) + : null; + // An index stored wider than a number (a hoisted `var` counter) keeps + // the checked-dynamic store below, which converts the key exactly. + if (bytes?.kind === "bytes" && index?.type.kind === "f64") { + const stmts: IrStmt[] = []; + const stable = stabilized(lowerer, recv, "%bytesReceiver", stmts); + const key = stabilized(lowerer, index, "%bytesIndex", stmts); + const value = stabilized( + lowerer, + lowerer.lowerExprExpecting(expr.right, F64), + "%bytesValue", + stmts, + ); + const name = lowerRecordPropertyKey(lowerer, key, target.argumentExpression); + const present: IrExpr = { + kind: "unionNarrow", + unionId, + tag: 1 - undefTag, + value: stable, + type: bytes, + loc, + }; + return { + kind: "block", + body: [ + ...stmts, + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId, + tag: undefTag, + negated: false, + value: stable, + type: BOOL, + loc, + }, + then: [{ kind: "exprStmt", expr: undefinedReceiverThrow(name, "set", VOID, loc), loc }], + else_: [{ kind: "bytesSet", arr: present, index: key, value, loc }], + loc, + }, + ], + loc, + }; + } + } + if (!lowerer.dynConvertible(recv.type)) { + lowerer.unsupported( + "SC1090", + target.expression, + `typed-array element writes through '${lowerer.fmt(recv.type)}' storage`, + ); + } + return { + kind: "exprStmt", + expr: lowerDynMemberAssignment(lowerer, expr, lowerer.coerceInto(target.expression, recv, DYN)), + loc, + }; +} + /** `r[k]` over a (non-tuple) record shape. A LITERAL key naming a declared * field is the bracket spelling of field access (the ONLY spelling tsc * permits for signature-declared fields under @@ -8374,6 +8457,8 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): // the runtime coerces JS-exactly; invalid indices are ignored. if (receiverIr?.kind === "bytes") { const recv = lowerer.lowerExpr(target.expression); + // The checker's flow type is a typed array, but the storage is wider. + if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv); const index = lowerer.lowerExpr(target.argumentExpression); if (index.type.kind === "dyn" && recv.type.kind === "bytes") { return { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json new file mode 100644 index 000000000..7c4e146e1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6184-js-bytes-write-wider-storage.js" + ], + "diags": [] +} diff --git a/tests/corpus/6184-js-bytes-write-wider-storage.js b/tests/corpus/6184-js-bytes-write-wider-storage.js new file mode 100644 index 000000000..ff0e3295f --- /dev/null +++ b/tests/corpus/6184-js-bytes-write-wider-storage.js @@ -0,0 +1,57 @@ +// Typed-array element writes through storage wider than the checker's +// flow type: a hoisted `var` (ecdsa-sig-formatter's joseToDer) and a +// field that holds undefined until it is assigned (@noble/hashes' HashMD +// buffer). Each write stores into the typed array the receiver holds, or +// throws Node's TypeError for an undefined one after the index and the +// value evaluated. +'use strict'; + +/** @param {number} n */ +function der(n) { + var dst = Buffer.alloc(n); + var offset = 0; + dst[offset++] = 48; + dst[offset++] = n; + dst[offset] = 257; + return dst.toString('hex'); +} + +/** @param {number} n */ +function grow(n) { + var dst = Buffer.alloc(n); + for (var i = 0; i < n; i++) dst[i] = i * 3; + dst = Buffer.concat([dst, Buffer.from([255])]); + dst[0] = 7; + return dst.toString('hex'); +} + +console.log(der(3), grow(4)); + +/** @param {number} v */ +function note(v) { + console.log('value', v); + return v; +} + +class Hash { + buffer; + /** @param {number} n */ + constructor(n) { + if (n > 0) this.buffer = new Uint8Array(n); + this.pos = 0; + } + digest() { + const { buffer } = this; + let { pos } = this; + buffer[pos++] = note(0x80); + buffer[pos] = 300; + return Array.from(buffer).join(','); + } +} + +console.log(new Hash(4).digest()); +try { + console.log(new Hash(0).digest()); +} catch (e) { + console.log(e instanceof TypeError, e.message); +} From a2707ac8d6d78dba7055f2dbe8ea5f1cae9a6d66 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:07:01 -0700 Subject: [PATCH 027/120] Name the inner location in package-anchored npm-static refusals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package-anchored preflight diagnostic that makes the build drop an npm package was reported only at the consumer's import, as `SC2013 cannot compile npm package 'ws' statically: SC1012: …`, with the package file and line lost. The refusal reason now carries them (`SC1012 at ws/lib/websocket.js:30: …`), in the SC2013 diagnostic and in the coverage report's package status. This is how the SC1012 construct behind ws, readable-stream, through2, socket.io and fastify was identified (nested require patterns, and a rest element in fastify), and how openai's SC1013 is located (`openai/internal/auth/x509-transport-state.mjs:2`). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/pipeline.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 2ec9553f6..78971d7db 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -30,6 +30,21 @@ import { CONTRACT_TYPES_MODULE } from "./contract-types.js"; import { THREADS_MODULE } from "./threads-import.js"; import { EXPERIMENTAL_MODULE } from "./experimental-import.js"; +/** Where a package-anchored preflight diagnostic sits, as + * `:` — the SC2013 refusal that drops the package + * reports only at the consumer's import, so its reason carries the inner + * location. */ +function packageDiagWhere(load: LoadResult, loc: SrcLoc): string { + const file = loc.file.split("\\").join("/"); + const at = file.lastIndexOf("/node_modules/"); + const shown = at >= 0 ? file.slice(at + "/node_modules/".length) : file; + const text = load.program.getSourceFile(loc.file)?.text; + if (text === undefined) return shown; + let line = 1; + for (let i = 0; i < loc.start && i < text.length; i++) if (text.charCodeAt(i) === 10) line++; + return `${shown}:${line}`; +} + export interface ProgramLoadOptions { npmStatic?: readonly string[]; externalTypes?: Readonly> | undefined; @@ -344,7 +359,8 @@ function loadFrontend( const reasons = new Map(npmStaticOffenders()); for (const d of preflight) { const pkg = npmStaticPackageOfPath(d.loc.file); - if (pkg !== null && !reasons.has(pkg)) reasons.set(pkg, `${d.code}: ${d.message}`); + if (pkg !== null && !reasons.has(pkg)) + reasons.set(pkg, `${d.code} at ${packageDiagWhere(load, d.loc)}: ${d.message}`); } if (![...reasons.keys()].some((p) => effective.has(p))) { const named = new Map(); From 547e59529f50446bee47663a19f652c41294a0be Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 15:23:20 -0500 Subject: [PATCH 028/120] Resolve builtin classes through const aliases `const E = EventEmitter`, `const M = Map`, `const T = TypeError` and `const R = Readable` stopped at SC2020, and so did `typeof EventEmitter`, `Readable.name`, `Map.name`, `Error.name` and `===` between builtin classes. The zlib class alias resolver now covers the error classes, EventEmitter, the node:stream classes and Map/Set: a const binding of one (an alias of an alias, or an export used through a named or namespace import) constructs, extends and tests instanceof like the direct name, and typeof, .name and === fold for aliases and direct names. In TypeScript files the alias owns no runtime slot and other value uses report SC1090 at the use. In JavaScript files a copy of a name or of a globalThis property keeps its slot, which holds the same value as the name it copies; builtin module members (`zlib.Gzip`) own none, as before. --- .../lowering/builtins/class-values.ts | 159 ++++++++++++++++++ .../frontend/lowering/builtins/compression.ts | 72 +------- .../src/frontend/lowering/lower-classes.ts | 41 ++++- .../src/frontend/lowering/lower-exprs.ts | 45 ++--- .../src/frontend/lowering/lower-modules.ts | 4 +- .../src/frontend/lowering/lower-stmts.ts | 7 +- .../compiler/src/frontend/lowering/lowerer.ts | 15 +- .../src/frontend/lowering/surfaces.ts | 6 +- .../corpus/5976-builtin-class-aliases.ts.json | 6 + .../5977-builtin-class-aliases.cjs.json | 6 + .../main.ts.json | 7 + .../builtin-class-alias-escape.ts.json | 6 + tests/corpus/5976-builtin-class-aliases.ts | 109 ++++++++++++ tests/corpus/5977-builtin-class-aliases.cjs | 45 +++++ .../classes.ts | 8 + .../5978-builtin-class-alias-modules/main.ts | 33 ++++ .../diagnostics/builtin-class-alias-escape.ts | 15 ++ tests/diagnostics/class-values.ts | 4 +- .../builtin-class-alias-escape.ts.txt | 22 +++ .../harness/__snapshots__/class-values.ts.txt | 64 +++---- .../zlib-class-alias-escape.ts.txt | 2 +- tests/harness/corpus-costs.json | 6 + 22 files changed, 541 insertions(+), 141 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/class-values.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5976-builtin-class-aliases.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5977-builtin-class-aliases.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5978-builtin-class-alias-modules/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-class-alias-escape.ts.json create mode 100644 tests/corpus/5976-builtin-class-aliases.ts create mode 100644 tests/corpus/5977-builtin-class-aliases.cjs create mode 100644 tests/corpus/5978-builtin-class-alias-modules/classes.ts create mode 100644 tests/corpus/5978-builtin-class-alias-modules/main.ts create mode 100644 tests/diagnostics/builtin-class-alias-escape.ts create mode 100644 tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts new file mode 100644 index 000000000..f24db09cf --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts @@ -0,0 +1,159 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { type ClassInfo } from "../lower-classes.js"; +import { isJsSourceFile } from "../../program.js"; +import { builtinMemberRequireDecl } from "./module-bindings.js"; +import { stripTypeCasts } from "./arguments.js"; +import { streamClassAliasDecl } from "../lower-stream.js"; +import { zlibClassName, zlibStreamModeOf } from "./compression.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; + +/** A builtin class an expression names at compile time: the error + * classes, node:events EventEmitter, the node:stream classes (each a + * runtime ClassInfo), the node:zlib stream classes (by node_zlib_mode), + * and the Map and Set constructors. `name` is the class's `name` + * property. */ +export type BuiltinClassRef = + | { kind: "error" | "emitter" | "stream"; info: ClassInfo; name: string } + | { kind: "zlib"; mode: number; name: string } + | { kind: "collection"; which: "Map" | "Set"; name: string }; + +/** The builtin class `expr` names: a direct spelling (`EventEmitter`, + * `stream.Readable`, `zlib.Gzip`, `Map`, `globalThis.TypeError`) or a + * `const` binding of one (`const E = EventEmitter`, an alias of an alias, + * an imported alias), through parentheses and type assertions. Null for + * anything else. */ +export function builtinClassRefOf(lowerer: Lowerer, expr: ts.Expression): BuiltinClassRef | null { + const e = stripTypeCasts(expr); + // Checker lookups only: answering must not run the fences and deferred + // diagnostics of a lowered reference (resolveValueSymbol). + let symbol: ts.Symbol | undefined; + if (ts.isIdentifier(e)) { + symbol = lowerer.checker.getSymbolAtLocation(e); + } else if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) { + symbol = lowerer.checker.getSymbolAtLocation(e.name); + } else { + return null; + } + if (!symbol) return null; + const alias = builtinClassAliasOf(lowerer, symbol); + if (alias !== null) return alias; + if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol); + const which = stdlibGlobalNameOf(lowerer, e); + if (which === "Map" || which === "Set") return { kind: "collection", which, name: which }; + const error = lowerer.builtinErrorInfoOf(symbol); + if (error) return { kind: "error", info: error, name: runtimeClassName(error) }; + const emitter = lowerer.builtinEmitterInfoOf(symbol); + if (emitter) return { kind: "emitter", info: emitter, name: runtimeClassName(emitter) }; + const stream = lowerer.builtinStreamInfoOf(symbol); + if (stream) return { kind: "stream", info: stream, name: runtimeClassName(stream) }; + const mode = zlibStreamModeOf(lowerer, symbol); + if (mode !== null) return { kind: "zlib", mode, name: zlibClassName(mode) }; + return null; +} + +/** The builtin class a `const` binding names (`const E = EventEmitter`, + * `const M = Map`, `const C = zlib.Gzip`, or an alias of such a binding), + * or null. `let` and `var` bindings could be reassigned, and annotated + * bindings give the value another type, so they are not aliases. Resolved from the declaration, so uses lowered before the + * declaration statement (hoisted functions) see the same answer. */ +export function builtinClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): BuiltinClassRef | null { + const cached = lowerer.builtinClassAliases.get(symbol); + if (cached !== undefined) return cached; + // An import of an exported alias names the exporting binding. + if (symbol.flags & ts.SymbolFlags.Alias) { + lowerer.builtinClassAliases.set(symbol, null); + const target = lowerer.checker.getAliasedSymbol(symbol); + const ref = target === symbol ? null : builtinClassAliasOf(lowerer, target); + lowerer.builtinClassAliases.set(symbol, ref); + return ref; + } + const init = constInitializerOf(lowerer, symbol); + if (init === null) { + lowerer.builtinClassAliases.set(symbol, null); + return null; + } + // Marked first: a cycle through other bindings answers null. + lowerer.builtinClassAliases.set(symbol, null); + const ref = builtinClassRefOf(lowerer, init); + lowerer.builtinClassAliases.set(symbol, ref); + return ref; +} + +/** True when `symbol` is a builtin class alias whose declaration owns no + * runtime slot: new, instanceof, typeof, .name and === lower at each use, + * and other uses report SC1090. In JavaScript files, a binding whose + * initializer has a value (`const E = EventEmitter`, `const M = + * globalThis.Map`, both identity tokens) keeps its slot; only builtin + * module members (`zlib.Gzip`) and copies of slotless aliases own none. + * The `require("events").EventEmitter` and `stream.Writable` spellings + * keep their own declaration handling. */ +export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol): boolean { + if (builtinClassAliasOf(lowerer, symbol) === null) return false; + const binding = + symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; + const decl = lowerer.checker.valueDeclarationOf(binding); + if (decl === undefined || !ts.isVariableDeclaration(decl)) return false; + if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false; + if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false; + if (!isJsSourceFile(decl.getSourceFile())) return true; + let init = decl.initializer; + while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; + if (init === undefined) return true; + let source: ts.Symbol | undefined; + if (ts.isPropertyAccessExpression(init)) { + if (lowerer.builtinNamespaceModuleOf(init.expression) !== null) return true; + source = lowerer.checker.getSymbolAtLocation(init.name); + } else { + source = lowerer.checker.getSymbolAtLocation(init); + } + return source !== undefined && builtinClassAliasOwnsNoSlot(lowerer, source); +} + +/** True for a `const` declaration that is a builtin class alias owning no + * runtime slot (see builtinClassAliasOwnsNoSlot): both declaration walks + * skip it. */ +export function registerBuiltinClassAlias( + lowerer: Lowerer, + nameNode: ts.Node, + init: ts.Expression | undefined, +): boolean { + if (!ts.isIdentifier(nameNode) || init === undefined) return false; + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol); +} + +/** True when two references name the same builtin class. */ +export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean { + if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode; + if (a.kind === "collection") return b.kind === "collection" && a.which === b.which; + return b.kind === a.kind && b.info === a.info; +} + +/** The initializer of an unannotated `const` binding whose initializer + * can name a class (an identifier or a property access), or null. */ +function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null { + if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null; + const decl = lowerer.checker.valueDeclarationOf(symbol); + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + !ts.isIdentifier(decl.name) || + decl.type !== undefined + ) { + return null; + } + const list = decl.parent; + if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null; + let init = decl.initializer; + while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; + if (init === undefined) return null; + if (ts.isIdentifier(init)) return init; + if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) return init; + return null; +} + +/** Node's constructor name of a runtime class (`%TypeError` → TypeError). */ +function runtimeClassName(info: ClassInfo): string { + return info.def.name.startsWith("%") ? info.def.name.slice(1) : info.def.name; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index c9553c2f9..6de345dfe 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -132,8 +132,8 @@ export function zlibStreamModeOf( symbol: ts.Symbol | null | undefined, ): number | null { if (!symbol) return null; - const aliasMode = zlibClassAliasModeOf(lowerer, symbol, 0); - if (aliasMode !== undefined) return aliasMode; + const alias = lowerer.builtinClassAliasOf(symbol); + if (alias !== null) return alias.kind === "zlib" ? alias.mode : null; const target = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const mode = ZLIB_STREAM_MODES.get(target.name); @@ -499,80 +499,12 @@ export function lowerZlibModuleCall( throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`); } -/** The zlib stream class an expression names (`zlib.Gzip`, an imported - * `Gzip`, a `const C = zlib.Gzip` alias), as its node_zlib_mode, or null. */ -export function zlibClassModeOfExpr(lowerer: Lowerer, expr: ts.Expression): number | null { - let e = expr; - while (ts.isParenthesizedExpression(e)) e = e.expression; - if (ts.isIdentifier(e)) return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e)); - if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) { - return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e.name)); - } - return null; -} - /** The class name of a zlib stream mode (Node's constructor name). */ export function zlibClassName(mode: number): string { for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name; return "Zlib"; } -/** The mode of a `const C = zlib.Gzip` binding (or an alias of such a - * binding), null when the symbol is a const binding of something else, - * undefined when it is not a const binding. Resolved from the declaration, - * so uses lowered before the declaration statement see the same answer. */ -function zlibClassAliasModeOf( - lowerer: Lowerer, - symbol: ts.Symbol, - depth: number, -): number | null | undefined { - const cached = lowerer.zlibClassAliases.get(symbol); - if (cached !== undefined) return cached; - if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return undefined; - const decl = lowerer.checker.valueDeclarationOf(symbol); - if (decl === undefined || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) { - return undefined; - } - // const only: a var or let binding could be reassigned - const list = decl.parent; - if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) { - return undefined; - } - let init = decl.initializer; - while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; - if (init === undefined || depth > 16) return null; - let mode: number | null = null; - if (ts.isIdentifier(init)) { - const next = lowerer.checker.getSymbolAtLocation(init); - if (next !== undefined) { - const nested = zlibClassAliasModeOf(lowerer, next, depth + 1); - mode = nested === undefined ? zlibStreamModeOf(lowerer, next) : nested; - } - } else if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) { - mode = zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(init.name)); - } - if (mode !== null) lowerer.zlibClassAliases.set(symbol, mode); - return mode; -} - -/** True for `const C = zlib.Gzip` (or an alias of an alias): a - * compile-time zlib class alias that owns no runtime slot, so the - * statement lowering skips it. */ -export function registerZlibClassAlias( - lowerer: Lowerer, - nameNode: ts.Node, - init: ts.Expression | undefined, -): boolean { - if (!ts.isIdentifier(nameNode) || init === undefined) return false; - const symbol = lowerer.checker.getSymbolAtLocation(nameNode); - return symbol !== undefined && zlibClassAliasOf(lowerer, symbol) !== null; -} - -/** The mode of a `const C = zlib.Gzip` alias symbol, or null. */ -export function zlibClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): number | null { - return zlibClassAliasModeOf(lowerer, symbol, 0) ?? null; -} - /** `value instanceof zlib.Gzip` (any spelling of the class, aliases * included). zlib streams are runtime Transforms: a Transform operand asks * the runtime for the stream's mode, and an operand typed as one of diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 39b3242d6..186936f48 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -12,6 +12,11 @@ import { InternalCompilerError } from "../../errors.js"; import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js"; +import { + type BuiltinClassRef, + builtinClassAliasOf, + builtinClassRefOf, +} from "./builtins/class-values.js"; import type { FnCtx, Lowerer } from "./lowerer.js"; import { BOOL, @@ -924,12 +929,15 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void { * "stream" ambient module, EXCLUDING @types/node's (whose stream.Readable * also types child stdio — under @types/node the childStream mapping * keeps priority and the static stream classes stand down; the shipped - * fallback declarations are the supported surface). */ + * fallback declarations are the supported surface). A const binding of + * one (`const R = Readable`) names the same class (builtinClassAliasOf). */ export function builtinStreamInfoOf( lowerer: Lowerer, symbol: ts.Symbol | null | undefined, ): ClassInfo | null { if (!symbol) return null; + const alias = builtinClassAliasOf(lowerer, symbol); + if (alias !== null) return alias.kind === "stream" ? alias.info : null; if (!lowerer.isStdlibSymbol(symbol)) { // A const ALIAS of a namespace member (`const Writable = // stream.Writable` — the two-step spelling; the one-step @@ -1284,12 +1292,16 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { * `extends EventEmitter`, `x instanceof EventEmitter`) — any import * spelling (named/default/namespace member, CJS require) resolves to * the ambient class. Provenance-checked like the error classes: only a - * stdlib-file declaration inside the "events" ambient module counts. */ + * stdlib-file declaration inside the "events" ambient module counts. A + * const binding of it (`const E = EventEmitter`) names the same class + * (builtinClassAliasOf). */ export function builtinEmitterInfoOf( lowerer: Lowerer, symbol: ts.Symbol | null | undefined, ): ClassInfo | null { if (!symbol) return null; + const alias = builtinClassAliasOf(lowerer, symbol); + if (alias !== null) return alias.kind === "emitter" ? alias.info : null; if (!lowerer.isStdlibSymbol(symbol)) { // A const ALIAS of the emitter class member (`const EventEmitter = // require('node:events').EventEmitter` — commander's spelling; the @@ -1330,12 +1342,17 @@ export function builtinEmitterInfoOf( /** The builtin error ClassInfo a VALUE symbol refers to (`new Error`, * `extends TypeError`, `x instanceof RangeError`), or null. Provenance- * checked: only the standard library's declarations count — a user's own - * `class Error` resolves through classBySymbol instead. */ + * `class Error` resolves through classBySymbol instead. A const binding + * of one (`const E = TypeError`) names the same class + * (builtinClassAliasOf). */ export function builtinErrorInfoOf( lowerer: Lowerer, symbol: ts.Symbol | null | undefined, ): ClassInfo | null { - if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + if (!symbol) return null; + const alias = builtinClassAliasOf(lowerer, symbol); + if (alias !== null) return alias.kind === "error" ? alias.info : null; + if (!lowerer.isStdlibSymbol(symbol)) return null; for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null; } @@ -8435,6 +8452,12 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null; } +/** Map, Set and the error classes construct in lowerNew's identifier + * branch, which a module member alias of one (`new mod.M()`) also takes. */ +function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { + return ref?.kind === "collection" || ref?.kind === "error"; +} + export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); const worker = lowerWorkerNew(lowerer, expr); @@ -8729,7 +8752,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ts.isIdentifier(expr.expression) || (ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.name) && - lowerer.isStdlibGlobal(expr.expression.expression, "globalThis")) + (lowerer.isStdlibGlobal(expr.expression.expression, "globalThis") || + // `new mod.M()` over an exported `const M = Map` or `const E = + // TypeError` + isCollectionOrErrorClass(builtinClassRefOf(lowerer, expr.expression)))) ) { // `import C = N.C; new C()` — the alias's own source-order guards // (a no-op for every non-import= binding). @@ -9094,7 +9120,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) { return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); } - if (symbol?.name === "Map" && lowerer.isStdlibSymbol(symbol)) { + // A const alias (`const M = Map`) constructs the same Map. + if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Map")) { const seedArg = (expr.arguments?.length ?? 0) === 1 ? expr.arguments![0]! : null; const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression => ts.isArrayLiteralExpression(el) && @@ -9226,7 +9253,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } // `new Set()`: construct + bulk add from supported values, // including live collection cursors. Duplicates collapse in insertion order. - if (symbol?.name === "Set" && lowerer.isStdlibSymbol(symbol)) { + if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) { const tsType = lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); const fieldType = assignedThisFieldType(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4d03591d..09296bdfe 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -35,10 +35,12 @@ import { isZlibStreamMember, lowerZlibInstanceOf, lowerZlibStreamBytesWritten, - zlibClassAliasOf, - zlibClassModeOfExpr, - zlibClassName, } from "./builtins/compression.js"; +import { + builtinClassAliasOwnsNoSlot, + builtinClassRefOf, + sameBuiltinClass, +} from "./builtins/class-values.js"; import { constituentTypes } from "../ts7/checker.js"; import { dirname } from "node:path"; import * as posix from "node:path/posix"; @@ -616,8 +618,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (isNodeModuleValue(lowerer, expr.expression)) { return { kind: "strLit", value: "object", type: STRING, loc }; } - // `typeof zlib.Gzip`, `typeof C` over a zlib class alias. - if (zlibClassModeOfExpr(lowerer, expr.expression) !== null) { + // `typeof EventEmitter`, `typeof zlib.Gzip`, `typeof C` over a + // builtin class alias. + if (builtinClassRefOf(lowerer, expr.expression) !== null) { return { kind: "strLit", value: "function", type: STRING, loc }; } if (ts.isPropertyAccessExpression(expr.expression)) { @@ -902,13 +905,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { `a builtin function alias as an escaping value (call '${expr.text}' directly)`, ); } - // `const C = zlib.Gzip` owns no runtime slot: construction, + // `const E = EventEmitter` owns no runtime slot: construction, // instanceof, typeof and .name lower at their use sites. - if (sym && zlibClassAliasOf(lowerer, sym) !== null) { + if (sym && builtinClassAliasOwnsNoSlot(lowerer, sym)) { lowerer.unsupported( "SC1090", expr, - `a zlib class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`, + `a builtin class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`, ); } } @@ -1849,10 +1852,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const table = lowerInternTableProperty(lowerer, expr); if (table) return table; } - // `zlib.Gzip.name`, `C.name` over a zlib class alias. + // `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin + // class alias. if (expr.name.text === "name" && !expr.questionDotToken) { - const mode = zlibClassModeOfExpr(lowerer, expr.expression); - if (mode !== null) return { kind: "strLit", value: zlibClassName(mode), type: STRING, loc }; + const ref = builtinClassRefOf(lowerer, expr.expression); + if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc }; } // Expando statics may still be absent even when JS inference sees // the eventual assignment's scalar type. Preserve the checked value. @@ -10266,17 +10270,18 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr lowerUnionTypeofTest(lowerer, expr, loc); if (test) return test; } - // `C === zlib.Gzip` between two zlib class references: the classes - // are compile-time values, so the answer folds. + // `C === EventEmitter` between two builtin class references: the + // classes are compile-time values, so the answer folds. if ( op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsEqualsToken ) { - const leftClass = zlibClassModeOfExpr(lowerer, expr.left); - const rightClass = leftClass === null ? null : zlibClassModeOfExpr(lowerer, expr.right); + const leftClass = builtinClassRefOf(lowerer, expr.left); + const rightClass = leftClass === null ? null : builtinClassRefOf(lowerer, expr.right); if (leftClass !== null && rightClass !== null) { const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken; - return { kind: "boolLit", value: (leftClass === rightClass) !== negated, type: BOOL, loc }; + const same = sameBuiltinClass(leftClass, rightClass); + return { kind: "boolLit", value: same !== negated, type: BOOL, loc }; } } @@ -12352,11 +12357,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // h2 compat 'connect' narrowing (lower-server.ts): a union tag test. const sockTest = lowerSocketInstanceOf(lowerer, expr, loc); if (sockTest !== null) return sockTest; - const zlibMode = zlibClassModeOfExpr(lowerer, expr.right); - if (zlibMode !== null) return lowerZlibInstanceOf(lowerer, expr, zlibMode, loc); + const rightClass = builtinClassRefOf(lowerer, expr.right); + if (rightClass?.kind === "zlib") return lowerZlibInstanceOf(lowerer, expr, rightClass.mode, loc); - if (lowerer.isStdlibGlobal(expr.right, "Set") || lowerer.isStdlibGlobal(expr.right, "Map")) { - const collection = lowerer.isStdlibGlobal(expr.right, "Map") ? "map" : "set"; + if (rightClass?.kind === "collection") { + const collection = rightClass.which === "Map" ? "map" : "set"; const value = lowerer.lowerExpr(expr.left); if (value.type.kind === "union") { const union = lowerer.unions.get(value.type.unionId); diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 3baab9dd0..da70cd53a 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -80,7 +80,7 @@ import { createRequireProgramModuleDecl, } from "./builtins/module-bindings.js"; import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; -import { registerZlibClassAlias } from "./builtins/compression.js"; +import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; import { bindingContextualGenericFnNodeOf, @@ -1701,7 +1701,7 @@ export function collectGlobals( // resolution and value reads reuse the source's interned closure. if (isConst && registerOverloadedCallableAlias(lowerer, decl)) continue; if (isConst && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) continue; - if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) continue; + if (registerBuiltinClassAlias(lowerer, decl.name, decl.initializer)) continue; // `const f = (x) => ...` at file scope of an npm-static JS file // whose params are implicit-any: the implicit-monomorphization // twin of the generic binding above — no global exists; calls and diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 0f1b606ef..76ee0d48d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -148,7 +148,7 @@ import { } from "./builtins/module-bindings.js"; import { lowerNodeModuleCall } from "./builtins/module-resolution.js"; import { registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; -import { registerZlibClassAlias } from "./builtins/compression.js"; +import { builtinClassAliasOwnsNoSlot, registerBuiltinClassAlias } from "./builtins/class-values.js"; import { lowerEnumDeclaration } from "./lower-enums.js"; import { abstractPropertyDeclOf, @@ -847,6 +847,9 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b if (!decl.initializer) return false; const flags = ts.getCombinedNodeFlags(decl); if ((flags & ts.NodeFlags.BlockScoped) === 0 || (flags & ts.NodeFlags.Using) !== 0) return false; + // `const E = EventEmitter` has no storage to box: uses resolve at + // compile time (builtinClassAliasOf). + if (builtinClassAliasOwnsNoSlot(lowerer, symbol)) return false; // MODULE-scope bindings are pre-registered globals (collectGlobals): // references resolve through globalOf after the local search fails, so // a TDZ box here would SHADOW the global and never fill (the top-level @@ -4423,7 +4426,7 @@ export function lowerVarDecl( // reads reuse its interned closure identity. if (!isLet && registerOverloadedCallableAlias(lowerer, decl)) return null; if (!isLet && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) return null; - if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) return null; + if (registerBuiltinClassAlias(lowerer, decl.name, decl.initializer)) return null; // `const knownBy = (cmd) => ...` — an IMPLICIT-ANY function-value // binding in npm-static JS (function-scope bindings register here, in diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e19c4785d..5adeb7481 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -261,6 +261,7 @@ import { stdlibGlobalMember, nodeTypesOnlySymbol, } from "./surfaces.js"; +import { type BuiltinClassRef, builtinClassAliasOf } from "./builtins/class-values.js"; import { type FileParts, splitFiles, @@ -1858,11 +1859,11 @@ export class Lowerer { * typeParamBindings — the checker has no `T` to substitute, so the * binding rides the node-type accessor instead of mapType. */ implicitParamTypes: Map | null = null; - /** Immutable bindings that name a zlib stream class (`const C = - * zlib.Gzip`), keyed by the binding's symbol: the class's - * node_zlib_mode. No runtime slot exists: new, typeof, .name and === - * resolve at compile time, and instanceof checks the stream's mode. */ - readonly zlibClassAliases = new Map(); + /** The builtin class each binding names (`const E = EventEmitter`, + * `const C = zlib.Gzip`), keyed by the binding's symbol; null for + * bindings that name none (builtinClassAliasOf). new, instanceof, + * typeof, .name and === resolve at compile time. */ + readonly builtinClassAliases = new Map(); /** Whether some net.Socket in this program may get an encoding: a * setEncoding call whose receiver is a socket or an untyped value. * Computed once on first use (null until then). */ @@ -3073,6 +3074,10 @@ export class Lowerer { return builtinStreamInfoOf(this, symbol); } + builtinClassAliasOf(symbol: ts.Symbol): BuiltinClassRef | null { + return builtinClassAliasOf(this, symbol); + } + /** Program-wide qualified name for a top-level declaration. */ qualify(sf: ts.SourceFile, name: string): string { return `${this.fileTag.get(sf) ?? ""}${name}`; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index d394c1960..73cc58c45 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -3207,7 +3207,9 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): * `global.process`) — `declare var` globals ARE properties of * `typeof globalThis`, same symbol either way; * - a local alias binding (`const process = globalThis.process`, the - * tamper-guard prologue) registered in stdlibGlobalAliases. */ + * tamper-guard prologue) registered in stdlibGlobalAliases; + * - a const binding of the Map or Set constructor (`const M = Map`, + * builtinClassAliasOf). */ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null { if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression); if (ts.isIdentifier(expr)) { @@ -3225,6 +3227,8 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin if (!symbol) return null; const alias = lowerer.stdlibGlobalAliases.get(symbol); if (alias !== undefined) return alias; + const classAlias = lowerer.builtinClassAliasOf(symbol); + if (classAlias?.kind === "collection") return classAlias.which; const decl = lowerer.checker.declarationsOf(symbol)[0]; if (decl !== undefined && ts.isImportClause(decl) && decl.name !== undefined) { const importDecl = decl.parent; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5976-builtin-class-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5976-builtin-class-aliases.ts.json new file mode 100644 index 000000000..f83d23fc7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5976-builtin-class-aliases.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5976-builtin-class-aliases.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5977-builtin-class-aliases.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5977-builtin-class-aliases.cjs.json new file mode 100644 index 000000000..99d07e4cd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5977-builtin-class-aliases.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5977-builtin-class-aliases.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5978-builtin-class-alias-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5978-builtin-class-alias-modules/main.ts.json new file mode 100644 index 000000000..fd6bb94f5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5978-builtin-class-alias-modules/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/5978-builtin-class-alias-modules/classes.ts", + "/tests/corpus/5978-builtin-class-alias-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-class-alias-escape.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-class-alias-escape.ts.json new file mode 100644 index 000000000..f56d058f1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-class-alias-escape.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/builtin-class-alias-escape.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5976-builtin-class-aliases.ts b/tests/corpus/5976-builtin-class-aliases.ts new file mode 100644 index 000000000..692b3928d --- /dev/null +++ b/tests/corpus/5976-builtin-class-aliases.ts @@ -0,0 +1,109 @@ +// Builtin classes through const aliases: EventEmitter, the stream classes, +// the error classes, Map and Set construct, extend and test instanceof +// through an alias, and typeof, .name and === answer as in Node for the +// aliases and the direct spellings. +import { EventEmitter } from "node:events"; +import * as events from "node:events"; +import { PassThrough, Readable, Transform } from "node:stream"; +import * as stream from "node:stream"; +import { AssertionError } from "node:assert"; +import * as assert from "node:assert"; + +function lateEmitter(): boolean { + return new LateEmitter() instanceof EventEmitter && typeof LateMap === "function"; +} + +function localHoist(): string { + function inner(): string { + return `${typeof LocalMap} ${LocalMap.name} ${new LocalMap().size}`; + } + const LocalMap = Map; + return inner(); +} + +const Emitter = EventEmitter; +const NsEmitter = events.EventEmitter; +const Again = Emitter; +const LateEmitter = events.EventEmitter; +const LateMap = Map; + +const e = new Emitter(); +e.on("ping", (n: number) => console.log("ping", n)); +e.emit("ping", 1); +console.log("emitter", e instanceof Emitter, e instanceof NsEmitter, e instanceof Again); +console.log("hoisted use", lateEmitter(), localHoist()); + +class Bus extends Again { + send(text: string): void { + this.emit("text", text); + } +} +const bus = new Bus(); +bus.on("text", (text: string) => console.log("bus", text)); +bus.send("hello"); +console.log("subclass", bus instanceof Emitter, bus instanceof Bus); + +const Pass = PassThrough; +const R = Readable; +const T = stream.Transform; +const pass = new Pass(); +console.log("stream", pass instanceof Pass, pass instanceof R, pass instanceof T); +console.log("readable", new R({ read() {} }) instanceof Pass, e instanceof R); + +class Upper extends T { + override _transform( + chunk: Buffer, + _encoding: BufferEncoding, + callback: (error: Error | null, data?: string) => void, + ): void { + callback(null, chunk.toString().toUpperCase()); + } +} +const upper = new Upper(); +upper.on("data", (chunk: Buffer) => console.log("upper", chunk.toString())); +upper.end("quiet"); + +const TE = TypeError; +const RE = RangeError; +class LimitError extends RE { + constructor(limit: number) { + super(`over ${limit}`); + this.name = "LimitError"; + } +} +try { + throw new TE("bad input"); +} catch (err) { + console.log("error", err instanceof TE, err instanceof RE, err instanceof Error, (err as Error).message); +} +try { + throw new LimitError(3); +} catch (err) { + console.log("subclass error", err instanceof RE, err instanceof LimitError, (err as Error).message); +} +const AE = AssertionError; +try { + assert.strictEqual(1, 2); +} catch (err) { + console.log("assertion", err instanceof AE, err instanceof TE); +} +const viaNamespace = new assert.AssertionError({ message: "custom" }); +console.log("namespace error", viaNamespace.message, viaNamespace instanceof AE); + +const M = Map; +const S = Set; +const m = new M([ + ["a", 1], + ["b", 2], +]); +m.set("c", 3); +const s = new S([1, 2, 2, 3]); +console.log("collections", m.get("b"), m.size, s.size, m instanceof M, s instanceof S, s instanceof M); +const groups = M.groupBy([1, 2, 3, 4, 5], (n: number) => (n % 2 === 0 ? "even" : "odd")); +console.log("groupBy", groups.get("even"), groups.get("odd")); + +console.log("typeof", typeof Emitter, typeof R, typeof TE, typeof M, typeof EventEmitter, typeof Readable); +console.log("name", Emitter.name, NsEmitter.name, Pass.name, T.name, TE.name, AE.name, M.name, S.name); +console.log("direct name", EventEmitter.name, stream.Duplex.name, Error.name, RangeError.name, Map.name, Set.name); +console.log("identity", Emitter === EventEmitter, Again === NsEmitter, T === Transform, R === stream.Readable); +console.log("other class", (Pass as unknown) === (R as unknown), (M as unknown) !== (S as unknown), (TE as unknown) === (RE as unknown)); diff --git a/tests/corpus/5977-builtin-class-aliases.cjs b/tests/corpus/5977-builtin-class-aliases.cjs new file mode 100644 index 000000000..c149884ba --- /dev/null +++ b/tests/corpus/5977-builtin-class-aliases.cjs @@ -0,0 +1,45 @@ +// Builtin classes through const aliases in a CommonJS file. A copy of a +// name (or of a globalThis property) keeps its value, so it can be stored +// and compared; construction, instanceof, typeof and .name resolve to the +// class. Builtin module members construct through the alias. +const { EventEmitter } = require("node:events"); +const events = require("node:events"); +const zlib = require("node:zlib"); +const { PassThrough } = require("node:stream"); + +const Emitter = EventEmitter; +const NsEmitter = events.EventEmitter; +const Gz = zlib.Gzip; +const Gz2 = Gz; +const M = Map; +const GlobalSet = globalThis.Set; +const Pass = PassThrough; +const TE = TypeError; + +const e = new Emitter(); +e.on("ping", (n) => console.log("ping", n)); +e.emit("ping", 2); +console.log("emitter", e instanceof Emitter, e instanceof NsEmitter, e instanceof EventEmitter); + +const m = new M(); +m.set("k", 1); +const s = new GlobalSet(["a", "b", "a"]); +console.log("collections", m.get("k"), s.size, m instanceof M, s instanceof GlobalSet, m instanceof GlobalSet); + +const gz = new Gz2(); +console.log("zlib", gz instanceof Gz, gz instanceof zlib.Gunzip, Gz2 === zlib.Gzip); + +const pass = new Pass(); +console.log("stream", pass instanceof Pass, pass instanceof EventEmitter); + +try { + throw new TE("typed"); +} catch (err) { + console.log("error", err instanceof TE, err.message); +} + +const stored = [Emitter, M, GlobalSet, Pass]; +console.log("stored", stored.length, stored[0] === EventEmitter, stored[1] === Map, stored[2] === Set); +console.log("typeof", typeof Emitter, typeof NsEmitter, typeof Gz2, typeof M, typeof GlobalSet, typeof TE); +console.log("name", Emitter.name, NsEmitter.name, Gz2.name, M.name, GlobalSet.name, Pass.name, TE.name); +console.log("identity", Emitter === NsEmitter, M === Map, GlobalSet === Set, M === GlobalSet); diff --git a/tests/corpus/5978-builtin-class-alias-modules/classes.ts b/tests/corpus/5978-builtin-class-alias-modules/classes.ts new file mode 100644 index 000000000..73590d1e8 --- /dev/null +++ b/tests/corpus/5978-builtin-class-alias-modules/classes.ts @@ -0,0 +1,8 @@ +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; + +export const Emitter = EventEmitter; +export const Pass = PassThrough; +export const Dict = Map; +export const Bag = Set; +export const Failure = TypeError; diff --git a/tests/corpus/5978-builtin-class-alias-modules/main.ts b/tests/corpus/5978-builtin-class-alias-modules/main.ts new file mode 100644 index 000000000..cf39cd712 --- /dev/null +++ b/tests/corpus/5978-builtin-class-alias-modules/main.ts @@ -0,0 +1,33 @@ +// Builtin class aliases exported from one module and used through named +// and namespace imports: construction, instanceof, typeof, .name and === +// resolve to the builtin class in the importing module. +import { EventEmitter } from "node:events"; +import { Emitter, Pass, Dict, Bag, Failure } from "./classes.ts"; +import * as classes from "./classes.ts"; + +const e = new Emitter(); +e.on("tick", (n: number) => console.log("tick", n)); +e.emit("tick", 7); +console.log("emitter", e instanceof Emitter, e instanceof classes.Emitter, e instanceof EventEmitter); + +const viaNs = new classes.Emitter(); +console.log("namespace emitter", viaNs instanceof Emitter, Emitter === classes.Emitter); + +const pass = new classes.Pass(); +console.log("stream", pass instanceof Pass, pass instanceof classes.Emitter); + +const dict = new Dict([["k", 1]]); +const viaNsDict = new classes.Dict(); +viaNsDict.set("a", "b"); +const bag = new classes.Bag([1, 1, 2]); +console.log("collections", dict.get("k"), viaNsDict.get("a"), bag.size, dict instanceof classes.Dict, bag instanceof Bag); + +try { + throw new classes.Failure("wrong type"); +} catch (err) { + console.log("error", err instanceof Failure, err instanceof classes.Failure, (err as Error).message); +} + +console.log("typeof", typeof Emitter, typeof classes.Pass, typeof Dict, typeof classes.Bag, typeof Failure); +console.log("name", Emitter.name, classes.Pass.name, Dict.name, classes.Bag.name, Failure.name); +console.log("identity", Dict === Map, classes.Bag === Set, Failure === TypeError, (Dict as unknown) === (Bag as unknown)); diff --git a/tests/diagnostics/builtin-class-alias-escape.ts b/tests/diagnostics/builtin-class-alias-escape.ts new file mode 100644 index 000000000..b3f9be5d3 --- /dev/null +++ b/tests/diagnostics/builtin-class-alias-escape.ts @@ -0,0 +1,15 @@ +// A builtin class alias owns no runtime value in a TypeScript file: +// construction, instanceof, typeof and .name compile, other uses report +// at the use. A let binding of a class is not an alias. +import { EventEmitter } from "node:events"; + +const Emitter = EventEmitter; +const kinds = [Emitter]; +console.log(kinds.length); + +const M = Map; +const boxed: unknown = M; +console.log(boxed !== undefined); + +let Mutable = EventEmitter; +console.log(typeof Mutable); diff --git a/tests/diagnostics/class-values.ts b/tests/diagnostics/class-values.ts index 02c5360fa..3a53e5185 100644 --- a/tests/diagnostics/class-values.ts +++ b/tests/diagnostics/class-values.ts @@ -18,8 +18,10 @@ class Unrelated { static tag = "unrelated"; } -// Builtin classes as values: their construction is libCall-shaped. +// Builtin classes as values: a const alias resolves at compile time, and +// an array of builtin classes has no element type. const E = Error; +const errorClasses = [E]; // Classes extending builtin bases as values (the error-message completion // rule has no thunk ABI). diff --git a/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt new file mode 100644 index 000000000..9d1d981e2 --- /dev/null +++ b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt @@ -0,0 +1,22 @@ +builtin-class-alias-escape.ts:7:16 - error SC1090: a builtin class alias as an escaping value (use 'Emitter' with new, instanceof, typeof or .name) is not supported yet + + 6 | const Emitter = EventEmitter; + 7 | const kinds = [Emitter]; + | ^~~~~~~ + 8 | console.log(kinds.length); + +builtin-class-alias-escape.ts:11:24 - error SC1090: a builtin class alias as an escaping value (use 'M' with new, instanceof, typeof or .name) is not supported yet + + 10 | const M = Map; + 11 | const boxed: unknown = M; + | ^ + 12 | console.log(boxed !== undefined); + +builtin-class-alias-escape.ts:14:15 - error SC2020: 'events.EventEmitter' is part of the standard library types but has no scriptc lowering yet + + 13 | + 14 | let Mutable = EventEmitter; + | ^~~~~~~~~~~~ + 15 | console.log(typeof Mutable); + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/class-values.ts.txt b/tests/harness/__snapshots__/class-values.ts.txt index 9d5a43c1f..953d1c2d1 100644 --- a/tests/harness/__snapshots__/class-values.ts.txt +++ b/tests/harness/__snapshots__/class-values.ts.txt @@ -1,52 +1,52 @@ -class-values.ts:22:7 - error SC2020: 'ErrorConstructor' is part of the standard library types but has no scriptc lowering yet +class-values.ts:24:7 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile - 21 | // Builtin classes as values: their construction is libCall-shaped. - 22 | const E = Error; - | ^ - 23 | + 23 | const E = Error; + 24 | const errorClasses = [E]; + | ^~~~~~~~~~~~ + 25 | - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -class-values.ts:22:11 - error SC2020: 'Error' is part of the standard library types but has no scriptc lowering yet +class-values.ts:24:22 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile - 21 | // Builtin classes as values: their construction is libCall-shaped. - 22 | const E = Error; - | ^~~~~ - 23 | + 23 | const E = Error; + 24 | const errorClasses = [E]; + | ^~~ + 25 | - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -class-values.ts:44:1 - error SC1090: assigning the static 'tag' through a class value (JS creates an OWN property on the runtime class — assign through the declaring class's name, which subclasses here) is not supported yet +class-values.ts:46:1 - error SC1090: assigning the static 'tag' through a class value (JS creates an OWN property on the runtime class — assign through the declaring class's name, which subclasses here) is not supported yet - 43 | let someClass: typeof Base = Base; - 44 | someClass.tag = "write"; + 45 | let someClass: typeof Base = Base; + 46 | someClass.tag = "write"; | ^~~~~~~~~~~~~ - 45 | + 47 | -class-values.ts:48:13 - error SC1090: reading the static member 'tag' through a class value (a subclass of 'Base' redeclares it, so the runtime class decides which declaration answers) is not supported yet +class-values.ts:50:13 - error SC1090: reading the static member 'tag' through a class value (a subclass of 'Base' redeclares it, so the runtime class decides which declaration answers) is not supported yet - 47 | // runtime class decides which declaration answers. - 48 | console.log(someClass.tag); + 49 | // runtime class decides which declaration answers. + 50 | console.log(someClass.tag); | ^~~~~~~~~~~~~ - 49 | + 51 | -class-values.ts:52:10 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet +class-values.ts:54:10 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet - 51 | function make(): unknown { - 52 | return class { static value = 1; }; + 53 | function make(): unknown { + 54 | return class { static value = 1; }; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ - 53 | } + 55 | } -class-values.ts:59:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet +class-values.ts:61:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet - 58 | // evaluates. - 59 | const pair = [1, class WithStatic { static x = 1; }] as const; + 60 | // evaluates. + 61 | const pair = [1, class WithStatic { static x = 1; }] as const; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 60 | + 62 | -class-values.ts:79:56 - error SC1090: receiver-dependent static methods through a polymorphic class value is not supported yet +class-values.ts:81:56 - error SC1090: receiver-dependent static methods through a polymorphic class value is not supported yet - 78 | class OtherThis extends UsesThis {} - 79 | function self(value: typeof UsesThis): string { return value.self(); } + 80 | class OtherThis extends UsesThis {} + 81 | function self(value: typeof UsesThis): string { return value.self(); } | ^~~~~~~~~~ - 80 | console.log(self(OtherThis)); \ No newline at end of file + 82 | console.log(self(OtherThis)); \ No newline at end of file diff --git a/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt b/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt index ea9d76089..703038025 100644 --- a/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt +++ b/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt @@ -1,4 +1,4 @@ -zlib-class-alias-escape.ts:7:18 - error SC1090: a zlib class alias as an escaping value (use 'Gz' with new, instanceof, typeof or .name) is not supported yet +zlib-class-alias-escape.ts:7:18 - error SC1090: a builtin class alias as an escaping value (use 'Gz' with new, instanceof, typeof or .name) is not supported yet 6 | const Gz = zlib.Gzip; 7 | const classes = [Gz]; diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index b91073f87..5403a1468 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2190,6 +2190,9 @@ "5973-large-closure-environment.ts": 4499, "5974-typeof-module-exports.cjs": 2000, "5975-zlib-class-values.ts": 2800, + "5976-builtin-class-aliases.ts": 3100, + "5977-builtin-class-aliases.cjs": 2950, + "5978-builtin-class-alias-modules/main.ts": 2750, "5990-worker-uncloneable.mjs": 3283, "5991-worker-async-dispose.mjs": 5699, "5992-worker-thread-name-lifetime.mjs": 2687, @@ -5226,6 +5229,9 @@ "5973-large-closure-environment.ts": 5145, "5974-typeof-module-exports.cjs": 2500, "5975-zlib-class-values.ts": 4200, + "5976-builtin-class-aliases.ts": 5450, + "5977-builtin-class-aliases.cjs": 4300, + "5978-builtin-class-alias-modules/main.ts": 4450, "5990-worker-uncloneable.mjs": 22432, "5991-worker-async-dispose.mjs": 25768, "5992-worker-thread-name-lifetime.mjs": 28932, From fbcdb1d641bb2d54f34fb81db2dffa4b133acd32 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:29:28 -0700 Subject: [PATCH 029/120] Compile inline require(), computed and chained CommonJS exports, and a module's own exports object The npm compatibility probes ranked these CommonJS forms among the top blockers. They now compile with Node's semantics inside the static CommonJS model, where exports stay module globals or alias plumbing to declarations. Every remaining form keeps a refusal that names the construct. - Inline require() of program modules (cjs-inline-require.ts). `require("./x").f`, `.f(...)` and `new (require("./x").C)()` over a table module run the module's run-once %init at the call's position, then resolve the member like a binding does. `require("./x")` as a value over a single-value module is that value (`require("debug")("ns")`, table entries, construction). Inline requires are preflight module edges. An edge in a function body is lazy: it orders the dependency after the requiring module and does not close initialization cycles. Other require() calls name their construct instead of reporting an unlowerable 'Require' value. A package that nothing installed resolves throws MODULE_NOT_FOUND. - module.exports = require("./y") or a require binding (re-export), = TypeError (a builtin global), and = any computed value (`a || b`, ternary, `new X()`). Chained `exports.a = exports.b = v` evaluates v once. Export members assigned in value position (`const src = exports.src = []`) or inside functions write the export global. A fresh `exports = module.exports = {}` reset at the top of a module is a no-op. Any other rebinding of `exports` is refused by name. - A module's own exports object (cjs-exports-object.ts). Identity comparisons, typeof and truthiness of `exports` / `module.exports` use the object that top-level `this` already represents, while the module keeps its original exports. This covers the `root === module.exports` statement that used to vanish from `.call(this)` wrappers. `exports.f(x)` calls the member's export global when no assigned value can read `this`. Other value uses are refused by name, including in CommonJS files where the message used to say "ES module". - tsc interop (cjs-tsc-interop.ts). `const x_1 = __importDefault(require(...))` and `__importStar(...)` are require bindings, stamped and unstamped. The helper declarations lower to nothing once every call is such a binding. - Export tables: method entries compile, and `...require("./x")` spread entries now evaluate x at their place. Before, the module was never initialized. - TypeScript reads of members a JavaScript module attached to an exported function go through the function's own property table. - Nested require destructuring of program modules, `const { codes: { E } } = require("./errors")`, destructures the named export at the require (SC1012). `const { A } = ns` over a module binding snapshots the export. The capture-buffer fence tests merged from opt/cjs-exports-value used `root === module.exports`, which now compiles. They use eval() to keep testing the fence. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/cjs-exports-object.ts | 431 ++++++++++++++++++ .../frontend/lowering/cjs-inline-require.ts | 371 +++++++++++++++ .../src/frontend/lowering/cjs-tsc-interop.ts | 223 +++++++++ .../src/frontend/lowering/lower-calls.ts | 65 +++ .../src/frontend/lowering/lower-classes.ts | 9 + .../src/frontend/lowering/lower-exprs.ts | 140 +++++- .../src/frontend/lowering/lower-modules.ts | 85 +++- .../frontend/lowering/lower-node-module.ts | 18 + .../src/frontend/lowering/lower-stmts.ts | 195 +++++++- .../compiler/src/frontend/lowering/lowerer.ts | 47 +- packages/compiler/src/frontend/program.ts | 135 +++++- .../6150-cjs-inline-require/fallback.js | 6 + tests/corpus/6150-cjs-inline-require/inner.js | 4 + tests/corpus/6150-cjs-inline-require/late.js | 2 + tests/corpus/6150-cjs-inline-require/lib.js | 6 + tests/corpus/6150-cjs-inline-require/main.js | 55 +++ .../6150-cjs-inline-require/make-logger.js | 6 + .../6150-cjs-inline-require/package.json | 1 + tests/corpus/6150-cjs-inline-require/pick.js | 7 + tests/corpus/6150-cjs-inline-require/point.js | 6 + .../reexport-binding.js | 3 + .../reexport-inline.js | 2 + .../6150-cjs-inline-require/registry.js | 13 + .../corpus/6150-cjs-inline-require/shapes.js | 9 + tests/corpus/6150-cjs-inline-require/table.js | 4 + .../6150-cjs-inline-require/type-error.js | 1 + .../corpus/6150-cjs-inline-require/verify.js | 4 + tests/corpus/6151-cjs-export-chains/chain.js | 10 + .../6151-cjs-export-chains/constants.js | 1 + tests/corpus/6151-cjs-export-chains/main.js | 9 + .../6151-cjs-export-chains/package.json | 1 + tests/corpus/6151-cjs-export-chains/re.js | 15 + tests/corpus/6151-cjs-export-chains/sem.js | 16 + tests/corpus/6152-cjs-tsc-interop/fn.js | 3 + tests/corpus/6152-cjs-tsc-interop/main.js | 17 + .../corpus/6152-cjs-tsc-interop/package.json | 1 + tests/corpus/6152-cjs-tsc-interop/plain.js | 5 + tests/corpus/6152-cjs-tsc-interop/stamped.js | 12 + tests/corpus/6153-cjs-exports-object/lib.js | 13 + tests/corpus/6153-cjs-exports-object/main.js | 19 + .../6153-cjs-exports-object/package.json | 1 + .../constants.js | 8 + .../errors.js | 12 + .../main.js | 14 + .../package.json | 1 + .../6155-cjs-export-table-methods/lib.js | 20 + .../6155-cjs-export-table-methods/main.js | 5 + .../6155-cjs-export-table-methods/more.js | 4 + .../package.json | 1 + .../consumer.cjs | 4 + .../6156-cjs-function-members-from-ts/main.ts | 9 + .../package.json | 1 + .../status.cjs | 10 + tests/harness/corpus-costs.json | 14 + tests/harness/errors.test.ts | 88 +++- tests/harness/npm.test.ts | 8 +- 56 files changed, 2129 insertions(+), 41 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/cjs-exports-object.ts create mode 100644 packages/compiler/src/frontend/lowering/cjs-inline-require.ts create mode 100644 packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts create mode 100644 tests/corpus/6150-cjs-inline-require/fallback.js create mode 100644 tests/corpus/6150-cjs-inline-require/inner.js create mode 100644 tests/corpus/6150-cjs-inline-require/late.js create mode 100644 tests/corpus/6150-cjs-inline-require/lib.js create mode 100644 tests/corpus/6150-cjs-inline-require/main.js create mode 100644 tests/corpus/6150-cjs-inline-require/make-logger.js create mode 100644 tests/corpus/6150-cjs-inline-require/package.json create mode 100644 tests/corpus/6150-cjs-inline-require/pick.js create mode 100644 tests/corpus/6150-cjs-inline-require/point.js create mode 100644 tests/corpus/6150-cjs-inline-require/reexport-binding.js create mode 100644 tests/corpus/6150-cjs-inline-require/reexport-inline.js create mode 100644 tests/corpus/6150-cjs-inline-require/registry.js create mode 100644 tests/corpus/6150-cjs-inline-require/shapes.js create mode 100644 tests/corpus/6150-cjs-inline-require/table.js create mode 100644 tests/corpus/6150-cjs-inline-require/type-error.js create mode 100644 tests/corpus/6150-cjs-inline-require/verify.js create mode 100644 tests/corpus/6151-cjs-export-chains/chain.js create mode 100644 tests/corpus/6151-cjs-export-chains/constants.js create mode 100644 tests/corpus/6151-cjs-export-chains/main.js create mode 100644 tests/corpus/6151-cjs-export-chains/package.json create mode 100644 tests/corpus/6151-cjs-export-chains/re.js create mode 100644 tests/corpus/6151-cjs-export-chains/sem.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/fn.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/main.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/package.json create mode 100644 tests/corpus/6152-cjs-tsc-interop/plain.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/stamped.js create mode 100644 tests/corpus/6153-cjs-exports-object/lib.js create mode 100644 tests/corpus/6153-cjs-exports-object/main.js create mode 100644 tests/corpus/6153-cjs-exports-object/package.json create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/constants.js create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/errors.js create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/main.js create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/package.json create mode 100644 tests/corpus/6155-cjs-export-table-methods/lib.js create mode 100644 tests/corpus/6155-cjs-export-table-methods/main.js create mode 100644 tests/corpus/6155-cjs-export-table-methods/more.js create mode 100644 tests/corpus/6155-cjs-export-table-methods/package.json create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/main.ts create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/package.json create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/status.cjs diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts new file mode 100644 index 000000000..c3edafddc --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -0,0 +1,431 @@ +/* A CommonJS module's own exports object, seen from inside the module. + * + * The compiled module system has no reflective exports object: each export + * is a module global (collectGlobals) or alias plumbing to a declaration. + * Inside the module that is enough for + * + * - member reads and member calls (`exports.f`, `exports.f(x)`, + * `module.exports.f(x)`): they are the member's export global, the + * storage every `exports.f =` statement assigns; + * - identity comparisons of the object itself (`root === module.exports`, + * `exports === module.exports`): while the module never replaces + * `module.exports` and never rebinds `exports`, both name the module's + * original exports object, which top-level `this` already represents + * (lowerCjsTopLevelThis). `typeof` and truthiness keep their existing + * lowerings. + * + * Every other use of the object as a value (passing it to a function, + * storing it, reading a member through an alias) would need a real object + * whose properties are the exports; those uses keep a named refusal. The + * dynamic CommonJS module object is the two-tier design's S9 stage. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + cjsExportAssignmentOf, + cjsExportTargetLiteral, + isCjsJsFile, + isModuleExportsAccess, + isRequireStatement, + locOf, +} from "../program.js"; +import { isCjsModuleGlobal } from "./lower-node-module.js"; +import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js"; + +/** True for the CommonJS wrapper's `exports` binding (not shadowed by any + * source declaration) in a CommonJS JS file. */ +export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isIdentifier(node) || node.text !== "exports") return false; + if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(node); + const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + return !decls.some( + (d) => + !d.getSourceFile().isDeclarationFile && + (ts.isVariableDeclaration(d) || + ts.isParameter(d) || + ts.isBindingElement(d) || + ts.isFunctionDeclaration(d) || + ts.isClassDeclaration(d) || + ts.isImportSpecifier(d) || + ts.isNamespaceImport(d) || + ts.isImportEqualsDeclaration(d)), + ); +} + +/** `module.exports` over the CommonJS `module` global in a CommonJS file. */ +export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boolean { + return ( + isModuleExportsAccess(node) && + isCjsModuleGlobal(lowerer, node.expression) && + isCjsJsFile(node.getSourceFile(), lowerer.program) + ); +} + +/** The export global behind a same-module member access `exports.` / + * `module.exports.`, or null (identifier-valued members are alias + * plumbing with no storage, and other receivers are not the exports + * object). */ +export function cjsInFileExportGlobalOf( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrGlobal | null { + if (access.questionDotToken || !ts.isIdentifier(access.name)) return null; + const sf = access.getSourceFile(); + const recv = + isModuleExportsAccess(access.expression) || + (ts.isIdentifier(access.expression) && + access.expression.text === "exports" && + !lowerer.resolveLocal(access.expression) && + !lowerer.globalOf(access.expression)); + if (!recv || !isCjsJsFile(sf, lowerer.program)) return null; + const sym = + lowerer.checker.getSymbolAtLocation(access.name) ?? + lowerer.cjsModuleExportSymbol(sf, access.name.text); + const resolved = + sym !== undefined && sym.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(sym) + : sym; + return ( + (sym ? lowerer.globalsBySymbol.get(sym) : undefined) ?? + (resolved ? lowerer.globalsBySymbol.get(resolved) : undefined) ?? + null + ); +} + +/** Whether `node` is an operand of an equality operator: only its identity + * is observed. */ +function identityUse(node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined || !ts.isBinaryExpression(parent)) return false; + const op = parent.operatorToken.kind; + return ( + (op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.EqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken) && + (parent.left === child || parent.right === child) + ); +} + +/** Uses whose answer does not need the object: `typeof`, `void`, and + * truthiness tests (their existing lowerings answer them). */ +function answeredWithoutObject(node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isTypeOfExpression(parent) || ts.isVoidExpression(parent)) return true; + return truthinessOnly(child as ts.Expression); +} + +function truthinessOnly(node: ts.Expression): boolean { + let child: ts.Node = node; + for (let parent = node.parent; parent !== undefined; child = parent, parent = parent.parent) { + if (ts.isParenthesizedExpression(parent)) continue; + if (ts.isPrefixUnaryExpression(parent)) + return parent.operator === ts.SyntaxKind.ExclamationToken; + if ( + ts.isBinaryExpression(parent) && + (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + parent.operatorToken.kind === ts.SyntaxKind.BarBarToken) + ) + continue; + if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) + return parent.expression === child; + if (ts.isForStatement(parent)) return parent.condition === child; + if (ts.isConditionalExpression(parent)) return parent.condition === child; + return false; + } + return false; +} + +/** Which of the module's exports bindings the file ever assigns (any + * nesting, compound operators included). */ +const assignmentsCache = new WeakMap(); + +function exportsAssignments( + lowerer: Lowerer, + sf: ts.SourceFile, +): { exports: boolean; moduleExports: boolean } { + const cached = assignmentsCache.get(sf); + if (cached !== undefined) return cached; + const out = { exports: false, moduleExports: false }; + assignmentsCache.set(sf, out); + ts.walkPreorder(sf, (node) => { + // A fresh `exports = module.exports = {}` reset leaves both bindings + // naming one (new, empty) exports object for the rest of the module. + if (ts.isExpressionStatement(node) && isFreshExportsReset(lowerer, node)) return "skip"; + if ( + !ts.isBinaryExpression(node) || + node.operatorToken.kind < ts.SyntaxKind.FirstAssignment || + node.operatorToken.kind > ts.SyntaxKind.LastAssignment + ) + return undefined; + let target = node.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (isCjsModuleExports(lowerer, target)) out.moduleExports = true; + if (isCjsExportsIdentifier(lowerer, target)) out.exports = true; + return undefined; + }); + return out; +} + +/** Whether `node` still names the module's ORIGINAL exports object wherever + * it runs: `exports` unless the file rebinds it; `module.exports` unless the + * file rebinds `exports` or replaces `module.exports`. */ +function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean { + const assigned = exportsAssignments(lowerer, node.getSourceFile()); + if (assigned.exports) return false; + return ts.isIdentifier(node) || !assigned.moduleExports; +} + +/** The uses this module answers with the original exports object: identity + * comparisons and `typeof` of either spelling, and truthiness or `void` of + * `exports` (`module.exports` truthiness keeps its existing fold). */ +function objectAnsweredUse(node: ts.Expression): boolean { + if (identityUse(node)) return true; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) parent = parent.parent; + if (parent !== undefined && ts.isTypeOfExpression(parent)) return true; + return ts.isIdentifier(node) && answeredWithoutObject(node); +} + +/** Whether `node` (`exports` or `module.exports`) is a use of the module's + * exports object as a VALUE (not a member access receiver, not an + * assignment target). */ +export function isCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): boolean { + if (!isCjsExportsIdentifier(lowerer, node) && !isCjsModuleExports(lowerer, node)) return false; + let outer: ts.Node = node; + while (outer.parent !== undefined && ts.isParenthesizedExpression(outer.parent)) + outer = outer.parent; + const parent = outer.parent; + if (parent === undefined) return false; + if ( + (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && + parent.expression === outer + ) + return false; + if ( + ts.isBinaryExpression(parent) && + parent.left === outer && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + return false; + return true; +} + +/** Files whose exports object is observed by identity somewhere: their top- + * level `this` object must exist (lowerFileInit creates it). */ +export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boolean { + if (!isCjsJsFile(sf, lowerer.program)) return false; + let found = false; + ts.walkPreorder(sf, (node) => { + if ( + (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) && + isCjsExportsObjectValue(lowerer, node) && + objectAnsweredUse(node) && + namesOriginalExports(lowerer, node) + ) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** `exports` / `module.exports` used as a value inside its own module: the + * original exports object for identity, typeof and truthiness, or a named + * refusal. Null when `node` is not such a use. */ +export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null { + if (!isCjsExportsObjectValue(lowerer, node)) return null; + const sf = node.getSourceFile(); + // A module that replaces module.exports answers `module.exports` reads + // through the replacement's own machinery (the class-expression whole + // export, the replaced-exports reads). + if (!ts.isIdentifier(node) && exportsAssignments(lowerer, sf).moduleExports) return null; + const spelled = ts.isIdentifier(node) ? "exports" : "module.exports"; + const original = lowerer.cjsExportsThis.get(sf); + if (objectAnsweredUse(node) && original !== undefined && namesOriginalExports(lowerer, node)) { + return { kind: "varRef", localId: original.id, type: DYN, loc: locOf(node) }; + } + // `module.exports` truthiness keeps its existing fold (and anything the + // object is not needed for keeps whatever answered it before). + if (answeredWithoutObject(node)) return null; + lowerer.unsupported( + "SC1090", + node, + identityUse(node) + ? `comparing '${spelled}' in a module that replaces module.exports or rebinds exports (the compiled exports are a static table, not an object with an identity)` + : `'${spelled}' as a value other than a member access, comparison, typeof or truthiness test (the compiled exports are a static table, not an object: read ${spelled}.)`, + ); +} + +/** `exports = module.exports = {}` (either order) as a top-level statement + * that runs before anything could observe or extend the original exports + * object: only directives, require statements and function declarations + * precede it, and the module assigns neither binding anywhere else. Node + * swaps the original empty exports object for another empty one that both + * bindings name; the compiled module's static exports start out exactly + * the same, so the statement is a no-op (semver's internal/re.js). */ +export function isFreshExportsReset(lowerer: Lowerer, stmt: ts.ExpressionStatement): boolean { + const sf = stmt.parent; + if (!ts.isSourceFile(sf) || !isCjsJsFile(sf, lowerer.program)) return false; + let e: ts.Expression = stmt.expression; + const targets: ts.Expression[] = []; + for (;;) { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + targets.push(e.left); + e = e.right; + } + if (!ts.isObjectLiteralExpression(e) || e.properties.length !== 0) return false; + if (targets.length !== 2) return false; + const hasExports = targets.some((t) => isCjsExportsIdentifier(lowerer, t)); + const hasModuleExports = targets.some((t) => isCjsModuleExports(lowerer, t)); + if (!hasExports || !hasModuleExports) return false; + for (const prior of sf.statements) { + if (prior === stmt) break; + if (ts.isFunctionDeclaration(prior) || isRequireStatement(prior)) continue; + if (ts.isExpressionStatement(prior) && ts.isStringLiteral(prior.expression)) continue; + return false; + } + let others = 0; + ts.walkPreorder(sf, (node) => { + if ( + !ts.isBinaryExpression(node) || + node.operatorToken.kind < ts.SyntaxKind.FirstAssignment || + node.operatorToken.kind > ts.SyntaxKind.LastAssignment + ) + return undefined; + let target = node.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (isCjsModuleExports(lowerer, target) || isCjsExportsIdentifier(lowerer, target)) others++; + return undefined; + }); + return others === 2; +} + +/** The export global a WRITE `exports. = v` / `module.exports. = + * v` updates when it is not a top-level export statement (inside a + * function, or in value position): only while the module keeps its + * original exports object, which is then the object every requirer reads + * the member from. */ +export function cjsExportMemberWriteGlobalOf( + lowerer: Lowerer, + target: ts.PropertyAccessExpression, +): IrGlobal | null { + const sf = target.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program)) return null; + const assigned = exportsAssignments(lowerer, sf); + if (assigned.exports || assigned.moduleExports) return null; + return cjsInFileExportGlobalOf(lowerer, target); +} + +/** Whether a function body reads `this` (arrow functions inherit it; other + * nested functions bind their own). */ +function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean { + if (fn.body === undefined) return false; + let found = false; + ts.walkPreorder(fn.body, (node) => { + if (node !== fn.body && ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip"; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** A function declaration, or a variable initialized with a function or + * arrow expression, that never reads `this`. */ +function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean { + const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const fn = decls.find(ts.isFunctionDeclaration); + if (fn !== undefined) return !functionUsesThis(fn); + const v = decls.find(ts.isVariableDeclaration); + let init = v?.initializer; + while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; + return ( + init !== undefined && + (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) && + !functionUsesThis(init) + ); +} + +/** True when every value the module assigns to its export `name` is a + * function that never reads `this`. A call through the exports object + * (`exports.f(x)`) binds `this` to that object, which the static export + * table cannot supply, so only these calls lower; the others keep a fence. */ +export function cjsExportCallIgnoresThis( + lowerer: Lowerer, + sf: ts.SourceFile, + name: string, + depth = 0, +): boolean { + if (depth > 16) return false; + const values: ts.Expression[] = []; + ts.walkPreorder(sf, (node) => { + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(node.left) && + node.left.name.text === name && + (isCjsExportsIdentifier(lowerer, node.left.expression) || + isCjsModuleExports(lowerer, node.left.expression)) + ) { + values.push(node.right); + } + return undefined; + }); + // Entries of a `module.exports = { ... }` table (direct, or a const + // literal the statement names). + let methodEntry: ts.MethodDeclaration | null = null; + let shorthandOnly = false; + for (const stmt of sf.statements) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind !== "table") continue; + const table = cjs.obj ?? cjsExportTargetLiteral(cjs.expr.right, sf); + for (const prop of table?.properties ?? []) { + if (prop.name === undefined || !ts.isIdentifier(prop.name) || prop.name.text !== name) + continue; + if (ts.isPropertyAssignment(prop)) values.push(prop.initializer); + else if (ts.isShorthandPropertyAssignment(prop)) { + const value = lowerer.checker.getShorthandAssignmentValueSymbol(prop); + if (!symbolIsThisFreeFunction(lowerer, value)) return false; + shorthandOnly = true; + } else if (ts.isMethodDeclaration(prop)) methodEntry = prop; + } + } + if (methodEntry !== null) return values.length === 0 && !functionUsesThis(methodEntry); + if (values.length === 0) return shorthandOnly; + return values.every((v) => { + let e = v; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return !functionUsesThis(e); + if (ts.isIdentifier(e)) { + return symbolIsThisFreeFunction(lowerer, lowerer.checker.getSymbolAtLocation(e)); + } + // Another module's member (`exports.f = require("./lib").f`, or through + // a module binding): that module's own export values decide. + if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) { + const dep = ts.isIdentifier(e.expression) + ? lowerer.cjsLocalModuleBindingOf(e.expression) + ? lowerer.cjsModuleBindingDepOf(e.expression) + : null + : lowerer.cjsModuleRefDepOf(e.expression); + return dep !== null && cjsExportCallIgnoresThis(lowerer, dep, e.name.text, depth + 1); + } + return false; + }); +} diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts new file mode 100644 index 000000000..75474565b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -0,0 +1,371 @@ +/* Inline CommonJS `require("./x")` of a program module in expression + * position. A top-level `const m = require("./x")` binding is alias + * plumbing: member reads resolve to the exporter's declarations, and the + * statement lowers to the dependency's run-once %init call. An inline + * require is the same thing without the binding: the %init call runs at + * exactly the call's position (Node evaluates the module there, or + * answers from its cache), and the value is then read statically: + * + * - `require("./x").f` and `require("./x").f(...)` over a module whose + * exports are a table (member exports or `module.exports = {...}`) + * read the member like a binding would; + * - `require("./x")` as a value over a module whose whole export is one + * value (`module.exports = fn` / `= class {}` / `= make()` / `= 42` / + * `= require("./y")`) answers that value. + * + * A table module's exports object as a value has no static + * representation; it keeps a named refusal (a first-class CommonJS module + * object is the two-tier design's S9 stage). */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + canonicalBuiltinModule, + cjsExportAssignmentOf, + cjsExportDiscardReason, + cjsExportTargetLiteral, + isCjsJsFile, + isJsSourceFile, + isInlineRequireCycleCall, + isModuleExportsAccess, + locOf, + npmStaticDepSf7, + requireSpecOf, + resolveImport, +} from "../program.js"; +import { createRequireCalleeFileOf } from "./builtins/module-bindings.js"; +import { isCjsRequireGlobal } from "./lower-node-module.js"; +import { DYN, type IrExpr } from "../../ir/ir.js"; +import { nodeThrowExpr } from "../../ir/build.js"; +import { probeNodeRequireRefusal } from "../npm.js"; + +export interface InlineRequire { + call: ts.CallExpression; + spec: string; + dep: ts.SourceFile; +} + +/** A `require("")` call of the CommonJS global in a CommonJS JS + * file whose target is a compiled program module (relative, aliased, or an + * opted-in --npm-static package). Builtins, JSON documents and + * createRequire bindings answer null: their own lowerings own them. */ +export function inlineProgramRequireOf( + lowerer: Lowerer, + expr: ts.Expression, +): InlineRequire | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null; + const spec = requireSpecOf(e); + if (spec === null || canonicalBuiltinModule(spec) !== null) return null; + const sf = e.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program)) return null; + if (!isCjsRequireGlobal(lowerer, e.expression)) return null; + if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null; + const dep = + resolveImport(lowerer.program, sf, spec, "require") ?? + npmStaticDepSf7(lowerer.program, sf, spec, "require"); + if (!dep || dep.isDeclarationFile || dep.fileName.endsWith(".json")) return null; + return { call: e, spec, dep }; +} + +/** The kept (last, not discarded) top-level `module.exports = ` of a + * CommonJS module whose whole export is a single value, or null when its + * exports are a table: member exports only, or a literal / const-literal / + * Proxy-over-literal table. */ +export function cjsWholeExportValueOf( + sf: ts.SourceFile, +): { assign: ts.BinaryExpression; rhs: ts.Expression } | null { + let kept: ts.BinaryExpression | null = null; + for (const stmt of sf.statements) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) kept = cjs.expr; + } + if (kept === null) return null; + let rhs: ts.Expression = kept.right; + while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + if (ts.isObjectLiteralExpression(rhs) || cjsExportTargetLiteral(rhs, sf) !== null) return null; + return { assign: kept, rhs }; +} + +/** True when `dep`'s exports, through `module.exports = require("./y")` + * re-export chains, are a table that member reads resolve against. */ +export function cjsNamespaceModule(lowerer: Lowerer, dep: ts.SourceFile, depth = 0): boolean { + const whole = cjsWholeExportValueOf(dep); + if (whole === null) return true; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + return inner !== null && depth < 32 && cjsNamespaceModule(lowerer, inner.dep, depth + 1); +} + +/** An inline program-module require over a table module (member reads and + * member calls resolve through it), or null. */ +export function inlineNamespaceRequireOf( + lowerer: Lowerer, + expr: ts.Expression, +): InlineRequire | null { + const req = inlineProgramRequireOf(lowerer, expr); + return req !== null && cjsNamespaceModule(lowerer, req.dep) ? req : null; +} + +/** `value` evaluated after the required module's run-once %init (and its + * module-registry link) at the require's own position. */ +export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr { + if (isInlineRequireCycleCall(req.call)) { + lowerer.unsupported( + "SC1090", + req.call, + `require() of '${req.spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`, + ); + } + // Every inline program require is a preflight module edge, so its module + // has an initializer; never read its exports without evaluating it. + if (!lowerer.initNameOf.has(req.dep)) { + lowerer.unsupported( + "SC1090", + req.call, + `require() of '${req.spec}' here (the module is not part of the compiled module order)`, + ); + } + const init = lowerer.requireInitStmt(req.spec, req.call); + if (init === null) return value; + return { kind: "seqExpr", stmts: [init], result: value, type: value.type, loc: value.loc }; +} + +/** The whole-export value of `dep` read from the requirer, following + * `module.exports = require("./y")` re-exports (the inner module already + * evaluated inside dep's own %init). Null for table modules and for + * whole-export shapes with no represented value. */ +export function wholeExportValue( + lowerer: Lowerer, + dep: ts.SourceFile, + blame: ts.Node, + depth = 0, +): IrExpr | null { + const whole = cjsWholeExportValueOf(dep); + if (whole === null || depth > 32) return null; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + if (inner !== null) return wholeExportValue(lowerer, inner.dep, blame, depth + 1); + // An identifier names a declaration of the exporting module (or a + // builtin global): reading it IS the export, exactly what requirer + // bindings alias to. + if (ts.isIdentifier(whole.rhs)) return lowerer.lowerExpr(whole.rhs); + if (ts.isClassExpression(whole.rhs)) { + return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.rhs), blame); + } + const g = lowerer.globalsByDeclNode.get(whole.assign); + if (g === undefined) return null; + return { kind: "varRef", localId: g.id, type: g.type, loc: locOf(blame) }; +} + +/** `require("./x")` as a value (`require("debug")("ns")`, `new + * (require("./C"))()`, `{ verify: require("./verify") }`): the module's + * %init at this position, then its whole-export value. Null when `call` + * is not an inline program-module require. */ +export function lowerInlineRequireValue(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const req = inlineProgramRequireOf(lowerer, call); + if (req === null) return null; + const value = wholeExportValue(lowerer, req.dep, call, 0); + if (value === null) { + lowerer.unsupported( + "SC1090", + call, + cjsNamespaceModule(lowerer, req.dep) + ? `the exports object of '${req.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(req.spec)}).member directly, or bind const m = require(${JSON.stringify(req.spec)}) at the module's top level)` + : `the whole export of '${req.spec}' as a value (its 'module.exports =' value has no static representation)`, + ); + } + return withRequireInit(lowerer, req, value); +} + +/** Lowers `inner` (a member read or member call whose receiver is the + * inline require) with that require marked as handled, so the receiver + * resolves as a module binding, then sequences the module's %init first. */ +export function lowerWithInlineRequireInit( + lowerer: Lowerer, + req: InlineRequire, + inner: () => IrExpr, +): IrExpr { + lowerer.inlineRequireInitDone.add(req.call); + try { + return withRequireInit(lowerer, req, inner()); + } finally { + lowerer.inlineRequireInitDone.delete(req.call); + } +} + +/** A `require(...)` call of the CommonJS global in a CommonJS file that no + * other lowering claimed (not a builtin, JSON document, program module or + * createRequire binding): a package nothing installed resolves throws + * Node's catchable MODULE_NOT_FOUND at the call, and every other form is + * refused by name instead of as an unlowerable `require` value. */ +export function lowerUncompiledRequireCall( + lowerer: Lowerer, + call: ts.CallExpression, +): IrExpr | null { + if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null; + const sf = call.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression)) + return null; + if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null; + const spec = requireSpecOf(call); + if (spec === null) { + lowerer.unsupported( + "SC1090", + call, + 'require() with a computed specifier (the compiled module graph is fixed at build time: require("") lowers)', + ); + } + if (canonicalBuiltinModule(spec) !== null) return null; + if (!spec.startsWith(".") && !spec.startsWith("/") && !spec.startsWith("#")) { + const refusal = probeNodeRequireRefusal(sf.fileName, spec); + if (refusal !== null) { + return nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, DYN, locOf(call)); + } + lowerer.unsupported( + "SC1090", + call, + `require() of the package '${spec}' in an expression (the package is not compiled into this program)`, + ); + } + lowerer.unsupported( + "SC1090", + call, + `require() of '${spec}' in an expression (it does not resolve to a compiled program module)`, + ); +} + +/** The elements of a top-level `const { key: { ... } } = require("./x")` + * declaration over a program module whose nested pattern destructures the + * named export's value (preflight admits exactly these). Plain elements + * stay alias plumbing. */ +export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.BindingElement[] { + if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return []; + const spec = requireSpecOf(decl.initializer); + if (spec === null || canonicalBuiltinModule(spec) !== null) return []; + if (!isJsSourceFile(decl.getSourceFile())) return []; + return decl.name.elements.filter( + (e) => + e.name !== undefined && + ts.isObjectBindingPattern(e.name) && + e.propertyName !== undefined && + ts.isIdentifier(e.propertyName) && + e.initializer === undefined && + e.dotDotDotToken === undefined, + ); +} + +/** A plain method entry of a `module.exports = { ... }` table: identifier + * name, a body, not a generator. Its value is the method's function. */ +export function cjsTableMethodEntry( + prop: ts.ObjectLiteralElementLike, +): (ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }) | null { + if (!ts.isMethodDeclaration(prop) || !ts.isIdentifier(prop.name) || prop.body === undefined) + return null; + if (prop.asteriskToken !== undefined) return null; + return prop as ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }; +} + +/** `exports. = value` / `module.exports. = value` assignments in + * VALUE position inside a top-level statement (`const src = exports.src = + * []`), outside function bodies and outside the statement's own export + * chain (`statementExport`, which the statement-level machinery owns). */ +export function nestedCjsExportAssignmentsOf( + stmt: ts.Statement, + statementExport: ts.BinaryExpression | undefined, +): { name: ts.Identifier; value: ts.Expression }[] { + if (!ts.isVariableStatement(stmt) && !ts.isExpressionStatement(stmt)) return []; + const owned = new Set(); + for (let e: ts.Expression | undefined = statementExport; e !== undefined;) { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + owned.add(e); + e = e.right; + } + const out: { name: ts.Identifier; value: ts.Expression }[] = []; + ts.walkPreorder(stmt, (node) => { + if ( + node !== stmt && + (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) + ) + return "skip"; + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + !owned.has(node) && + ts.isPropertyAccessExpression(node.left) && + node.left.questionDotToken === undefined && + ts.isIdentifier(node.left.name) && + ((ts.isIdentifier(node.left.expression) && node.left.expression.text === "exports") || + isModuleExportsAccess(node.left.expression)) + ) { + out.push({ name: node.left.name, value: node.right }); + } + return undefined; + }); + return out; +} + +/** `const { A, B: b } = ns` at a module's top level, where `ns` is a + * CommonJS module binding over a table module (picomatch's `const { + * MAX_LENGTH, ... } = constants`): each name takes a snapshot of the named + * export's value at this statement, exactly like destructuring the exports + * object would. Null for every other declaration; the plain elements and + * their key names otherwise. */ +export function cjsNamespaceDestructureOf( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): { dep: ts.SourceFile; elements: { name: ts.Identifier; key: string }[] } | null { + if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return null; + const stmt = decl.parent?.parent; + if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) + return null; + if (!isJsSourceFile(stmt.parent)) return null; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isIdentifier(init) || !lowerer.cjsLocalModuleBindingOf(init)) return null; + const dep = lowerer.cjsModuleBindingDepOf(init); + if (dep === null) return null; + const elements: { name: ts.Identifier; key: string }[] = []; + for (const e of decl.name.elements) { + if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return null; + if (e.name === undefined || !ts.isIdentifier(e.name)) return null; + const key = + e.propertyName === undefined + ? e.name.text + : ts.isIdentifier(e.propertyName) || ts.isStringLiteral(e.propertyName) + ? e.propertyName.text + : null; + if (key === null) return null; + elements.push({ name: e.name, key }); + } + return { dep, elements }; +} + +/** The value of `dep`'s export `key` read from another module: its export + * global, or the function/class declaration it aliases. Null when it has no + * static value. */ +export function cjsExportValueByName( + lowerer: Lowerer, + dep: ts.SourceFile, + key: string, + blame: ts.Node, +): IrExpr | null { + let sym = lowerer.cjsModuleExportSymbol(dep, key); + if (sym === undefined) return null; + if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); + const direct = lowerer.globalsBySymbol.get(sym); + if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) }; + for (const d of lowerer.checker.declarationsOf(sym)) { + const byDecl = lowerer.globalsByDeclNode.get(d); + if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) }; + // Alias plumbing: a shorthand/identifier table entry or a member + // export naming a declaration. Reading that identifier IS the value. + if (ts.isShorthandPropertyAssignment(d)) return lowerer.lowerShorthandValue(d); + if (ts.isPropertyAssignment(d) && ts.isIdentifier(d.initializer)) { + return lowerer.lowerExpr(d.initializer); + } + if ((ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d)) && d.name !== undefined) { + return lowerer.lowerExpr(d.name); + } + } + return null; +} diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts new file mode 100644 index 000000000..f7955fc23 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -0,0 +1,223 @@ +/* tsc's CommonJS import interop: `const x_1 = __importDefault(require("./m"))` + * and `const x_1 = __importStar(require("./m"))` at a module's top level, + * with the helpers tsc emits at the top of the file: + * + * var __importDefault = (this && this.__importDefault) || function (mod) { + * return (mod && mod.__esModule) ? mod : { "default": mod }; + * }; + * + * For a CommonJS dependency the helper's answer is fixed by the + * dependency's source: stamped with `__esModule` (tsc output), the binding + * IS the module's exports; otherwise `x_1.default` is the module's exports + * (and __importStar also copies every export onto x_1). Both are the + * static alias model a plain `const x_1 = require("./m")` already has, so + * the binding lowers as a require binding: the statement is the module's + * %init, `x_1.f` / `x_1.default.f` read the exports statically, and + * `x_1.default` over a single-value module is that value. The helper + * declarations themselves are dead once every call is such a binding (the + * self-probe `this.__importDefault` reads the pristine exports object, + * which never has the name), so they lower to nothing. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + isCjsJsFile, + isEsModuleStamp, + npmStaticDepSf7, + requireSpecOf, + resolveImport, +} from "../program.js"; +import { cjsNamespaceModule } from "./cjs-inline-require.js"; + +const HELPERS = new Set([ + "__importDefault", + "__importStar", + "__createBinding", + "__setModuleDefault", +]); + +export interface TscInteropBinding { + helper: "__importDefault" | "__importStar"; + spec: string; + dep: ts.SourceFile; + /** The dependency stamps `__esModule`: the binding IS its exports. */ + stamped: boolean; +} + +/** `var = (this && this.) || ` at a module's top + * level: the tsc helper declaration shape. */ +function tscHelperDeclName(stmt: ts.Statement): string | null { + if (!ts.isVariableStatement(stmt)) return null; + const decls = stmt.declarationList.declarations; + if (decls.length !== 1) return null; + const d = decls[0]!; + if (!ts.isIdentifier(d.name) || !HELPERS.has(d.name.text) || d.initializer === undefined) + return null; + let init: ts.Expression = d.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.BarBarToken) + return null; + let probe: ts.Expression = init.left; + while (ts.isParenthesizedExpression(probe)) probe = probe.expression; + if ( + !ts.isBinaryExpression(probe) || + probe.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken || + probe.left.kind !== ts.SyntaxKind.ThisKeyword || + !ts.isPropertyAccessExpression(probe.right) || + probe.right.expression.kind !== ts.SyntaxKind.ThisKeyword || + probe.right.name.text !== d.name.text + ) + return null; + // The fallback is the helper body: a function, or (newer tsc output) a + // conditional or IIFE producing one. It never runs when the helper is dead. + return d.name.text; +} + +/** True when the dependency stamps itself as an ES module translation. */ +function stampsEsModule(dep: ts.SourceFile): boolean { + return dep.statements.some((s) => { + if (!ts.isExpressionStatement(s)) return false; + if (isEsModuleStamp(s.expression)) return true; + const e = s.expression; + return ( + ts.isBinaryExpression(e) && + e.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(e.left) && + ts.isIdentifier(e.left.expression) && + e.left.expression.text === "exports" && + e.left.name.text === "__esModule" && + e.right.kind === ts.SyntaxKind.TrueKeyword + ); + }); +} + +/** The interop binding a top-level `const x = __importDefault(require(s))` / + * `__importStar(require(s))` declaration makes, or null. The helper must be + * the module's own tsc helper declaration and the dependency a compiled + * CommonJS program module. */ +export function tscInteropBindingOfDecl( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): TscInteropBinding | null { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; + const stmt = decl.parent?.parent; + if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) + return null; + const sf = stmt.parent; + if (!isCjsJsFile(sf, lowerer.program)) return null; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isCallExpression(init) || init.arguments.length !== 1) return null; + const callee = init.expression; + if (!ts.isIdentifier(callee)) return null; + if (callee.text !== "__importDefault" && callee.text !== "__importStar") return null; + if (!sf.statements.some((s) => tscHelperDeclName(s) === callee.text)) return null; + const spec = requireSpecOf(init.arguments[0]!); + if (spec === null) return null; + const dep = + resolveImport(lowerer.program, sf, spec, "require") ?? + npmStaticDepSf7(lowerer.program, sf, spec, "require"); + if (!dep || dep.isDeclarationFile || !isCjsJsFile(dep, lowerer.program)) return null; + return { + helper: callee.text as TscInteropBinding["helper"], + spec, + dep, + stamped: stampsEsModule(dep), + }; +} + +/** The interop binding an identifier reads, or null. */ +export function tscInteropBindingOf( + lowerer: Lowerer, + expr: ts.Expression, +): TscInteropBinding | null { + if (!ts.isIdentifier(expr)) return null; + const sym = lowerer.checker.getSymbolAtLocation(expr); + const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined; + return decl ? tscInteropBindingOfDecl(lowerer, decl) : null; +} + +/** The module whose exports `expr` denotes through an interop binding: + * the binding itself when the dependency is stamped (or for __importStar, + * whose copy carries every export), and `binding.default` when it is not. + * Null for anything else. */ +export function tscInteropModuleOf( + lowerer: Lowerer, + expr: ts.Expression, +): TscInteropBinding | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + const direct = tscInteropBindingOf(lowerer, e); + if (direct !== null) { + return direct.stamped || direct.helper === "__importStar" ? direct : null; + } + if (ts.isPropertyAccessExpression(e) && !e.questionDotToken && e.name.text === "default") { + const via = tscInteropBindingOf(lowerer, e.expression); + if (via !== null && !via.stamped) return via; + } + return null; +} + +/** `binding.default` over an unstamped single-value dependency: that value + * (the caller lowers the dependency's whole export). */ +export function tscInteropDefaultValueOf( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): TscInteropBinding | null { + if (expr.questionDotToken || expr.name.text !== "default") return null; + const via = tscInteropBindingOf(lowerer, expr.expression); + if (via === null || via.stamped) return null; + return cjsNamespaceModule(lowerer, via.dep) ? null : via; +} + +/** True for a tsc helper declaration every reference to which is a + * recognized interop binding's callee, its own self-probe, or a reference + * from another such dead helper: the statement lowers to nothing. */ +export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean { + const name = tscHelperDeclName(stmt); + if (name === null) return false; + const sf = stmt.getSourceFile(); + const helperSpans = new Map(); + for (const s of sf.statements) { + const n = tscHelperDeclName(s); + if (n !== null) helperSpans.set(n, s); + } + const dead = new Set(); + for (let changed = true; changed;) { + changed = false; + for (const [helper, decl] of helperSpans) { + if (dead.has(helper)) continue; + let live = false; + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || node.text !== helper) return undefined; + const parent = node.parent; + if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) + return undefined; + const at = node.getStart(sf); + if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined; + for (const [other, otherDecl] of helperSpans) { + if (other !== helper && dead.has(other)) { + if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined; + } + } + const call = parent; + const binding = call?.parent; + if ( + call !== undefined && + ts.isCallExpression(call) && + call.expression === node && + binding !== undefined && + ts.isVariableDeclaration(binding) && + tscInteropBindingOfDecl(lowerer, binding) !== null + ) + return undefined; + live = true; + return "stop"; + }); + if (!live) { + dead.add(helper); + changed = true; + } + } + } + return dead.has(name); +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2037ba17c..213e605bc 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -24,6 +24,13 @@ import { lowerZlibStreamMethodCall, } from "./builtins/compression.js"; import type { Lowerer } from "./lowerer.js"; +import { + inlineNamespaceRequireOf, + lowerInlineRequireValue, + lowerUncompiledRequireCall, + lowerWithInlineRequireInit, +} from "./cjs-inline-require.js"; +import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js"; import { lowerGenMethodCall } from "./lower-generators.js"; import { lowerClassSymbolMethodCall } from "./symbol-methods.js"; import { errorToStringCall } from "./error-methods.js"; @@ -234,6 +241,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const json = lowerJsonRequireCall(lowerer, expr); if (json) return json; + // Inline require() of a program module (cjs-inline-require.ts): a + // member call over its export table resolves like a binding's member + // call after the module's %init at this position; the require itself + // as a value answers the module's whole-export value. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + // Node calls the member with `this` bound to the module's exports. + if ( + ts.isIdentifier(expr.expression.name) && + !cjsExportCallIgnoresThis(lowerer, req.dep, expr.expression.name.text) + ) { + lowerer.unsupported( + "SC1090", + expr, + `calling '${expr.expression.name.text}' through require(${JSON.stringify(req.spec)}) when the function may read 'this' (Node binds it to the module's exports object, which compiles to a static table — bind the module and call a local function instead)`, + ); + } + return lowerWithInlineRequireInit(lowerer, req, () => lowerCall(lowerer, expr)); + } + } + { + const inlineValue = lowerInlineRequireValue(lowerer, expr); + if (inlineValue) return inlineValue; + } + // A call whose chain ROOTS at an ambient-undefined name (`declare // const value: Y | undefined; value?.foo("a")`, `declare function // chain...; chain(o).mapValues(f).value()`, a trap binding's read): @@ -310,6 +343,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerImportMetaResolveCall(lowerer, expr) ?? lowerRequireResolveCall(lowerer, expr); if (resolved) return resolved; } + // Any other require() call of the CommonJS global: MODULE_NOT_FOUND for + // packages nothing installed resolves, a named refusal otherwise. + { + const uncompiled = lowerUncompiledRequireCall(lowerer, expr); + if (uncompiled) return uncompiled; + } // `process.getuid?.()` — intercepted BEFORE the optional-chain // machinery (the member always exists on a POSIX target, so the @@ -1274,6 +1313,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const args = completeFuncValueArgs(lowerer, expr, callee.type, loc); return { kind: "callValue", callee, args, type: callee.type.ret, loc }; } + // `exports.f(x)` / `module.exports.f(x)` inside the module: a call of + // the member's export global (cjs-exports-object.ts), the value every + // `exports.f =` statement assigns. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.questionDotToken) { + const g = cjsInFileExportGlobalOf(lowerer, expr.expression); + if ( + g && + (g.type.kind === "func" || g.type.kind === "dyn") && + !cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text) + ) { + lowerer.unsupported( + "SC1090", + expr, + `calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`, + ); + } + if (g && (g.type.kind === "func" || g.type.kind === "dyn")) { + const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc }; + const checked = lowerCheckedCallableValue(lowerer, expr, callee, loc); + if (checked) return checked; + if (callee.type.kind === "func") { + const args = completeFuncValueArgs(lowerer, expr, callee.type, loc); + return { kind: "callValue", callee, args, type: callee.type.ret, loc }; + } + } + } if (ts.isPropertyAccessExpression(expr.expression)) { const intrinsic = fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index fcc54b32b..a18fb83a3 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -13,6 +13,7 @@ import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js"; import type { FnCtx, Lowerer } from "./lowerer.js"; +import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js"; import { BOOL, DATE_T, @@ -8445,6 +8446,14 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); + // `new (require("./x").C)()`: the module's %init at the require's + // position, then construction through the member as for a binding. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + return lowerWithInlineRequireInit(lowerer, req, () => lowerNew(lowerer, expr)); + } + } const worker = lowerWorkerNew(lowerer, expr); if (worker) return worker; const readlineUi = lowerReadlineUiNew(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4d03591d..a50b2ee00 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -43,6 +43,18 @@ import { constituentTypes } from "../ts7/checker.js"; import { dirname } from "node:path"; import * as posix from "node:path/posix"; import type { Lowerer } from "./lowerer.js"; +import { + inlineNamespaceRequireOf, + lowerWithInlineRequireInit, + wholeExportValue, +} from "./cjs-inline-require.js"; +import { tscInteropDefaultValueOf } from "./cjs-tsc-interop.js"; +import { + cjsExportMemberWriteGlobalOf, + cjsInFileExportGlobalOf, + isCjsExportsIdentifier, + lowerCjsExportsObjectValue, +} from "./cjs-exports-object.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js"; import { checkedArmExtraction } from "./coercions/unions.js"; import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js"; @@ -205,6 +217,7 @@ import { isNodeModuleValue, lowerCjsTopLevelThis, lowerNodeModuleIdentifier, + isCjsModuleGlobal, lowerNodeModuleProperty, lowerRequireCacheElement, lowerRequireCacheHas, @@ -858,6 +871,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const moduleValue = lowerNodeModuleIdentifier(lowerer, expr); if (moduleValue) return moduleValue; + if (expr.text === "exports") { + const exportsValue = lowerCjsExportsObjectValue(lowerer, expr); + if (exportsValue) return exportsValue; + } if (lowerer.isSelfReference(expr)) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; } @@ -1836,6 +1853,28 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + // `require("./x").member` over a program module's export table: the + // module's %init at this position, then the member read exactly like + // a `const m = require("./x")` binding's (cjs-inline-require.ts). + if (!expr.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + return lowerWithInlineRequireInit(lowerer, req, () => lowerExpr(lowerer, expr)); + } + // `module.exports` compared by identity inside its own module, or + // refused by name as any other value (cjs-exports-object.ts). + if (expr.name.text === "exports") { + const exportsValue = lowerCjsExportsObjectValue(lowerer, expr); + if (exportsValue) return exportsValue; + } + // tsc's `x_1.default` over a dependency whose whole export is one + // value: that value (cjs-tsc-interop.ts). + const interop = tscInteropDefaultValueOf(lowerer, expr); + if (interop !== null) { + const value = wholeExportValue(lowerer, interop.dep, expr); + if (value !== null) return value; + } + } { const member = lifetimeRegionMemberOf(lowerer.program, expr, lowerer.experimentalFiles); if (member !== null) @@ -2181,35 +2220,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // global (pure alias plumbing) and fall through unchanged; write // positions belong to the export-assignment machinery. { - const sf = expr.getSourceFile(); - const cjsMemberRecv = - isModuleExportsAccess(expr.expression) || - (ts.isIdentifier(expr.expression) && - expr.expression.text === "exports" && - !lowerer.resolveLocal(expr.expression) && - !lowerer.globalOf(expr.expression)); const writePos = ts.isBinaryExpression(expr.parent) && expr.parent.left === expr && expr.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; - if ( - !expr.questionDotToken && - cjsMemberRecv && - !writePos && - isCjsJsFile(sf, lowerer.program) - ) { - const sym = - lowerer.checker.getSymbolAtLocation(expr.name) ?? - lowerer.cjsModuleExportSymbol(sf, expr.name.text); - const resolved = - sym !== undefined && sym.flags & ts.SymbolFlags.Alias - ? lowerer.checker.getAliasedSymbol(sym) - : sym; - const g = - (sym ? lowerer.globalsBySymbol.get(sym) : undefined) ?? - (resolved ? lowerer.globalsBySymbol.get(resolved) : undefined); - if (g) return { kind: "varRef", localId: g.id, type: g.type, loc }; - } + const g = cjsInFileExportGlobalOf(lowerer, expr); + if (g && !writePos) return { kind: "varRef", localId: g.id, type: g.type, loc }; } // Namespace-qualified reads (`N.x`, `A.B.C.f`, import= alias // chains): the member is a compile-time-known declaration of a @@ -2805,6 +2821,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ); } } + // A member a JavaScript module attached to one of its functions + // (`status.message = codes; module.exports = status`), read from + // TypeScript: JavaScript stores such members on the function's own + // property table (`fn.member = v` writes through the function's + // checked-dynamic face), so the read takes the same path and enters + // the static code at the type inference gave the member. + if (recvLowered.type.kind === "func" && jsFunctionExpandoMember(lowerer, expr)) { + const read: IrExpr = { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, + type: DYN, + loc, + }; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); + return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read; + } lowerer.unsupported( "SC1090", expr, @@ -10151,6 +10184,35 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // RHS value is the expression's value — its OWN type, so the // consumer sees exactly what the checker typed. if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) { + // `const src = exports.src = []`: an export member assigned in + // value position writes its export global and yields the value. + { + let inFunction = false; + for (let p: ts.Node | undefined = expr.parent; p !== undefined; p = p.parent) { + if (ts.isFunctionLike(p) || ts.isClassDeclaration(p) || ts.isClassExpression(p)) { + inFunction = true; + break; + } + } + const g = inFunction + ? cjsExportMemberWriteGlobalOf(lowerer, expr.left) + : cjsInFileExportGlobalOf(lowerer, expr.left); + if (g) { + const value = lowerer.lowerExprExpecting(expr.right, g.type); + const tmp = lowerer.declareHiddenLocal("%exportValue", value.type); + const result = varRef(tmp.id, value.type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: tmp.id, init: value, loc }, + { kind: "assign", localId: g.id, value: result, loc }, + ], + result, + type: value.type, + loc, + }; + } + } const recv = tryLowerExpression(lowerer, expr.left.expression); if (recv && recv.type.kind === "dyn") return lowerDynMemberAssignment(lowerer, expr, recv); const unionWrite = lowerUnionFieldWrite(lowerer, expr.left, expr.right); @@ -14977,6 +15039,9 @@ export function fieldTarget( access: ts.PropertyAccessExpression, ): FieldTarget | null { if (lowerer.cjsLocalModuleBindingOf(access.expression)) return null; + // `module.exports` / `module.id`: CommonJS module surface, never a field. + if (isCjsModuleGlobal(lowerer, access.expression)) return null; + if (isCjsExportsIdentifier(lowerer, access.expression)) return null; if (lowerer.chainBlocked(access)) return null; if ( access.name.text === "stackTraceLimit" && @@ -16263,3 +16328,28 @@ function isBuiltinModuleValueDecl(decl: ts.Node): boolean { ts.isVariableDeclaration(decl)) ); } + +/** True when `access` reads a member that JavaScript sources attach to a + * function by assignment (`fn.member = value`): the receiver's type is + * callable and every declaration of the member is such an assignment in a + * JS file. */ +function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + if (access.questionDotToken || !ts.isIdentifier(access.name)) return false; + const recvType = lowerer.typeOf(access.expression); + if (lowerer.checker.getCallSignatures(recvType).length === 0) return false; + const member = lowerer.checker.getPropertyOfType(recvType, access.name.text); + const decls = member ? lowerer.checker.declarationsOf(member) : []; + return ( + decls.length > 0 && + decls.every((d) => { + if (!isJsSourceFile(d.getSourceFile())) return false; + const assign = ts.isBinaryExpression(d) ? d : d.parent; + return ( + assign !== undefined && + ts.isBinaryExpression(assign) && + assign.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(assign.left) + ); + }) + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 3baab9dd0..c114bea4f 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -18,7 +18,9 @@ import { isRelativeSpecifier } from "../workspace-registry.js"; import { canonicalBuiltinModule, cjsExportAssignmentOf, + cjsExportChainOf, cjsExportDiscardReason, + cjsExportTargetLiteral, entryPackageFilePredicate, isCjsJsFile, isJsSourceFile, @@ -26,6 +28,7 @@ import { locOf, makeCycleAdmission, orderedImportsOf, + requireSpecOf, resolveImport, resolveNpmImport, } from "../program.js"; @@ -36,6 +39,13 @@ import { withCycleInitFlags, } from "./cycle-initialization.js"; import { markRequireEarlyBindings } from "./require-initialization.js"; +import { + cjsTableMethodEntry, + inlineProgramRequireOf, + nestedCjsExportAssignmentsOf, + requireNestedPatternsOf, +} from "./cjs-inline-require.js"; +import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js"; import { invalidJsonModuleDiag, @@ -1391,6 +1401,11 @@ export function collectGlobals( deferJsCollectionDiags(lowerer, sf, diagsBefore); } }; + // Export members assigned in VALUE position at the module's top + // level (`const src = exports.src = []`) are exports too. + for (const nested of nestedCjsExportAssignmentsOf(stmt, cjs?.expr)) { + registerExport(nested.name, nested.name.text, nested.value); + } if (cjs?.kind === "member" && ts.isIdentifier(cjs.name)) { // `exports.C = C` naming a CLASS declaration: alias plumbing — // no storage (a class VALUE global would fence for @@ -1398,6 +1413,13 @@ export function collectGlobals( // symbol (cjsMemberExportClassSymbol) and the class registry // applies unchanged. if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) continue; + // A chain `exports.a = exports.b = value`: every member is an + // export holding the chain's one value. + const chain = cjsExportChainOf(cjs.expr); + if (chain !== null) { + for (const nameNode of chain.names) registerExport(nameNode, nameNode.text, chain.value); + continue; + } registerExport(cjs.name, cjs.name.text, cjs.value); continue; } @@ -1429,7 +1451,22 @@ export function collectGlobals( // statement lowering evaluates the call exactly once at its // source position and assigns that slot. Unmappable calls retain // the existing statement-level fence. - const factoryValued = ts.isCallExpression(rhs); + // Any other computed value (`os.homedir || function () {…}`, + // `cond ? a : b`, `new Registry()`, `ns.member`) is the same single + // snapshot: Node stores whatever the expression produced. A + // require() of a program module is not: the exporter re-exports + // that module's exports, which requirers resolve through the + // checker's alias (the statement lowers to the dep's %init). + const factoryValued = + !ts.isIdentifier(rhs) && + !ts.isClassExpression(rhs) && + !ts.isObjectLiteralExpression(rhs) && + !( + requireSpecOf(rhs) !== null && + inlineProgramRequireOf(lowerer, rhs) !== null && + !sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + ) && + cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { const diagsBefore = lowerer.diags.length; try { @@ -1464,6 +1501,10 @@ export function collectGlobals( if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { registerExport(prop.name, prop.name.text, prop.initializer); } + // A METHOD entry (`module.exports = { f(x) { … } }`) is an + // expression-valued export whose value is the method's function. + const method = cjsTableMethodEntry(prop); + if (method !== null) registerExport(method.name, method.name.text, method.name); // other property forms fence in the statement lowering } continue; @@ -1527,6 +1568,16 @@ export function collectGlobals( collectNestedVarGlobals(lowerer, sf, stmt, tag); continue; } + // tsc's interop bindings and its then-dead helper declarations own no + // storage either (cjs-tsc-interop.ts). + if ( + isJsSourceFile(sf) && + (isDeadTscHelperDecl(lowerer, stmt) || + (stmt.declarationList.declarations.length === 1 && + tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null)) + ) { + continue; + } // CommonJS require declarations are alias plumbing (preflight owns // the module edges; resolveValueSymbol routes reads through to the // exporter's declarations): no storage, and the statement lowers to @@ -1549,6 +1600,38 @@ export function collectGlobals( stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); } } + // `const { codes: { ERR_X } } = require("./errors")`: the outer + // keys stay alias plumbing, but a nested pattern destructures the + // export's value at the require, so its bindings own storage like + // any destructured declaration (lowerVarDeclarator assigns them). + if (ts.isVariableStatement(stmt)) { + const isLetList = + (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || + (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; + for (const decl of stmt.declarationList.declarations) { + for (const element of requireNestedPatternsOf(decl)) { + for (const nameNode of boundIdentifiersOf(element.name!)) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); + const type = mapped && mapped.kind !== "void" ? mapped : DYN; + const g: IrGlobal = { + id: `%g.${tag}${nameNode.text}`, + name: nameNode.text, + type, + mutable: isLetList, + source: bindingSource(nameNode), + ...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl) + ? { tdz: true as const } + : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + } + } + } continue; } // `declare const __VERSION__: string` — AMBIENT: no storage exists diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index c15749601..292b38e87 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -8,6 +8,7 @@ import * as win32 from "node:path/win32"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; +import { lowerCjsExportsObjectValue } from "./cjs-exports-object.js"; import { isCjsJsFile, isNodeEsmFile, locOf } from "../program.js"; import { runtimePathForTarget } from "../runtime-resolve.js"; import { @@ -177,6 +178,12 @@ export function cjsModuleRef(lowerer: Lowerer, node: ts.Node): IrExpr | null { return id === undefined ? null : numLit(id, locOf(node)); } +/** True when `node` is the CommonJS `require` global (not shadowed by a + * source binding, and not in an ES module). */ +export function isCjsRequireGlobal(lowerer: Lowerer, node: ts.Expression): boolean { + return cjsGlobal(lowerer, node, "require"); +} + export function isCjsModuleGlobal(lowerer: Lowerer, node: ts.Expression): boolean { return cjsGlobal(lowerer, node, "module"); } @@ -428,6 +435,13 @@ export function lowerNodeModuleIdentifier(lowerer: Lowerer, expr: ts.Identifier) if (!isCjsModuleGlobal(lowerer, expr)) return null; const ref = cjsModuleRef(lowerer, expr); if (ref) return ref; + if (isCjsJsFile(expr.getSourceFile(), lowerer.program)) { + lowerer.unsupported( + "SC1090", + expr, + "the CommonJS 'module' object in this position (module.exports members, the whole module.exports in comparisons, and the module metadata fields lower)", + ); + } lowerer.unsupported("SC1090", expr, "the CommonJS 'module' object in an ES module"); } @@ -441,6 +455,10 @@ export function lowerNodeModuleProperty( const member = expr.name.text; if (member === "exports" && direct && moduleExportsAlwaysTruthy(lowerer, expr)) return boolLit(true, locOf(expr)); + if (member === "exports" && direct) { + const value = lowerCjsExportsObjectValue(lowerer, expr); + if (value) return value; + } if (member === "exports" || member === "require") return null; let moduleRef = direct ? cjsModuleRef(lowerer, expr.expression) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index f35df085c..72f49a80b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -15,6 +15,19 @@ import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; +import { + cjsExportValueByName, + cjsNamespaceDestructureOf, + cjsTableMethodEntry, + inlineProgramRequireOf, + requireNestedPatternsOf, +} from "./cjs-inline-require.js"; +import { + cjsExportMemberWriteGlobalOf, + isCjsExportsIdentifier, + isFreshExportsReset, +} from "./cjs-exports-object.js"; +import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { arrayValueRead, arrayValueStore, @@ -60,6 +73,7 @@ import { import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; import { cjsExportAssignmentOf, + cjsExportChainOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, @@ -1553,6 +1567,16 @@ function lowerVarDeclarator( const init = lowerer.requireInitStmt(target.spec, decl); return init ? [init] : []; } + // tsc's interop bindings (`const x_1 = __importDefault(require("./m"))`) + // are require bindings (cjs-tsc-interop.ts): the module's %init here. + if (ts.isSourceFile(stmt.parent)) { + const interop = tscInteropBindingOfDecl(lowerer, decl); + if (interop !== null) { + const init = lowerer.requireInitStmt(interop.spec, decl); + return init ? [init] : []; + } + if (isDeadTscHelperDecl(lowerer, stmt)) return []; + } // CommonJS require declarations (JS files): at the module's top // level the BINDINGS are alias plumbing (no storage; // resolveValueSymbol routes the reads), but the require itself is @@ -1573,7 +1597,21 @@ function lowerVarDeclarator( if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); if (ts.isSourceFile(stmt.parent)) { const init = lowerer.requireInitStmt(spec, decl); - return init ? [init] : []; + const out: IrStmt[] = init ? [init] : []; + // Nested patterns destructure the named export's value right after + // the module evaluated (`const { codes: { ERR_X } } = require(...)`). + for (const element of requireNestedPatternsOf(decl)) { + const key = element.propertyName as ts.Identifier; + const value = lowerer.lowerExpr(key); + lowerer.lowerBindingPattern( + element.name as ts.ObjectBindingPattern, + () => lowerer.lowerExpr(key), + value.type, + isLet, + out, + ); + } + return out; } // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync } // = require('child_process'); ... }` — test/common's lazy-getter @@ -1600,6 +1638,35 @@ function lowerVarDeclarator( // `const { createSign } = crypto` over a builtin NAMESPACE binding: // alias plumbing (builtinImportOf routes the reads) — no statement. if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return []; + // `const { A, B } = ns` over a CommonJS module binding: each name + // snapshots the named export's value here (cjs-inline-require.ts). + { + const nsDestructure = cjsNamespaceDestructureOf(lowerer, decl); + if (nsDestructure !== null) { + const out: IrStmt[] = []; + for (const element of nsDestructure.elements) { + const target = lowerer.globalOf(element.name); + const value = cjsExportValueByName(lowerer, nsDestructure.dep, element.key, element.name); + if (target === null || value === null) { + lowerer.unsupported( + "SC1090", + element.name, + `destructuring the export '${element.key}' of a CommonJS module binding (it has no static value)`, + ); + } + out.push({ + kind: "assign", + localId: target.id, + value: typeEquals(value.type, target.type) + ? value + : lowerer.coerceInto(element.name, value, target.type), + loc: locOf(element.name), + ...("tdz" in target && target.tdz ? { initializes: true as const } : {}), + }); + } + return out; + } + } return lowerer.lowerDestructuringDecl(decl, isLet); } // STORED protocol values with statically closed representations: @@ -6049,6 +6116,49 @@ function lowerCjsExportStatement( if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue; root = lowerer.globalsByDeclNode.get(whole.expr); } + // A CHAIN `exports.a = exports.b = value`: Node evaluates the value + // once, assigns the innermost member first, and every member holds + // the same value. Each member is its own export global + // (collectGlobals registers the inner ones too). + { + const chain = cjsExportChainOf(cjs.expr); + if (chain !== null && root?.type.kind !== "dyn") { + const globalOfName = (nameNode: ts.Identifier): IrGlobal => { + const symbol = + lowerer.checker.getSymbolAtLocation(nameNode) ?? + lowerer.cjsModuleExportSymbol(sf, nameNode.text); + const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; + if (!g) { + lowerer.lowerExpr(chain.value); + lowerer.badType(chain.value, lowerer.typeOf(chain.value)); + } + return g; + }; + const inner = chain.names[chain.names.length - 1]!; + const innerG = globalOfName(inner); + const body: IrStmt[] = [ + { + kind: "assign", + localId: innerG.id, + value: lowerer.lowerExprExpecting(chain.value, innerG.type), + loc: locOf(chain.value), + }, + ]; + for (let i = chain.names.length - 2; i >= 0; i--) { + const g = globalOfName(chain.names[i]!); + const read: IrExpr = { kind: "varRef", localId: innerG.id, type: innerG.type, loc }; + body.push({ + kind: "assign", + localId: g.id, + value: typeEquals(g.type, innerG.type) + ? read + : lowerer.coerceInto(chain.value, read, g.type), + loc, + }); + } + return { kind: "block", body, loc }; + } + } return assignExport(cjs.name, cjs.value, root); } let tableObj = cjs.obj; @@ -6114,6 +6224,30 @@ function lowerCjsExportStatement( body.push(assignExport(prop.name, prop.initializer)); continue; } + // METHOD entries: the export global holds the method's function, + // created at this statement like a function-expression entry. Node + // calls it with `this` bound to the table, which the static export + // table cannot represent, so a body that uses `this` keeps a fence. + { + const method = cjsTableMethodEntry(prop); + if (method !== null && !resolvedTarget) { + lowerer.rejectThisInObjectMethod(method.body); + const symbol = + lowerer.checker.getSymbolAtLocation(method.name) ?? + lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), method.name.text); + const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; + if (g) { + const fn = lowerer.lowerLambda(method); + body.push({ + kind: "assign", + localId: g.id, + value: typeEquals(fn.type, g.type) ? fn : lowerer.coerceInto(method, fn, g.type), + loc: locOf(method), + }); + continue; + } + } + } // ACCESSOR entries: no storage and no code here — reads lift the // getter as a module-level function and call it per read // (cjsExportAccessorRead); writes through the setter keep their @@ -6129,7 +6263,15 @@ function lowerCjsExportStatement( if (ts.isSpreadAssignment(prop)) { let inner: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(inner)) inner = inner.expression; - if (requireSpecOf(inner) !== null) continue; + if (requireSpecOf(inner) !== null) { + // The spread still EVALUATES the module, at its place among the + // table's entries (a program module joins the graph through this + // inline require; cjs-inline-require.ts). + const req = inlineProgramRequireOf(lowerer, inner); + const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null; + if (init !== null && !resolvedTarget) body.push(init); + continue; + } } lowerer.unsupported( "SC1090", @@ -6162,6 +6304,16 @@ function lowerCjsSingleValueExport( ): IrStmt | null { let rhs: ts.Expression = expr.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + // `module.exports = require("./x")`: the module re-exports x's exports. + // Requirers resolve through the checker's alias to x's own export + // machinery; this statement is x's evaluation at this position. + { + const req = inlineProgramRequireOf(lowerer, rhs); + // With later member exports (`module.exports.extra = …`) the module + // extends that object, so it keeps the factory root registration. + if (req !== null && !lowerer.globalsByDeclNode.has(expr)) + return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc }; + } if (ts.isIdentifier(rhs)) { const sym = lowerer.resolveValueSymbol(rhs); if (!sym) return null; @@ -6172,6 +6324,17 @@ function lowerCjsSingleValueExport( ) { return { kind: "block", body: [], loc }; } + // A require binding (`const x = require("./x"); module.exports = x`) + // re-exports that module exactly like the inline form (the binding's + // own statement already evaluated it), and a builtin global + // (`module.exports = TypeError`) is the global itself: requirer + // bindings alias straight through to it, so nothing is stored. + if ( + lowerer.checker.declarationsOf(sym).some((d) => ts.isSourceFile(d)) || + lowerer.isStdlibSymbol(sym) + ) { + return { kind: "block", body: [], loc }; + } const g = lowerer.globalsBySymbol.get(sym); if (g) { if (g.mutable) { @@ -6393,6 +6556,21 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); } if (topLevelJs && ts.isExpressionStatement(stmtNode)) { + if (isFreshExportsReset(lowerer, stmtNode)) + return { kind: "block", body: [], loc: locOf(expr) }; + // Any other `exports = …` rebinds the wrapper's exports binding: the + // members later attached through it belong to the new value. + if ( + ts.isBinaryExpression(expr) && + expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isCjsExportsIdentifier(lowerer, expr.left) + ) { + lowerer.unsupported( + "SC1090", + expr.left, + "rebinding 'exports' (exports = module.exports = value): members attached through 'exports' afterwards are properties of that value, which the static export table does not model — write module.exports. = … and module.exports = value", + ); + } const cjs = cjsExportAssignmentOf(stmtNode); if (cjs) return lowerCjsExportStatement(lowerer, cjs); // The tsc CJS interop stamp — `Object.defineProperty(exports, @@ -6934,6 +7112,19 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm const parts = lowerDestructuringAssignParts(lowerer, expr.left, expr.right, locOf(expr)); return { kind: "block", body: parts.stmts, loc: locOf(expr) }; } + // `exports.x = v` inside a function updates the member's export + // global (cjs-exports-object.ts). + if (ts.isPropertyAccessExpression(expr.left)) { + const g = cjsExportMemberWriteGlobalOf(lowerer, expr.left); + if (g) { + return { + kind: "assign", + localId: g.id, + value: lowerer.lowerExprExpecting(expr.right, g.type), + loc: locOf(expr), + }; + } + } if (!ts.isIdentifier(expr.left)) { lowerer.unsupported("SC1090", expr.left, "assignment to non-variables"); } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 9df0f01a9..46267b85d 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -272,6 +272,13 @@ import { appendWorkerModules, } from "./lower-modules.js"; import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js"; +import { + cjsNamespaceModule, + inlineNamespaceRequireOf, + inlineProgramRequireOf, +} from "./cjs-inline-require.js"; +import { tscInteropModuleOf } from "./cjs-tsc-interop.js"; +import { usesCjsExportsIdentity } from "./cjs-exports-object.js"; import { registerBuiltinEventTargetClasses } from "./lower-event-target.js"; import { type ClassInfo, @@ -1863,6 +1870,9 @@ export class Lowerer { /** Per CommonJS file using top-level `this`: the global holding the * object that stands for its original exports (lowerCjsTopLevelThis). */ readonly cjsExportsThis = new Map(); + /** Inline program-module require calls whose %init the enclosing member + * read or member call is already sequencing (cjs-inline-require.ts). */ + readonly inlineRequireInitDone = new Set(); /** Bindings whose unchecked string arithmetic can produce either a number * (NaN for an absent read) or a string. Preserve that result union at bare * reads instead of applying the checker-bare string narrowing adapter. */ @@ -3093,8 +3103,10 @@ export class Lowerer { ident.parent.name === ident ) { const recv = ident.parent.expression; - if (ts.isIdentifier(recv) && this.cjsLocalModuleBindingOf(recv)) { - const dep = this.localModuleBindingDepOf(recv); + if (this.cjsLocalModuleBindingOf(recv)) { + const dep = ts.isIdentifier(recv) + ? this.localModuleBindingDepOf(recv) + : this.cjsModuleRefDepOf(recv); const exported = dep ? this.cjsModuleExportSymbol(dep, ident.text) : undefined; // A createRequire call is typed as `any`, so tsgo can manufacture a // transient property symbol with no relation to the target module. @@ -3486,6 +3498,21 @@ export class Lowerer { return dep; } + /** The table module a non-identifier module reference names: an inline + * `require("./x")` (cjs-inline-require.ts) or tsc's unstamped + * `x_1.default` interop read (cjs-tsc-interop.ts). */ + cjsModuleRefDepOf(expr: ts.Expression): ts.SourceFile | null { + const inline = inlineNamespaceRequireOf(this, expr); + if (inline !== null) return inline.dep; + const interop = tscInteropModuleOf(this, expr); + return interop !== null && cjsNamespaceModule(this, interop.dep) ? interop.dep : null; + } + + /** The table module a CommonJS module binding identifier names. */ + cjsModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null { + return this.localModuleBindingDepOf(expr); + } + /** The compiled module behind a require-like namespace binding: an * ordinary CommonJS require declaration, a canonical createRequire * declaration, or an ESM default import of CommonJS. One resolver keeps @@ -3498,6 +3525,10 @@ export class Lowerer { if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer) return null; + { + const interop = tscInteropModuleOf(this, expr); + if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null; + } const directSpec = requireSpecOf(decl.initializer); if (directSpec !== null) { if (isJsonRequireDecl(this, decl)) return null; @@ -3544,6 +3575,10 @@ export class Lowerer { if (wholeAssignment === undefined) return null; let rhs: ts.Expression = wholeAssignment.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + // A pure re-export (`module.exports = require("./core")`) is the inner + // module's export table, read like any namespace binding. + if (inlineProgramRequireOf(this, rhs) !== null && !this.globalsByDeclNode.has(wholeAssignment)) + return null; if (ts.isIdentifier(rhs)) { const value = this.checker.getSymbolAtLocation(rhs); if (value) { @@ -3596,7 +3631,9 @@ export class Lowerer { * symbols → resolveValueSymbol); the bare value keeps the * namespace-object fence, like ESM namespace imports of builtins. */ cjsLocalModuleBindingOf(expr: ts.Expression): boolean { - if (!ts.isIdentifier(expr)) return false; + // An inline `require("./x")` over a table module reads its members like + // a binding (cjs-inline-require.ts sequences the module's %init). + if (!ts.isIdentifier(expr)) return this.cjsModuleRefDepOf(expr) !== null; const sym = this.checker.getSymbolAtLocation(expr); if (this.localModuleBindingDepOf(expr) === null) return false; if (this.createRequireWholeExportSymbolOf(expr) !== null) return false; @@ -3609,6 +3646,8 @@ export class Lowerer { // the object literal) keep the namespace reading — member accesses // resolve through the table's property symbols. if (sym && sym.flags & ts.SymbolFlags.Alias) { + // `module.exports = TypeError`: the binding IS the builtin global. + if (this.isStdlibSymbol(this.checker.getAliasedSymbol(sym))) return false; const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0]; if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false; if (d && ts.isVariableDeclaration(d)) { @@ -3706,7 +3745,7 @@ export class Lowerer { const id = `%g.${tag}%loaded`; this.moduleGuardOf.set(fp.sf, id); this.globalsList.push({ id, name: "%loaded", type: BOOL, mutable: true }); - if (usesCjsTopLevelThis(this, fp.sf)) { + if (usesCjsTopLevelThis(this, fp.sf) || usesCjsExportsIdentity(this, fp.sf)) { const exportsThis: IrGlobal = { id: `%g.${tag}%exportsThis`, name: "%exportsThis", diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index df0cb673e..158bb2e59 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1837,6 +1837,36 @@ function nestedBareRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { return out; } +/** INLINE `require("...")` calls in expression position anywhere in a file + * (`require("./x").f`, `require("debug")("ns")`, `module.exports = + * require("./y")`, `{ v: require("./v") }`): each evaluates its module at + * that position, exactly like a bare require statement. Top-level require + * statements and bare require statements are collected by their own + * scans; a require that initializes a nested declaration keeps its + * lowering fence and is not an edge. */ +function inlineRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { + const out: ts.CallExpression[] = []; + ts.walkPreorder(sf, (n) => { + if (!ts.isCallExpression(n) || requireSpecOf7(n) === null) return; + let outer: ts.Node = n; + while ( + outer.parent !== undefined && + (ts.isParenthesizedExpression(outer.parent) || + ts.isAsExpression(outer.parent) || + ts.isTypeAssertion(outer.parent) || + ts.isNonNullExpression(outer.parent)) + ) { + outer = outer.parent; + } + const parent = outer.parent; + if (parent !== undefined && ts.isExpressionStatement(parent)) return; + if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.initializer === outer) + return; + out.push(n); + }); + return out; +} + type RequireCallBindingKind7 = "ambient" | "createRequire" | "source"; function stripRequireCasts7(expr: ts.Expression): ts.Expression { @@ -2403,6 +2433,24 @@ function nsBindingUsesAreBareStatements7( export interface CycleEdge { dep: ts.SourceFile; stmt?: ts.ImportDeclaration | ts.ExportDeclaration; + /** An inline require() in a deferred position (a function body): Node + * evaluates the dependency when that code runs, after this module's own + * top level, so the edge orders the dependency after the module and + * never closes a cycle through the module's initialization. */ + lazy?: true; + /** The inline require() call behind the edge (inline edges only). */ + call?: ts.CallExpression; +} + +/** Inline require() calls whose edge closes a require cycle. Node answers + * them with the still-evaluating module's partial exports, which the static + * export globals do not model; their lowering refuses them by name at run + * time instead of refusing the whole program (the inline call kept its + * runtime fence before it became a module edge). */ +const inlineRequireCycleCalls = new WeakSet(); + +export function isInlineRequireCycleCall(call: ts.Node): boolean { + return inlineRequireCycleCalls.has(call); } /** True when the reference at `node` can only evaluate AFTER the module @@ -4860,6 +4908,21 @@ function preflight7(load: LoadResult): { pattern.elements.every((e) => { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; if (e.name !== undefined && ts.isIdentifier(e.name)) return true; + // A nested pattern under a named export of a program module + // destructures that export's VALUE at the require + // (`const { codes: { ERR_X } } = require("./errors")`): the + // inner bindings are ordinary destructured storage. + if ( + module === null && + builtin === null && + bindingKind !== "createRequire" && + pattern === req.decl!.name && + e.name !== undefined && + ts.isObjectBindingPattern(e.name) && + e.propertyName !== undefined && + ts.isIdentifier(e.propertyName) + ) + return true; if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name)) return false; const key = e.propertyName; @@ -4973,6 +5036,32 @@ function preflight7(load: LoadResult): { continue; } } + // Inline requires of program modules are module edges like the bare + // statements above (cjs-inline-require.ts lowers each to the module's + // %init at its position). Only the CommonJS global in a CommonJS file + // counts, and targets outside the compiled graph add nothing here: + // their use sites keep their existing lowering fences. + if (jsRequireFile && !isNodeEsmFile7(sf, program)) { + for (const call of inlineRequiresOf7(sf)) { + if (requireCallBindingKind7(program, call) !== "ambient") continue; + const spec = requireSpecOf7(call)!; + if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue; + const lazy = inDeferredPosition7(call); + const edgeTo = (dep: ts.SourceFile): CycleEdge => + lazy ? { dep, lazy: true, call } : { dep, call }; + const projectDep = resolveImport7(program, sf, spec, "require"); + if (projectDep !== null) { + if (!projectDep.fileName.endsWith(".json")) deps.push(edgeTo(projectDep)); + continue; + } + if (isRelativeSpecifier(spec) || spec.startsWith("#")) continue; + const npmReq = resolveNpmImport7(sf.fileName, spec, "require"); + if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { + const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); + if (nDep !== null) deps.push(edgeTo(nDep)); + } + } + } } } @@ -4999,7 +5088,10 @@ function preflight7(load: LoadResult): { // reads are all let/const bindings with module storage — the // lowering checks each one (cycleEarlyBindings) and throws Node's // ReferenceError on a read before the declaration ran. - const cycleAdmissionReason = makeCycleAdmission(program, (sf) => edges.get(sf) ?? []); + const cycleAdmissionReason = makeCycleAdmission(program, (sf) => { + const all = edges.get(sf) ?? []; + return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all; + }); const order: ts.SourceFile[] = []; const state = new Map(); const stack: string[] = []; @@ -5008,9 +5100,14 @@ function preflight7(load: LoadResult): { stack.push(sf.fileName); for (const e of edges.get(sf) ?? []) { if (e.dep === sf) continue; // self-import: a Node cache hit, not a cycle + if (e.lazy === true) continue; // visited after this module completes const s = state.get(e.dep); if (s === "done") continue; if (s === "visiting") { + if (e.call !== undefined) { + inlineRequireCycleCalls.add(e.call); + continue; + } // A back-edge: Node answers it from the cache (the module is // already evaluating) — admitted by the cheap per-edge rule, the // declaration-only init window rule, or the exact initialization @@ -5034,6 +5131,11 @@ function preflight7(load: LoadResult): { stack.pop(); state.set(sf, "done"); order.push(sf); + // Lazy inline requires run after this module's top level: their + // modules join the order afterwards (cache hits when already loaded). + for (const e of edges.get(sf) ?? []) { + if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep); + } }; visit(entry); @@ -5830,6 +5932,37 @@ export function cjsExportAssignmentOf( return null; } +/** The members and shared value of a CHAINED member export + * `exports.a = exports.b = module.exports.c = value` (outermost name + * first), or null when the assignment is not a chain of at least two + * identifier-named export members or its value is undefined. */ +export function cjsExportChainOf( + e: ts.BinaryExpression, +): { names: ts.Identifier[]; value: ts.Expression } | null { + const names: ts.Identifier[] = []; + let cur: ts.Expression = e; + for (;;) { + while (ts.isParenthesizedExpression(cur)) cur = cur.expression; + if (!ts.isBinaryExpression(cur) || cur.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + const left = cur.left; + if ( + !ts.isPropertyAccessExpression(left) || + left.questionDotToken || + !ts.isIdentifier(left.name) + ) + break; + const recv = left.expression; + if (!((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))) break; + names.push(left.name); + cur = cur.right; + } + // tsc's `exports.a = exports.b = void 0;` preamble is not a value chain + // (the export-statement lowering owns it). + const isVoid = + cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined"); + return names.length >= 2 && !isVoid ? { names, value: cur } : null; +} + /** Why Node would DISCARD this CommonJS export statement, or null when it * survives (Node's export-object identity rules — checker-free, pure * AST): `module.exports = {...}` REPLACES the export object, so members diff --git a/tests/corpus/6150-cjs-inline-require/fallback.js b/tests/corpus/6150-cjs-inline-require/fallback.js new file mode 100644 index 000000000..d89da1a00 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/fallback.js @@ -0,0 +1,6 @@ +const preferred = undefined; +module.exports = + preferred || + function fallback() { + return "fallback"; + }; diff --git a/tests/corpus/6150-cjs-inline-require/inner.js b/tests/corpus/6150-cjs-inline-require/inner.js new file mode 100644 index 000000000..7aa065c1e --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/inner.js @@ -0,0 +1,4 @@ +console.log("inner: evaluated"); +exports.inner = function () { + return "inner"; +}; diff --git a/tests/corpus/6150-cjs-inline-require/late.js b/tests/corpus/6150-cjs-inline-require/late.js new file mode 100644 index 000000000..a28525b26 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/late.js @@ -0,0 +1,2 @@ +console.log("late: evaluated"); +exports.value = "late value"; diff --git a/tests/corpus/6150-cjs-inline-require/lib.js b/tests/corpus/6150-cjs-inline-require/lib.js new file mode 100644 index 000000000..94a5872a4 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/lib.js @@ -0,0 +1,6 @@ +console.log("lib: evaluated"); +exports.f = function (x) { + return x * 10; +}; +exports.n = 7; +exports.counter = { hits: 0 }; diff --git a/tests/corpus/6150-cjs-inline-require/main.js b/tests/corpus/6150-cjs-inline-require/main.js new file mode 100644 index 000000000..0ebc20d36 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/main.js @@ -0,0 +1,55 @@ +// Inline require() of program modules: each call evaluates its module at +// exactly that position (or answers from the cache), and the value is the +// module's exports, read statically. +console.log("main: start"); +const f = require("./lib.js").f; +console.log("main: first inline require done", f(1)); +console.log("main: cached", require("./lib.js").n, require("./lib.js").f(2)); +exports.viaInline = require("./lib.js").f; +console.log("main: exported member", exports.viaInline(3), exports.viaInline === f); + +// Mutation through an inline read is visible to every requirer. +require("./lib.js").counter.hits++; +const lib = require("./lib.js"); +console.log("main: shared counter", lib.counter.hits, require("./lib.js").counter === lib.counter); + +// A whole-export function called directly off the require. +const debug = require("./make-logger.js")("app"); +console.log(debug("ready")); + +// Construction through a whole-export class and through a member class. +const p = new (require("./point.js"))(1, 2); +console.log("point", p.x + p.y); +const q = new (require("./shapes.js").Square)(3); +console.log("square", q.area()); + +// Lazy inline require: the module loads at the first call, once. +function late() { + return require("./late.js").value; +} +console.log("main: before late"); +console.log("late", late(), late()); + +// Member calls off an inline require of a table module. +console.log("table member", require("./table.js").twice(4), require("./table.js").label); + +// Table entries holding inline requires. +const table = { verify: require("./verify.js"), sign: require("./lib.js").f }; +console.log("table", table.verify("tok"), table.sign(4)); + +// Re-exports: module.exports = require(...) and through a binding. +const a = require("./reexport-inline.js"); +const b = require("./reexport-binding.js"); +console.log("reexports", a.inner(), b.inner(), a.inner === b.inner); + +// Single computed values as the whole export. +const T = require("./type-error.js"); +try { + throw new T("bad input"); +} catch (e) { + console.log(e instanceof TypeError, String(e)); +} +const pick = require("./pick.js"); +console.log("pick", pick(5), typeof require("./fallback.js"), require("./fallback.js")()); +const reg = require("./registry.js"); +console.log("registry", reg.size(), reg.add("x"), reg.size(), require("./registry.js") === reg); diff --git a/tests/corpus/6150-cjs-inline-require/make-logger.js b/tests/corpus/6150-cjs-inline-require/make-logger.js new file mode 100644 index 000000000..f698b54a7 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/make-logger.js @@ -0,0 +1,6 @@ +console.log("make-logger: evaluated"); +module.exports = function (ns) { + return function (msg) { + return ns + ": " + msg; + }; +}; diff --git a/tests/corpus/6150-cjs-inline-require/package.json b/tests/corpus/6150-cjs-inline-require/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6150-cjs-inline-require/pick.js b/tests/corpus/6150-cjs-inline-require/pick.js new file mode 100644 index 000000000..aadc1eb8e --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/pick.js @@ -0,0 +1,7 @@ +function double(x) { + return x * 2; +} +function triple(x) { + return x * 3; +} +module.exports = process.env.SCRIPTC_CORPUS_NEVER_SET ? triple : double; diff --git a/tests/corpus/6150-cjs-inline-require/point.js b/tests/corpus/6150-cjs-inline-require/point.js new file mode 100644 index 000000000..0f002800c --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/point.js @@ -0,0 +1,6 @@ +module.exports = class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } +}; diff --git a/tests/corpus/6150-cjs-inline-require/reexport-binding.js b/tests/corpus/6150-cjs-inline-require/reexport-binding.js new file mode 100644 index 000000000..e8307cc65 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/reexport-binding.js @@ -0,0 +1,3 @@ +const inner = require("./inner.js"); +console.log("reexport-binding: evaluated"); +module.exports = inner; diff --git a/tests/corpus/6150-cjs-inline-require/reexport-inline.js b/tests/corpus/6150-cjs-inline-require/reexport-inline.js new file mode 100644 index 000000000..26b6d44ea --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/reexport-inline.js @@ -0,0 +1,2 @@ +console.log("reexport-inline: evaluated"); +module.exports = require("./inner.js"); diff --git a/tests/corpus/6150-cjs-inline-require/registry.js b/tests/corpus/6150-cjs-inline-require/registry.js new file mode 100644 index 000000000..e34b1015f --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/registry.js @@ -0,0 +1,13 @@ +class Registry { + constructor() { + this.items = []; + } + add(item) { + this.items.push(item); + return this.items.length; + } + size() { + return this.items.length; + } +} +module.exports = new Registry(); diff --git a/tests/corpus/6150-cjs-inline-require/shapes.js b/tests/corpus/6150-cjs-inline-require/shapes.js new file mode 100644 index 000000000..4dd410384 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/shapes.js @@ -0,0 +1,9 @@ +class Square { + constructor(side) { + this.side = side; + } + area() { + return this.side * this.side; + } +} +exports.Square = Square; diff --git a/tests/corpus/6150-cjs-inline-require/table.js b/tests/corpus/6150-cjs-inline-require/table.js new file mode 100644 index 000000000..ce8f1e364 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/table.js @@ -0,0 +1,4 @@ +function twice(x) { + return x * 2; +} +module.exports = { twice, label: "table" }; diff --git a/tests/corpus/6150-cjs-inline-require/type-error.js b/tests/corpus/6150-cjs-inline-require/type-error.js new file mode 100644 index 000000000..483613b44 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/type-error.js @@ -0,0 +1 @@ +module.exports = TypeError; diff --git a/tests/corpus/6150-cjs-inline-require/verify.js b/tests/corpus/6150-cjs-inline-require/verify.js new file mode 100644 index 000000000..7adb1f64a --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/verify.js @@ -0,0 +1,4 @@ +console.log("verify: evaluated"); +module.exports = function verify(token) { + return "verified " + token; +}; diff --git a/tests/corpus/6151-cjs-export-chains/chain.js b/tests/corpus/6151-cjs-export-chains/chain.js new file mode 100644 index 000000000..533e35ba8 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/chain.js @@ -0,0 +1,10 @@ +let evaluations = 0; +function make() { + evaluations++; + return function () { + return "shared"; + }; +} +exports.a = exports.b = make(); +module.exports.c = exports.d = module.exports.e = 5; +exports.evaluations = evaluations; diff --git a/tests/corpus/6151-cjs-export-chains/constants.js b/tests/corpus/6151-cjs-export-chains/constants.js new file mode 100644 index 000000000..7a67f9f21 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/constants.js @@ -0,0 +1 @@ +exports.VERSION = "1.2.3"; diff --git a/tests/corpus/6151-cjs-export-chains/main.js b/tests/corpus/6151-cjs-export-chains/main.js new file mode 100644 index 000000000..394c5e3d2 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/main.js @@ -0,0 +1,9 @@ +// Chained member exports share one value, evaluated once; a fresh +// `exports = module.exports = {}` at the top of a module is a no-op reset. +const chain = require("./chain.js"); +console.log(chain.a(), chain.b(), chain.a === chain.b, chain.evaluations); +console.log(chain.c, chain.d, chain.e); +const re = require("./re.js"); +console.log(re.src.length, re.t.FULL, re.tokens.join(",")); +const sem = require("./sem.js"); +console.log(sem.src.join(" "), sem.re.join(","), sem.count()); diff --git a/tests/corpus/6151-cjs-export-chains/package.json b/tests/corpus/6151-cjs-export-chains/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6151-cjs-export-chains/re.js b/tests/corpus/6151-cjs-export-chains/re.js new file mode 100644 index 000000000..8839b5cb4 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/re.js @@ -0,0 +1,15 @@ +'use strict' +const constants = require("./constants.js"); +exports = module.exports = {} +const src = []; +const t = {}; +exports.src = src; +exports.t = t; +exports.tokens = []; +function createToken(name, value) { + t[name] = src.length; + src.push(value); + exports.tokens.push(name); +} +createToken("FULL", "^" + constants.VERSION + "$"); +createToken("LOOSE", "v?" + constants.VERSION); diff --git a/tests/corpus/6151-cjs-export-chains/sem.js b/tests/corpus/6151-cjs-export-chains/sem.js new file mode 100644 index 000000000..f12b5b25f --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/sem.js @@ -0,0 +1,16 @@ +'use strict' +// semver's internal/re.js shape: export members assigned in value position. +exports = module.exports = {} +const re = exports.re = [] +const src = exports.src = [] +const t = exports.t = {} +let R = 0 +const createToken = (name, value) => { + const index = R++ + t[name] = index + src[index] = value + re[index] = value.length +} +exports.count = () => R +createToken('MAIN', 'x.y.z') +createToken('LOOSE', 'v?x') diff --git a/tests/corpus/6152-cjs-tsc-interop/fn.js b/tests/corpus/6152-cjs-tsc-interop/fn.js new file mode 100644 index 000000000..09b227f5b --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/fn.js @@ -0,0 +1,3 @@ +module.exports = function square(x) { + return x * x; +}; diff --git a/tests/corpus/6152-cjs-tsc-interop/main.js b/tests/corpus/6152-cjs-tsc-interop/main.js new file mode 100644 index 000000000..0924dce35 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/main.js @@ -0,0 +1,17 @@ +"use strict"; +// tsc's CommonJS output: interop helpers over require(), the void 0 +// preamble, and the (0, x.default)() call spelling. +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = void 0; +console.log("main: start"); +const plain_1 = __importDefault(require("./plain.js")); +const stamped_1 = __importDefault(require("./stamped.js")); +const fn_1 = __importDefault(require("./fn.js")); +function run() { + return [plain_1.default.greet("a"), (0, stamped_1.default)("b"), stamped_1.helper(), (0, fn_1.default)(2)].join(" "); +} +exports.run = run; +console.log(run(), plain_1.default.count, typeof fn_1.default); diff --git a/tests/corpus/6152-cjs-tsc-interop/package.json b/tests/corpus/6152-cjs-tsc-interop/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6152-cjs-tsc-interop/plain.js b/tests/corpus/6152-cjs-tsc-interop/plain.js new file mode 100644 index 000000000..60e6732ab --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/plain.js @@ -0,0 +1,5 @@ +console.log("plain: evaluated"); +exports.greet = function (who) { + return "hello " + who; +}; +exports.count = 3; diff --git a/tests/corpus/6152-cjs-tsc-interop/stamped.js b/tests/corpus/6152-cjs-tsc-interop/stamped.js new file mode 100644 index 000000000..9e844af05 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/stamped.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helper = exports.default = void 0; +console.log("stamped: evaluated"); +function greet(who) { + return "stamped " + who; +} +exports.default = greet; +function helper() { + return "helper"; +} +exports.helper = helper; diff --git a/tests/corpus/6153-cjs-exports-object/lib.js b/tests/corpus/6153-cjs-exports-object/lib.js new file mode 100644 index 000000000..4f69bfca2 --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/lib.js @@ -0,0 +1,13 @@ +exports.lookup = function (ext) { + return "application/" + ext; +}; +exports.contentType = function (ext) { + return exports.lookup(ext) + "; charset=utf-8"; +}; +exports.describe = function () { + return module.exports.lookup("xml"); +}; +// A later reassignment is what member calls see from then on. +exports.lookup = function (ext) { + return "text/" + ext; +}; diff --git a/tests/corpus/6153-cjs-exports-object/main.js b/tests/corpus/6153-cjs-exports-object/main.js new file mode 100644 index 000000000..39cf61c6c --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/main.js @@ -0,0 +1,19 @@ +// A module's own exports object, used inside the module: identity +// comparisons (the UMD self-test), typeof and truthiness, and member calls. +const lib = require("./lib.js"); +(function () { + var root = this; + console.log("wrapper", root === module.exports, root === exports, typeof root); +}).call(this); +function check() { + return [exports === module.exports, typeof exports, typeof module.exports, !exports].join(" "); +} +console.log(check(), module.exports ? "truthy" : "falsy"); +console.log(lib.contentType("json"), lib.describe()); +exports.answer = function () { + return 42; +}; +function ask() { + return module.exports.answer() + exports.answer(); +} +console.log(exports.answer(), ask()); diff --git a/tests/corpus/6153-cjs-exports-object/package.json b/tests/corpus/6153-cjs-exports-object/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6154-cjs-require-nested-destructure/constants.js b/tests/corpus/6154-cjs-require-nested-destructure/constants.js new file mode 100644 index 000000000..6a53c5d81 --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/constants.js @@ -0,0 +1,8 @@ +exports.MAX_LENGTH = 1024; +exports.SEP = "/"; +exports.helper = function (x) { + return x * 3; +}; +exports.bump = function () { + exports.MAX_LENGTH = 2048; +}; diff --git a/tests/corpus/6154-cjs-require-nested-destructure/errors.js b/tests/corpus/6154-cjs-require-nested-destructure/errors.js new file mode 100644 index 000000000..bdddfec3f --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/errors.js @@ -0,0 +1,12 @@ +console.log("errors: evaluated"); +const codes = { + ERR_ONE: { code: "ERR_ONE" }, + ERR_TWO: { code: "ERR_TWO" }, +}; +class AbortError extends Error { + constructor() { + super("aborted"); + this.name = "AbortError"; + } +} +module.exports = { codes, AbortError, version: "4.0" }; diff --git a/tests/corpus/6154-cjs-require-nested-destructure/main.js b/tests/corpus/6154-cjs-require-nested-destructure/main.js new file mode 100644 index 000000000..d928d5a27 --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/main.js @@ -0,0 +1,14 @@ +// Nested patterns under a named export of a program module destructure +// that export's value at the require (a snapshot), while plain keys stay +// live bindings to the export. +console.log("main: start"); +const { codes: { ERR_ONE, ERR_TWO }, AbortError, version } = require("./errors.js"); +console.log(ERR_ONE.code, ERR_TWO.code, new AbortError().name, version); +const errors = require("./errors.js"); +errors.codes.ERR_ONE = { code: "replaced" }; +console.log(ERR_ONE.code, errors.codes.ERR_ONE.code); +// Destructuring a module BINDING snapshots its exports the same way. +const constants = require("./constants.js"); +const { MAX_LENGTH, SEP: sep, helper } = constants; +constants.bump(); +console.log(MAX_LENGTH, constants.MAX_LENGTH, sep, helper(2)); diff --git a/tests/corpus/6154-cjs-require-nested-destructure/package.json b/tests/corpus/6154-cjs-require-nested-destructure/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6155-cjs-export-table-methods/lib.js b/tests/corpus/6155-cjs-export-table-methods/lib.js new file mode 100644 index 000000000..d8aa56495 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/lib.js @@ -0,0 +1,20 @@ +console.log("lib: start"); +const WIN = { SEP: "\\" }; +const POSIX = { SEP: "/" }; +function describe() { + return "table"; +} +module.exports = { + scale(x) { + return x * 10; + }, + chars(win32) { + return win32 === true ? WIN.SEP : POSIX.SEP; + }, + get label() { + return describe(); + }, + ...require("./more.js"), + k: (console.log("lib: k entry"), 1), +}; +console.log("lib: end"); diff --git a/tests/corpus/6155-cjs-export-table-methods/main.js b/tests/corpus/6155-cjs-export-table-methods/main.js new file mode 100644 index 000000000..b2ce150d0 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/main.js @@ -0,0 +1,5 @@ +// Export tables with method entries, accessors and spreads of required +// modules: the spread evaluates its module at its place in the table. +console.log("main: start"); +const m = require("./lib.js"); +console.log(m.scale(2), m.chars(true), m.chars(false), m.label, m.extra(), m.k); diff --git a/tests/corpus/6155-cjs-export-table-methods/more.js b/tests/corpus/6155-cjs-export-table-methods/more.js new file mode 100644 index 000000000..4051ce311 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/more.js @@ -0,0 +1,4 @@ +console.log("more: evaluated"); +exports.extra = function () { + return "more"; +}; diff --git a/tests/corpus/6155-cjs-export-table-methods/package.json b/tests/corpus/6155-cjs-export-table-methods/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs b/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs new file mode 100644 index 000000000..12299e189 --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs @@ -0,0 +1,4 @@ +const status = require("./status.cjs"); +exports.describe = function (code) { + return code + " " + status.message[code]; +}; diff --git a/tests/corpus/6156-cjs-function-members-from-ts/main.ts b/tests/corpus/6156-cjs-function-members-from-ts/main.ts new file mode 100644 index 000000000..070d576ff --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/main.ts @@ -0,0 +1,9 @@ +// A CommonJS module exporting a function that carries its own members +// (statuses' `status.message = codes; module.exports = status`), read from +// TypeScript through the default import: calls, member reads, member calls. +import status from "./status.cjs"; +import { describe } from "./consumer.cjs"; + +console.log(status(404), status.message[200], status.codes.length, status.isOk(200)); +console.log(status.redirect[301] === true, status.redirect[302]); +console.log(describe(500)); diff --git a/tests/corpus/6156-cjs-function-members-from-ts/package.json b/tests/corpus/6156-cjs-function-members-from-ts/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6156-cjs-function-members-from-ts/status.cjs b/tests/corpus/6156-cjs-function-members-from-ts/status.cjs new file mode 100644 index 000000000..7a06d9ab1 --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/status.cjs @@ -0,0 +1,10 @@ +function status(code) { + return code + 1; +} +status.message = { 200: "OK", 404: "Not Found", 500: "Internal Server Error" }; +status.codes = [200, 404, 500]; +status.redirect = { 301: true, 302: true }; +status.isOk = function (code) { + return code === 200; +}; +module.exports = status; diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index acaa95479..b9fecac04 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2214,6 +2214,13 @@ "6143-cjs-require-json/main.js": 3245, "6144-cjs-require-builtin-nested/main.js": 2181, "6145-cjs-require-cycle/main.js": 3507, + "6150-cjs-inline-require/main.js": 4200, + "6151-cjs-export-chains/main.js": 3000, + "6152-cjs-tsc-interop/main.js": 2600, + "6153-cjs-exports-object/main.js": 2400, + "6154-cjs-require-nested-destructure/main.js": 2800, + "6155-cjs-export-table-methods/main.js": 2600, + "6156-cjs-function-members-from-ts/main.ts": 3000, "700-classes-basic.ts": 2530, "701-classes-composition.ts": 2357, "702-classes-this-capture.ts": 2140, @@ -5244,6 +5251,13 @@ "6143-cjs-require-json/main.js": 3050, "6144-cjs-require-builtin-nested/main.js": 3329, "6145-cjs-require-cycle/main.js": 4761, + "6150-cjs-inline-require/main.js": 9600, + "6151-cjs-export-chains/main.js": 7000, + "6152-cjs-tsc-interop/main.js": 6000, + "6153-cjs-exports-object/main.js": 5600, + "6154-cjs-require-nested-destructure/main.js": 6400, + "6155-cjs-export-table-methods/main.js": 6000, + "6156-cjs-function-members-from-ts/main.ts": 7000, "700-classes-basic.ts": 2674, "701-classes-composition.ts": 2606, "702-classes-this-capture.ts": 2403, diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 51f04d310..564d53bc3 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -972,7 +972,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `(function () { var root = this; console.log("before"); - console.log(root === module.exports); + console.log(root === eval("this")); console.log("after"); }).call(this); `, @@ -983,7 +983,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () "speculative-apply", `(function () { console.log("before"); - console.log(typeof module.exports); + console.log(typeof eval("1")); }).apply(null, []); `, "before\n", @@ -994,7 +994,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `class Base {} class Derived extends (function () { console.log("before"); - console.log(typeof module.exports); + console.log(typeof eval("1")); return Base; })() {} console.log(typeof Derived); @@ -1008,8 +1008,88 @@ console.log(typeof Derived); expect(r.stdout).toBe(stdout); expect(r.stderr).toMatch( new RegExp( - `^Uncaught Error: 'module\\.exports' is part of the standard library types but has no scriptc lowering yet \\[SC2020 at .*${name}\\.cjs:${line}\\]\\n$`, + `^Uncaught Error: values of type '\\(x: string\\) => any' cannot be compiled: .* \\[SC2009 at .*${name}\\.cjs:${line}\\]\\n$`, ), ); }); }); + +// CommonJS forms around the module's exports that keep a refusal name the +// construct itself (they used to report an unlowerable `require` value, a +// "binding form with no lowering", or the ES-module `module` message). +describe("CommonJS export-object and inline require refusals", () => { + test.each([ + [ + "exports-as-argument", + `exports.x = 1; +function keys(o) { + return Object.keys(o); +} +console.log("before"); +console.log(keys(exports)); +`, + {}, + "before\n", + 6, + "'exports' as a value other than a member access, comparison, typeof or truthiness test \\(the compiled exports are a static table, not an object: read exports\\.\\)", + ], + [ + "exports-rebinding", + `console.log("before"); +exports = module.exports = function hash(x) { + return x; +}; +exports.extra = 1; +`, + {}, + "before\n", + 2, + "rebinding 'exports' \\(exports = module\\.exports = value\\)", + ], + [ + "require-computed", + `const name = "./lib.cjs"; +console.log("before"); +console.log(require(name).x); +`, + { "lib.cjs": "exports.x = 1;\n" }, + "before\n", + 3, + "require\\(\\) with a computed specifier", + ], + [ + "exports-call-reads-this", + `exports.timeouts = function (n) { + return this.scale * n; +}; +exports.scale = 2; +console.log("before"); +console.log(exports.timeouts(3)); +`, + {}, + "before\n", + 6, + "calling 'exports\\.timeouts' through the module's exports object when the function may read 'this'", + ], + [ + "require-table-as-value", + `function count(o) { + return Object.keys(o).length; +} +console.log("before"); +console.log(count(require("./lib.cjs"))); +`, + { "lib.cjs": "exports.x = 1;\n" }, + "before\n", + 5, + "the exports object of '\\./lib\\.cjs' as a value", + ], + ] as const)("%s", async (name, source, extra, stdout, line, message) => { + const r = await compileAndRun(name, source, "cjs", extra); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(stdout); + expect(r.stderr).toMatch( + new RegExp(`^Uncaught Error: ${message}.*\\[SC1090 at .*${name}\\.cjs:${line}\\]\\n$`), + ); + }); +}); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index c4e70294e..a025d0274 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -43,15 +43,15 @@ const KNOWN: Record = { "cjs-lexer-invisible": { build: "SC2013" }, "commander-calc": { run: "SC1090", argvs: [["--help"]] }, "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, - "create-require": { run: "SC2009" }, - "dotfolder": { run: "SC2009" }, + "create-require": { run: "SC1090" }, + "dotfolder": { run: "SC1090" }, "dual-package-kinds": { build: "SC2013" }, "dynamic-import": { build: "SC2012" }, "dynamic-import-local": { build: "SC2012" }, "esbuild-require": { build: "SC2013" }, "esm-missing-default": { build: "SC1090" }, "esm-missing-export": { build: "SC1090" }, - "fetch-destructure": { build: "SC1090" }, + "fetch-destructure": { run: "SC1031" }, "fs-shims": { run: "SC2020" }, "hoisted-workspace": { build: "SC2013" }, "import-refusal": { build: "SC2020" }, @@ -65,7 +65,7 @@ const KNOWN: Record = { "misc-shims": { build: "SC2013" }, "namespace": { build: "SC1013" }, "node24-async-events": { run: "SC2020" }, - "node24-commonjs-metadata": { run: "SC1090" }, + "node24-commonjs-metadata": { run: "SC2020" }, "node24-crypto-kdf": { run: "SC2020" }, "node24-deep-equality": { run: "SC2020" }, "node24-diagnostics-async": { run: "SC1090" }, From 78100a4124328b7cd11cebda153ff1bb95d18396 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:29:28 -0700 Subject: [PATCH 030/120] Record ts7 order baselines for the new CommonJS corpus programs A full SCRIPTC_UPDATE_BASELINES=1 re-record of order-parity.test.ts changed no existing baseline; these are the seven new fixtures (6150-6156). 6150's order places late.js after main.js, the lazy inline-require edge. Co-Authored-By: Claude Opus 5.5 --- .../6150-cjs-inline-require/main.js.json | 20 +++++++++++++++++++ .../6151-cjs-export-chains/main.js.json | 10 ++++++++++ .../corpus/6152-cjs-tsc-interop/main.js.json | 9 +++++++++ .../6153-cjs-exports-object/main.js.json | 7 +++++++ .../main.js.json | 8 ++++++++ .../main.js.json | 8 ++++++++ .../main.ts.json | 8 ++++++++ 7 files changed, 70 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json new file mode 100644 index 000000000..5f1e322f7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json @@ -0,0 +1,20 @@ +{ + "order": [ + "/tests/corpus/6150-cjs-inline-require/lib.js", + "/tests/corpus/6150-cjs-inline-require/inner.js", + "/tests/corpus/6150-cjs-inline-require/reexport-inline.js", + "/tests/corpus/6150-cjs-inline-require/reexport-binding.js", + "/tests/corpus/6150-cjs-inline-require/type-error.js", + "/tests/corpus/6150-cjs-inline-require/pick.js", + "/tests/corpus/6150-cjs-inline-require/registry.js", + "/tests/corpus/6150-cjs-inline-require/make-logger.js", + "/tests/corpus/6150-cjs-inline-require/point.js", + "/tests/corpus/6150-cjs-inline-require/shapes.js", + "/tests/corpus/6150-cjs-inline-require/table.js", + "/tests/corpus/6150-cjs-inline-require/verify.js", + "/tests/corpus/6150-cjs-inline-require/fallback.js", + "/tests/corpus/6150-cjs-inline-require/main.js", + "/tests/corpus/6150-cjs-inline-require/late.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json new file mode 100644 index 000000000..883aa872a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/6151-cjs-export-chains/chain.js", + "/tests/corpus/6151-cjs-export-chains/constants.js", + "/tests/corpus/6151-cjs-export-chains/re.js", + "/tests/corpus/6151-cjs-export-chains/sem.js", + "/tests/corpus/6151-cjs-export-chains/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json new file mode 100644 index 000000000..19a1b20de --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6152-cjs-tsc-interop/plain.js", + "/tests/corpus/6152-cjs-tsc-interop/stamped.js", + "/tests/corpus/6152-cjs-tsc-interop/fn.js", + "/tests/corpus/6152-cjs-tsc-interop/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json new file mode 100644 index 000000000..070b131e3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6153-cjs-exports-object/lib.js", + "/tests/corpus/6153-cjs-exports-object/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json new file mode 100644 index 000000000..ee9f71d22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6154-cjs-require-nested-destructure/errors.js", + "/tests/corpus/6154-cjs-require-nested-destructure/constants.js", + "/tests/corpus/6154-cjs-require-nested-destructure/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json new file mode 100644 index 000000000..9ac5f0e41 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6155-cjs-export-table-methods/more.js", + "/tests/corpus/6155-cjs-export-table-methods/lib.js", + "/tests/corpus/6155-cjs-export-table-methods/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json new file mode 100644 index 000000000..e73e603e3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6156-cjs-function-members-from-ts/status.cjs", + "/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs", + "/tests/corpus/6156-cjs-function-members-from-ts/main.ts" + ], + "diags": [] +} From 87b85d5b28011d4a6c95efe003e9dbd24425cf0a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:34:43 -0700 Subject: [PATCH 031/120] Name the weak collection constructors whole in the dispose gate The runtime ABI test reads every @scr_* spelling in the backend sources as a symbol the backend can emit; the gate's regex spelled a truncated @scr_weak_ prefix. Test the two declarations by their full names. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/backend/llvm/emitter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index a68f008f3..84614b6cb 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -2002,7 +2002,9 @@ export class LlEmitter { * builders, JSON schemas and weak collection constructors are requested * lazily. */ private disposeObserverGates(): string[] { - const weak = [...this.decls].some((d) => /@scr_weak_(?:map|set)_new\(/.test(d)); + const weak = [...this.decls].some( + (d) => d.includes("@scr_weak_map_new(") || d.includes("@scr_weak_set_new("), + ); return [ `${WEAK_KEYS_GATE} = internal constant i1 ${weak}`, `${RECORD_SOURCES_GATE} = internal constant i1 ${this.dyn.keepsRecordSources}`, From e90c77d36a07279063bc9f7bd21e80824d221a4e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:52:04 -0700 Subject: [PATCH 032/120] Make selected npm packages join the type-checked program scriptc's resolver and tsgo must answer one runtime file for every opted-in npm edge; four disagreements left the file outside tsgo's program (SC2013 "did not join the type-checked program") for express, axios, body-parser, execa, fast-glob, globby, knex, uuid and tldts: - uuid: the node-condition hoist re-added the inner "types" key after the claims were deleted; tsgo then took the browser build. Delete the claims after the hoist. - tldts: tsgo's TypeScript-first lookup took the shipped root index.ts over "main". The fs shadow hides installed TypeScript sources of opted-in packages that have no same-stem runtime JavaScript. - express: tsgo redirected same-version duplicate installs to one copy, either dropping a copy or merging two Node module instances into one. The shadow serves opted-in package.json files without "version". - deep chains (get-intrinsic, dunder-proto, run-parallel, reusify, supports-color): maxNodeModuleJsDepth charges every node_modules hop. The program-root fixpoint also roots opted-in bare edges' runtime files that the program is missing. All changes are gated on --npm-static being active. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 + .../compiler/src/frontend/npm-static.test.ts | 24 ++++- packages/compiler/src/frontend/npm-static.ts | 33 ++++++- packages/compiler/src/frontend/program.ts | 94 +++++++++++++++---- .../fixtures/npm-static/deep-packages-cli.ts | 3 + tests/fixtures/npm-static/dup-install-cli.ts | 4 + .../node_modules/dup-left/index.d.ts | 1 + .../npm-static/node_modules/dup-left/index.js | 5 + .../dup-left/node_modules/dup-leaf/index.js | 6 ++ .../node_modules/dup-leaf/package.json | 5 + .../node_modules/dup-left/package.json | 5 + .../node_modules/dup-right/index.d.ts | 1 + .../node_modules/dup-right/index.js | 5 + .../dup-right/node_modules/dup-leaf/index.js | 6 ++ .../node_modules/dup-leaf/package.json | 5 + .../node_modules/dup-right/package.json | 5 + .../npm-static/node_modules/hop-1/index.d.ts | 1 + .../npm-static/node_modules/hop-1/index.js | 5 + .../node_modules/hop-1/package.json | 5 + .../npm-static/node_modules/hop-10/index.js | 5 + .../node_modules/hop-10/package.json | 5 + .../npm-static/node_modules/hop-11/index.js | 4 + .../node_modules/hop-11/package.json | 5 + .../npm-static/node_modules/hop-2/index.js | 5 + .../node_modules/hop-2/package.json | 5 + .../npm-static/node_modules/hop-3/index.js | 5 + .../node_modules/hop-3/package.json | 5 + .../npm-static/node_modules/hop-4/index.js | 5 + .../node_modules/hop-4/package.json | 5 + .../npm-static/node_modules/hop-5/index.js | 5 + .../node_modules/hop-5/package.json | 5 + .../npm-static/node_modules/hop-6/index.js | 5 + .../node_modules/hop-6/package.json | 5 + .../npm-static/node_modules/hop-7/index.js | 5 + .../node_modules/hop-7/package.json | 5 + .../npm-static/node_modules/hop-8/index.js | 5 + .../node_modules/hop-8/package.json | 5 + .../npm-static/node_modules/hop-9/index.js | 5 + .../node_modules/hop-9/package.json | 5 + .../split-node-types/dist-node/build.js | 3 + .../split-node-types/dist-node/index.js | 2 + .../split-node-types/dist/index.d.ts | 2 + .../split-node-types/dist/index.js | 4 + .../split-node-types/package.json | 16 ++++ .../ts-root-source/dist/cjs/index.js | 6 ++ .../ts-root-source/dist/cjs/src/domain.js | 5 + .../ts-root-source/dist/types/index.d.ts | 1 + .../node_modules/ts-root-source/index.ts | 1 + .../node_modules/ts-root-source/package.json | 7 ++ .../node_modules/ts-root-source/src/domain.ts | 3 + .../npm-static/split-node-types-cli.ts | 3 + .../fixtures/npm-static/ts-root-source-cli.ts | 3 + tests/harness/npm-static.test.ts | 36 ++++++- 53 files changed, 384 insertions(+), 22 deletions(-) create mode 100644 tests/fixtures/npm-static/deep-packages-cli.ts create mode 100644 tests/fixtures/npm-static/dup-install-cli.ts create mode 100644 tests/fixtures/npm-static/node_modules/dup-left/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/dup-left/index.js create mode 100644 tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/index.js create mode 100644 tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/package.json create mode 100644 tests/fixtures/npm-static/node_modules/dup-left/package.json create mode 100644 tests/fixtures/npm-static/node_modules/dup-right/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/dup-right/index.js create mode 100644 tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/index.js create mode 100644 tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/package.json create mode 100644 tests/fixtures/npm-static/node_modules/dup-right/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-1/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/hop-1/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-1/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-10/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-10/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-11/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-11/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-2/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-2/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-3/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-3/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-4/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-4/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-5/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-5/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-6/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-6/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-7/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-7/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-8/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-8/package.json create mode 100644 tests/fixtures/npm-static/node_modules/hop-9/index.js create mode 100644 tests/fixtures/npm-static/node_modules/hop-9/package.json create mode 100644 tests/fixtures/npm-static/node_modules/split-node-types/dist-node/build.js create mode 100644 tests/fixtures/npm-static/node_modules/split-node-types/dist-node/index.js create mode 100644 tests/fixtures/npm-static/node_modules/split-node-types/dist/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/split-node-types/dist/index.js create mode 100644 tests/fixtures/npm-static/node_modules/split-node-types/package.json create mode 100644 tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/index.js create mode 100644 tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/src/domain.js create mode 100644 tests/fixtures/npm-static/node_modules/ts-root-source/dist/types/index.d.ts create mode 100644 tests/fixtures/npm-static/node_modules/ts-root-source/index.ts create mode 100644 tests/fixtures/npm-static/node_modules/ts-root-source/package.json create mode 100644 tests/fixtures/npm-static/node_modules/ts-root-source/src/domain.ts create mode 100644 tests/fixtures/npm-static/split-node-types-cli.ts create mode 100644 tests/fixtures/npm-static/ts-root-source-cli.ts diff --git a/.gitignore b/.gitignore index e5b54a5fa..2ced4f3c3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ node_modules/ !tests/corpus/*/node_modules/ !tests/fixtures/npm-static/node_modules/ !tests/fixtures/npm-static/nested/inner/node_modules/ +!tests/fixtures/npm-static/node_modules/*/node_modules/ !tests/fixtures/npm/store/*/node_modules/ !tests/fixtures/npm/cases/*/node_modules/ !tests/library-mode/*/node_modules/ @@ -32,6 +33,7 @@ output/ !tests/fixtures/npm/cases/workspace-copied/members/*/dist/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ !tests/fixtures/provenance/node_modules/*/dist/ +!tests/fixtures/npm-static/node_modules/*/dist/ .scriptc/ *.log .DS_Store diff --git a/packages/compiler/src/frontend/npm-static.test.ts b/packages/compiler/src/frontend/npm-static.test.ts index a97b2f41c..0b4614e34 100644 --- a/packages/compiler/src/frontend/npm-static.test.ts +++ b/packages/compiler/src/frontend/npm-static.test.ts @@ -1,7 +1,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { npmStaticIneligibleReason } from "./npm-static.js"; +import { npmStaticIneligibleReason, npmStaticTransformPkgJson } from "./npm-static.js"; test("an unreadable runtime entry is an eligibility refusal", async () => { const dir = await mkdtemp("/tmp/scriptc-npm-static-unreadable-"); @@ -14,3 +14,25 @@ test("an unreadable runtime entry is an eligibility refusal", async () => { await rm(dir, { recursive: true, force: true }); } }); + +test("a hoisted node condition leaves no types claim behind", () => { + // uuid 14: the node object carries its own "types" key. Left in place, + // tsgo maps the hidden declaration to its same-stem browser build. + const pkg: Record = { + types: "./dist/index.d.ts", + exports: { + ".": { + node: { types: "./dist/index.d.ts", default: "./dist-node/index.js" }, + default: "./dist/index.js", + }, + "./package.json": "./package.json", + }, + }; + npmStaticTransformPkgJson(pkg); + expect(pkg).toEqual({ + exports: { + ".": { default: "./dist-node/index.js" }, + "./package.json": "./package.json", + }, + }); +}); diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index d2f4ea3e8..3c8fa22e9 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -157,8 +157,6 @@ export function npmStaticTransformPkgJson(pkg: Record): void { return; } const obj = v as Record; - delete obj["types"]; - delete obj["typings"]; // 0. The "node" condition HOISTS: Node's runtime resolution matches it // (yaml's `{ types, node: "./dist/index.js", default: "./browser/ // index.js" }` runs the node dist; the browser dist is a DIFFERENT @@ -187,6 +185,14 @@ export function npmStaticTransformPkgJson(pkg: Record): void { } } } + // 1. The types claims go AFTER the hoist: a node object's own + // "types" key (uuid's `node: { types: "./dist/index.d.ts", default: + // "./dist-node/index.js" }`) splices in with its siblings, and left + // in place tsgo follows it, maps the hidden declaration to its + // same-stem JS (dist/index.js, the browser build) and disagrees with + // this resolver's runtime answer. + delete obj["types"]; + delete obj["typings"]; for (const value of Object.values(obj)) transform(value); }; if (pkg["exports"] !== undefined) transform(pkg["exports"]); @@ -208,6 +214,13 @@ function npmStaticTransformPkgJsonText(text: string): string { } if (doc === null || typeof doc !== "object" || Array.isArray(doc)) return text; npmStaticTransformPkgJson(doc as Record); + // tsgo deduplicates installs by package id (name + version): a second + // copy of the same name@version (express/node_modules/fresh beside + // send/node_modules/fresh) becomes a redirect to the first copy loaded, + // so its own path never joins the program. Node loads every installed + // copy as its own module, and resolve.ts answers each importer's own + // copy; without a version the checker keeps the copies apart too. + delete (doc as Record)["version"]; return JSON.stringify(doc); } @@ -275,6 +288,20 @@ function packageIsUntyped(path: string): boolean { return untyped; } +/** An installed TypeScript source with no runtime JavaScript at its stem + * (tldts ships `index.ts` beside `main: "dist/cjs/index.js"`). Node never + * executes TypeScript under node_modules and resolve.ts answers only + * runtime JS for opted-in packages, yet tsgo's TypeScript-first lookup + * would take the package-root `index.ts` over `main`. A same-stem JS twin + * keeps the source visible: that pair is the shape npmStaticProgramDep + * accepts in the JavaScript's place. */ +function isUnloadableInstalledTsSource(path: string): boolean { + if (!/\.(ts|tsx|mts|cts)$/.test(path) || isDeclarationFileName(path)) return false; + if (!isNodeModulesPathNorm(path)) return false; + const stem = path.replace(/\.(ts|tsx|mts|cts)$/, ""); + return ![".js", ".jsx", ".mjs", ".cjs"].some((ext) => trackedExists(stem + ext)); +} + export interface NpmStaticFsShadow { /** Shadowed CONTENT for a real path (the types-stripped package.json), * or undefined (no shadow — fall through). */ @@ -383,7 +410,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow const target = shadowTargetOf(path); if (!target) return false; if (target.viaTypes) return true; // the @types twin hides whole - return isDeclarationFileName(path); + return isDeclarationFileName(path) || isUnloadableInstalledTsSource(path); }, }; } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index df0cb673e..d5a0bbd51 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -635,9 +635,14 @@ export function entryPackageFilePredicate(entryPath: string): (file: string) => } /** maxNodeModuleJsDepth can omit relative imports within an admitted - * package. Discover its runtime sources to a fixpoint so the checker and - * module resolver see the same graph regardless of import traversal order. */ -function entryPackageProgramRoots7(program: ts.Program, entryPath: string): string[] { + * package, and opted-in packages deep in the dependency graph. Discover + * their runtime sources to a fixpoint so the checker and module resolver + * see the same graph regardless of import traversal order or depth. */ +function entryPackageProgramRoots7( + program: ts.Program, + entryPath: string, + rootNpmStaticEdges: boolean, +): string[] { const belongsToEntry = entryPackageFilePredicate(entryPath); const roots = new Set(); for (const sf of program.getImplementationSourceFiles()) { @@ -656,15 +661,63 @@ function entryPackageProgramRoots7(program: ts.Program, entryPath: string): stri program.getSourceFile(target) === undefined ) roots.add(target); + // An opted-in package's bare edge counts against the same depth + // budget: tsgo charges every node_modules JS hop, so express's + // body-parser → qs → side-channel → get-intrinsic chain runs out of + // depth before its leaves load. Root the runtime file this edge + // lowers to (preflight's own resolution and miss rule) so the leaf + // joins like a shallow one; complete programs add nothing. Only + // the shadowed lowering world does this (the authored world checks + // user code against declarations), and only from package files: a + // program file's own edges are never deeper than one hop. + // (resolveBareModule directly: resolveNpmImport7's workspace + // registration stays with preflight's own walk.) + if ( + target === null && + rootNpmStaticEdges && + npmStaticPackageOfPath(sf.fileName) !== null && + !isRelativeSpecifier(specifier.text) + ) { + const spec = specifier.text; + if (spec.startsWith("#") || canonicalBuiltinModule(spec) !== null) continue; + const npm = resolveBareModule( + sf.fileName, + spec, + undefined, + moduleSpecifierKind7(specifier), + ); + if ( + npm !== null && + isNpmStaticPackage(npm.packageName) && + (isNodeModulesPath(npm.typesFile) || npm.workspaceDir !== undefined) && + isJsSourceFileName(npm.typesFile) && + npmStaticProgramFile(program, npm.typesFile) === null + ) + roots.add(tsgoPath(resolve(npm.typesFile))); + } } } return [...roots].sort(); } +/** "require" for a CommonJS edge (`require("x")`, `import x = require("x")`), + * "import" for every ESM form — the resolution kind preflight uses. */ +function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "require" { + const parent = specifier.parent; + if (ts.isExternalModuleReference(parent)) return "require"; + return ts.isCallExpression(parent) && + ts.isIdentifier(parent.expression) && + parent.expression.text === "require" + ? "require" + : "import"; +} + function loadProgram7( host: ts.Ts7Host, entryPath: string, externalTypes: ReadonlyMap = new Map(), + /** The --npm-static lowering world: root opted-in packages' deep edges. */ + rootNpmStaticEdges = false, ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); const configuredPaths = config.options["paths"]; @@ -712,7 +765,7 @@ function loadProgram7( try { for (let pass = 0; pass < 32; pass++) { const candidates = [ - ...entryPackageProgramRoots7(program, entryPath), + ...entryPackageProgramRoots7(program, entryPath, rootNpmStaticEdges), ...createRequireProgramRoots7(program), ...forkTargetPaths(program, program.getImplementationSourceFiles()), ...workerTargetPaths(program, program.getImplementationSourceFiles()), @@ -926,7 +979,7 @@ export function loadProgram( }; const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow }); try { - const load = loadProgram7(host, entryPath, externalTypes); + const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0); let authoredHost: ts.Ts7Host | null = null; let authoredLoad: ReturnType | null = null; return { @@ -4000,6 +4053,24 @@ function locOf7(node: ts.Node): { file: string; start: number; end: number } { return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() }; } +/** The program file holding an opted-in package's resolved runtime JS: + * the file itself, or its same-stem TypeScript twin. */ +function npmStaticProgramFile(program: ts.Program, resolvedFile: string): ts.SourceFile | null { + let dep = program.getSourceFile(resolvedFile) ?? null; + if (dep === null) { + // A workspace-linked package shipping its .ts SOURCE beside the + // compiled .js: the checker's resolution preferred the source (it + // outranks the sibling .js under bundler resolution), so the program + // holds the .ts twin at the same stem — the same module, in its + // richer spelling. + for (const ext of [".ts", ".tsx", ".mts", ".cts"]) { + dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null; + if (dep !== null) break; + } + } + return dep; +} + /** The PROGRAM source file an opted-in --npm-static package's resolved * entry maps to, or null with the package marked an offender (resolution * still answering declarations, or the file missing from the type-checked @@ -4016,18 +4087,7 @@ function npmStaticProgramDep( ); return null; } - let dep = program.getSourceFile(resolvedFile) ?? null; - if (dep === null) { - // A workspace-linked package shipping its .ts SOURCE beside the - // compiled .js: the checker's resolution preferred the source (it - // outranks the sibling .js under bundler resolution), so the program - // holds the .ts twin at the same stem — the same module, in its - // richer spelling. - for (const ext of [".ts", ".tsx", ".mts", ".cts"]) { - dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null; - if (dep !== null) break; - } - } + const dep = npmStaticProgramFile(program, resolvedFile); if (dep === null) { reportNpmStaticOffender( packageName, diff --git a/tests/fixtures/npm-static/deep-packages-cli.ts b/tests/fixtures/npm-static/deep-packages-cli.ts new file mode 100644 index 000000000..9b10fdf46 --- /dev/null +++ b/tests/fixtures/npm-static/deep-packages-cli.ts @@ -0,0 +1,3 @@ +import { depth } from "hop-1"; + +console.log(depth()); diff --git a/tests/fixtures/npm-static/dup-install-cli.ts b/tests/fixtures/npm-static/dup-install-cli.ts new file mode 100644 index 000000000..346be854d --- /dev/null +++ b/tests/fixtures/npm-static/dup-install-cli.ts @@ -0,0 +1,4 @@ +import { left } from "dup-left"; +import { right } from "dup-right"; + +console.log(left(), left(), right(), left()); diff --git a/tests/fixtures/npm-static/node_modules/dup-left/index.d.ts b/tests/fixtures/npm-static/node_modules/dup-left/index.d.ts new file mode 100644 index 000000000..f3363e894 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-left/index.d.ts @@ -0,0 +1 @@ +export declare function left(): number; diff --git a/tests/fixtures/npm-static/node_modules/dup-left/index.js b/tests/fixtures/npm-static/node_modules/dup-left/index.js new file mode 100644 index 000000000..ef32a8a0a --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-left/index.js @@ -0,0 +1,5 @@ +const leaf = require("dup-leaf"); +function left() { + return leaf.tick(); +} +exports.left = left; diff --git a/tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/index.js b/tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/index.js new file mode 100644 index 000000000..9246c7839 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/index.js @@ -0,0 +1,6 @@ +let calls = 0; +function tick() { + calls += 1; + return calls; +} +exports.tick = tick; diff --git a/tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/package.json b/tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/package.json new file mode 100644 index 000000000..f01a0c08f --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-left/node_modules/dup-leaf/package.json @@ -0,0 +1,5 @@ +{ + "name": "dup-leaf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/dup-left/package.json b/tests/fixtures/npm-static/node_modules/dup-left/package.json new file mode 100644 index 000000000..feab6391f --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-left/package.json @@ -0,0 +1,5 @@ +{ + "name": "dup-left", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/dup-right/index.d.ts b/tests/fixtures/npm-static/node_modules/dup-right/index.d.ts new file mode 100644 index 000000000..51ed905a7 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-right/index.d.ts @@ -0,0 +1 @@ +export declare function right(): number; diff --git a/tests/fixtures/npm-static/node_modules/dup-right/index.js b/tests/fixtures/npm-static/node_modules/dup-right/index.js new file mode 100644 index 000000000..d0bba85ad --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-right/index.js @@ -0,0 +1,5 @@ +const leaf = require("dup-leaf"); +function right() { + return leaf.tick(); +} +exports.right = right; diff --git a/tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/index.js b/tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/index.js new file mode 100644 index 000000000..9246c7839 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/index.js @@ -0,0 +1,6 @@ +let calls = 0; +function tick() { + calls += 1; + return calls; +} +exports.tick = tick; diff --git a/tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/package.json b/tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/package.json new file mode 100644 index 000000000..f01a0c08f --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-right/node_modules/dup-leaf/package.json @@ -0,0 +1,5 @@ +{ + "name": "dup-leaf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/dup-right/package.json b/tests/fixtures/npm-static/node_modules/dup-right/package.json new file mode 100644 index 000000000..9889f0261 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/dup-right/package.json @@ -0,0 +1,5 @@ +{ + "name": "dup-right", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-1/index.d.ts b/tests/fixtures/npm-static/node_modules/hop-1/index.d.ts new file mode 100644 index 000000000..4d0af9c97 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-1/index.d.ts @@ -0,0 +1 @@ +export declare function depth(): number; diff --git a/tests/fixtures/npm-static/node_modules/hop-1/index.js b/tests/fixtures/npm-static/node_modules/hop-1/index.js new file mode 100644 index 000000000..57fdb622e --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-1/index.js @@ -0,0 +1,5 @@ +const next = require("hop-2"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-1/package.json b/tests/fixtures/npm-static/node_modules/hop-1/package.json new file mode 100644 index 000000000..9dda7b293 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-1/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-1", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-10/index.js b/tests/fixtures/npm-static/node_modules/hop-10/index.js new file mode 100644 index 000000000..2fa002968 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-10/index.js @@ -0,0 +1,5 @@ +const next = require("hop-11"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-10/package.json b/tests/fixtures/npm-static/node_modules/hop-10/package.json new file mode 100644 index 000000000..624842847 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-10/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-10", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-11/index.js b/tests/fixtures/npm-static/node_modules/hop-11/index.js new file mode 100644 index 000000000..3af42d043 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-11/index.js @@ -0,0 +1,4 @@ +function depth() { + return 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-11/package.json b/tests/fixtures/npm-static/node_modules/hop-11/package.json new file mode 100644 index 000000000..258cf14d9 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-11/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-11", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-2/index.js b/tests/fixtures/npm-static/node_modules/hop-2/index.js new file mode 100644 index 000000000..d21c4aa70 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-2/index.js @@ -0,0 +1,5 @@ +const next = require("hop-3"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-2/package.json b/tests/fixtures/npm-static/node_modules/hop-2/package.json new file mode 100644 index 000000000..268f49b69 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-2/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-2", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-3/index.js b/tests/fixtures/npm-static/node_modules/hop-3/index.js new file mode 100644 index 000000000..83e9c2655 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-3/index.js @@ -0,0 +1,5 @@ +const next = require("hop-4"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-3/package.json b/tests/fixtures/npm-static/node_modules/hop-3/package.json new file mode 100644 index 000000000..68d73b9be --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-3/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-3", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-4/index.js b/tests/fixtures/npm-static/node_modules/hop-4/index.js new file mode 100644 index 000000000..e6af833de --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-4/index.js @@ -0,0 +1,5 @@ +const next = require("hop-5"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-4/package.json b/tests/fixtures/npm-static/node_modules/hop-4/package.json new file mode 100644 index 000000000..35987d11b --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-4/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-4", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-5/index.js b/tests/fixtures/npm-static/node_modules/hop-5/index.js new file mode 100644 index 000000000..e91733984 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-5/index.js @@ -0,0 +1,5 @@ +const next = require("hop-6"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-5/package.json b/tests/fixtures/npm-static/node_modules/hop-5/package.json new file mode 100644 index 000000000..45ee8a1bf --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-5/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-5", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-6/index.js b/tests/fixtures/npm-static/node_modules/hop-6/index.js new file mode 100644 index 000000000..c55749fc8 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-6/index.js @@ -0,0 +1,5 @@ +const next = require("hop-7"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-6/package.json b/tests/fixtures/npm-static/node_modules/hop-6/package.json new file mode 100644 index 000000000..e663924bd --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-6/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-6", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-7/index.js b/tests/fixtures/npm-static/node_modules/hop-7/index.js new file mode 100644 index 000000000..7438cec8d --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-7/index.js @@ -0,0 +1,5 @@ +const next = require("hop-8"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-7/package.json b/tests/fixtures/npm-static/node_modules/hop-7/package.json new file mode 100644 index 000000000..751f50e6c --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-7/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-7", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-8/index.js b/tests/fixtures/npm-static/node_modules/hop-8/index.js new file mode 100644 index 000000000..f60c54f01 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-8/index.js @@ -0,0 +1,5 @@ +const next = require("hop-9"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-8/package.json b/tests/fixtures/npm-static/node_modules/hop-8/package.json new file mode 100644 index 000000000..df486ce40 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-8/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-8", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/hop-9/index.js b/tests/fixtures/npm-static/node_modules/hop-9/index.js new file mode 100644 index 000000000..36861384c --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-9/index.js @@ -0,0 +1,5 @@ +const next = require("hop-10"); +function depth() { + return next.depth() + 1; +} +exports.depth = depth; diff --git a/tests/fixtures/npm-static/node_modules/hop-9/package.json b/tests/fixtures/npm-static/node_modules/hop-9/package.json new file mode 100644 index 000000000..5a6ffeb84 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/hop-9/package.json @@ -0,0 +1,5 @@ +{ + "name": "hop-9", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/fixtures/npm-static/node_modules/split-node-types/dist-node/build.js b/tests/fixtures/npm-static/node_modules/split-node-types/dist-node/build.js new file mode 100644 index 000000000..a155b52d4 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/split-node-types/dist-node/build.js @@ -0,0 +1,3 @@ +export default function build() { + return "node build"; +} diff --git a/tests/fixtures/npm-static/node_modules/split-node-types/dist-node/index.js b/tests/fixtures/npm-static/node_modules/split-node-types/dist-node/index.js new file mode 100644 index 000000000..1f79b2f93 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/split-node-types/dist-node/index.js @@ -0,0 +1,2 @@ +export { default as build } from "./build.js"; +export const NIL = "node"; diff --git a/tests/fixtures/npm-static/node_modules/split-node-types/dist/index.d.ts b/tests/fixtures/npm-static/node_modules/split-node-types/dist/index.d.ts new file mode 100644 index 000000000..47272b882 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/split-node-types/dist/index.d.ts @@ -0,0 +1,2 @@ +export declare function build(): string; +export declare const NIL: string; diff --git a/tests/fixtures/npm-static/node_modules/split-node-types/dist/index.js b/tests/fixtures/npm-static/node_modules/split-node-types/dist/index.js new file mode 100644 index 000000000..308044e34 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/split-node-types/dist/index.js @@ -0,0 +1,4 @@ +export function build() { + return "browser build"; +} +export const NIL = "browser"; diff --git a/tests/fixtures/npm-static/node_modules/split-node-types/package.json b/tests/fixtures/npm-static/node_modules/split-node-types/package.json new file mode 100644 index 000000000..aaa29cebc --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/split-node-types/package.json @@ -0,0 +1,16 @@ +{ + "name": "split-node-types", + "version": "1.0.0", + "type": "module", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "node": { + "types": "./dist/index.d.ts", + "default": "./dist-node/index.js" + }, + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + } +} diff --git a/tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/index.js b/tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/index.js new file mode 100644 index 000000000..aacc3bcce --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/index.js @@ -0,0 +1,6 @@ +"use strict"; +const domain = require("./src/domain.js"); +function getDomain(host) { + return domain.lastTwo(host); +} +exports.getDomain = getDomain; diff --git a/tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/src/domain.js b/tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/src/domain.js new file mode 100644 index 000000000..98b461afa --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ts-root-source/dist/cjs/src/domain.js @@ -0,0 +1,5 @@ +"use strict"; +function lastTwo(host) { + return host.split(".").slice(-2).join("."); +} +exports.lastTwo = lastTwo; diff --git a/tests/fixtures/npm-static/node_modules/ts-root-source/dist/types/index.d.ts b/tests/fixtures/npm-static/node_modules/ts-root-source/dist/types/index.d.ts new file mode 100644 index 000000000..dac3eccb0 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ts-root-source/dist/types/index.d.ts @@ -0,0 +1 @@ +export declare function getDomain(host: string): string; diff --git a/tests/fixtures/npm-static/node_modules/ts-root-source/index.ts b/tests/fixtures/npm-static/node_modules/ts-root-source/index.ts new file mode 100644 index 000000000..de03041f8 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ts-root-source/index.ts @@ -0,0 +1 @@ +export { getDomain } from "./src/domain"; diff --git a/tests/fixtures/npm-static/node_modules/ts-root-source/package.json b/tests/fixtures/npm-static/node_modules/ts-root-source/package.json new file mode 100644 index 000000000..edfff933b --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ts-root-source/package.json @@ -0,0 +1,7 @@ +{ + "name": "ts-root-source", + "version": "1.0.0", + "main": "dist/cjs/index.js", + "module": "dist/es6/index.js", + "types": "dist/types/index.d.ts" +} diff --git a/tests/fixtures/npm-static/node_modules/ts-root-source/src/domain.ts b/tests/fixtures/npm-static/node_modules/ts-root-source/src/domain.ts new file mode 100644 index 000000000..fe1e5eb18 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ts-root-source/src/domain.ts @@ -0,0 +1,3 @@ +export function getDomain(host: string): string { + return "TypeScript source: " + host; +} diff --git a/tests/fixtures/npm-static/split-node-types-cli.ts b/tests/fixtures/npm-static/split-node-types-cli.ts new file mode 100644 index 000000000..ecfac96ba --- /dev/null +++ b/tests/fixtures/npm-static/split-node-types-cli.ts @@ -0,0 +1,3 @@ +import { build, NIL } from "split-node-types"; + +console.log(build(), NIL); diff --git a/tests/fixtures/npm-static/ts-root-source-cli.ts b/tests/fixtures/npm-static/ts-root-source-cli.ts new file mode 100644 index 000000000..3741b48e5 --- /dev/null +++ b/tests/fixtures/npm-static/ts-root-source-cli.ts @@ -0,0 +1,3 @@ +import { getDomain } from "ts-root-source"; + +console.log(getDomain("a.b.github.io")); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index c5021fd06..99d890d4c 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -69,7 +69,7 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise const hash = createHash("sha256"); const inputs = [ entry, - ...globSync(join(pilotRoot, "**/node_modules/**/*.{js,mjs,cjs,json,d.ts}")).sort(), + ...globSync(join(pilotRoot, "**/node_modules/**/*.{js,mjs,cjs,json,ts}")).sort(), ...globSync( join(fixturesRoot, "commander-calc/node_modules/**/*.{js,mjs,cjs,json,d.ts}"), ).sort(), @@ -999,6 +999,40 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }, 180_000); + // The two resolvers must answer one file for every opted-in edge. Each + // shape below once left the runtime file outside tsgo's program (SC2013 + // "did not join the type-checked program"): uuid's node condition with + // declarations in another tree, tldts's package-root TypeScript source + // beside `main`, express's same-version copies of one package (tsgo's + // package-id redirect), and get-intrinsic's dependency chain deeper than + // maxNodeModuleJsDepth. + test.each([ + ["split-node-types-cli.ts", ["split-node-types"]], + ["ts-root-source-cli.ts", ["ts-root-source"]], + ["dup-install-cli.ts", ["dup-left", "dup-right", "dup-leaf"]], + ["deep-packages-cli.ts", Array.from({ length: 11 }, (_, i) => `hop-${i + 1}`)], + ])( + "%s joins the type-checked program and matches Node", + async (name, pkgs) => { + const entry = join(pilotRoot, name); + const { coverage } = analyze(entry, { npmStatic: pkgs }); + expect(coverage.npmStatic.map((p) => [p.package, p.status]).sort()).toEqual( + pkgs.map((p) => [p, "static"]).sort(), + ); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, pkgs); + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry]), + runBinary(binary, []), + ]); + expect(nativeRes.stdout).toEqual(nodeRes.stdout); + expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + }, + 180_000, + ); + test("recursive inferred JavaScript results retain their promised ABI", async () => { const entry = join(pilotRoot, "recursive-result-cli.ts"); const binary = await buildStatic(entry, ["recursive-result"]); From 21ae43bd88ed3d616dc542ff2e55b1fc529f6726 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:17 -0700 Subject: [PATCH 033/120] Guard keyed reads of a null-initialized record table A keyed read whose checker type is a record but whose storage is `record | null` (iconv-lite's `module.exports.encodings = null`, filled on first use and read as `encodings[enc]`) built a recordKeyGet on the union and failed IR validation with SC9001. The optional-receiver guard that keyed reads already use for `record | undefined` now also takes a null arm for string and number keys: the receiver and key evaluate once, a null receiver throws Node's "Cannot read properties of null (reading 'k')", and a present one reads the record. Other keys keep their checked-dynamic read, and keyed writes are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-exprs.ts | 44 ++++++++++++++----- .../6185-cjs-null-table-keyed-read.cjs.json | 6 +++ .../corpus/6185-cjs-null-table-keyed-read.cjs | 23 ++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json create mode 100644 tests/corpus/6185-cjs-null-table-keyed-read.cjs diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d8d969973..c54341df8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -7173,25 +7173,38 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre /** A record receiver that an outer keyed read left optional * (`nested[a][b]`, where `nested[a]` can miss). The receiver evaluates * once into `stmts`; `present` is its record arm, valid only once the - * access has tested `stable` against the undefined arm. */ + * access has tested `stable` against the undefined (or, for reads, null) + * arm. */ interface OptionalRecordReceiver { stmts: IrStmt[]; stable: IrExpr; present: IrExpr; unionId: string; undefTag: number; + nullish: "undefined" | "null"; } function optionalRecordReceiver( lowerer: Lowerer, value: IrExpr, loc: SrcLoc, + allowNull = false, ): OptionalRecordReceiver | null { if (value.type.kind !== "union") return null; const unionId = value.type.unionId; - const undefTag = lowerer.armTag(unionId, UNDEFINED_T); - if (undefTag < 0 || lowerer.unions.get(unionId)?.arms.length !== 2) return null; - const record = lowerer.stripUndefinedArm(value.type); + const arms = lowerer.unions.get(unionId)?.arms; + if (arms?.length !== 2) return null; + let undefTag = lowerer.armTag(unionId, UNDEFINED_T); + let nullish: OptionalRecordReceiver["nullish"] = "undefined"; + // A null-initialized slot assigned a record later (iconv-lite's + // `encodings = null` table, filled on first use): the checker narrows + // the read to the record, the storage keeps the null arm. + if (undefTag < 0 && allowNull) { + undefTag = lowerer.armTag(unionId, NULL_T); + nullish = "null"; + } + if (undefTag < 0) return null; + const record = arms[1 - undefTag]!; if (record.kind !== "record") return null; const stmts: IrStmt[] = []; const stable = stabilized(lowerer, value, "%recordReceiver", stmts); @@ -7203,7 +7216,7 @@ function optionalRecordReceiver( type: record, loc, }; - return { stmts, stable, present, unionId, undefTag }; + return { stmts, stable, present, unionId, undefTag, nullish }; } /** `value`, evaluated once at this point of `out`. */ @@ -7226,14 +7239,16 @@ function undefinedReceiver(receiver: OptionalRecordReceiver, loc: SrcLoc): IrExp }; } -/** Node's TypeError for a keyed read or write on an undefined receiver. */ +/** Node's TypeError for a keyed read or write on an undefined (or null) + * receiver. */ function undefinedReceiverThrow( key: IrExpr, action: "read" | "set", type: IrType, loc: SrcLoc, + nullish: OptionalRecordReceiver["nullish"] = "undefined", ): IrExpr { - const head = `Cannot ${action} properties of undefined (${action === "read" ? "reading" : "setting"} '`; + const head = `Cannot ${action} properties of ${nullish} (${action === "read" ? "reading" : "setting"} '`; const message: IrExpr = { kind: "strConcat", left: { @@ -7263,8 +7278,9 @@ function guardedRecordAccess( lowerer: Lowerer, access: ts.ElementAccessExpression, rawObj: IrExpr, + allowNull = false, ): { receiver: OptionalRecordReceiver; key: IrExpr; name: IrExpr } | null { - const receiver = optionalRecordReceiver(lowerer, rawObj, locOf(access.expression)); + const receiver = optionalRecordReceiver(lowerer, rawObj, locOf(access.expression), allowNull); if (!receiver) return null; const keyNode = access.argumentExpression; const litKey = recordKeyLiteralText(keyNode) ?? recordKeyTypeLiteralText(lowerer, keyNode); @@ -7450,7 +7466,15 @@ export function lowerRecordKeyRead( // evaluates the receiver before the key). const obj = lowerer.lowerExpr(expr.expression); { - const guarded = guardedRecordAccess(lowerer, expr, obj); + // A null arm is guarded only for string and number keys: other keys + // already take the checked-dynamic read, which handles null itself. + // Only a receiver with a null arm asks about the key. + let allowNull = false; + if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, NULL_T) >= 0) { + const keyType = litKey !== null ? STRING : lowerer.mapTypeOf(lowerer.typeOf(keyNode)); + allowNull = keyType?.kind === "string" || keyType?.kind === "f64"; + } + const guarded = guardedRecordAccess(lowerer, expr, obj, allowNull); if (guarded) { const { receiver, key, name } = guarded; const read = lowerer.withExpressionOverride(expr.expression, receiver.present, () => @@ -7466,7 +7490,7 @@ export function lowerRecordKeyRead( result: { kind: "ternary", cond: undefinedReceiver(receiver, loc), - then: undefinedReceiverThrow(name, "read", read.type, loc), + then: undefinedReceiverThrow(name, "read", read.type, loc, receiver.nullish), else_: read, type: read.type, loc, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json new file mode 100644 index 000000000..c3a6e5492 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6185-cjs-null-table-keyed-read.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/6185-cjs-null-table-keyed-read.cjs b/tests/corpus/6185-cjs-null-table-keyed-read.cjs new file mode 100644 index 000000000..41bfb1f1f --- /dev/null +++ b/tests/corpus/6185-cjs-null-table-keyed-read.cjs @@ -0,0 +1,23 @@ +// A keyed read of an exported table that starts out null and is filled +// later (iconv-lite's `encodings`). The checker narrows the read to the +// table, while the export's storage keeps the null arm. +'use strict'; + +/** @type {{ [name: string]: string } | null} */ +module.exports.encodings = null; + +/** @param {string} name */ +function note(name) { + console.log('key', name); + return name; +} + +/** @param {string} name */ +const lookup = function lookup(name) { + if (!module.exports.encodings) return 'none'; + return module.exports.encodings[note(name)]; +}; + +console.log(lookup('utf8')); +module.exports.encodings = { utf8: 'UTF-8', latin1: 'ISO-8859-1' }; +console.log(lookup('utf8'), lookup('latin1'), lookup('ascii')); From 74646b783ff75d1dabdf0d10be1f01c474913614 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:18 -0700 Subject: [PATCH 034/120] Check a dynamic `{ encoding }` option into its record for encoded reads `fs.readFileSync(file, { encoding })` and `fs.promises.readFile` read the encoding field straight off the lowered options literal. When the encoding value is checked-dynamic (dotenv's `t = e.encoding`), the literal lowers to a dynamic object, and the record field read failed IR validation with SC9001 "recordGet receiver: expected record, got dyn". Check such a literal into the `{ encoding: string }` record the checker describes before reading the field. Co-Authored-By: Claude Opus 5.5 --- .../lowering/builtins/filesystem-calls.ts | 13 +++++++++-- ...186-js-fs-encoding-dynamic-option.cjs.json | 6 +++++ .../6186-js-fs-encoding-dynamic-option.cjs | 22 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json create mode 100644 tests/corpus/6186-js-fs-encoding-dynamic-option.cjs diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index 754eb6aa9..d6864a2d9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -23,6 +23,7 @@ import { VOID, arrayOf, funcOf, + typeEquals, } from "../../../ir/ir.js"; import { lowerBuiltinOptionalDefault, optionMember, literalBoolOptions } from "./arguments.js"; import { @@ -603,6 +604,14 @@ export function lowerFsReadDescriptorCall( return { kind: "libCall", fn: "fs.readFdSync", args: [fd, enc], type: STRING, loc }; } +/** The `{ encoding }` options record. A literal whose value is checked- + * dynamic (an encoding variable reassigned from an untyped source) lowers + * to a dynamic object: check it into the record the checker describes. */ +function encodingOptions(lowerer: Lowerer, options: ts.Expression, type: IrType): IrExpr { + const value = lowerer.lowerExpr(options); + return typeEquals(value.type, type) ? value : lowerer.coerceInto(options, value, type); +} + /** Encoding-bearing reads share Buffer's decoder and runtime alias checks. * Keep the read and encoding evaluation in source order in one helper. */ export function lowerFsEncodedRead( @@ -631,7 +640,7 @@ export function lowerFsEncodedRead( ? lowerer.lowerExprExpecting(options, STRING) : { kind: "recordGet", - obj: lowerer.lowerExpr(options), + obj: encodingOptions(lowerer, options, optionsType), shapeId: optionsType.shapeId, field: "encoding", type: STRING, @@ -692,7 +701,7 @@ export function lowerFsPromisesEncodedRead( ? lowerer.lowerExprExpecting(options, STRING) : { kind: "recordGet", - obj: lowerer.lowerExpr(options), + obj: encodingOptions(lowerer, options, optionsType), shapeId: optionsType.shapeId, field: "encoding", type: STRING, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json new file mode 100644 index 000000000..7e6f4d8d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs b/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs new file mode 100644 index 000000000..cea0fae8f --- /dev/null +++ b/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs @@ -0,0 +1,22 @@ +// readFileSync with a `{ encoding }` literal whose value comes from an +// untyped source (dotenv's configDotenv): the option object is checked +// into the record the checker describes. +'use strict'; + +const fs = require('fs'); + +/** + * @param {string} file + * @param {any} options + */ +function read(file, options) { + let encoding = 'utf8'; + if (options && options.encoding) encoding = options.encoding; + return fs.readFileSync(file, { encoding }); +} + +const latin1 = read(__filename, { encoding: 'latin1' }); +const utf8 = read(__filename, null); +if (typeof latin1 === 'string' && typeof utf8 === 'string') { + console.log(latin1.length === utf8.length, utf8.split('\n')[0]); +} From 3b9272e8f899192f4c7aa448e9929ab64b53bc73 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:18 -0700 Subject: [PATCH 035/120] Keep Date slots out of runtime-optional undefined widening The runtime-optional analysis widens a parameter or binding that can observe undefined with an undefined arm. For a Date it interned the union `Date | undefined`, which is not a valid union (Date is not a union arm kind), and the whole program failed IR validation with SC9001 "union uN: arm 0 is date" (morgan's date token and clfdate). Like generators, a Date keeps its type: the canonical undefined-arm helper already refuses Date, and a site that would store undefined into the slot keeps its own diagnostic. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 6 +++- .../corpus/6183-js-date-optional-slot.js.json | 6 ++++ tests/corpus/6183-js-date-optional-slot.js | 36 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json create mode 100644 tests/corpus/6183-js-date-optional-slot.js diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index adca3dfc5..5d7713728 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4353,7 +4353,11 @@ export class Lowerer { const addUndefined = (t: IrType): IrType => { if (t.kind === "union") return this.armTag(t.unionId, UNDEFINED_T) >= 0 ? t : (this.withUndefinedArmOf(t) ?? t); - if (t.kind === "void" || t.kind === "dyn" || t.kind === "generator") return t; + // A Date has no undefined-armed union (it is not a union arm kind): + // like a generator it keeps its type, and the site that would store + // undefined into it keeps its own diagnostic. + if (t.kind === "void" || t.kind === "dyn" || t.kind === "generator" || t.kind === "date") + return t; return this.withUndefinedArm(t); }; const fieldName = (node: ts.PropertyAccessExpression): string => node.name.text; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json new file mode 100644 index 000000000..ded598761 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6183-js-date-optional-slot.js" + ], + "diags": [] +} diff --git a/tests/corpus/6183-js-date-optional-slot.js b/tests/corpus/6183-js-date-optional-slot.js new file mode 100644 index 000000000..9c37d85b9 --- /dev/null +++ b/tests/corpus/6183-js-date-optional-slot.js @@ -0,0 +1,36 @@ +// A Date binding and parameter that the runtime-optional analysis would +// widen with an undefined arm (morgan's date token). Date values have no +// undefined-armed union, so the bindings keep their Date type. +'use strict'; + +/** @type {Record string | undefined>} */ +const tokens = {}; + +/** + * @param {string} name + * @param {(format: string | undefined) => string | undefined} fn + */ +function token(name, fn) { + tokens[name] = fn; +} + +token('date', function getDateToken(format) { + var date = new Date(86400000 * 400); + switch (format || 'iso') { + case 'clf': + return clfdate(date); + case 'iso': + return date.toISOString(); + } +}); + +/** + * @param {Date} dateTime + * @return {string} + */ +function clfdate(dateTime) { + return dateTime.getUTCDate() + '/' + dateTime.getUTCMonth() + '/' + dateTime.getUTCFullYear(); +} + +const date = tokens['date']; +if (date) console.log(date('clf'), date('iso'), date(undefined), date('other')); From 92310a68fc573e7c6583e13d6e1c872bc0a1962d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:43:15 -0700 Subject: [PATCH 036/120] Keep statement fences in functions hoisted during a speculative lowering A reference to a later function declaration in the same function scope lowers that declaration on the spot (predeclareForwardFnDecl), and the result is kept. When the reference sat inside a speculative attempt (tryLowerExpression's temporary diagnostic sink, here the native function assignment in bignumber.js's `var P = BigNumber.prototype = {...}`), the hoisted body lowered under that sink: its failing JavaScript statements were dropped instead of becoming runtime fences. bignumber's `isBigNumber` lost its only `return`, so every program using the package aborted at load with SC9002 "a non-void function completed without returning", and its constructor silently lost its instanceof check. Lower the hoisted declaration without the probe's sink, as eager generic instances already do. A declaration that fails as a whole is not kept, so its diagnostics go back to the probe. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stmts.ts | 11 ++++ .../speculative-hoisted-functions.test.ts | 64 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 packages/compiler/test/speculative-hoisted-functions.test.ts diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 0f1b606ef..65f39c6ab 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -935,12 +935,23 @@ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): bo entry.ctx.hoistedFnDecls.add(decl); const fnTail = lowerer.fnStack.splice(depth + 1); const scopeTail = entry.ctx.scopes.splice(frameIdx + 1); + // The hoisted function is kept even when the reference lowers inside + // a speculative attempt (a probe's temporary diagnostic sink): lower + // it without that sink, so its failing JS statements keep their + // runtime fences instead of vanishing from a body that is kept. A + // declaration that fails as a whole is not kept: its diagnostics go + // back to the probe. + const previousSink = lowerer.diagSink; + const diagsBefore = lowerer.diags.length; + lowerer.diagSink = null; try { entry.out.push(lowerer.lowerNestedFunctionDecl(decl)); } catch (e) { entry.ctx.hoistedFnDecls.delete(decl); + if (previousSink !== null) previousSink.push(...lowerer.diags.splice(diagsBefore)); throw e; } finally { + lowerer.diagSink = previousSink; entry.ctx.scopes.push(...scopeTail); lowerer.fnStack.push(...fnTail); } diff --git a/packages/compiler/test/speculative-hoisted-functions.test.ts b/packages/compiler/test/speculative-hoisted-functions.test.ts new file mode 100644 index 000000000..a58310b2a --- /dev/null +++ b/packages/compiler/test/speculative-hoisted-functions.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; + +// bignumber.js's clone(): `P = BigNumber.prototype = {...}` in a `var` +// list above the constructor. The initializer lowers speculatively first, +// which hoists the constructor and the helper it calls. Those bodies are +// kept, so their failing statements must keep runtime fences; dropping +// them left `isNum` without its return (an SC9002 trap at load). +const source = ` +function clone(config) { + var P = (Num.prototype = { constructor: Num, toString: null }), + unit = config ? 2 : 1; + function Num(v) { + var x = this; + if (!(x instanceof Num)) return new Num(v); + x.v = isNum(v) ? 0 : v * unit; + } + function isNum(v) { + return v instanceof Num || (!!v && v._isNum === true); + } + P._isNum = true; + return Num; +} +const Num = clone(); +console.log(typeof Num); +`; + +async function lower(): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-speculative-hoisted-")); + try { + const entry = join(dir, "main.mjs"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function fenceCodes(node: unknown, out: string[] = []): string[] { + if (Array.isArray(node)) { + for (const item of node) fenceCodes(item, out); + } else if (node !== null && typeof node === "object") { + const record = node as Record; + if (record["kind"] === "runtimeFence") out.push(String(record["code"])); + for (const [key, value] of Object.entries(record)) if (key !== "loc") fenceCodes(value, out); + } + return out; +} + +test("speculatively hoisted JavaScript functions keep their statement fences", async () => { + const mod = await lower(); + const isNum = mod.functions.find((fn) => fn.name.endsWith("_isNum")); + expect(isNum).toBeDefined(); + expect(fenceCodes(isNum!.body)).toEqual(["SC1090"]); + expect(mod.functions.flatMap((fn) => fenceCodes(fn.body))).not.toContain("SC9002"); +}); From beddc5f94ab61c0cfc0ad2503d971e83199eb1fa Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:52:04 -0700 Subject: [PATCH 037/120] Keep typescript-go's tuple-literal panic with the statements that ask `s.match(re) || []` contextually types the empty array by the tuple-like RegExpMatchArray, so typescript-go builds an empty tuple target and clones it as an array literal. The clone keeps ObjectFlagsTuple on TypeReference data, and the API's newTypeResponse panics (interface conversion to *TupleType) whenever it serializes the clone, for example as a member of `RegExpMatchArray | []`. Whole-program analyses asked for such union members first, so every program containing the pattern (moment.js, or a two-line TypeScript file) failed at 1:1 with SC0004. A union whose members typescript-go panics to serialize is now unmappable at the top of mapType, the any-array probe answers false for it, and the panic is remembered per type instead of re-requested. Queries that still need the members keep the source-anchored SC0004 on their own statement (a runtime fence in JavaScript). Other checker panics are unchanged. The panic itself is an upstream bug in internal/api/proto.go. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 8 +++++++- packages/compiler/src/frontend/ts7/checker.ts | 10 ++++++++++ .../src/frontend/ts7/semantic-model.ts | 20 +++++++++++++++++-- packages/compiler/src/frontend/type-mapper.ts | 11 +++++++++- .../6187-js-match-or-empty-array.js.json | 6 ++++++ tests/corpus/6187-js-match-or-empty-array.js | 20 +++++++++++++++++++ tests/diagnostics/checker-panic-tuple.ts | 5 +++-- .../__snapshots__/checker-panic-tuple.ts.txt | 18 ++++++++--------- 8 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json create mode 100644 tests/corpus/6187-js-match-or-empty-array.js diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 5d7713728..5bcf15b1a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -7746,7 +7746,13 @@ export class Lowerer { if (part.isUnionType()) return ts.constituentTypes(part).every(visit); return false; }; - return visit(t); + // A union typescript-go cannot describe is not proven an array. + try { + return visit(t); + } catch (e) { + if (ts.isUnreadableConstituentsPanic(e)) return false; + throw e; + } } /** The lowered static array/tuple value behind checkerAnyArray's diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 9d7634591..bf83b367c 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -120,6 +120,16 @@ export function constituentTypes(type: Type): readonly Type[] { return type.getTypes() ?? []; } +/** True for the panic typescript-go's API raises serializing a union's + * constituents (an empty tuple cloned as an array literal keeps the tuple + * flag without tuple data: `s.match(re) || []`). The union itself is + * readable; its members are not. */ +export function isUnreadableConstituentsPanic(e: unknown): boolean { + return ( + e instanceof Error && (e as { unreadableConstituents?: true }).unreadableConstituents === true + ); +} + /** Function-like declarations whose body is deferred until reachability * asks for it. Header prefetch walks their names, type parameters, params, * and return types but leaves the body for a later explicit body wave. */ diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 28625dd03..095021922 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -408,6 +408,7 @@ export class SemanticType { private trueType: number | false = false; private falseType: number | false = false; private constituents: SemanticType[] | undefined; + private constituentsPanic: Error | undefined; constructor( private readonly data: TypeResponse, @@ -497,12 +498,27 @@ export class SemanticType { this.project.ensureActive(); if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) return undefined; - if (this.constituents === undefined) - this.constituents = this.project.fetchTypes(this.id, "getTypesOfType"); + if (this.constituentsPanic !== undefined) throw this.constituentsPanic; + if (this.constituents === undefined) { + try { + this.constituents = this.project.fetchTypes(this.id, "getTypesOfType"); + } catch (e) { + // typescript-go panics serializing some constituents (an empty + // tuple cloned as an array literal): answer every later read of + // this snapshot's type with the same panic instead of a new + // request. + if (e instanceof Error && e.message.startsWith("panic:")) { + (e as { unreadableConstituents?: true }).unreadableConstituents = true; + this.constituentsPanic = e; + } + throw e; + } + } return this.constituents; } dispose(): void { this.constituents = undefined; + this.constituentsPanic = undefined; } getBaseTypes(): SemanticType[] | undefined { this.project.ensureActive(); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f37703fd8..e0558ae5f 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1336,7 +1336,16 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const sensitivityAtEntry = contextResolutions; mapTypeDepth++; try { - const mapped = mapTypeInner(type, ctx); + let mapped: IrType | null; + try { + mapped = mapTypeInner(type, ctx); + } catch (e) { + // typescript-go's API can panic serializing a union's members (an + // empty tuple cloned as an array literal, `s.match(re) || []`): a + // union the checker cannot describe has no representation. + if (!topLevel || !ts.isUnreadableConstituentsPanic(e)) throw e; + mapped = null; + } if ( mapped !== null && memoKey !== null && diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json new file mode 100644 index 000000000..75792b997 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6187-js-match-or-empty-array.js" + ], + "diags": [] +} diff --git a/tests/corpus/6187-js-match-or-empty-array.js b/tests/corpus/6187-js-match-or-empty-array.js new file mode 100644 index 000000000..2cce0e17d --- /dev/null +++ b/tests/corpus/6187-js-match-or-empty-array.js @@ -0,0 +1,20 @@ +// @deferred-fences: 1 +// `s.match(re) || []` types the empty array as an empty tuple that the +// typescript-go API panics to describe (moment.js's configFromStringAndFormat). +// The panic stays with the statements whose lowering asks about that +// type, so the rest of the program compiles and runs. + +/** @param {string} s */ +function firstDigit(s) { + return (s.match(/\d/) || [])[0]; +} + +/** @param {string} s */ +function digitCount(s) { + let count = 0; + for (const ch of s) if (ch >= '0' && ch <= '9') count++; + return count; +} + +const parsers = { firstDigit, digitCount }; +console.log(typeof parsers.firstDigit, parsers.digitCount('a1b22')); diff --git a/tests/diagnostics/checker-panic-tuple.ts b/tests/diagnostics/checker-panic-tuple.ts index fd6eacb8c..cf59bc1f0 100644 --- a/tests/diagnostics/checker-panic-tuple.ts +++ b/tests/diagnostics/checker-panic-tuple.ts @@ -1,5 +1,6 @@ // `''.match(/x/) || []` drives tsgo into the TupleType interface-conversion -// panic (upstream signature 01/13): the panic fence turns it into the -// source-anchored SC0004 — a failed compile, never a crashed CLI. +// panic (upstream signature 01/13) whenever the union's members are +// serialized: the union stays unmappable, so the statement reports its own +// refusals — a failed compile, never a crashed CLI. const a = ''.match(/x/) || []; console.log(a.length); diff --git a/tests/harness/__snapshots__/checker-panic-tuple.ts.txt b/tests/harness/__snapshots__/checker-panic-tuple.ts.txt index 46d22bcd0..3927e5518 100644 --- a/tests/harness/__snapshots__/checker-panic-tuple.ts.txt +++ b/tests/harness/__snapshots__/checker-panic-tuple.ts.txt @@ -1,13 +1,13 @@ -checker-panic-tuple.ts:4:1 - error SC0004: the TypeScript checker crashed answering a query here (an upstream typescript-go bug): panic: interface conversion: checker.TypeData is *checker.TypeReference, not *checker.TupleType +checker-panic-tuple.ts:5:11 - error SC1090: logical operators on union-typed values outside conditions (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet - 3 | // source-anchored SC0004 — a failed compile, never a crashed CLI. - 4 | const a = ''.match(/x/) || []; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 5 | console.log(a.length); + 4 | // refusals — a failed compile, never a crashed CLI. + 5 | const a = ''.match(/x/) || []; + | ^~~~~~~~~~~~~~~~~~~ + 6 | console.log(a.length); -checker-panic-tuple.ts:5:13 - error SC1090: reading 'length' on a union-typed value (every arm must be an object/record with a same-typed field 'length'; narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet +checker-panic-tuple.ts:6:13 - error SC1090: reading 'length' on a union-typed value (every arm must be an object/record with a same-typed field 'length'; narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet - 4 | const a = ''.match(/x/) || []; - 5 | console.log(a.length); + 5 | const a = ''.match(/x/) || []; + 6 | console.log(a.length); | ^~~~~~~~ - 6 | \ No newline at end of file + 7 | \ No newline at end of file From 66027d850bf9d6245f0fa8184276cb3aace5da7e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 15:53:37 -0500 Subject: [PATCH 038/120] Explain 6110's deferred-fence count and record its corpus costs Corpus 6110 has three statements with no static lowering, and its @deferred-fences directive is 9 because coverage counts each lowering of a wrapper body: the .call and .apply wrapper bodies are each lowered four times. The program's header now says so. The commit also adds the program's corpus-costs entries, which shard balancing reads. --- tests/corpus/6110-cjs-speculative-wrapper-fences.cjs | 3 +++ tests/harness/corpus-costs.json | 2 ++ 2 files changed, 5 insertions(+) diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index 6040f009c..1b3546518 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -4,6 +4,9 @@ // every statement of their bodies. A statement with no static lowering // (here `module.exports` read as a value) compiles to its runtime fence // instead of vanishing; these never run, so the binary matches Node. +// Coverage counts the three fenced statements nine times: each lowering of +// a wrapper body counts its statements, and the `.call` and `.apply` +// wrapper bodies are each lowered four times. const verbose = process.argv.length > 99; (function () { var root = this; diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 5403a1468..18147374d 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2217,6 +2217,7 @@ "6104-typeof-builtin-bindings.mjs": 2031, "6105-cjs-top-level-this/main.js": 1371, "6106-esm-top-level-this.mjs": 1915, + "6110-cjs-speculative-wrapper-fences.cjs": 1980, "6140-cjs-require-tdz-throws/main.js": 2731, "6141-cjs-require-tdz-checked/main.js": 2856, "6142-cjs-require-published-early/main.js": 3400, @@ -5256,6 +5257,7 @@ "6104-typeof-builtin-bindings.mjs": 1715, "6105-cjs-top-level-this/main.js": 3213, "6106-esm-top-level-this.mjs": 1672, + "6110-cjs-speculative-wrapper-fences.cjs": 2500, "6140-cjs-require-tdz-throws/main.js": 2989, "6141-cjs-require-tdz-checked/main.js": 4062, "6142-cjs-require-published-early/main.js": 5156, From afef6f09f58ca84de5e2510c7f247afd2a5a829e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 16:12:14 -0500 Subject: [PATCH 039/120] Apply each request's maxHeaderSize to a pooled socket's parser A request that took a socket from a keepAlive agent, either queued for a freed socket or taking a free one, parsed its response with the parser of the request that used the socket before, and with that request's maxHeaderSize. A smaller limit was not enforced, and a larger or default limit was cut down to the earlier one. The request now keeps its limit (0 for the default) and applies it to the pooled parser when it takes the socket, before any response bytes are parsed. Corpus 5594 covers both handoffs with smaller, larger and default limits. --- .../5594-http-agent-header-limit.ts.json | 6 ++ packages/runtime/src/scr_http.c | 7 +- tests/corpus/5594-http-agent-header-limit.ts | 70 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5594-http-agent-header-limit.ts.json create mode 100644 tests/corpus/5594-http-agent-header-limit.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5594-http-agent-header-limit.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5594-http-agent-header-limit.ts.json new file mode 100644 index 000000000..a5ad20f1f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5594-http-agent-header-limit.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5594-http-agent-header-limit.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index a931ba1f6..f4e44bf6e 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -3763,6 +3763,8 @@ struct ScrHttpClientReq { * fired in place of the teardown's own error */ ScrDyn *abort_dispose; /* options.signal: removes the abort listener (+1) */ double timeout_ms; /* the timeout option (0 = none), reapplied on a pooled socket */ + size_t max_header_size; /* the maxHeaderSize option (0 = the default), applied to + * whichever parser serves the request */ bool reused; /* req.reusedSocket: the socket came from an agent's free list */ bool sock_pooled; /* the socket went back to the agent after the response */ ScrStr *pool_key; /* https: TLS options a pooled socket must match (+1), or NULL */ @@ -4345,7 +4347,8 @@ void scr_http_client_max_header_size(ScrHttpClientReq *c, double value) { scr_http_client_destroy(c); return; } - if (c->conn != NULL) c->conn->max_header_size = (size_t)value; + c->max_header_size = (size_t)value; + if (c->conn != NULL) c->conn->max_header_size = c->max_header_size; } /* req.destroy(err): tears the request down like destroy() and reports @@ -5284,6 +5287,8 @@ static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, boo c->conn = conn; conn->client = scr_http_client_retain(c); conn->pool_agent = NULL; + /* the pooled parser still holds the previous request's limit */ + conn->max_header_size = c->max_header_size; conn->state = SCR_HTTP_HEAD; conn->len = 0; conn->body_remaining = 0; diff --git a/tests/corpus/5594-http-agent-header-limit.ts b/tests/corpus/5594-http-agent-header-limit.ts new file mode 100644 index 000000000..e3deb9954 --- /dev/null +++ b/tests/corpus/5594-http-agent-header-limit.ts @@ -0,0 +1,70 @@ +// A request's maxHeaderSize bounds its own response head, also when the +// request runs on a socket an earlier request used. Through a keepAlive +// agent with maxSockets 1, a queued request receives the freed socket and +// a later request takes the free one (reusedSocket). In both cases a +// smaller limit fails the response with HPE_HEADER_OVERFLOW, and a larger +// limit, or the default one, accepts a head the earlier request's limit +// would reject. +import * as http from "node:http"; +import * as net from "node:net"; + +// Answers each request with an X-Big header of the size in its path, on a +// connection that stays open. +const upstream = net.createServer((s) => { + s.on("error", () => {}); + let pending = ""; + s.on("data", (chunk: Buffer) => { + pending += chunk.toString("latin1"); + let end = pending.indexOf("\r\n\r\n"); + while (end >= 0) { + const head = pending.slice(0, end); + pending = pending.slice(end + 4); + const size = Number(head.split(" ")[1]!.slice(1)); + s.write(`HTTP/1.1 200 OK\r\nX-Big: ${"y".repeat(size)}\r\nContent-Length: 2\r\n\r\nok`); + end = pending.indexOf("\r\n\r\n"); + } + }); +}); + +function get(agent: http.Agent, port: number, label: string, size: number, limit?: number): Promise { + return new Promise((resolve) => { + const onResponse = (res: http.IncomingMessage) => { + res.resume(); + res.on("end", () => { + console.log(label, "status", res.statusCode, "reused", req.reusedSocket); + resolve(); + }); + }; + const path = `/${size}`; + const req = + limit === undefined + ? http.get({ host: "127.0.0.1", port, path, agent }, onResponse) + : http.get({ host: "127.0.0.1", port, path, agent, maxHeaderSize: limit }, onResponse); + req.on("error", (err: NodeJS.ErrnoException) => { + console.log(label, "error", err.code, err.message, "reused", req.reusedSocket); + resolve(); + }); + }); +} + +upstream.listen(0, "127.0.0.1", async () => { + const port = (upstream.address() as { port: number }).port; + + // Queued: the second request waits for the slot and receives the socket. + const queued = new http.Agent({ keepAlive: true, maxSockets: 1 }); + await Promise.all([get(queued, port, "queued first", 100, 1000), get(queued, port, "queued smaller", 100, 50)]); + await Promise.all([get(queued, port, "queued first", 10, 100), get(queued, port, "queued larger", 17000, 20000)]); + await Promise.all([get(queued, port, "queued first", 10, 100), get(queued, port, "queued default", 1000)]); + queued.destroy(); + + // Free: each later request starts after the earlier one finished. + const free = new http.Agent({ keepAlive: true, maxSockets: 1 }); + await get(free, port, "free first", 100, 1000); + await get(free, port, "free smaller", 100, 50); + await get(free, port, "free first", 10, 100); + await get(free, port, "free larger", 17000, 20000); + await get(free, port, "free first", 10, 100); + await get(free, port, "free default", 1000); + free.destroy(); + upstream.close(); +}); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 88a1dea36..d16266dc9 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1945,6 +1945,7 @@ "5570-fs-cp-async.ts": 3348, "5571-fs-cp-deep.ts": 4817, "5585-record-results-inlined.ts": 3419, + "5594-http-agent-header-limit.ts": 2622, "5595-net-ref-unref.ts": 3245, "5596-http-agent-keep-alive.ts": 3194, "5597-http-agent-idle-sockets.ts": 4248, @@ -4970,6 +4971,7 @@ "5570-fs-cp-async.ts": 4176, "5571-fs-cp-deep.ts": 5306, "5585-record-results-inlined.ts": 3957, + "5594-http-agent-header-limit.ts": 2691, "5595-net-ref-unref.ts": 5321, "5596-http-agent-keep-alive.ts": 3568, "5597-http-agent-idle-sockets.ts": 5008, From 6eb11bf7f4affc14bd0d4c86268ba078aab6d871 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:29:24 -0700 Subject: [PATCH 040/120] Pin frame records that are arms of record unions in the corpus A record shape that is an arm of a union of records carries a hidden shape word at struct index 1, so its fields start at index 2. The new program keeps such records in frame storage while they own string and array fields, reads and writes those fields, passes the shapes through the union, compares identities and serializes them. With the frame storage's reference-field slots addressed at index + 1 instead of the shape's field base, the compiled program segfaults in the plain lane and ASan reports a heap-buffer-overflow in the sanitized one. Co-Authored-By: Claude Opus 5.5 --- ...997-frame-records-in-record-unions.ts.json | 6 ++ .../5997-frame-records-in-record-unions.ts | 88 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json create mode 100644 tests/corpus/5997-frame-records-in-record-unions.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json new file mode 100644 index 000000000..7e3fb1c50 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5997-frame-records-in-record-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5997-frame-records-in-record-unions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5997-frame-records-in-record-unions.ts b/tests/corpus/5997-frame-records-in-record-unions.ts new file mode 100644 index 000000000..c066c3edb --- /dev/null +++ b/tests/corpus/5997-frame-records-in-record-unions.ts @@ -0,0 +1,88 @@ +// Records that are arms of a union of records and also live in frame +// storage. An arm of such a union carries a hidden shape word before its +// fields, so every field address has to skip it: the reference fields a +// frame record owns are read, overwritten and released through the same +// slots as heap records of the shape. The same shapes also flow through +// the union, are compared by identity and are serialized. +interface Ident { + kind: "ident"; + name: string; + tags: string[]; +} +interface Call { + kind: "call"; + callee: string; + args: Ident[]; +} +type Node = Ident | Call; + +const words = ["alpha", "beta", "gamma", "delta", "epsilon"]; +function word(i: number): string { + return words[i % words.length]! + i; +} + +function describe(node: Node): string { + switch (node.kind) { + case "ident": + return `${node.name}[${node.tags.join(",")}]`; + case "call": + return `${node.callee}(${node.args.map((a) => a.name).join(", ")})`; + } +} +function weight(node: Node): number { + return node.kind === "ident" ? node.name.length + node.tags.length : node.args.length * 10; +} + +// Heap values of the union: the arms are tagged shapes. +const nodes: Node[] = []; +for (let i = 0; i < 6; i++) { + const id: Ident = { kind: "ident", name: word(i), tags: [word(i + 1)] }; + nodes.push(i % 2 === 0 ? id : { kind: "call", callee: word(i + 2), args: [id, id] }); +} +console.log(nodes.map(describe).join(" | ")); + +// Frame records of the arms that only flow into field reads. +let total = 0; +let log = ""; +for (let i = 0; i < 10; i++) { + const w = word(i); + const id: Ident = { kind: "ident", name: w + "!", tags: [w, word(i + 3)] }; + total += id.name.length + id.tags.length + id.tags[1]!.length; + const call: Call = { kind: "call", callee: w.toUpperCase(), args: [] }; + total += call.callee.length + call.args.length; + log += id.kind[0]! + call.kind[0]! + id.name[0]!; +} +console.log(total, log); + +// A reassigned frame binding: each new value is built apart and copied over +// the slot, and the replaced reference fields are released. +let cur: Ident = { kind: "ident", name: word(0), tags: [] }; +for (let i = 1; i < 8; i++) { + cur = { kind: "ident", name: cur.name.slice(0, 3) + word(i), tags: [...cur.tags, cur.name] }; + if (i % 3 === 0) cur = { kind: "ident", name: cur.tags[0] ?? "none", tags: cur.tags.slice(1) }; +} +console.log(cur.name, cur.tags.length, cur.tags.join("+")); + +// Field writes into a frame record's reference slots. +for (let i = 0; i < 4; i++) { + const id: Ident = { kind: "ident", name: word(i), tags: [word(i)] }; + id.name = id.name + "/" + i; + id.tags = [id.name, ...id.tags]; + id.tags.push(word(i + 7)); + console.log(id.name, id.tags.join(" ")); +} + +// Through the union: describe and weigh by union, keep identity, and +// serialize the arms and the union values. +const a: Ident = { kind: "ident", name: "x", tags: ["t1", "t2"] }; +const b: Call = { kind: "call", callee: "f", args: [a] }; +const asNode: Node = a; +const picked: Node = weight(b) > weight(a) ? b : a; +console.log(describe(asNode), describe(b), weight(asNode), weight(picked)); +console.log(asNode === a, picked === b, b.args[0] === a, (picked as Call).args[0] === asNode); +a.tags.push("t3"); +console.log(describe(asNode), describe(picked)); +console.log(JSON.stringify(a), JSON.stringify(picked)); +console.log(JSON.stringify(nodes.slice(0, 2))); +const mixed: Node[] = [a, b, nodes[1]!]; +console.log(mixed.filter((n) => n.kind === "ident").length, mixed.indexOf(asNode), mixed.indexOf(b)); From f4daefa6beaaf8866c65522835181fbab7b17028 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:10:18 -0700 Subject: [PATCH 041/120] Compile overloaded methods on interfaces and object types An interface or type-literal member with several call signatures (the `on("event", handler)` registry shape, arity overloads, type overloads sharing one implementation) failed its whole record shape: the type mapper maps only single-signature function types, so the member mapped to nothing and every type mentioning the interface failed with it. JavaScript stores one function under such a member; overloads only constrain call sites. The member now keeps a record slot holding that function. Its type joins the overloads per parameter position and result: equal types stay, scalar mixes form their union, and other mixes travel as checked `unknown` values when every joined type crosses that boundary both ways (differing callbacks become checked function values that any implementation's callback type adapts from). Positions some overloads omit are optional, and an overload's own type parameters erase to `unknown`. Rest parameters and a void result beside a value result keep the member fenced, now with a diagnostic naming the overload and parameter that block it. Calls convert their arguments into the slot and the result back to the resolved overload's type, on record and union receivers, through optional chains, and on call results. Object-literal methods find their member slot through the literal's contextual type, so an implementation with optional parameters fills an arity overload set (and a single-signature member's slot). Value reads of the member keep SC2007. Library, declaration-file and class overload declarations are unchanged. The zero-cost gate passes against the parent commit with all 2379 programs identical. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/call-signatures.ts | 18 + .../src/frontend/lowering/lower-calls.ts | 53 +- .../compiler/src/frontend/lowering/lowerer.ts | 11 + packages/compiler/src/frontend/type-mapper.ts | 479 ++++++++++++------ .../6180-interface-overload-events.ts.json | 6 + .../6181-interface-overload-arity.ts.json | 6 + .../6182-interface-overload-types.ts.json | 6 + .../6183-interface-overload-flow.ts.json | 6 + .../diagnostics/overloaded-members.ts.json | 6 + .../corpus/6180-interface-overload-events.ts | 95 ++++ tests/corpus/6181-interface-overload-arity.ts | 56 ++ tests/corpus/6182-interface-overload-types.ts | 61 +++ tests/corpus/6183-interface-overload-flow.ts | 89 ++++ tests/diagnostics/overloaded-members.ts | 25 + .../__snapshots__/overloaded-members.ts.txt | 35 ++ 15 files changed, 794 insertions(+), 158 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-interface-overload-events.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-interface-overload-arity.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-interface-overload-types.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-interface-overload-flow.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overloaded-members.ts.json create mode 100644 tests/corpus/6180-interface-overload-events.ts create mode 100644 tests/corpus/6181-interface-overload-arity.ts create mode 100644 tests/corpus/6182-interface-overload-types.ts create mode 100644 tests/corpus/6183-interface-overload-flow.ts create mode 100644 tests/diagnostics/overloaded-members.ts create mode 100644 tests/harness/__snapshots__/overloaded-members.ts.txt diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 8e30e8b82..c1cc72021 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -503,6 +503,24 @@ export function requireExactArityValue( ? lowerer.mapTypeOf(lowerer.typeOf(contextual)) : null; if (mapped && typeEquals(mapped, funcType)) return; + // An object-literal METHOD has no contextual type of its own: the + // literal's contextual type names the member slot it fills. An + // overloaded member's slot holds its erased function, which spells the + // completed parameters of an implementation like `m(a?: string)`. + if ( + contextual === null && + ts.isMethodDeclaration(blame) && + blame.parent !== undefined && + ts.isObjectLiteralExpression(blame.parent) && + (ts.isIdentifier(blame.name) || ts.isStringLiteral(blame.name)) + ) { + const literalType = lowerer.checker.getContextualType(blame.parent); + const member = literalType + ? lowerer.checker.getPropertyOfType(literalType, blame.name.text) + : undefined; + const slot = member ? lowerer.memberSlotTypeOf(lowerer.checker.getTypeOfSymbol(member)) : null; + if (slot && typeEquals(slot, funcType)) return; + } // A union-typed slot (`runner || defaultRunner` under a // `CommandRunner | undefined` context): the value can only inhabit // the union's one func arm — judge by it. diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2037ba17c..3589c960b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -61,7 +61,11 @@ import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; import { lowerJsonRequireCall } from "./json-require.js"; import { lowerAssertInstanceCall } from "./assert-instances.js"; -import { isGenericCallableMemberType, typeKey } from "../type-mapper.js"; +import { + isGenericCallableMemberType, + isOverloadedCallableMemberType, + typeKey, +} from "../type-mapper.js"; import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js"; import { NARROW_FIRST, @@ -8185,6 +8189,30 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE return { kind: "recordLit", fields, type: mapped, loc }; } +/** The accessed member is an overload set (isOverloadedCallableMemberType), + * also under an optional chain whose access still spells `| undefined`. */ +function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + return isOverloadedCallableMemberType( + lowerer.checker.getNonNullableType(lowerer.typeOf(access)), + lowerer.checker, + ); +} + +/** The result type of the overload a call through an overloaded member + * resolved to: the erased slot's result converts to it. A non-generic + * overload answers through its declaration, because the resolved signature + * of an optional-chain call (`api?.m(x)`) also carries the chain's + * undefined; a generic one answers through its instantiation. */ +function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType { + const { checker } = lowerer; + const signature = checker.getResolvedSignature(call); + const decl = signature ? checker.signatureDeclaration(signature) : undefined; + const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined; + const source = declared && declared.getTypeParameters().length === 0 ? declared : signature; + const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null; + return result ?? lowerer.irTypeOf(call); +} + export function lowerRecordFieldCall( lowerer: Lowerer, call: ts.CallExpression, @@ -8216,7 +8244,18 @@ export function lowerRecordFieldCall( const callee = lowerer.lowerUnionProperty(access); if (callee?.type.kind !== "func") return null; const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call)); - return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(call) }; + const value: IrExpr = { + kind: "callValue", + callee, + args, + type: callee.type.ret, + loc: locOf(call), + }; + // An overloaded member's erased result converts to the resolved + // overload's type, as on a single record receiver below. + return isOverloadedMemberAccess(lowerer, access) + ? lowerer.coerceInto(call, value, resolvedOverloadResultType(lowerer, call)) + : value; } const target = lowerer.fieldTarget(access); const loc = locOf(call); @@ -8291,10 +8330,14 @@ export function lowerRecordFieldCall( type: callee.type.ret, loc, }; - // A generic member's field holds its erased instance: the result - // converts to this call's instantiated return type at the call. - if (isGenericCallableMemberType(lowerer.typeOf(access), lowerer.checker)) + // A generic member's field holds its erased instance, and an overloaded + // member's field its erased function (the overloads joined): the result + // converts to this call's instantiated or resolved return type. + const memberType = lowerer.typeOf(access); + if (isGenericCallableMemberType(memberType, lowerer.checker)) result = lowerer.coerceInto(call, result, lowerer.irTypeOf(call)); + else if (isOverloadedMemberAccess(lowerer, access)) + result = lowerer.coerceInto(call, result, resolvedOverloadResultType(lowerer, call)); return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result; } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 94d526831..9b5b47a6b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -230,9 +230,11 @@ import { containsUnion, describeComponentBlocker, describeRecordMemberBlocker, + erasedOverloadMemberType, formatIrType, FETCH_AMBIENT_TYPES, isOsConstantsSymbol, + isOverloadedCallableMemberType, isUnitOnlyTsType, isWorkerHandleType, jsOpenObjectType, @@ -7679,6 +7681,15 @@ export class Lowerer { : mapped; } + /** The runtime type of a record member slot typed `t`: an overloaded + * member's erased function (erasedOverloadMemberType — the type a record + * shape stores for it), otherwise mapTypeOf. */ + memberSlotTypeOf(t: ts.Type): IrType | null { + if (isOverloadedCallableMemberType(t, this.checker)) + return erasedOverloadMemberType(t, this.typeCtx) ?? null; + return this.mapTypeOf(t); + } + /** The one position where a contextual UNION must not be adopted over the * expression's own: the LEFT operand of `&&`, `||`, or `??`. Adopting a * contextual union is normally safe because tsc proved the value diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f37703fd8..c972b324c 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -9,6 +9,7 @@ import { BOOL, bytesOf, canConvertToDyn, + canDynCheckTo, CHILD_T, CRYPTOHASH_T, CRYPTOHMAC_T, @@ -3892,158 +3893,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (hybrid) return hybrid; } const callSigs = checker.getCallSignatures(widened); - if (callSigs.length === 1) { - const sig = callSigs[0]!; - if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null; - // A SYNTHESIZED rest param (tsc's JS inference for a function body - // reading `arguments` — `(...args: any[]) => any` whose args symbol - // has no declaration): the dotDotDot check below can't see it, and a - // fixed-arity func mapping would LIE about the value's calling - // convention. Detected by the param-count mismatch against the - // signature's own declaration, excluding an erased `this` parameter. - { - const sigDecl = checker.signatureDeclaration(sig); - const declParams = - sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined; - if ( - declParams !== undefined && - runtimeParameterCount(declParams) !== sig.getParameters().length - ) { - return null; - } - // tsgo never SYNTHESIZES that rest param into the inferred signature - // (5.9.3 did — the count mismatch above was the whole detector - // there), so the declaration's own body answers directly. - if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) { - return null; - } - } - const params: IrType[] = []; - let typedRest = false; - for (const p of sig.getParameters()) { - const decl = checker.valueDeclarationOf(p); - const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined; - - // arity ABI selected by lambdaSignature. Treating an inferred any[] - // declaration as a typed packed-array closure would bypass its boxed - // spread/error semantics and change coverage classification. - if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null; - const optional = - decl !== undefined && - ts.isParameter(decl) && - (decl.questionToken !== undefined || decl.initializer !== undefined); - let pt = mapType(checker.getTypeOfSymbol(p), ctx); - if ( - decl && - ts.isParameter(decl) && - ts.isSetAccessorDeclaration(decl.parent) && - isJsSourceFile(decl.getSourceFile()) - ) - pt = DYN; - // An inferred JavaScript binding pattern consumes an iterable; it - // does not require the caller's array to have exactly this length. - const executor = decl && ts.isParameter(decl) ? decl.parent : null; - const construction = executor?.parent; - const constructorSymbol = - construction && ts.isNewExpression(construction) - ? checker.getSymbolAtLocation(construction.expression) - : undefined; - const nativeExecutor = - executor && - (ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) && - construction && - ts.isNewExpression(construction) && - construction.arguments?.[0] === executor && - ts.isIdentifier(construction.expression) && - construction.expression.text === "Promise" && - constructorSymbol && - checker - .declarationsOf(constructorSymbol) - .some((site) => ctx.isStdlibFile(site.getSourceFile())); - if ( - !nativeExecutor && - decl && - ts.isParameter(decl) && - !decl.dotDotDotToken && - !ts.isSetAccessorDeclaration(decl.parent) && - isJsSourceFile(decl.getSourceFile()) && - !decl.type && - !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), - ) - ) - pt = DYN; - if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); - // Belt and braces for non-strict type worlds: an optional param's - // ABI slot is always the undefined-armed union (strictNullChecks - // already spells it that way; arm it here if the world didn't). - if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") { - const armed = withUndefinedArm(pt, ctx.unions); - if (!armed) return null; - pt = armed; - } - if (!pt) return null; - if (rest) { - if (pt.kind !== "array") return null; - typedRest = true; - } - // `(value: void) => void` (Promise's resolve) is callable with - // no arguments — a void param is dropped, not a mapping failure. - if (pt.kind === "void") continue; - params.push(pt); - } - const retT = checker.getReturnTypeOfSignature(sig); - // `() => never` (a throw-only lambda's inferred type) is assignable to - // `() => void` and its calls never produce a value — map the return - // like declaredReturnType does for declarations. JS null/undefined - // returns can read mutable fields, so callable values use the same - // checked-native return ABI as their implementations. - const sigDecl = checker.signatureDeclaration(sig); - const jsUnitReturn = - sigDecl !== undefined && - isJsSourceFile(sigDecl.getSourceFile()) && - (retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; - let ret = - jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0 - ? DYN - : retT.flags & ts.TypeFlags.Never - ? VOID - : mapType(retT, ctx); - if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile())) - ret = DYN; - if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN; - if ( - sigDecl && - ts.isFunctionLike(sigDecl) && - isJsSourceFile(sigDecl.getSourceFile()) && - !sigDecl.type && - !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && - ret?.kind === "record" - ) - ret = DYN; - if ( - sigDecl && - ts.isFunctionLike(sigDecl) && - isJsSourceFile(sigDecl.getSourceFile()) && - !sigDecl.type && - !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && - (ret?.kind === "date" || - (ret?.kind === "union" && - ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func"))) - ) - ret = DYN; - if ( - ret && - sigDecl && - isJsSourceFile(sigDecl.getSourceFile()) && - functionCanReturnUndefined(sigDecl) - ) - ret = withUndefinedArm(ret, ctx.unions) ?? ret; - if (!ret) return null; - return typedRest - ? { kind: "func", params, ret, rest: true, restAbi: "typed" } - : funcOf(params, ret); - } + if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx); // Records: object types whose members are all data properties (shorthand // methods in type position count — they're func-typed fields) with // mappable types; no call/construct signatures, no index signatures, not @@ -4317,6 +4167,162 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return null; } +/** One non-overloaded call signature's func type (mapTypeInner's + * single-signature rule; erasedOverloadMemberType maps each overload of a + * member through it). */ +function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null { + const { checker } = ctx; + if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null; + // A SYNTHESIZED rest param (tsc's JS inference for a function body + // reading `arguments` — `(...args: any[]) => any` whose args symbol + // has no declaration): the dotDotDot check below can't see it, and a + // fixed-arity func mapping would LIE about the value's calling + // convention. Detected by the param-count mismatch against the + // signature's own declaration, excluding an erased `this` parameter. + { + const sigDecl = checker.signatureDeclaration(sig); + const declParams = + sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined; + if ( + declParams !== undefined && + runtimeParameterCount(declParams) !== sig.getParameters().length + ) { + return null; + } + // tsgo never SYNTHESIZES that rest param into the inferred signature + // (5.9.3 did — the count mismatch above was the whole detector + // there), so the declaration's own body answers directly. + if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) { + return null; + } + } + const params: IrType[] = []; + let typedRest = false; + for (const p of sig.getParameters()) { + const decl = checker.valueDeclarationOf(p); + const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined; + + // arity ABI selected by lambdaSignature. Treating an inferred any[] + // declaration as a typed packed-array closure would bypass its boxed + // spread/error semantics and change coverage classification. + if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null; + const optional = + decl !== undefined && + ts.isParameter(decl) && + (decl.questionToken !== undefined || decl.initializer !== undefined); + let pt = mapType(checker.getTypeOfSymbol(p), ctx); + if ( + decl && + ts.isParameter(decl) && + ts.isSetAccessorDeclaration(decl.parent) && + isJsSourceFile(decl.getSourceFile()) + ) + pt = DYN; + // An inferred JavaScript binding pattern consumes an iterable; it + // does not require the caller's array to have exactly this length. + const executor = decl && ts.isParameter(decl) ? decl.parent : null; + const construction = executor?.parent; + const constructorSymbol = + construction && ts.isNewExpression(construction) + ? checker.getSymbolAtLocation(construction.expression) + : undefined; + const nativeExecutor = + executor && + (ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) && + construction && + ts.isNewExpression(construction) && + construction.arguments?.[0] === executor && + ts.isIdentifier(construction.expression) && + construction.expression.text === "Promise" && + constructorSymbol && + checker + .declarationsOf(constructorSymbol) + .some((site) => ctx.isStdlibFile(site.getSourceFile())); + if ( + !nativeExecutor && + decl && + ts.isParameter(decl) && + !decl.dotDotDotToken && + !ts.isSetAccessorDeclaration(decl.parent) && + isJsSourceFile(decl.getSourceFile()) && + !decl.type && + !/@(?:param|type)\b/.test( + decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ) + ) + pt = DYN; + if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); + // Belt and braces for non-strict type worlds: an optional param's + // ABI slot is always the undefined-armed union (strictNullChecks + // already spells it that way; arm it here if the world didn't). + if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") { + const armed = withUndefinedArm(pt, ctx.unions); + if (!armed) return null; + pt = armed; + } + if (!pt) return null; + if (rest) { + if (pt.kind !== "array") return null; + typedRest = true; + } + // `(value: void) => void` (Promise's resolve) is callable with + // no arguments — a void param is dropped, not a mapping failure. + if (pt.kind === "void") continue; + params.push(pt); + } + const retT = checker.getReturnTypeOfSignature(sig); + // `() => never` (a throw-only lambda's inferred type) is assignable to + // `() => void` and its calls never produce a value — map the return + // like declaredReturnType does for declarations. JS null/undefined + // returns can read mutable fields, so callable values use the same + // checked-native return ABI as their implementations. + const sigDecl = checker.signatureDeclaration(sig); + const jsUnitReturn = + sigDecl !== undefined && + isJsSourceFile(sigDecl.getSourceFile()) && + (retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; + let ret = + jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0 + ? DYN + : retT.flags & ts.TypeFlags.Never + ? VOID + : mapType(retT, ctx); + if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile())) + ret = DYN; + if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN; + if ( + sigDecl && + ts.isFunctionLike(sigDecl) && + isJsSourceFile(sigDecl.getSourceFile()) && + !sigDecl.type && + !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && + ret?.kind === "record" + ) + ret = DYN; + if ( + sigDecl && + ts.isFunctionLike(sigDecl) && + isJsSourceFile(sigDecl.getSourceFile()) && + !sigDecl.type && + !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && + (ret?.kind === "date" || + (ret?.kind === "union" && + ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func"))) + ) + ret = DYN; + if ( + ret && + sigDecl && + isJsSourceFile(sigDecl.getSourceFile()) && + functionCanReturnUndefined(sigDecl) + ) + ret = withUndefinedArm(ret, ctx.unions) ?? ret; + if (!ret) return null; + return typedRest + ? { kind: "func", params, ret, rest: true, restAbi: "typed" } + : funcOf(params, ret); +} + /** Retain a literal discriminator before widening erases its values. A * structural match alone is insufficient: `{ kind: "loop", body: ... }` * also fits a smaller `{ kind: "empty" }` record. Choosing that layout @@ -5551,6 +5557,164 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType return erased?.kind === "func" ? erased : undefined; } +/** An OVERLOADED callable member type (`on(event: "a", h: (x: number) => + * void): () => void; on(event: "b", h: (x: string) => void): () => void` + * in a program interface or type literal): two or more call signatures and + * nothing else (no data properties, construct signatures, or index + * signatures), every one declared in TypeScript program source. Such a + * member keeps a record slot holding its erased runtime function + * (erasedOverloadMemberType); calls convert per their resolved overload + * (lowerRecordFieldCall). */ +export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeChecker): boolean { + const sigs = checker.getCallSignatures(t); + if (sigs.length < 2) return false; + if ( + checker.getConstructSignatures(t).length > 0 || + checker.getPropertiesOfType(t).length > 0 || + checker.getIndexInfosOfType(t).length > 0 + ) + return false; + return sigs.every((sig) => { + const decl = checker.signatureDeclaration(sig); + if (decl === undefined) return false; + if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl)) + return false; + const file = decl.getSourceFile(); + return !file.isDeclarationFile && !isJsSourceFile(file); + }); +} + +/** Scalar arms an overload join may combine into one union. */ +const OVERLOAD_JOIN_SCALARS = new Set(["f64", "string", "bool", "undefinedT", "nullT"]); + +/** The join of one position's types across an overload set: equal types + * stay; scalar mixes form their union; anything else is `unknown` when + * every joined type crosses the checked-dynamic boundary in both directions + * (the call site converts into the slot, the implementation converts out + * of it). Differing callbacks (`(x: number) => void` beside `(x: string) => + * void`) therefore travel as checked function values, which any + * implementation's own callback type adapts from. Null when no join + * exists. */ +function joinOverloadTypes(types: readonly IrType[], ctx: TypeMapperCtx): IrType | null { + const first = types[0]!; + if (types.every((t) => typeEquals(t, first))) return first; + const getRecord = (id: string) => ctx.shapes.get(id); + const getUnion = (id: string) => ctx.unions.get(id); + const arms = new Map(); + let scalar = true; + for (const t of types) { + const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? []) : [t]; + if (parts.length === 0) scalar = false; + for (const part of parts) { + if (!OVERLOAD_JOIN_SCALARS.has(part.kind)) scalar = false; + arms.set(typeKey(part), part); + } + } + if (scalar && [...arms.values()].some((arm) => !isUnitType(arm))) { + const sorted = [...arms.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: ctx.unions.intern(sorted) }; + } + if ( + types.every( + (t) => + t.kind === "dyn" || + (t.kind !== "void" && + canConvertToDyn(t, getRecord, getUnion) && + canDynCheckTo(t, getRecord, getUnion)), + ) + ) + return DYN; + return null; +} + +/** An overloaded member's runtime type (isOverloadedCallableMemberType). + * JavaScript stores ONE function under the member; overload signatures only + * constrain call sites. The slot's type joins the overloads per parameter + * position and result (joinOverloadTypes), and a position some overloads + * omit is optional. Each overload's own type parameters erase to `unknown` + * first. Calls pick their overload statically, convert arguments into the + * slot's parameter types and the result back to the overload's; an + * implementation converts into the slot like any function value. Undefined + * when the set does not join (rest parameters, a void result beside a + * value result, a component outside the checked-dynamic boundary): the + * member keeps its fence. */ +export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType | undefined { + const funcs: (IrType & { kind: "func" })[] = []; + for (const sig of ctx.checker.getCallSignatures(t)) { + const own = new Set(sig.getTypeParameters()); + const outer = ctx.resolveTypeParam; + const mapped = mapCallSignature( + sig, + own.size === 0 + ? ctx + : { + ...ctx, + erasingSignature: sig, + resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)), + }, + ); + if (mapped?.kind !== "func" || mapped.rest === true || mapped.argumentsAll === true) + return undefined; + funcs.push(mapped); + } + const arity = Math.max(...funcs.map((f) => f.params.length)); + const params: IrType[] = []; + for (let i = 0; i < arity; i++) { + const present = funcs.filter((f) => i < f.params.length).map((f) => f.params[i]!); + let param = joinOverloadTypes(present, ctx); + if (param !== null && present.length < funcs.length && param.kind !== "dyn") + param = withUndefinedArm(param, ctx.unions); + if (param === null) return undefined; + params.push(param); + } + const voids = funcs.filter((f) => f.ret.kind === "void").length; + if (voids !== 0 && voids !== funcs.length) return undefined; + const ret = + voids === funcs.length + ? VOID + : joinOverloadTypes( + funcs.map((f) => f.ret), + ctx, + ); + if (ret === null) return undefined; + return funcOf(params, ret); +} + +/** Why an overloaded member's set has no erased type + * (erasedOverloadMemberType): the first overload, and within it the first + * parameter or result, that does not map; otherwise the join itself. A + * message tail after "its member 'name'". */ +function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { + const { checker } = ctx; + for (const sig of checker.getCallSignatures(t)) { + const own = new Set(sig.getTypeParameters()); + const outer = ctx.resolveTypeParam; + const sigCtx: TypeMapperCtx = + own.size === 0 + ? ctx + : { + ...ctx, + erasingSignature: sig, + resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)), + }; + const mapped = mapCallSignature(sig, sigCtx); + if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue; + const decl = checker.signatureDeclaration(sig); + const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : ""; + const which = spelled ? `overload '${spelled}'` : "one overload"; + if (mapped?.kind === "func") + return `is an overload set, and its ${which} has a rest parameter (an overload set stores one fixed-arity function)`; + for (const param of sig.getParameters()) { + const paramTs = checker.getTypeOfSymbol(param); + if (!mapType(paramTs, sigCtx)) + return `is an overload set, and its ${which} has parameter '${param.name}' of type '${checker.typeToString(paramTs)}', which does not compile`; + } + const retTs = checker.getReturnTypeOfSignature(sig); + return `is an overload set, and its ${which} returns '${checker.typeToString(retTs)}', which does not compile`; + } + return "is an overload set whose signatures do not join into one stored function (a void result beside a value result, or positions whose differing types do not cross the checked-dynamic boundary)"; +} + /** The computed pieces of a record shape mapRecordTypeInner hands back for * the OUTER frame to intern — or, when a back-reference minted a recursive * placeholder for the type mid-construction, to finalize INTO that @@ -5932,9 +6096,14 @@ function mapRecordTypeInner( // can hold them — see isGenericCallableMemberType): the shape keeps // its data fields, and calls of the member monomorphize per call // site against the defining object literal's declaration. + // OVERLOADED members hold their erased runtime function (see + // erasedOverloadMemberType); a set that does not join fails the + // shape as before. let pt = isGenericCallableMemberType(fieldTs, checker) ? erasedGenericMemberType(fieldTs, ctx) - : mapType(fieldTs, ctx); + : isOverloadedCallableMemberType(fieldTs, checker) + ? (erasedOverloadMemberType(fieldTs, ctx) ?? null) + : mapType(fieldTs, ctx); if (pt === undefined) continue; // tsgo PANICS computing `readonly []` through the symbol-type query // (the TupleType conversion — the facade's panic fence answers @@ -6255,6 +6424,10 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx return null; } const fieldTs = checker.getTypeOfSymbol(p); + if (isOverloadedCallableMemberType(fieldTs, checker)) { + if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue; + return `the record shape is supported, but its member '${p.name}' ${describeOverloadSetBlocker(fieldTs, ctx)}`; + } let pt = isGenericCallableMemberType(fieldTs, checker) ? erasedGenericMemberType(fieldTs, ctx) : mapType(fieldTs, ctx); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-interface-overload-events.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-interface-overload-events.ts.json new file mode 100644 index 000000000..28b4381d2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-interface-overload-events.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6180-interface-overload-events.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-interface-overload-arity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-interface-overload-arity.ts.json new file mode 100644 index 000000000..919c56254 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-interface-overload-arity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6181-interface-overload-arity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-interface-overload-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-interface-overload-types.ts.json new file mode 100644 index 000000000..249d2084e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-interface-overload-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6182-interface-overload-types.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-interface-overload-flow.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-interface-overload-flow.ts.json new file mode 100644 index 000000000..b63d2780c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-interface-overload-flow.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6183-interface-overload-flow.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overloaded-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overloaded-members.ts.json new file mode 100644 index 000000000..33ea8a976 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overloaded-members.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/overloaded-members.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6180-interface-overload-events.ts b/tests/corpus/6180-interface-overload-events.ts new file mode 100644 index 000000000..d76a118bc --- /dev/null +++ b/tests/corpus/6180-interface-overload-events.ts @@ -0,0 +1,95 @@ +// An interface method with literal-discriminated overloads (the event +// registry shape: on("x", (e: X) => R)) holds ONE function at runtime, as +// JavaScript does. Each call site resolves its overload statically; handlers +// whose types differ per overload cross into the stored function as checked +// function values, and the implementation receives them in its own +// callback type. Events keep their identity across that boundary. +export {}; + +interface SaveEvent { + type: "save"; + path: string; + cancel: boolean; +} +interface LoadEvent { + type: "load"; + path: string; +} +interface Context { + cwd: string; +} +interface Api { + on(event: "save", handler: (e: SaveEvent, ctx: Context) => void): () => void; + on(event: "load", handler: (e: LoadEvent, ctx: Context) => boolean): () => void; + on(event: "tick", handler: (n: number) => void): () => void; + name: string; +} + +let nextId = 0; +const saves: { id: number; h: (e: SaveEvent, ctx: Context) => void }[] = []; +const loads: { id: number; h: (e: LoadEvent, ctx: Context) => boolean }[] = []; +const ticks: { id: number; h: (n: number) => void }[] = []; + +function createApi(name: string): Api { + return { + name, + on(event: "save" | "load" | "tick", handler: any): () => void { + const id = ++nextId; + const count = + event === "save" + ? saves.push({ id, h: handler }) + : event === "load" + ? loads.push({ id, h: handler }) + : ticks.push({ id, h: handler }); + console.log(`${name}: registered ${event} #${id} (${count})`); + return () => { + if (event === "save") saves.splice(saves.findIndex((x) => x.id === id), 1); + else if (event === "load") loads.splice(loads.findIndex((x) => x.id === id), 1); + else ticks.splice(ticks.findIndex((x) => x.id === id), 1); + console.log(`${name}: removed ${event} #${id}`); + }; + }, + }; +} + +// An extension sees only the interface. +function extension(pi: Api): () => void { + pi.on("save", (e, ctx) => { + e.cancel = e.path.endsWith(".lock"); + console.log(`save ${ctx.cwd}/${e.path}`); + }); + pi.on("load", (e) => e.path.length > 3); + return pi.on("tick", (n) => console.log("tick", n * 2, pi.name)); +} + +const api = createApi("main"); +const stopTicks = extension(api); +const ctx: Context = { cwd: "/work" }; +for (const path of ["a.txt", "b.lock"]) { + const ev: SaveEvent = { type: "save", path, cancel: false }; + for (const { h } of saves) h(ev, ctx); + console.log(path, "cancelled:", ev.cancel); +} +for (const { h } of loads) console.log(h({ type: "load", path: "ab" }, ctx), h({ type: "load", path: "abcd" }, ctx)); +for (const { h } of ticks) h(21); +stopTicks(); +console.log("ticks left", ticks.length); + +// A named handler value converts the same way as a lambda. +function onTick(n: number): void { + console.log("named tick", n); +} +const off = api.on("tick", onTick); +for (const { h } of ticks) h(7); +off(); + +// A handler that throws propagates through the boundary unchanged. +api.on("load", (e) => { + if (e.path === "") throw new Error("empty path"); + return true; +}); +try { + for (const { h } of loads) h({ type: "load", path: "" }, ctx); +} catch (err) { + console.log("caught", (err as Error).message); +} diff --git a/tests/corpus/6181-interface-overload-arity.ts b/tests/corpus/6181-interface-overload-arity.ts new file mode 100644 index 000000000..ae4dc5192 --- /dev/null +++ b/tests/corpus/6181-interface-overload-arity.ts @@ -0,0 +1,56 @@ +// Arity overloads on interface and type-literal members: positions some +// overloads omit are optional in the stored function, so one implementation +// with optional parameters serves every overload, and a call omitting a +// trailing argument passes undefined exactly as Node does. +export {}; + +interface Formatter { + label(): string; + label(prefix: string): string; + label(prefix: string, width: number): string; +} +const formatter: Formatter = { + label(prefix?: string, width?: number): string { + const text = prefix ?? "none"; + return width === undefined ? text : text.padEnd(width, ".") + "|"; + }, +}; +console.log(formatter.label()); +console.log(formatter.label("p")); +console.log(formatter.label("q", 6)); + +// Overloads spelled as call signatures of a type-literal member. +type Logger = { + log: { + (message: string): void; + (level: number, message: string): void; + }; + lines: string[]; +}; +const lines: string[] = []; +const logger: Logger = { + lines, + log: (a: string | number, b?: string) => { + lines.push(typeof a === "number" ? `[${a}] ${b}` : a); + }, +}; +logger.log("plain"); +logger.log(2, "leveled"); +console.log(logger.lines.join("; ")); + +// A single-signature method with an optional parameter fills its slot the +// same way. +interface Greeter { + greet(name?: string): string; +} +const greeter: Greeter = { + greet(name?: string): string { + return `hello ${name ?? "stranger"}`; + }, +}; +console.log(greeter.greet(), greeter.greet("ada")); + +// Arguments are evaluated once, left to right, before the call. +let n = 0; +const next = (): string => `#${++n}`; +console.log(formatter.label(next(), 4 + n), n); diff --git a/tests/corpus/6182-interface-overload-types.ts b/tests/corpus/6182-interface-overload-types.ts new file mode 100644 index 000000000..17ce95d17 --- /dev/null +++ b/tests/corpus/6182-interface-overload-types.ts @@ -0,0 +1,61 @@ +// Type overloads with one shared implementation: the stored function takes +// the join of the overload parameter types (a scalar union here) and each +// call converts its result back to the resolved overload's type. Records, +// generic overloads, and implementations typed with unions or `any` share +// the same slot. +export {}; + +interface Converter { + conv(x: number): string; + conv(x: string): number; + conv(x: boolean): boolean; +} +const converter: Converter = { + conv(x: number | string | boolean): any { + return typeof x === "number" ? `n${x}` : typeof x === "string" ? x.length : !x; + }, +}; +const s: string = converter.conv(12); +const len: number = converter.conv("abcd"); +const flipped: boolean = converter.conv(false); +console.log(s.toUpperCase(), len + 1, flipped); + +interface Point { + x: number; + y: number; +} +interface Named { + name: string; +} +interface Factory { + make(kind: "point", a: number, b: number): Point; + make(kind: "named", a: string): Named; +} +const factory: Factory = { + make(kind: string, a: number | string, b?: number): any { + return kind === "point" ? { x: a, y: b } : { name: a }; + }, +}; +const p = factory.make("point", 3, 4); +const named = factory.make("named", "zed"); +console.log(Math.hypot(p.x, p.y), named.name.length, p, named); + +// Generic overloads erase their own type parameters to `unknown`. +interface Store { + get(key: "names", fallback: string[]): string[]; + get(key: "count"): number; + at(items: T[], i: number): T; + at(items: string, i: number): string; +} +const data: Record = { names: ["a", "b"], count: 3 }; +const store: Store = { + get(key: string, fallback?: unknown): any { + return data[key] ?? fallback; + }, + at(items: any, i: number): any { + return items[i]; + }, +}; +console.log(store.get("names", []).join("|"), store.get("count") * 2); +console.log(store.at([10, 20, 30], 1) + 1, store.at("xyz", 2).toUpperCase()); + diff --git a/tests/corpus/6183-interface-overload-flow.ts b/tests/corpus/6183-interface-overload-flow.ts new file mode 100644 index 000000000..d2f3151dc --- /dev/null +++ b/tests/corpus/6183-interface-overload-flow.ts @@ -0,0 +1,89 @@ +// Values of an interface with an overloaded member flow like any record: +// through arrays, factory callbacks, and function results. A class that +// implements the interface dispatches to its own overloaded method. +export {}; + +interface Bus { + emit(kind: "num", value: number): void; + emit(kind: "text", value: string): void; + label: string; +} +type Plugin = (bus: Bus) => void; + +const log: string[] = []; +function makeBus(label: string): Bus { + return { + label, + emit(kind: "num" | "text", value: number | string): void { + log.push(`${label}:${kind}:${typeof value}:${value}`); + }, + }; +} +const plugins: Plugin[] = [ + (bus) => bus.emit("num", 1), + (bus) => { + bus.emit("text", "two"); + bus.emit("num", bus.label.length + 2); + }, +]; +const buses: Bus[] = [makeBus("x"), makeBus("yy")]; +for (const bus of buses) for (const plugin of plugins) plugin(bus); +console.log(log.join("\n")); +console.log(typeof buses[1]!.emit, JSON.stringify(buses[0])); + +interface Picker { + pick(kind: "s"): string; + pick(kind: "n"): number; +} +class Impl implements Picker { + private readonly base: number; + constructor(base: number) { + this.base = base; + } + pick(kind: "s"): string; + pick(kind: "n"): number; + pick(kind: "s" | "n"): string | number { + return kind === "s" ? `str${this.base}` : this.base * 7; + } +} +function use(p: Picker): string { + return `${p.pick("s").length}/${p.pick("n") + 1}`; +} +console.log(use(new Impl(1)), use(new Impl(10))); +const literal: Picker = { + pick(kind: "s" | "n"): any { + return kind === "s" ? "literal" : -1; + }, +}; +console.log(use(literal)); + +// Receivers of every shape resolve the overload the same way: a union of +// records sharing the member, an optional chain, and a call result. +interface Measure { + kind: "len"; + of(x: number): string; + of(x: string): number; +} +interface Sign { + kind: "sign"; + of(x: number): string; + of(x: string): number; +} +const measure: Measure = { + kind: "len", + of(x: number | string): any { + return typeof x === "number" ? `#${x}` : x.length; + }, +}; +const sign: Sign = { + kind: "sign", + of(x: number | string): any { + return typeof x === "number" ? (x < 0 ? "-" : "+") : -x.length; + }, +}; +for (const m of [measure, sign] as (Measure | Sign)[]) console.log(m.kind, m.of(-4).length, m.of("xyz") + 1); +const maybe: Measure | undefined = buses.length > 5 ? undefined : measure; +const none: Measure | undefined = buses.length > 5 ? measure : undefined; +console.log(maybe?.of(5), maybe?.of("abc"), none?.of(1), none?.of("q")); +const pickMeasure = (): Measure => measure; +console.log(pickMeasure().of(7).toUpperCase()); diff --git a/tests/diagnostics/overloaded-members.ts b/tests/diagnostics/overloaded-members.ts new file mode 100644 index 000000000..07ffa9b63 --- /dev/null +++ b/tests/diagnostics/overloaded-members.ts @@ -0,0 +1,25 @@ +// Overloaded interface members compile as one stored function whose type +// joins the overloads; calls resolve per site. Two fences remain: reading +// the member as a VALUE (its type is still the overload set), and a set +// whose overloads do not join (here, a rest parameter beside a fixed one). +interface Api { + on(event: "a", handler: (x: number) => void): void; + on(event: "b", handler: (x: string) => void): void; +} +const api: Api = { + on(event: string, handler: (x: any) => void): void { + handler(event === "a" ? 1 : "one"); + }, +}; +api.on("a", (x) => console.log(x + 1)); +const detached = api.on; +console.log(typeof detached); + +interface Emitter { + emit(event: "many", ...values: number[]): void; + emit(event: "one", value: string): void; +} +function fire(e: Emitter): void { + e.emit("many", 1, 2); +} +console.log(typeof fire); diff --git a/tests/harness/__snapshots__/overloaded-members.ts.txt b/tests/harness/__snapshots__/overloaded-members.ts.txt new file mode 100644 index 000000000..17e608785 --- /dev/null +++ b/tests/harness/__snapshots__/overloaded-members.ts.txt @@ -0,0 +1,35 @@ +overloaded-members.ts:15:7 - error SC2007: values of type '{ (event: "a", handler: (x: number) => void): void; (event: "b", handler: (x: string) => void): void; }' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature + + 14 | api.on("a", (x) => console.log(x + 1)); + 15 | const detached = api.on; + | ^~~~~~~~ + 16 | console.log(typeof detached); + + hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow + +overloaded-members.ts:16:20 - error SC2004: uses of 'detached' inherit the blocker on its declaration + + 15 | const detached = api.on; + 16 | console.log(typeof detached); + | ^~~~~~~~ + 17 | + + hint: the declaration of 'detached' did not compile — fix the diagnostic reported there and these sites clear with it + +overloaded-members.ts:22:15 - error SC2009: values of type 'Emitter' cannot be compiled: the record shape is supported, but its member 'emit' is an overload set, and its overload 'emit(event: "many", ...values: number[]): void' has a rest parameter (an overload set stores one fixed-arity function) + + 21 | } + 22 | function fire(e: Emitter): void { + | ^ + 23 | e.emit("many", 1, 2); + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +overloaded-members.ts:25:20 - error SC2004: uses of 'fire' inherit the blocker on its declaration + + 24 | } + 25 | console.log(typeof fire); + | ^~~~ + 26 | + + hint: the declaration of 'fire' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file From 5a574e86ce5196d26f9eaed772f26d7f4b971c79 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 16:46:10 -0500 Subject: [PATCH 042/120] Raise the static hello size ceiling to 96 KiB on Linux The tree-shaking test failed on Linux x64 after main was merged into this branch: "expected 84496 to be less than 81920". Measured with the release runtime pack (hello = console.log("hello", "world")): tree file R+E segment main b866ae323 80288 0x5fe9 989d0d6bc 79928 0x5fe9 merge 474012f04 84488 0x6579 No runtime family comes back: hello's absence checks still pass. The code that grows is the cycle collector and the release paths that feed it, which hello's exit reaches (scr_lib_init -> scr_lib_cleanup -> scr_arr_release -> scr_cyc_on_release -> scr_cyc_collect_due -> the pass). Main grew it by 1,447 bytes (the death rule and scr_mem_order_for_free), this branch by 1,386 (the inline nursery appends in the runtime's release functions, scr_cyc_route_bypassed and the window). Main left .text 23 bytes under the executable segment's 24 KiB page, so together they cross it, and every later segment and the file move by 4 KiB. 96 KiB, the Mach-O ceiling, restores several pages of slack. --- tests/harness/runtime-tree-shaking.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/harness/runtime-tree-shaking.test.ts b/tests/harness/runtime-tree-shaking.test.ts index b0d09545f..7d85a48c3 100644 --- a/tests/harness/runtime-tree-shaking.test.ts +++ b/tests/harness/runtime-tree-shaking.test.ts @@ -160,12 +160,14 @@ nativeToolchainTest( // Symbol absence is the primary reachability contract. Keep a deliberately // roomy, platform-specific hello-world ceiling too: it catches losing // section GC without pinning an exact linker/SDK byte count. The canonical - // Linux build is about 64-68KB and current Mach-O builds are about 70KB. - // ELF segments are page aligned, so the Linux size moves in 4 KiB steps - // whenever .text crosses a page boundary. These limits leave several - // native pages of linker-version slack while remaining far below the - // former roughly-400KB always-linked runtime. - const helloSizeLimit = process.platform === "linux" ? 80 * 1024 : 96 * 1024; + // Linux build is about 84KB and current Mach-O builds are about 70KB. Most + // of Linux hello's runtime code is the cycle collector, which its exit + // path reaches (scr_lib_cleanup releases an array). ELF segments are page + // aligned, so the Linux size moves in 4 KiB steps whenever .text crosses + // a page boundary. These limits leave several native pages of + // linker-version slack while remaining far below the former roughly-400KB + // always-linked runtime. + const helloSizeLimit = 96 * 1024; expect(statSync(hello.binaryPath).size).toBeLessThan(helloSizeLimit); }, ); From 8a204b8e0f06f3162b225552438c10aada14e26b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:57:19 -0700 Subject: [PATCH 043/120] Dispatch methods on class instances passed to untyped JS callbacks `list.map((rec) => rec.match(x))` over an array of class instances in a JavaScript file lowers the callback with a checked-dynamic parameter and checks the boxed closure into the native callback type. The instances cross into the dynamic tier only when the callback is called, inside that function boundary, so the native-capsule dispatch never learned their classes and `rec.match` threw "rec.match is not a function". One class or several, inherited methods or own, all failed (chardet's recogniser list, any `.map`/`.filter`/`.forEach` with an untyped arrow). A dynamic value checked into a function type now registers the classes its parameters carry, as a dynFrom of those values already does. Co-Authored-By: Claude Opus 5.5 --- .../lowering/class-dynamic-dispatch.ts | 6 +++ .../6188-js-callback-class-instances.js.json | 6 +++ .../6188-js-callback-class-instances.js | 39 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json create mode 100644 tests/corpus/6188-js-callback-class-instances.js diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 120032bf5..a0ed496e1 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -830,6 +830,12 @@ export class ClassDynamicDispatch { break; case "dynCheck": if (isDynTypedRefType(expr.type)) rewrite.add(fn); + // A dynamic function checked into a native function type + // receives its arguments as dynamic values: class instances + // passed to it cross the boundary when it is called (a JS + // `list.map((rec) => rec.match(x))` callback with an untyped + // parameter). + if (expr.type.kind === "func") expr.type.params.forEach(discover); break; case "libCall": if ( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json new file mode 100644 index 000000000..baf651d61 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6188-js-callback-class-instances.js" + ], + "diags": [] +} diff --git a/tests/corpus/6188-js-callback-class-instances.js b/tests/corpus/6188-js-callback-class-instances.js new file mode 100644 index 000000000..22df40f9b --- /dev/null +++ b/tests/corpus/6188-js-callback-class-instances.js @@ -0,0 +1,39 @@ +// Class instances passed to an untyped JavaScript callback (chardet's +// `recognisers.map((rec) => rec.match(context))`): the callback reads its +// parameter as a dynamic value, and method calls on it reach the +// instances' compiled methods, inherited ones included. + +class Recogniser { + name() { + return 'base'; + } + match(input) { + return input.length > 2 ? `${this.name()}:${input.length}` : null; + } +} +class ShiftJis extends Recogniser { + name() { + return 'Shift_JIS'; + } +} +class Big5 extends Recogniser { + name() { + return 'Big5'; + } + match(input) { + return `big5 ${super.match(input)}`; + } +} +class Utf8 { + match(input) { + return `utf8:${input}`; + } +} + +const one = [new ShiftJis(), new ShiftJis()]; +console.log(one.map((rec) => rec.match('abc')).join(' ')); + +const mixed = [new ShiftJis(), new Big5(), new Utf8()]; +console.log(JSON.stringify(mixed.map((rec) => rec.match('abcd')))); +console.log(mixed.filter((rec) => rec.match('ab') !== null).length); +mixed.forEach((rec, i) => console.log(i, rec.match('xyz'))); From 29909ed1aa6011ece16726cc26a23d62f98fd645 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:57:20 -0700 Subject: [PATCH 044/120] Give tsc's void-init export preamble no storage for class exports tsc opens every CommonJS file with `exports.A = exports.B = void 0;` and later aliases `exports.A = A;`. Collection registered an export global for the chain's first name at the preamble, while the preamble lowered to nothing and the class alias registered and stored nothing. Importers then read `lib.A` from a global that was never assigned, and `new lib.A()` crashed with a segfault (chardet's encoding modules). The preamble detection moves to program.ts so collection and lowering share it. A dropped preamble whose real assignment is a class alias now registers no storage, so importers resolve to the class. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-modules.ts | 6 ++ .../src/frontend/lowering/lower-stmts.ts | 43 +------------ packages/compiler/src/frontend/program.ts | 61 +++++++++++++++++++ .../main.js.json | 7 +++ .../lib.js | 25 ++++++++ .../main.js | 9 +++ .../package.json | 1 + 7 files changed, 111 insertions(+), 41 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json create mode 100644 tests/corpus/6189-cjs-void-preamble-class-exports/lib.js create mode 100644 tests/corpus/6189-cjs-void-preamble-class-exports/main.js create mode 100644 tests/corpus/6189-cjs-void-preamble-class-exports/package.json diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index df6677d5b..9f651925b 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -18,6 +18,7 @@ import { isRelativeSpecifier } from "../workspace-registry.js"; import { canonicalBuiltinModule, cjsExportAssignmentOf, + cjsVoidInitPreambleReals, cjsExportDiscardReason, entryPackageFilePredicate, isCjsJsFile, @@ -1398,6 +1399,11 @@ export function collectGlobals( // symbol (cjsMemberExportClassSymbol) and the class registry // applies unchanged. if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) continue; + // A dropped tsc void-init preamble whose real assignment is such a + // class alias registers nothing either: storage here would never + // be assigned, and importers would read it instead of the class. + const real = cjsVoidInitPreambleReals(cjs)?.get(cjs.name.text); + if (real && lowerer.cjsMemberExportClassSymbol(real.expr) !== null) continue; registerExport(cjs.name, cjs.name.text, cjs.value); continue; } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 65f39c6ab..40dc3ccae 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -60,6 +60,7 @@ import { import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; import { cjsExportAssignmentOf, + cjsVoidInitPreambleReals, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, @@ -6006,47 +6007,7 @@ function lowerCjsExportStatement( // the real assignment's position already orders. Preambles with no // later real assignment keep the ordinary path (the export IS // undefined then). - if (ts.isIdentifier(cjs.name)) { - const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression => - ts.isPropertyAccessExpression(e) && - ts.isIdentifier(e.name) && - ((ts.isIdentifier(e.expression) && e.expression.text === "exports") || - (ts.isPropertyAccessExpression(e.expression) && - ts.isIdentifier(e.expression.expression) && - e.expression.expression.text === "module" && - ts.isIdentifier(e.expression.name) && - e.expression.name.text === "exports")); - const isVoid = (e: ts.Expression): boolean => { - while (ts.isParenthesizedExpression(e)) e = e.expression; - return ( - e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined") - ); - }; - // chains: `exports.a = exports.b = void 0;` collects every name - const names: string[] = [cjs.name.text]; - let tail: ts.Expression = cjs.value; - while ( - ts.isBinaryExpression(tail) && - tail.operatorToken.kind === ts.SyntaxKind.EqualsToken && - isExportsMember(tail.left) - ) { - names.push((tail.left.name as ts.Identifier).text); - tail = tail.right; - } - if (isVoid(tail)) { - const sfHere = cjs.expr.getSourceFile(); - const myPos = cjs.expr.getStart(sfHere); - const laterRealOf = (name: string): boolean => - sfHere.statements.some((s) => { - const other = cjsExportAssignmentOf(s); - if (other?.kind !== "member" || !ts.isIdentifier(other.name)) return false; - if (other.name.text !== name) return false; - if (other.expr.getStart(sfHere) <= myPos) return false; - return !isVoid(other.value); - }); - if (names.every(laterRealOf)) return { kind: "block", body: [], loc }; - } - } + if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc }; const sf = cjs.expr.getSourceFile(); let root: IrGlobal | undefined; for (const stmt of sf.statements) { diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index df0cb673e..0deb5261d 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -5830,6 +5830,67 @@ export function cjsExportAssignmentOf( return null; } +/** The tsc VOID-INIT PREAMBLE (`exports.a = exports.b = void 0;` with the + * real `exports.a = a;` further down; every tsc-emitted dist file opens its + * exports this way): when every name in the chain has a later real + * assignment, the export takes its value there and the preamble lowers to + * nothing. Returns those later assignments by name, or null when this is + * not such a preamble (a preamble with no later real assignment keeps the + * ordinary path: the export IS undefined then). */ +export function cjsVoidInitPreambleReals( + cjs: Extract, { kind: "member" }>, +): Map, { kind: "member" }>> | null { + if (!ts.isIdentifier(cjs.name)) return null; + const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression => + ts.isPropertyAccessExpression(e) && + ts.isIdentifier(e.name) && + ((ts.isIdentifier(e.expression) && e.expression.text === "exports") || + (ts.isPropertyAccessExpression(e.expression) && + ts.isIdentifier(e.expression.expression) && + e.expression.expression.text === "module" && + ts.isIdentifier(e.expression.name) && + e.expression.name.text === "exports")); + const isVoid = (e: ts.Expression): boolean => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return ( + e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined") + ); + }; + // chains: `exports.a = exports.b = void 0;` collects every name + const names: string[] = [cjs.name.text]; + let tail: ts.Expression = cjs.value; + while ( + ts.isBinaryExpression(tail) && + tail.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isExportsMember(tail.left) + ) { + names.push((tail.left.name as ts.Identifier).text); + tail = tail.right; + } + if (!isVoid(tail)) return null; + const sf = cjs.expr.getSourceFile(); + const myPos = cjs.expr.getStart(sf); + const reals = new Map< + string, + Extract, { kind: "member" }> + >(); + for (const name of names) { + const real = sf.statements + .map((s) => cjsExportAssignmentOf(s)) + .find( + (other) => + other?.kind === "member" && + ts.isIdentifier(other.name) && + other.name.text === name && + other.expr.getStart(sf) > myPos && + !isVoid(other.value), + ); + if (real?.kind !== "member") return null; + reals.set(name, real); + } + return reals; +} + /** Why Node would DISCARD this CommonJS export statement, or null when it * survives (Node's export-object identity rules — checker-free, pure * AST): `module.exports = {...}` REPLACES the export object, so members diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json new file mode 100644 index 000000000..3e7347489 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js", + "/tests/corpus/6189-cjs-void-preamble-class-exports/main.js" + ], + "diags": [] +} diff --git a/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js b/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js new file mode 100644 index 000000000..dfbfcc30e --- /dev/null +++ b/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Big5 = exports.ShiftJis = exports.describe = void 0; +class Recogniser { + name() { + return 'base'; + } +} +class ShiftJis extends Recogniser { + name() { + return 'Shift_JIS'; + } +} +exports.ShiftJis = ShiftJis; +class Big5 extends Recogniser { + name() { + return 'Big5'; + } +} +exports.Big5 = Big5; +/** @param {Recogniser} rec */ +function describe(rec) { + return `recogniser ${rec.name()}`; +} +exports.describe = describe; diff --git a/tests/corpus/6189-cjs-void-preamble-class-exports/main.js b/tests/corpus/6189-cjs-void-preamble-class-exports/main.js new file mode 100644 index 000000000..385bc7a4f --- /dev/null +++ b/tests/corpus/6189-cjs-void-preamble-class-exports/main.js @@ -0,0 +1,9 @@ +// tsc's CommonJS export preamble (`exports.A = exports.B = void 0;`) ahead +// of class exports (chardet's encoding modules): the class read through +// the module object is the class itself, for every name in the chain. +"use strict"; +const lib = require("./lib.js"); + +const list = [new lib.ShiftJis(), new lib.Big5()]; +for (const rec of list) console.log(lib.describe(rec), rec instanceof lib.ShiftJis); +console.log(typeof lib.Big5, new lib.Big5().name()); diff --git a/tests/corpus/6189-cjs-void-preamble-class-exports/package.json b/tests/corpus/6189-cjs-void-preamble-class-exports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6189-cjs-void-preamble-class-exports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } From 0e9e908942bc0bf7cb6030e36d60ca43dc1c920d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:19:55 -0700 Subject: [PATCH 045/120] Short-circuit optional chain tails over unchecked reads `a[0]?.b.c` read `.c` from undefined when the element was missing: the tail only joined the guard when the checker typed the receiver nullable, and an array or index-signature read is typed present while it lowers to an undefined-armed value. The tail check now also accepts receivers that can lower to a hidden undefined; present receivers re-dispatch the plain lowering. A non-null assertion stays inside the chain (`a?.b!.c`), and parentheses end it for checked-dynamic reads (`(a?.b).c` throws). Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/optional-chains.ts | 29 ++++-- .../compiler/src/frontend/lowering/lowerer.ts | 63 +++++++----- ...optional-chain-unchecked-receivers.ts.json | 6 ++ ...6230-optional-chain-unchecked-receivers.ts | 98 +++++++++++++++++++ 4 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-optional-chain-unchecked-receivers.ts.json create mode 100644 tests/corpus/6230-optional-chain-unchecked-receivers.ts diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 7da847062..ceedc3e94 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -26,7 +26,8 @@ import type { Lowerer } from "../lowerer.js"; * deeper — `x?.trim().toLowerCase()` reads nothing past the guard when x * is nullish, so the whole tail must lower inside it. Walks * property/element accesses and call steps only (parens and `!` break - * the chain per the grammar: `(a?.b).c` throws on undefined in JS); + * the chain per the grammar: `(a?.b).c` throws on undefined in JS, while + * an unparenthesized `a?.b!.c` stays one chain, as TypeScript emits it); * the node's OWN token — and a call's immediate callee token — are the * plain entries' cases, not tails, and handled markers make the chain * lowering's re-dispatch walk past its own step. `f?.()` steps stay out @@ -41,6 +42,10 @@ function chainTailDot( // An active chain's bound receiver read is a LEAF — its own token // was consumed by the chain that bound it. if (lowerer.chainRecvByNode.has(cur)) return null; + if (ts.isNonNullExpression(cur)) { + cur = cur.expression; + continue; + } if (ts.isCallExpression(cur)) { // `f?.()` steps route through their own entry (or are being // handled); a deeper token under one is that chain's business. @@ -91,20 +96,29 @@ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): bool if (!tail) return false; const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression)); if (recvT?.kind === "dyn") return true; - if (recvT?.kind !== "union") return false; - const def = lowerer.unions.get(recvT.unionId); - return !!def && def.arms.some(isUnitType); + if (recvT?.kind === "union") { + const def = lowerer.unions.get(recvT.unionId); + if (def?.arms.some(isUnitType)) return true; + } + // The checker calls the receiver present, but an unchecked read (`a[0]` + // past the end, a missing index-signature key) can still lower to an + // undefined-armed value, and `a[0]?.b.c` must then short-circuit `.c` + // too. The chain lowering inspects the lowered receiver: a present value + // re-dispatches the plain lowering, so only genuinely optional receivers + // change shape. + return lowerer.isRuntimeOptionalOrigin(tail.expression); } /** True when an EARLIER step of this access chain carries `?.` — JS * short-circuits the whole tail (`a?.b.c` reads nothing when a is * nullish), so a dyn tail read must answer undefined instead of * throwing. Walks the receiver spine only; the argument of the access - * itself is not part of the guard. */ + * itself is not part of the guard. Parentheses end the chain + * (`(a?.b).c` reads `.c` from undefined); `!` does not. */ export function hasOptionalChainGuard(expr: ts.Expression): boolean { let cur: ts.Expression = expr; for (;;) { - if (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur)) { + if (ts.isNonNullExpression(cur)) { cur = cur.expression; continue; } @@ -393,7 +407,8 @@ export function lowerOptionalChain( for (let cur: ts.Expression = expr; cur !== dotNode;) { const next: ts.Expression = ts.isCallExpression(cur) ? cur.expression - : (cur as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression; + : (cur as ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.NonNullExpression) + .expression; if (next === dotNode) break; if (!lowerer.chainNarrowedType.has(next)) { lowerer.chainNarrowedType.set( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 2b86d6617..ee38fa2c1 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2075,6 +2075,42 @@ export class Lowerer { }; } + /** True when `node`'s lowered value can hold a hidden `undefined` the + * checker type does not admit: an unchecked element or member read, a call + * whose lowered return admits undefined, or a binding whose storage the + * optional-read analysis marked. Whether the value actually carries the + * undefined arm is only known once it is lowered + * (runtimeOptionalSourceValue); this is the syntactic precondition. */ + isRuntimeOptionalOrigin(node: ts.Expression): boolean { + let origin: ts.Expression = node; + while (ts.isParenthesizedExpression(origin)) origin = origin.expression; + if (ts.isElementAccessExpression(origin) || ts.isPropertyAccessExpression(origin)) return true; + if (ts.isCallExpression(origin)) { + const name = ts.isPropertyAccessExpression(origin.expression) + ? origin.expression.name + : origin.expression; + const symbol = ts.isIdentifier(name) ? this.resolveValueSymbol(name) : undefined; + const signature = symbol ? this.fnSigsBySymbol.get(symbol) : undefined; + return ( + this.runtimeOptionalFunctionReturns.has(origin) || + (signature?.returnType.kind === "union" && + this.armTag(signature.returnType.unionId, UNDEFINED_T) >= 0) + ); + } + if (ts.isIdentifier(origin)) { + const local = this.resolveLocal(origin); + const root = local ? this.runtimeOptionalRootOf(local) : null; + const global = this.globalOf(origin); + return ( + (!!root && this.runtimeOptionalStorageLocals.has(root)) || + (!!global && + (this.isRuntimeOptionalGlobal(global) || + (global.type.kind === "union" && this.armTag(global.type.unionId, UNDEFINED_T) >= 0))) + ); + } + return false; + } + /** Recover the original optional union when a checked single-arm helper * was installed before a strict property consumer could name its member. * Recovery covers member reads and storage roots marked by the @@ -2083,32 +2119,7 @@ export class Lowerer { runtimeOptionalSourceValue(node: ts.Expression, value: IrExpr): IrExpr | null { const slot = this.deferredFieldSlot(value); if (slot) return slot; - let origin: ts.Expression = node; - while (ts.isParenthesizedExpression(origin)) origin = origin.expression; - let optionalOrigin = - ts.isElementAccessExpression(origin) || ts.isPropertyAccessExpression(origin); - if (ts.isCallExpression(origin)) { - const name = ts.isPropertyAccessExpression(origin.expression) - ? origin.expression.name - : origin.expression; - const symbol = ts.isIdentifier(name) ? this.resolveValueSymbol(name) : undefined; - const signature = symbol ? this.fnSigsBySymbol.get(symbol) : undefined; - optionalOrigin = - this.runtimeOptionalFunctionReturns.has(origin) || - (signature?.returnType.kind === "union" && - this.armTag(signature.returnType.unionId, UNDEFINED_T) >= 0); - } - if (ts.isIdentifier(origin)) { - const local = this.resolveLocal(origin); - const root = local ? this.runtimeOptionalRootOf(local) : null; - const global = this.globalOf(origin); - optionalOrigin = - (!!root && this.runtimeOptionalStorageLocals.has(root)) || - (!!global && - (this.isRuntimeOptionalGlobal(global) || - (global.type.kind === "union" && this.armTag(global.type.unionId, UNDEFINED_T) >= 0))); - } - if (!optionalOrigin) return null; + if (!this.isRuntimeOptionalOrigin(node)) return null; // A checked JavaScript indexed read can already be boxed. The checker // calls the element present, but that inference is not a runtime guard. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-optional-chain-unchecked-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-optional-chain-unchecked-receivers.ts.json new file mode 100644 index 000000000..338ab8c6e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-optional-chain-unchecked-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6230-optional-chain-unchecked-receivers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6230-optional-chain-unchecked-receivers.ts b/tests/corpus/6230-optional-chain-unchecked-receivers.ts new file mode 100644 index 000000000..b80f6134a --- /dev/null +++ b/tests/corpus/6230-optional-chain-unchecked-receivers.ts @@ -0,0 +1,98 @@ +// An optional chain short-circuits EVERY later step when its guarded +// receiver is nullish, even when the checker calls that receiver present: +// an array read past the end or a missing index-signature key is undefined +// at runtime, so `choices[0]?.delta.content` must not read `.content` from +// undefined (the streaming SSE shape). Covers property, element, call, +// non-null-assertion, and nested tails, plus the forms that must still throw. +type Delta = { content?: string; parts: number[] }; +type Choice = { + delta: Delta; + pick(): { c: string }; + maybe?: () => { c: string }; +}; + +const none: Choice[] = []; +const some: Choice[] = [ + { delta: { content: "hi", parts: [7, 8] }, pick: () => ({ c: "picked" }) }, + { delta: { parts: [] }, pick: () => ({ c: "second" }), maybe: () => ({ c: "maybe" }) }, +]; + +// Property tails. +console.log("prop", none[0]?.delta.content, some[0]?.delta.content, some[1]?.delta.content); +console.log("prop ===", none[0]?.delta.content === undefined); +console.log("prop ??", none[0]?.delta.content ?? "none", some[0]?.delta.content ?? "none"); +console.log("prop len", none[0]?.delta.parts.length, some[0]?.delta.parts.length); + +// Element tails, with lazy index side effects on the short-circuit. +let evals = 0; +function at(i: number): number { + evals++; + return i; +} +console.log("elem", none[0]?.delta.parts[at(0)], "evals", evals); +console.log("elem", some[0]?.delta.parts[at(1)], "evals", evals); +console.log("elem ?.[]", none?.[0]?.delta.parts[0], some?.[0]?.delta.parts[0]); + +// Call tails: method call results and optional calls inside the chain. +console.log("call", none[0]?.pick().c, some[0]?.pick().c); +console.log("call ?.()", none[0]?.maybe?.().c, some[0]?.maybe?.().c, some[1]?.maybe?.().c); +let pushed = 0; +function sideEffect(): number { + pushed++; + return 1; +} +none[0]?.delta.parts.push(sideEffect()); +some[1]?.delta.parts.push(sideEffect()); +console.log("call args", pushed, some[1]!.delta.parts.length); + +// A non-null assertion stays inside the chain (`a?.b!.c` is one chain). +console.log("assert", none[0]?.delta!.content, some[0]?.delta!.content); +console.log("assert ===", none[0]?.delta!.parts.length === undefined); + +// Index-signature records: a missing key is undefined. +const byId: { [id: string]: Choice } = { a: some[0]! }; +console.log("index", byId["zz"]?.delta.content, byId["a"]?.delta.content); +console.log("index dot", byId.zz?.delta.parts.length, byId.a?.delta.parts.length); + +// Nested receivers and nested guards. +const grid: Choice[][] = [[], some]; +console.log("nested", grid[0]?.[0]?.delta.content, grid[1]?.[0]?.delta.content); +console.log("nested", grid[5]?.[0].delta.content, grid[1]?.[1]?.delta.content); + +// Optional members holding arrays. +const holder: { list?: Choice[] } = {}; +const full: { list?: Choice[] } = { list: some }; +console.log("member", holder.list?.[0].delta.content, full.list?.[0].delta.content); + +// Strings and conditions. +const words: string[] = []; +const padded = [" ab "]; +console.log("string", words[0]?.trim().length, padded[0]?.trim().length); +console.log("typeof", typeof none[0]?.delta.content, typeof some[0]?.delta.content); +if (none[0]?.delta.content) console.log("unreachable"); +else console.log("cond ok"); +console.log(`template ${none[0]?.delta.content} ${some[0]?.delta.content}`); + +// Typed JSON.parse results: an empty choices array short-circuits too. +type Chunk = { choices: { delta: Delta }[] }; +const streamChunk = JSON.parse('{"choices":[]}') as Chunk; +console.log("json", streamChunk.choices[0]?.delta.content ?? "(no delta)"); +const textChunk = JSON.parse('{"choices":[{"delta":{"content":"tok","parts":[]}}]}') as Chunk; +console.log("json", textChunk.choices[0]?.delta.content ?? "(no delta)"); + +// A PRESENT receiver followed by a missing step still throws: the guard +// covers only the receiver it tests. +function message(f: () => unknown): string { + try { + f(); + return "no throw"; + } catch (e) { + return (e as Error).message; + } +} +console.log("throws", message(() => some?.[5].delta)); +console.log("throws", message(() => none[0].delta)); +// Parentheses end the chain: the outer read sees undefined. +const loose = JSON.parse("{}"); +console.log("parens", message(() => (loose.missing?.deep).deeper)); +console.log("no parens", message(() => loose.missing?.deep.deeper)); From 77b137268d528b2c7d88b7038c9287b8e9959283 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:13 -0700 Subject: [PATCH 046/120] Visit each Set-Cookie value separately in Headers#forEach Fetch's "sort and combine" never combines Set-Cookie: Node's Headers iterator yields one entry per Set-Cookie value, in insertion order, while get() still joins them with ", ". The native forEach walked the sorted unique names and read the combined value, so repeated cookies reached the callback as one joined string. forEach now indexes the expanded list per step (sf_headers_sorted_entry), keeping the live re-sort so callback mutations shift later entries as in Node. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_fetch.c | 63 +++++++++++++++---- .../fetch-headers-set-cookie-iteration.ts | 63 +++++++++++++++++++ 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 tests/corpus/fetch-headers-set-cookie-iteration.ts diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index f586b96f8..dc53c7f22 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -2386,6 +2386,50 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) { return names; } +static bool sf_headers_is_set_cookie(const ScrStr *name) { + return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0; +} + +/* The `index`th entry of Fetch's "sort and combine" list: names in + * byte order, each with its combined value, except that every set-cookie + * value is its own entry (in insertion order), as Node's Headers + * iterator yields them. False past the end. Outputs are +1. */ +static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, + ScrStr **name_out, + ScrStr **value_out) { + size_t count = 0; + ScrStr **names = sf_headers_sorted_names(h, &count); + bool found = false; + size_t at = 0; + for (size_t i = 0; i < count && !found; i++) { + if (!sf_headers_is_set_cookie(names[i])) { + if (at == index) { + *name_out = scr_str_retain(names[i]); + *value_out = sf_headers_get_value(h, names[i]); + found = true; + } + at++; + continue; + } + size_t n = (size_t)scr_arr_len(h->pairs); + for (size_t j = 0; j + 1 < n && !found; j += 2) { + ScrStr *key = scr_arr_get_ref(h->pairs, (double)j); + bool match = sf_headers_is_set_cookie(key); + scr_str_release(key); + if (!match) continue; + if (at == index) { + *name_out = scr_str_retain(names[i]); + *value_out = scr_arr_get_ref(h->pairs, (double)(j + 1)); + found = true; + } + at++; + } + } + for (size_t j = 0; j < count; j++) scr_str_release(names[j]); + free(names); + return found; +} + static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, @@ -2438,20 +2482,15 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, * a future name skips it, inserting a later name visits it, and * inserting before the current index can make the current name appear * again. The old one-time snapshot was only safe while every native - * Headers handle was immutable. */ + * Headers handle was immutable. Set-Cookie values are visited one by + * one (sf_headers_sorted_entry). */ for (size_t i = 0;; i++) { - size_t count = 0; - ScrStr **names = sf_headers_sorted_names(h, &count); - if (i >= count) { - for (size_t j = 0; j < count; j++) scr_str_release(names[j]); - free(names); - break; - } - ScrStr *value = sf_headers_get_value(h, names[i]); + ScrStr *name = NULL; + ScrStr *value = NULL; + if (!sf_headers_sorted_entry(h, i, &name, &value)) break; ScrDyn *boxed_value = scr_dyn_new_str(value); - ScrDyn *boxed_name = scr_dyn_new_str(names[i]); - for (size_t j = 0; j < count; j++) scr_str_release(names[j]); - free(names); + ScrDyn *boxed_name = scr_dyn_new_str(name); + scr_str_release(name); ScrDyn *call_args[3] = {boxed_value, boxed_name, self}; scr_dyn_this_push_dyn(this_arg); ScrDyn *result = diff --git a/tests/corpus/fetch-headers-set-cookie-iteration.ts b/tests/corpus/fetch-headers-set-cookie-iteration.ts new file mode 100644 index 000000000..4d39bab70 --- /dev/null +++ b/tests/corpus/fetch-headers-set-cookie-iteration.ts @@ -0,0 +1,63 @@ +// Headers iteration follows Fetch's "sort and combine": names in byte +// order with their combined values, except that every Set-Cookie value is +// visited on its own. get() still joins them; getSetCookie() lists them. +const h = new Headers(); +h.append("set-cookie", "a=1; Path=/"); +h.append("x-b", "1"); +h.append("Set-Cookie", "b=2, c=3"); +h.append("x-a", "1"); +h.append("x-a", "2"); +h.append("set-cookie", "d=4"); +h.forEach((value, name) => console.log(`${name}: ${value}`)); +console.log(JSON.stringify(h.get("set-cookie"))); +console.log(JSON.stringify(h.getSetCookie())); + +// A thisArg and the Headers object reach the callback. +const self = { tag: "this" }; +h.forEach(function (this: typeof self, _value, name, headers) { + if (name === "x-b") console.log(this.tag, headers === h); +}, self); + +// Iteration is live and indexes the expanded list: deleting the cookies +// while visiting the first one shifts the next name into its slot. +const live = new Headers(); +live.append("set-cookie", "a=1"); +live.append("set-cookie", "b=2"); +live.append("x-z", "z"); +const seen: string[] = []; +live.forEach((value, name) => { + seen.push(`${name}=${value}`); + if (value === "a=1") live.delete("set-cookie"); +}); +console.log(seen.join(" | ")); + +// Appending a cookie during the walk is visited when it sorts later. +const grow = new Headers(); +grow.append("set-cookie", "a=1"); +grow.append("x-y", "y"); +const grown: string[] = []; +grow.forEach((value, name) => { + grown.push(`${name}=${value}`); + if (value === "a=1") grow.append("set-cookie", "b=2"); +}); +console.log(grown.join(" | ")); + +// Only set-cookie stays separate; cookie and other repeats combine. +const plain = new Headers([ + ["cookie", "a=1"], + ["cookie", "b=2"], + ["accept", "x"], + ["accept", "y"], +]); +plain.forEach((value, name) => console.log(`${name}: ${value}`)); +console.log(JSON.stringify(plain.getSetCookie())); + +// A response's headers walk the same way. +const response = new Response("ok", { + headers: [ + ["set-cookie", "s=1"], + ["set-cookie", "t=2"], + ["content-type", "text/plain"], + ], +}); +response.headers.forEach((value, name) => console.log(`${name}: ${value}`)); From 46d238fc0a70613d011ec5fb5eefd7b17fb92371 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:21 -0700 Subject: [PATCH 047/120] Send repeated Set-Cookie request headers as one combined line Headers stores each Set-Cookie value separately, but undici dispatches its header list's combined value: one line with the values joined by ", " at the position of the first one. fetch() wrote one line per value. The request serializer now joins later Set-Cookie values into the first entry and skips the rest, for every HeadersInit form. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_fetch.c | 27 +++++++++++- tests/corpus/fetch-request-set-cookie-wire.ts | 44 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/fetch-request-set-cookie-wire.ts diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index dc53c7f22..f5970ea0f 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4454,14 +4454,39 @@ static bool sf_start_hop(SfTransfer *t) { } if (connection_close) sf_push_header_text(headers, "connection", "close", 5); else sf_push_header_text(headers, "connection", "keep-alive", 10); + /* Headers keeps each Set-Cookie value separately, but undici sends its + * header list's combined value: one line, the values joined by ", ", + * at the position of the first one. */ + bool set_cookie_sent = false; for (size_t i = 0; i + 1 < user_len; i += 2) { ScrStr *name = scr_arr_get_ref(t->headers, (double)i); ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1)); - if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection")) { + if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection") || + (set_cookie_sent && sf_eq_ci(name, "set-cookie"))) { scr_str_release(name); scr_str_release(value); continue; } + if (sf_eq_ci(name, "set-cookie")) { + set_cookie_sent = true; + for (size_t j = i + 2; j + 1 < user_len; j += 2) { + ScrStr *later = scr_arr_get_ref(t->headers, (double)j); + bool match = sf_eq_ci(later, "set-cookie"); + scr_str_release(later); + if (!match) continue; + ScrStr *next = scr_arr_get_ref(t->headers, (double)(j + 1)); + size_t joined_len = value->len + 2 + next->len; + char *joined = malloc(joined_len ? joined_len : 1); + if (!joined) sf_oom(); + memcpy(joined, value->data, value->len); + memcpy(joined + value->len, ", ", 2); + memcpy(joined + value->len + 2, next->data, next->len); + scr_str_release(next); + scr_str_release(value); + value = scr_str_new(joined, joined_len); + free(joined); + } + } scr_arr_push_ref(headers, name); scr_arr_push_ref(headers, value); } diff --git a/tests/corpus/fetch-request-set-cookie-wire.ts b/tests/corpus/fetch-request-set-cookie-wire.ts new file mode 100644 index 000000000..54789a3fb --- /dev/null +++ b/tests/corpus/fetch-request-set-cookie-wire.ts @@ -0,0 +1,44 @@ +// fetch() sends its header list's combined values: repeated Set-Cookie +// request headers go out as one line, the values joined by ", ", at the +// position of the first one, whatever form the headers were given in. +import { createServer } from "node:http"; + +function shown(raw: string[]): string { + const lines: string[] = []; + for (let i = 0; i < raw.length; i += 2) { + const name = raw[i]!; + const lower = name.toLowerCase(); + if (lower === "host" || lower === "connection" || lower.startsWith("accept") || lower.startsWith("sec-") || lower === "user-agent") continue; + lines.push(`${name}: ${raw[i + 1]}`); + } + return lines.join(" | "); +} + +const server = createServer((req, res) => { + res.end(shown(req.rawHeaders)); +}); + +server.listen(0, "127.0.0.1", async () => { + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + const url = `http://127.0.0.1:${port}/`; + + const headers = new Headers(); + headers.append("set-cookie", "a=1"); + headers.append("x-first", "1"); + headers.append("set-cookie", "b=2"); + console.log(await (await fetch(url, { headers })).text()); + + const pairs: [string, string][] = [ + ["x-before", "0"], + ["set-cookie", "c=3"], + ["set-cookie", "d=4; Path=/"], + ]; + console.log(await (await fetch(url, { headers: pairs })).text()); + + console.log(await (await fetch(url, { headers: { "set-cookie": "e=5", "x-record": "r" } })).text()); + + const single = new Headers({ "set-cookie": "only=1" }); + console.log(await (await fetch(url, { headers: single })).text()); + server.close(); +}); From 2957a9c042f6f51e0f90910b068e50a51fd4d072 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:21 -0700 Subject: [PATCH 048/120] Report unparsable fetch and Request URLs as "Failed to parse URL from" undici's Request constructor wraps the URL parser's failure: TypeError "Failed to parse URL from " with the parser's TypeError as its non-enumerable cause. fetch() rejected, and new Request() threw, with the bare "Invalid URL" error instead. Both paths now replace the pending parser error with the wrapping TypeError (sf_throw_url_parse_failure). Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_fetch.c | 27 ++++++++++++++++ tests/corpus/fetch-invalid-url-cause.ts | 41 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/corpus/fetch-invalid-url-cause.ts diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index f5970ea0f..7aba5a4b2 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -312,6 +312,31 @@ static ScrDyn *sf_type_reason(const char *message, ScrError **error_out) { return d; } +/* Request's URL parse failure: with the URL parser's TypeError pending, + * replace it by undici's TypeError "Failed to parse URL from " + * whose (non-enumerable) cause is the parser error. */ +static void sf_throw_url_parse_failure(const ScrStr *input) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *cause = scr_caught_to_dyn(caught); + scr_caught_release(caught); + /* Sized without the NUL so it stays out of the unit's shared mergeable + * string section (which every program linking this unit carries). */ + static const char prefix[25] = "Failed to parse URL from "; + size_t message_len = sizeof prefix + input->len; + char *message = malloc(message_len); + if (!message) sf_oom(); + memcpy(message, prefix, sizeof prefix); + memcpy(message + sizeof prefix, input->data, input->len); + ScrStr *text = scr_str_new(message, message_len); + free(message); + ScrError *e = scr_error_new(SCR_ERR_TYPE, text); + scr_str_release(text); + scr_error_set_cause(e, cause); + e->cause_enumerable = false; + scr_dyn_release(cause); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + /* ── AbortSignal ─────────────────────────────────────────────────── */ static void sf_signal_track_reason(SfSignal *s) { @@ -4873,6 +4898,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init) { ScrStr *url_text = source ? scr_str_retain(source->url) : scr_dyn_string_coerce_js(input); if (!url_text) return NULL; ScrUrl *url = scr_url_new(url_text); + if (!url) sf_throw_url_parse_failure(url_text); scr_str_release(url_text); if (!url) return NULL; if (url->userinfo->len) { scr_url_release(url); sf_type_error("Request cannot be constructed from a URL that includes credentials"); return NULL; } @@ -5182,6 +5208,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) { ScrUrl *u = scr_url_new(url); if (!u) { + sf_throw_url_parse_failure(url); scr_dyn_release(coerced_body); scr_arr_release(headers); scr_str_release(method); diff --git a/tests/corpus/fetch-invalid-url-cause.ts b/tests/corpus/fetch-invalid-url-cause.ts new file mode 100644 index 000000000..5517c4ae2 --- /dev/null +++ b/tests/corpus/fetch-invalid-url-cause.ts @@ -0,0 +1,41 @@ +// An unparsable fetch() or Request URL is undici's TypeError +// "Failed to parse URL from ", whose non-enumerable cause is the +// URL parser's TypeError. +function report(label: string, e: unknown): void { + const serialized = JSON.stringify(e); + if (!(e instanceof Error)) { + console.log(label, "not an Error"); + return; + } + console.log(label, e.name, e.message, e instanceof TypeError); + console.log(" has cause:", "cause" in e, "own enumerable:", serialized); + console.log( + " cause:", + e.cause instanceof Error ? `${e.cause.name} ${e.cause.message} ${e.cause instanceof TypeError}` : String(e.cause), + ); +} + +async function main(): Promise { + for (const input of ["http://127.0.0.1:1http://x/", "/relative/path", "", "http://[::1"]) { + try { + await fetch(input); + console.log("resolved?"); + } catch (e) { + report(`fetch(${JSON.stringify(input)})`, e); + } + } + try { + new Request("not a url"); + console.log("constructed?"); + } catch (e) { + report("new Request", e); + } + // A valid URL with credentials keeps its own message. + try { + await fetch("http://user:pass@127.0.0.1:1/"); + } catch (e) { + report("credentials", e); + } +} + +void main(); From 43cefeb56b7c559507185239296f38fe006c7815 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:28 -0700 Subject: [PATCH 049/120] Retag flow-narrowed unions before primitive toString() `headers.host.toString()` after `!Array.isArray(headers.host)` narrows a `string | number | string[]` slot to `string | number`, and the union toString lowering accepted the narrowed checker type but emitted the toString node over the wider storage union. The validator rejected the non-stringable arm as an internal compiler error (SC9001). The lowering now retags the read to the proven union (the same checked coercion other narrowed reads use) before the per-union ToString helper. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-calls.ts | 9 ++++++- tests/corpus/union-narrowed-tostring.ts | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/union-narrowed-tostring.ts diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2037ba17c..cd96f2aae 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4210,8 +4210,15 @@ function lowerUnionToStringCall( (a.kind === "bytes" && a.elem === "u8"), ); if (!stringable) return null; - const operand = lowerer.lowerExpr(access.expression); + let operand = lowerer.lowerExpr(access.expression); if (operand.type.kind !== "union") return null; + // A flow-narrowed read keeps its wider storage union (`string | number` + // after `!Array.isArray(h.host)` over a `string | number | string[]` + // slot): retag it to the proven union before the per-union helper. + if (operand.type.unionId !== recvT.unionId) { + operand = lowerer.coerceInto(access.expression, operand, recvT); + if (operand.type.kind !== "union" || operand.type.unionId !== recvT.unionId) return null; + } return { kind: "toString", operand, type: STRING, loc: locOf(call) }; } diff --git a/tests/corpus/union-narrowed-tostring.ts b/tests/corpus/union-narrowed-tostring.ts new file mode 100644 index 000000000..93660b332 --- /dev/null +++ b/tests/corpus/union-narrowed-tostring.ts @@ -0,0 +1,27 @@ +// .toString() on a flow-narrowed union whose storage union is wider: the +// read retags to the proven arms, then formats like String(). +import type { OutgoingHttpHeaders } from "node:http"; + +function host(headers?: OutgoingHttpHeaders): string { + if (headers?.host && !Array.isArray(headers.host)) return headers.host.toString(); + return ""; +} +console.log(JSON.stringify([host({ host: "a:1" }), host({ host: 3 }), host({}), host()])); + +function label(v: string | number | string[] | undefined): string { + if (v === undefined) return "none"; + if (Array.isArray(v)) return `list(${v.length})`; + return v.toString(); +} +console.log(label("x"), label(1.5), label(-0), label(["a", "b"]), label(undefined)); + +function flag(v: boolean | number | null): string { + return v === null ? "null" : v.toString(); +} +console.log(flag(true), flag(false), flag(1e21), flag(null)); + +type Field = string | number | { nested: string }; +function field(f: Field): string { + return typeof f === "object" ? f.nested : f.toString(); +} +console.log(field("s"), field(42), field({ nested: "n" })); From def4626396c81e789fe903fc6b803c4cb830914a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:28 -0700 Subject: [PATCH 050/120] Lower optional calls of always-present library methods as plain calls `h.getSetCookie?.()` on a native Headers handle failed with SC2020: the optional-chain lowering first reads the callee as a value, and native handle methods have no first-class value. A required standard-library method on a never-nullish receiver always exists, so `?.()` is the plain call. When that method read has no lowering, the optional chain now re-dispatches the plain call. Reads that lower keep the existing path unchanged. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/optional-chains.ts | 47 ++++++++++++++++++- tests/corpus/optional-call-native-method.ts | 28 +++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/optional-call-native-method.ts diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 7da847062..51afc01ab 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -4,6 +4,8 @@ import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../.. import type { IrExpr, IrStmt, SrcLoc } from "../../../ir/ir.js"; import { isNodeEsmFile, locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; +import { tryLowerExpression } from "./try-lower-expression.js"; +import { constituentTypes } from "../../ts7/checker.js"; /** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on * the member access) and `f?.()` (the token on the call). The receiver @@ -121,6 +123,31 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean { } } +/** `recv.m` where `m` is a required method the standard library declares + * and `recv` itself is a non-optional, never-nullish typed receiver. */ +function alwaysPresentStdlibMethod(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isPropertyAccessExpression(node) || node.questionDotToken) return false; + if (hasOptionalChainGuard(node.expression)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(node.name); + if (!symbol || !(symbol.flags & ts.SymbolFlags.Method) || symbol.flags & ts.SymbolFlags.Optional) + return false; + if (!lowerer.isStdlibSymbol(symbol)) return false; + const nullable = + ts.TypeFlags.Any | + ts.TypeFlags.Unknown | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.Void; + const receiverType = lowerer.checker.getTypeAtLocation(node.expression); + const calleeType = lowerer.checker.getTypeAtLocation(node); + const parts = (type: ts.Type): readonly ts.Type[] => + type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type]; + return ( + parts(receiverType).every((part) => !(part.flags & nullable)) && + parts(calleeType).every((part) => !(part.flags & nullable)) + ); +} + export function lowerOptionalChain( lowerer: Lowerer, expr: ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression, @@ -167,7 +194,25 @@ export function lowerOptionalChain( dotNode = tail; recvNode = tail.expression; } - const loweredReceiver = lowerer.lowerExpr(recvNode); + // `h.getSetCookie?.()`: a required standard-library METHOD on a + // never-nullish receiver always exists, so `?.()` is the plain call. + // Native handle methods have no first-class value to guard; when the + // method read itself has no lowering, re-dispatch the plain call (which + // reports its own diagnostics) instead of failing on the value read. + let loweredReceiver: IrExpr | null; + if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) { + loweredReceiver = tryLowerExpression(lowerer, recvNode); + if (loweredReceiver === null) { + lowerer.chainHandled.add(dotNode); + try { + return lowerer.lowerExpr(expr); + } finally { + lowerer.chainHandled.delete(dotNode); + } + } + } else { + loweredReceiver = lowerer.lowerExpr(recvNode); + } const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { diff --git a/tests/corpus/optional-call-native-method.ts b/tests/corpus/optional-call-native-method.ts new file mode 100644 index 000000000..e3b9aa4a7 --- /dev/null +++ b/tests/corpus/optional-call-native-method.ts @@ -0,0 +1,28 @@ +// An optional call of a standard-library method that always exists on a +// never-nullish receiver is the plain call (`getSetCookie?.()` is always +// present on Node 24). +const h = new Headers(); +h.append("set-cookie", "a=1"); +h.append("set-cookie", "b=2"); +const cookies = h.getSetCookie?.() ?? []; +console.log(JSON.stringify(cookies)); +console.log(h.get?.("set-cookie")); +console.log(h.has?.("x-missing")); +h.set?.("x-set", "1"); +h.forEach?.((value, name) => console.log(`${name}=${value}`)); + +// The receiver evaluates once; arguments evaluate after it. +let reads = 0; +function headers(): Headers { + reads++; + return h; +} +function arg(): string { + console.log("argument after receiver", reads); + return "x-set"; +} +console.log(headers().get?.(arg()), reads); + +// Strings and arrays: the same rule. +const s = "Hello"; +console.log(s.toUpperCase?.(), [3, 1, 2].sort?.().join(",")); From 4d3dde55a72992470925d71fac1d17a21668b1e6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:17:48 -0700 Subject: [PATCH 051/120] Return undefined from JSON.stringify of unknown roots where it is observed JSON.stringify answers undefined for an undefined, function, or symbol root. Through `unknown` (or a generic instantiated with it) the dyn-root lowering converted the runtime result to a string, so the result was the text "undefined": `text === undefined` was false and `typeof text` was "string". Where the result reaches a use that distinguishes undefined from that text (undefined/null comparisons, typeof, ??, ?., truth tests, a return from a function declaring undefined, or an unannotated local binding used that way), the call now keeps the checked `string | undefined` result the replacer path already produces; later string uses of such a binding narrow by the checker's flow type. Other uses keep the existing string ABI, so programs that cannot observe the difference compile unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/json.ts | 109 +++++++++++++++++- .../json-stringify-unknown-undefined-root.ts | 38 ++++++ 2 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 tests/corpus/json-stringify-unknown-undefined-root.ts diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index a73c9d5e7..7ce902772 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -15,6 +15,7 @@ import { isUnitType, } from "../../../ir/ir.js"; import { optionalStringTags } from "./arguments.js"; +import { constituentTypes } from "../../ts7/checker.js"; /** JSON parse produces checked-dynamic data; a native reviver walks it * bottom-up before any checked cast. Stringify without a callback keeps @@ -177,9 +178,11 @@ export function lowerJsonMethodCall( type: DYN, loc, }; - // Preserve the existing dyn-root string ABI, including its textual - // "undefined" result, while letting the runtime honor toJSON hooks. - return value.type.kind === "dyn" + // A dyn root can serialize to undefined (undefined, a function, a + // symbol). Where the program can observe that (jsonResultObserved), + // keep the actual undefined; elsewhere the dyn-root string ABI + // stays, whose undefined result prints the same text. + return value.type.kind === "dyn" && !jsonResultObserved(lowerer, call) ? { kind: "toString", operand: raw, type: STRING, loc } : { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc }; } @@ -276,6 +279,106 @@ function lowerOptionalStringifyRoot( return { kind: "call", callee: helper, args: [value], type: resultT, loc }; } +/** True when a `JSON.stringify` result is used where JavaScript can tell + * undefined from the text "undefined": compared with undefined/null, + * `typeof`, `??`, `?.`, truth tests, a return from a function declaring + * an undefined result, or an unannotated local binding whose references + * do one of these. The lib types say `string`, so other uses (concatenation, + * printing, string parameters) cannot tell the difference. */ +function jsonResultObserved(lowerer: Lowerer, call: ts.CallExpression): boolean { + let node: ts.Node = call; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + if (observesUndefined(node as ts.Expression)) return true; + const parent = node.parent; + if (ts.isReturnStatement(parent)) { + let fn: ts.Node | undefined = parent.parent; + while (fn && !ts.isFunctionLike(fn)) fn = fn.parent; + const annotation = fn && ts.isFunctionLike(fn) ? fn.type : undefined; + if (!annotation) return false; + const type = lowerer.checker.getTypeAtLocation(annotation); + const parts = type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type]; + return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0); + } + // Module-scope bindings are globals whose slot type comes from the + // checker's `string`; only function- and block-scoped locals adopt the + // initializer's checked `string | undefined`. + if ( + !ts.isVariableDeclaration(parent) || + parent.initializer !== node || + parent.type || + !ts.isIdentifier(parent.name) || + (ts.isVariableDeclarationList(parent.parent) && + ts.isVariableStatement(parent.parent.parent) && + ts.isSourceFile(parent.parent.parent.parent)) + ) + return false; + const name = parent.name; + const symbol = lowerer.checker.getSymbolAtLocation(name); + if (!symbol) return false; + let scope: ts.Node = parent; + while (!ts.isBlock(scope.parent) && !ts.isSourceFile(scope.parent) && scope.parent) + scope = scope.parent; + let observed = false; + const visit = (current: ts.Node): void => { + if (observed) return; + if (ts.isIdentifier(current)) { + if ( + current !== name && + current.text === name.text && + lowerer.checker.getSymbolAtLocation(current) === symbol && + observesUndefined(current) + ) + observed = true; + return; + } + ts.forEachChild(current, visit); + }; + visit(scope.parent ?? scope); + return observed; +} + +function observesUndefined(expr: ts.Expression): boolean { + let node: ts.Node = expr; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isTypeOfExpression(parent)) return true; + if (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken) + return true; + if ( + (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && + parent.expression === node && + parent.questionDotToken + ) + return true; + if ( + (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) && + parent.expression === node + ) + return true; + if (ts.isConditionalExpression(parent) && parent.condition === node) return true; + if (ts.isBinaryExpression(parent)) { + const op = parent.operatorToken.kind; + if (op === ts.SyntaxKind.QuestionQuestionToken) return parent.left === node; + if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) + return parent.left === node; + if ( + op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.EqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken + ) { + let other: ts.Node = parent.left === node ? parent.right : parent.left; + while (ts.isParenthesizedExpression(other)) other = other.expression; + return ( + other.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(other) && other.text === "undefined") || + ts.isVoidExpression(other) + ); + } + } + return false; +} + /** Only explicit null/undefined select the no-callback path. */ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean { if (ts.isParenthesizedExpression(node)) return jsonNullishArgument(lowerer, node.expression); diff --git a/tests/corpus/json-stringify-unknown-undefined-root.ts b/tests/corpus/json-stringify-unknown-undefined-root.ts new file mode 100644 index 000000000..52528afbf --- /dev/null +++ b/tests/corpus/json-stringify-unknown-undefined-root.ts @@ -0,0 +1,38 @@ +// JSON.stringify of an unknown (or generic) root whose value is undefined, +// a function, or a symbol returns undefined, not text. Wherever the program +// can tell the two apart (undefined/null comparisons, typeof, ??, ?., truth +// tests, returns declaring undefined, and bindings used that way) the +// native result is the actual undefined. Nested values follow Node's +// member rules: dropped from objects, null in arrays. +const u: unknown = undefined; +const f: unknown = () => 1; +const s: unknown = Symbol("x"); +const ok: unknown = { a: [1, undefined] }; +for (const v of [u, f, s, ok]) { + const t = JSON.stringify(v); + console.log(t === undefined, typeof t, t ?? "fb", t?.length, t == null, t !== undefined); +} +for (const v of [u, ok]) { + const t = JSON.stringify(v); + if (t) console.log("truthy", t.length); + else console.log("falsy"); + console.log(!t, t ? "y" : "n", t || "or", t && "and"); +} +console.log(JSON.stringify(u) === undefined, typeof JSON.stringify(f), JSON.stringify(s) ?? "fb2", JSON.stringify(u)?.length); +console.log(JSON.stringify(u) || "or2", JSON.stringify(ok) || "or3", !JSON.stringify(u)); +function raw(v: T): string | undefined { + return JSON.stringify(v); +} +console.log(typeof raw(u), raw(ok)); +function ser(v: T): string { + const text = JSON.stringify(v); + if (text === undefined) return "undefined result"; + return `string ${text} ${text.length}`; +} +console.log(ser(u), ser({ a: 1 }), ser(1), ser("s"), ser(ok)); +// Unobserved uses keep the plain string path. +const plain = JSON.stringify(ok); +console.log(plain.length, plain, `${JSON.stringify(u)}`, "x" + JSON.stringify(u)); +// A module-scope binding keeps the string slot; direct uses still observe. +const top = JSON.stringify(ok); +console.log(top.length, JSON.stringify(u) === undefined); From 58d3981476bf1ced130095cb4372c32cd64894f8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:20:14 -0700 Subject: [PATCH 052/120] Accept initializer-less symbol-keyed fields assigned in the constructor A computed TypeScript field without an initializer (`[INTERNALS]: T`) was refused even with a stable Symbol()/Symbol.for() or literal key, which contradicts the documented rule that TypeScript fields need an initializer or an unconditional constructor assignment. Such a field now takes its declared type and the identifier fields' definite-assignment rule: an unconditional top-level `this[key] = ...` in the constructor, otherwise the checked deferred-initialization slot. Types that admit undefined (optional fields) start undefined. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 44 ++++++++++++++++--- ...class-symbol-field-constructor-assigned.ts | 43 ++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 tests/corpus/class-symbol-field-constructor-assigned.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index f3a4830c3..6a24b5417 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2644,9 +2644,15 @@ export function collectClassShapeInner( ? String(Number(member.name.text)) : member.name.text; const fieldName = symbolKey?.fieldName ?? stringKey; + // A TypeScript field without an initializer takes its declared + // type and the identifier fields' definite-assignment rule (an + // unconditional `this[key] = ...` at the constructor's top + // level), checked with the unguarded fields below. + const declaredOnly = + !member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type; if ( fieldName === null || - (!member.initializer && !isJsSourceFile(member.getSourceFile())) + (!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly) ) { lowerer.unsupported( "SC1090", @@ -2659,9 +2665,10 @@ export function collectClassShapeInner( member.type || !member.initializer ? declared : lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(member.initializer)); - const mapped = member.initializer - ? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred)) - : DYN; + const mapped = + member.initializer || declaredOnly + ? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred)) + : DYN; let type = mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type ? DYN @@ -2681,7 +2688,24 @@ export function collectClassShapeInner( if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN; if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN; } - const undefinedInitializer = !member.initializer ? locOf(member) : undefined; + const undefinedInitializer = + !member.initializer && (!declaredOnly || type.kind === "dyn") + ? locOf(member) + : undefined; + if ( + declaredOnly && + type.kind !== "dyn" && + !( + type.kind === "union" && + lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT") + ) + ) { + unguardedFields.push({ + node: member.name, + name: fieldName, + why: `initializer-less computed fields not assigned at the constructor's top level (nothing guarantees '${symbolKey ? symbolFieldDisplayName(fieldName) : fieldName}' is assigned before a read — assign it unconditionally at the top of the constructor, give it an initializer, or include undefined in its type)`, + }); + } const previous = fields.get(fieldName); if (previous) { if (symbolKey && !symbolFields.has(symbolKey.identity)) { @@ -3588,6 +3612,16 @@ export function collectClassShapeInner( stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword ) { topAssigned.add(stmt.expression.left.name.text); + } else if ( + ts.isExpressionStatement(stmt) && + ts.isBinaryExpression(stmt.expression) && + stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isElementAccessExpression(stmt.expression.left) && + stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword + ) { + const key = stmt.expression.left.argumentExpression; + const name = classSymbolKeyOf(lowerer, key)?.fieldName ?? lowerer.foldedStringKeyOf(key); + if (name !== null) topAssigned.add(name); } } for (const f of unguardedFields) { diff --git a/tests/corpus/class-symbol-field-constructor-assigned.ts b/tests/corpus/class-symbol-field-constructor-assigned.ts new file mode 100644 index 000000000..55e6e4e4a --- /dev/null +++ b/tests/corpus/class-symbol-field-constructor-assigned.ts @@ -0,0 +1,43 @@ +// Symbol-keyed (and literal-keyed) TypeScript fields without an +// initializer take their declared type when the constructor assigns them +// unconditionally; an optional one starts undefined. A field assigned +// later (here from a method the constructor calls) reads through the +// checked deferred-initialization slot. +const Internal = Symbol("internal"); +const Registered = Symbol.for("pkg.registered"); +const Pending = Symbol("pending"); + +class Box { + [Internal]: { n: number }; + [Registered]: string[]; + ["literal"]: number; + [Pending]?: string[]; + constructor(n: number) { + this[Internal] = { n }; + this[Registered] = ["a"]; + this["literal"] = n * 2; + } + get n(): number { + return this[Internal].n; + } +} + +const b = new Box(5); +console.log(b.n, b[Registered].length, b.literal, b[Pending] === undefined); +b[Internal].n = 7; +b[Registered].push("b"); +b[Pending] = ["p"]; +console.log(b.n, b[Registered].join(","), b[Pending]?.join(",")); +console.log(JSON.stringify(Object.entries({ literal: b.literal }))); + +class Late { + [Internal]!: { n: number }; + constructor(flag: boolean) { + if (flag) this[Internal] = { n: 1 }; + else this.init(); + } + init(): void { + this[Internal] = { n: 2 }; + } +} +console.log(new Late(true)[Internal].n, new Late(false)[Internal].n); From 1fd405c03373a0422f06abb3218d3c5f48b71f05 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:25:55 -0700 Subject: [PATCH 053/120] Add Date.prototype.toUTCString `new Date(ms).toUTCString()` was refused (SC2020). It now lowers to a runtime formatter over the stored time value with V8's output: "Www, DD Mmm YYYY HH:mm:ss GMT" in UTC, a leading "-" and at least four digits for negative years, and "Invalid Date" for an invalid time value. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/builtins/dates.ts | 5 +- .../src/frontend/lowering/surfaces.ts | 7 ++ .../compiler/src/ir/builtin-signatures.ts | 3 + packages/runtime/src/scr_lib.c | 66 +++++++++++++++++++ packages/runtime/src/scr_number.h | 3 + tests/corpus/date-to-utc-string.ts | 26 ++++++++ tests/harness/library-profile.test.ts | 1 + 8 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/date-to-utc-string.ts diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02bf37192..d5aebf0d8 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -314,6 +314,7 @@ export const LIB_FN_SYMS: Record = { "date.getUTCDate": "scr_date_get_date_utc", "date.getDay": "scr_date_get_day_local", "date.getUTCDay": "scr_date_get_day_utc", + "date.toUTCString": "scr_date_to_utc_string", "date.getHours": "scr_date_get_hours_local", "date.getUTCHours": "scr_date_get_hours_utc", "date.getMinutes": "scr_date_get_minutes_local", diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index fa880bdc9..0101a3ec3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -34,7 +34,7 @@ const DATE_GETTER_FNS: Readonly> = { }; const DATE_METHOD_HINT = - "getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering"; + "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering"; /** The Date slice: statics plus read-only Date values backed by one * TimeClip'd millisecond scalar. Construction lives in lowerNew; @@ -113,6 +113,9 @@ export function lowerDateCall( if (name === "toISOString") { return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc }; } + if (name === "toUTCString") { + return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc }; + } const fn = own(DATE_GETTER_FNS, name); if (fn !== undefined) { return { kind: "libCall", fn, args: [receiver], type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 73cc58c45..598fb6f9a 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2207,6 +2207,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["date.toISOString", "date.toISOStringValue"], note: "UTC ISO formatting over constructed and stored Date values", }, + { + id: "stdlib.date.toUTCString", + kind: "stdlib", + name: "Date.prototype.toUTCString", + fns: ["date.toUTCString"], + note: "RFC 7231 IMF-fixdate formatting over stored Date values", + }, { id: "stdlib.date.getFullYear", kind: "stdlib", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 08c493ae4..a3462c7d8 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4169,6 +4169,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "date.valueOf": { argTypes: [DATE_T], result: F64 }, "date.toISOString": { argTypes: [F64], result: STRING }, "date.toISOStringValue": { argTypes: [DATE_T], result: STRING }, + /** Date.prototype.toUTCString over a stored Date ("Invalid Date" for + * NaN; never throws). */ + "date.toUTCString": { argTypes: [DATE_T], result: STRING }, "date.getFullYear": { argTypes: [DATE_T], result: F64 }, "date.getUTCFullYear": { argTypes: [DATE_T], result: F64 }, "date.getMonth": { argTypes: [DATE_T], result: F64 }, diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index fd51e9c78..cd3f5d9a7 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10296,6 +10296,72 @@ ScrStr *scr_date_to_iso(double ms) { return scr_str_new(buf, (size_t)len); } +/* Node's Date.prototype.toUTCString over a stored (TimeClip'd) time + * value: "Www, DD Mmm YYYY HH:mm:ss GMT", a "-" and at least four digits + * for negative years, and "Invalid Date" for NaN. Never throws. +1. + * The text is assembled from function-local arrays rather than string + * literals: literals would land in this unit's shared mergeable string + * section and grow every executable, while these arrays are removed with + * the function by --gc-sections when a program never calls it. */ +ScrStr *scr_date_to_utc_string(double ms) { + /* Sized without the terminating NUL, so none is a C string the + * compiler could move into a mergeable string section. */ + static const char invalid[12] = "Invalid Date"; + static const char weekdays[21] = "SunMonTueWedThuFriSat"; + static const char months[36] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + if (!(fabs(ms) <= 8640000000000000.0)) return scr_str_new(invalid, sizeof invalid); + double t = trunc(ms); + double dayd = floor(t / 86400000.0); + long long msday = (long long)(t - dayd * 86400000.0); + long long days = (long long)dayd; + int weekday = (int)(((days + 4) % 7 + 7) % 7); + long long z = days + 719468; + long long era = (z >= 0 ? z : z - 146096) / 146097; + unsigned long long doe = (unsigned long long)(z - era * 146097); + unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + long long y = (long long)yoe + era * 400; + unsigned long long doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + unsigned long long mp = (5 * doy + 2) / 153; + unsigned long long d = doy - (153 * mp + 2) / 5 + 1; + unsigned long long m = mp < 10 ? mp + 3 : mp - 9; + if (m <= 2) y += 1; + int hh = (int)(msday / 3600000), mi = (int)(msday / 60000 % 60), + ss = (int)(msday / 1000 % 60); + char buf[40]; + size_t n = 0; + memcpy(buf, weekdays + 3 * weekday, 3); + n = 3; + buf[n++] = ','; + buf[n++] = ' '; + buf[n++] = (char)('0' + d / 10); + buf[n++] = (char)('0' + d % 10); + buf[n++] = ' '; + memcpy(buf + n, months + 3 * (m - 1), 3); + n += 3; + buf[n++] = ' '; + if (y < 0) buf[n++] = '-'; + unsigned long long year = (unsigned long long)(y < 0 ? -y : y); + char digits[20]; + size_t count = 0; + do { + digits[count++] = (char)('0' + year % 10); + year /= 10; + } while (year > 0); + while (count < 4) digits[count++] = '0'; + while (count > 0) buf[n++] = digits[--count]; + int parts[3] = {hh, mi, ss}; + for (int i = 0; i < 3; i++) { + buf[n++] = i == 0 ? ' ' : ':'; + buf[n++] = (char)('0' + parts[i] / 10); + buf[n++] = (char)('0' + parts[i] % 10); + } + buf[n++] = ' '; + buf[n++] = 'G'; + buf[n++] = 'M'; + buf[n++] = 'T'; + return scr_str_new(buf, n); +} + /* ── new Date(dateString).getTime() ────────────────────────────────────── * * The BOUNDED date-string parse (documented divergence — V8's parser diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 84db27075..029a75d05 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -101,6 +101,9 @@ double scr_date_get_time(double ms); * 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or * |ms| > 8.64e15 and returns NULL; +1 otherwise. */ ScrStr *scr_date_to_iso(double ms); +/* Node's Date.prototype.toUTCString ("Thu, 01 Jan 1970 00:00:00 GMT"; + * "Invalid Date" for NaN). Never throws; +1. */ +ScrStr *scr_date_to_utc_string(double ms); /* new Date(dateString).getTime(): the BOUNDED parse (the X509 validity * shape + ECMA's format with an explicit offset); NaN elsewhere. */ double scr_date_parse_get_time(ScrStr *s); diff --git a/tests/corpus/date-to-utc-string.ts b/tests/corpus/date-to-utc-string.ts new file mode 100644 index 000000000..633de5439 --- /dev/null +++ b/tests/corpus/date-to-utc-string.ts @@ -0,0 +1,26 @@ +// Date.prototype.toUTCString: "Www, DD Mmm YYYY HH:mm:ss GMT" in UTC, a +// leading "-" and at least four digits for negative years, "Invalid Date" +// for an invalid time value. +const times = [ + 0, + -1, + 1445412480000, + 951782400000, // 2000-02-29 + Date.UTC(1969, 11, 31, 23, 59, 59, 999), + Date.UTC(0, 0, 1) - 1900 * 365.2425 * 86400000, + Date.UTC(-1, 5, 15, 12, 30, 45), + Date.UTC(-271821, 3, 20), + 8.64e15, + -8.64e15, + 1234.9, + -1234.9, + NaN, + 8.64e15 + 1, +]; +for (const t of times) { + const d = new Date(t); + console.log(d.toUTCString()); +} +for (let day = 0; day < 7; day++) console.log(new Date(Date.UTC(2024, 0, day + 1)).toUTCString()); +for (let month = 0; month < 12; month++) console.log(new Date(Date.UTC(2023, month, 9, 8, 7, 6)).toUTCString()); +console.log(new Date("2015-10-21T07:28:00Z").toUTCString(), new Date("garbage").toUTCString()); diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index fc243ccb5..caa3b4e72 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -552,6 +552,7 @@ describe("library profile fences", () => { "stdlib.date.now", "stdlib.date.parse", "stdlib.date.toISOString", + "stdlib.date.toUTCString", "stdlib.date.valueOf", ]); expect(r.profile.fences[0]!.surfaces.every((s) => s.detector !== undefined)).toBe(true); From 6c1af69d038379f02ab4a33938729904003a7ee5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:25:55 -0700 Subject: [PATCH 054/120] Add static Response.json `Response.json(data, init)` was refused (SC2020). It now builds a native Response the way undici does: ResponseInit converts, the data serializes through the checked-dynamic JSON serializer (toJSON hooks included; TypeError "Value is not JSON serializable" when JSON.stringify answers undefined), then the response initializes like the constructor with "application/json" as the default content type, so status, statusText, header, and null-body-status errors keep their order. The fetch compatibility profile moves Response.json from unsupported to a static operation with the static-response-json differential fixture. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + packages/compiler/src/compat/fetch-profile.ts | 12 +++- .../src/frontend/lowering/lower-calls.ts | 3 + .../src/frontend/lowering/lower-stdlib.ts | 38 +++++++++++- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + packages/runtime/src/scr_fetch.c | 61 +++++++++++++++++-- packages/runtime/src/scr_generator.h | 1 + .../fetch/static-response-json/main.mts | 30 +++++++++ tests/harness/fetch.test.ts | 1 + 10 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/fetch/static-response-json/main.mts diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d5aebf0d8..82b339b05 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -810,6 +810,7 @@ export const LIB_FN_SYMS: Record = { // ambient-this read never throw. The fs dyn read is the sync-fs story. "fetch.start": "scr_fetch_static", "fetch.responseNew": "scr_fetch_response_new", + "fetch.responseJsonStatic": "scr_fetch_response_json_static", "fetch.input": "scr_fetch_input", "abort.controllerNew": "scr_fetch_abort_controller_new", "event.targetNew": "scr_event_target_new", diff --git a/packages/compiler/src/compat/fetch-profile.ts b/packages/compiler/src/compat/fetch-profile.ts index c1b43a810..c4d1e3f50 100644 --- a/packages/compiler/src/compat/fetch-profile.ts +++ b/packages/compiler/src/compat/fetch-profile.ts @@ -518,6 +518,15 @@ export const NODE24_FETCH_COMPAT_PROFILE = { evidence: [fixture("static")], }), ), + { + id: "stdlib.response.static.json", + name: "Response.json", + kind: "static-method", + facets: ["webidl-conversion", "error-shape"], + scope: + "data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor", + evidence: [fixture("static-response-json")], + }, ...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({ id: `stdlib.response.${member}`, name: `Response.${member}`, @@ -681,7 +690,8 @@ export const NODE24_FETCH_COMPAT_PROFILE = { ), staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"), - ...["error", "json", "redirect"].map((member) => + staticEntry("stdlib.response.static.json", "Response", "json", "static"), + ...["error", "redirect"].map((member) => unsupportedEntry( `stdlib.response.static.${member}`, "Response", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index cd96f2aae..91ef32181 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,6 +1,7 @@ import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; import { lowerHashStreamCall } from "./builtins/hash-stream.js"; +import { lowerResponseStaticJson } from "./lower-stdlib.js"; import { lowerArrayCallbackCall } from "./containers/array-callback-call.js"; /** Dispatch source calls, builtin members and receiver-specific operations. * Signature collection, argument completion and function bodies have their @@ -1389,6 +1390,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // compile-time-known, so a literal key folds to a constant. lowerClassHasOwnPropertyCall(lowerer, expr, expr.expression) ?? lowerObjectOwnMethodCall(lowerer, expr, expr.expression) ?? + // Static Response.json(data, init?) builds a native Response. + lowerResponseStaticJson(lowerer, expr) ?? // Refuse Response constructor operations before generic-call // fallbacks can report an unrelated SC1090 diagnostic. lowerer.fenceUnsupportedFetchConstructorMember(expr.expression) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 20caf184b..9fbe87c3d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -13,6 +13,7 @@ import { type IrExpr, type IrStmt, type IrType, + type SrcLoc, STRING, VOID, canConvertToDyn, @@ -1932,11 +1933,39 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null; const args = expr.arguments ?? []; if (args.some(ts.isSpreadElement)) return null; - const loc = locOf(expr); + return lowerResponseConstruction(lowerer, args, locOf(expr), false); +} +/** Static `Response.json(data, init?)`: the data crosses into the runtime's + * checked-dynamic JSON serializer (toJSON hooks included) after ResponseInit + * converts, then the response initializes as the constructor does. */ +export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; + const access = call.expression; + if (access.questionDotToken || access.name.text !== "json") return null; + if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null; + if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null; + return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true); +} + +function lowerResponseConstruction( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, + json: boolean, +): IrExpr { const toDyn = (node: ts.Expression | undefined, value: IrExpr, what: string): IrExpr => { if (node === undefined) return dynUndefinedExpr(loc); if (value.type.kind === "dyn") return value; + if (what === "JSON data") { + const boxed = lowerer.coerceToExpected(value, DYN); + if (boxed.type.kind === "dyn") return boxed; + lowerer.noLowering( + `Response.json of '${lowerer.fmt(value.type)}' values`, + node, + "the data must cross the checked-dynamic boundary (number, string, boolean, null, records, arrays, and 'unknown')", + ); + } /* A user class can satisfy ResponseInit structurally through plain * fields. Project just the declared dictionary members into a record, * then use the ordinary checked-dynamic copy. objRecordWidthHelper @@ -2059,8 +2088,11 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx } const result: IrExpr = { kind: "libCall", - fn: "fetch.responseNew", - args: [toDyn(bodyNode, bodyRef, "a body"), toDyn(initNode, initRef, "an init value")], + fn: json ? "fetch.responseJsonStatic" : "fetch.responseNew", + args: [ + toDyn(bodyNode, bodyRef, json ? "JSON data" : "a body"), + toDyn(initNode, initRef, "an init value"), + ], type: DYN, loc, }; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 1bb9cbd14..19082006b 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -119,6 +119,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fetch.requestNew", "fetch.headersNew", "fetch.responseNew", + "fetch.responseJsonStatic", "fetch.abortTimeout", "fetch.abortAny", "abort.timeout", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index a3462c7d8..952df7579 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -64,6 +64,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * are opaque checked-dynamic handles. */ "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "abort.controllerNew": { argTypes: [], result: DYN }, /** EventTarget / Event / CustomEvent construction (scr_event_target.c): diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 7aba5a4b2..a3013c01b 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4667,14 +4667,33 @@ static ScrDyn *sf_response_init_get( return NULL; } -ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { +/* The Response constructor, and Response.json(data, init) when json_data + * is non-NULL: undici converts ResponseInit, serializes the data (TypeError + * "Value is not JSON serializable" when JSON.stringify answers undefined), + * then initializes the response exactly as the constructor does, with + * "application/json" as the default content type. */ +typedef ScrDyn *(*SfJsonSerialize)(const ScrDyn *value); + +/* The JSON serializer reaches the constructor only through Response.json's + * entry point, so programs that never call it link no JSON walker. */ +static ScrDyn *sf_response_json_serialize(const ScrDyn *value) { + ScrStr *gap = scr_str_new("", 0); + ScrDyn *serialized = scr_json_stringify_replacer(value, scr_dyn_undefined(), gap); + scr_str_release(gap); + return serialized; +} + +static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, + const ScrDyn *json_data, + SfJsonSerialize serialize) { ScrArr *header_pairs = NULL; ScrArr *converted_headers = NULL; ScrBytes *body_bytes = NULL; ScrStr *status_text = NULL; SfStream *stream = NULL; - bool null_body = - !body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL; + bool json_body = json_data != NULL; + bool null_body = !json_body && + (!body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL); bool text_body = false; int status = 200; bool status_range_error = false; @@ -4683,7 +4702,7 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { * string arm therefore runs before ResponseInit's dictionary conversion, * while a ReadableStream is only brand-converted here — its locked/ * disturbed state belongs to the later body-extraction phase. */ - if (!null_body && + if (!null_body && !json_body && !(body->kind == SCR_DYN_HANDLE && body->v.handle.tag == SCR_DYNH_WEB_STREAM)) { if (body->kind == SCR_DYN_BYTES) { @@ -4757,7 +4776,25 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { /* Extract BodyInit before validating the response metadata. This makes a * locked/disturbed stream win over the later status range, statusText HTTP * reason-phrase, and HeadersInit validation exactly as in pinned Node. */ - if (null_body) { + if (json_body) { + ScrDyn *serialized = serialize(json_data); + if (!serialized) goto fail; + if (serialized->kind != SCR_DYN_STR) { + static const char unserializable[30] = "Value is not JSON serializable"; + scr_dyn_release(serialized); + scr_throw_error_msg(SCR_ERR_TYPE, unserializable, sizeof unserializable); + goto fail; + } + ScrStr *encoding = scr_str_new("utf8", 4); + body_bytes = scr_bytes_from_str(serialized->v.str, encoding); + scr_str_release(encoding); + scr_dyn_release(serialized); + stream = sf_stream_new_native(); + sf_stream_enqueue_bytes(stream, body_bytes); + sf_stream_close(stream); + scr_bytes_release(body_bytes); + body_bytes = NULL; + } else if (null_body) { stream = sf_stream_new_native(); sf_stream_close(stream); } else if (body->kind == SCR_DYN_HANDLE && @@ -4808,6 +4845,11 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { sf_push_header_text(header_pairs, "content-type", "text/plain;charset=UTF-8", 24); } + if (json_body && !sf_pairs_have(header_pairs, "content-type")) { + static const char json_type[16] = "application/json"; + scr_arr_push_ref(header_pairs, scr_str_new("content-type", 12)); + scr_arr_push_ref(header_pairs, scr_str_new(json_type, sizeof json_type)); + } SfResponse *response = calloc(1, sizeof *response); if (!response) sf_oom(); @@ -4833,6 +4875,15 @@ fail: return NULL; } +ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { + return sf_response_construct(body, init, NULL, NULL); +} + +ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init) { + return sf_response_construct(NULL, init, data ? data : scr_dyn_undefined(), + &sf_response_json_serialize); +} + /* Request uses the same body stream and Headers ownership as Response. */ typedef struct SfRequest { size_t rc; diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index 2c2c0e953..605cabec5 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -163,6 +163,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init); ScrDyn *scr_fetch_headers_new(ScrDyn *init); ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */ +ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init); /* Response.json; borrowed args; +1 Response handle or NULL pending */ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ diff --git a/tests/fixtures/fetch/static-response-json/main.mts b/tests/fixtures/fetch/static-response-json/main.mts new file mode 100644 index 000000000..3bbde4977 --- /dev/null +++ b/tests/fixtures/fetch/static-response-json/main.mts @@ -0,0 +1,30 @@ +// Response.json(data, init): ResponseInit converts, the data serializes with +// JSON.stringify (TypeError when it answers undefined), then the response +// initializes like the constructor, defaulting content-type to +// application/json. +function show(label: string, make: () => Response): void { + try { + const r = make(); + console.log(label, r.status, JSON.stringify(r.statusText), r.headers.get("content-type"), r.headers.get("x-a")); + } catch (e) { + console.log(label, e instanceof Error ? `${e.name}: ${e.message}` : String(e)); + } +} +show("plain", () => Response.json({ ok: true })); +show("init", () => Response.json([1, 2], { status: 201, statusText: "Made", headers: { "x-a": "1" } })); +show("content-type", () => Response.json("s", { headers: { "content-type": "text/x" } })); +show("headers object", () => Response.json(null, { headers: new Headers([["x-a", "h"]]) })); +show("undefined", () => Response.json(undefined)); +const fn: unknown = () => 1; +show("function", () => Response.json(fn)); +show("range", () => Response.json({}, { status: 99 })); +show("undefined before range", () => Response.json(undefined, { status: 99 })); +show("null body status", () => Response.json({}, { status: 204 })); +show("bad header", () => Response.json({}, { headers: { "bad name": "x" } })); +show("undefined before bad header", () => Response.json(undefined, { headers: { "bad name": "x" } })); +show("bad statusText", () => Response.json({}, { statusText: "a\nb" })); +const data: unknown = JSON.parse('{"a":[1,"x",null],"n":{"deep":true}}'); +const r = Response.json({ data, d: new Date(0), skip: undefined }); +console.log(r.bodyUsed, await r.text(), r.bodyUsed); +const parsed: unknown = await Response.json({ round: [1, 2] }).json(); +console.log(JSON.stringify(parsed)); diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index 6b04acf59..337eafec6 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -163,6 +163,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { "static-listener-noncallable", "static-abort-throw", "static-dispatch-throw", + "static-response-json", ] as const; test.for( staticCases.flatMap((name) => (["llvm"] as const).map((backend) => [name, backend] as const)), From 30e23af63ff1b03bcfc50bca4ce4c942fa9433da Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:27:35 -0700 Subject: [PATCH 055/120] Store fetch Headers, Request, and Response handles in class fields A class field typed `Headers`, `Request`, or `Response` (or an array or nullable form of them) maps to checked-dynamic storage, and the class-field rule refused it (SC1090) although `unknown`, Map values, and closures already store the same native handles. Such fields now take the checked-dynamic slot, like the worker handles; reads keep the handle's identity and the static fetch member lowering. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 7 ++-- packages/compiler/src/frontend/type-mapper.ts | 34 ++++++++++++++++++ tests/corpus/class-fetch-handle-fields.ts | 36 +++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tests/corpus/class-fetch-handle-fields.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6a24b5417..a10874928 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2680,7 +2680,7 @@ export function collectClassShapeInner( type.kind === "void" || (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile()) && - !lowerer.classImplementedProtocol(inferred)) + !lowerer.classFieldStoresDyn(inferred)) ) lowerer.badType(member, inferred); if (isJsSourceFile(member.getSourceFile())) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 145fb3ae6..f933b8f5c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -235,6 +235,7 @@ import { isOsConstantsSymbol, isUnitOnlyTsType, isWorkerHandleType, + isFetchHandleType, jsOpenObjectType, mapType, ShapeRegistry, @@ -3004,12 +3005,14 @@ export class Lowerer { /** Declared class fields may store a DYN value only when its static type * is `unknown` or `object` (also with `null` or `undefined`, or an array * of those), names checked protocol identity, or is an owned runtime - * handle (a Worker or MessagePort); `any` fields stay refused. */ + * handle (a Worker or MessagePort, or a fetch Headers/Request/Response); + * `any` fields stay refused. */ classFieldStoresDyn(type: ts.Type): boolean { return ( this.opaqueValueType(type) || this.classImplementedProtocol(type) || - isWorkerHandleType(type, this.typeCtx) + isWorkerHandleType(type, this.typeCtx) || + isFetchHandleType(type, this.typeCtx) ); } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f37703fd8..8089304f0 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1313,6 +1313,40 @@ export function isWorkerHandleType( return handle; } +/** The native fetch handles (Headers, Request, Response), optionally in + * arrays or with null/undefined: checked-dynamic handles wherever they are + * stored, like the worker handles above. */ +export function isFetchHandleType( + type: ts.Type, + ctx: Pick, +): boolean { + const members = type.isUnionType() ? ts.constituentTypes(type) : [type]; + let handle = false; + for (const member of members) { + if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue; + if (ctx.checker.isArrayType(member)) { + const element = ctx.checker.getTypeArguments(member as ts.TypeReference)[0]; + if (element === undefined || !isFetchHandleType(element, ctx)) return false; + } else { + const symbol = member.getSymbol(); + if ( + !symbol || + (symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") || + !ctx.checker + .declarationsOf(symbol) + .some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) + ) + return false; + } + handle = true; + } + return handle; +} + export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null; // These intrinsic domains cannot depend on generic bindings, declaration diff --git a/tests/corpus/class-fetch-handle-fields.ts b/tests/corpus/class-fetch-handle-fields.ts new file mode 100644 index 000000000..b06df1313 --- /dev/null +++ b/tests/corpus/class-fetch-handle-fields.ts @@ -0,0 +1,36 @@ +// Class fields typed as native fetch handles (Headers, Request, Response), +// directly, optional, nullable, in arrays, and inside record fields, hold +// the live handle: mutation through one reference is seen through another. +class Holder { + headers: Headers; + list: Headers[] = []; + maybe?: Response; + req: Request | null = null; + bag: { headers: Headers; status: number }; + constructor(headers: Headers) { + this.headers = headers; + this.bag = { headers, status: 1 }; + } +} +const holder = new Holder(new Headers({ a: "1" })); +console.log(holder.headers.get("a"), holder.list.length, holder.maybe === undefined, holder.req); +holder.list.push(new Headers({ b: "2" })); +holder.maybe = new Response("x", { status: 201 }); +holder.req = new Request("http://localhost/p", { method: "POST" }); +console.log(holder.list[0]!.get("b"), holder.maybe.status, holder.req.method, holder.req.url); +holder.headers.set("c", "3"); +console.log(holder.bag.headers.get("c"), holder.bag.headers === holder.headers); + +// Symbol-keyed fields hold the handles the same way. +const NATIVE = Symbol("native"); +class Wrapper { + [NATIVE]: Response; + constructor(body: string) { + this[NATIVE] = new Response(body, { status: 202 }); + } + get status(): number { + return this[NATIVE].status; + } +} +const wrapped = new Wrapper("ok"); +console.log(wrapped.status, wrapped[NATIVE].headers.get("content-type")); From e6dbad9567c88078c08b25b1ff9dd667866a5f76 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:27:35 -0700 Subject: [PATCH 056/120] Destructure Maps and Sets with array patterns `const [[k, v], ...rest] = map` was refused (SC1031). The source now snapshots once into its iteration order (Map entries as pairs, Set values), the spread's own machinery, and the pattern lowers as an array pattern over that snapshot, as string sources already do. Element defaults keep a fence because they run between iterator steps. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stmts.ts | 34 +++++++++++++++++++ tests/corpus/destructure-map-set-iterables.ts | 18 ++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/corpus/destructure-map-set-iterables.ts diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 12544979c..d6247a9ef 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; +import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; import { importMetaBindingSource } from "./import-meta.js"; @@ -2380,6 +2381,39 @@ export function lowerBindingPattern( }); return; } + // Map and Set sources (`const [[k, v], ...rest] = map`): the iterator + // yields the collection's entries (Map pairs) or values in order, so + // the source snapshots ONCE into that array — the spread's own + // machinery — and the pattern lowers as an array pattern over it, + // like the string case above. Element defaults keep a fence: they + // run between iterator steps and could mutate the live collection. + if (srcType.kind === "map" || srcType.kind === "set") { + const defaulted = pattern.elements.find( + (el): el is ts.BindingElement => + ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken, + ); + if (defaulted) + lowerer.unsupported("SC1031", defaulted, "binding defaults over Map/Set sources"); + const snapshot = lowerCollectionSpread( + lowerer, + srcRef(), + pattern as unknown as ts.Expression, + ); + if (snapshot && snapshot.type.kind === "array") { + const loc = locOf(pattern); + const snapshotT = snapshot.type; + const entries = lowerer.declareHiddenLocal("%dentries", snapshotT); + out.push({ kind: "varDecl", localId: entries.id, init: snapshot, loc }); + lowerer.lowerBindingPattern( + pattern, + () => ({ kind: "varRef", localId: entries.id, type: snapshotT, loc }), + snapshotT, + isLet, + out, + ); + return; + } + } if (srcType.kind !== "array") { lowerer.unsupported( "SC1031", diff --git a/tests/corpus/destructure-map-set-iterables.ts b/tests/corpus/destructure-map-set-iterables.ts new file mode 100644 index 000000000..295a224d8 --- /dev/null +++ b/tests/corpus/destructure-map-set-iterables.ts @@ -0,0 +1,18 @@ +// Array destructuring of a Map or Set walks its iterator: Map entries as +// [key, value] pairs, Set values, holes and rest included. +const m = new Map([["a", "1"], ["b", "2"], ["c", "3"]]); +const [[name, value], ...rest] = m; +console.log(name, value, rest.length, JSON.stringify(rest)); +const [first, , third] = m; +console.log(JSON.stringify(first), JSON.stringify(third)); +const s = new Set([3, 1, 2]); +const [x, y, ...others] = s; +console.log(x, y, others); +let [p] = new Map([["only", 1]]); +p = ["changed", 2]; +console.log(p); +function firstKey(map: Map): string { + const [[k]] = map; + return k; +} +console.log(firstKey(new Map([["z", 26]]))); From a8b7bafefb6b95b8c27eb2dd347ee3b794bd101f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:29:57 -0700 Subject: [PATCH 057/120] Lower `...(cond && { k: v })` spreads as conditional fields Spreading a falsy primitive copies nothing, so `...(cond && { k: v })` is the optional-field idiom `...(cond ? { k: v } : {})`. The `&&` spelling (the @edge-runtime/cookies parseSetCookie shape) was refused with a record shape mismatch (SC2002). When the spread's type mixes the record with primitive arms, the literal lowering now uses the existing conditional-spread desugar: cond evaluates once, v only when taken, and the absent arm leaves the optional field undefined. Record and record-or-unit spreads keep the source-copy path. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/object-literals.ts | 68 +++++++++++++++++-- tests/corpus/object-spread-and-conditional.ts | 37 ++++++++++ 2 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 tests/corpus/object-spread-and-conditional.ts diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index c95731fc7..78d783a11 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -823,7 +823,7 @@ export function lowerObjectLiteral( // `k?: ...` — which desugar to one conditional field at the // spread's own position (any position: they introduce one fresh // name, collision-fenced below). Everything else stays fenced. - const cs = conditionalSpreadOf(prop.expression); + const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { lowerer.unsupported( "SC1090", @@ -1264,6 +1264,7 @@ export function lowerObjectLiteral( expr.properties.length >= 2 && ts.isSpreadAssignment(expr.properties[0]!) && !conditionalSpreadOf(expr.properties[0]!.expression) && + !andSpreadOf(lowerer, expr.properties[0]!.expression) && expr.properties .slice(1) .every( @@ -1340,7 +1341,7 @@ export function lowerObjectLiteral( let mergeable = true; for (const prop of expr.properties) { if (ts.isSpreadAssignment(prop)) { - const cs = conditionalSpreadOf(prop.expression); + const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { lowerer.unsupported( "SC1090", @@ -1481,7 +1482,12 @@ export function lowerObjectLiteral( !shape.tuple && shape.fields.length > 0 && expr.properties.length > 0 && - expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) && + expr.properties.every( + (p) => + ts.isSpreadAssignment(p) && + !conditionalSpreadOf(p.expression) && + !andSpreadOf(lowerer, p.expression), + ) && expr.properties.some((p) => { const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression)); return ( @@ -1599,7 +1605,7 @@ export function lowerObjectLiteral( } } if (ts.isSpreadAssignment(prop)) { - const cs = conditionalSpreadOf(prop.expression); + const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs && cs !== "unsupported") { // `...(c ? { k: v } : {})` — ONE conditional field at the // spread's own position: cond evaluates exactly once, `v` only @@ -1728,7 +1734,8 @@ export function lowerObjectLiteral( const laterNames = new Set(); for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { - if (conditionalSpreadOf(later.expression)) continue; + if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) + continue; const lt = laterSpreadType(lowerer, later.expression); if (lt?.kind === "record") { // A field the later source may lack defines nothing. @@ -1963,7 +1970,8 @@ export function lowerObjectLiteral( const laterNames = new Set(); for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { - if (conditionalSpreadOf(later.expression)) continue; + if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) + continue; const lt = laterSpreadType(lowerer, later.expression); if (lt?.kind === "record") { // A field the later source may lack defines nothing. @@ -2517,6 +2525,54 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio }; } +/** `...(c && { k: v })`: spreading a falsy primitive (false, 0, "", NaN) + * copies nothing, so the idiom is `...(c ? { k: v } : {})` with `c` + * evaluated once. Recognized only where the spread's own type mixes the + * record with primitive arms; a record or record-or-unit spread keeps the + * ordinary source-copy path. */ +export function andSpreadOf( + lowerer: Lowerer, + expr: ts.Expression, +): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" | null { + let e: ts.Expression = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) + return null; + let right: ts.Expression = e.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + if (!ts.isObjectLiteralExpression(right)) return null; + const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e)); + if (spreadType?.kind !== "union") return null; + const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? []; + if ( + arms.filter((arm) => arm.kind === "record").length !== 1 || + !arms.some((arm) => arm.kind === "bool" || arm.kind === "f64" || arm.kind === "string") || + !arms.every( + (arm) => + arm.kind === "record" || + arm.kind === "bool" || + arm.kind === "f64" || + arm.kind === "string" || + isUnitType(arm), + ) + ) + return null; + if (right.properties.length === 0) return "unsupported"; + const props: CondSpreadProp[] = []; + for (const p of right.properties) { + if ( + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + p.name && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) + ) { + props.push(p as CondSpreadProp); + } else { + return "unsupported"; + } + } + return { cond: e.left, props, whenTrue: true }; +} + /** A conditional spread's carrier property: `name: value` or shorthand, * with an identifier/string-literal name. */ export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & { diff --git a/tests/corpus/object-spread-and-conditional.ts b/tests/corpus/object-spread-and-conditional.ts new file mode 100644 index 000000000..dd42063d0 --- /dev/null +++ b/tests/corpus/object-spread-and-conditional.ts @@ -0,0 +1,37 @@ +// `...(cond && { k: v })` spreads nothing when cond is a falsy primitive +// (false, 0, ""), so it is the optional-field idiom `...(cond ? { k: v } : {})`: +// cond evaluates once, v only when taken. (Fields print one by one: the +// declared-shape property order of spread-built records is a documented +// limitation.) +type C = { name: string; name2: string; httpOnly?: boolean; maxAge?: number; path?: string; sameSite?: string }; +function parse(attrs: Record, flag: boolean): C { + const httponly = attrs.httponly; + const maxage = attrs["max-age"]; + const samesite = attrs.samesite; + let calls = 0; + const lower = (s: string): string => { + calls++; + return s.toLowerCase(); + }; + const c: C = { + name: "a", + ...(flag && { httpOnly: true }), + ...(httponly && { path: httponly }), + ...(typeof maxage === "string" && { maxAge: Number(maxage) }), + name2: "/", + ...(samesite && { sameSite: lower(samesite) }), + }; + console.log("calls", calls); + return c; +} +for (const [attrs, flag] of [ + [{}, false], + [{ httponly: "yes", "max-age": "10", samesite: "Lax" }, true], + [{ httponly: "", "max-age": "", samesite: "" }, true], +] as [Record, boolean][]) { + const c = parse(attrs, flag); + console.log(c.name, c.name2, c.httpOnly, c.path, c.maxAge, c.sameSite, "httpOnly" in c, "path" in c); +} +const n: number = Number(process.argv.length > 99 ? "1" : "0"); +const d: { v?: number; name: string } = { name: "n", ...(n && { v: n }) }; +console.log(d.name, d.v, "v" in d); From dd0c255fa7a7b56550ab1a09df974e97aba92424 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:34:42 -0700 Subject: [PATCH 058/120] Stringify class instances that declare toJSON `JSON.stringify(new Url(...))` was refused (SC1090) for every program class, although JSON.stringify of a class with a toJSON hook serializes whatever toJSON(key) returns. A class whose chain declares toJSON now crosses into the runtime JSON walker through its checked-dynamic class view, which calls the hook with the root key "" and handles nested hooks; an undefined hook result keeps the dyn-root undefined rule. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/json.ts | 11 ++++- tests/corpus/json-stringify-class-tojson.ts | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/json-stringify-class-tojson.ts diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 7ce902772..8cce83beb 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -149,9 +149,18 @@ export function lowerJsonMethodCall( } const optionalString = lowerOptionalStringifyRoot(lowerer, value, indent, loc); if (optionalString) return optionalString; + // A program class instance whose class (or a base) declares toJSON: + // JSON.stringify serializes whatever toJSON(key) returns, so the + // instance crosses into the runtime walker, which calls the hook + // through the checked-dynamic class view. + const toJsonClass = + value.type.kind === "object" && + lowerer.findMethodOn(lowerer.classes.get(value.type.className) ?? null, "toJSON") !== null && + lowerer.dynConvertible(value.type); if ( value.type.kind === "url" || value.type.kind === "dyn" || + toJsonClass || isJsonStringifyDynamicType( value.type, (id) => lowerer.shapes.get(id), @@ -182,7 +191,7 @@ export function lowerJsonMethodCall( // symbol). Where the program can observe that (jsonResultObserved), // keep the actual undefined; elsewhere the dyn-root string ABI // stays, whose undefined result prints the same text. - return value.type.kind === "dyn" && !jsonResultObserved(lowerer, call) + return (value.type.kind === "dyn" || toJsonClass) && !jsonResultObserved(lowerer, call) ? { kind: "toString", operand: raw, type: STRING, loc } : { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc }; } diff --git a/tests/corpus/json-stringify-class-tojson.ts b/tests/corpus/json-stringify-class-tojson.ts new file mode 100644 index 000000000..617e5ac25 --- /dev/null +++ b/tests/corpus/json-stringify-class-tojson.ts @@ -0,0 +1,44 @@ +// JSON.stringify of a class instance whose class (or a base) declares +// toJSON serializes what toJSON(key) returns: the root key is "", nested +// hooks run through the same walker, and a toJSON answering undefined +// makes the whole result undefined. +class Url { + href: string; + constructor(href: string) { + this.href = href; + } + toJSON(): string { + return this.href; + } +} +class NextUrl extends Url { + base = "ignored"; +} +class Keyed { + toJSON(key: string): { key: string; at: Url[] } { + return { key, at: [new Url("n")] }; + } +} +class Missing { + toJSON(): undefined { + return undefined; + } +} +console.log(JSON.stringify(new Url("http://x/"))); +console.log(JSON.stringify(new NextUrl("http://y/"))); +console.log(JSON.stringify(new Keyed())); +console.log(JSON.stringify(new Keyed(), null, 2)); +function serialize(value: Missing): string { + const text = JSON.stringify(value); + return text === undefined ? "undefined result" : `text ${text}`; +} +console.log(serialize(new Missing()), JSON.stringify(new Missing()) === undefined); +let calls = 0; +class Counted { + toJSON(): number { + calls++; + return calls; + } +} +const c = new Counted(); +console.log(JSON.stringify(c), JSON.stringify(c), calls); From cd63b935e0dd280eaef5e1fd9c4488ba8d07ffe0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:34:42 -0700 Subject: [PATCH 059/120] Instantiate generics with a literal undefined argument `id(undefined)` binds T = undefined, which the type mapper answers as VOID (the return convention), so the instance's parameter was refused (SC2001) although a declared `v: undefined` parameter compiles. Instantiated parameters are value positions and now take the unit-only slot, as declared ones do. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/call-signatures.ts | 14 ++++++++++++-- tests/corpus/generic-undefined-argument.ts | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/corpus/generic-undefined-argument.ts diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 8e30e8b82..65a013bf3 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -13,7 +13,12 @@ import { typeEquals, } from "../../ir/ir.js"; import { isJsSourceFile, isNodeEsmFile } from "../program.js"; -import { genResultRecord, jsOpenObjectType } from "../type-mapper.js"; +import { + genResultRecord, + isUnitOnlyTsType, + jsOpenObjectType, + unitOnlyUnion, +} from "../type-mapper.js"; import { dynFallbackType } from "./lowerer.js"; import { builtinModuleFnOf } from "./surfaces.js"; import { mixinFnShapeOf } from "./lower-mixins.js"; @@ -373,7 +378,7 @@ export function paramShapes( const tsType = symbol ? lowerer.checker.getTypeOfSymbol(symbol) : lowerer.typeOf(declParam.name); - const mapped = jsOpenObjectType( + let mapped = jsOpenObjectType( declParam, lowerer.runtimeOptionalBindingType( declParam.name, @@ -382,6 +387,11 @@ export function paramShapes( lowerer.shapes, lowerer.unions, ); + // An instantiation with a literal `undefined` argument (`id(undefined)` + // binds T = undefined): standalone undefined maps to VOID (the return + // convention), and a parameter is a value position, so it takes the + // unit-only slot like a declared `v: undefined` parameter. + if (mapped?.kind === "void" && isUnitOnlyTsType(tsType)) mapped = unitOnlyUnion(lowerer.unions); if (!mapped || mapped.kind === "void") lowerer.badType(blameOf?.(declParam, i) ?? declParam.name, tsType); if (declParam.dotDotDotToken) { diff --git a/tests/corpus/generic-undefined-argument.ts b/tests/corpus/generic-undefined-argument.ts new file mode 100644 index 000000000..97d438549 --- /dev/null +++ b/tests/corpus/generic-undefined-argument.ts @@ -0,0 +1,19 @@ +// A generic call with a literal `undefined` argument binds T = undefined; +// the parameter holds undefined like a declared `v: undefined` parameter. +function kind(v: T): string { + return typeof v; +} +console.log(kind(undefined), kind(null), kind(1), kind("s")); +function same(a: T, b: T): boolean { + return a === b; +} +console.log(same(undefined, undefined)); +function wrap(v: T): { v: T } { + return { v }; +} +const w = wrap(undefined); +console.log(JSON.stringify(w), w.v === undefined, "v" in w); +function describe(label: string, v: T): string { + return `${label}=${String(v)}`; +} +console.log(describe("u", undefined)); From b831d840d73f32787ed67388ec1aa72701af1d95 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:34:42 -0700 Subject: [PATCH 060/120] Accept getters that never return a value A getter that only throws (`get page(): never`, or Next.js's unannotated removed-API getters) or has no return statement declared a VOID property type, so the class was refused (SC2001). No read of such a getter produces a value other than undefined; the property now takes the checked-dynamic slot, and each read throws the getter's error or answers undefined. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 12 ++++++- tests/corpus/class-never-getter.ts | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/class-never-getter.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a10874928..f68ff45da 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -3465,7 +3465,17 @@ export function collectClassShapeInner( } let sig: { params: ParamShape[]; ret: IrType }; if (isGet) { - const ret = lowerer.declaredReturnType(member, member.name); + let ret = lowerer.declaredReturnType(member, member.name); + // A getter that never returns a value: typed `never` (it only + // throws, Next.js's removed `page`/`ua` getters) or, unannotated + // with no return statement, inferred `void`. A read answers + // undefined or throws, so the property takes the checked-dynamic + // slot instead of VOID. + if ( + ret.kind === "void" && + (lowerer.typeOf(member.name).flags & (ts.TypeFlags.Never | ts.TypeFlags.Void)) !== 0 + ) + ret = DYN; if (ret.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); sig = { params: [], ret }; } else { diff --git a/tests/corpus/class-never-getter.ts b/tests/corpus/class-never-getter.ts new file mode 100644 index 000000000..497fa3d25 --- /dev/null +++ b/tests/corpus/class-never-getter.ts @@ -0,0 +1,36 @@ +// A getter that never returns a value compiles: annotated `never` or +// unannotated (the removed-API getter idiom, which throws), and a getter +// without a return statement, whose reads answer undefined. +class Req { + ok = 1; + get page(): never { + throw new Error("page has been removed"); + } + get ua(): never { + throw new TypeError("ua has been removed"); + } + public get legacy() { + throw new RangeError("legacy has been removed"); + } + get nothing() { + return; + } +} +const r = new Req(); +try { + console.log(r.page); +} catch (e) { + console.log(e instanceof Error ? `${e.name}: ${e.message}` : e); +} +try { + const ua = r.ua; + console.log("unreachable", ua); +} catch (e) { + console.log(e instanceof Error ? `${e.name}: ${e.message}` : e); +} +try { + console.log(r.legacy); +} catch (e) { + console.log(e instanceof Error ? `${e.name}: ${e.message}` : e); +} +console.log(r.ok, r.nothing); From 3149e7be9177dfd48e03f3c12ece5bee7e6f6970 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:36:51 -0700 Subject: [PATCH 061/120] Regenerate the surface manifest Date.prototype.toUTCString and the static Response.json operation are static surfaces now (pnpm manifest). pnpm node-compat leaves the Node ledger and the public artifact unchanged. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/surface-manifest.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index f5313f12d..a71cccaf4 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4159,6 +4159,13 @@ "status": "static", "note": "UTC ISO formatting over constructed and stored Date values" }, + { + "id": "stdlib.date.toUTCString", + "kind": "stdlib", + "name": "Date.prototype.toUTCString", + "status": "static", + "note": "RFC 7231 IMF-fixdate formatting over stored Date values" + }, { "id": "stdlib.date.valueOf", "kind": "stdlib", @@ -5236,9 +5243,8 @@ "id": "stdlib.response.static.json", "kind": "stdlib", "name": "Response.json", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.22.0 / Undici 7.29.1; Response static constructor-object operations have no compiler lowering" + "status": "static", + "note": "Node 24.22.0 / Undici 7.29.1; facets: webidl-conversion, error-shape; supported scope: data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor; differential evidence: fixture:static-response-json" }, { "id": "stdlib.response.static.redirect", From d84e410981499a181df64771c94d55c4dba66ef6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:46:52 -0700 Subject: [PATCH 062/120] Upcast devirtualized calls whose override returns a subclass A base method calling `this.set()` where only a subclass is constructed devirtualizes to the subclass override. With a polymorphic `this` return type, the override's lowered return type is the subclass object while the site keeps the base method's type, and the validator reported an internal compiler error (SC9001: "call %Sub.set type object != return object"). The Next.js extract hit this once its placeholder ResponseCookies instance was gone: MiddlewareResponseCookies became the only constructed class. The direct call now carries the callee's call-site return type and upcasts to the site's class; any other mismatch keeps the virtual call. Calls whose types already matched are emitted exactly as before. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/devirtualize.ts | 34 +++++++++++++++---- packages/compiler/src/ir/validate.ts | 2 +- .../corpus/class-devirtualized-this-return.ts | 27 +++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 tests/corpus/class-devirtualized-this-return.ts diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index 48a5712e3..04a53aeb1 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -1,6 +1,7 @@ import * as ts from "../ts7/adapter.js"; -import type { IrExpr, IrFunction } from "../../ir/ir.js"; +import { type IrExpr, type IrFunction, type IrType, typeEquals } from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import { callSiteReturnType } from "../../ir/validate.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; @@ -56,6 +57,9 @@ export function devirtualizeVirtualCalls( class ConstructedClasses { private readonly lowered = new Set(); + /** Lowered bodies' return types: an override may return a subclass + * (`set(): this` resolves per declaring class). */ + private readonly returnTypes = new Map(); /** Classes some retained IR allocates or exposes as a class value. */ private readonly allocated = new Set(); private readonly exportedNames: ReadonlySet; @@ -74,6 +78,7 @@ class ConstructedClasses { }; for (const fn of functions) { this.lowered.add(fn.name); + this.returnTypes.set(fn.name, callSiteReturnType(fn)); everyStmtList(fn.body, { stmt: () => true, expr: (e) => { @@ -102,7 +107,7 @@ class ConstructedClasses { this.facts.reachable.has(`%${candidate.def.name}.${e.method}`), ) ) - return this.direct(e, info, nearest.declarer); + return this.direct(e, info, nearest.declarer) ?? e; if (this.open(info)) return e; const targets: ClassInfo[] = []; for (const candidate of this.descendants(info)) { @@ -117,7 +122,7 @@ class ConstructedClasses { const callee = `%${target.def.name}.${e.method}`; if (!this.facts.reachable.has(callee) || !this.lowered.has(callee)) return e; } - if (targets.length === 1) return this.direct(e, info, targets[0]!); + if (targets.length === 1) return this.direct(e, info, targets[0]!) ?? e; // A small closed set: the backend tests the loaded slot against each // implementation and calls it directly (no indirect fallback; every // instance that can reach this site resolves to one of them). @@ -125,7 +130,19 @@ class ConstructedClasses { return { ...e, targets: targets.map((t) => t.def.name) }; } - private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr { + /** The direct call, or null when the target's return type cannot stand + * for the site's: an override returning a subclass object (a polymorphic + * `this` return) upcasts to the site's class; any other mismatch keeps + * the virtual call. */ + private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr | null { + const callee = `%${target.def.name}.${e.method}`; + const ret = this.returnTypes.get(callee) ?? e.type; + const upcast = + !typeEquals(ret, e.type) && + ret.kind === "object" && + e.type.kind === "object" && + this.lowerer.isSubclassOf(ret.className, e.type.className); + if (!typeEquals(ret, e.type) && !upcast) return null; const receiver = e.args[0]!; const typed: IrExpr = target === info || this.lowerer.isSubclassOf(info.def.name, target.def.name) @@ -139,13 +156,16 @@ class ConstructedClasses { type: { kind: "object", className: target.def.name }, loc: receiver.loc, }; - return { + const call: IrExpr = { kind: "call", - callee: `%${target.def.name}.${e.method}`, + callee, args: [typed, ...e.args.slice(1)], - type: e.type, + type: ret, loc: e.loc, }; + return upcast && e.type.kind === "object" + ? this.lowerer.upcastTo(call, e.type.className) + : call; } /** The class and every class below it, in the isSubclassOf sense diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 09ab7d951..979b70115 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -90,7 +90,7 @@ export interface IrValidationError { * calls and closures over it see Promise; a generator body's is the * TReturn channel, so call sites see the generator type. The one place * the body/call-site split is spelled out in the validator. */ -function callSiteReturnType(fn: IrFunction): IrType { +export function callSiteReturnType(fn: IrFunction): IrType { if (fn.async && fn.generator !== undefined) { return { kind: "generator", diff --git a/tests/corpus/class-devirtualized-this-return.ts b/tests/corpus/class-devirtualized-this-return.ts new file mode 100644 index 000000000..e7f2fc9fa --- /dev/null +++ b/tests/corpus/class-devirtualized-this-return.ts @@ -0,0 +1,27 @@ +// A base method calling `this.set()` where only a subclass is ever +// constructed devirtualizes to the subclass override; a polymorphic `this` +// return resolves per declaring class, so the direct call's subclass +// result upcasts to the base method's type (was an internal compiler error). +class Cookies { + values: string[] = []; + set(value: string): this { + this.values.push(value); + return this; + } + delete(name: string): this { + return this.set(`${name}=; Max-Age=0`); + } +} +class MirroredCookies extends Cookies { + mirror: string[] = []; + override set(value: string): this { + super.set(value); + this.mirror.push(value.toUpperCase()); + return this; + } +} +const cookies = new MirroredCookies(); +cookies.set("a=1").delete("b").set("c=3"); +console.log(cookies.values.join(" | ")); +console.log(cookies.mirror.join(" | ")); +console.log(cookies.delete("d") === cookies, cookies.values.length); From 8e457b2b05a632bb919cd7ef208686ae3774b429 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:53:15 -0700 Subject: [PATCH 063/120] Record ts7 preflight/order baselines for the new corpus programs pnpm test:ts7 --baselines-only reported the fifteen new corpus programs as missing. Recorded with SCRIPTC_UPDATE_BASELINES=1 from the native frontend; every entry has no preflight diagnostics and a single-module order, and no existing baseline changed. Co-Authored-By: Claude Opus 5.5 --- .../tests/corpus/class-devirtualized-this-return.ts.json | 6 ++++++ .../tests/corpus/class-fetch-handle-fields.ts.json | 6 ++++++ .../order-parity/tests/corpus/class-never-getter.ts.json | 6 ++++++ .../corpus/class-symbol-field-constructor-assigned.ts.json | 6 ++++++ .../order-parity/tests/corpus/date-to-utc-string.ts.json | 6 ++++++ .../tests/corpus/destructure-map-set-iterables.ts.json | 6 ++++++ .../tests/corpus/fetch-headers-set-cookie-iteration.ts.json | 6 ++++++ .../tests/corpus/fetch-invalid-url-cause.ts.json | 6 ++++++ .../tests/corpus/fetch-request-set-cookie-wire.ts.json | 6 ++++++ .../tests/corpus/generic-undefined-argument.ts.json | 6 ++++++ .../tests/corpus/json-stringify-class-tojson.ts.json | 6 ++++++ .../corpus/json-stringify-unknown-undefined-root.ts.json | 6 ++++++ .../tests/corpus/object-spread-and-conditional.ts.json | 6 ++++++ .../tests/corpus/optional-call-native-method.ts.json | 6 ++++++ .../tests/corpus/union-narrowed-tostring.ts.json | 6 ++++++ 15 files changed, 90 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json new file mode 100644 index 000000000..f75299bf2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-devirtualized-this-return.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json new file mode 100644 index 000000000..69dfd8454 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-fetch-handle-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json new file mode 100644 index 000000000..4b5ee103f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-never-getter.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json new file mode 100644 index 000000000..4078df7c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-symbol-field-constructor-assigned.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json new file mode 100644 index 000000000..6c5e37d54 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/date-to-utc-string.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json new file mode 100644 index 000000000..fee4d0ebc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/destructure-map-set-iterables.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json new file mode 100644 index 000000000..f0d413603 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-headers-set-cookie-iteration.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json new file mode 100644 index 000000000..70f7a83fd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-invalid-url-cause.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json new file mode 100644 index 000000000..9357bc1d8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-request-set-cookie-wire.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json new file mode 100644 index 000000000..bbc59d2ca --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-undefined-argument.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json new file mode 100644 index 000000000..9b1aa55ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-stringify-class-tojson.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json new file mode 100644 index 000000000..8c424c6a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-stringify-unknown-undefined-root.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json new file mode 100644 index 000000000..c58247184 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-spread-and-conditional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json new file mode 100644 index 000000000..a8678d806 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/optional-call-native-method.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json new file mode 100644 index 000000000..454df784e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-narrowed-tostring.ts" + ], + "diags": [] +} From 238d782fdc94874f0293f3cd20321fbb1112dc61 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 17:17:00 -0500 Subject: [PATCH 064/120] Keep the CommonJS caches on their owners and recount 6110's fences The exports-assignment cache moves from a module-level WeakMap onto the Lowerer (cjsExportsAssignments), and the inline require() calls that close a require cycle move from a module-level WeakSet into preflight's result: checkPreflight stores them on the LoadResult, and lowering receives them with the other preflight facts (LowerOptions.inlineRequireCycleCalls). 6110's call and apply wrappers now compile `root === module.exports` and `typeof module.exports`, which print what Node prints when the wrappers' untaken branches run. The class base's `console.log(module.exports)` is the one statement left as a runtime fence, and its body is lowered once, so the program's deferred-fence count is 1. --- .../frontend/lowering/cjs-exports-object.ts | 9 +++--- .../frontend/lowering/cjs-inline-require.ts | 3 +- .../compiler/src/frontend/lowering/lowerer.ts | 18 ++++++++++++ packages/compiler/src/frontend/pipeline.ts | 1 + packages/compiler/src/frontend/program.ts | 29 ++++++++++--------- .../6110-cjs-speculative-wrapper-fences.cjs | 14 ++++----- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index c3edafddc..d72792ae7 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -148,17 +148,16 @@ function truthinessOnly(node: ts.Expression): boolean { } /** Which of the module's exports bindings the file ever assigns (any - * nesting, compound operators included). */ -const assignmentsCache = new WeakMap(); - + * nesting, compound operators included), cached per file on the lowerer + * (Lowerer.cjsExportsAssignments). */ function exportsAssignments( lowerer: Lowerer, sf: ts.SourceFile, ): { exports: boolean; moduleExports: boolean } { - const cached = assignmentsCache.get(sf); + const cached = lowerer.cjsExportsAssignments.get(sf); if (cached !== undefined) return cached; const out = { exports: false, moduleExports: false }; - assignmentsCache.set(sf, out); + lowerer.cjsExportsAssignments.set(sf, out); ts.walkPreorder(sf, (node) => { // A fresh `exports = module.exports = {}` reset leaves both bindings // naming one (new, empty) exports object for the rest of the module. diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 75474565b..7f31337f5 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -25,7 +25,6 @@ import { cjsExportTargetLiteral, isCjsJsFile, isJsSourceFile, - isInlineRequireCycleCall, isModuleExportsAccess, locOf, npmStaticDepSf7, @@ -109,7 +108,7 @@ export function inlineNamespaceRequireOf( /** `value` evaluated after the required module's run-once %init (and its * module-registry link) at the require's own position. */ export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr { - if (isInlineRequireCycleCall(req.call)) { + if (lowerer.inlineRequireCycleCalls.has(req.call)) { lowerer.unsupported( "SC1090", req.call, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6da1ede27..55447e1e0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -753,6 +753,8 @@ export interface LowerOptions { * resolution walk and named-import link checks): the program * compiles to that startup crash. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** LIBRARY mode's reachability roots: the profile-mapped exports of the * entry module. Executable builds root at the entry's top level alone * (an unreferenced export dead-strips); a library's exports are called from @@ -845,6 +847,8 @@ export interface LowererMode { * module init — Node refuses the whole graph before anything evaluates, * so nothing runs. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** The build's outbound native FFI declarations. */ ffiImports?: readonly IrFfiImport[]; /** LowerOptions.libraryCallbacks (see there). */ @@ -958,6 +962,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -981,6 +986,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1011,6 +1017,7 @@ export function lowerToIr( reachable, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1069,6 +1076,7 @@ export function lowerToIr( reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, targetPlatform, ffiImports, libraryCallbacks, @@ -1628,6 +1636,15 @@ export class Lowerer { readonly checker: ts.TypeChecker; readonly diags: ScrDiagnostic[] = []; readonly fnSigsBySymbol = new Map(); + /** Per file: whether it assigns `exports` and `module.exports` + * (cjs-exports-object.ts). */ + readonly cjsExportsAssignments = new Map< + ts.SourceFile, + { exports: boolean; moduleExports: boolean } + >(); + /** Inline require() calls whose module edge closes a require cycle + * (LoadResult.inlineRequireCycleCalls). */ + readonly inlineRequireCycleCalls: ReadonlySet; /** Immutable callable aliases and compiler-generated adapters. The map * is symbol-keyed, so shadowing and cross-module imports keep the same * exact binding discipline as globals/functions. */ @@ -2859,6 +2876,7 @@ export class Lowerer { this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); this.targetPlatform = mode.targetPlatform ?? process.platform; this.startupCrash = mode.startupCrash ?? null; + this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); this.projectCheckJs = mode.projectCheckJs ?? false; this.ffiImports = mode.ffiImports ?? []; this.libraryCallbacks = mode.libraryCallbacks ?? false; diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 48d7e45a0..f7ab33429 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -593,6 +593,7 @@ function loadFrontend( ...opts, frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, + inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls, projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 47543007f..8433338d0 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -338,6 +338,12 @@ export interface LoadResult { * last. Empty until checkPreflight runs (and empty when preflight fails * structurally). */ moduleOrder: ts.SourceFile[]; + /** Inline require() calls whose edge closes a require cycle. Node answers + * them with the still-evaluating module's partial exports, which the + * static export globals do not model; their lowering refuses them by name + * at run time instead of refusing the whole program. Filled by + * checkPreflight. */ + inlineRequireCycleCalls?: ReadonlySet; /** Node's startup refusal, when the graph carries an import edge Node * rejects before ANY module evaluates — a RESOLUTION refusal (a bare * specifier nothing installed resolves, an exports/imports map that @@ -975,9 +981,10 @@ export function loadProgram( * preflight diagnostics. Preserve the caller's complete load shape when * mutating it, including its owned services and disposal callback. */ export function checkPreflight(load: T): ScrDiagnostic[] { - const { diags, moduleOrder, startupCrash } = preflight7(load); + const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load); load.moduleOrder = moduleOrder; load.startupCrash = startupCrash; + load.inlineRequireCycleCalls = inlineRequireCycleCalls; return diags; } @@ -2447,17 +2454,6 @@ export interface CycleEdge { call?: ts.CallExpression; } -/** Inline require() calls whose edge closes a require cycle. Node answers - * them with the still-evaluating module's partial exports, which the static - * export globals do not model; their lowering refuses them by name at run - * time instead of refusing the whole program (the inline call kept its - * runtime fence before it became a module edge). */ -const inlineRequireCycleCalls = new WeakSet(); - -export function isInlineRequireCycleCall(call: ts.Node): boolean { - return inlineRequireCycleCalls.has(call); -} - /** True when the reference at `node` can only evaluate AFTER the module * graph finished initializing: inside a function-like BODY, inside a * parameter (default initializers evaluate at call time), or inside an @@ -4133,6 +4129,7 @@ function preflight7(load: LoadResult): { diags: ScrDiagnostic[]; moduleOrder: ts.SourceFile[]; startupCrash: StartupCrash | null; + inlineRequireCycleCalls: ReadonlySet; } { const { program, entry } = load; const diags: ScrDiagnostic[] = [...load.configDiags]; @@ -5118,6 +5115,7 @@ function preflight7(load: LoadResult): { return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all; }); const order: ts.SourceFile[] = []; + const inlineRequireCycleCalls = new Set(); const state = new Map(); const stack: string[] = []; const visit = (sf: ts.SourceFile): void => { @@ -5214,7 +5212,12 @@ function preflight7(load: LoadResult): { esmNamedImportLinkCheck(program, entry, order, diags), ); - return { diags, moduleOrder: order, startupCrash: resolveCrash ?? linkCrash }; + return { + diags, + moduleOrder: order, + startupCrash: resolveCrash ?? linkCrash, + inlineRequireCycleCalls, + }; } /* ── the CJS named-import link check ───────────────────────────────────── diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index 1b3546518..fbb1a65fc 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -1,12 +1,12 @@ -// @deferred-fences: 9 +// @deferred-fences: 1 // Function expressions lowered speculatively — the `.call(this)` and // `.apply` receivers of CommonJS wrappers, and a computed class base — keep -// every statement of their bodies. A statement with no static lowering -// (here `module.exports` read as a value) compiles to its runtime fence -// instead of vanishing; these never run, so the binary matches Node. -// Coverage counts the three fenced statements nine times: each lowering of -// a wrapper body counts its statements, and the `.call` and `.apply` -// wrapper bodies are each lowered four times. +// every statement of their bodies. The wrappers' `root === module.exports` +// and `typeof module.exports` compile; a statement with no static lowering +// (here `module.exports` passed as a value in the class base) compiles to +// its runtime fence instead of vanishing. It never runs, so the binary +// matches Node. Coverage counts that one fenced statement once: the class +// base's body is lowered once. const verbose = process.argv.length > 99; (function () { var root = this; From 4c27ab8bd7fbd72f9683eeb1b314c5430ee9b5e0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 17:17:22 -0500 Subject: [PATCH 065/120] Compile classes that extend a generic class factory's result `class Counter extends base(42) {}` with `function base(initial: T) { return class { value: T = initial; }; }` failed with SC1090 "extending computed expressions": the call's result type is the factory's local class instantiated with T = number, which only maps inside the factory's own generic instance. A call of a generic function whose result is one of its local classes (or an instance of one) now keys the instance on its type bindings and maps the result inside that instance, where the local class takes the instance's name and bindings. The class value then works as a base (`extends base(42)`), as a `const` binding, through `new`, and for method calls and field reads on its instances. Each instantiation has its own class, so `base(1) === base(1)` is false and `instanceof` tells them apart, as in Node. Calls inside another generic instance bind through the declaration; calls elsewhere also record the instantiated checker type, so later uses of it name the same class. Fixes #1241 Co-authored-by: BaoLei <50226652+blurname@users.noreply.github.com> --- .../local-class-expressions.mdx | 2 +- .../frontend/lowering/generic-functions.ts | 148 +++++++++++++++++- .../src/frontend/lowering/lower-classes.ts | 6 + .../src/frontend/lowering/lower-stmts.ts | 4 + .../compiler/src/frontend/lowering/lowerer.ts | 12 +- packages/compiler/src/frontend/type-mapper.ts | 10 +- ...6051-generic-class-factory-results.ts.json | 6 + .../6051-generic-class-factory-results.ts | 111 +++++++++++++ tests/diagnostics/local-class-boundaries.ts | 4 - .../local-class-boundaries.ts.txt | 9 +- tests/harness/corpus-costs.json | 2 + 11 files changed, 296 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6051-generic-class-factory-results.ts.json create mode 100644 tests/corpus/6051-generic-class-factory-results.ts diff --git a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx index 1f60c6101..d18ecac60 100644 --- a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx +++ b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx @@ -1 +1 @@ -- **Local class expressions:** Static members, decorators, private brands, generic members, explicitly generic factories returning local constructors, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported. +- **Local class expressions:** Static members, decorators, private brands, generic members, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported. diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index bd785a256..49bb33882 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -15,7 +15,7 @@ import { isJsSourceFile, locOf } from "../program.js"; import { typeKey } from "../type-mapper.js"; import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; -import { findGenericMethodOn, type ClassInfo } from "./lower-classes.js"; +import { findGenericMethodOn, storedClassValueType, type ClassInfo } from "./lower-classes.js"; import { nsPathPrefix } from "./lower-namespaces.js"; import { declSymbolOf } from "./lower-modules.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; @@ -292,6 +292,8 @@ export function genericCallInstance( (_param, i) => expr.arguments[i] ?? expr, ); const retTs = lowerer.checker.getReturnTypeOfSignature(rsig); + const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs); + if (local) return local; const returnType = lowerer.mapTypeOf(retTs); if (!returnType) { fenceGenericSignatureResult(lowerer, expr, retTs); @@ -331,6 +333,150 @@ export function genericCallInstance( ); } +/** A generic factory whose result is one of its own local classes + * (`function base(v: T) { return class { value: T = v }; }`). Each + * instance has its own class, so the instance is keyed on its bindings and + * the result maps inside it, where the local class takes the instance's + * name and bindings. */ +function localClassResultInstance( + lowerer: Lowerer, + expr: ts.CallExpression, + info: GenericFnInfo, + rsig: ts.Signature, + params: ParamShape[], + retTs: ts.Type, +): GenericInstance | null { + if (!localClassOfResult(lowerer, info, retTs)) return null; + const tsBindings = new Map(); + const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings); + const bindingKey = info.typeParams + .map((tp) => { + const bound = bindings.get(tp); + return bound ? typeKey(bound) : "?"; + }) + .join(","); + const pending: IrType = { kind: "void" }; + const inst = internGenericInstance(lowerer, expr, info, params, pending, () => bindings, { + extraKey: `@class<${bindingKey}>`, + tsBindings, + }); + if (inst.returnType !== pending) return inst; + const returnType = withInstanceScope(lowerer, info, inst, () => lowerer.mapTypeOf(retTs)); + if (!returnType) { + fenceGenericSignatureResult(lowerer, expr, retTs); + lowerer.badType(expr, retTs); + } + inst.returnType = returnType; + // Values of the class outside the factory (`const b = new B()`, `b.read()`) + // carry the instantiated checker type; record which class it names. A + // call inside another generic instance can have a type that depends on + // that instance's parameters, which no single class answers. + if (lowerer.typeParamBindings !== null) return inst; + const instance = + returnType.kind === "classval" + ? lowerer.checker + .getConstructSignatures(retTs) + .map((sig) => lowerer.checker.getReturnTypeOfSignature(sig)) + : [retTs]; + const className = + returnType.kind === "classval" || returnType.kind === "object" ? returnType.className : null; + if (className !== null) + for (const type of instance) lowerer.localClassInstanceTypes.set(type.id, className); + return inst; +} + +/** The class value a call of a generic factory returns when the result is + * one of the factory's local classes (`class C extends base(42) {}`), or + * null for any other call. */ +export function genericLocalClassResult(lowerer: Lowerer, expr: ts.CallExpression): IrType | null { + if (!ts.isIdentifier(expr.expression)) return null; + const info = lowerer.genericFnOf(expr.expression); + if (!info || info.implicitParams) return null; + const rsig = lowerer.checker.getResolvedSignature(expr); + if (!rsig || !localClassOfResult(lowerer, info, lowerer.checker.getReturnTypeOfSignature(rsig))) + return null; + return genericCallInstance(lowerer, expr, info).returnType; +} + +/** The type of a `const` binding whose initializer calls a generic factory + * returning one of its local classes (`const B = base(7)`), or constructs + * such a class (`const b = new B()`); null for any other binding. */ +export function genericLocalClassBinding(lowerer: Lowerer, node: ts.Node): IrType | null { + const decl = node.parent; + if ( + !ts.isIdentifier(node) || + !decl || + !ts.isVariableDeclaration(decl) || + decl.name !== node || + !decl.initializer || + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0 + ) + return null; + // An `any` binding keeps the checked-dynamic representation. + if ((lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0) return null; + let init = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isCallExpression(init)) return genericLocalClassResult(lowerer, init); + if (!ts.isNewExpression(init)) return null; + const constructed = storedClassValueType(lowerer, init.expression); + if (constructed?.kind !== "classval") return null; + const classDecl = lowerer.classes.get(constructed.className)?.decl; + return classDecl && insideGenericFunction(classDecl) + ? { kind: "object", className: constructed.className } + : null; +} + +/** Whether `node` is declared inside a function with type parameters. */ +function insideGenericFunction(node: ts.Node): boolean { + for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) + if (ts.isFunctionLike(parent) && (parent.typeParameters?.length ?? 0) > 0) return true; + return false; +} + +/** The class declared inside a generic function that `type` is the + * constructor or instance type of, or null. */ +function localClassOfResult( + lowerer: Lowerer, + info: GenericFnInfo, + type: ts.Type, +): ts.ClassLikeDeclaration | null { + const symbol = type.getSymbol(); + if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null; + const decl = lowerer.checker.valueDeclarationOf(symbol); + if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null; + for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) + if (parent === info.decl) return decl; + return null; +} + +/** Runs `fn` under an instance's type bindings and local-class names, as + * its body lowers. */ +function withInstanceScope( + lowerer: Lowerer, + info: GenericFnInfo, + inst: GenericInstance, + fn: () => T, +): T { + const prevBindings = lowerer.typeParamBindings; + const prevTsBindings = lowerer.typeParamTsBindings; + const previousPath = lowerer.genericInstantiationPath; + const prevContext = lowerer.instantiationContext; + lowerer.typeParamBindings = inst.bindings; + lowerer.typeParamTsBindings = inst.tsBindings ?? null; + lowerer.genericInstantiationPath = inst.path; + lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`; + lowerer.localClassInstantiations.push({ owner: info.decl, name: inst.name }); + try { + return fn(); + } finally { + lowerer.localClassInstantiations.pop(); + lowerer.typeParamBindings = prevBindings; + lowerer.typeParamTsBindings = prevTsBindings; + lowerer.genericInstantiationPath = previousPath; + lowerer.instantiationContext = prevContext; + } +} + /** The one instance table both instantiation routes share: key identity IS * signature identity, so a call (`identity(1)`) and a pinned VALUE * (`const f: (x: number) => number = identity`) reuse one compiled diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index f3a4830c3..22ab24e63 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -50,6 +50,7 @@ import { implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, + genericLocalClassResult, type GenericFnInfo, } from "./generic-functions.js"; import { @@ -228,6 +229,11 @@ export function storedClassValueType( if (value?.kind === "classval") return value; } } + // A generic factory's own local class has one class per instance. + if (ts.isCallExpression(expression)) { + const local = genericLocalClassResult(lowerer, expression); + if (local?.kind === "classval") return local; + } const type = lowerer.typeOf(expression); const mapped = lowerer.mapTypeOf(type); if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) { diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 57e6c2311..7f3be4d44 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -107,6 +107,7 @@ import { implicitLocalFnInfoOf, implicitLocalFnNodeOf, implicitMethodCallInfersReturn, + genericLocalClassBinding, } from "./generic-functions.js"; import { bindingNeverReassigned, @@ -4913,6 +4914,9 @@ export function lowerVarDecl( ) { type = init.type; } + // `const B = factory(1)` inside a generic instance: the factory's local + // class for the instance the call names. + if (!type && !isLet) type = genericLocalClassBinding(lowerer, decl.name); if (!type) { // The JS declaration fallback (irTypeOf's story): the binding holds // the checked-dynamic kind when even the initializer's type has no diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 2b86d6617..b5cedad7a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -369,6 +369,7 @@ import { lowerGenericFnValue, inferTypeParamBindings, lowerGenericInstance, + genericLocalClassBinding, } from "./generic-functions.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; import type { InstantiationPath } from "./instantiation-path.js"; @@ -1849,6 +1850,9 @@ export class Lowerer { * and erased instances). */ erasingDepth = 0; localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = []; + /** Instance types (by checker type id) of the local classes generic + * factory calls return, and the per-instance class each names. */ + readonly localClassInstanceTypes = new Map(); /** The ts-level twin of typeParamBindings, non-null while a CALL-keyed * instance body lowers: type-parameter symbol → the bound CHECKER type, * consulted where the mapped IrType has already widened away information @@ -2882,7 +2886,11 @@ export class Lowerer { this.mixinTypeContext && this.mixinTypeContext.classNode === decl ? { kind: "object", className: this.mixinTypeContext.className } : null, - localClassInstance: (decl) => { + localClassInstance: (decl, instanceType) => { + // A generic factory's local class, by the instance type a call of + // the factory produced (localClassResultInstance). + const instanceClass = instanceType && this.localClassInstanceTypes.get(instanceType.id); + if (instanceClass) return { kind: "object", className: instanceClass }; // Computed heritage can close over parameters of its factory. // A speculative checker query outside that factory must not lower // the extends expression in the caller's lexical scope. @@ -7962,6 +7970,8 @@ export class Lowerer { if (!mapped) { const argumentsType = jsArgumentsFunctionType(this, t); if (argumentsType !== null) return argumentsType; + const localClass = genericLocalClassBinding(this, node); + if (localClass !== null) return localClass; // The checked-dynamic declaration fallback (dynFallbackType): a // JAVASCRIPT binding of any inference residue, or a TypeScript // binding of genuine checker-`any`, becomes the checked-dynamic diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 89a5d78c1..c6e70577c 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1035,7 +1035,7 @@ export interface TypeMapperCtx { * inside members, self-referential member types), null outside any * mixin context (the type alone cannot name a call site). */ mixinClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null; - localClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null; + localClassInstance?: (decl: ts.ClassLikeDeclaration, instanceType?: ts.Type) => IrType | null; /** Live class protocols retain identity and receiver dispatch rather than * copying an instance into an unrelated record layout. */ classImplementedProtocol?: (type: ts.Type) => boolean; @@ -1950,7 +1950,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } } if (classExprNeedsContext(classDecl)) { - const local = ctx.localClassInstance?.(classDecl); + const local = ctx.localClassInstance?.(classDecl, widened); if (local) contextResolutions++; return local ?? null; } @@ -1989,7 +1989,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } } if (classExprNeedsContext(classDecl)) { - const local = ctx.localClassInstance?.(classDecl); + const ctorSigs = checker.getConstructSignatures(widened); + const local = ctx.localClassInstance?.( + classDecl, + ctorSigs.length === 1 ? checker.getReturnTypeOfSignature(ctorSigs[0]!) : undefined, + ); if (local?.kind === "object") { contextResolutions++; return { kind: "classval", className: local.className }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6051-generic-class-factory-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6051-generic-class-factory-results.ts.json new file mode 100644 index 000000000..fc148dcea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6051-generic-class-factory-results.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6051-generic-class-factory-results.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6051-generic-class-factory-results.ts b/tests/corpus/6051-generic-class-factory-results.ts new file mode 100644 index 000000000..542619946 --- /dev/null +++ b/tests/corpus/6051-generic-class-factory-results.ts @@ -0,0 +1,111 @@ +// Classes extending the result of a generic class factory, and the +// factory's class values and instances: each instantiation of the factory +// has its own class, as each call does in Node. +function base(initial: T) { + return class { + value: T = initial; + read(): T { + return this.value; + } + describe(): string { + return `value=${String(this.value)}`; + } + }; +} + +class Counter extends base(42) { + bump(): number { + this.value += 1; + return this.value; + } +} +const counter = new Counter(); +console.log(counter.value, counter.bump(), counter.read(), counter.describe()); + +class Label extends base("hello") { + shout(): string { + return this.read().toUpperCase(); + } + describe(): string { + return `label ${super.describe()}`; + } +} +const label = new Label(); +console.log(label.shout(), label.describe(), label.value.length); + +const NumberBox = base(7); +const box = new NumberBox(); +console.log(box.read() * 2, box instanceof NumberBox, counter instanceof NumberBox); +console.log(base(1) === base(1), typeof NumberBox); +console.log(Counter.name, Label.name, counter instanceof Counter); + +function pair(first: A, second: B) { + return class { + first: A = first; + second: B = second; + swap(): [B, A] { + return [this.second, this.first]; + } + }; +} +class Entry extends pair("id", 3) { + label: string; + constructor(label: string) { + super(); + this.label = label; + } + render(): string { + const [n, s] = this.swap(); + return `${this.label}:${s}:${n * 10}`; + } +} +console.log(new Entry("row").render(), new Entry("col").first); + +interface Shape { + name: string; + size: number; +} +function model(defaults: S) { + return class { + data: S; + constructor(data?: S) { + this.data = data ?? defaults; + } + get(): S { + return this.data; + } + }; +} +class ShapeModel extends model({ name: "square", size: 2 }) { + area(): number { + return this.data.size * this.data.size; + } +} +const square = new ShapeModel(); +const big = new ShapeModel({ name: "big", size: 5 }); +console.log(square.get().name, square.area(), big.get().name, big.area()); + +function tagged(tag: string, payload: T) { + class Tagged { + tag = tag; + payload = payload; + toString(): string { + return `${this.tag}(${String(this.payload)})`; + } + } + return new Tagged(); +} +const first = tagged("a", 1); +const second = tagged("b", true); +console.log(first.toString(), second.toString(), first.payload + 1, second.payload === true); + +const arrowFactory = (initial: T) => + class { + item: T = initial; + }; +class FromArrow extends arrowFactory("arrow") {} +function wrap(value: U): U { + const Made = base(value); + return new Made().read(); +} +console.log(new FromArrow().item, wrap("w"), wrap(9), wrap([1, 2])); diff --git a/tests/diagnostics/local-class-boundaries.ts b/tests/diagnostics/local-class-boundaries.ts index 5b509931d..a82dfb869 100644 --- a/tests/diagnostics/local-class-boundaries.ts +++ b/tests/diagnostics/local-class-boundaries.ts @@ -7,8 +7,4 @@ privateBrand(); selfName(); derived(); generic(); -function genericFactory(value: T) { - return class Stored { read(): T { return value; } }; -} -genericFactory(7); // End of unsupported local-class forms. diff --git a/tests/harness/__snapshots__/local-class-boundaries.ts.txt b/tests/harness/__snapshots__/local-class-boundaries.ts.txt index e3e27ad95..04dbc336c 100644 --- a/tests/harness/__snapshots__/local-class-boundaries.ts.txt +++ b/tests/harness/__snapshots__/local-class-boundaries.ts.txt @@ -16,11 +16,4 @@ local-class-boundaries.ts:5:29 - error SC1090: function-local classes with stati 4 | function derived() { return class extends Base {}; } 5 | function generic() { return class { read(value: T): T { return value; } }; } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | privateBrand(); - -local-class-boundaries.ts:13:1 - error SC2001: values of type 'typeof Stored' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') - - 12 | } - 13 | genericFactory(7); - | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 14 | // End of unsupported local-class forms. \ No newline at end of file + 6 | privateBrand(); \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 18147374d..b34eeecb0 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2211,6 +2211,7 @@ "6042-regex-match-object-destructuring.ts": 3393, "6043-string-index-narrowed-union.ts": 2983, "605-closures-forward-capture-tdz.ts": 3277, + "6051-generic-class-factory-results.ts": 4363, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5251,6 +5252,7 @@ "6042-regex-match-object-destructuring.ts": 3091, "6043-string-index-narrowed-union.ts": 3627, "605-closures-forward-capture-tdz.ts": 4638, + "6051-generic-class-factory-results.ts": 46581, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From d3bfa6932303a052bf62b956c9d376e6dc0c7c14 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:09:52 -0700 Subject: [PATCH 066/120] Coerce the value passed to a JavaScript setter that overrides Error.name or message A JavaScript class that extends Error and declares `set name(v)` or `set message(v)` (quickjs-wasi's JSException) made the Error property write dispatcher, which exists once any program writes `.name` or `.message` on an Error, pass a string where the untyped setter takes a dynamic value. IR validation failed with SC9001 "expected dyn, got string". The dispatcher now coerces the value to the setter's parameter type, which leaves TypeScript setters (string parameters) unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/error-methods.ts | 8 +++- .../error-js-accessor-name-message.js.json | 6 +++ .../corpus/error-js-accessor-name-message.js | 45 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/error-js-accessor-name-message.js.json create mode 100644 tests/corpus/error-js-accessor-name-message.js diff --git a/packages/compiler/src/frontend/lowering/error-methods.ts b/packages/compiler/src/frontend/lowering/error-methods.ts index bcbbe13a4..979cf4814 100644 --- a/packages/compiler/src/frontend/lowering/error-methods.ts +++ b/packages/compiler/src/frontend/lowering/error-methods.ts @@ -315,7 +315,13 @@ export function refreshErrorPropertyDispatch( info.def.name, member, value, - [varRef("value", STRING, loc)], + [ + // A JavaScript setter takes its value untyped. + lowerer.coerceToExpected( + varRef("value", STRING, loc), + accessor.sig.params[0]?.type ?? STRING, + ), + ], VOID, loc, ), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/error-js-accessor-name-message.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/error-js-accessor-name-message.js.json new file mode 100644 index 000000000..e4c388b08 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/error-js-accessor-name-message.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/error-js-accessor-name-message.js" + ], + "diags": [] +} diff --git a/tests/corpus/error-js-accessor-name-message.js b/tests/corpus/error-js-accessor-name-message.js new file mode 100644 index 000000000..77b8668fa --- /dev/null +++ b/tests/corpus/error-js-accessor-name-message.js @@ -0,0 +1,45 @@ +// A JavaScript Error subclass that overrides name and message with accessor +// pairs, as quickjs-wasi's JSException does. Writing `.name` or `.message` +// on an ordinary Error builds the Error property dispatchers, whose branch +// for this class calls the JavaScript setters with an untyped value. +class GuestException extends Error { + #name; + #message; + constructor(name, message) { + super(); + this.#name = name; + this.#message = message; + } + get name() { + return this.#name; + } + set name(v) { + this.#name = v; + } + get message() { + return this.#message; + } + set message(v) { + this.#message = `[${v}]`; + } +} + +const guest = new GuestException("GuestError", "boom"); +console.log(guest.name, guest.message, String(guest)); +guest.name = "Renamed"; +guest.message = "changed"; +console.log(guest.name, guest.message, String(guest)); + +const plain = new Error("plain"); +plain.name = "PlainName"; +plain.message = "plain message"; +console.log(plain.name, plain.message, String(plain)); + +/** @type {Error[]} */ +const errors = [plain, guest, new TypeError("bad")]; +for (const err of errors) console.log(String(err)); +try { + throw guest; +} catch (caught) { + console.log(caught instanceof GuestException, caught instanceof Error, String(caught)); +} From 4b78531ab46fbed9fa6af1600489dcc4cbe46ddb Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:09:53 -0700 Subject: [PATCH 067/120] Size handle tables by a fixed capacity and let handles cross threads Appending a ScrDynHandleTag used to grow the always-linked scr_dynh_ops table, which shifted .bss and code addresses in every program, including programs that never box the new handle (Blob and File paid this when they were added). The table is now sized by SCR_DYNH_CAPACITY (128; 52 tags are in use), so a link-gated unit can add a tag without changing any other program's executable. This change is the one layout shift that makes later tags free. Structured clone between threads (postMessage, workerData) rejected every handle other than MessagePorts and buffers. A handle's ops can now carry a ScrDynHandleTransfer (share on the sender, adopt on the receiver, drop with the message), which scr_message.c uses for any tag, so a gated unit such as a future WebAssembly.Module needs no reference from the message encoder and no per-tag registry. No handle declares the hooks yet; scr_message.test.c exercises them across threads through a test handle in the last capacity slot. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_dyn.h | 29 ++++++ packages/runtime/src/scr_json.c | 2 +- packages/runtime/src/scr_message.c | 22 ++++- packages/runtime/src/scr_message.test.c | 111 +++++++++++++++++++++++ packages/runtime/src/scr_message.test.ts | 3 +- 5 files changed, 164 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 667ceced0..24f3578c3 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -174,6 +174,17 @@ typedef enum { SCR_DYNH_COUNT, } ScrDynHandleTag; +/* Every per-tag table (scr_dynh_ops in scr_json.c, the message clone hooks + * in scr_message.c) is sized by this fixed capacity, never by + * SCR_DYNH_COUNT. Appending a tag for a link-gated unit therefore changes + * no always-linked storage or code: a program that never boxes the new + * handle links byte-identical objects. Raising the capacity is a + * program-wide layout change; do it in a change of its own, measured as + * neutral, and only when the enum outgrows it. */ +#define SCR_DYNH_CAPACITY 128 +_Static_assert(SCR_DYNH_COUNT <= SCR_DYNH_CAPACITY, + "ScrDynHandleTag outgrew SCR_DYNH_CAPACITY: raise it in its own change"); + typedef struct ScrDyn ScrDyn; typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */ typedef struct ScrClosure ScrClosure; /* full definition below (C11 repeat) */ @@ -1162,7 +1173,25 @@ typedef struct ScrDynHandleOps { * lookup): +1 value, or NULL when the class has no member for `key` * (the read answers undefined). NULL hook = no symbol-keyed members. */ ScrDyn *(*symbol_get)(void *h, const ScrDyn *key); + /* Cross-thread structured clone (postMessage, workerData); NULL = + * DataCloneError. See ScrDynHandleTransfer. */ + const struct ScrDynHandleTransfer *transfer; } ScrDynHandleOps; + +/* How a handle crosses threads, supplied by the handle's owning unit so the + * message encoder (scr_message.c) needs no reference to gated units and no + * per-tag registry. share runs on the sending thread: it answers a payload + * that any thread may use, +1, or NULL when this handle cannot be cloned + * (with an exception pending, or DataCloneError is thrown). adopt runs on + * the receiving thread and answers a fresh box over the payload (+1; the + * message keeps its payload reference). drop releases the payload when the + * message is freed, whether or not it was delivered. All three must be + * callable from any thread. */ +typedef struct ScrDynHandleTransfer { + void *(*share)(void *h); + ScrDyn *(*adopt)(void *payload); + void (*drop)(void *payload); +} ScrDynHandleTransfer; ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection); /* ── promises in the checked-dynamic tree (SCR_DYN_PROMISE) ──────────────────────────── diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b23e78241..ab4a10dd8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3649,7 +3649,7 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) { * always-linked core never references gated units. A missing tag at use * is an internal error: emitted programs install a unit's ops whenever * they can box its handles. */ -static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_COUNT]; +static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_CAPACITY]; /* BroadcastChannel's EventTarget table (scr_et_broadcast_install). */ SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops = NULL; diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 82428a6fe..7c969a2e3 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -23,6 +23,8 @@ typedef enum { SCR_MESSAGE_TYPED_REF, /* A transferred MessagePort: its channel end (+1), the side in number. */ SCR_MESSAGE_PORT, + /* A native handle cloned through its unit's ScrDynHandleTransfer. */ + SCR_MESSAGE_HANDLE, } ScrMessageKind; const ScrMessagePortOps *scr_message_port_ops = NULL; @@ -49,6 +51,9 @@ typedef struct { bool data_view; ScrDyn *published; /* SCR_MESSAGE_PUBLISHED: immortal root, owns nothing */ ScrChannel *channel; /* SCR_MESSAGE_PORT */ + /* SCR_MESSAGE_HANDLE: the owning unit's hooks and its payload (+1). */ + const ScrDynHandleTransfer *transfer; + void *payload; /* SCR_MESSAGE_TYPED_REF: the capsule's process-wide adapters and type key * (vtables, adapters and type keys are shared by every thread). */ struct { @@ -389,7 +394,20 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) break; } bool buffer = scr_buffer_storage_is(value); - if (value->kind == SCR_DYN_HANDLE && !buffer) goto unsupported; + if (value->kind == SCR_DYN_HANDLE && !buffer) { + const ScrDynHandleTransfer *transfer = scr_dyn_handle_ops_of(value)->transfer; + if (!transfer) goto unsupported; + void *payload = transfer->share(value->v.handle.ptr); + if (!payload) { + if (scr_exc_pending()) return SIZE_MAX; + goto unsupported; + } + node = &encoder->message->nodes[id]; + node->kind = SCR_MESSAGE_HANDLE; + node->transfer = transfer; + node->payload = payload; + break; + } const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes; size_t storage = scr_message_storage(encoder, bytes); /* Adding the storage node can reallocate the node array. */ @@ -765,6 +783,7 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { case SCR_MESSAGE_PORT: values[i] = scr_message_port_ops->adopt(node->channel, (int)node->number); break; + case SCR_MESSAGE_HANDLE: values[i] = node->transfer->adopt(node->payload); break; } if (!values[i] || scr_exc_pending()) goto done; } @@ -820,6 +839,7 @@ void scr_message_free(ScrMessage *message) { free(node->data); scr_shared_release(node->shared); if (node->kind == SCR_MESSAGE_PORT) scr_message_port_ops->release(node->channel); + if (node->kind == SCR_MESSAGE_HANDLE) node->transfer->drop(node->payload); } free(message->nodes); free(message); diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index 4e8aa7d62..0c6bd7ae3 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -172,6 +172,115 @@ static void check_documents(void) { scr_message_free(message); } +/* A link-gated unit's handle crosses threads through the transfer hooks in + * its ops: share on the sender, adopt on the receiver, drop with the + * message. The test borrows the last capacity slot, which no unit uses, so + * it also proves that slots past SCR_DYNH_COUNT work. */ +#define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1)) +typedef struct { + _Atomic size_t rc; + int value; +} TransferBox; +static _Atomic int transfer_shares, transfer_adopts, transfer_drops; +static void *transfer_retain(void *h) { + ((TransferBox *)h)->rc++; + return h; +} +static void transfer_release(void *h) { + if (--((TransferBox *)h)->rc == 0) free(h); +} +static void *transfer_share(void *h) { + TransferBox *source = h; + if (source->value < 0) return NULL; + transfer_shares++; + TransferBox *copy = malloc(sizeof *copy); + copy->rc = 1; + copy->value = source->value; + return copy; +} +static ScrDyn *transfer_adopt(void *payload) { + transfer_adopts++; + return scr_dyn_new_handle(payload, TRANSFER_TAG); +} +static void transfer_drop(void *payload) { + transfer_drops++; + transfer_release(payload); +} +static const ScrDynHandleTransfer transfer_hooks = {transfer_share, transfer_adopt, transfer_drop}; +static const ScrDynHandleOps transfer_ops = { + .cls = "Transferable", + .retain = transfer_retain, + .release = transfer_release, + .transfer = &transfer_hooks, +}; +static const ScrDynHandleOps opaque_ops = { + .cls = "Opaque", + .retain = transfer_retain, + .release = transfer_release, +}; + +static void *receive_handles(void *data) { + scr_context_enter(1); + scr_init(); + scr_lib_init(0, NULL); + scr_dyn_handle_install(TRANSFER_TAG, &transfer_ops); + ScrDyn *root = scr_message_decode(data); + assert(root && !scr_exc_pending()); + ScrDyn *first = scr_dyn_obj_get(root, "first", 5); + assert(first->kind == SCR_DYN_HANDLE && first->v.handle.tag == TRANSFER_TAG); + assert(((TransferBox *)first->v.handle.ptr)->value == 7); + /* One handle reached twice decodes to one box. */ + assert(first == scr_dyn_obj_get(root, "again", 5)); + assert(transfer_adopts == 1); + scr_dyn_release(root); + scr_collect_cycles(); + scr_context_cleanup(); + return NULL; +} + +static ScrDyn *transfer_handle(int value, const ScrDynHandleOps *ops) { + scr_dyn_handle_install(TRANSFER_TAG, ops); + TransferBox *box = malloc(sizeof *box); + box->rc = 1; + box->value = value; + ScrDyn *handle = scr_dyn_new_handle(box, TRANSFER_TAG); + transfer_release(box); + return handle; +} + +static void check_handle_transfer(void) { + ScrDyn *root = scr_dyn_new_obj(); + ScrDyn *handle = transfer_handle(7, &transfer_ops); + scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(handle)); + scr_dyn_obj_set(root, "again", 5, handle); + ScrMessage *message = scr_message_encode(root); + assert(message && !scr_exc_pending()); + assert(transfer_shares == 1); + scr_dyn_release(root); + pthread_t receiver; + assert(pthread_create(&receiver, NULL, receive_handles, message) == 0); + assert(pthread_join(receiver, NULL) == 0); + assert(transfer_drops == 0); + scr_message_free(message); + assert(transfer_drops == 1); + + /* A handle whose unit declines (share answers NULL) and a handle with no + * transfer hooks both throw DataCloneError, releasing what was shared. */ + root = scr_dyn_new_arr(); + scr_dyn_arr_push(root, transfer_handle(1, &transfer_ops)); + scr_dyn_arr_push(root, transfer_handle(-1, &transfer_ops)); + assert(!scr_message_encode(root) && scr_exc_pending()); + scr_exc_clear(); + assert(transfer_shares == 2 && transfer_drops == 2); + scr_dyn_release(root); + root = transfer_handle(3, &opaque_ops); + assert(!scr_message_encode(root) && scr_exc_pending()); + scr_exc_clear(); + assert(transfer_shares == 2); + scr_dyn_release(root); + scr_collect_cycles(); +} + int main(void) { scr_init(); scr_lib_init(0, NULL); @@ -188,6 +297,8 @@ int main(void) { check_getters(); check_rejection(); check_documents(); + check_handle_transfer(); scr_context_cleanup(); puts("message graphs preserve identity, sparse values, byte aliases and getter order"); + puts("native handles cross threads through their transfer hooks"); } diff --git a/packages/runtime/src/scr_message.test.ts b/packages/runtime/src/scr_message.test.ts index b2364a5d8..56b35f1ac 100644 --- a/packages/runtime/src/scr_message.test.ts +++ b/packages/runtime/src/scr_message.test.ts @@ -34,7 +34,8 @@ test.skipIf(process.platform === "win32")( env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, }); expect(result.stdout).toBe( - "message graphs preserve identity, sparse values, byte aliases and getter order\n", + "message graphs preserve identity, sparse values, byte aliases and getter order\n" + + "native handles cross threads through their transfer hooks\n", ); expect(result.stderr).toBe(""); } finally { From c7f3fa6564cc8a2f19b41cf5b21b7c551f88c949 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:09:59 -0700 Subject: [PATCH 068/120] Type-check programs that mention WebAssembly and refuse its API precisely Packages whose declarations name WebAssembly types (quickjs-wasi, photon-node, and therefore pi) failed scriptc's type check with SC0001 "Cannot find namespace 'WebAssembly'", because scriptc's ES-only type world never loads lib.dom, so `scriptc coverage` could not analyze them. - scriptc.d.ts declares lib.dom's WebAssembly namespace and BufferSource (admitting every ArrayBufferView, so Node's Buffer types fit). - The type mapper gives every type declared in the namespace, and unions over one, the checked-dynamic representation, so declarations that only mention them compile. - Every reached use of the API value refuses at its site with a named SC2020 diagnostic: `WebAssembly.` reads and calls, `new WebAssembly.`, `instanceof WebAssembly.`, the bare namespace, `globalThis.WebAssembly`, and the global object viewed through a cast to a type with a WebAssembly member (pi codemode's spelling). TypeScript sources fail the build; JavaScript sources get a located runtime fence. - `typeof WebAssembly` and `typeof WebAssembly.` need no value and fold to Node's answers ("object", "function"), including inside `typeof x === "..."` tests, so feature probes in JavaScript glue behave as in Node. Before, a JavaScript probe answered "string" (the identity token) and silently took the non-wasm branch. - The surface manifest projects the refusal (stdlib.webassembly), and the compatibility ledger's `WebAssembly` global row moves from not-implemented to refused; no other row changes. Co-Authored-By: Claude Opus 5.5 --- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 10 +- .../generated/node-v24-backlog.json | 14 +- .../generated/node-v24-internal.json | 8 +- internal/compatibility/src/generate.mjs | 4 + packages/compiler/ambient/scriptc.d.ts | 134 ++++++++++++++++++ .../compiler/src/coverage/surface-manifest.ts | 15 +- .../compiler/src/diagnostics/diagnostic.ts | 14 ++ .../frontend/lowering/builtins/webassembly.ts | 132 +++++++++++++++++ .../src/frontend/lowering/lower-exprs.ts | 25 ++++ .../compiler/src/frontend/lowering/lowerer.ts | 3 + packages/compiler/src/frontend/type-mapper.ts | 26 ++++ packages/compiler/surface-manifest.json | 8 ++ .../corpus/webassembly-feature-probe.js.json | 6 + .../corpus/webassembly-type-mentions.ts.json | 6 + .../diagnostics/webassembly-fences.ts.json | 6 + tests/corpus/webassembly-feature-probe.js | 16 +++ tests/corpus/webassembly-type-mentions.ts | 22 +++ tests/diagnostics/webassembly-fences.ts | 30 ++++ .../__snapshots__/webassembly-fences.ts.txt | 98 +++++++++++++ 20 files changed, 561 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/webassembly.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-feature-probe.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-type-mentions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webassembly-fences.ts.json create mode 100644 tests/corpus/webassembly-feature-probe.js create mode 100644 tests/corpus/webassembly-type-mentions.ts create mode 100644 tests/diagnostics/webassembly-fences.ts create mode 100644 tests/harness/__snapshots__/webassembly-fences.ts.txt diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index a64169933..fd7701ca4 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "a610bdd1ae56e1c4cd38", + "artifactVersion": "225ed9018e6eb1724a8c", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index d4b6839e7..c3cac7ead 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -330,8 +330,8 @@ "static": { "supported": 0, "partial": 12, - "refused": 0, - "not-implemented": 80, + "refused": 1, + "not-implemented": 79, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -36590,9 +36590,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index f36c865cf..ec0f9416b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "audit-partial": 1155, - "verify-gap": 2174, - "replace-refusal": 40, + "verify-gap": 2173, + "replace-refusal": 41, "classify": 2, "implement": 85 } @@ -34178,11 +34178,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:stdlib.webassembly", "tests": [] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 514282a0a..a1802baa6 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -342,8 +342,8 @@ "static": { "supported": 0, "partial": 12, - "refused": 0, - "not-implemented": 80, + "refused": 1, + "not-implemented": 79, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -53521,8 +53521,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "refused", + "evidence": "surface-manifest:unsupported:stdlib.webassembly" }, "anchorSource": "exact" }, diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index dfefc6e48..3433e14f9 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -203,6 +203,10 @@ function featureOf(row, entries, parentSignature) { function symbolCandidates(row) { const result = new Set([row.apiSymbol]); + // A global's own row (`WebAssembly`) matches the compiler's + // `globalThis.` entries. + if (row.chapter === "globals" && row.apiSymbol && !row.apiSymbol.includes(".")) + result.add(`globalThis.${row.apiSymbol}`); const aliases = [ ["fsPromises.", "fs/promises."], ["dnsPromises.", "dns/promises."], diff --git a/packages/compiler/ambient/scriptc.d.ts b/packages/compiler/ambient/scriptc.d.ts index 424e49065..cec26f139 100644 --- a/packages/compiler/ambient/scriptc.d.ts +++ b/packages/compiler/ambient/scriptc.d.ts @@ -13,6 +13,9 @@ * they MERGE as overloads instead of colliding — the Timeout handle maps * to the numeric timer id either way, and .unref()/.ref()/.hasRef() are * loop-liveness bookkeeping). + * 4. The WebAssembly namespace and BufferSource, which the DOM library + * supplies in ordinary TypeScript projects (types only; every reached + * use of the API is refused at its site). * * This file ships to EVERY program scriptc builds — the lowering program * and preflight's project-world second-chance program alike (a project @@ -108,3 +111,134 @@ declare function clearTimeout(handle?: Timeout | number | null | undefined): voi * baked result is exactly what Node would compute at runtime (the * differential harness verifies this with a `comptime = (f) => f()` shim). */ declare function comptime(compute: () => T): T; + +/* The JavaScript WebAssembly API, as the DOM library declares it (scriptc's + * ES-only type world never loads lib.dom, and @types/node leaves it to + * lib.dom). Packages whose declarations name these types (quickjs-wasi, + * photon-node, ...) typecheck against them. These are types only: the + * type mapper gives every type declared here the checked-dynamic + * representation, and the lowerer refuses each reached use of the API with + * a located diagnostic (frontend/lowering/builtins/webassembly.ts). + * BufferSource admits every ArrayBufferView, as lib.dom did before + * TypeScript 5.7: Node's Buffer types are views over ArrayBufferLike. */ +type BufferSource = ArrayBufferView | ArrayBuffer; +declare namespace WebAssembly { + interface CompileError extends Error {} + var CompileError: { + prototype: CompileError; + new (message?: string): CompileError; + (message?: string): CompileError; + }; + interface Global { + value: ValueTypeMap[T]; + valueOf(): ValueTypeMap[T]; + } + var Global: { + prototype: Global; + new ( + descriptor: GlobalDescriptor, + v?: ValueTypeMap[T], + ): Global; + }; + interface Instance { + readonly exports: Exports; + } + var Instance: { + prototype: Instance; + new (module: Module, importObject?: Imports): Instance; + }; + interface LinkError extends Error {} + var LinkError: { + prototype: LinkError; + new (message?: string): LinkError; + (message?: string): LinkError; + }; + interface Memory { + readonly buffer: ArrayBuffer; + grow(delta: number): number; + } + var Memory: { + prototype: Memory; + new (descriptor: MemoryDescriptor): Memory; + }; + interface Module {} + var Module: { + prototype: Module; + new (bytes: BufferSource): Module; + customSections(moduleObject: Module, sectionName: string): ArrayBuffer[]; + exports(moduleObject: Module): ModuleExportDescriptor[]; + imports(moduleObject: Module): ModuleImportDescriptor[]; + }; + interface RuntimeError extends Error {} + var RuntimeError: { + prototype: RuntimeError; + new (message?: string): RuntimeError; + (message?: string): RuntimeError; + }; + interface Table { + readonly length: number; + get(index: number): any; + grow(delta: number, value?: any): number; + set(index: number, value?: any): void; + } + var Table: { + prototype: Table; + new (descriptor: TableDescriptor, value?: any): Table; + }; + interface GlobalDescriptor { + mutable?: boolean; + value: T; + } + interface MemoryDescriptor { + initial: number; + maximum?: number; + shared?: boolean; + } + interface ModuleExportDescriptor { + kind: ImportExportKind; + name: string; + } + interface ModuleImportDescriptor { + kind: ImportExportKind; + module: string; + name: string; + } + interface TableDescriptor { + element: TableKind; + initial: number; + maximum?: number; + } + interface ValueTypeMap { + anyfunc: Function; + externref: any; + f32: number; + f64: number; + i32: number; + i64: bigint; + v128: never; + } + interface WebAssemblyInstantiatedSource { + instance: Instance; + module: Module; + } + type ImportExportKind = "function" | "global" | "memory" | "table"; + type TableKind = "anyfunc" | "externref"; + type ExportValue = Function | Global | Memory | Table; + type Exports = Record; + type ImportValue = ExportValue | number; + type Imports = Record; + type ModuleImports = Record; + type ValueType = keyof ValueTypeMap; + function compile(bytes: BufferSource): Promise; + function compileStreaming(source: Response | PromiseLike): Promise; + function instantiate( + bytes: BufferSource, + importObject?: Imports, + ): Promise; + function instantiate(moduleObject: Module, importObject?: Imports): Promise; + function instantiateStreaming( + source: Response | PromiseLike, + importObject?: Imports, + ): Promise; + function validate(bytes: BufferSource): boolean; +} diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 45814cb8b..5728c41f2 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -38,7 +38,7 @@ import { InternalCompilerError } from "../errors.js"; * harness asserts refusals match entry codes. renderSurfaceManifest is * byte-deterministic: entries sort by id, keys are emitted in one fixed * order, and the output carries no timestamps or absolute paths. */ -import { FENCE_CODES, UNSUPPORTED } from "../diagnostics/diagnostic.js"; +import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js"; import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js"; import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js"; import { @@ -444,6 +444,19 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes } } + // ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─ + { + const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 }); + add({ + id: "stdlib.webassembly", + kind: "stdlib", + name: "globalThis.WebAssembly", + status: "unsupported", + code: refusal.code, + ...(refusal.hint !== undefined ? { note: refusal.hint } : {}), + }); + } + // ── fetch/Web platform: one explicit, versioned compatibility profile ─ const fetchTarget = `Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` + diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index eaae1b531..bbb4667e9 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -751,6 +751,20 @@ export function noLoweringDiag( }; } +/** A REACHED use of the WebAssembly API (SC2020): scriptc.d.ts declares + * its types so packages that mention them typecheck, but no module + * compiles to native code yet. `surface` names what was used + * ('WebAssembly.compile', 'new WebAssembly.Module', 'globalThis.WebAssembly'). */ +export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic { + return { + code: "SC2020", + message: `'${surface}' is not supported yet: scriptc does not compile WebAssembly modules to native code yet`, + loc, + milestone: "later", + hint: "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)", + }; +} + /** A resolved native addon needs Node's embedding ABI. The migration uses the * underlying native operation through an ordinary C ABI declaration. */ export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic { diff --git a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts new file mode 100644 index 000000000..dc61853f8 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts @@ -0,0 +1,132 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer, PoisonError } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js"; +import { STRING, type IrExpr } from "../../../ir/ir.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; + +/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's + * namespace) so that packages whose declarations mention them typecheck, + * and the type mapper gives those types the checked-dynamic representation. + * No module compiles to native code yet, so every reached use of the API + * value fences at its expression with webAssemblyDiag: a build error in + * TypeScript sources, a located runtime fence in JavaScript ones. typeof + * needs no value and folds to Node's answer. */ + +/** Strips the spellings that leave a value unchanged: parentheses, type + * assertions, `satisfies` and non-null assertions. */ +function bare(expr: ts.Expression): ts.Expression { + for (;;) { + if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) expr = expr.expression; + else if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) expr = expr.expression; + else if (ts.isSatisfiesExpression(expr)) expr = expr.expression; + else return expr; + } +} + +/** The global object itself, as `globalThis` or `global` spells it. */ +function isGlobalThis(lowerer: Lowerer, expr: ts.Expression): boolean { + const node = bare(expr); + return ( + ts.isIdentifier(node) && + (node.text === "globalThis" || node.text === "global") && + stdlibGlobalNameOf(lowerer, node) === "globalThis" + ); +} + +/** THE WebAssembly namespace value: `WebAssembly` or `globalThis.WebAssembly` + * (and the global object reached through a cast, as in + * `(globalThis as any).WebAssembly`). */ +function isWebAssemblyGlobal(lowerer: Lowerer, expr: ts.Expression): boolean { + expr = bare(expr); + if (ts.isIdentifier(expr)) { + return expr.text === "WebAssembly" && stdlibGlobalNameOf(lowerer, expr) === "WebAssembly"; + } + if (ts.isPropertyAccessExpression(expr)) { + return expr.name.text === "WebAssembly" && isGlobalThis(lowerer, expr.expression); + } + if (ts.isElementAccessExpression(expr)) { + const key = expr.argumentExpression; + return ( + (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) && + key.text === "WebAssembly" && + isGlobalThis(lowerer, expr.expression) + ); + } + return false; +} + +/** The surface a WebAssembly API expression names ('WebAssembly', + * 'globalThis.WebAssembly', 'WebAssembly.compile'), or null. A cheap + * spelling check runs first: almost every expression is rejected without a + * checker query. */ +function webAssemblySurface(lowerer: Lowerer, expr: ts.Expression): string | null { + // `globalThis as unknown as { WebAssembly: ... }`: the cast's target type + // names the member, and the value it checks is the global object's + // WebAssembly (the spelling pi's codemode uses). + if ( + (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) && + isGlobalThis(lowerer, expr.expression) + ) { + const target = lowerer.checker.getTypeAtLocation(expr.type); + return lowerer.checker.getPropertyOfType(target, "WebAssembly") !== undefined + ? "globalThis.WebAssembly" + : null; + } + const node = bare(expr); + if (ts.isIdentifier(node)) { + return node.text === "WebAssembly" && isWebAssemblyGlobal(lowerer, node) ? "WebAssembly" : null; + } + if (ts.isPropertyAccessExpression(node)) { + if (node.name.text === "WebAssembly") { + return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null; + } + // WebAssembly., and members of members + // (WebAssembly.Module.customSections). + const names = [node.name.text]; + let receiver = bare(node.expression); + while (ts.isPropertyAccessExpression(receiver) && receiver.name.text !== "WebAssembly") { + names.unshift(receiver.name.text); + receiver = bare(receiver.expression); + } + const spelled = + (ts.isIdentifier(receiver) && receiver.text === "WebAssembly") || + (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "WebAssembly"); + return spelled && isWebAssemblyGlobal(lowerer, receiver) + ? `WebAssembly.${names.join(".")}` + : null; + } + if (ts.isElementAccessExpression(node)) { + return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null; + } + return null; +} + +/** Fences a reached use of the WebAssembly API. `prefix` qualifies a + * construction ('new '). Returns normally when `expr` is anything else. */ +export function refuseWebAssemblyUse(lowerer: Lowerer, expr: ts.Expression, prefix = ""): void { + const surface = webAssemblySurface(lowerer, expr); + if (surface === null) return; + lowerer.pushDiag(webAssemblyDiag(`${prefix}${surface}`, locOf(expr))); + throw new PoisonError(); +} + +/** `typeof WebAssembly` is "object" and `typeof WebAssembly.` is + * "function" for every member the namespace declares, as in Node. The + * operand is an effect-free global read, so nothing else evaluates. */ +export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression): IrExpr | null { + const surface = webAssemblySurface(lowerer, operand); + if (surface === null) return null; + const loc = locOf(operand); + if (surface === "WebAssembly" || surface === "globalThis.WebAssembly") { + return { kind: "strLit", value: "object", type: STRING, loc }; + } + const t = lowerer.typeOf(operand); + if ( + lowerer.checker.getCallSignatures(t).length > 0 || + lowerer.checker.getConstructSignatures(t).length > 0 + ) { + return { kind: "strLit", value: "function", type: STRING, loc }; + } + return null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 09296bdfe..2006486bf 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -11,6 +11,7 @@ import { lowerWorkerMetadata } from "./builtins/workers.js"; import { esmImportBindingKind } from "./builtins/module-bindings.js"; import { lowerReadlineProperty } from "./builtins/readline.js"; import { lowerBlobInstanceof } from "./builtins/blob.js"; +import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerKeyObjectInstanceof } from "./builtins/key-object.js"; import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js"; import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js"; @@ -444,6 +445,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (externalTypeSpecifier !== null) { lowerer.externalHostFence(externalTypeSpecifier, expr); } + // Every reached use of the WebAssembly API fences at its expression. + refuseWebAssemblyUse(lowerer, expr); if (ts.isNumericLiteral(expr)) { const value = Number(expr.text.replace(/_/g, "")); @@ -609,6 +612,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ); } if (ts.isTypeOfExpression(expr)) { + const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression); + if (webAssembly) return webAssembly; if (lowerer.isStdlibGlobal(expr.expression, "process")) return { kind: "strLit", value: "object", type: STRING, loc }; if (lowerer.isStdlibGlobal(expr.expression, "fetch")) @@ -10264,6 +10269,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr op === ts.SyntaxKind.ExclamationEqualsEqualsToken ) { const test = + lowerWebAssemblyTypeofTest(lowerer, expr, loc) ?? lowerErrorCodeTypeofTest(lowerer, expr, loc) ?? lowerer.lowerCaughtTypeofTest(expr, loc) ?? lowerDynTypeofTest(lowerer, expr, loc) ?? @@ -11656,6 +11662,24 @@ function errorRootedObjectOf(lowerer: Lowerer, t: ts.Type): IrType | null { return null; } +/** `typeof WebAssembly === "object"` and `typeof WebAssembly. === + * "function"`: the operand needs no value, so the test folds as bare typeof + * does (builtins/webassembly.ts) before any dyn-typed operand lowers. */ +function lowerWebAssemblyTypeofTest( + lowerer: Lowerer, + expr: ts.BinaryExpression, + loc: SrcLoc, +): IrExpr | null { + const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken; + for (const [a, b] of operandPairs(expr)) { + if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue; + const folded = lowerWebAssemblyTypeof(lowerer, a.expression); + if (folded?.kind !== "strLit") continue; + return { kind: "boolLit", value: (folded.value === b.text) !== negated, type: BOOL, loc }; + } + return null; +} + /** `typeof e.code === "string"` / `"undefined"` (and the `!==` forms) * where e — through parens and as-casts — is a catch binding or a value * of %Error-rooted type: the runtime error's code slot is a string @@ -12353,6 +12377,7 @@ export function caughtLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal | * Folding is limited to side-effect-free operands — dropping a computed * operand would skip its effects, so those are rejected instead. */ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr { + refuseWebAssemblyUse(lowerer, expr.right); // `x instanceof net.Socket` over a union with a netSocket arm — the // h2 compat 'connect' narrowing (lower-server.ts): a union tag test. const sockTest = lowerSocketInstanceOf(lowerer, expr, loc); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 145fb3ae6..f341fb7f0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -26,6 +26,7 @@ import { isSettledIteratorValueRead } from "./lower-generators.js"; import { concreteCollectionNarrow } from "./collection-narrowing.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js"; +import { refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; import { hashStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; @@ -11255,6 +11256,7 @@ export class Lowerer { // dispatch. The general identifier-call paths have no lowering for a // promise-returning ambient global, and `import` is a keyword callee no // identifier path matches. + refuseWebAssemblyUse(this, expr.expression); return ( lowerLifetimeRegionCall(this, expr) ?? lowerInternTableCall(this, expr) ?? @@ -11480,6 +11482,7 @@ export class Lowerer { } lowerNew(expr: ts.NewExpression): IrExpr { + refuseWebAssemblyUse(this, expr.expression, "new "); const builtin = lowerLifetimeRegionNew(this, expr) ?? lowerInternTableNew(this, expr) ?? diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f37703fd8..c7b2e08fa 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -919,6 +919,24 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean return false; } +/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts): + * its modules, instances, memories, tables, globals, error classes and the + * Exports/Imports records. */ +export function isWebAssemblyType( + type: ts.Type, + ctx: Pick, +): boolean { + const symbol = type.getAliasSymbol() ?? type.getSymbol(); + if (!symbol) return false; + return ctx.checker + .declarationsOf(symbol) + .some( + (d) => + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), + ); +} + /** True when a type contains a record anywhere (the trigger for the * shape-mismatch diagnostic SC2002 — non-record mismatches are tsc's or * the validator's business). */ @@ -1418,6 +1436,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return { kind: "object", className: "%Error" }; } if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN; + // The WebAssembly API's values have the checked-dynamic representation, so + // declarations that merely mention them compile; each reached use of the + // API itself is refused at its site (builtins/webassembly.ts). + if (isWebAssemblyType(type, ctx)) return DYN; + // A union over one of them (quickjs-wasi's `BufferSource | WebAssembly.Module` + // option) holds that dynamic value too, so the whole union is dynamic. + if (type.isUnionType() && ts.constituentTypes(type).some((t) => isWebAssemblyType(t, ctx))) + return DYN; // Immutable (@scriptc/types) is a deep-readonly view: the value is // still the T it maps. const immutableSource = immutableSourceType(type, ctx); diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index f5313f12d..4fa9bbcc3 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -5573,6 +5573,14 @@ "status": "static", "note": "the lowered call form takes no arguments" }, + { + "id": "stdlib.webassembly", + "kind": "stdlib", + "name": "globalThis.WebAssembly", + "status": "unsupported", + "code": "SC2020", + "note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)" + }, { "id": "syntax.async-generators", "kind": "syntax", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-feature-probe.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-feature-probe.js.json new file mode 100644 index 000000000..70988ec5f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-feature-probe.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/webassembly-feature-probe.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-type-mentions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-type-mentions.ts.json new file mode 100644 index 000000000..b86f1bd8a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/webassembly-type-mentions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/webassembly-type-mentions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webassembly-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webassembly-fences.ts.json new file mode 100644 index 000000000..feb8cb469 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webassembly-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/webassembly-fences.ts" + ], + "diags": [] +} diff --git a/tests/corpus/webassembly-feature-probe.js b/tests/corpus/webassembly-feature-probe.js new file mode 100644 index 000000000..7a164fd90 --- /dev/null +++ b/tests/corpus/webassembly-feature-probe.js @@ -0,0 +1,16 @@ +// @deferred-fences: 2 +// A JavaScript dependency's WebAssembly glue: the feature probe answers as +// in Node, and the loader that would instantiate a module is never reached +// (reaching it would throw scriptc's located WebAssembly fence). +function supportsWasm() { + return typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function"; +} + +async function instantiate(bytes, imports) { + const { instance } = await WebAssembly.instantiate(bytes, imports); + return instance.exports; +} + +const backend = process.argv.includes("--wasm") ? instantiate : null; +console.log("supports wasm:", supportsWasm()); +console.log("backend:", backend === null ? "javascript" : "wasm"); diff --git a/tests/corpus/webassembly-type-mentions.ts b/tests/corpus/webassembly-type-mentions.ts new file mode 100644 index 000000000..fbaec710e --- /dev/null +++ b/tests/corpus/webassembly-type-mentions.ts @@ -0,0 +1,22 @@ +// Declarations that mention the WebAssembly API's types compile: the types +// have the checked-dynamic representation, and only a reached use of the +// API itself is refused. typeof needs no value and folds to Node's answer. +interface LoaderOptions { + wasm?: BufferSource | WebAssembly.Module; + imports?: WebAssembly.Imports; + memory?: WebAssembly.Memory; +} + +function describe(name: string, options: LoaderOptions): string { + const parts = [name]; + parts.push(options.wasm === undefined ? "no module" : "module given"); + parts.push(options.imports === undefined ? "no imports" : "imports given"); + return parts.join(", "); +} + +console.log(describe("empty", {})); +console.log(describe("bytes", { wasm: new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]) })); +console.log(typeof WebAssembly, typeof globalThis.WebAssembly); +console.log(typeof WebAssembly.compile, typeof WebAssembly.instantiate, typeof WebAssembly.validate); +console.log(typeof WebAssembly.Module, typeof WebAssembly.Instance, typeof WebAssembly.Memory); +console.log(typeof WebAssembly.RuntimeError, typeof WebAssembly.CompileError); diff --git a/tests/diagnostics/webassembly-fences.ts b/tests/diagnostics/webassembly-fences.ts new file mode 100644 index 000000000..56b2991de --- /dev/null +++ b/tests/diagnostics/webassembly-fences.ts @@ -0,0 +1,30 @@ +// No WebAssembly module compiles to native code yet: every reached use of +// the API refuses at its site, in each spelling, while declarations that +// only mention its types compile. +interface LoaderOptions { + wasm?: BufferSource | WebAssembly.Module; + imports?: WebAssembly.Imports; +} + +async function load(options: LoaderOptions, bytes: Uint8Array): Promise { + console.log(WebAssembly.validate(bytes)); + const module = await WebAssembly.compile(bytes); + const { instance } = await WebAssembly.instantiate(bytes, options.imports); + console.log(instance instanceof WebAssembly.Instance, module); + const direct = new WebAssembly.Instance(new WebAssembly.Module(bytes)); + console.log(direct.exports); + const memory = new WebAssembly.Memory({ initial: 1 }); + console.log(memory.buffer.byteLength); + const api = globalThis.WebAssembly; + console.log(api); + console.log(WebAssembly.Module.imports(module).length); +} + +// pi's codemode spelling: the global object viewed through a cast. +interface WebAssemblyGlobal { + compile(bytes: Uint8Array): Promise; +} +const { WebAssembly: viewed } = globalThis as unknown as { WebAssembly: WebAssemblyGlobal }; +console.log(typeof viewed.compile); + +await load({}, new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])); diff --git a/tests/harness/__snapshots__/webassembly-fences.ts.txt b/tests/harness/__snapshots__/webassembly-fences.ts.txt new file mode 100644 index 000000000..da38a2c57 --- /dev/null +++ b/tests/harness/__snapshots__/webassembly-fences.ts.txt @@ -0,0 +1,98 @@ +webassembly-fences.ts:10:15 - error SC2020: 'WebAssembly.validate' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 9 | async function load(options: LoaderOptions, bytes: Uint8Array): Promise { + 10 | console.log(WebAssembly.validate(bytes)); + | ^~~~~~~~~~~~~~~~~~~~ + 11 | const module = await WebAssembly.compile(bytes); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:11:24 - error SC2020: 'WebAssembly.compile' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 10 | console.log(WebAssembly.validate(bytes)); + 11 | const module = await WebAssembly.compile(bytes); + | ^~~~~~~~~~~~~~~~~~~ + 12 | const { instance } = await WebAssembly.instantiate(bytes, options.imports); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:12:30 - error SC2020: 'WebAssembly.instantiate' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 11 | const module = await WebAssembly.compile(bytes); + 12 | const { instance } = await WebAssembly.instantiate(bytes, options.imports); + | ^~~~~~~~~~~~~~~~~~~~~~~ + 13 | console.log(instance instanceof WebAssembly.Instance, module); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:13:35 - error SC2020: 'WebAssembly.Instance' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 12 | const { instance } = await WebAssembly.instantiate(bytes, options.imports); + 13 | console.log(instance instanceof WebAssembly.Instance, module); + | ^~~~~~~~~~~~~~~~~~~~ + 14 | const direct = new WebAssembly.Instance(new WebAssembly.Module(bytes)); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:14:22 - error SC2020: 'new WebAssembly.Instance' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 13 | console.log(instance instanceof WebAssembly.Instance, module); + 14 | const direct = new WebAssembly.Instance(new WebAssembly.Module(bytes)); + | ^~~~~~~~~~~~~~~~~~~~ + 15 | console.log(direct.exports); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:16:22 - error SC2020: 'new WebAssembly.Memory' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 15 | console.log(direct.exports); + 16 | const memory = new WebAssembly.Memory({ initial: 1 }); + | ^~~~~~~~~~~~~~~~~~ + 17 | console.log(memory.buffer.byteLength); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:17:15 - error SC2004: uses of 'memory' inherit the blocker on its declaration + + 16 | const memory = new WebAssembly.Memory({ initial: 1 }); + 17 | console.log(memory.buffer.byteLength); + | ^~~~~~~~~~~~~~~~~~~~~~~~ + 18 | const api = globalThis.WebAssembly; + + hint: the declaration of 'memory' did not compile — fix the diagnostic reported there and these sites clear with it + +webassembly-fences.ts:18:15 - error SC2020: 'globalThis.WebAssembly' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 17 | console.log(memory.buffer.byteLength); + 18 | const api = globalThis.WebAssembly; + | ^~~~~~~~~~~~~~~~~~~~~~ + 19 | console.log(api); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:19:15 - error SC2004: uses of 'api' inherit the blocker on its declaration + + 18 | const api = globalThis.WebAssembly; + 19 | console.log(api); + | ^~~ + 20 | console.log(WebAssembly.Module.imports(module).length); + + hint: the declaration of 'api' did not compile — fix the diagnostic reported there and these sites clear with it + +webassembly-fences.ts:20:15 - error SC2020: 'WebAssembly.Module.imports' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 19 | console.log(api); + 20 | console.log(WebAssembly.Module.imports(module).length); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 | } + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) + +webassembly-fences.ts:27:33 - error SC2020: 'globalThis.WebAssembly' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + + 26 | } + 27 | const { WebAssembly: viewed } = globalThis as unknown as { WebAssembly: WebAssemblyGlobal }; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 28 | console.log(typeof viewed.compile); + + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) \ No newline at end of file From daf4ff6a2bb1848935eeb7c7a6d52ad7132d934a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:43:35 -0700 Subject: [PATCH 069/120] Decode and validate the WebAssembly modules a program can load, and report them in coverage `scriptc coverage` now lists every WebAssembly module whose bytes the build can see, with its size, whether it validates, its function, import and export counts, and the WebAssembly proposals it uses. Nothing is translated yet; builds are unchanged. - packages/compiler/src/wasm/binary.ts decodes and validates a module in plain TypeScript (the native compiler compiles it too) at the feature level scriptc means to translate: the MVP plus sign-extension, non-trapping float-to-int, bulk memory, multi-value, mutable globals, reference types and extended constant expressions. Where WebAssembly 3.0 and V8 relaxed a 2.0 rule (global initializers reading defined globals, memory indices), it follows V8, the behavior oracle. Modules that use SIMD, threads, exception handling, tail calls, function references or GC, memory64 or multiple memories still decode, and are reported as needing that proposal instead of as invalid. - packages/compiler/src/wasm/discover.ts finds modules from four sources: paths the compiler resolves from the source text (require.resolve, createRequire(...).resolve, import.meta.resolve, new URL(..., import.meta.url), __dirname joins), inline base64 and byte-array literals, the .wasm files of reached packages whose code mentions WebAssembly, and a `scriptc.wasm` list in package.json or the new coverage-only `--wasm ` flag. Modules are reported once, by SHA-256, with every source that found them; module files are input probes of the coverage verdict cache. The JSON envelope (--print=diagnostics) carries the same records under `wasmModules`. Evidence: packages/compiler/test/wasm-spec.test.ts runs the decoder over the 3,017 distinct binary modules of the WebAssembly 2.0 core test suite (without SIMD) and the extended-const proposal's tests, generated by gen-wasm-spec-cases.mjs with wabt's wast2json, and requires V8's verdict for each; 8,000 seeded mutations of those modules must also agree with V8, as must undici's inline llhttp modules (scalar valid, SIMD reported) and es-module-lexer's. src/wasm/binary.test.ts checks each feature and each error class against V8. On the survey's real modules the decoder agrees with V8 for quickjs-wasi (637 KB, 36 ms) and photon (1.9 MB, 121 ms). tests/coverage-fixtures/wasm pins the four discovery sources. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + docs/content/docs/cli.mdx | 3 + docs/content/docs/limitations.mdx | 4 + .../limitations/webassembly-modules.mdx | 3 + packages/compiler/src/cli/command.ts | 6 + packages/compiler/src/cli/usage.ts | 6 + packages/compiler/src/compile-types.ts | 3 + packages/compiler/src/coverage/report.ts | 71 + .../compiler/src/coverage/verdict-cache.ts | 6 + packages/compiler/src/diagnostics/envelope.ts | 4 + packages/compiler/src/frontend/analysis.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 23 + packages/compiler/src/wasm/binary.test.ts | 244 ++ packages/compiler/src/wasm/binary.ts | 1957 +++++++++++ packages/compiler/src/wasm/discover.test.ts | 30 + packages/compiler/src/wasm/discover.ts | 426 +++ .../compiler/test/gen-wasm-spec-cases.mjs | 69 + packages/compiler/test/wasm-spec-cases.txt | 3017 +++++++++++++++++ packages/compiler/test/wasm-spec.test.ts | 168 + tests/coverage-fixtures/wasm/add.wasm | Bin 0 -> 41 bytes .../wasm/build/generated.wasm | Bin 0 -> 43 bytes tests/coverage-fixtures/wasm/glue.js | 18 + tests/coverage-fixtures/wasm/main.ts | 9 + .../node_modules/wasm-fixture-pkg/index.d.ts | 1 + .../node_modules/wasm-fixture-pkg/index.js | 6 + .../wasm-fixture-pkg/lib/core.wasm | Bin 0 -> 27 bytes .../wasm-fixture-pkg/package.json | 7 + tests/coverage-fixtures/wasm/package.json | 8 + tests/harness/__snapshots__/coverage-wasm.txt | 31 + tests/harness/coverage.test.ts | 30 + 30 files changed, 6153 insertions(+) create mode 100644 docs/content/limitations/webassembly-modules.mdx create mode 100644 packages/compiler/src/wasm/binary.test.ts create mode 100644 packages/compiler/src/wasm/binary.ts create mode 100644 packages/compiler/src/wasm/discover.test.ts create mode 100644 packages/compiler/src/wasm/discover.ts create mode 100644 packages/compiler/test/gen-wasm-spec-cases.mjs create mode 100644 packages/compiler/test/wasm-spec-cases.txt create mode 100644 packages/compiler/test/wasm-spec.test.ts create mode 100644 tests/coverage-fixtures/wasm/add.wasm create mode 100644 tests/coverage-fixtures/wasm/build/generated.wasm create mode 100644 tests/coverage-fixtures/wasm/glue.js create mode 100644 tests/coverage-fixtures/wasm/main.ts create mode 100644 tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.d.ts create mode 100644 tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.js create mode 100644 tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm create mode 100644 tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/package.json create mode 100644 tests/coverage-fixtures/wasm/package.json create mode 100644 tests/harness/__snapshots__/coverage-wasm.txt diff --git a/.gitignore b/.gitignore index e5b54a5fa..931da612d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules/ !tests/fixtures/npm/node_modules/ !tests/corpus/*/node_modules/ !tests/fixtures/npm-static/node_modules/ +!tests/coverage-fixtures/wasm/node_modules/ !tests/fixtures/npm-static/nested/inner/node_modules/ !tests/fixtures/npm/store/*/node_modules/ !tests/fixtures/npm/cases/*/node_modules/ diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index e8596fc3d..410b6fb1c 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -133,6 +133,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
--explain-dynamic[=<name>]
Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as x, Point, or Point.x, show only the matching units. See Dynamic tier.
+
--wasm <file.wasm>
+
Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and .wasm files shipped by packages that use WebAssembly; a scriptc.wasm array in package.json adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.
+
--sanitize
Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with --emit=asm or --emit=obj.
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 544bd2b2d..548e577ab 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -236,6 +236,10 @@ title: "Limitations" ../limitations/native-addons.mdx +## WebAssembly modules + +../limitations/webassembly-modules.mdx + ## WASI target ../limitations/wasi-target.mdx diff --git a/docs/content/limitations/webassembly-modules.mdx b/docs/content/limitations/webassembly-modules.mdx new file mode 100644 index 000000000..492b2fc1a --- /dev/null +++ b/docs/content/limitations/webassembly-modules.mdx @@ -0,0 +1,3 @@ +Compiled programs can't load WebAssembly modules yet. Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js. Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`. TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs. + +`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use. It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`. diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 6810cc8fb..79442f19e 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -285,6 +285,11 @@ async function main(args: string[], host: CliHost): Promise { } externalTypes[specifier] = declarationPath; } + const wasmArgs = values.wasm ?? []; + if (wasmArgs.length > 0 && command !== "coverage") { + fail(`--wasm is a coverage-only option\n\n${USAGE}`); + } + const wasmFiles = wasmArgs.map((file) => resolve(file)); const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined; if (values.backend !== undefined && values.backend !== "llvm") { fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`); @@ -364,6 +369,7 @@ async function main(args: string[], host: CliHost): Promise { ...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}), ...(tier !== undefined ? { tier } : {}), ...(explainDynamic !== undefined ? { explainDynamic: true } : {}), + ...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}), }); if (printDiagnostics) { // Keep stdout pure JSON for tooling. diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index 97c56c274..2419a26d0 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -79,6 +79,11 @@ Options: values and the bindings, fields, functions, classes and modules the dynamic tier would take over, with the refusal behind each; a name shows only that unit's chain + --wasm + coverage only: report a WebAssembly module the program + loads whose path its sources do not spell (repeatable; + modules the sources name, inline modules and packaged + .wasm files are found without it) -h, --help show this help -v, --version print the version `; @@ -102,6 +107,7 @@ export const CLI_OPTIONS = { "external-types": { type: "string", multiple: true }, tier: { type: "string" }, "explain-dynamic": { type: "string" }, + wasm: { type: "string", multiple: true }, lib: { type: "boolean", default: false }, profile: { type: "string" }, help: { type: "boolean", short: "h", default: false }, diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts index f069df247..2773af71b 100644 --- a/packages/compiler/src/compile-types.ts +++ b/packages/compiler/src/compile-types.ts @@ -171,6 +171,9 @@ export interface AnalyzeOptions { /** `--explain-dynamic`: attach the tier-assignment report * (CoverageInput.tiers). Such analyses bypass the verdict cache. */ explainDynamic?: boolean; + /** `--wasm `: WebAssembly module files the program loads that its + * sources do not name (absolute paths); reported with the discovered ones. */ + wasm?: readonly string[]; } export interface AnalyzeResult { diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 730704f6f..4b05e5fd1 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -24,6 +24,7 @@ import { renderDiagnostics } from "../diagnostics/render.js"; import type { LowerStats } from "../frontend/lowering/lowerer.js"; import type { ProvenanceSources } from "../frontend/provenance-registry.js"; import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js"; +import type { WasmModuleReport } from "../wasm/discover.js"; export interface CoverageInput { file: string; @@ -57,6 +58,9 @@ export interface CoverageInput { /** --explain-dynamic: the report-only tier assignment (which bindings, * fields, functions, classes and modules the dynamic tier would take). */ tiers?: TierReport; + /** The WebAssembly modules the build can see (wasm/discover.ts): their + * size, validity and the proposals they use. */ + wasmModules?: WasmModuleReport[]; /** True when tsc preflight failed — no coverage can be computed. */ preflightFailed: boolean; } @@ -215,6 +219,51 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions out.push(""); } + // The WebAssembly modules the build can see: what translating them would + // take (wasm/discover.ts). No module compiles to native code yet; the + // sites that load one are blockers or deferred fences above. + const wasm = input.wasmModules ?? []; + if (wasm.length > 0) { + out.push( + ` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; none compile to native code yet)")}`, + ); + for (const m of wasm) { + const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module"; + const state = + m.status === "valid" + ? c(GREEN, "valid") + : m.status === "unsupported" + ? c(YELLOW, `uses ${m.unsupported.join(", ")}`) + : c(RED, `${m.status}: ${m.error ?? ""}`); + out.push(` ${name} ${c(DIM, `${groupDigits(m.size)} bytes`)} ${state}`); + const imports = m.importModules.length > 0 ? ` (${m.importModules.join(", ")})` : ""; + out.push( + ` ${c(DIM, `${counted(m.functions, "function")}, ${counted(m.imports, "import")}${imports}, ${counted(m.exports, "export")}`)}`, + ); + if (m.features.length > 0) out.push(` ${c(DIM, `features: ${m.features.join(", ")}`)}`); + const shown = m.sources.slice(0, SITES_SHOWN); + for (const s of shown) { + const at = + s.line !== undefined + ? `${displayPath(s.file, opts.root)}:${s.line}` + : displayPath(s.file, opts.root); + const how = + s.kind === "path" + ? `path at ${at}` + : s.kind === "inline" + ? `inline at ${at}` + : s.kind === "package" + ? `shipped by the package at ${displayPath(dirOf(s.file), opts.root)}` + : `configured by ${at}`; + out.push(` ${c(DIM, `found: ${how}`)}`); + } + if (m.sources.length > shown.length) { + out.push(` ${c(DIM, `found: +${m.sources.length - shown.length} more`)}`); + } + } + out.push(""); + } + // --provenance-sources: per-package attribution — the answer to "did // the DEPENDENCY's statements compile static?", computed by summing // statsByFile entries under each package's source dir. Elided @@ -333,6 +382,27 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions return out.join("\n"); } +/** "1 function", "1,698 functions". */ +function counted(n: number, noun: string): string { + return `${groupDigits(n)} ${noun}${n === 1 ? "" : "s"}`; +} + +/** 637405 → "637,405". */ +function groupDigits(n: number): string { + const digits = String(n); + let out = ""; + for (let i = 0; i < digits.length; i++) { + if (i > 0 && (digits.length - i) % 3 === 0) out += ","; + out += digits[i]; + } + return out; +} + +function dirOf(file: string): string { + const slash = file.lastIndexOf("/"); + return slash <= 0 ? file : file.slice(0, slash); +} + function DIMPath(file: string, color: boolean): string { return color ? DIM + file + RESET : file; } @@ -451,6 +521,7 @@ export function coverageEnvelope( }, }), ...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}), + ...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}), groups, diagnostics: located, }; diff --git a/packages/compiler/src/coverage/verdict-cache.ts b/packages/compiler/src/coverage/verdict-cache.ts index 250259685..422bf5e4d 100644 --- a/packages/compiler/src/coverage/verdict-cache.ts +++ b/packages/compiler/src/coverage/verdict-cache.ts @@ -14,6 +14,7 @@ import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import type { LowerStats } from "../frontend/lowering/lowerer.js"; import type { NpmStaticStatus } from "./report.js"; +import type { WasmModuleReport } from "../wasm/discover.js"; import { FrontendInputTracker, frontendInputsStillMatch, @@ -38,6 +39,7 @@ export interface CachedCoverage { unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] }; divergences?: ScrDiagnostic[]; npmStatic?: NpmStaticStatus[]; + wasmModules?: WasmModuleReport[]; preflightFailed: boolean; } @@ -125,6 +127,8 @@ export function verdictKey( external.join("\n"), // Demotion (stage S6) makes the tier mode part of the verdict. ...(opts.tier === "static-only" ? ["tier=static-only"] : []), + // The module files' contents are probes; their names are options. + ...(opts.wasm !== undefined && opts.wasm.length > 0 ? [`wasm=${opts.wasm.join("\n")}`] : []), ].join("\0"), ); } @@ -146,6 +150,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null { if (coverage.unreached !== undefined) out.unreached = coverage.unreached; if (coverage.divergences !== undefined) out.divergences = coverage.divergences; if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic; + if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules; return out; } @@ -162,6 +167,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult if (cached.unreached !== undefined) coverage.unreached = cached.unreached; if (cached.divergences !== undefined) coverage.divergences = cached.divergences; if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic; + if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules; return { coverage, sourceTexts }; } diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index 4c7c158d9..c986fc5b1 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -16,6 +16,7 @@ * bumps it. */ import { readFileSync } from "node:fs"; import type { ScrDiagnostic } from "./diagnostic.js"; +import type { WasmModuleReport } from "../wasm/discover.js"; export const DIAGNOSTICS_SCHEMA = "scriptc-diagnostics"; export const DIAGNOSTICS_SCHEMA_VERSION = 1; @@ -138,6 +139,9 @@ export interface DiagnosticsEnvelope { artifact?: string; /** coverage --explain-dynamic only: the report-only tier assignment. */ tiers?: TiersEnvelope; + /** coverage only: the WebAssembly modules the build can see, with their + * size, validity, proposals and the sources that found them. */ + wasmModules?: WasmModuleReport[]; groups: DiagnosticGroup[]; diagnostics: LocatedDiagnostic[]; } diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index 87c7d22ca..e2245eb14 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -92,6 +92,7 @@ export function analyzeWithFrontend( ...(ffi !== null ? { ffiImports: ffi.functions } : {}), ...(opts.tier !== undefined ? { tierMode: opts.tier } : {}), ...(opts.explainDynamic === true ? { tierReport: { preflight } } : {}), + ...(opts.wasm !== undefined && opts.wasm.length > 0 ? { wasmFiles: opts.wasm } : {}), }); const provenance = provenanceSources(); return { @@ -112,6 +113,7 @@ export function analyzeWithFrontend( ...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}), ...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}), ...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}), + ...(lowered.wasmModules !== undefined ? { wasmModules: lowered.wasmModules } : {}), preflightFailed: false, }, sourceTexts: fe.sourceTexts(), diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index f341fb7f0..cb190cb6a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -27,6 +27,8 @@ import { concreteCollectionNarrow } from "./collection-narrowing.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js"; import { refuseWebAssemblyUse } from "./builtins/webassembly.js"; +import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js"; +import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; import { hashStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; @@ -714,6 +716,8 @@ export interface LowerResult { /** Coverage only (LowerOptions.coverage): the unreached remainder, * lowered in a throwaway pass — blockers in it can never fail a build. */ unreached?: { diagnostics: ScrDiagnostic[]; stats: LowerStats; runtimeFences?: ScrDiagnostic[] }; + /** Coverage only: the WebAssembly modules the build can see (wasm/discover.ts). */ + wasmModules?: WasmModuleReport[]; /** Compile-time-detectable behavior differences from Node (SC6xxx * divergence warnings) in the program's own modules. Never failures. */ divergences?: ScrDiagnostic[]; @@ -734,6 +738,8 @@ export interface LowerOptions { * diagnostics and stats under `unreached` — the whole-program analysis * builds deliberately gave up. */ coverage?: boolean; + /** Coverage only: module files named by --wasm (absolute). */ + wasmFiles?: readonly string[]; /** The platform the build TARGETS ("win32" under a windows cross triple, * the host platform otherwise — see buildTargetPlatform in index.ts). * The whole program compiles for one platform, so the platform-keyed @@ -1107,6 +1113,23 @@ export function lowerToIr( }, }; if (tiers !== undefined) covered.tiers = tiers; + const wasmModules = discoverWasmModules({ + files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry], + entry: entry.fileName, + configured: options.wasmFiles ?? [], + resolveRequest: (fromFile, specifier) => { + const resolved = resolveRequireRuntime( + fromFile, + specifier, + targetPlatform, + undefined, + options.frontendServices, + ); + return resolved.ok ? resolved.value : null; + }, + }); + timing("wasm-discovery", { modules: wasmModules.length }); + if (wasmModules.length > 0) covered.wasmModules = wasmModules; return covered; } diff --git a/packages/compiler/src/wasm/binary.test.ts b/packages/compiler/src/wasm/binary.test.ts new file mode 100644 index 000000000..5f95f91d2 --- /dev/null +++ b/packages/compiler/src/wasm/binary.test.ts @@ -0,0 +1,244 @@ +import { describe, expect, test } from "vitest"; +import { analyzeWasm, isTranslatableFeature, isWasmHeader } from "./binary.js"; + +/** V8's verdict (the compiler's type world declares no WebAssembly value). */ +const v8Validate = (bytes: Uint8Array): boolean => + ( + globalThis as unknown as { WebAssembly: { validate(bytes: Uint8Array): boolean } } + ).WebAssembly.validate(bytes); + +// A minimal module assembler: enough structure to aim at one decoder rule +// per test. Every module is also checked against V8 (Node is the oracle). + +function leb(n: number): number[] { + const out: number[] = []; + do { + let b = n % 128; + n = Math.floor(n / 128); + if (n > 0) b |= 0x80; + out.push(b); + } while (n > 0); + return out; +} + +function name(text: string): number[] { + const bytes = [...new TextEncoder().encode(text)]; + return [...leb(bytes.length), ...bytes]; +} + +function vec(items: number[][]): number[] { + return [...leb(items.length), ...items.flat()]; +} + +function section(id: number, body: number[]): number[] { + return [id, ...leb(body.length), ...body]; +} + +function module(...sections: number[][]): Uint8Array { + return new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...sections.flat()]); +} + +const I32 = 0x7f; +const I64 = 0x7e; +const F32 = 0x7d; +const V128 = 0x7b; +const EXTERNREF = 0x6f; + +const funcType = (params: number[], results: number[]): number[] => [ + 0x60, + ...vec(params.map((p) => [p])), + ...vec(results.map((r) => [r])), +]; +const types = (...fts: number[][]): number[] => section(1, vec(fts)); +const funcs = (...typeIndices: number[]): number[] => + section(3, vec(typeIndices.map((t) => leb(t)))); +const body = (code: number[], locals: number[][] = []): number[] => { + const content = [...vec(locals), ...code, 0x0b]; + return [...leb(content.length), ...content]; +}; +const code = (...bodies: number[][]): number[] => section(10, vec(bodies)); +const memory = (...limits: number[][]): number[] => section(5, vec(limits)); +const exportsOf = (...entries: number[][]): number[] => section(7, vec(entries)); + +function check(bytes: Uint8Array) { + const summary = analyzeWasm(bytes); + const accepted = summary.status === "valid" || summary.status === "unsupported"; + expect(accepted, summary.error ?? "").toBe(v8Validate(bytes)); + return summary; +} + +describe("analyzeWasm", () => { + test("an empty module is valid and uses no proposal", () => { + const s = check(module()); + expect(s).toMatchObject({ status: "valid", error: null, features: [], functions: 0, size: 8 }); + expect(isWasmHeader(module())).toBe(true); + }); + + test("counts functions, imports, exports, memories, tables and globals", () => { + const s = check( + module( + types(funcType([I32, I32], [I32]), funcType([], [])), + section( + 2, + vec([ + [...name("env"), ...name("log"), 0x00, 0x01], + [...name("wasi_snapshot_preview1"), ...name("proc_exit"), 0x00, 0x01], + ]), + ), + funcs(0), + section(4, vec([[0x70, 0x00, 0x01]])), + memory([0x00, 0x01]), + section(6, vec([[I32, 0x01, 0x41, 0x00, 0x0b]])), + exportsOf([...name("add"), 0x00, 0x02], [...name("memory"), 0x02, 0x00]), + code(body([0x20, 0x00, 0x20, 0x01, 0x6a])), + ), + ); + expect(s).toMatchObject({ + status: "valid", + functions: 1, + imports: 2, + exports: 2, + importModules: ["env", "wasi_snapshot_preview1"], + memories: 1, + tables: 1, + globals: 1, + }); + }); + + test.each([ + [ + "sign-extension", + module(types(funcType([I32], [I32])), funcs(0), code(body([0x20, 0x00, 0xc0]))), + ], + [ + "nontrapping-fptoint", + module(types(funcType([F32], [I32])), funcs(0), code(body([0x20, 0x00, 0xfc, 0x00]))), + ], + [ + "bulk-memory", + module( + types(funcType([], [])), + funcs(0), + memory([0x00, 0x01]), + code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x0b, 0x00])), + ), + ], + [ + "multi-value", + module(types(funcType([], [I32, I64])), funcs(0), code(body([0x41, 0x01, 0x42, 0x02]))), + ], + ["reference-types", module(section(4, vec([[EXTERNREF, 0x00, 0x01]])))], + ["extended-const", module(section(6, vec([[I32, 0x00, 0x41, 0x01, 0x41, 0x02, 0x6a, 0x0b]])))], + [ + "mutable-globals", + module(section(2, vec([[...name("env"), ...name("sp"), 0x03, I32, 0x01]]))), + ], + ])("detects %s and stays translatable", (feature, bytes) => { + const s = check(bytes); + expect(s.status).toBe("valid"); + expect(s.features).toContain(feature); + expect(isTranslatableFeature(feature)).toBe(true); + }); + + test.each([ + [ + "simd", + module( + types(funcType([], [V128])), + funcs(0), + code(body([0xfd, 0x0c, ...Array.from({ length: 16 }, () => 0)])), + ), + ], + ["threads", module(memory([0x03, 0x01, 0x01]))], + ["exceptions", module(types(funcType([], [])), section(13, vec([[0x00, 0x00]])))], + ["tail-call", module(types(funcType([], [])), funcs(0), code(body([0x12, 0x00])))], + ["memory64", module(memory([0x04, 0x01]))], + ["multi-memory", module(memory([0x00, 0x01], [0x00, 0x01]))], + ["gc", module(section(1, vec([[0x5f, 0x00]])))], + ])("reports %s as outside the translatable set", (feature, bytes) => { + const s = check(bytes); + expect(s.status).toBe("unsupported"); + expect(s.unsupported).toEqual([feature]); + expect(isTranslatableFeature(feature)).toBe(false); + }); + + test.each([ + ["bad magic", new Uint8Array([0x00, 0x61, 0x73, 0x6e, 1, 0, 0, 0]), "malformed", "magic"], + ["bad version", new Uint8Array([0x00, 0x61, 0x73, 0x6d, 2, 0, 0, 0]), "malformed", "version"], + ["sections out of order", module(funcs(), types()), "malformed", "unexpected section"], + ["section size mismatch", module([0x01, 0x02, 0x00, 0x00]), "malformed", "section size"], + [ + "overlong integer", + module([0x01, 0x06, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00]), + "malformed", + "integer", + ], + ["invalid UTF-8 name", module(exportsOf([0x01, 0xff, 0x00, 0x00])), "malformed", "UTF-8"], + [ + "function and code counts differ", + module(types(funcType([], [])), funcs(0, 0), code(body([]))), + "malformed", + "inconsistent", + ], + [ + "illegal opcode", + module(types(funcType([], [])), funcs(0), code(body([0xff]))), + "malformed", + "illegal opcode", + ], + [ + "memory.init without a data count section", + module( + types(funcType([], [])), + funcs(0), + memory([0x00, 0x01]), + code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x08, 0x00, 0x00])), + ), + "malformed", + "data count", + ], + [ + "type mismatch", + module(types(funcType([], [I32])), funcs(0), code(body([0x42, 0x00]))), + "invalid", + "type mismatch", + ], + [ + "unknown function", + module(types(funcType([], [])), funcs(0), code(body([0x10, 0x05]))), + "invalid", + "unknown function", + ], + [ + "duplicate export", + module( + types(funcType([], [])), + funcs(0), + exportsOf([...name("f"), 0x00, 0x00], [...name("f"), 0x00, 0x00]), + code(body([])), + ), + "invalid", + "duplicate export", + ], + ["memory above 4 GiB", module(memory([0x00, ...leb(65537)])), "invalid", "65536 pages"], + [ + "undeclared function reference", + module(types(funcType([], [])), funcs(0), code(body([0xd2, 0x00, 0x1a]))), + "invalid", + "undeclared", + ], + ])("rejects %s", (_label, bytes, status, message) => { + const s = check(bytes); + expect(s.status).toBe(status); + expect(s.error).toContain(message); + }); + + test("never throws on truncated input", () => { + const whole = module( + types(funcType([I32, I32], [I32])), + funcs(0), + code(body([0x20, 0x00, 0x20, 0x01, 0x6a])), + ); + for (let n = 0; n < whole.length; n++) check(whole.subarray(0, n)); + }); +}); diff --git a/packages/compiler/src/wasm/binary.ts b/packages/compiler/src/wasm/binary.ts new file mode 100644 index 000000000..5ddc28cc9 --- /dev/null +++ b/packages/compiler/src/wasm/binary.ts @@ -0,0 +1,1957 @@ +/* The WebAssembly binary decoder and validator. + * + * Decodes a module completely and validates it against the feature level + * scriptc means to translate to native code: the MVP plus the finished + * post-MVP proposals that real npm modules use (sign-extension operators, + * non-trapping float-to-int conversions, bulk memory, multi-value, mutable + * globals, reference types and extended constant expressions). Where the + * WebAssembly 3.0 specification and V8 relax a 2.0 rule inside that set (a + * global initializer may read any earlier immutable global), the decoder + * follows them, because Node is the behavior oracle. + * + * Modules that use a proposal outside that set (SIMD, threads, exception + * handling, tail calls, function references and GC, memory64, multiple + * memories) still decode: the instructions that need the proposal are read + * for their immediates but not type-checked, and the summary names the + * proposal so that the module can be reported as not translatable rather + * than as invalid. Only GC type definitions stop the decode, since their + * layout would need the full GC type system. + * + * The decoder never evaluates anything and keeps no module contents beyond + * the counts and the feature set it reports. It is plain TypeScript that + * the native compiler also compiles. */ + +/** Proposals the summary can name, in report order. The first group is the + * translatable set; the rest are reported as unsupported. */ +export const WASM_FEATURES = [ + "mutable-globals", + "sign-extension", + "nontrapping-fptoint", + "bulk-memory", + "multi-value", + "reference-types", + "extended-const", + "simd", + "threads", + "exceptions", + "tail-call", + "function-references", + "gc", + "memory64", + "multi-memory", +] as const; +export type WasmFeature = (typeof WASM_FEATURES)[number]; + +const TRANSLATABLE: ReadonlySet = new Set([ + "mutable-globals", + "sign-extension", + "nontrapping-fptoint", + "bulk-memory", + "multi-value", + "reference-types", + "extended-const", +]); + +/** True for proposals inside the translatable feature level. */ +export function isTranslatableFeature(feature: string): boolean { + return TRANSLATABLE.has(feature); +} + +/** What a decoded module reports. `status` is "valid" when the module + * validates within the translatable feature level, "unsupported" when it + * uses a proposal outside it (and is otherwise well-formed as far as the + * decoder checks), and "malformed" or "invalid" for a module that every + * engine rejects; `error` then says where and why. */ +export interface WasmModuleSummary { + size: number; + status: "valid" | "unsupported" | "malformed" | "invalid"; + error: string | null; + features: string[]; + unsupported: string[]; + functions: number; + imports: number; + exports: number; + importModules: string[]; + memories: number; + tables: number; + globals: number; + customSections: string[]; +} + +/** A decode failure: malformed bytes or an invalid module. */ +export class WasmError extends Error { + kind: "malformed" | "invalid"; + offset: number; + constructor(kind: "malformed" | "invalid", message: string, offset: number) { + super(message); + this.kind = kind; + this.offset = offset; + } +} + +// Value types. Zero is the "unknown" type of unreachable code. +const UNKNOWN = 0; +const I32 = 0x7f; +const I64 = 0x7e; +const F32 = 0x7d; +const F64 = 0x7c; +const V128 = 0x7b; +const FUNCREF = 0x70; +const EXTERNREF = 0x6f; + +// V8's implementation limits that are part of validation (wasm-limits.h). +const MAX_TYPES = 1000000; +const MAX_FUNCTIONS = 1000000; +const MAX_IMPORTS = 100000; +const MAX_EXPORTS = 100000; +const MAX_GLOBALS = 1000000; +const MAX_DATA_SEGMENTS = 100000; +const MAX_LOCALS = 50000; +const MAX_PARAMS = 1000; +const MAX_RESULTS = 1000; +const MAX_FUNCTION_SIZE = 7654321; +const MAX_MEMORY_PAGES = 65536; +const MAX_TABLE_INIT = 10000000; + +interface FuncType { + params: number[]; + results: number[]; +} + +interface Global { + type: number; + mutable: boolean; + imported: boolean; +} + +interface Table { + type: number; +} + +interface Frame { + opcode: number; + start: number[]; + end: number[]; + height: number; + unreachable: boolean; + /** An `if` that has not seen its `else`. */ + hasElse: boolean; +} + +class Reader { + bytes: Uint8Array; + pos: number; + end: number; + constructor(bytes: Uint8Array, pos: number, end: number) { + this.bytes = bytes; + this.pos = pos; + this.end = end; + } + + malformed(message: string): never { + throw new WasmError("malformed", message, this.pos); + } + + eof(): boolean { + return this.pos >= this.end; + } + + u8(): number { + if (this.pos >= this.end) this.malformed("unexpected end"); + const b = this.bytes[this.pos]!; + this.pos++; + return b; + } + + peek(): number { + if (this.pos >= this.end) this.malformed("unexpected end"); + return this.bytes[this.pos]!; + } + + skip(n: number): void { + if (n > this.end - this.pos) this.malformed("unexpected end"); + this.pos += n; + } + + u32(): number { + let result = 0; + let scale = 1; + for (let i = 0; i < 5; i++) { + const b = this.u8(); + if (i === 4 && (b & 0xf0) !== 0) { + this.malformed(b & 0x80 ? "integer representation too long" : "integer too large"); + } + result += (b & 0x7f) * scale; + if ((b & 0x80) === 0) return result; + scale *= 128; + } + return this.malformed("integer representation too long"); + } + + /** A signed LEB128 of `bits` bits (32, 33 or 64); answers the value for + * 32 and 33 bits and 0 for 64 (no caller needs it). */ + sleb(bits: number): number { + const maxBytes = Math.ceil(bits / 7); + let result = 0; + let scale = 1; + for (let i = 0; i < maxBytes; i++) { + const b = this.u8(); + if (i === maxBytes - 1) { + // The final byte's bits above the value's width must repeat its + // sign bit, and it may not continue. + if (b & 0x80) this.malformed("integer representation too long"); + const used = bits - 7 * i; + const unused = (b >> (used - 1)) & ((1 << (8 - used)) - 1) & 0x7f; + const all = (1 << (7 - used + 1)) - 1; + if (unused !== 0 && unused !== all) this.malformed("integer too large"); + } + if (bits < 64) result += (b & 0x7f) * scale; + scale *= 128; + if ((b & 0x80) === 0) { + if (bits < 64 && b & 0x40) result -= scale; + return bits < 64 ? result : 0; + } + } + return this.malformed("integer representation too long"); + } + + u64(): void { + this.sleb(64); + } + + name(): string { + const length = this.u32(); + if (length > this.end - this.pos) this.malformed("length out of bounds"); + const start = this.pos; + this.pos += length; + return utf8(this.bytes, start, this.pos, this); + } +} + +function utf8(bytes: Uint8Array, start: number, end: number, reader: Reader): string { + let out = ""; + let i = start; + while (i < end) { + const b = bytes[i]!; + let cp = 0; + let need = 0; + if (b < 0x80) { + cp = b; + } else if (b >= 0xc2 && b <= 0xdf) { + cp = b & 0x1f; + need = 1; + } else if (b >= 0xe0 && b <= 0xef) { + cp = b & 0x0f; + need = 2; + } else if (b >= 0xf0 && b <= 0xf4) { + cp = b & 0x07; + need = 3; + } else { + reader.malformed("malformed UTF-8 encoding"); + } + if (i + need >= end) reader.malformed("malformed UTF-8 encoding"); + for (let k = 1; k <= need; k++) { + const c = bytes[i + k]!; + if ((c & 0xc0) !== 0x80) reader.malformed("malformed UTF-8 encoding"); + cp = cp * 64 + (c & 0x3f); + } + if ( + (need === 2 && (cp < 0x800 || (cp >= 0xd800 && cp <= 0xdfff))) || + (need === 3 && (cp < 0x10000 || cp > 0x10ffff)) + ) { + reader.malformed("malformed UTF-8 encoding"); + } + out += String.fromCodePoint(cp); + i += need + 1; + } + return out; +} + +/** Load and store natural alignments (log2) by opcode, 0x28..0x3e. */ +const MEMORY_ALIGN = [2, 3, 2, 3, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 2, 3, 2, 3, 0, 1, 0, 1, 2]; +/** Load result / store operand types by opcode, 0x28..0x3e. */ +const MEMORY_TYPE = [ + I32, + I64, + F32, + F64, + I32, + I32, + I32, + I32, + I64, + I64, + I64, + I64, + I64, + I64, + I32, + I64, + F32, + F64, + I32, + I32, + I64, + I64, + I64, +]; + +/** Numeric instruction signatures, 0x45..0xc4: operand type, operand + * count, result type. */ +function numericSignature(op: number): [number, number, number] { + if (op === 0x45) return [I32, 1, I32]; + if (op <= 0x4f) return [I32, 2, I32]; + if (op === 0x50) return [I64, 1, I32]; + if (op <= 0x5a) return [I64, 2, I32]; + if (op <= 0x60) return [F32, 2, I32]; + if (op <= 0x66) return [F64, 2, I32]; + if (op <= 0x69) return [I32, 1, I32]; + if (op <= 0x78) return [I32, 2, I32]; + if (op <= 0x7b) return [I64, 1, I64]; + if (op <= 0x8a) return [I64, 2, I64]; + if (op <= 0x91) return [F32, 1, F32]; + if (op <= 0x98) return [F32, 2, F32]; + if (op <= 0x9f) return [F64, 1, F64]; + if (op <= 0xa6) return [F64, 2, F64]; + switch (op) { + case 0xa7: + return [I64, 1, I32]; + case 0xa8: + case 0xa9: + return [F32, 1, I32]; + case 0xaa: + case 0xab: + return [F64, 1, I32]; + case 0xac: + case 0xad: + return [I32, 1, I64]; + case 0xae: + case 0xaf: + return [F32, 1, I64]; + case 0xb0: + case 0xb1: + return [F64, 1, I64]; + case 0xb2: + case 0xb3: + return [I32, 1, F32]; + case 0xb4: + case 0xb5: + return [I64, 1, F32]; + case 0xb6: + return [F64, 1, F32]; + case 0xb7: + case 0xb8: + return [I32, 1, F64]; + case 0xb9: + case 0xba: + return [I64, 1, F64]; + case 0xbb: + return [F32, 1, F64]; + case 0xbc: + return [F32, 1, I32]; + case 0xbd: + return [F64, 1, I64]; + case 0xbe: + return [I32, 1, F32]; + case 0xbf: + return [I64, 1, F64]; + case 0xc0: + case 0xc1: + return [I32, 1, I32]; + default: + return [I64, 1, I64]; + } +} + +const SECTION_ORDER: Record = { + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 13: 6, + 6: 7, + 7: 8, + 8: 9, + 9: 10, + 12: 11, + 10: 12, + 11: 13, +}; + +class ModuleDecoder { + bytes: Uint8Array; + features = new Set(); + types: FuncType[] = []; + funcs: number[] = []; + importedFuncs = 0; + tables: Table[] = []; + memories = 0; + memory64 = false; + globals: Global[] = []; + importedGlobals = 0; + tags = 0; + elemTypes: number[] = []; + dataCount: number | null = null; + dataSegments = 0; + declaredRefs = new Set(); + exportNames = new Set(); + importModules: string[] = []; + customSections: string[] = []; + imports = 0; + exports = 0; + codeCount = 0; + sawCode = false; + /** A type definition outside the function-type subset stopped typing. */ + gcTypes = false; + + constructor(bytes: Uint8Array) { + this.bytes = bytes; + } + + invalid(message: string, offset: number): never { + throw new WasmError("invalid", message, offset); + } + + feature(name: WasmFeature): void { + this.features.add(name); + } + + // ── types ──────────────────────────────────────────────────────────── + + /** A value type; GC and exception-handling reference types are named as + * features and answer UNKNOWN (the code that uses them is not typed). */ + valType(r: Reader): number { + const b = r.u8(); + switch (b) { + case I32: + case I64: + case F32: + case F64: + return b; + case V128: + this.feature("simd"); + return V128; + case FUNCREF: + case EXTERNREF: + return b; + default: + return this.otherRefType(r, b); + } + } + + otherRefType(r: Reader, b: number): number { + if (b === 0x63 || b === 0x64) { + // (ref null ht) / (ref ht): a heap type follows. + this.feature("function-references"); + this.heapType(r); + return UNKNOWN; + } + if (b === 0x69 || b === 0x68 || b === 0x74 || b === 0x75) { + this.feature("exceptions"); + return UNKNOWN; + } + if (b >= 0x6a && b <= 0x73) { + // anyref, eqref, i31ref, structref, arrayref, nullref, ... + this.feature("gc"); + return UNKNOWN; + } + return r.malformed("malformed value type"); + } + + heapType(r: Reader): void { + const b = r.peek(); + if (b >= 0x60 && b <= 0x7f) { + r.u8(); + if (b === 0x69 || b === 0x74) this.feature("exceptions"); + else if (b !== FUNCREF && b !== EXTERNREF) this.feature("gc"); + return; + } + // A concrete heap type: a typed function reference. + const offset = r.pos; + const index = r.sleb(33); + if (index < 0) r.malformed("malformed heap type"); + this.feature("function-references"); + if (index >= this.types.length && !this.gcTypes) this.invalid(`unknown type ${index}`, offset); + } + + refType(r: Reader): number { + const b = r.u8(); + if (b === FUNCREF || b === EXTERNREF) return b; + if (b >= 0x63 && b <= 0x75 && b !== 0x67 && b !== 0x66 && b !== 0x65) { + this.otherRefType(r, b); + return UNKNOWN; + } + return r.malformed("malformed reference type"); + } + + typeSection(r: Reader): void { + const count = r.u32(); + if (count > MAX_TYPES) r.malformed("too many types"); + for (let i = 0; i < count; i++) { + const form = r.u8(); + if (form === 0x60) { + this.types.push(this.funcType(r)); + continue; + } + if (form === 0x4e || form === 0x50 || form === 0x4f || form === 0x5f || form === 0x5e) { + // Recursive groups, subtypes, structs and arrays: the GC type + // system. Stop decoding types; nothing after them can be typed. + this.feature("gc"); + this.gcTypes = true; + r.pos = r.end; + return; + } + r.malformed("malformed function type"); + } + } + + funcType(r: Reader): FuncType { + const params: number[] = []; + const nParams = r.u32(); + if (nParams > MAX_PARAMS) r.malformed("too many parameters"); + for (let i = 0; i < nParams; i++) params.push(this.valType(r)); + const results: number[] = []; + const nResults = r.u32(); + if (nResults > MAX_RESULTS) r.malformed("too many results"); + for (let i = 0; i < nResults; i++) results.push(this.valType(r)); + if (results.length > 1) this.feature("multi-value"); + return { params, results }; + } + + funcTypeAt(index: number, offset: number): FuncType { + if (index >= this.types.length) { + if (this.gcTypes) return { params: [], results: [] }; + this.invalid(`unknown type ${index}`, offset); + } + return this.types[index]!; + } + + limits(r: Reader, kind: "memory" | "table"): { min: number; max: number | null } { + const flags = r.u8(); + if (kind === "memory") { + if (flags > 0x07) r.malformed("malformed limits flags"); + if (flags & 0x02) this.feature("threads"); + if (flags & 0x04) { + this.feature("memory64"); + this.memory64 = true; + } + } else { + // Tables: a maximum flag and the table64 flag; no shared tables. + if ((flags & ~0x05) !== 0) r.malformed("malformed limits flags"); + if (flags & 0x04) this.feature("memory64"); + } + const wide = (flags & 0x04) !== 0; + const min = wide ? (r.u64(), 0) : r.u32(); + let max: number | null = null; + if (flags & 0x01) max = wide ? (r.u64(), 0) : r.u32(); + if (kind === "memory" && flags & 0x02 && max === null && !wide) { + this.invalid("shared memory must have maximum", r.pos); + } + return { min, max }; + } + + memoryType(r: Reader): void { + const offset = r.pos; + const { min, max } = this.limits(r, "memory"); + if (this.memory64) return; + if (min > MAX_MEMORY_PAGES) + this.invalid("memory size must be at most 65536 pages (4GiB)", offset); + if (max !== null && max > MAX_MEMORY_PAGES) { + this.invalid("memory size must be at most 65536 pages (4GiB)", offset); + } + if (max !== null && min > max) + this.invalid("size minimum must not be greater than maximum", offset); + } + + addMemory(): void { + this.memories++; + if (this.memories > 1) this.feature("multi-memory"); + } + + tableType(r: Reader): Table { + const offset = r.pos; + if (r.peek() === 0x40) { + // A table with an explicit initializer (function references). + r.u8(); + if (r.u8() !== 0x00) r.malformed("malformed table type"); + this.feature("function-references"); + } + const type = this.refType(r); + const { min, max } = this.limits(r, "table"); + if (min > MAX_TABLE_INIT) this.invalid("table size must be at most 10000000", offset); + if (max !== null && min > max) + this.invalid("size minimum must not be greater than maximum", offset); + return { type }; + } + + addTable(table: Table): void { + this.tables.push(table); + if (this.tables.length > 1) this.feature("reference-types"); + if (table.type === EXTERNREF) this.feature("reference-types"); + } + + globalType(r: Reader): Global { + const type = this.valType(r); + if (type === FUNCREF || type === EXTERNREF) this.feature("reference-types"); + const mut = r.u8(); + if (mut > 1) r.malformed("malformed mutability"); + return { type, mutable: mut === 1, imported: false }; + } + + // ── sections ───────────────────────────────────────────────────────── + + importSection(r: Reader): void { + const count = r.u32(); + if (count > MAX_IMPORTS) r.malformed("too many imports"); + for (let i = 0; i < count; i++) { + const module = r.name(); + r.name(); + if (!this.importModules.includes(module)) this.importModules.push(module); + this.imports++; + const offset = r.pos; + const kind = r.u8(); + switch (kind) { + case 0x00: { + const index = r.u32(); + this.funcTypeAt(index, offset); + this.funcs.push(index); + this.importedFuncs++; + break; + } + case 0x01: + this.addTable(this.tableType(r)); + break; + case 0x02: + this.memoryType(r); + this.addMemory(); + break; + case 0x03: { + const global = this.globalType(r); + if (global.mutable) this.feature("mutable-globals"); + global.imported = true; + this.globals.push(global); + this.importedGlobals++; + break; + } + case 0x04: + this.tagType(r); + break; + default: + r.malformed("malformed import kind"); + } + } + } + + tagType(r: Reader): void { + const offset = r.pos; + this.feature("exceptions"); + if (r.u8() !== 0x00) r.malformed("malformed tag attribute"); + const type = this.funcTypeAt(r.u32(), offset); + if (type.results.length !== 0 && !this.gcTypes) + this.invalid("non-empty tag result type", offset); + this.tags++; + } + + functionSection(r: Reader): void { + const count = r.u32(); + if (count > MAX_FUNCTIONS) r.malformed("too many functions"); + for (let i = 0; i < count; i++) { + const offset = r.pos; + const index = r.u32(); + this.funcTypeAt(index, offset); + this.funcs.push(index); + } + } + + tableSection(r: Reader): void { + const count = r.u32(); + for (let i = 0; i < count; i++) { + const hasInit = r.peek() === 0x40; + const table = this.tableType(r); + if (hasInit) this.constExpr(r, table.type); + this.addTable(table); + } + } + + memorySection(r: Reader): void { + const count = r.u32(); + for (let i = 0; i < count; i++) { + this.memoryType(r); + this.addMemory(); + } + } + + tagSection(r: Reader): void { + const count = r.u32(); + for (let i = 0; i < count; i++) this.tagType(r); + } + + globalSection(r: Reader): void { + const count = r.u32(); + if (count + this.globals.length > MAX_GLOBALS) r.malformed("too many globals"); + for (let i = 0; i < count; i++) { + const global = this.globalType(r); + this.constExpr(r, global.type); + this.globals.push(global); + } + } + + exportSection(r: Reader): void { + const count = r.u32(); + if (count > MAX_EXPORTS) r.malformed("too many exports"); + for (let i = 0; i < count; i++) { + const offset = r.pos; + const name = r.name(); + if (this.exportNames.has(name)) this.invalid("duplicate export name", offset); + this.exportNames.add(name); + this.exports++; + const kind = r.u8(); + const indexOffset = r.pos; + const index = r.u32(); + switch (kind) { + case 0x00: + if (index >= this.funcs.length) this.invalid(`unknown function ${index}`, indexOffset); + this.declaredRefs.add(index); + break; + case 0x01: + if (index >= this.tables.length) this.invalid(`unknown table ${index}`, indexOffset); + break; + case 0x02: + if (index >= this.memories) this.invalid(`unknown memory ${index}`, indexOffset); + break; + case 0x03: + if (index >= this.globals.length) this.invalid(`unknown global ${index}`, indexOffset); + if (this.globals[index]!.mutable) this.feature("mutable-globals"); + break; + case 0x04: + if (index >= this.tags) this.invalid(`unknown tag ${index}`, indexOffset); + break; + default: + r.malformed("malformed export kind"); + } + } + } + + startSection(r: Reader): void { + const offset = r.pos; + const index = r.u32(); + if (index >= this.funcs.length) this.invalid(`unknown function ${index}`, offset); + const type = this.funcTypeAt(this.funcs[index]!, offset); + if (type.params.length !== 0 || type.results.length !== 0) { + this.invalid("start function must have type [] -> []", offset); + } + } + + elementSection(r: Reader): void { + const count = r.u32(); + if (count > MAX_TABLE_INIT) r.malformed("too many element segments"); + for (let i = 0; i < count; i++) { + const offset = r.pos; + const flags = r.u32(); + if (flags > 7) r.malformed("malformed elements segment kind"); + const passiveOrDeclarative = (flags & 0x01) !== 0; + const explicitTable = (flags & 0x02) !== 0; + const usesExprs = (flags & 0x04) !== 0; + if (flags !== 0) this.feature(usesExprs || explicitTable ? "reference-types" : "bulk-memory"); + let tableIndex = 0; + if (!passiveOrDeclarative) { + if (explicitTable) tableIndex = r.u32(); + if (tableIndex >= this.tables.length) this.invalid(`unknown table ${tableIndex}`, offset); + this.constExpr(r, this.memory64 ? UNKNOWN : I32); + } + let elemType = FUNCREF; + if (passiveOrDeclarative || explicitTable) { + if (usesExprs) { + elemType = this.refType(r); + } else if (r.u8() !== 0x00) { + r.malformed("malformed element kind"); + } + } + if (!passiveOrDeclarative) { + const tableType = this.tables[tableIndex]!.type; + if (elemType !== UNKNOWN && tableType !== UNKNOWN && elemType !== tableType) { + this.invalid("type mismatch", offset); + } + } + const n = r.u32(); + for (let k = 0; k < n; k++) { + if (usesExprs) { + this.constExpr(r, elemType); + } else { + const at = r.pos; + const index = r.u32(); + if (index >= this.funcs.length) this.invalid(`unknown function ${index}`, at); + this.declaredRefs.add(index); + } + } + this.elemTypes.push(elemType); + } + } + + dataCountSection(r: Reader): void { + this.dataCount = r.u32(); + this.feature("bulk-memory"); + } + + dataSection(r: Reader): void { + const count = r.u32(); + if (count > MAX_DATA_SEGMENTS) r.malformed("too many data segments"); + if (this.dataCount !== null && count !== this.dataCount) { + r.malformed("data count and data section have inconsistent lengths"); + } + for (let i = 0; i < count; i++) { + const offset = r.pos; + const flags = r.u32(); + if (flags > 2) r.malformed("malformed data segment kind"); + if (flags !== 1) { + let memory = 0; + if (flags === 2) memory = r.u32(); + if (memory !== 0) this.feature("multi-memory"); + if (memory >= this.memories) this.invalid(`unknown memory ${memory}`, offset); + this.constExpr(r, this.memory64 ? UNKNOWN : I32); + } else { + this.feature("bulk-memory"); + } + const length = r.u32(); + r.skip(length); + } + this.dataSegments = count; + } + + customSection(r: Reader): void { + this.customSections.push(r.name()); + r.pos = r.end; + } + + // ── constant expressions ──────────────────────────────────────────── + + /** A constant expression producing `expected` (UNKNOWN: not checked). + * global.get may read any earlier immutable global, as in 3.0 and V8 + * (2.0 allowed only imported ones in global initializers). */ + constExpr(r: Reader, expected: number): void { + const stack: number[] = []; + const start = r.pos; + for (;;) { + const offset = r.pos; + const op = r.u8(); + switch (op) { + case 0x0b: { + if (expected !== UNKNOWN) { + if (stack.length !== 1 || !matches(stack[0]!, expected)) { + this.invalid("type mismatch", start); + } + } + return; + } + case 0x41: + r.sleb(32); + stack.push(I32); + break; + case 0x42: + r.sleb(64); + stack.push(I64); + break; + case 0x43: + r.skip(4); + stack.push(F32); + break; + case 0x44: + r.skip(8); + stack.push(F64); + break; + case 0x23: { + // A global initializer sees the globals before it (they are all + // that exist yet); offsets see every global. + const index = r.u32(); + if (index >= this.globals.length) this.invalid(`unknown global ${index}`, offset); + const global = this.globals[index]!; + if (global.mutable) this.invalid("constant expression required", offset); + stack.push(global.type); + break; + } + case 0xd0: { + this.feature("reference-types"); + const t = this.refHeapNull(r); + stack.push(t); + break; + } + case 0xd2: { + this.feature("reference-types"); + const index = r.u32(); + if (index >= this.funcs.length) this.invalid(`unknown function ${index}`, offset); + this.declaredRefs.add(index); + stack.push(FUNCREF); + break; + } + case 0x6a: + case 0x6b: + case 0x6c: + case 0x7c: + case 0x7d: + case 0x7e: { + this.feature("extended-const"); + const t = op <= 0x6c ? I32 : I64; + const b = stack.pop(); + const a = stack.pop(); + if (a === undefined || b === undefined || !matches(a, t) || !matches(b, t)) { + this.invalid("type mismatch", offset); + } + stack.push(t); + break; + } + case 0xfd: { + this.feature("simd"); + const sub = r.u32(); + if (sub !== 12) this.invalid("constant expression required", offset); + r.skip(16); + stack.push(V128); + break; + } + case 0xfb: + // GC's struct.new and friends: not typed here. + this.feature("gc"); + return this.skipToEnd(r); + default: + this.invalid("constant expression required", offset); + } + } + } + + skipToEnd(r: Reader): void { + // Only reachable for GC constant expressions, whose immediates this + // decoder does not model: skip to the next end byte. + while (r.u8() !== 0x0b) { + /* scan */ + } + } + + /** ref.null's heap type, as a value type (UNKNOWN for GC heap types). */ + refHeapNull(r: Reader): number { + const b = r.peek(); + if (b === FUNCREF || b === EXTERNREF) { + r.u8(); + return b; + } + this.heapType(r); + return UNKNOWN; + } + + // ── code ───────────────────────────────────────────────────────────── + + codeSection(r: Reader): void { + const count = r.u32(); + this.sawCode = true; + const defined = this.funcs.length - this.importedFuncs; + if (count !== defined) r.malformed("function and code section have inconsistent lengths"); + this.codeCount = count; + for (let i = 0; i < count; i++) { + const size = r.u32(); + if (size > r.end - r.pos) r.malformed("length out of bounds"); + if (size > MAX_FUNCTION_SIZE) r.malformed("function body too large"); + const body = new Reader(this.bytes, r.pos, r.pos + size); + const type = this.funcTypeAt(this.funcs[this.importedFuncs + i]!, r.pos); + new FunctionValidator(this, body, type).run(); + r.pos += size; + } + } + + decode(): void { + const bytes = this.bytes; + const r = new Reader(bytes, 0, bytes.length); + if ( + bytes.length < 4 || + bytes[0] !== 0 || + bytes[1] !== 0x61 || + bytes[2] !== 0x73 || + bytes[3] !== 0x6d + ) { + r.malformed("magic header not detected"); + } + r.pos = 4; + if (bytes.length < 8) r.malformed("unexpected end"); + if (bytes[4] !== 1 || bytes[5] !== 0 || bytes[6] !== 0 || bytes[7] !== 0) { + r.malformed("unknown binary version"); + } + r.pos = 8; + let last = 0; + while (!r.eof()) { + const id = r.u8(); + const size = r.u32(); + if (size > r.end - r.pos) r.malformed("section size out of bounds"); + const section = new Reader(bytes, r.pos, r.pos + size); + if (id !== 0) { + const order = SECTION_ORDER[id]; + if (order === undefined) return r.malformed("malformed section id"); + if (order <= last) r.malformed("unexpected section (out of order or duplicate)"); + last = order; + } + if (!(this.gcTypes && id !== 0)) { + switch (id) { + case 0: + this.customSection(section); + break; + case 1: + this.typeSection(section); + break; + case 2: + this.importSection(section); + break; + case 3: + this.functionSection(section); + break; + case 4: + this.tableSection(section); + break; + case 5: + this.memorySection(section); + break; + case 6: + this.globalSection(section); + break; + case 7: + this.exportSection(section); + break; + case 8: + this.startSection(section); + break; + case 9: + this.elementSection(section); + break; + case 10: + this.codeSection(section); + break; + case 11: + this.dataSection(section); + break; + case 12: + this.dataCountSection(section); + break; + case 13: + this.tagSection(section); + break; + } + } else { + section.pos = section.end; + } + if (section.pos !== section.end) { + r.pos = section.pos; + r.malformed("section size mismatch"); + } + r.pos = section.end; + } + if (!this.gcTypes) { + if (!this.sawCode && this.funcs.length > this.importedFuncs) { + r.malformed("function and code section have inconsistent lengths"); + } + if (this.dataCount !== null && this.dataSegments !== this.dataCount && this.dataCount !== 0) { + r.malformed("data count and data section have inconsistent lengths"); + } + } + } +} + +function isRef(t: number): boolean { + return t === FUNCREF || t === EXTERNREF; +} + +function isNum(t: number): boolean { + return t === I32 || t === I64 || t === F32 || t === F64 || t === V128; +} + +function matches(actual: number, expected: number): boolean { + return actual === expected || actual === UNKNOWN || expected === UNKNOWN; +} + +/** Validates one function body (the algorithm of the specification's + * validation appendix). Once an instruction needs a proposal outside the + * translatable set, the rest of the body is only decoded. */ +class FunctionValidator { + m: ModuleDecoder; + r: Reader; + type: FuncType; + locals: number[] = []; + vals: number[] = []; + ctrls: Frame[] = []; + typed = true; + + constructor(m: ModuleDecoder, r: Reader, type: FuncType) { + this.m = m; + this.r = r; + this.type = type; + } + + invalid(message: string): never { + throw new WasmError("invalid", message, this.r.pos); + } + + /** Stops typing: the rest of the body only decodes. */ + untyped(feature: WasmFeature): void { + this.m.feature(feature); + this.typed = false; + } + + run(): void { + const r = this.r; + for (const p of this.type.params) this.locals.push(p); + const groups = r.u32(); + let total = this.locals.length; + for (let i = 0; i < groups; i++) { + const n = r.u32(); + total += n; + if (total > MAX_LOCALS) r.malformed("too many locals"); + const t = this.m.valType(r); + if (t === V128) this.typed = false; + if (isRef(t)) this.m.feature("reference-types"); + for (let k = 0; k < n; k++) this.locals.push(t); + } + if (this.type.params.includes(V128) || this.type.results.includes(V128)) this.typed = false; + if (this.m.memory64 || this.m.gcTypes) this.typed = false; + this.pushCtrl(-1, [], this.type.results); + while (this.ctrls.length > 0) { + if (r.eof()) r.malformed("unexpected end of section or function"); + this.instruction(); + } + if (!r.eof()) r.malformed("operators remaining after end of function"); + } + + // The operand stack. + push(t: number): void { + this.vals.push(t); + } + + pop(): number { + const frame = this.ctrls[this.ctrls.length - 1]!; + if (this.vals.length === frame.height) { + if (frame.unreachable) return UNKNOWN; + this.invalid("type mismatch"); + } + return this.vals.pop()!; + } + + popExpect(expected: number): number { + const actual = this.pop(); + if (!matches(actual, expected)) this.invalid("type mismatch"); + // Unreachable code's unknown operand stays unknown when pushed back + // (br_if, br_table), so later labels can still meet it. + return actual; + } + + pushAll(types: number[]): void { + for (const t of types) this.push(t); + } + + popAll(types: number[]): number[] { + const out: number[] = []; + for (let i = types.length - 1; i >= 0; i--) out.unshift(this.popExpect(types[i]!)); + return out; + } + + pushCtrl(opcode: number, start: number[], end: number[]): void { + this.ctrls.push({ + opcode, + start, + end, + height: this.vals.length, + unreachable: false, + hasElse: false, + }); + this.pushAll(start); + } + + popCtrl(): Frame { + if (this.ctrls.length === 0) this.invalid("control stack underflow"); + const frame = this.ctrls[this.ctrls.length - 1]!; + this.popAll(frame.end); + if (this.vals.length !== frame.height) this.invalid("type mismatch"); + this.ctrls.pop(); + return frame; + } + + labelTypes(frame: Frame): number[] { + return frame.opcode === 0x03 ? frame.start : frame.end; + } + + setUnreachable(): void { + const frame = this.ctrls[this.ctrls.length - 1]!; + this.vals.length = frame.height; + frame.unreachable = true; + } + + label(depth: number): Frame { + if (depth >= this.ctrls.length) this.invalid(`unknown label ${depth}`); + return this.ctrls[this.ctrls.length - 1 - depth]!; + } + + blockType(): FuncType { + const r = this.r; + const b = r.peek(); + if (b === 0x40) { + r.u8(); + return { params: [], results: [] }; + } + if ( + b === I32 || + b === I64 || + b === F32 || + b === F64 || + b === V128 || + b === FUNCREF || + b === EXTERNREF + ) { + return { params: [], results: [this.m.valType(r)] }; + } + if (b >= 0x60) { + return { params: [], results: [this.m.valType(r)] }; + } + const offset = r.pos; + const index = r.sleb(33); + if (index < 0) r.malformed("malformed block type"); + this.m.feature("multi-value"); + return this.m.funcTypeAt(index, offset); + } + + local(index: number): number { + if (index >= this.locals.length) this.invalid(`unknown local ${index}`); + return this.locals[index]!; + } + + global(index: number): Global { + if (index >= this.m.globals.length) this.invalid(`unknown global ${index}`); + return this.m.globals[index]!; + } + + table(index: number): Table { + if (index >= this.m.tables.length) this.invalid(`unknown table ${index}`); + return this.m.tables[index]!; + } + + /** A memory other than 0 (multi-memory): it must exist; typing stops. */ + otherMemory(index: number): void { + if (index >= this.m.memories) this.invalid(`unknown memory ${index}`); + this.untyped("multi-memory"); + } + + memoryZero(): void { + if (this.m.memories === 0) this.invalid("unknown memory 0"); + } + + func(index: number): FuncType { + if (index >= this.m.funcs.length) this.invalid(`unknown function ${index}`); + return this.m.funcTypeAt(this.m.funcs[index]!, this.r.pos); + } + + /** A memarg. A memory other than 0 stops typing (multi-memory). */ + memarg(naturalAlign: number): void { + const r = this.r; + let align = r.u32(); + let multi = false; + if (align >= 64 && align < 128) { + align -= 64; + multi = true; + } else if (align >= 64) { + r.malformed("malformed memop flags"); + } + let memory = 0; + if (multi) { + memory = r.u32(); + if (memory !== 0) this.otherMemory(memory); + } + if (this.m.memory64) r.u64(); + else r.u32(); + if (!this.typed) return; + if (memory >= this.m.memories) this.invalid(`unknown memory ${memory}`); + if (align > naturalAlign) this.invalid("alignment must not be larger than natural"); + } + + instruction(): void { + const r = this.r; + const op = r.u8(); + if (!this.typed) { + this.scan(op); + return; + } + switch (op) { + case 0x00: + this.setUnreachable(); + return; + case 0x01: + return; + case 0x02: + case 0x03: { + const bt = this.blockType(); + this.popAll(bt.params); + this.pushCtrl(op, bt.params, bt.results); + return; + } + case 0x04: { + const bt = this.blockType(); + this.popExpect(I32); + this.popAll(bt.params); + this.pushCtrl(op, bt.params, bt.results); + return; + } + case 0x05: { + const frame = this.ctrls[this.ctrls.length - 1]!; + if (frame.opcode !== 0x04 || frame.hasElse) this.invalid("else without matching if"); + this.popCtrl(); + this.pushCtrl(0x04, frame.start, frame.end); + this.ctrls[this.ctrls.length - 1]!.hasElse = true; + return; + } + case 0x0b: { + const frame = this.popCtrl(); + // An if without else must leave its params as its results. + if (frame.opcode === 0x04 && !frame.hasElse) { + if (frame.start.length !== frame.end.length) this.invalid("type mismatch"); + for (let i = 0; i < frame.start.length; i++) { + if (frame.start[i] !== frame.end[i]) this.invalid("type mismatch"); + } + } + this.pushAll(frame.end); + return; + } + case 0x0c: { + const depth = r.u32(); + this.popAll(this.labelTypes(this.label(depth))); + this.setUnreachable(); + return; + } + case 0x0d: { + const depth = r.u32(); + this.popExpect(I32); + // br_if leaves the label's types (even over unknown operands); + // br_table below leaves what it popped. + const types = this.labelTypes(this.label(depth)); + this.popAll(types); + this.pushAll(types); + return; + } + case 0x0e: { + const n = r.u32(); + const targets: number[] = []; + for (let i = 0; i < n; i++) targets.push(r.u32()); + const defaultDepth = r.u32(); + this.popExpect(I32); + const arity = this.labelTypes(this.label(defaultDepth)).length; + for (const depth of targets) { + const types = this.labelTypes(this.label(depth)); + if (types.length !== arity) this.invalid("type mismatch"); + this.pushAll(this.popAll(types)); + } + this.popAll(this.labelTypes(this.label(defaultDepth))); + this.setUnreachable(); + return; + } + case 0x0f: + this.popAll(this.type.results); + this.setUnreachable(); + return; + case 0x10: { + const type = this.func(r.u32()); + this.popAll(type.params); + this.pushAll(type.results); + return; + } + case 0x11: { + const typeIndex = r.u32(); + const tableIndex = r.u32(); + if (tableIndex !== 0) this.m.feature("reference-types"); + const table = this.table(tableIndex); + if (table.type !== FUNCREF && table.type !== UNKNOWN) this.invalid("type mismatch"); + const type = this.m.funcTypeAt(typeIndex, r.pos); + this.popExpect(I32); + this.popAll(type.params); + this.pushAll(type.results); + return; + } + case 0x12: + case 0x13: + case 0x14: + case 0x15: + this.untyped(op === 0x14 || op === 0x15 ? "function-references" : "tail-call"); + this.scan(op); + return; + case 0x06: + case 0x07: + case 0x08: + case 0x09: + case 0x0a: + case 0x18: + case 0x19: + case 0x1f: + this.untyped("exceptions"); + this.scan(op); + return; + case 0x1a: + this.pop(); + return; + case 0x1b: { + this.popExpect(I32); + const t1 = this.pop(); + const t2 = this.pop(); + if (!(isNum(t1) || t1 === UNKNOWN) || !(isNum(t2) || t2 === UNKNOWN)) + this.invalid("type mismatch"); + if (t1 !== t2 && t1 !== UNKNOWN && t2 !== UNKNOWN) this.invalid("type mismatch"); + this.push(t1 === UNKNOWN ? t2 : t1); + return; + } + case 0x1c: { + const n = r.u32(); + if (n !== 1) this.invalid("invalid result arity"); + const t = this.m.valType(r); + if (isRef(t)) this.m.feature("reference-types"); + this.popExpect(I32); + this.popExpect(t); + this.popExpect(t); + this.push(t); + return; + } + case 0x20: + this.push(this.local(r.u32())); + return; + case 0x21: + this.popExpect(this.local(r.u32())); + return; + case 0x22: { + const t = this.local(r.u32()); + this.popExpect(t); + this.push(t); + return; + } + case 0x23: + this.push(this.global(r.u32()).type); + return; + case 0x24: { + const global = this.global(r.u32()); + if (!global.mutable) this.invalid("global is immutable"); + this.popExpect(global.type); + return; + } + case 0x25: { + this.m.feature("reference-types"); + const table = this.table(r.u32()); + this.popExpect(I32); + this.push(table.type); + return; + } + case 0x26: { + this.m.feature("reference-types"); + const table = this.table(r.u32()); + this.popExpect(table.type); + this.popExpect(I32); + return; + } + case 0x3f: + case 0x40: { + const memory = r.u8(); + if (memory !== 0) { + // A memory index (multi-memory) or a malformed reserved byte. + r.pos--; + const index = r.u32(); + if (index !== 0) this.otherMemory(index); + if (!this.typed) return; + } + this.memoryZero(); + if (op === 0x40) this.popExpect(I32); + this.push(I32); + return; + } + case 0x41: + r.sleb(32); + this.push(I32); + return; + case 0x42: + r.sleb(64); + this.push(I64); + return; + case 0x43: + r.skip(4); + this.push(F32); + return; + case 0x44: + r.skip(8); + this.push(F64); + return; + case 0xd0: { + this.m.feature("reference-types"); + const t = this.m.refHeapNull(r); + if (t === UNKNOWN) { + this.typed = false; + return; + } + this.push(t); + return; + } + case 0xd1: { + this.m.feature("reference-types"); + const t = this.pop(); + if (!isRef(t) && t !== UNKNOWN) this.invalid("type mismatch"); + this.push(I32); + return; + } + case 0xd2: { + this.m.feature("reference-types"); + const index = r.u32(); + this.func(index); + if (!this.m.declaredRefs.has(index)) this.invalid("undeclared function reference"); + this.push(FUNCREF); + return; + } + case 0xd3: + case 0xd4: + case 0xd5: + case 0xd6: + this.untyped(op === 0xd3 ? "gc" : "function-references"); + this.scan(op); + return; + case 0xfc: + this.prefixFC(); + return; + case 0xfd: + this.untyped("simd"); + this.scanSimd(); + return; + case 0xfe: + this.untyped("threads"); + this.scanAtomic(); + return; + case 0xfb: + this.untyped("gc"); + r.pos = r.end; + this.ctrls.length = 0; + return; + } + if (op >= 0x28 && op <= 0x3e) { + const k = op - 0x28; + this.memarg(MEMORY_ALIGN[k]!); + if (!this.typed) return; + this.memoryZero(); + if (op <= 0x35) { + this.popExpect(I32); + this.push(MEMORY_TYPE[k]!); + } else { + this.popExpect(MEMORY_TYPE[k]!); + this.popExpect(I32); + } + return; + } + if (op >= 0x45 && op <= 0xc4) { + if (op >= 0xc0) this.m.feature("sign-extension"); + const [operand, count, result] = numericSignature(op); + for (let i = 0; i < count; i++) this.popExpect(operand); + this.push(result); + return; + } + r.pos--; + r.malformed(`illegal opcode 0x${op.toString(16)}`); + } + + prefixFC(): void { + const r = this.r; + const sub = r.u32(); + if (sub <= 7) { + this.m.feature("nontrapping-fptoint"); + const from = sub <= 1 || (sub >= 4 && sub <= 5) ? F32 : F64; + const to = sub <= 3 ? I32 : I64; + this.popExpect(from); + this.push(to); + return; + } + switch (sub) { + case 8: { + this.m.feature("bulk-memory"); + const segment = r.u32(); + const memory = r.u32(); + if (memory !== 0) { + this.otherMemory(memory); + return; + } + this.memoryZero(); + const dataCount = this.m.dataCount; + if (dataCount === null) return r.malformed("data count section required"); + if (segment >= dataCount) this.invalid(`unknown data segment ${segment}`); + this.popExpect(I32); + this.popExpect(I32); + this.popExpect(I32); + return; + } + case 9: { + this.m.feature("bulk-memory"); + const segment = r.u32(); + const dataCount = this.m.dataCount; + if (dataCount === null) return r.malformed("data count section required"); + if (segment >= dataCount) this.invalid(`unknown data segment ${segment}`); + return; + } + case 10: { + this.m.feature("bulk-memory"); + const dst = r.u32(); + const src = r.u32(); + if (dst !== 0 || src !== 0) { + this.otherMemory(Math.max(dst, src)); + return; + } + this.memoryZero(); + this.popExpect(I32); + this.popExpect(I32); + this.popExpect(I32); + return; + } + case 11: { + this.m.feature("bulk-memory"); + const memory = r.u32(); + if (memory !== 0) { + this.otherMemory(memory); + return; + } + this.memoryZero(); + this.popExpect(I32); + this.popExpect(I32); + this.popExpect(I32); + return; + } + case 12: { + this.m.feature("bulk-memory"); + const segment = r.u32(); + const table = this.table(r.u32()); + if (segment >= this.m.elemTypes.length) this.invalid(`unknown elem segment ${segment}`); + const elemType = this.m.elemTypes[segment]!; + if (!matches(elemType, table.type)) this.invalid("type mismatch"); + this.popExpect(I32); + this.popExpect(I32); + this.popExpect(I32); + return; + } + case 13: { + this.m.feature("bulk-memory"); + const segment = r.u32(); + if (segment >= this.m.elemTypes.length) this.invalid(`unknown elem segment ${segment}`); + return; + } + case 14: { + this.m.feature("bulk-memory"); + const dst = this.table(r.u32()); + const src = this.table(r.u32()); + if (!matches(src.type, dst.type)) this.invalid("type mismatch"); + this.popExpect(I32); + this.popExpect(I32); + this.popExpect(I32); + return; + } + case 15: { + this.m.feature("reference-types"); + const table = this.table(r.u32()); + this.popExpect(I32); + this.popExpect(table.type); + this.push(I32); + return; + } + case 16: + this.m.feature("reference-types"); + this.table(r.u32()); + this.push(I32); + return; + case 17: { + this.m.feature("reference-types"); + const table = this.table(r.u32()); + this.popExpect(I32); + this.popExpect(table.type); + this.popExpect(I32); + return; + } + } + r.malformed(`illegal opcode 0xfc ${sub}`); + } + + // ── decode-only scanning (proposals outside the translatable set) ───── + + /** One instruction, decoded for its immediates only. Tracks block + * nesting so that the body's final end is found. */ + scan(op: number): void { + const r = this.r; + switch (op) { + case 0x02: + case 0x03: + case 0x04: + case 0x06: + this.scanBlockType(); + this.ctrls.push({ + opcode: op, + start: [], + end: [], + height: 0, + unreachable: true, + hasElse: false, + }); + return; + case 0x1f: { + this.m.feature("exceptions"); + this.scanBlockType(); + const n = r.u32(); + for (let i = 0; i < n; i++) { + const kind = r.u8(); + if (kind > 3) r.malformed("malformed catch kind"); + if (kind < 2) r.u32(); + r.u32(); + } + this.ctrls.push({ + opcode: op, + start: [], + end: [], + height: 0, + unreachable: true, + hasElse: false, + }); + return; + } + case 0x0b: + this.ctrls.pop(); + return; + case 0x18: + // delegate ends its try block. + this.m.feature("exceptions"); + r.u32(); + this.ctrls.pop(); + return; + case 0xfc: + this.scanFC(); + return; + case 0xfd: + this.m.feature("simd"); + this.scanSimd(); + return; + case 0xfe: + this.m.feature("threads"); + this.scanAtomic(); + return; + case 0xfb: + this.m.feature("gc"); + r.pos = r.end; + this.ctrls.length = 0; + return; + } + this.scanImmediates(op); + } + + scanBlockType(): void { + const r = this.r; + const b = r.peek(); + if (b === 0x40) { + r.u8(); + return; + } + if (b >= 0x60) { + this.m.valType(r); + return; + } + if (r.sleb(33) < 0) r.malformed("malformed block type"); + } + + /** The immediates of a single-byte opcode that opens no block. */ + scanImmediates(op: number): void { + const r = this.r; + switch (op) { + case 0x00: + case 0x01: + case 0x05: + case 0x0f: + case 0x1a: + case 0x1b: + case 0x0a: + case 0x19: + case 0xd1: + case 0xd3: + case 0xd4: + if (op === 0x0a || op === 0x19) this.m.feature("exceptions"); + return; + case 0x07: + case 0x08: + case 0x09: + case 0x0c: + case 0x0d: + case 0x10: + case 0x12: + case 0x14: + case 0x15: + case 0x20: + case 0x21: + case 0x22: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0xd2: + case 0xd5: + case 0xd6: + if (op === 0x07 || op === 0x08 || op === 0x09) this.m.feature("exceptions"); + if (op === 0x12) this.m.feature("tail-call"); + if (op === 0x14 || op === 0x15 || op === 0xd5 || op === 0xd6) + this.m.feature("function-references"); + r.u32(); + return; + case 0x0e: { + const n = r.u32(); + for (let i = 0; i <= n; i++) r.u32(); + return; + } + case 0x11: + case 0x13: + if (op === 0x13) this.m.feature("tail-call"); + r.u32(); + r.u32(); + return; + case 0x1c: { + const n = r.u32(); + for (let i = 0; i < n; i++) this.m.valType(r); + return; + } + case 0x3f: + case 0x40: + r.u32(); + return; + case 0x41: + r.sleb(32); + return; + case 0x42: + r.sleb(64); + return; + case 0x43: + r.skip(4); + return; + case 0x44: + r.skip(8); + return; + case 0xd0: + this.m.refHeapNull(r); + return; + } + if (op >= 0x28 && op <= 0x3e) { + this.scanMemarg(); + return; + } + if (op >= 0x45 && op <= 0xc4) { + if (op >= 0xc0) this.m.feature("sign-extension"); + return; + } + r.pos--; + r.malformed(`illegal opcode 0x${op.toString(16)}`); + } + + scanMemarg(): void { + const r = this.r; + const align = r.u32(); + if (align >= 128) r.malformed("malformed memop flags"); + if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory"); + if (this.m.memory64) r.u64(); + else r.u32(); + } + + scanFC(): void { + const r = this.r; + const sub = r.u32(); + if (sub <= 7) { + this.m.feature("nontrapping-fptoint"); + return; + } + if (sub > 17) r.malformed(`illegal opcode 0xfc ${sub}`); + this.m.feature(sub >= 15 ? "reference-types" : "bulk-memory"); + switch (sub) { + case 8: + case 10: + case 12: + case 14: + r.u32(); + r.u32(); + return; + default: + r.u32(); + } + } + + scanSimd(): void { + const r = this.r; + const sub = r.u32(); + if (sub <= 11 || sub === 92 || sub === 93) { + this.scanMemarg(); + return; + } + if (sub === 12 || sub === 13) { + r.skip(16); + return; + } + if (sub >= 21 && sub <= 34) { + r.u8(); + return; + } + if (sub >= 84 && sub <= 91) { + this.scanMemarg(); + r.u8(); + return; + } + if (sub > 0x113) r.malformed(`illegal opcode 0xfd ${sub}`); + } + + scanAtomic(): void { + const r = this.r; + const sub = r.u32(); + if (sub === 0x03) { + if (r.u8() !== 0) r.malformed("zero byte expected"); + return; + } + if (sub > 0x4e || (sub > 0x03 && sub < 0x10)) r.malformed(`illegal opcode 0xfe ${sub}`); + this.scanMemarg(); + } +} + +/** Decodes and validates `bytes` (a whole module). Never throws. */ +export function analyzeWasm(bytes: Uint8Array): WasmModuleSummary { + const m = new ModuleDecoder(bytes); + let status: WasmModuleSummary["status"] = "valid"; + let error: string | null = null; + try { + m.decode(); + } catch (e) { + if (!(e instanceof WasmError)) throw e; + status = e.kind; + error = `${e.message} (at byte ${e.offset})`; + } + const features: string[] = []; + const unsupported: string[] = []; + for (const feature of WASM_FEATURES) { + if (!m.features.has(feature)) continue; + features.push(feature); + if (!TRANSLATABLE.has(feature)) unsupported.push(feature); + } + if (status === "valid" && unsupported.length > 0) status = "unsupported"; + return { + size: bytes.length, + status, + error, + features, + unsupported, + functions: Math.max(0, m.funcs.length - m.importedFuncs), + imports: m.imports, + exports: m.exports, + importModules: m.importModules, + memories: m.memories, + tables: m.tables.length, + globals: m.globals.length, + customSections: m.customSections, + }; +} + +/** True when `bytes` starts with the WebAssembly magic and version 1. */ +export function isWasmHeader(bytes: Uint8Array): boolean { + return ( + bytes.length >= 8 && + bytes[0] === 0 && + bytes[1] === 0x61 && + bytes[2] === 0x73 && + bytes[3] === 0x6d && + bytes[4] === 1 && + bytes[5] === 0 && + bytes[6] === 0 && + bytes[7] === 0 + ); +} diff --git a/packages/compiler/src/wasm/discover.test.ts b/packages/compiler/src/wasm/discover.test.ts new file mode 100644 index 000000000..1a3ba5e26 --- /dev/null +++ b/packages/compiler/src/wasm/discover.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "vitest"; +import { decodeBase64, packageRootOf } from "./discover.js"; + +describe("decodeBase64", () => { + test("decodes standard and URL-safe spellings, with or without padding", () => { + const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 0xfb, 0xff]); + const standard = Buffer.from(bytes).toString("base64"); + expect(decodeBase64(standard)).toEqual(bytes); + expect(decodeBase64(standard.replace(/=+$/, ""))).toEqual(bytes); + expect(decodeBase64(Buffer.from(bytes).toString("base64url"))).toEqual(bytes); + }); + + test("answers null for text that is not base64", () => { + expect(decodeBase64("AGFzbQ*")).toBeNull(); + expect(decodeBase64("AGFzb")).toBeNull(); + expect(decodeBase64("AGFzbQ===")).toBeNull(); + }); +}); + +describe("packageRootOf", () => { + test("finds the innermost package, scoped or not", () => { + expect(packageRootOf("/p/node_modules/quickjs-wasi/dist/index.js")).toBe( + "/p/node_modules/quickjs-wasi", + ); + expect(packageRootOf("/p/node_modules/a/node_modules/@scope/b/lib/x.js")).toBe( + "/p/node_modules/a/node_modules/@scope/b", + ); + expect(packageRootOf("/p/src/main.ts")).toBeNull(); + }); +}); diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts new file mode 100644 index 000000000..610a389f1 --- /dev/null +++ b/packages/compiler/src/wasm/discover.ts @@ -0,0 +1,426 @@ +/* Build-time discovery of the WebAssembly modules a program can load. + * + * `scriptc coverage` reports every module whose bytes the build can see, + * from four sources: + * + * 1. path: a `.wasm` path the compiler resolves from the source text + * (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, + * `new URL("./x.wasm", import.meta.url)`, and `path.join(__dirname, ...)` + * or `__dirname + "/x.wasm"`); + * 2. inline: a string literal that decodes as base64 to a module, or an + * array literal of its bytes; + * 3. package: every `.wasm` file shipped by a reached npm package whose + * program files mention `WebAssembly` (this finds paths computed at run + * time, such as a loader's default argument); + * 4. config: the `scriptc.wasm` list in the entry's package.json and the + * `--wasm ` flag. + * + * Each module is decoded and validated (binary.ts) and reported once, by + * content, with every source that found it. Nothing here changes what a + * build emits. */ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { dirname, isAbsolute, join, resolve } from "node:path"; +import * as ts from "../frontend/ts7/adapter.js"; +import { + trackedAccessibleEntries, + trackedFileExists, + trackedReadFile, +} from "../frontend/input-tracker.js"; +import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js"; + +export type WasmSourceKind = "path" | "inline" | "package" | "config"; + +export interface WasmSource { + kind: WasmSourceKind; + /** The source file (path, inline) or the file that names the module. */ + file: string; + /** 1-based line of the site, when there is one. */ + line?: number; +} + +/** One discovered module. JSON-shaped: the coverage verdict cache stores it. */ +export interface WasmModuleReport extends WasmModuleSummary { + sha256: string; + /** The module file, absent for inline modules. */ + path?: string; + sources: WasmSource[]; +} + +export interface WasmDiscoveryInput { + /** The program's source files (entry and modules). */ + files: readonly ts.SourceFile[]; + /** The entry file. */ + entry: string; + /** --wasm files, absolute. */ + configured: readonly string[]; + /** require.resolve from `fromFile`, or null when it does not resolve. */ + resolveRequest: (fromFile: string, specifier: string) => string | null; +} + +const SCAN_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.StringLiteral, + ts.SyntaxKind.NoSubstitutionTemplateLiteral, + ts.SyntaxKind.CallExpression, + ts.SyntaxKind.NewExpression, + ts.SyntaxKind.BinaryExpression, + ts.SyntaxKind.ArrayLiteralExpression, +]); + +/** The base64 spelling of a module's first bytes ("\0asm"). */ +const BASE64_MAGIC = "AGFzbQ"; +const BYTES_MAGIC = /\[\s*(?:0|0x0+)\s*,\s*(?:97|0x61)\s*,\s*(?:115|0x73)\s*,\s*(?:109|0x6d)\s*,/i; +/** Package directories deeper than this are not searched for modules. */ +const PACKAGE_SCAN_DEPTH = 6; +/** Larger files are reported by size but not read. */ +const MAX_MODULE_BYTES = 256 * 1024 * 1024; + +class Discovery { + input: WasmDiscoveryInput; + byHash = new Map(); + byPath = new Map(); + order: string[] = []; + + constructor(input: WasmDiscoveryInput) { + this.input = input; + } + + /** Records a module and one source that found it; answers its hash. */ + addBytes(bytes: Uint8Array, path: string | undefined, source: WasmSource): string { + const sha256 = createHash("sha256").update(bytes).digest("hex"); + let report = this.byHash.get(sha256); + if (report === undefined) { + const summary = analyzeWasm(bytes); + report = { + size: summary.size, + status: summary.status, + error: summary.error, + features: summary.features, + unsupported: summary.unsupported, + functions: summary.functions, + imports: summary.imports, + exports: summary.exports, + importModules: summary.importModules, + memories: summary.memories, + tables: summary.tables, + globals: summary.globals, + customSections: summary.customSections, + sha256, + sources: [], + }; + this.byHash.set(sha256, report); + this.order.push(sha256); + } + if (report.path === undefined && path !== undefined) report.path = path; + addSource(report, source); + return sha256; + } + + addFile(path: string, source: WasmSource): void { + path = resolve(path); + const known = this.byPath.get(path); + if (known !== undefined) { + addSource(this.byHash.get(known)!, source); + return; + } + // The tracked read makes the coverage verdict cache depend on the file. + if (trackedReadFile(path) === null) return; + let bytes: Uint8Array; + try { + bytes = new Uint8Array(readFileSync(path)); + } catch { + return; + } + if (bytes.length > MAX_MODULE_BYTES) return; + this.byPath.set(path, this.addBytes(bytes, path, source)); + } + + // ── source 1 and 2: the program's source text ─────────────────────── + + scanFile(sf: ts.SourceFile): void { + const text = sf.text; + const paths = text.includes(".wasm"); + const base64 = text.includes(BASE64_MAGIC); + const bytes = BYTES_MAGIC.test(text); + if (!paths && !base64 && !bytes) return; + const file = sf.fileName; + const lineOf = (node: ts.Node): number => + ts.getLineAndCharacterOfPosition(sf, node.getStart()).line + 1; + ts.walkPreorder( + sf, + (node) => { + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { + if (base64 && node.text.startsWith(BASE64_MAGIC)) { + const decoded = decodeBase64(node.text); + if (decoded !== null && isWasmHeader(decoded)) { + this.addBytes(decoded, undefined, { kind: "inline", file, line: lineOf(node) }); + } + } + return undefined; + } + if (ts.isArrayLiteralExpression(node)) { + if (bytes) { + const literal = byteArrayLiteral(node); + if (literal !== null && isWasmHeader(literal)) { + this.addBytes(literal, undefined, { kind: "inline", file, line: lineOf(node) }); + return "skip"; + } + } + return undefined; + } + if (!paths) return undefined; + const path = this.resolvedPath(sf, node); + if (path !== null) this.addFile(path, { kind: "path", file, line: lineOf(node) }); + return undefined; + }, + SCAN_KINDS, + ); + } + + /** The module path a resolution expression names, when it ends in .wasm. */ + resolvedPath(sf: ts.SourceFile, node: ts.Node): string | null { + const dir = dirname(sf.fileName); + if (ts.isCallExpression(node)) { + const callee = node.expression; + const first = node.arguments[0]; + if (first === undefined) return null; + // require.resolve / createRequire(...).resolve / import.meta.resolve + if (ts.isPropertyAccessExpression(callee) && callee.name.text === "resolve") { + const specifier = wasmLiteral(first); + if (specifier === null) { + return this.joinedPath(dir, node.arguments); + } + const receiver = callee.expression; + if (ts.isMetaProperty(receiver) || isRequireLike(receiver)) { + if (specifier.startsWith("./") || specifier.startsWith("../")) { + return resolve(dir, specifier); + } + if (isAbsolute(specifier)) return specifier; + return this.input.resolveRequest(sf.fileName, specifier); + } + return null; + } + // path.join(__dirname, "x.wasm") and its resolve/bare spellings + if ( + (ts.isPropertyAccessExpression(callee) && + (callee.name.text === "join" || callee.name.text === "resolve")) || + (ts.isIdentifier(callee) && (callee.text === "join" || callee.text === "resolve")) + ) { + return this.joinedPath(dir, node.arguments); + } + return null; + } + if (ts.isNewExpression(node)) { + // new URL("./x.wasm", import.meta.url) + if (!ts.isIdentifier(node.expression) || node.expression.text !== "URL") return null; + const args = node.arguments; + if (args === undefined || args.length !== 2) return null; + const specifier = wasmLiteral(args[0]!); + const base = args[1]!; + if (specifier === null || !isImportMetaUrl(base)) return null; + if (specifier.startsWith("./") || specifier.startsWith("../")) return resolve(dir, specifier); + return null; + } + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) { + // __dirname + "/x.wasm" + if (!isDirname(node.left)) return null; + const rest = wasmLiteral(node.right); + return rest === null ? null : join(dir, rest); + } + return null; + } + + joinedPath(dir: string, args: readonly ts.Expression[]): string | null { + if (args.length < 2 || !isDirname(args[0]!)) return null; + const parts: string[] = [dir]; + for (let i = 1; i < args.length; i++) { + const arg = args[i]!; + if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return null; + parts.push(arg.text); + } + if (!parts[parts.length - 1]!.endsWith(".wasm")) return null; + let path = parts[0]!; + for (let i = 1; i < parts.length; i++) path = resolve(path, parts[i]!); + return path; + } + + // ── source 3: package files ───────────────────────────────────────── + + scanPackages(): void { + const roots = new Map(); + for (const sf of this.input.files) { + const root = packageRootOf(sf.fileName); + if (root === null) continue; + const mentions = sf.text.includes("WebAssembly"); + roots.set(root, (roots.get(root) ?? false) || mentions); + } + for (const [root, mentions] of roots) { + if (!mentions) continue; + const found: string[] = []; + collectWasmFiles(root, 0, found); + found.sort(); + for (const path of found) + this.addFile(path, { kind: "package", file: join(root, "package.json") }); + } + } + + // ── source 4: configuration ───────────────────────────────────────── + + scanConfig(): void { + const manifest = nearestPackageJson(dirname(this.input.entry)); + if (manifest !== null) { + const text = trackedReadFile(manifest); + if (text !== null) { + let listed: unknown; + try { + const parsed = JSON.parse(text) as { scriptc?: { wasm?: unknown } }; + listed = parsed.scriptc?.wasm; + } catch { + listed = undefined; + } + if (Array.isArray(listed)) { + for (const entry of listed) { + if (typeof entry === "string") { + this.addFile(resolve(dirname(manifest), entry), { kind: "config", file: manifest }); + } + } + } + } + } + for (const path of this.input.configured) { + this.addFile(path, { kind: "config", file: path }); + } + } + + run(): WasmModuleReport[] { + for (const sf of this.input.files) this.scanFile(sf); + this.scanPackages(); + this.scanConfig(); + return this.order.map((sha) => this.byHash.get(sha)!); + } +} + +function addSource(report: WasmModuleReport, source: WasmSource): void { + for (const s of report.sources) { + if (s.kind === source.kind && s.file === source.file && s.line === source.line) return; + } + report.sources.push(source); +} + +function wasmLiteral(node: ts.Expression): string | null { + if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) return null; + return node.text.endsWith(".wasm") ? node.text : null; +} + +function isDirname(node: ts.Expression): boolean { + return ts.isIdentifier(node) && node.text === "__dirname"; +} + +function isImportMetaUrl(node: ts.Expression): boolean { + return ( + ts.isPropertyAccessExpression(node) && + node.name.text === "url" && + ts.isMetaProperty(node.expression) + ); +} + +/** `require`, a `createRequire(...)` call, or a binding named like one. */ +function isRequireLike(node: ts.Expression): boolean { + if (ts.isIdentifier(node)) return node.text === "require" || /require$/i.test(node.text); + if (ts.isCallExpression(node)) { + const callee = node.expression; + return ( + (ts.isIdentifier(callee) && callee.text === "createRequire") || + (ts.isPropertyAccessExpression(callee) && callee.name.text === "createRequire") + ); + } + return false; +} + +/** The bytes of an array literal of integer literals, or null. */ +function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null { + const elements = node.elements; + const out = new Uint8Array(elements.length); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]!; + if (!ts.isNumericLiteral(element)) return null; + const value = Number(element.text); + if (!Number.isInteger(value) || value < 0 || value > 255) return null; + out[i] = value; + } + return out; +} + +const BASE64_VALUES = (() => { + const table = new Int16Array(128).fill(-1); + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i; + // base64url spellings + table["-".charCodeAt(0)] = 62; + table["_".charCodeAt(0)] = 63; + return table; +})(); + +/** Decodes standard or URL-safe base64 (padding optional); null when the + * text is not base64. */ +export function decodeBase64(text: string): Uint8Array | null { + let end = text.length; + while (end > 0 && text.charCodeAt(end - 1) === 61) end--; + if (text.length - end > 2 || end % 4 === 1) return null; + const out = new Uint8Array(Math.floor((end * 3) / 4)); + let acc = 0; + let bits = 0; + let j = 0; + for (let i = 0; i < end; i++) { + const c = text.charCodeAt(i); + const v = c < 128 ? BASE64_VALUES[c]! : -1; + if (v < 0) return null; + acc = ((acc << 6) | v) & 0xffffff; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[j++] = (acc >> bits) & 0xff; + } + } + return j === out.length ? out : out.subarray(0, j); +} + +/** The root directory of the npm package that contains `file` (the + * directory below its innermost node_modules), or null outside one. */ +export function packageRootOf(file: string): string | null { + const marker = "/node_modules/"; + const at = file.lastIndexOf(marker); + if (at < 0) return null; + const rest = file.slice(at + marker.length); + const segments = rest.split("/"); + const take = segments[0]!.startsWith("@") ? 2 : 1; + if (segments.length <= take) return null; + return file.slice(0, at + marker.length) + segments.slice(0, take).join("/"); +} + +function collectWasmFiles(dir: string, depth: number, out: string[]): void { + const entries = trackedAccessibleEntries(dir); + if (entries === null) return; + for (const name of entries.files) if (name.endsWith(".wasm")) out.push(join(dir, name)); + if (depth >= PACKAGE_SCAN_DEPTH) return; + for (const name of entries.directories) { + if (name === "node_modules" || name.startsWith(".")) continue; + collectWasmFiles(join(dir, name), depth + 1, out); + } +} + +function nearestPackageJson(dir: string): string | null { + for (;;) { + const candidate = join(dir, "package.json"); + if (trackedFileExists(candidate)) return candidate; + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +/** Every WebAssembly module the program's sources, packages and + * configuration make visible to the build, in discovery order. */ +export function discoverWasmModules(input: WasmDiscoveryInput): WasmModuleReport[] { + return new Discovery(input).run(); +} diff --git a/packages/compiler/test/gen-wasm-spec-cases.mjs b/packages/compiler/test/gen-wasm-spec-cases.mjs new file mode 100644 index 000000000..068053f0e --- /dev/null +++ b/packages/compiler/test/gen-wasm-spec-cases.mjs @@ -0,0 +1,69 @@ +/* Regenerates wasm-spec-cases.txt: the binary modules of the WebAssembly + * specification test suite at the feature level the decoder validates + * (src/wasm/binary.ts), each with the suite's verdict and V8's. + * + * node gen-wasm-spec-cases.mjs --wast2json /bin/wast2json \ + * --spec /test/core \ + * --extended-const /test/core > wasm-spec-cases.txt + * + * Inputs: every .wast of the 2.0 core suite except simd/ (SIMD is outside + * the translatable set; the decoder only reports it), plus the extended + * constant expression proposal's global, data and elem tests. Modules given + * as text for assert_malformed are skipped (they test the text format). + * Identical modules are kept once, at their first location. + * + * Line format: : , where a + * verdict is "valid", "invalid" or "malformed" (V8: "valid" or "invalid"). + * The suite's tests are Apache-2.0 licensed (WebAssembly Community Group). */ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { parseArgs } from "node:util"; + +const { values } = parseArgs({ + options: { + wast2json: { type: "string" }, + spec: { type: "string" }, + "extended-const": { type: "string" }, + }, +}); +const inputs = []; +for (const name of readdirSync(values.spec).sort()) { + if (name.endsWith(".wast")) inputs.push({ label: name, path: join(values.spec, name) }); +} +for (const name of ["global.wast", "data.wast", "elem.wast"]) { + inputs.push({ label: `extended-const/${name}`, path: join(values["extended-const"], name) }); +} +const seen = new Set(); +const lines = [ + "# WebAssembly spec test modules for src/wasm/binary.ts; generated by gen-wasm-spec-cases.mjs.", + "# Sources: WebAssembly/spec wg-2.0 test/core (without simd/) and WebAssembly/extended-const", + "# test/core global, data and elem, converted with wabt 1.0.37 wast2json (Apache-2.0).", + "# : ", +]; +for (const { label, path } of inputs) { + const dir = mkdtempSync(join(tmpdir(), "wasm-spec-")); + try { + execFileSync(values.wast2json, [path, "-o", join(dir, "x.json")], { stdio: "pipe" }); + const json = JSON.parse(readFileSync(join(dir, "x.json"), "utf8")); + for (const command of json.commands) { + let verdict; + if (["module", "assert_unlinkable", "assert_uninstantiable"].includes(command.type)) + verdict = "valid"; + else if (command.type === "assert_invalid") verdict = "invalid"; + else if (command.type === "assert_malformed") verdict = "malformed"; + else continue; + if (command.module_type === "text" || !command.filename?.endsWith(".wasm")) continue; + const bytes = readFileSync(join(dir, command.filename)); + const b64 = bytes.toString("base64"); + if (seen.has(b64)) continue; + seen.add(b64); + const v8 = WebAssembly.validate(bytes) ? "valid" : "invalid"; + lines.push(`${verdict} ${v8} ${label}:${command.line} ${b64}`); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} +process.stdout.write(`${lines.join("\n")}\n`); diff --git a/packages/compiler/test/wasm-spec-cases.txt b/packages/compiler/test/wasm-spec-cases.txt new file mode 100644 index 000000000..16d2ae2b6 --- /dev/null +++ b/packages/compiler/test/wasm-spec-cases.txt @@ -0,0 +1,3017 @@ +# WebAssembly spec test modules for src/wasm/binary.ts; generated by gen-wasm-spec-cases.mjs. +# Sources: WebAssembly/spec wg-2.0 test/core (without simd/) and WebAssembly/extended-const +# test/core global, data and elem, converted with wabt 1.0.37 wast2json (Apache-2.0). +# : +valid valid address.wast:3 AGFzbQEAAAABCgJgAX8Bf2ABfwADHx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEFAwEAAQfNAh4IOHVfZ29vZDEAAAg4dV9nb29kMgABCDh1X2dvb2QzAAIIOHVfZ29vZDQAAwg4dV9nb29kNQAECDhzX2dvb2QxAAUIOHNfZ29vZDIABgg4c19nb29kMwAHCDhzX2dvb2Q0AAgIOHNfZ29vZDUACQkxNnVfZ29vZDEACgkxNnVfZ29vZDIACwkxNnVfZ29vZDMADAkxNnVfZ29vZDQADQkxNnVfZ29vZDUADgkxNnNfZ29vZDEADwkxNnNfZ29vZDIAEAkxNnNfZ29vZDMAEQkxNnNfZ29vZDQAEgkxNnNfZ29vZDUAEwgzMl9nb29kMQAUCDMyX2dvb2QyABUIMzJfZ29vZDMAFggzMl9nb29kNAAXCDMyX2dvb2Q1ABgGOHVfYmFkABkGOHNfYmFkABoHMTZ1X2JhZAAbBzE2c19iYWQAHAYzMl9iYWQAHQqKAh4HACAALQAACwcAIAAtAAALBwAgAC0AAQsHACAALQACCwcAIAAtABkLBwAgACwAAAsHACAALAAACwcAIAAsAAELBwAgACwAAgsHACAALAAZCwcAIAAvAQALBwAgAC8AAAsHACAALwABCwcAIAAvAQILBwAgAC8BGQsHACAALgEACwcAIAAuAAALBwAgAC4AAQsHACAALgECCwcAIAAuARkLBwAgACgCAAsHACAAKAAACwcAIAAoAAELBwAgACgBAgsHACAAKAIZCwwAIAAtAP////8PGgsMACAALAD/////DxoLDAAgAC8B/////w8aCwwAIAAuAf////8PGgsMACAAKAL/////DxoLCyABAEEACxphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eg== +valid valid address.wast:223 AGFzbQEAAAABCgJgAX8BfmABfwADKyoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEFAwEAAQfZAyoIOHVfZ29vZDEAAAg4dV9nb29kMgABCDh1X2dvb2QzAAIIOHVfZ29vZDQAAwg4dV9nb29kNQAECDhzX2dvb2QxAAUIOHNfZ29vZDIABgg4c19nb29kMwAHCDhzX2dvb2Q0AAgIOHNfZ29vZDUACQkxNnVfZ29vZDEACgkxNnVfZ29vZDIACwkxNnVfZ29vZDMADAkxNnVfZ29vZDQADQkxNnVfZ29vZDUADgkxNnNfZ29vZDEADwkxNnNfZ29vZDIAEAkxNnNfZ29vZDMAEQkxNnNfZ29vZDQAEgkxNnNfZ29vZDUAEwkzMnVfZ29vZDEAFAkzMnVfZ29vZDIAFQkzMnVfZ29vZDMAFgkzMnVfZ29vZDQAFwkzMnVfZ29vZDUAGAkzMnNfZ29vZDEAGQkzMnNfZ29vZDIAGgkzMnNfZ29vZDMAGwkzMnNfZ29vZDQAHAkzMnNfZ29vZDUAHQg2NF9nb29kMQAeCDY0X2dvb2QyAB8INjRfZ29vZDMAIAg2NF9nb29kNAAhCDY0X2dvb2Q1ACIGOHVfYmFkACMGOHNfYmFkACQHMTZ1X2JhZAAlBzE2c19iYWQAJgczMnVfYmFkACcHMzJzX2JhZAAoBjY0X2JhZAApCvQCKgcAIAAxAAALBwAgADEAAAsHACAAMQABCwcAIAAxAAILBwAgADEAGQsHACAAMAAACwcAIAAwAAALBwAgADAAAQsHACAAMAACCwcAIAAwABkLBwAgADMBAAsHACAAMwAACwcAIAAzAAELBwAgADMBAgsHACAAMwEZCwcAIAAyAQALBwAgADIAAAsHACAAMgABCwcAIAAyAQILBwAgADIBGQsHACAANQIACwcAIAA1AAALBwAgADUAAQsHACAANQECCwcAIAA1AhkLBwAgADQCAAsHACAANAAACwcAIAA0AAELBwAgADQBAgsHACAANAIZCwcAIAApAwALBwAgACkAAAsHACAAKQABCwcAIAApAQILBwAgACkDGQsMACAAMQD/////DxoLDAAgADAA/////w8aCwwAIAAzAf////8PGgsMACAAMgH/////DxoLDAAgADUC/////w8aCwwAIAA0Av////8PGgsMACAAKQP/////DxoLCyABAEEACxphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eg== +valid valid address.wast:514 AGFzbQEAAAABCgJgAX8BfWABfwADBwYAAAAAAAEFAwEAAQdBBggzMl9nb29kMQAACDMyX2dvb2QyAAEIMzJfZ29vZDMAAggzMl9nb29kNAADCDMyX2dvb2Q1AAQGMzJfYmFkAAUKNgYHACAAKgIACwcAIAAqAAALBwAgACoAAQsHACAAKgECCwcAIAAqAggLDAAgACoC/////w8aCwsSAQBBAAsMAAAAAAAAoH8BANB/ +valid valid address.wast:564 AGFzbQEAAAABCgJgAX8BfGABfwADBwYAAAAAAAEFAwEAAQdBBgg2NF9nb29kMQAACDY0X2dvb2QyAAEINjRfZ29vZDMAAgg2NF9nb29kNAADCDY0X2dvb2Q1AAQGNjRfYmFkAAUKNgYHACAAKwMACwcAIAArAAALBwAgACsAAQsHACAAKwECCwcAIAArAxILDAAgACsD/////w8aCwsgAQBBAAsaAAAAAAAAAAAAAAAAAAAAAPR/AQAAAAAA/H8= +valid valid align.wast:3 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALAAAGgs= +valid valid align.wast:4 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALQAAGgs= +valid valid align.wast:5 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALgEAGgs= +valid valid align.wast:6 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALwEAGgs= +valid valid align.wast:7 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKAIAGgs= +valid valid align.wast:8 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMAAAGgs= +valid valid align.wast:9 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMQAAGgs= +valid valid align.wast:10 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMgEAGgs= +valid valid align.wast:11 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMwEAGgs= +valid valid align.wast:12 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEANAIAGgs= +valid valid align.wast:13 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEANQIAGgs= +valid valid align.wast:14 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKQMAGgs= +valid valid align.wast:15 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKgIAGgs= +valid valid align.wast:16 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKwMAGgs= +valid valid align.wast:17 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQQE6AAAL +valid valid align.wast:18 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQQE7AQAL +valid valid align.wast:19 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQQE2AgAL +valid valid align.wast:20 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgE8AAAL +valid valid align.wast:21 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgE9AQAL +valid valid align.wast:22 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgE+AgAL +valid valid align.wast:23 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgE3AwAL +valid valid align.wast:24 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDgEMAEEAQwAAgD84AgAL +valid valid align.wast:25 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKEgEQAEEARAAAAAAAAPA/OQMACw== +invalid invalid align.wast:306 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALAEAGgs= +invalid invalid align.wast:310 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALQEAGgs= +invalid invalid align.wast:314 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALgIAGgs= +invalid invalid align.wast:318 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEALwIAGgs= +invalid invalid align.wast:322 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKAMAGgs= +invalid invalid align.wast:326 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMAEAGgs= +invalid invalid align.wast:330 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMQEAGgs= +invalid invalid align.wast:334 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMgIAGgs= +invalid invalid align.wast:338 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAMwIAGgs= +invalid invalid align.wast:342 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEANAMAGgs= +invalid invalid align.wast:346 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEANQMAGgs= +invalid invalid align.wast:350 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKQQAGgs= +invalid invalid align.wast:354 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKgMAGgs= +invalid invalid align.wast:358 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAKwQAGgs= +invalid invalid align.wast:420 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQQA6AQAL +invalid invalid align.wast:424 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQQA7AgAL +invalid invalid align.wast:428 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQQA2AwAL +invalid invalid align.wast:432 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgA8AQAL +invalid invalid align.wast:436 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgA9AgAL +invalid invalid align.wast:440 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgA+AwAL +invalid invalid align.wast:444 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQgA3BAAL +invalid invalid align.wast:448 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDgEMAEEAQwAAAAA4AwAL +invalid invalid align.wast:452 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKEgEQAEEARAAAAAAAAAAAOQQACw== +valid valid align.wast:458 AGFzbQEAAAABFwRgAX8BfWABfwF8YAJ/fwF/YAJ/fwF+AwUEAAECAwUDAQABB00EEGYzMl9hbGlnbl9zd2l0Y2gAABBmNjRfYWxpZ25fc3dpdGNoAAEQaTMyX2FsaWduX3N3aXRjaAACEGk2NF9hbGlnbl9zd2l0Y2gAAwr7CARjAQJ9QwAAIEEhAQJAAkACQAJAAkAgAA4EAAECAwQLQQAgATgCAEEAKgIAIQIMAwtBACABOAAAQQAqAAAhAgwCC0EAIAE4AQBBACoBACECDAELQQAgATgCAEEAKgIAIQILIAILewECfEQAAAAAAAAkQCEBAkACQAJAAkACQAJAIAAOBQABAgMEBQtBACABOQMAQQArAwAhAgwEC0EAIAE5AABBACsAACECDAMLQQAgATkBAEEAKwEAIQIMAgtBACABOQIAQQArAgAhAgwBC0EAIAE5AwBBACsDACECCyACC+ICAQJ/QQohAgJAAkACQAJAAkACQCAADgUAAQIDBAULIAFBAEYEQEEAIAI6AABBACwAACEDCyABQQFGBEBBACACOgAAQQAsAAAhAwsMBAsgAUEARgRAQQAgAjoAAEEALQAAIQMLIAFBAUYEQEEAIAI6AABBAC0AACEDCwwDCyABQQBGBEBBACACOwEAQQAuAQAhAwsgAUEBRgRAQQAgAjsAAEEALgAAIQMLIAFBAkYEQEEAIAI7AQBBAC4BACEDCwwCCyABQQBGBEBBACACOwEAQQAvAQAhAwsgAUEBRgRAQQAgAjsAAEEALwAAIQMLIAFBAkYEQEEAIAI7AQBBAC8BACEDCwwBCyABQQBGBEBBACACNgIAQQAoAgAhAwsgAUEBRgRAQQAgAjYAAEEAKAAAIQMLIAFBAkYEQEEAIAI2AQBBACgBACEDCyABQQRGBEBBACACNgIAQQAoAgAhAwsLIAMLtAQBAn5CCiECAkACQAJAAkACQAJAAkACQCAADgcAAQIDBAUGBwsgAUEARgRAQQAgAjwAAEEAMAAAIQMLIAFBAUYEQEEAIAI8AABBADAAACEDCwwGCyABQQBGBEBBACACPAAAQQAxAAAhAwsgAUEBRgRAQQAgAjwAAEEAMQAAIQMLDAULIAFBAEYEQEEAIAI9AQBBADIBACEDCyABQQFGBEBBACACPQAAQQAyAAAhAwsgAUECRgRAQQAgAj0BAEEAMgEAIQMLDAQLIAFBAEYEQEEAIAI9AQBBADMBACEDCyABQQFGBEBBACACPQAAQQAzAAAhAwsgAUECRgRAQQAgAj0BAEEAMwEAIQMLDAMLIAFBAEYEQEEAIAI+AgBBADQCACEDCyABQQFGBEBBACACPgAAQQA0AAAhAwsgAUECRgRAQQAgAj4BAEEANAEAIQMLIAFBBEYEQEEAIAI+AgBBADQCACEDCwwCCyABQQBGBEBBACACPgIAQQA1AgAhAwsgAUEBRgRAQQAgAj4AAEEANQAAIQMLIAFBAkYEQEEAIAI+AQBBADUBACEDCyABQQRGBEBBACACPgIAQQA1AgAhAwsMAQsgAUEARgRAQQAgAjcDAEEAKQMAIQMLIAFBAUYEQEEAIAI3AABBACkAACEDCyABQQJGBEBBACACNwEAQQApAQAhAwsgAUEERgRAQQAgAjcCAEEAKQIAIQMLIAFBCEYEQEEAIAI3AwBBACkDACEDCwsgAws= +valid valid align.wast:854 AGFzbQEAAAABCwJgAn9+AGABfwF/AwMCAAEFAwEAAQcQAgVzdG9yZQAABGxvYWQAAQoTAgkAIAAgATcCAAsHACAAKAIACw== +invalid invalid align.wast:873 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEEAKB8AGgs= +malformed invalid align.wast:892 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEEAKCAAGgs= +malformed invalid align.wast:911 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEEAKCEAGgs= +malformed invalid align.wast:930 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEEAKD8AGgs= +malformed invalid align.wast:949 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEEAKEAAGgs= +malformed invalid align.wast:968 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEEAKEEAGgs= +valid valid binary-leb128.wast:2 AGFzbQEAAAAFBAEAggA= +valid valid binary-leb128.wast:7 AGFzbQEAAAAFBwEAgoCAgAA= +valid valid binary-leb128.wast:12 AGFzbQEAAAAFBgEBggCCAA== +valid valid binary-leb128.wast:18 AGFzbQEAAAAFCQEBggCCgICAAA== +valid valid binary-leb128.wast:24 AGFzbQEAAAAFAwEAAAsHAYAAQQALAA== +valid valid binary-leb128.wast:32 AGFzbQEAAAAEBAFwAAAJCQECgABBAAsAAA== +valid valid binary-leb128.wast:41 AGFzbQEAAAAAigABMTIzNDU2Nzg5 +valid valid binary-leb128.wast:49 AGFzbQEAAAAAC4gAMTIzNDU2Nzg5 +valid valid binary-leb128.wast:57 AGFzbQEAAAABCAFgggB/fgF/ +valid valid binary-leb128.wast:66 AGFzbQEAAAABCAFgAn9+gQB/ +valid valid binary-leb128.wast:75 AGFzbQEAAAABBQFgAX8AAhcBiABzcGVjdGVzdAlwcmludF9pMzIAAA== +valid valid binary-leb128.wast:87 AGFzbQEAAAABBQFgAX8AAhcBCHNwZWN0ZXN0iQBwcmludF9pMzIAAA== +valid valid binary-leb128.wast:99 AGFzbQEAAAABBQFgAX8AAhcBCHNwZWN0ZXN0CXByaW50X2kzMgCAAA== +valid valid binary-leb128.wast:111 AGFzbQEAAAABBAFgAAADAwGAAAoEAQIACw== +valid valid binary-leb128.wast:120 AGFzbQEAAAABBAFgAAADAgEABwcBggBmMQAACgQBAgAL +valid valid binary-leb128.wast:133 AGFzbQEAAAABBAFgAAADAgEABwcBAmYxAIAACgQBAgAL +valid valid binary-leb128.wast:146 AGFzbQEAAAABBAFgAAADAgEACgWBAAIACw== +valid valid binary-leb128.wast:158 AGFzbQEAAAAGBwF/AEGAAAs= +valid valid binary-leb128.wast:165 AGFzbQEAAAAGBwF/AEH/fws= +valid valid binary-leb128.wast:172 AGFzbQEAAAAGCgF/AEGAgICAAAs= +valid valid binary-leb128.wast:179 AGFzbQEAAAAGCgF/AEH/////fws= +valid valid binary-leb128.wast:187 AGFzbQEAAAAGBwF+AEKAAAs= +valid valid binary-leb128.wast:194 AGFzbQEAAAAGBwF+AEL/fws= +valid valid binary-leb128.wast:201 AGFzbQEAAAAGDwF+AEKAgICAgICAgIAACw== +valid valid binary-leb128.wast:208 AGFzbQEAAAAGDwF+AEL///////////9/Cw== +malformed invalid binary-leb128.wast:218 AGFzbQEAAAAFCAEAgoCAgIAA +malformed invalid binary-leb128.wast:226 AGFzbQEAAAAFCgEBggCCgICAgAA= +malformed invalid binary-leb128.wast:235 AGFzbQEAAAAFAwEAAAsLAYCAgICAAEEACwA= +malformed invalid binary-leb128.wast:246 AGFzbQEAAAAEBAFwAAAJCwGAgICAgABBAAsA +malformed invalid binary-leb128.wast:257 AGFzbQEAAAAAg4CAgIAAATEy +malformed invalid binary-leb128.wast:268 AGFzbQEAAAAACoOAgICAADEyMzQ= +malformed invalid binary-leb128.wast:279 AGFzbQEAAAABDAFggoCAgIAAf34Bfw== +malformed invalid binary-leb128.wast:291 AGFzbQEAAAABDAFgAn9+gYCAgIAAfw== +malformed invalid binary-leb128.wast:303 AGFzbQEAAAABBQFgAX8AAhsBiICAgIAAc3BlY3Rlc3QJcHJpbnRfaTMyAAA= +malformed invalid binary-leb128.wast:318 AGFzbQEAAAABBQFgAX8AAhsBCHNwZWN0ZXN0iYCAgIAAcHJpbnRfaTMyAAA= +malformed invalid binary-leb128.wast:333 AGFzbQEAAAABBQFgAX8AAhsBCHNwZWN0ZXN0CXByaW50X2kzMgCAgICAgAA= +malformed invalid binary-leb128.wast:348 AGFzbQEAAAABBAFgAAADAwGAgICAgAAKBAECAAs= +malformed invalid binary-leb128.wast:360 AGFzbQEAAAABBAFgAAADAgEABwsBgoCAgIAAZjEAAAoEAQIACw== +malformed invalid binary-leb128.wast:376 AGFzbQEAAAABBAFgAAADAgEABwsBAmYxAICAgICAAAoEAQIACw== +malformed invalid binary-leb128.wast:392 AGFzbQEAAAABBAFgAAADAgEACgWBgICAgAACAAs= +malformed invalid binary-leb128.wast:405 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAQF/QQAoAoKAgICAgICAgIAAGgs= +malformed invalid binary-leb128.wast:424 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAQF/QQAogoCAgIAAABoL +malformed invalid binary-leb128.wast:443 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEgEQAQF/QQBBAzaCgICAgAADCw== +malformed invalid binary-leb128.wast:462 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEgEQAQF/QQBBAzYCgoCAgICAgICAgAAL +malformed invalid binary-leb128.wast:483 AGFzbQEAAAAGCwF/AEGAgICAgAAL +malformed invalid binary-leb128.wast:493 AGFzbQEAAAAGCwF/AEH//////38L +malformed invalid binary-leb128.wast:504 AGFzbQEAAAAGEAF+AEKAgICAgICAgICAAAs= +malformed invalid binary-leb128.wast:514 AGFzbQEAAAAGEAF+AEL/////////////fws= +malformed invalid binary-leb128.wast:526 AGFzbQEAAAAFBwEAgoCAgHA= +malformed invalid binary-leb128.wast:534 AGFzbQEAAAAFBwEAgoCAgEA= +malformed invalid binary-leb128.wast:542 AGFzbQEAAAAFCQEBggCCgICAEA== +malformed invalid binary-leb128.wast:551 AGFzbQEAAAAFCQEBggCCgICAQA== +malformed invalid binary-leb128.wast:560 AGFzbQEAAAAFAwEAAAsKAYCAgIAQQQALAA== +malformed invalid binary-leb128.wast:571 AGFzbQEAAAAEBAFwAAAJCgGAgICAEEEACwA= +malformed invalid binary-leb128.wast:582 AGFzbQEAAAAAg4CAgBABMTI= +malformed invalid binary-leb128.wast:593 AGFzbQEAAAAACYOAgIBAMTIzNA== +malformed invalid binary-leb128.wast:604 AGFzbQEAAAABCwFggoCAgBB/fgF/ +malformed invalid binary-leb128.wast:616 AGFzbQEAAAABCwFgAn9+gYCAgEB/ +malformed invalid binary-leb128.wast:628 AGFzbQEAAAABBQFgAX8AAhoBiICAgBBzcGVjdGVzdAlwcmludF9pMzIAAA== +malformed invalid binary-leb128.wast:643 AGFzbQEAAAABBQFgAX8AAhoBCHNwZWN0ZXN0iYCAgEBwcmludF9pMzIAAA== +malformed invalid binary-leb128.wast:658 AGFzbQEAAAABBQFgAX8AAhoBCHNwZWN0ZXN0CXByaW50X2kzMgCAgICAEA== +malformed invalid binary-leb128.wast:673 AGFzbQEAAAABBAFgAAADBgGAgICAEAoEAQIACw== +malformed invalid binary-leb128.wast:686 AGFzbQEAAAABBAFgAAADAgEABwoBgoCAgBBmMQAACgQBAgAL +malformed invalid binary-leb128.wast:702 AGFzbQEAAAABBAFgAAADAgEABwoBAmYxAICAgIAQCgQBAgAL +malformed invalid binary-leb128.wast:718 AGFzbQEAAAABBAFgAAADAgEACgiBgICAEAIACw== +malformed invalid binary-leb128.wast:731 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEAEOAQF/QQAoAoKAgICAgICAgBAaCw== +malformed invalid binary-leb128.wast:751 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEAEOAQF/QQAoAoKAgICAgICAgEAaCw== +malformed invalid binary-leb128.wast:771 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEAEOAQF/QQAogoCAgBAAGgs= +malformed invalid binary-leb128.wast:789 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEAEOAQF/QQAogoCAgEAAGgs= +malformed invalid binary-leb128.wast:808 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAQF/QQBBAzaCgICAEAML +malformed invalid binary-leb128.wast:827 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAQF/QQBBAzaCgICAQAML +malformed invalid binary-leb128.wast:846 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAQF/QQBBAzYCgoCAgICAgICAEAs= +malformed invalid binary-leb128.wast:866 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAQF/QQBBAzYCgoCAgICAgICAQAs= +malformed invalid binary-leb128.wast:888 AGFzbQEAAAAGCgF/AEGAgICAcAs= +malformed invalid binary-leb128.wast:898 AGFzbQEAAAAGCgF/AEH/////Dws= +malformed invalid binary-leb128.wast:908 AGFzbQEAAAAGCgF/AEGAgICAHws= +malformed invalid binary-leb128.wast:918 AGFzbQEAAAAGCgF/AEH/////Tws= +malformed invalid binary-leb128.wast:929 AGFzbQEAAAAGDwF+AEKAgICAgICAgIB+Cw== +malformed invalid binary-leb128.wast:939 AGFzbQEAAAAGDwF+AEL///////////8BCw== +malformed invalid binary-leb128.wast:949 AGFzbQEAAAAGDwF+AEKAgICAgICAgIACCw== +malformed invalid binary-leb128.wast:959 AGFzbQEAAAAGDwF+AEL///////////9BCw== +valid valid binary-leb128.wast:969 AGFzbQEAAAABBAFgAAADAgEAChsBGQAA/IAAAPyBgAAA/IaAgAAA/IeAgIAAAAs= +malformed invalid binary-leb128.wast:990 AGFzbQEAAAABBAFgAAADAgEACg0BCwAA/IeAgICAAAAL +valid valid binary-leb128.wast:1015 AGFzbQEAAAAFAwEAAAsIAYIAAEEACwA= +valid valid binary-leb128.wast:1024 AGFzbQEAAAAFAwEAAAsJAYIAgABBAAsA +valid valid binary-leb128.wast:1035 AGFzbQEAAAAEBAFwAAAJBwGAAEEACwA= +valid valid binary-leb128.wast:1052 AGFzbQEAAAAEBAFwAAAJCQGCAABBAAsAAA== +valid valid binary-leb128.wast:1061 AGFzbQEAAAAEBAFwAAAJCgGCAIAAQQALAAA= +malformed invalid binary-leb128.wast:1073 AGFzbQEAAAABBQHgfwAA +valid valid binary.wast:1 AGFzbQEAAAA= +malformed invalid binary.wast:6 +malformed invalid binary.wast:7 AQ== +malformed invalid binary.wast:8 AGFz +malformed invalid binary.wast:9 YXNtAA== +malformed invalid binary.wast:10 bXNhAA== +malformed invalid binary.wast:11 bXNhAAEAAAA= +malformed invalid binary.wast:12 bXNhAAAAAAE= +malformed invalid binary.wast:13 YXNtAQAAAAA= +malformed invalid binary.wast:14 d2FzbQEAAAA= +malformed invalid binary.wast:15 f2FzbQEAAAA= +malformed invalid binary.wast:16 gGFzbQEAAAA= +malformed invalid binary.wast:17 gmFzbQEAAAA= +malformed invalid binary.wast:18 /2FzbQEAAAA= +malformed invalid binary.wast:21 AAAAAW1zYQA= +malformed invalid binary.wast:24 YQBtcwABAAA= +malformed invalid binary.wast:25 c20AYQAAAQA= +malformed invalid binary.wast:28 AEFTTQEAAAA= +malformed invalid binary.wast:31 AIGilAEAAAA= +malformed invalid binary.wast:34 77u/AGFzbQEAAAA= +malformed invalid binary.wast:37 AGFzbQ== +malformed invalid binary.wast:38 AGFzbQE= +malformed invalid binary.wast:39 AGFzbQEAAA== +malformed invalid binary.wast:40 AGFzbQAAAAA= +malformed invalid binary.wast:41 AGFzbQ0AAAA= +malformed invalid binary.wast:42 AGFzbQ4AAAA= +malformed invalid binary.wast:43 AGFzbQABAAA= +malformed invalid binary.wast:44 AGFzbQAAAQA= +malformed invalid binary.wast:45 AGFzbQAAAAE= +malformed invalid binary.wast:48 AGFzbQEAAAAOAQA= +malformed invalid binary.wast:49 AGFzbQEAAAB/AQA= +malformed invalid binary.wast:50 AGFzbQEAAACAAQABAQA= +malformed invalid binary.wast:51 AGFzbQEAAACBAQABAQA= +malformed invalid binary.wast:52 AGFzbQEAAAD/AQABAQA= +malformed invalid binary.wast:56 AGFzbQEAAAABBAFgAAADAwIAAAoMAgQAQQEaBQBBARoL +malformed invalid binary.wast:77 AGFzbQEAAAABBAFgAAADAgEACgYBBABBARo= +malformed invalid binary.wast:93 AGFzbQEAAAABBAFgAAADAgEACgYBBABBARoLAwEBAA== +malformed invalid binary.wast:113 AGFzbQEAAAABBAFgAAADAgEABgUBfwBBAAoEAQIACw== +malformed invalid binary.wast:126 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCQEHAEEAQAEaCw== +malformed valid binary.wast:146 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAQIAAGgs= +malformed valid binary.wast:166 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAQICAABoL +malformed valid binary.wast:185 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDAEKAEEAQICAgAAaCw== +malformed valid binary.wast:204 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDQELAEEAQICAgIAAGgs= +malformed invalid binary.wast:224 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKBwEFAD8BGgs= +malformed valid binary.wast:243 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCAEGAD+AABoL +malformed valid binary.wast:262 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCQEHAD+AgAAaCw== +malformed valid binary.wast:280 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAD+AgIAAGgs= +malformed valid binary.wast:298 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAD+AgICAABoL +malformed invalid binary.wast:317 AGFzbQEAAAABBAFgAAADAgEACgwBCgKAgICAEH8Cfgs= +malformed invalid binary.wast:351 AGFzbQEAAAABBAFgAAADAgEACgwBCgL/////D38Cfgs= +malformed invalid binary.wast:367 AGFzbQEAAAABBgFgAn9/AAMCAQAKHAEaBICAgIAEf4CAgIAEfoCAgIAEfYCAgIAEfAs= +valid valid binary.wast:385 AGFzbQEAAAABBAFgAAADAgEACgoBCAMAfwB+An0L +malformed invalid binary.wast:401 AGFzbQEAAAABBAFgAAADAwIAAA== +malformed invalid binary.wast:411 AGFzbQEAAAAKBAECAAs= +malformed invalid binary.wast:420 AGFzbQEAAAABBAFgAAADAwIAAAoEAQIACw== +malformed invalid binary.wast:431 AGFzbQEAAAABBAFgAAADAgEACgcCAgALAgAL +valid valid binary.wast:441 AGFzbQEAAAADAQA= +valid valid binary.wast:447 AGFzbQEAAAAKAQA= +malformed invalid binary.wast:454 AGFzbQEAAAAMAQMLBQIBAAEA +malformed invalid binary.wast:466 AGFzbQEAAAAMAQELBQIBAAEA +malformed invalid binary.wast:478 AGFzbQEAAAAFAwEAAQwBAQ== +valid valid binary.wast:487 AGFzbQEAAAAMAQA= +malformed invalid binary.wast:494 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDgEMAEEAQQBBAPwIAAALCwMBAQA= +malformed invalid binary.wast:517 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKBwEFAPwJAAsLAwEBAA== +malformed invalid binary.wast:537 AGFzbQEAAAABBAFgAAADAgEABAQBcAAABQMBAAAJBwEFcAHzAAsKBAECAAs= +malformed invalid binary.wast:565 AGFzbQEAAAABBAFgAAADAgEABAQBcAAABQMBAAAJBwEFfwHSAAsKBAECAAs= +valid valid binary.wast:592 AGFzbQEAAAABBAFgAAADAgEABAQBcAAABQMBAAAJBwEFcAHSAAsKBAECAAs= +valid valid binary.wast:617 AGFzbQEAAAABBAFgAAADAgEABAQBcAAABQMBAAAJBwEFcAHQcAsKBAECAAs= +valid valid binary.wast:643 AGFzbQEAAAABAQA= +malformed invalid binary.wast:650 AGFzbQEAAAABBwJgAAA= +malformed invalid binary.wast:661 AGFzbQEAAAABBwFgAABgAAA= +valid valid binary.wast:671 AGFzbQEAAAABBQFgAX8AAgEA +malformed invalid binary.wast:680 AGFzbQEAAAACBAEAAAQ= +malformed invalid binary.wast:690 AGFzbQEAAAACBQEAAAQA +malformed invalid binary.wast:701 AGFzbQEAAAACBAEAAAU= +malformed invalid binary.wast:711 AGFzbQEAAAACBQEAAAUA +malformed invalid binary.wast:722 AGFzbQEAAAACBAEAAIA= +malformed invalid binary.wast:732 AGFzbQEAAAACBQEAAIAA +malformed invalid binary.wast:745 AGFzbQEAAAABBQFgAX8AAhYCCHNwZWN0ZXN0CXByaW50X2kzMgAA +malformed invalid binary.wast:764 AGFzbQEAAAABCQJgAX8AYAF9AAIrAQhzcGVjdGVzdAlwcmludF9pMzIAAAhzcGVjdGVzdAlwcmludF9mMzIAAQ== +valid valid binary.wast:788 AGFzbQEAAAAEAQA= +malformed invalid binary.wast:795 AGFzbQEAAAAEAQE= +malformed invalid binary.wast:805 AGFzbQEAAAAEAwFwAg== +malformed invalid binary.wast:814 AGFzbQEAAAAEBAFwCAA= +malformed invalid binary.wast:824 AGFzbQEAAAAEBgFwgQAAAA== +valid valid binary.wast:835 AGFzbQEAAAAFAQA= +malformed invalid binary.wast:842 AGFzbQEAAAAFAQE= +malformed invalid binary.wast:852 AGFzbQEAAAAFAgEC +malformed invalid binary.wast:860 AGFzbQEAAAAFAwEQAA== +malformed invalid binary.wast:869 AGFzbQEAAAAFBQGBAAAA +malformed invalid binary.wast:878 AGFzbQEAAAAFBQGBAQAA +valid valid binary.wast:888 AGFzbQEAAAAGAQA= +malformed invalid binary.wast:895 AGFzbQEAAAAGBgJ/AEEACw== +malformed invalid binary.wast:906 AGFzbQEAAAAGCwF/AEEAC38AQQAL +valid valid binary.wast:916 AGFzbQEAAAABBAFgAAADAwIAAAcBAAoHAgIACwIACw== +malformed invalid binary.wast:929 AGFzbQEAAAABBAFgAAADAwIAAAcGAgJmMQAACgcCAgALAgAL +malformed invalid binary.wast:950 AGFzbQEAAAABBAFgAAADAwIAAAcLAQJmMQAAAmYyAAEKBwICAAsCAAs= +valid valid binary.wast:970 AGFzbQEAAAABBAFgAAADAgEABAQBcAABCQEACgQBAgAL +malformed invalid binary.wast:984 AGFzbQEAAAABBAFgAAADAgEABAQBcAABCQcCAEEACwEA +malformed invalid binary.wast:1000 AGFzbQEAAAABBAFgAAADAgEABAQBcAABCQoCAEEACwEAAEEA +malformed invalid binary.wast:1017 AGFzbQEAAAABBAFgAAADAgEABAQBcAABCQ0BAEEACwEAAEEACwEACgQBAgAL +valid valid binary.wast:1034 AGFzbQEAAAAFAwEAAQsBAA== +malformed invalid binary.wast:1043 AGFzbQEAAAAFAwEAAQsHAgBBAAsBYQ== +malformed invalid binary.wast:1056 AGFzbQEAAAAFAwEAAQsNAQBBAAsBYQBBAQsBYg== +malformed invalid binary.wast:1069 AGFzbQEAAAAFAwEAAQsMAQBBAwsHYWJjZGVm +malformed invalid binary.wast:1083 AGFzbQEAAAAFAwEAAQsMAQBBAAsFYWJjZGVm +valid valid binary.wast:1096 AGFzbQEAAAABBAFgAAADAgEAChEBDwACQEEBBEBBAQ4AAgsLCw== +malformed invalid binary.wast:1114 AGFzbQEAAAABJQxgAABgAABgAABgAABgAABgAABgAABgAABgAABgAABgAABgAAADAgEAChMBEQACQEEBBEBBAQ4BAAECCwsL +valid valid binary.wast:1147 AGFzbQEAAAABBAFgAAADAgEACAEACgQBAgAL +malformed invalid binary.wast:1161 AGFzbQEAAAABBAFgAAADAgEACAEACAEACgQBAgAL +malformed invalid binary.wast:1178 AGFzbQEAAAABBAFgAAADAgEAAwIBAAoHAgIACwIACw== +malformed invalid binary.wast:1190 AGFzbQEAAAABBAFgAAADAwIAAAoEAQIACwoEAQIACw== +malformed invalid binary.wast:1202 AGFzbQEAAAAMAQEMAQE= +malformed invalid binary.wast:1212 AGFzbQEAAAALAQALAQA= +malformed invalid binary.wast:1222 AGFzbQEAAAAGAQAGAQA= +malformed invalid binary.wast:1232 AGFzbQEAAAAHAQAHAQA= +malformed invalid binary.wast:1242 AGFzbQEAAAAEAQAEAQA= +malformed invalid binary.wast:1252 AGFzbQEAAAAJAQAJAQA= +malformed invalid binary.wast:1262 AGFzbQEAAAACAQACAQA= +malformed invalid binary.wast:1272 AGFzbQEAAAABAQABAQA= +malformed invalid binary.wast:1282 AGFzbQEAAAAFAQAFAQA= +malformed invalid binary.wast:1292 AGFzbQEAAAACAQABAQA= +malformed invalid binary.wast:1302 AGFzbQEAAAADAQACAQA= +malformed invalid binary.wast:1312 AGFzbQEAAAAEAQADAQA= +malformed invalid binary.wast:1322 AGFzbQEAAAAFAQAEAQA= +malformed invalid binary.wast:1332 AGFzbQEAAAAGAQAFAQA= +malformed invalid binary.wast:1342 AGFzbQEAAAAHAQAGAQA= +malformed invalid binary.wast:1352 AGFzbQEAAAABBAFgAAADAgEACAEABwEA +malformed invalid binary.wast:1364 AGFzbQEAAAABBAFgAAADAgEACQEACAEA +malformed invalid binary.wast:1376 AGFzbQEAAAAMAQEJAQA= +malformed invalid binary.wast:1386 AGFzbQEAAAAKAQAMAQE= +malformed invalid binary.wast:1396 AGFzbQEAAAALAQAKAQA= +valid valid block.wast:3 AGFzbQEAAAABPQtgAn9/AX9gAABgAAF/YAF/AGADf3x/A398f2AAA39+f2ABfwF/YAACf39gAAJ9fWAAA39/fmACf38Cf38DODcBAQICAgICAgICAgIBAgICAgICAAICAgEBAgYCAgECAgICAgICAgICAgICAgkCAgICAgICAgIBBAUBcAEBAQUDAQABBgYBfwFBCgsH0AY0BWVtcHR5AAEIc2luZ3VsYXIAAgVtdWx0aQADBm5lc3RlZAAEBGRlZXAABQ9hcy1zZWxlY3QtZmlyc3QABg1hcy1zZWxlY3QtbWlkAAcOYXMtc2VsZWN0LWxhc3QACA1hcy1sb29wLWZpcnN0AAkLYXMtbG9vcC1taWQACgxhcy1sb29wLWxhc3QACw9hcy1pZi1jb25kaXRpb24ADAphcy1pZi10aGVuAA0KYXMtaWYtZWxzZQAODmFzLWJyX2lmLWZpcnN0AA8NYXMtYnJfaWYtbGFzdAAQEWFzLWJyX3RhYmxlLWZpcnN0ABEQYXMtYnJfdGFibGUtbGFzdAASFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAFBRhcy1jYWxsX2luZGlyZWN0LW1pZAAVFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAAWDmFzLXN0b3JlLWZpcnN0ABcNYXMtc3RvcmUtbGFzdAAYFGFzLW1lbW9yeS5ncm93LXZhbHVlABkNYXMtY2FsbC12YWx1ZQAbD2FzLXJldHVybi12YWx1ZQAcD2FzLWRyb3Atb3BlcmFuZAAdC2FzLWJyLXZhbHVlAB4SYXMtbG9jYWwuc2V0LXZhbHVlAB8SYXMtbG9jYWwudGVlLXZhbHVlACATYXMtZ2xvYmFsLnNldC12YWx1ZQAhD2FzLWxvYWQtb3BlcmFuZAAiEGFzLXVuYXJ5LW9wZXJhbmQAIxFhcy1iaW5hcnktb3BlcmFuZAAkD2FzLXRlc3Qtb3BlcmFuZAAlEmFzLWNvbXBhcmUtb3BlcmFuZAAmEmFzLWJpbmFyeS1vcGVyYW5kcwAnE2FzLWNvbXBhcmUtb3BlcmFuZHMAKBFhcy1taXhlZC1vcGVyYW5kcwApCmJyZWFrLWJhcmUAKgticmVhay12YWx1ZQArEWJyZWFrLW11bHRpLXZhbHVlACwOYnJlYWstcmVwZWF0ZWQALQticmVhay1pbm5lcgAuBXBhcmFtAC8GcGFyYW1zADAJcGFyYW1zLWlkADELcGFyYW0tYnJlYWsAMgxwYXJhbXMtYnJlYWsAMw9wYXJhbXMtaWQtYnJlYWsANAdlZmZlY3RzADUIdHlwZS11c2UANgkHAQBBAAsBEwrSCDcCAAsIAAJACwJACwsLAAJAAQsCf0EHCws+AAJAEAAQABAAEAALAn8QABAAEABBBxAACxoCBRAAEAAQAEEIEAAQABAAEABCBxAAEAAQABAAQQkQAAsaGgsVAAJ/AkAQAAJACwELAn8QAEEJCwsLeQACfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/An8CfwJ/EABBlgELCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsMAAJ/QQELQQJBAxsLDABBAgJ/QQELQQMbCwwAQQJBAwJ/QQELGwsOAAN/An9BAQsQABAACwsOAAN/EAACf0EBCxAACwsOAAN/EAAQAAJ/QQELCwsMAAJ/QQELBEAQAAsLDwBBAQR/An9BAQsFQQILCw8AQQEEf0ECBQJ/QQELCwsOAAJ/An9BAQtBAg0ACwsOAAJ/QQICf0EBCw0ACwsQAAJ/An9BAQtBAg4BAAALCxAAAn9BAgJ/QQELDgEAAAsLBAAgAAsRAAJ/An9BAQtBAkEAEQAACwsRAAJ/QQICf0EBC0EAEQAACwsRAAJ/QQFBAgJ/QQALEQAACwsMAAJ/QQELQQE2AgALDABBCgJ/QQELNgIACwkAAn9BAQtAAAsEACAACwkAAn9BAQsQGgsIAAJ/QQELDwsIAAJ/QQELGgsMAAJ/An9BAQsMAAsLDQEBfwJ/QQELIQAgAAsLAQF/An9BAQsiAAsLAAJ/QQELJAAjAAsKAAJ/QQELKAIACwoAAn8QAEENC2gLEQACfxAAQQMLAn8QAEEEC2wLCgACfxAAQQ0LRQsXAAJ9EABDAABAQAsCfRAAQwAAQEALXgsOAAIHEABBAxAAQQQLbAsUAAIIEABDAABAQBAAQwAAQEALXgsRAAIHEABBAxAAQQQLQQVqbAsmAAJADAAACwJAQQENAAALAkBBAA4AAAALAkBBAQ4CAAAAAAtBEwsLAAJ/QRIMAEETCwsTAAIJQRJBbkISDABBE0FtQhMLCzEAAn9BEgwAQRMMAEEUQQANABpBFEEBDQAaQRUMAEEWQQQOAABBF0EBDgIAAABBFQsLRQEBf0EAIQAgAAJ/An9BAQwBCwtqIQAgAAJ/AkAMAAtBAgtqIQAgAAJ/QQQMAGgLaiEAIAACfwJ/QQgMAQtoC2ohACAACwoAQQECBkECagsLCgBBAUECAgBqCwsKAEEBQQICCgtqCwwAQQECBkECagwACwsMAEEBQQICAGoMAAsLDABBAUECAgoMAAtqCy8BAX8CQEEBIQAgAEEDbCEAIABBBWshACAAQQdsIQAMACAAQeQAbCEACyAAQXJGCz0AAkALAn9BAAsCAxoLQQBEAAAAAAAAAABBAAIECxoaGgJ/QQALAgMaC0EARAAAAAAAAAAAQQACBAsaGhoL +invalid invalid block.wast:497 AGFzbQEAAAABBAFgAAADAgEACgkBBwACQEEACws= +invalid invalid block.wast:505 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAAkALCw== +invalid invalid block.wast:509 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAAkALCw== +invalid invalid block.wast:513 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAAkALCw== +invalid invalid block.wast:517 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAAkALCw== +invalid invalid block.wast:522 AGFzbQEAAAABBAFgAAADAgEACgkBBwACQEEBCws= +invalid invalid block.wast:528 AGFzbQEAAAABBAFgAAADAgEACgkBBwACQEIBCws= +invalid invalid block.wast:534 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEMAAIA/Cws= +invalid invalid block.wast:540 AGFzbQEAAAABBAFgAAADAgEAChABDgACQEQAAAAAAADwPwsL +invalid invalid block.wast:546 AGFzbQEAAAABBAFgAAADAgEACgsBCQACQEEBQQILCw== +invalid invalid block.wast:552 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAAn8LCw== +invalid invalid block.wast:558 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAAn4LCw== +invalid invalid block.wast:564 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAAn0LCw== +invalid invalid block.wast:570 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAAnwLCw== +invalid invalid block.wast:576 AGFzbQEAAAABBgFgAAJ/fwMCAQAKBwEFAAIACws= +invalid invalid block.wast:583 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAAJAAn8LGgsL +invalid invalid block.wast:592 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAANAAn8LGgsL +invalid invalid block.wast:601 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEABEACfwsaCws= +invalid invalid block.wast:611 AGFzbQEAAAABBQFgAAF/AwIBAAoIAQYAAn8BCws= +invalid invalid block.wast:617 AGFzbQEAAAABBQFgAAF+AwIBAAoIAQYAAn4BCws= +invalid invalid block.wast:623 AGFzbQEAAAABBQFgAAF9AwIBAAoIAQYAAn0BCws= +invalid invalid block.wast:629 AGFzbQEAAAABBQFgAAF8AwIBAAoIAQYAAnwBCws= +invalid invalid block.wast:635 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCAEGAAIAAQsL +invalid invalid block.wast:641 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcAAn9CAAsL +invalid invalid block.wast:647 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAAn9DAAAAAAsL +invalid invalid block.wast:653 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn9EAAAAAAAAAAALCw== +invalid invalid block.wast:659 AGFzbQEAAAABBQFgAAF+AwIBAAoJAQcAAn5BAAsL +invalid invalid block.wast:665 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAAn5DAAAAAAsL +invalid invalid block.wast:671 AGFzbQEAAAABBQFgAAF+AwIBAAoQAQ4AAn5EAAAAAAAAAAALCw== +invalid invalid block.wast:677 AGFzbQEAAAABBQFgAAF9AwIBAAoJAQcAAn1BAAsL +invalid invalid block.wast:683 AGFzbQEAAAABBQFgAAF9AwIBAAoJAQcAAn1CAAsL +invalid invalid block.wast:689 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AAn1EAAAAAAAAAAALCw== +invalid invalid block.wast:695 AGFzbQEAAAABBQFgAAF8AwIBAAoJAQcAAnxBAAsL +invalid invalid block.wast:701 AGFzbQEAAAABBQFgAAF8AwIBAAoJAQcAAnxCAAsL +invalid invalid block.wast:707 AGFzbQEAAAABBQFgAAF9AwIBAAoMAQoAAnxDAAAAAAsL +invalid invalid block.wast:713 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCQEHAAIAQQALCw== +invalid invalid block.wast:719 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCwEJAEEBAgBBAgsL +invalid invalid block.wast:725 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAn9BAUECCws= +invalid invalid block.wast:732 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAn4AAAAbCws= +invalid invalid block.wast:738 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAn0AAAAbCws= +invalid invalid block.wast:744 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAnwAAAAbCws= +invalid invalid block.wast:750 AGFzbQEAAAABBQFgAAF+AwIBAAoLAQkAAn8AAAAbCws= +invalid invalid block.wast:756 AGFzbQEAAAABBQFgAAF+AwIBAAoLAQkAAn0AAAAbCws= +invalid invalid block.wast:762 AGFzbQEAAAABBQFgAAF+AwIBAAoLAQkAAnwAAAAbCws= +invalid invalid block.wast:768 AGFzbQEAAAABBQFgAAF9AwIBAAoLAQkAAn8AAAAbCws= +invalid invalid block.wast:774 AGFzbQEAAAABBQFgAAF9AwIBAAoLAQkAAn4AAAAbCws= +invalid invalid block.wast:780 AGFzbQEAAAABBQFgAAF9AwIBAAoLAQkAAnwAAAAbCws= +invalid invalid block.wast:786 AGFzbQEAAAABBQFgAAF8AwIBAAoLAQkAAn8AAAAbCws= +invalid invalid block.wast:792 AGFzbQEAAAABBQFgAAF8AwIBAAoLAQkAAn4AAAAbCws= +invalid invalid block.wast:798 AGFzbQEAAAABBQFgAAF8AwIBAAoLAQkAAn0AAAAbCws= +invalid invalid block.wast:805 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcAAn8MAAsL +invalid invalid block.wast:811 AGFzbQEAAAABBQFgAAF+AwIBAAoJAQcAAn4MAAsL +invalid invalid block.wast:817 AGFzbQEAAAABBQFgAAF9AwIBAAoJAQcAAn0MAAsL +invalid invalid block.wast:823 AGFzbQEAAAABBQFgAAF8AwIBAAoJAQcAAnwMAAsL +invalid invalid block.wast:829 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCQEHAAIADAALCw== +invalid invalid block.wast:836 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAn8MAEEBCws= +invalid invalid block.wast:842 AGFzbQEAAAABBQFgAAF+AwIBAAoLAQkAAn4MAEIBCws= +invalid invalid block.wast:848 AGFzbQEAAAABBQFgAAF9AwIBAAoOAQwAAn0MAEMAAIA/Cws= +invalid invalid block.wast:854 AGFzbQEAAAABBQFgAAF8AwIBAAoSARAAAnwMAEQAAAAAAADwPwsL +invalid invalid block.wast:860 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDQELAAIADABBAUECCws= +invalid invalid block.wast:867 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAAn8BDABBAQsL +invalid invalid block.wast:873 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAAn4BDABCAQsL +invalid invalid block.wast:879 AGFzbQEAAAABBQFgAAF9AwIBAAoPAQ0AAn0BDABDAACAPwsL +invalid invalid block.wast:885 AGFzbQEAAAABBQFgAAF8AwIBAAoTAREAAnwBDABEAAAAAAAA8D8LCw== +invalid invalid block.wast:892 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAAn9CAQwAQQELCw== +invalid invalid block.wast:898 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn9DAACAPwwAQQELCw== +invalid invalid block.wast:904 AGFzbQEAAAABBQFgAAF/AwIBAAoUARIAAn9EAAAAAAAA8D8MAEEBCws= +invalid invalid block.wast:910 AGFzbQEAAAABBQFgAAF+AwIBAAoNAQsAAn5BAQwAQgELCw== +invalid invalid block.wast:916 AGFzbQEAAAABBQFgAAF+AwIBAAoQAQ4AAn5DAACAPwwAQgELCw== +invalid invalid block.wast:922 AGFzbQEAAAABBQFgAAF+AwIBAAoUARIAAn5EAAAAAAAA8D8MAEIBCws= +invalid invalid block.wast:928 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AAn1BAQwAQwAAgD8LCw== +invalid invalid block.wast:934 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AAn1CAQwAQwAAgD8LCw== +invalid invalid block.wast:940 AGFzbQEAAAABBQFgAAF9AwIBAAoXARUAAn1EAAAAAAAA8D8MAEMAAIA/Cws= +invalid invalid block.wast:946 AGFzbQEAAAABBQFgAAF8AwIBAAoUARIAAn5BAQwARAAAAAAAAPA/Cws= +invalid invalid block.wast:952 AGFzbQEAAAABBQFgAAF8AwIBAAoUARIAAnxCAQwARAAAAAAAAPA/Cws= +invalid invalid block.wast:958 AGFzbQEAAAABBQFgAAF8AwIBAAoXARUAAnxDAACAPwwARAAAAAAAAPA/Cws= +invalid invalid block.wast:964 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDwENAAIAQQAMAEEBQQILCw== +invalid invalid block.wast:970 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDwENAEEBAgBBAAwAQQILCw== +invalid invalid block.wast:977 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAn8BDABBAQwACws= +invalid invalid block.wast:983 AGFzbQEAAAABBQFgAAF+AwIBAAoOAQwAAn4BDABCAQwACws= +invalid invalid block.wast:989 AGFzbQEAAAABBQFgAAF9AwIBAAoRAQ8AAn0BDABDAACAPwwACws= +invalid invalid block.wast:995 AGFzbQEAAAABBQFgAAF8AwIBAAoVARMAAnwBDABEAAAAAAAA8D8MAAsL +invalid invalid block.wast:1001 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEAEOAAIAAQwAQQFBAgwACws= +invalid invalid block.wast:1008 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAn9CAQwAQQEMAAsL +invalid invalid block.wast:1014 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAAn9DAACAPwwAQQEMAAsL +invalid invalid block.wast:1020 AGFzbQEAAAABBQFgAAF/AwIBAAoWARQAAn9EAAAAAAAA8D8MAEEBDAALCw== +invalid invalid block.wast:1026 AGFzbQEAAAABBQFgAAF+AwIBAAoPAQ0AAn5BAQwAQgEMAAsL +invalid invalid block.wast:1032 AGFzbQEAAAABBQFgAAF+AwIBAAoSARAAAn5DAACAPwwAQgEMAAsL +invalid invalid block.wast:1038 AGFzbQEAAAABBQFgAAF+AwIBAAoWARQAAn5EAAAAAAAA8D8MAEIBDAALCw== +invalid invalid block.wast:1044 AGFzbQEAAAABBQFgAAF9AwIBAAoSARAAAn1BAQwAQwAAgD8MAAsL +invalid invalid block.wast:1050 AGFzbQEAAAABBQFgAAF9AwIBAAoSARAAAn1CAQwAQwAAgD8MAAsL +invalid invalid block.wast:1056 AGFzbQEAAAABBQFgAAF9AwIBAAoZARcAAn1EAAAAAAAA8D8MAEMAAIA/DAALCw== +invalid invalid block.wast:1062 AGFzbQEAAAABBQFgAAF8AwIBAAoWARQAAnxBAQwARAAAAAAAAPA/DAALCw== +invalid invalid block.wast:1068 AGFzbQEAAAABBQFgAAF8AwIBAAoWARQAAnxCAQwARAAAAAAAAPA/DAALCw== +invalid invalid block.wast:1074 AGFzbQEAAAABBQFgAAF8AwIBAAoZARcAAnxDAACAPwwARAAAAAAAAPA/DAALCw== +invalid invalid block.wast:1080 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEQEPAAIAQQAMAEEBQQIMAAsL +invalid invalid block.wast:1087 AGFzbQEAAAABBAFgAAADAgEAChABDgACfwJ/QQEMAQsMAAsL +invalid invalid block.wast:1093 AGFzbQEAAAABBAFgAAADAgEAChABDgACfgJ+QgEMAQsMAAsL +invalid invalid block.wast:1099 AGFzbQEAAAABBAFgAAADAgEAChMBEQACfQJ9QwAAgD8MAQsMAAsL +invalid invalid block.wast:1105 AGFzbQEAAAABBAFgAAADAgEAChcBFQACfAJ8RAAAAAAAAPA/DAELDAALCw== +invalid invalid block.wast:1111 AGFzbQEAAAABCQJgAABgAAJ/fwMCAQAKEgEQAAIBAgFBAUECDAELDAALCw== +invalid invalid block.wast:1118 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn8CQAwBC0EBDAALCw== +invalid invalid block.wast:1124 AGFzbQEAAAABBQFgAAF+AwIBAAoQAQ4AAn4CQAwBC0IBDAALCw== +invalid invalid block.wast:1130 AGFzbQEAAAABBQFgAAF9AwIBAAoTAREAAn0CQAwBC0MAAIA/DAALCw== +invalid invalid block.wast:1136 AGFzbQEAAAABBQFgAAF8AwIBAAoXARUAAnwCQAwBC0QAAAAAAADwPwwACws= +invalid invalid block.wast:1142 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEgEQAAIAAkAMAQtBAUECDAALCw== +invalid invalid block.wast:1149 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAn8CfwEMAQtBAQwACws= +invalid invalid block.wast:1155 AGFzbQEAAAABBQFgAAF+AwIBAAoRAQ8AAn4CfgEMAQtCAQwACws= +invalid invalid block.wast:1161 AGFzbQEAAAABBQFgAAF9AwIBAAoUARIAAn0CfQEMAQtDAACAPwwACws= +invalid invalid block.wast:1167 AGFzbQEAAAABBQFgAAF8AwIBAAoYARYAAnwCfAEMAQtEAAAAAAAA8D8MAAsL +invalid invalid block.wast:1173 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEwERAAIAAgABDAELQQFBAgwACws= +invalid invalid block.wast:1180 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAAn8Cf0IBDAELQQEMAAsL +invalid invalid block.wast:1188 AGFzbQEAAAABBQFgAAF/AwIBAAoVARMAAn8Cf0MAAIA/DAELQQEMAAsL +invalid invalid block.wast:1196 AGFzbQEAAAABBQFgAAF/AwIBAAoZARcAAn8Cf0QAAAAAAADwPwwBC0EBDAALCw== +invalid invalid block.wast:1204 AGFzbQEAAAABBQFgAAF+AwIBAAoSARAAAn4CfkEBDAELQgEMAAsL +invalid invalid block.wast:1212 AGFzbQEAAAABBQFgAAF+AwIBAAoVARMAAn4CfkMAAIA/DAELQgEMAAsL +invalid invalid block.wast:1220 AGFzbQEAAAABBQFgAAF+AwIBAAoZARcAAn4CfkQAAAAAAADwPwwBC0IBDAALCw== +invalid invalid block.wast:1228 AGFzbQEAAAABBQFgAAF9AwIBAAoVARMAAn0CfUEBDAELQwAAgD8MAAsL +invalid invalid block.wast:1236 AGFzbQEAAAABBQFgAAF9AwIBAAoVARMAAn0CfUIBDAELQwAAgD8MAAsL +invalid invalid block.wast:1244 AGFzbQEAAAABBQFgAAF9AwIBAAocARoAAn0CfUQAAAAAAADwPwwBC0MAAIA/DAALCw== +invalid invalid block.wast:1252 AGFzbQEAAAABBQFgAAF8AwIBAAoZARcAAnwCfEEBDAELRAAAAAAAAPA/DAALCw== +invalid invalid block.wast:1260 AGFzbQEAAAABBQFgAAF8AwIBAAoZARcAAnwCfEIBDAELRAAAAAAAAPA/DAALCw== +invalid invalid block.wast:1268 AGFzbQEAAAABBQFgAAF8AwIBAAocARoAAnwCfEMAAIA/DAELRAAAAAAAAPA/DAALCw== +invalid invalid block.wast:1276 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFAESAAIAAgBBAAwBC0EBQQIMAAsL +invalid invalid block.wast:1285 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAAkAMAAtoCw== +invalid invalid block.wast:1291 AGFzbQEAAAABBQFgAAF+AwIBAAoKAQgAAkAMAAt6Cw== +invalid invalid block.wast:1297 AGFzbQEAAAABBQFgAAF9AwIBAAoKAQgAAkAMAAuOCw== +invalid invalid block.wast:1303 AGFzbQEAAAABBQFgAAF8AwIBAAoKAQgAAkAMAAucCw== +invalid invalid block.wast:1309 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAAkAMAAtqCw== +invalid invalid block.wast:1316 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAkABDAALaAs= +invalid invalid block.wast:1322 AGFzbQEAAAABBQFgAAF+AwIBAAoLAQkAAkABDAALegs= +invalid invalid block.wast:1328 AGFzbQEAAAABBQFgAAF9AwIBAAoLAQkAAkABDAALjgs= +invalid invalid block.wast:1334 AGFzbQEAAAABBQFgAAF8AwIBAAoLAQkAAkABDAALnAs= +invalid invalid block.wast:1340 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAkABDAALags= +invalid invalid block.wast:1347 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAAkBCCQwAC3oL +invalid invalid block.wast:1353 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAkBDAAAQQQwAC44L +invalid invalid block.wast:1359 AGFzbQEAAAABBQFgAAF/AwIBAAoTAREAAkBEAAAAAAAAIkAMAAucCw== +invalid invalid block.wast:1365 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAAkBBCQwAC2gL +invalid invalid block.wast:1371 AGFzbQEAAAABBQFgAAF+AwIBAAoPAQ0AAkBDAAAQQQwAC44L +invalid invalid block.wast:1377 AGFzbQEAAAABBQFgAAF+AwIBAAoTAREAAkBEAAAAAAAAIkAMAAucCw== +invalid invalid block.wast:1383 AGFzbQEAAAABBQFgAAF9AwIBAAoMAQoAAkBBCQwAC2gL +invalid invalid block.wast:1389 AGFzbQEAAAABBQFgAAF9AwIBAAoMAQoAAkBCCQwAC3oL +invalid invalid block.wast:1395 AGFzbQEAAAABBQFgAAF9AwIBAAoTAREAAkBEAAAAAAAAIkAMAAucCw== +invalid invalid block.wast:1401 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAAkBBCQwAC2gL +invalid invalid block.wast:1407 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAAkBCCQwAC3oL +invalid invalid block.wast:1413 AGFzbQEAAAABBQFgAAF8AwIBAAoPAQ0AAkBDAAAQQQwAC44L +invalid invalid block.wast:1419 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAkBCCUEKDAALags= +invalid invalid block.wast:1426 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoIAQYAAgEaCws= +invalid invalid block.wast:1432 AGFzbQEAAAABCQJgAABgAn98AAMCAQAKCQEHAAIBGhoLCw== +invalid invalid block.wast:1438 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoNAQsAQwAAAAACARoLCw== +invalid invalid block.wast:1444 AGFzbQEAAAABCQJgAABgAn1/AAMCAQAKDgEMAEMAAAAAAgEaGgsL +invalid invalid block.wast:1450 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoLAQkAAkACARoLCws= +invalid invalid block.wast:1456 AGFzbQEAAAABCQJgAABgAn98AAMCAQAKDAEKAAJAAgEaGgsLCw== +invalid invalid block.wast:1462 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoQAQ4AAkBDAAAAAAIBGgsLCw== +invalid invalid block.wast:1468 AGFzbQEAAAABCQJgAABgAn1/AAMCAQAKEQEPAAJAQwAAAAACARoaCwsL +valid valid br.wast:3 AGFzbQEAAAABKwlgA39/fwF/YAAAYAABf2AAAX5gAAF9YAABfGAAAnx8YAACf35gAn9/AX8DS0oBAQEBAQEBAQECAwQFBgEBAQICAgICAQICAQICAwcCCAgICAICAAICAgICAgICAgICAgQDAgICAgICBAIDAgICAgICAgICAgICAgQFAXABAQEFAwEAAQYGAX8BQQoLB5IKSAh0eXBlLWkzMgABCHR5cGUtaTY0AAIIdHlwZS1mMzIAAwh0eXBlLWY2NAAEDHR5cGUtaTMyLWkzMgAFDHR5cGUtaTY0LWk2NAAGDHR5cGUtZjMyLWYzMgAHDHR5cGUtZjY0LWY2NAAIDnR5cGUtaTMyLXZhbHVlAAkOdHlwZS1pNjQtdmFsdWUACg50eXBlLWYzMi12YWx1ZQALDnR5cGUtZjY0LXZhbHVlAAwSdHlwZS1mNjQtZjY0LXZhbHVlAA0OYXMtYmxvY2stZmlyc3QADgxhcy1ibG9jay1taWQADw1hcy1ibG9jay1sYXN0ABAOYXMtYmxvY2stdmFsdWUAEQ1hcy1sb29wLWZpcnN0ABILYXMtbG9vcC1taWQAEwxhcy1sb29wLWxhc3QAFAthcy1ici12YWx1ZQAVDWFzLWJyX2lmLWNvbmQAFg5hcy1icl9pZi12YWx1ZQAXE2FzLWJyX2lmLXZhbHVlLWNvbmQAGBFhcy1icl90YWJsZS1pbmRleAAZEWFzLWJyX3RhYmxlLXZhbHVlABoXYXMtYnJfdGFibGUtdmFsdWUtaW5kZXgAGw9hcy1yZXR1cm4tdmFsdWUAHBBhcy1yZXR1cm4tdmFsdWVzAB0KYXMtaWYtY29uZAAeCmFzLWlmLXRoZW4AHwphcy1pZi1lbHNlACAPYXMtc2VsZWN0LWZpcnN0ACEQYXMtc2VsZWN0LXNlY29uZAAiDmFzLXNlbGVjdC1jb25kACMNYXMtc2VsZWN0LWFsbAAkDWFzLWNhbGwtZmlyc3QAJgthcy1jYWxsLW1pZAAnDGFzLWNhbGwtbGFzdAAoC2FzLWNhbGwtYWxsACkVYXMtY2FsbF9pbmRpcmVjdC1mdW5jACoWYXMtY2FsbF9pbmRpcmVjdC1maXJzdAArFGFzLWNhbGxfaW5kaXJlY3QtbWlkACwVYXMtY2FsbF9pbmRpcmVjdC1sYXN0AC0UYXMtY2FsbF9pbmRpcmVjdC1hbGwALhJhcy1sb2NhbC5zZXQtdmFsdWUALxJhcy1sb2NhbC50ZWUtdmFsdWUAMBNhcy1nbG9iYWwuc2V0LXZhbHVlADEPYXMtbG9hZC1hZGRyZXNzADIQYXMtbG9hZE4tYWRkcmVzcwAzEGFzLXN0b3JlLWFkZHJlc3MANA5hcy1zdG9yZS12YWx1ZQA1DWFzLXN0b3JlLWJvdGgANhFhcy1zdG9yZU4tYWRkcmVzcwA3D2FzLXN0b3JlTi12YWx1ZQA4DmFzLXN0b3JlTi1ib3RoADkQYXMtdW5hcnktb3BlcmFuZAA6DmFzLWJpbmFyeS1sZWZ0ADsPYXMtYmluYXJ5LXJpZ2h0ADwOYXMtYmluYXJ5LWJvdGgAPQ9hcy10ZXN0LW9wZXJhbmQAPg9hcy1jb21wYXJlLWxlZnQAPxBhcy1jb21wYXJlLXJpZ2h0AEAPYXMtY29tcGFyZS1ib3RoAEESYXMtY29udmVydC1vcGVyYW5kAEITYXMtbWVtb3J5Lmdyb3ctc2l6ZQBDEm5lc3RlZC1ibG9jay12YWx1ZQBED25lc3RlZC1ici12YWx1ZQBFEm5lc3RlZC1icl9pZi12YWx1ZQBGF25lc3RlZC1icl9pZi12YWx1ZS1jb25kAEcVbmVzdGVkLWJyX3RhYmxlLXZhbHVlAEgbbmVzdGVkLWJyX3RhYmxlLXZhbHVlLWluZGV4AEkJBwEAQQALASUK5QhKAgALCQACQAwAaBoLCwkAAkAMAHoaCwsJAAJADACMGgsLCQACQAwAmhoLCwkAAkAMAGoaCwsJAAJADAB8GgsLCQACQAwAkhoLCwkAAkAMAKAaCwsKAAJ/QQEMAGgLCwoAAn5CAgwAegsLDQACfUMAAEBADACMCwsRAAJ8RAAAAAAAABBADACaCwsjAAIGRAAAAAAAABBARAAAAAAAABRADACgRAAAAAAAABhACwsJAAJADAAQAAsLCwACQBAADAAQAAsLCgACQAEQAAwACwsMAAJ/ARAAQQIMAAsLDgACfwN/QQMMAUECCwsLEAACfwN/EABBBAwBQQILCwsPAAJ/A38BEABBBQwBCwsLCwACf0EJDAAMAAsLCQACQAwADQALCxAAAn9BCAwAQQENABpBBwsLEAACf0EGQQkMAA0AGkEHCwsMAAJADAAOAgAAAAsLEgACf0EKDABBAQ4CAAAAQQcLCxEAAn9BBkELDAAOAQAAQQcLCwoAAn5CBwwADwsLDgBBAgJ+QQFCBwwADwsLEQACf0ECDAAEf0EABUEBCwsLEQACfyAABH9BAwwBBSABCwsLEQACfyAABH8gAQVBBAwBCwsLDgACf0EFDAAgACABGwsLDgACfyAAQQYMACABGwsLDgACf0EAQQFBBwwAGwsLCgACf0EIDAAbCwsEAEF/Cw8AAn9BDAwAQQJBAxAlCwsPAAJ/QQFBDQwAQQMQJQsLDwACf0EBQQJBDgwAECULCwsAAn9BDwwAECULCxIAAn9BFAwAQQFBAkEDEQAACwsSAAJ/QQBBFQwAQQJBAxEAAAsLEgACf0EAQQFBFgwAQQMRAAALCxIAAn9BAEEBQQJBFwwAEQAACwsMAAJ/QRgMABEAAAsLDwEBfQJ/QREMACEAQX8LCw0BAX8Cf0EBDAAiAAsLCwACf0EBDAAkAAsLDwACfUOamdk/DAAqAgALCwwAAn5CHgwAMAAACwsXAAJ/QR4MAEQAAAAAAAAcQDkDAEF/CwsQAAJ/QQJBHwwANwMAQX8LCw4AAn9BIAwANwMAQX8LCxAAAn9BIAwAQQc6AABBfwsLEAACf0ECQSEMAD0BAEF/CwsOAAJ/QSIMAD0BAEF/CwsNAAJ9Q5qZWUAMAIwLCwwAAn9BAwwAQQpqCwsMAAJ+QgpCLQwAfQsLCgACf0EuDABqCwsKAAJ/QSwMAEULCxMAAn9BKwwARAAAAAAAACRAZQsLDwACf0MAACBBQSoMAFwLCwoAAn9BLAwAZQsLCgACf0EpDACnCwsLAAJ/QSgMAEAACwsRAEEBAn8QAEEEQQgMAGoLagsaAEEBAn9BAhoCf0EEGkEIDAEMAAsaQRALagsfAEEBAn9BAhoCf0EEGkEIDAFBAQ0AGkEgCxpBEAtqCxYAQQECf0ECGkEEQQgMAA0AGkEQC2oLHQBBAQJ/QQIaAn9BBBpBCAwBQQEOAAALGkEQC2oLFgBBAQJ/QQIaQQRBCAwADgAAQRALags= +invalid invalid br.wast:484 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAn9BAAJADAELCws= +invalid invalid br.wast:497 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAAJ/DAAMAAtFGgs= +invalid invalid br.wast:506 AGFzbQEAAAABBAFgAAADAgEAChEBDwBBAAJ/DABBAQ0AC0UaCw== +invalid invalid br.wast:515 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAAJ/DAAOAAALRRoL +invalid invalid br.wast:524 AGFzbQEAAAABBAFgAAADAgEACgwBCgACfwwADwtFGgs= +invalid invalid br.wast:535 AGFzbQEAAAABBAFgAAADAgEAChABDgACfwwAQQFBAhsLRRoL +invalid invalid br.wast:546 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABChICCwACfwwAEAELRRoLBAAgAAs= +invalid invalid br.wast:558 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoVAgQAIAALDgACfwwAQQARAAALRRoL +invalid invalid br.wast:574 AGFzbQEAAAABBAFgAAADAgEAChEBDwEBfwJ/DAAhACAAC0UaCw== +invalid invalid br.wast:586 AGFzbQEAAAABBAFgAAADAgEACg8BDQEBfwJ/DAAiAAtFGgs= +invalid invalid br.wast:598 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKDwENAAJ/DAAkACMAC0UaCw== +invalid invalid br.wast:610 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDQELAAJ/DABAAAtFGgs= +invalid invalid br.wast:622 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAAJ/DAAoAgALRRoL +invalid invalid br.wast:634 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEAEOAAJ/DABBADYCAAtFGgs= +invalid invalid br.wast:647 AGFzbQEAAAABBAFgAAADAgEACgYBBAAMAQs= +invalid invalid br.wast:651 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQAJADAULCws= +invalid invalid br.wast:655 AGFzbQEAAAABBAFgAAADAgEACgoBCAAMgYCAgAEL +valid valid br_if.wast:3 AGFzbQEAAAABKQlgA39/fwF/YAAAYAABf2AAAX5gAAF9YAABfGABfwF/YAF/AGACf38AA0A/AQEBAQECAwQFBgYHBgYGBgYHAgECBgECAgMGCAgGBgIAAgICAAICAgIGBgYCAgICAgIFAgICAgICBgYGBgYGBAUBcAEBAQUDAQABBgYBfwFBCgsH0Qg8CHR5cGUtaTMyAAEIdHlwZS1pNjQAAgh0eXBlLWYzMgADCHR5cGUtZjY0AAQOdHlwZS1pMzItdmFsdWUABQ50eXBlLWk2NC12YWx1ZQAGDnR5cGUtZjMyLXZhbHVlAAcOdHlwZS1mNjQtdmFsdWUACA5hcy1ibG9jay1maXJzdAAJDGFzLWJsb2NrLW1pZAAKDWFzLWJsb2NrLWxhc3QACxRhcy1ibG9jay1maXJzdC12YWx1ZQAMEmFzLWJsb2NrLW1pZC12YWx1ZQANE2FzLWJsb2NrLWxhc3QtdmFsdWUADg1hcy1sb29wLWZpcnN0AA8LYXMtbG9vcC1taWQAEAxhcy1sb29wLWxhc3QAEQthcy1ici12YWx1ZQASDWFzLWJyX2lmLWNvbmQAEw5hcy1icl9pZi12YWx1ZQAUE2FzLWJyX2lmLXZhbHVlLWNvbmQAFRFhcy1icl90YWJsZS1pbmRleAAWEWFzLWJyX3RhYmxlLXZhbHVlABcXYXMtYnJfdGFibGUtdmFsdWUtaW5kZXgAGA9hcy1yZXR1cm4tdmFsdWUAGQphcy1pZi1jb25kABoKYXMtaWYtdGhlbgAbCmFzLWlmLWVsc2UAHA9hcy1zZWxlY3QtZmlyc3QAHRBhcy1zZWxlY3Qtc2Vjb25kAB4OYXMtc2VsZWN0LWNvbmQAHw1hcy1jYWxsLWZpcnN0ACELYXMtY2FsbC1taWQAIgxhcy1jYWxsLWxhc3QAIxVhcy1jYWxsX2luZGlyZWN0LWZ1bmMAJRZhcy1jYWxsX2luZGlyZWN0LWZpcnN0ACYUYXMtY2FsbF9pbmRpcmVjdC1taWQAJxVhcy1jYWxsX2luZGlyZWN0LWxhc3QAKBJhcy1sb2NhbC5zZXQtdmFsdWUAKRJhcy1sb2NhbC50ZWUtdmFsdWUAKhNhcy1nbG9iYWwuc2V0LXZhbHVlACsPYXMtbG9hZC1hZGRyZXNzACwQYXMtbG9hZE4tYWRkcmVzcwAtEGFzLXN0b3JlLWFkZHJlc3MALg5hcy1zdG9yZS12YWx1ZQAvEWFzLXN0b3JlTi1hZGRyZXNzADAPYXMtc3RvcmVOLXZhbHVlADEQYXMtdW5hcnktb3BlcmFuZAAyDmFzLWJpbmFyeS1sZWZ0ADMPYXMtYmluYXJ5LXJpZ2h0ADQPYXMtdGVzdC1vcGVyYW5kADUPYXMtY29tcGFyZS1sZWZ0ADYQYXMtY29tcGFyZS1yaWdodAA3E2FzLW1lbW9yeS5ncm93LXNpemUAOBJuZXN0ZWQtYmxvY2stdmFsdWUAOQ9uZXN0ZWQtYnItdmFsdWUAOhJuZXN0ZWQtYnJfaWYtdmFsdWUAOxduZXN0ZWQtYnJfaWYtdmFsdWUtY29uZAA8FW5lc3RlZC1icl90YWJsZS12YWx1ZQA9G25lc3RlZC1icl90YWJsZS12YWx1ZS1pbmRleAA+CQcBAEEACwEkCtoIPwIACw0AAkBBAEEBDQBoGgsLDQACQEIAQQENAHoaCwsQAAJAQwAAAABBAQ0AjBoLCxQAAkBEAAAAAAAAAABBAQ0AmhoLCwwAAn9BAUEBDQBoCwsMAAJ+QgJBAQ0AegsLDwACfUMAAEBAQQENAIwLCxMAAnxEAAAAAAAAEEBBAQ0AmgsLDgACQCAADQBBAg8LQQMLEAACQBAAIAANAEECDwtBAwsNAAJAEAAQACAADQALCw8AAn9BCiAADQAaQQsPCwsRAAJ/EABBFCAADQAaQRUPCwsPAAJ/EAAQAEELIAANAAsLEQACQANAIAANAUECDwsLQQMLEwACQANAEAAgAA0BQQIPCwtBBAsLAANAEAAgAA0BCwsNAAJ/QQFBAg0ADAALCw0AAkBBAUEBDQANAAsLEgACf0EBQQINAEEDDQAaQQQLCxIAAn9BAkEBIAANAA0AGkEECwsQAAJAQQFBAg0ADgIAAAALCxQAAn9BAUECDQBBAw4CAAAAQQQLCxMAAn9BAkEBQQMNAA4BAABBBAsLDAACfkIBQQINAA8LCxMAAn9BASAADQAEf0ECBUEDCwsLEQACQCAABEAgAQ0BBRAACwsLEQACQCAABEAQAAUgAQ0BCwsLEAACf0EDQQoNAEECIAAbCwsQAAJ/QQFBA0EKDQAgABsLCxAAAn9BAUECQQNBCg0AGwsLBABBfwsRAAJ/QQxBAQ0AQQJBAxAgCwsRAAJ/QQFBDUEBDQBBAxAgCwsRAAJ/QQFBAkEOQQENABAgCwsEACAACxQAAn9BBEEKDQBBAUECQQARAAALCxQAAn9BAUEEQQoNAEECQQARAAALCxQAAn9BAUECQQRBCg0AQQARAAALCxQAAn9BAUECQQNBBEEKDQARAAALCxEBAX8Cf0ERIAANACEAQX8LCxAAAn9BASAADQAiAEF/DwsLEAACf0EBIAANACQAQX8PCwsOAAJ/QQFBAQ0AKAIACwsOAAJ/QR5BAQ0ALAAACwsSAAJ/QR5BAQ0AQQc2AgBBfwsLEgACf0ECQR9BAQ0ANgIAQX8LCxIAAn9BIEEBDQBBBzoAAEF/CwsSAAJ/QQJBIUEBDQA7AQBBfwsLEwACfEQAAAAAAADwP0EBDQCaCwsOAAJ/QQFBAQ0AQQpqCwsOAAJ/QQpBAUEBDQBrCwsMAAJ/QQBBAQ0ARQsLDgACf0EBQQENAEEKTQsLDgACf0EKQQFBKg0ARwsLDQACf0EBQQENAEAACwsaAEEBAn9BAhpBBAJ/QQggAA0BGkEQC2oLagsbAEEBAn9BAhoCf0EIIAANARpBBAsMAEEQC2oLHgBBAQJ/QQIaAn9BCCAADQEaQQQLQQENABpBEAtqCx4AQQECf0ECGkEEAn9BCCAADQEaQQELDQAaQRALagseAEEBAn9BAhoCf0EIIAANARpBBAtBAQ4AAEEQC2oLHgBBAQJ/QQIaQQQCf0EIIAANARpBAQsOAABBEAtqCw== +invalid invalid br_if.wast:481 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEEADQBoCws= +invalid invalid br_if.wast:485 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEEADQB6Cws= +invalid invalid br_if.wast:489 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEEADQCMCws= +invalid invalid br_if.wast:493 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEEADQCaCws= +invalid invalid br_if.wast:498 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEEBDQBoCws= +invalid invalid br_if.wast:502 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEIBDQB6Cws= +invalid invalid br_if.wast:506 AGFzbQEAAAABBAFgAAADAgEACg8BDQACQEMAAIA/DQCMCws= +invalid invalid br_if.wast:510 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEIBDQCaCws= +invalid invalid br_if.wast:515 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAAn9BAA0AQQELCw== +invalid invalid br_if.wast:521 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAAn9BAQ0AQQELCw== +invalid invalid br_if.wast:527 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEAQQANAAsL +invalid invalid br_if.wast:533 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEAQQENAAsL +invalid invalid br_if.wast:540 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAn8BQQANAEEBCws= +invalid invalid br_if.wast:546 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAn8BQQENAEEBCws= +invalid invalid br_if.wast:552 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn9CAUEADQAaQQELCw== +invalid invalid br_if.wast:569 AGFzbQEAAAABBAFgAAADAgEACgkBBwACQA0ACws= +invalid invalid br_if.wast:575 AGFzbQEAAAABBAFgAAADAgEACgoBCAACQAENAAsL +invalid invalid br_if.wast:581 AGFzbQEAAAABBAFgAAADAgEACgsBCQACQEIADQALCw== +invalid invalid br_if.wast:587 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAn9BAAENAEEBCws= +invalid invalid br_if.wast:593 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn9BAAJAQQENAQsLCw== +invalid invalid br_if.wast:599 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAn9BAEIADQBBAQsL +invalid invalid br_if.wast:606 AGFzbQEAAAABBAFgAAADAgEAChIBEAACQEEAQQAEfw0ACwtFGgs= +invalid invalid br_if.wast:618 AGFzbQEAAAABBAFgAAADAgEAChQBEgACQEEAQQAEf0EBDQALC0UaCw== +invalid invalid br_if.wast:630 AGFzbQEAAAABBAFgAAADAgEACgwBCgACfw0ADwtFGgs= +invalid invalid br_if.wast:641 AGFzbQEAAAABBAFgAAADAgEACg4BDAACf0EBDQAPC0UaCw== +invalid invalid br_if.wast:654 AGFzbQEAAAABBAFgAAADAgEACggBBgBBAQ0BCw== +invalid invalid br_if.wast:658 AGFzbQEAAAABBAFgAAADAgEACg4BDAACQAJAQQENBQsLCw== +invalid invalid br_if.wast:662 AGFzbQEAAAABBAFgAAADAgEACgwBCgBBAQ2BgICAAQs= +valid valid br_table.wast:3 AGFzbQEAAAABLAlgA39/fwF/YAAAYAABf2AAAX5gAAF9YAABfGABfwF/YAJ/fwF/YAJ/bwFvA0dGAQEBAQECAwQFBgYGBgYGBgEBAQICAgICAQICAQICAwIHBwcHAgACAgICAgICAgICBAMCAgICBAIDAgICAgIGBgYGBgYGCAQFAXABAQEFAwEAAQYGAX8BQQoLB7sJRAh0eXBlLWkzMgABCHR5cGUtaTY0AAIIdHlwZS1mMzIAAwh0eXBlLWY2NAAEDnR5cGUtaTMyLXZhbHVlAAUOdHlwZS1pNjQtdmFsdWUABg50eXBlLWYzMi12YWx1ZQAHDnR5cGUtZjY0LXZhbHVlAAgFZW1wdHkACQtlbXB0eS12YWx1ZQAKCXNpbmdsZXRvbgALD3NpbmdsZXRvbi12YWx1ZQAMCG11bHRpcGxlAA0ObXVsdGlwbGUtdmFsdWUADgVsYXJnZQAPDmFzLWJsb2NrLWZpcnN0ABAMYXMtYmxvY2stbWlkABENYXMtYmxvY2stbGFzdAASDmFzLWJsb2NrLXZhbHVlABMNYXMtbG9vcC1maXJzdAAUC2FzLWxvb3AtbWlkABUMYXMtbG9vcC1sYXN0ABYLYXMtYnItdmFsdWUAFw1hcy1icl9pZi1jb25kABgOYXMtYnJfaWYtdmFsdWUAGRNhcy1icl9pZi12YWx1ZS1jb25kABoRYXMtYnJfdGFibGUtaW5kZXgAGxFhcy1icl90YWJsZS12YWx1ZQAcF2FzLWJyX3RhYmxlLXZhbHVlLWluZGV4AB0PYXMtcmV0dXJuLXZhbHVlAB4KYXMtaWYtY29uZAAfCmFzLWlmLXRoZW4AIAphcy1pZi1lbHNlACEPYXMtc2VsZWN0LWZpcnN0ACIQYXMtc2VsZWN0LXNlY29uZAAjDmFzLXNlbGVjdC1jb25kACQNYXMtY2FsbC1maXJzdAAmC2FzLWNhbGwtbWlkACcMYXMtY2FsbC1sYXN0ACgWYXMtY2FsbF9pbmRpcmVjdC1maXJzdAApFGFzLWNhbGxfaW5kaXJlY3QtbWlkACoVYXMtY2FsbF9pbmRpcmVjdC1sYXN0ACsVYXMtY2FsbF9pbmRpcmVjdC1mdW5jACwSYXMtbG9jYWwuc2V0LXZhbHVlAC0SYXMtbG9jYWwudGVlLXZhbHVlAC4TYXMtZ2xvYmFsLnNldC12YWx1ZQAvD2FzLWxvYWQtYWRkcmVzcwAwEGFzLWxvYWROLWFkZHJlc3MAMRBhcy1zdG9yZS1hZGRyZXNzADIOYXMtc3RvcmUtdmFsdWUAMxFhcy1zdG9yZU4tYWRkcmVzcwA0D2FzLXN0b3JlTi12YWx1ZQA1EGFzLXVuYXJ5LW9wZXJhbmQANg5hcy1iaW5hcnktbGVmdAA3D2FzLWJpbmFyeS1yaWdodAA4D2FzLXRlc3Qtb3BlcmFuZAA5D2FzLWNvbXBhcmUtbGVmdAA6EGFzLWNvbXBhcmUtcmlnaHQAOxJhcy1jb252ZXJ0LW9wZXJhbmQAPBNhcy1tZW1vcnkuZ3Jvdy1zaXplAD0SbmVzdGVkLWJsb2NrLXZhbHVlAD4PbmVzdGVkLWJyLXZhbHVlAD8SbmVzdGVkLWJyX2lmLXZhbHVlAEAXbmVzdGVkLWJyX2lmLXZhbHVlLWNvbmQAQRVuZXN0ZWQtYnJfdGFibGUtdmFsdWUAQhtuZXN0ZWQtYnJfdGFibGUtdmFsdWUtaW5kZXgAQxpuZXN0ZWQtYnJfdGFibGUtbG9vcC1ibG9jawBEDm1lZXQtZXh0ZXJucmVmAEUJBwEAQQALASUK04kBRgIACw0AAkBBAA4BAABoGgsLDQACQEEADgEAAHoaCwsNAAJAQQAOAQAAjBoLCw0AAkBBAA4BAACaGgsLDgACf0EBQQAOAQAAaAsLDgACfkICQQAOAQAAegsLEQACfUMAAEBAQQAOAQAAjAsLFQACfEQAAAAAAAAQQEEADgEAAJoLCw8AAkAgAA4AAEEVDwtBFgsOAAJ/QSEgAA4AAEEfCwsWAAJAAkAgAA4BAQBBFQ8LQRQPC0EWCxYAAn8Cf0EhIAAOAQABQR8PCxpBIAsLMQACQAJAAkACQAJAIAAOBAMCAQAEQeMADwtB5AAPC0HlAA8LQeYADwtB5wAPC0HoAAtNAQF/An8CfwJ/An8Cf0HIASAADgQDAgEABCABQeMAag8LIQEgAUEKag8LIQEgAUELag8LIQEgAUEMag8LIQEgAUENag8LIQEgAUEOagusfgACQAJAIAAOlX4AAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABQX8PC0EADwtBAQ8LDgACQEEADgIAAAAQAAsLEAACQBAAQQAOAgAAABAACwsPAAJAARAAQQAOAgAAAAsLEQACfwEQAEECQQAOAgAAAAsLDwADf0EDQQAOAQEBQQELCxIAA38QAEEEQX8OAgEBAUECCwsRAAN/ARAAQQVBAQ4CAQEBCwsOAAJ/QQlBAA4AAAwACwsOAAJAQQEOAgAAAA0ACwsTAAJ/QQhBAA4AAEEBDQAaQQcLCxQAAn9BBkEJQQAOAQAADQAaQQcLCw8AAkBBAQ4AAA4CAAAACwsVAAJ/QQpBAA4AAEEBDgIAAABBBwsLFAACf0EGQQtBAQ4AAA4BAABBBwsLDQACfkIHQQAOAAAPCwsUAAJ/QQJBAA4AAAR/QQAFQQELCwsUAAJ/IAAEf0EDQQAOAAEFIAELCwsVAAJ/IAAEfyABBUEEQQAOAQEACwsLEQACf0EFQQAOAAAgACABGwsLEQACfyAAQQZBAQ4AACABGwsLEQACf0EAQQFBB0EBDgAAGwsLBABBfwsSAAJ/QQxBAQ4AAEECQQMQJQsLEgACf0EBQQ1BAQ4AAEEDECULCxIAAn9BAUECQQ5BAQ4AABAlCwsVAAJ/QRRBAQ4AAEEBQQJBAxEAAAsLFQACf0EAQRVBAQ4AAEECQQMRAAALCxUAAn9BAEEBQRZBAQ4AAEEDEQAACwsVAAJ/QQBBAUECQRdBAQ4AABEAAAsLEgEBfQJ/QRFBAQ4AACEAQX8LCxIBAX8Cf0EBQQEOAAAhAEF/CwsQAAJ/QQFBAQ4AACQAQX8LCxIAAn1DmpnZP0EBDgAAKgIACwsPAAJ+Qh5BAQ4AADAAAAsLGgACf0EeQQEOAABEAAAAAAAAHEA5AwBBfwsLEwACf0ECQR9BAQ4AADcDAEF/CwsTAAJ/QSBBAA4AAEEHOgAAQX8LCxMAAn9BAkEhQQAOAAA9AQBBfwsLEAACfUOamVlAQQAOAACMCwsQAAJ/QQNBAA4BAABBCmoLCw8AAn5CCkItQQAOAAB9CwsNAAJ/QSxBAA4AAEULCxcAAn9BK0EADgEAAEQAAAAAAAAkQGULCxIAAn9DAAAgQUEqQQAOAABcCwsNAAJ/QSlBAA4AAKcLCw4AAn9BKEEADgAAQAALCyMAAn9BfxpBAQJ/QQICf0EEGkEIQRAgAA4CAAECagtqC2oLCyIAAn9BAQJ/QQIaAn9BBBpBCCAADgICAQAMAAsaQRALagsLJwACf0EBAn9BAhoCf0EEGkEIIAAOAgABAkEBDQAaQSALGkEQC2oLCx4AAn9BAQJ/QQIaQQRBCCAADgIAAQANABpBEAtqCwsnAAJ/QQECf0ECGgJ/QQQaQQggAA4CAAECQQEOAABBIAsaQRALagsLHgACf0EBAn9BAhpBBEEIIAAOAgABAA4AAEEQC2oLCyIAA38CQCAADgIBAAALQQALIQADfwJAIAAOAgABAQtBAwsLEQACbwJvIAEgAA4CAQABCwsL +invalid invalid br_table.wast:1193 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAkBBAQ4AAEEBCws= +invalid invalid br_table.wast:1200 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAAkAOAABBAQsL +invalid invalid br_table.wast:1207 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAn8BQQEOAABBAQsL +invalid invalid br_table.wast:1213 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAAn9CAUEBDgIAAABBAQsL +invalid invalid br_table.wast:1221 AGFzbQEAAAABBAFgAAADAgEAChYBFAACQAJ9QwAAAABBAA4BAAELGgsL +invalid invalid br_table.wast:1232 AGFzbQEAAAABBAFgAAADAgEAChIBEAACfwJ+QQBBAA4BAAELCws= +invalid invalid br_table.wast:1243 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQAEOAgAAAAsL +invalid invalid br_table.wast:1249 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEIADgAACws= +invalid invalid br_table.wast:1255 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn9BAAEOAQAAQQELCw== +invalid invalid br_table.wast:1261 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAn9BAAJAQQAOAAELCws= +invalid invalid br_table.wast:1267 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAn9BAEIADgEAAEEBCws= +invalid invalid br_table.wast:1283 AGFzbQEAAAABBAFgAAADAgEAChMBEQACQEEAQQAEfw4AAAsLRRoL +invalid invalid br_table.wast:1295 AGFzbQEAAAABBAFgAAADAgEAChUBEwACQEEAQQAEf0EBDgAACwtFGgs= +invalid invalid br_table.wast:1307 AGFzbQEAAAABBAFgAAADAgEACg0BCwACfw4AAA8LRRoL +invalid invalid br_table.wast:1318 AGFzbQEAAAABBAFgAAADAgEACg8BDQACf0EBDgAADwtFGgs= +invalid invalid br_table.wast:1330 AGFzbQEAAAABBgFgAX8BfwMCAQAKEgEQAAN/An9BASAADgEAAQsLCw== +invalid invalid br_table.wast:1342 AGFzbQEAAAABBgFgAX8BfwMCAQAKEgEQAAJ/A39BASAADgEAAQsLCw== +invalid invalid br_table.wast:1356 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEBDgECAQsL +invalid invalid br_table.wast:1362 AGFzbQEAAAABBAFgAAADAgEAChABDgACQAJAQQEOAQAFCwsL +invalid invalid br_table.wast:1368 AGFzbQEAAAABBAFgAAADAgEAChIBEAACQEEBDgIAgYCAgAEACws= +invalid invalid br_table.wast:1375 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEBDgEBAgsL +invalid invalid br_table.wast:1387 AGFzbQEAAAABBAFgAAADAgEAChIBEAACQEEBDgIAAIGAgIABCws= +valid valid bulk.wast:2 AGFzbQEAAAAFAwEAAQsGAQEDZm9v +valid valid bulk.wast:6 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAAAwkNAQVwA9IAC9BwC9IBCwoHAgIACwIACw== +valid valid bulk.wast:13 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQMBAAEHEgIEZmlsbAAAB2xvYWQ4X3UAAQoVAgsAIAAgASAC/AsACwcAIAAtAAAL +valid valid bulk.wast:57 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxICBGNvcHkAAAdsb2FkOF91AAEKFgIMACAAIAEgAvwKAAALBwAgAC0AAAsLCgEAQQALBKq7zN0= +valid valid bulk.wast:115 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQMBAAEHEgIEaW5pdAAAB2xvYWQ4X3UAAQwBAQoWAgwAIAAgASAC/AgAAAsHACAALQAACwsHAQEEqrvM3Q== +valid valid bulk.wast:154 AGFzbQEAAAABCAJgAABgAX8AAwUEAAEAAQUDAQABBzsEDGRyb3BfcGFzc2l2ZQAADGluaXRfcGFzc2l2ZQABC2Ryb3BfYWN0aXZlAAILaW5pdF9hY3RpdmUAAwwBAgonBAUA/AkACwwAQQBBACAA/AgAAAsFAPwJAQsMAEEAQQAgAPwIAQALCwoCAQF4AEEACwF4 +valid valid bulk.wast:181 AGFzbQEAAAABBAFgAAADAgEADAFBCgcBBQD8CUALC4MBQQEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQA= +valid valid bulk.wast:196 AGFzbQEAAAABBAFgAAADAgEADAEBCgcBBQD8CQALCwoBAQdnb29kYnll +valid valid bulk.wast:199 AGFzbQEAAAABEANgAAF/YAN/f38AYAF/AX8DBQQAAAECBAQBcAADBw8CBGluaXQAAgRjYWxsAAMJCAEBAAQAAQABCiAEBABBAAsEAEEBCwwAIAAgASAC/AwAAAsHACAAEQAACw== +valid valid bulk.wast:244 AGFzbQEAAAABCAJgAABgAX8AAwYFAAABAAEEBAFwAAEHOwQMZHJvcF9wYXNzaXZlAAEMaW5pdF9wYXNzaXZlAAILZHJvcF9hY3RpdmUAAwtpbml0X2FjdGl2ZQAECQsCAQABAABBAAsBAAoqBQIACwUA/A0ACwwAQQBBACAA/AwAAAsFAPwNAQsMAEEAQQAgAPwMAQAL +valid valid bulk.wast:274 AGFzbQEAAAABBAFgAAADAgEACcQBQQEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAoHAQUA/A1ACw== +valid valid bulk.wast:297 AGFzbQEAAAABBAFgAAADAgEACQUBAQABAAoHAQUA/A0ACw== +valid valid bulk.wast:300 AGFzbQEAAAABEANgAAF/YAN/f38AYAF/AX8DBgUAAAABAgQEAXAACgcPAgRjb3B5AAMEY2FsbAAECQkBAEEACwMAAQIKJQUEAEEACwQAQQELBABBAgsMACAAIAEgAvwOAAALBwAgABEAAAs= +valid valid call.wast:3 AGFzbQEAAAAB6QEZYAJ/fwF/YAABf2AAAX5gAAF9YAABfGAAAn9+YAF/AX9gAX4BfmABfQF9YAF8AXxgAn98An98YAJ/fwJ/f2ACfXwCfH1gAnx/An98YAJ9fwF/YAJ/fgF+YAJ8fQF9YAJ+fAF8YAACf3xgAAJ/f2AAAnx9YAJ+fgF+YAF+AX9gAABgZH1/f3x9fX18fX9/fXx+fn9+fn1+fn5/fX19fH1/fn18fH1/fX18fnx/fn18f39/fnx/fn58fHx8fHx/fXx8f359fX1/fHx8fHx9fn5/f399fH9+fX19f399fH59fH19fX99fn8BfwNPTgECAwQFBgcICQoLDA0ODxARAQIDBAUBAgMEAQIDBBITFBIBARMHFQcWFhcXFwEBAQEBAQEBAAEBARcXAQEXAQEBAQEGCAMBAQEBAQIYBgQFAXABAQEFAwEAAQYGAX8BQQoLB8oHOAh0eXBlLWkzMgARCHR5cGUtaTY0ABIIdHlwZS1mMzIAEwh0eXBlLWY2NAAUDHR5cGUtaTMyLWk2NAAVDnR5cGUtZmlyc3QtaTMyABYOdHlwZS1maXJzdC1pNjQAFw50eXBlLWZpcnN0LWYzMgAYDnR5cGUtZmlyc3QtZjY0ABkPdHlwZS1zZWNvbmQtaTMyABoPdHlwZS1zZWNvbmQtaTY0ABsPdHlwZS1zZWNvbmQtZjMyABwPdHlwZS1zZWNvbmQtZjY0AB0QdHlwZS1hbGwtaTMyLWY2NAAeEHR5cGUtYWxsLWkzMi1pMzIAHxB0eXBlLWFsbC1mMzItZjY0ACAQdHlwZS1hbGwtZjY0LWkzMgAhFmFzLWJpbmFyeS1hbGwtb3BlcmFuZHMAIhFhcy1taXhlZC1vcGVyYW5kcwAjFGFzLWNhbGwtYWxsLW9wZXJhbmRzACQDZmFjACUHZmFjLWFjYwAmA2ZpYgAnBGV2ZW4AKANvZGQAKQdydW5hd2F5ACoObXV0dWFsLXJ1bmF3YXkAKw9hcy1zZWxlY3QtZmlyc3QALQ1hcy1zZWxlY3QtbWlkAC4OYXMtc2VsZWN0LWxhc3QALw9hcy1pZi1jb25kaXRpb24AMA5hcy1icl9pZi1maXJzdAAxDWFzLWJyX2lmLWxhc3QAMhFhcy1icl90YWJsZS1maXJzdAAzEGFzLWJyX3RhYmxlLWxhc3QANBZhcy1jYWxsX2luZGlyZWN0LWZpcnN0ADYUYXMtY2FsbF9pbmRpcmVjdC1taWQANxVhcy1jYWxsX2luZGlyZWN0LWxhc3QAOA5hcy1zdG9yZS1maXJzdAA5DWFzLXN0b3JlLWxhc3QAOhRhcy1tZW1vcnkuZ3Jvdy12YWx1ZQA7D2FzLXJldHVybi12YWx1ZQA8D2FzLWRyb3Atb3BlcmFuZAA9C2FzLWJyLXZhbHVlAD4SYXMtbG9jYWwuc2V0LXZhbHVlAD8SYXMtbG9jYWwudGVlLXZhbHVlAEATYXMtZ2xvYmFsLnNldC12YWx1ZQBBD2FzLWxvYWQtb3BlcmFuZABCEGFzLXVuYXJ5LW9wZXJhbmQARQ5hcy1iaW5hcnktbGVmdABGD2FzLWJpbmFyeS1yaWdodABHD2FzLXRlc3Qtb3BlcmFuZABID2FzLWNvbXBhcmUtbGVmdABJEGFzLWNvbXBhcmUtcmlnaHQAShJhcy1jb252ZXJ0LW9wZXJhbmQASx5yZXR1cm4tZnJvbS1sb25nLWFyZ3VtZW50LWxpc3QATQkHAQBBAAsBNQr2CU4FAEGyAgsFAELkAgsHAEMAIHNFCwsARAAAAAAAyK5ACwgAQbICQuQCCwQAIAALBAAgAAsEACAACwQAIAALBgAgACABCwYAIAEgAAsGACABIAALBgAgASAACwQAIAELBAAgAQsEACABCwQAIAELBAAQAAsEABABCwQAEAILBAAQAwsEABAECwYAQSAQBQsHAELAABAGCwkAQ8P1qD8QBwsNAEQ9CtejcD36PxAICwsAQ2ZmAEJBIBANCwkAQSBCwAAQDgsSAEQAAAAAAABQQEMAAABCEA8LEABCwABEZmZmZmYGUEAQEAsPAEEgRD0K16NwPfo/EAkLCABBAUECEAoLEgBDAACAP0QAAAAAAAAAQBALCw8ARAAAAAAAAPA/QQIQDAsJAEEDQQQQCmoLDABBA0EEEApBBWpsCwoAQQNBBBAKEAoLFQAgAFAEfkIBBSAAIABCAX0QJX4LCxcAIABQBH4gAQUgAEIBfSAAIAF+ECYLCxwAIABCAVgEfkIBBSAAQgJ9ECcgAEIBfRAnfAsLEgAgAFAEf0EsBSAAQgF9ECkLCxMAIABQBH9B4wAFIABCAX0QKAsLBAAQKgsEABAsCwQAECsLCQAQAEECQQMbCwkAQQIQAEEDGwsJAEECQQMQABsLDAAQAAR/QQEFQQILCwsAAn8QAEECDQALCwsAAn9BAhAADQALCw0AAn8QAEECDgEAAAsLDQACf0ECEAAOAQAACwsEACAACw4AAn8QAEECQQARAAALCw4AAn9BAhAAQQARAAALCw4AAn9BAUECEAARAAALCwkAEABBATYCAAsJAEEKEAA2AgALBgAQAEAACwUAEAAPCwUAEAAaCwkAAn8QAAwACwsKAQF/EAAhACAACwgBAX8QACIACwgAEAAkACMACwcAEAAoAgALBAAgAAsEACAACw0AAn1DAAAAABBEkQsLDAACf0EBEENBCmoLCwwAAn9BCkEBEENrCwsKAAJ/QQEQQ0ULCwwAAn9BARBDQQpNCwsMAAJ/QQpBARBDRwsLCgACfkEBEEOsCwsEACBjC98DAEMAAAAAQQBBAEQAAAAAAAAAAEMAAAAAQwAAAABDAAAAAEQAAAAAAAAAAEMAAAAAQQBBAEMAAAAARAAAAAAAAAAAQgBCAEEAQgBCAEMAAAAAQgBCAEIAQQBDAAAAAEMAAAAAQwAAAABEAAAAAAAAAABDAAAAAEEAQgBDAAAAAEQAAAAAAAAAAEQAAAAAAAAAAEMAAAAAQQBDAAAAAEMAAAAARAAAAAAAAAAAQgBEAAAAAAAAAABBAEIAQwAAAABEAAAAAAAAAABBAEEAQQBCAEQAAAAAAAAAAEEAQgBCAEQAAAAAAAAAAEQAAAAAAAAAAEQAAAAAAAAAAEQAAAAAAAAAAEQAAAAAAAAAAEQAAAAAAAAAAEEAQwAAAABEAAAAAAAAAABEAAAAAAAAAABBAEIAQwAAAABDAAAAAEMAAAAAQQBEAAAAAAAAAABEAAAAAAAAAABEAAAAAAAAAABEAAAAAAAAAABEAAAAAAAAAABDAAAAAEIAQgBBAEEAQQBDAAAAAEQAAAAAAAAAAEEAQgBDAAAAAEMAAAAAQwAAAABBAEEAQwAAAABEAAAAAAAAAABCAEMAAAAARAAAAAAAAAAAQwAAAABDAAAAAEMAAAAAQQBDAAAAAEIAIAAQTAs= +invalid invalid call.wast:381 AGFzbQEAAAABBAFgAAADAwIAAAoKAgUAEAFFCwIACw== +invalid invalid call.wast:388 AGFzbQEAAAABCAJgAABgAAF+AwMCAAEKDAIFABABRQsEAEIBCw== +invalid invalid call.wast:396 AGFzbQEAAAABCAJgAABgAX8AAwMCAAEKCQIEABABCwIACw== +invalid invalid call.wast:403 AGFzbQEAAAABCQJgAABgAnx/AAMDAgABCgkCBAAQAQsCAAs= +invalid invalid call.wast:410 AGFzbQEAAAABBAFgAAADAwIAAAoLAgYAQQEQAQsCAAs= +invalid invalid call.wast:417 AGFzbQEAAAABBAFgAAADAwIAAAoUAg8ARAAAAAAAAABAQQEQAQsCAAs= +invalid invalid call.wast:425 AGFzbQEAAAABCQJgAABgAn9/AAMDAgABCgwCBwABQQEQAQsCAAs= +invalid invalid call.wast:432 AGFzbQEAAAABCQJgAABgAn9/AAMDAgABCgwCBwBBAQEQAQsCAAs= +invalid invalid call.wast:439 AGFzbQEAAAABCQJgAABgAn98AAMDAgABChQCDwBEAAAAAAAA8D9BARABCwIACw== +invalid invalid call.wast:446 AGFzbQEAAAABCQJgAABgAnx/AAMDAgABChQCDwBBAUQAAAAAAADwPxABCwIACw== +invalid invalid call.wast:454 AGFzbQEAAAABCAJgAABgAX8AAwMCAAEKDAIHAAJAEAELCwIACw== +invalid invalid call.wast:463 AGFzbQEAAAABCQJgAABgAn9/AAMDAgABCg4CCQACQEEAEAELCwIACw== +invalid invalid call.wast:472 AGFzbQEAAAABCAJgAABgAX8AAwMCAAEKDAIHAANAEAELCwIACw== +invalid invalid call.wast:481 AGFzbQEAAAABCQJgAABgAn9/AAMDAgABCg4CCQADQEEAEAELCwIACw== +invalid invalid call.wast:490 AGFzbQEAAAABCAJgAABgAX8AAwMCAAEKDgIJAEEABEAQAQsLAgAL +invalid invalid call.wast:499 AGFzbQEAAAABCQJgAABgAn9/AAMDAgABChACCwBBAARAQQAQAQsLAgAL +invalid invalid call.wast:512 AGFzbQEAAAABBAFgAAADAgEACgYBBAAQAQs= +invalid invalid call.wast:516 AGFzbQEAAAABBAFgAAADAgEACgoBCAAQlJjb4gML +valid valid call_indirect.wast:3 AGFzbQEAAAABmAEdYAAAYAABf2AAAX5gAAF9YAABfGAAAnx/YAF/AX9gAX4BfmABfQF9YAF8AXxgAn98An98YAJ/fgJ+f2ACfX8Bf2ACf34BfmACfH0BfWACfnwBfGABfwF/YAF+AX5gAX0BfWABfAF8YAF+AGAEfnx/fgBgAX4Bf2AEfnx/fgF/YAACf3xgAAJ+f2ABfwF+YAF/AX1gAX8BfANRUAECAwQFBgcICQoLDQ8MDhAREhMAAQIDBAUCAQIDBAECAwQFGBkNGgYbHAcHBggJBggJBgYAAAABAQEBAgEDAQAAAQEAAwQEBAEDAQEBAQECBAUBcAEgIAUDAQABBg0BfAFEAAAAAAAAJEALB8QHOwh0eXBlLWkzMgAUCHR5cGUtaTY0ABUIdHlwZS1mMzIAFgh0eXBlLWY2NAAXDHR5cGUtZjY0LWkzMgAYCnR5cGUtaW5kZXgAGQ50eXBlLWZpcnN0LWkzMgAaDnR5cGUtZmlyc3QtaTY0ABsOdHlwZS1maXJzdC1mMzIAHA50eXBlLWZpcnN0LWY2NAAdD3R5cGUtc2Vjb25kLWkzMgAeD3R5cGUtc2Vjb25kLWk2NAAfD3R5cGUtc2Vjb25kLWYzMgAgD3R5cGUtc2Vjb25kLWY2NAAhEHR5cGUtYWxsLWY2NC1pMzIAIhB0eXBlLWFsbC1pMzItZjY0ACMQdHlwZS1hbGwtaTMyLWk2NAAkCGRpc3BhdGNoACUXZGlzcGF0Y2gtc3RydWN0dXJhbC1pNjQAJhdkaXNwYXRjaC1zdHJ1Y3R1cmFsLWkzMgAnF2Rpc3BhdGNoLXN0cnVjdHVyYWwtZjMyACgXZGlzcGF0Y2gtc3RydWN0dXJhbC1mNjQAKQdmYWMtaTY0ACoHZmliLWk2NAArB2ZhYy1pMzIALAdmYWMtZjMyAC0HZmFjLWY2NAAuB2ZpYi1pMzIALwdmaWItZjMyADAHZmliLWY2NAAxBGV2ZW4AMgNvZGQAMwdydW5hd2F5ADQObXV0dWFsLXJ1bmF3YXkANQ9hcy1zZWxlY3QtZmlyc3QANw1hcy1zZWxlY3QtbWlkADgOYXMtc2VsZWN0LWxhc3QAOQ9hcy1pZi1jb25kaXRpb24AOg5hcy1icl9pZi1maXJzdAA7DWFzLWJyX2lmLWxhc3QAPBFhcy1icl90YWJsZS1maXJzdAA9EGFzLWJyX3RhYmxlLWxhc3QAPg5hcy1zdG9yZS1maXJzdAA/DWFzLXN0b3JlLWxhc3QAQBRhcy1tZW1vcnkuZ3Jvdy12YWx1ZQBBD2FzLXJldHVybi12YWx1ZQBCD2FzLWRyb3Atb3BlcmFuZABDC2FzLWJyLXZhbHVlAEQSYXMtbG9jYWwuc2V0LXZhbHVlAEUSYXMtbG9jYWwudGVlLXZhbHVlAEYTYXMtZ2xvYmFsLnNldC12YWx1ZQBHD2FzLWxvYWQtb3BlcmFuZABIEGFzLXVuYXJ5LW9wZXJhbmQASQ5hcy1iaW5hcnktbGVmdABKD2FzLWJpbmFyeS1yaWdodABLD2FzLXRlc3Qtb3BlcmFuZABMD2FzLWNvbXBhcmUtbGVmdABNEGFzLWNvbXBhcmUtcmlnaHQAThJhcy1jb252ZXJ0LW9wZXJhbmQATwkmAQBBAAsgAAECAwUGBwgNCw4MKisyMzQ1Ng8QERIsLS4vMDEECQoK4QlQBQBBsgILBQBC5AILBwBDACBzRQsLAEQAAAAAAMiuQAsNAEQAAAAAAMiuQEEgCwQAIAALBAAgAAsEACAACwQAIAALBgAgACABCwYAIAEgAAsEACABCwQAIAELBAAgAQsEACABCwQAIAALBAAgAAsEACAACwQAIAALXQBBABEAAEIAQQARFABCAEQAAAAAAAAAAEEAQgBBABEVAEEAEQAAQQARAQBFGkEAEQEARRpCAEEAERYARRpCAEQAAAAAAAAAAEEAQgBBABEXAEUaQgBBABEHAFAaCwcAQQARAQALBwBBARECAAsHAEECEQMACwcAQQMRBAALBwBBHREFAAsKAELkAEEFEQcACwkAQSBBBBEGAAsKAELAAEEFEQcACwwAQ8P1qD9BBhEIAAsQAEQ9CtejcD36P0EHEQkACw4AQ2ZmAEJBIEEIEQwACwwAQSBCwABBCRENAAsVAEQAAAAAAABQQEMAAABCQQoRDgALEwBCwABEZmZmZmYGUEBBCxEPAAsHAEEdEQUACxIAQQFEAAAAAAAAAEBBHhEKAAsLAEEBQgJBHxELAAsJACABIAARBwALCQBCCSAAEREACwkAQQkgABEQAAsMAEMAABBBIAAREgALEABEAAAAAAAAIkAgABETAAsYACAAUAR+QgEFIAAgAEIBfUEMEQcAfgsLIgAgAEIBWAR+QgEFIABCAn1BDREHACAAQgF9QQ0RBwB8CwsYACAARQR/QQEFIAAgAEEBa0EXEQYAbAsLIwAgAEMAAAAAWwR9QwAAgD8FIAAgAEMAAIA/k0EYEQgAlAsLLwAgAEQAAAAAAAAAAGEEfEQAAAAAAADwPwUgACAARAAAAAAAAPA/oUEZEQkAogsLIgAgAEEBTQR/QQEFIABBAmtBGhEGACAAQQFrQRoRBgBqCwsuACAAQwAAgD9fBH1DAACAPwUgAEMAAABAk0EbEQgAIABDAACAP5NBGxEIAJILCz4AIABEAAAAAAAA8D9lBHxEAAAAAAAA8D8FIABEAAAAAAAAAEChQRwRCQAgAEQAAAAAAADwP6FBHBEJAKALCxUAIABFBH9BLAUgAEEBa0EPEQYACwsWACAARQR/QeMABSAAQQFrQQ4RBgALCwcAQRARAAALBwBBEhEAAAsHAEEREQAACwwAQQARAQBBAkEDGwsMAEECQQARAQBBAxsLDABBAkEDQQARAQAbCw8AQQARAQAEf0EBBUECCwsOAAJ+QQERAgBBAg0ACwsOAAJ/QQJBABEBAA0ACwsQAAJ9QQIRAwBBAg4BAAALCxAAAn9BAkEAEQEADgEAAAsLDABBABEBAEEBNgIACwwAQQpBAxEEADkDAAsJAEEAEQEAQAALCgBBAUEEEQYADwsKAEIBQQURBwAaCxEAAn1DAACAP0EGEQgADAALCxYBAXxEAAAAAAAA8D9BBxEJACEAIAALFAEBfEQAAAAAAADwP0EHEQkAIgALFABEAAAAAAAA8D9BBxEJACQAIwALCgBBABEBACgCAAsQAAJ9QwAAAABBBhEIAJELCw8AAn9BAUEEEQYAQQpqCwsPAAJ/QQpBAUEEEQYAawsLDQACf0EBQQQRBgBFCwsPAAJ/QQFBBBEGAEEKTQsLDwACf0EKQQFBBBEGAEcLCw0AAn5BAUEEEQYArAsL +valid valid call_indirect.wast:623 AGFzbQEAAAABEQNgAn9/AX9gAABgA39/fwF/AwoJAAAAAAABAgICBAwDcAECAnABAwNwAAQHHAMGY2FsbC0xAAYGY2FsbC0yAAcGY2FsbC0zAAgJIwQAQQALAgABAgFBAAsAAwIDBAICQQALAAIBAgICQQMLAAEFClAJBwAgACABagsHACAAIAFrCwcAIAAgAWwLBwAgACABbgsHACAAIAFwCwIACwsAIAAgASACEQAACwsAIAAgASACEQABCwsAIAAgASACEQACCw== +invalid invalid call_indirect.wast:791 AGFzbQEAAAABBAFgAAADAgEACgkBBwBBABEAAAs= +invalid invalid call_indirect.wast:799 AGFzbQEAAAABBAFgAAADAgEABAQBcAAACgoBCABBABEAAEUL +invalid invalid call_indirect.wast:807 AGFzbQEAAAABCAJgAAF+YAAAAwIBAQQEAXAAAAoKAQgAQQARAABFCw== +invalid invalid call_indirect.wast:816 AGFzbQEAAAABCAJgAX8AYAAAAwIBAQQEAXAAAAoJAQcAQQARAAAL +invalid invalid call_indirect.wast:824 AGFzbQEAAAABCQJgAnx/AGAAAAMCAQEEBAFwAAAKCQEHAEEAEQAACw== +invalid invalid call_indirect.wast:832 AGFzbQEAAAABBAFgAAADAgEABAQBcAAACgsBCQBBAUEAEQAACw== +invalid invalid call_indirect.wast:840 AGFzbQEAAAABBAFgAAADAgEABAQBcAAAChQBEgBEAAAAAAAAAEBBAUEAEQAACw== +invalid invalid call_indirect.wast:851 AGFzbQEAAAABCAJgAX8AYAAAAwIBAQQEAXAAAAoKAQgAQQEBEQAACw== +invalid invalid call_indirect.wast:859 AGFzbQEAAAABCAJgAX8AYAAAAwIBAQQEAXAAAAoLAQkAQQBCAREAAAs= +invalid invalid call_indirect.wast:868 AGFzbQEAAAABCQJgAn9/AGAAAAMCAQEEBAFwAAAKDAEKAAFBAUEAEQAACw== +invalid invalid call_indirect.wast:878 AGFzbQEAAAABCQJgAn9/AGAAAAMCAQEEBAFwAAAKDAEKAEEBAUEAEQAACw== +invalid invalid call_indirect.wast:888 AGFzbQEAAAABCQJgAn98AGAAAAMCAQEEBAFwAAAKFAESAEQAAAAAAADwP0EBQQARAAAL +invalid invalid call_indirect.wast:898 AGFzbQEAAAABCQJgAnx/AGAAAAMCAQEEBAFwAAAKFAESAEEBRAAAAAAAAPA/QQARAAAL +invalid invalid call_indirect.wast:909 AGFzbQEAAAABCAJgAX8AYAAAAwMCAAEEBQFwAQEBCQcBAEEACwEACg8CAgALCgACQEEAEQAACws= +invalid invalid call_indirect.wast:922 AGFzbQEAAAABCQJgAn9/AGAAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoRAgIACwwAAkBBAEEAEQAACws= +invalid invalid call_indirect.wast:935 AGFzbQEAAAABCAJgAX8AYAAAAwMCAAEEBQFwAQEBCQcBAEEACwEACg8CAgALCgADQEEAEQAACws= +invalid invalid call_indirect.wast:948 AGFzbQEAAAABCQJgAn9/AGAAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoRAgIACwwAA0BBAEEAEQAACws= +invalid invalid call_indirect.wast:961 AGFzbQEAAAABCAJgAX8AYAAAAwMCAAEEBQFwAQEBCQcBAEEACwEAChMCAgALDgBBAEEABEBBABEAAAsL +invalid invalid call_indirect.wast:977 AGFzbQEAAAABCQJgAn9/AGAAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoVAgIACxAAQQBBAARAQQBBABEAAAsL +invalid invalid call_indirect.wast:995 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCgkBBwBBABEAAAs= +invalid invalid call_indirect.wast:1006 AGFzbQEAAAABBAFgAAADAgEABAQBcAAACgkBBwBBABEBAAs= +invalid invalid call_indirect.wast:1013 AGFzbQEAAAABBAFgAAADAgEABAQBcAAACg0BCwBBABGUmNviAwAL +invalid invalid call_indirect.wast:1022 AGFzbQEAAAABCAJgAX8AYAAAAwIBAQQEAXAAAQoSARAAQQEEQEEAEf////8PAAsL +invalid invalid call_indirect.wast:1038 AGFzbQEAAAAEBQFwAQICCQgBAEEACwIAAA== +valid valid call_indirect.wast:1047 AGFzbQEAAAABCAJgAABgAAF/AwwLAAAAAAAAAAAAAQEEBAFwAAEKZgsGAAARAAALBwAAEQAAAQsJAAARAAARAAALCQAAEQAAEQAACwwAABEAABEAABEAAAsGAAARAAALBgAAEQAACwkAABEAABEAAAsJAAARAAARAAALBwAAEQAAGwsKAAARAAAbEQAACw== +valid valid comments.wast:0 AGFzbQEAAAABBQFgAAF/AwQDAAAABxADAmYxAAACZjIAAQJmMwACChkDBwBBAUECDwsHAEEBQQIPCwcAQQFBAg8L +valid valid const.wast:5 AGFzbQEAAAABBAFgAAADAgEACgoBCABBlZrvOhoL +valid valid const.wast:6 AGFzbQEAAAABBAFgAAADAgEACgsBCQBB3/e/1nkaCw== +valid valid const.wast:24 AGFzbQEAAAABBAFgAAADAgEACgoBCABClZrvOhoL +valid valid const.wast:25 AGFzbQEAAAABBAFgAAADAgEACg8BDQBC7/m+75rx2ZIBGgs= +valid valid const.wast:43 AGFzbQEAAAABBAFgAAADAgEACgoBCABDo3nrTBoL +valid valid const.wast:44 AGFzbQEAAAABBAFgAAADAgEACgoBCABDe01/bBoL +valid valid const.wast:46 AGFzbQEAAAABBAFgAAADAgEACgoBCABD/y9ZLRoL +valid valid const.wast:55 AGFzbQEAAAABBAFgAAADAgEACgoBCABDtKKRWxoL +valid valid const.wast:56 AGFzbQEAAAABBAFgAAADAgEACgoBCABDtKIRZRoL +valid valid const.wast:58 AGFzbQEAAAABBAFgAAADAgEACgoBCABDtKIRUhoL +valid valid const.wast:145 AGFzbQEAAAABBAFgAAADAgEACg4BDABEAAAAVDRvnUEaCw== +valid valid const.wast:146 AGFzbQEAAAABBAFgAAADAgEACg4BDABE+hZeW6/pj0UaCw== +valid valid const.wast:148 AGFzbQEAAAABBAFgAAADAgEACg4BDABEEUMr1v8lqz0aCw== +valid valid const.wast:153 AGFzbQEAAAABBAFgAAADAgEACg4BDABEWKQMVDRvnUEaCw== +valid valid const.wast:154 AGFzbQEAAAABBAFgAAADAgEACg4BDABE1ctrW6/pj0UaCw== +valid valid const.wast:156 AGFzbQEAAAABBAFgAAADAgEACg4BDABEEuw21v8lqz0aCw== +valid valid const.wast:164 AGFzbQEAAAABBAFgAAADAgEACg4BDABE37yaeFY08kQaCw== +valid valid const.wast:165 AGFzbQEAAAABBAFgAAADAgEACg4BDABE37yaeFY0IkYaCw== +valid valid const.wast:167 AGFzbQEAAAABBAFgAAADAgEACg4BDABE37yaeFY0wkMaCw== +valid valid const.wast:264 AGFzbQEAAAABBAFgAAADAgEACgcBBQBBfxoL +valid valid const.wast:265 AGFzbQEAAAABBAFgAAADAgEACgsBCQBBgICAgHgaCw== +valid valid const.wast:286 AGFzbQEAAAABBAFgAAADAgEACgcBBQBCfxoL +valid valid const.wast:287 AGFzbQEAAAABBAFgAAADAgEAChABDgBCgICAgICAgICAfxoL +valid valid const.wast:308 AGFzbQEAAAABBAFgAAADAgEACgoBCABDAAAAfxoL +valid valid const.wast:309 AGFzbQEAAAABBAFgAAADAgEACgoBCABDAAAA/xoL +valid valid const.wast:310 AGFzbQEAAAABBAFgAAADAgEACgoBCABD//9/fxoL +valid valid const.wast:311 AGFzbQEAAAABBAFgAAADAgEACgoBCABD//9//xoL +valid valid const.wast:335 AGFzbQEAAAABBAFgAAADAgEACgoBCABDmXaWfhoL +valid valid const.wast:336 AGFzbQEAAAABBAFgAAADAgEACgoBCABDmXaW/hoL +valid valid const.wast:357 AGFzbQEAAAABBAFgAAADAgEACg4BDABEAAAAAAAA4H8aCw== +valid valid const.wast:358 AGFzbQEAAAABBAFgAAADAgEACg4BDABEAAAAAAAA4P8aCw== +valid valid const.wast:359 AGFzbQEAAAABBAFgAAADAgEACg4BDABE////////738aCw== +valid valid const.wast:360 AGFzbQEAAAABBAFgAAADAgEACg4BDABE////////7/8aCw== +valid valid const.wast:382 AGFzbQEAAAABBAFgAAADAgEACg4BDABEoMjrhfPM4X8aCw== +valid valid const.wast:383 AGFzbQEAAAABBAFgAAADAgEACg4BDABEoMjrhfPM4f8aCw== +valid valid const.wast:404 AGFzbQEAAAABBAFgAAADAgEACgoBCABDAQCAfxoL +valid valid const.wast:405 AGFzbQEAAAABBAFgAAADAgEACg4BDABEAQAAAAAA8H8aCw== +valid valid const.wast:406 AGFzbQEAAAABBAFgAAADAgEACgoBCABD////fxoL +valid valid const.wast:407 AGFzbQEAAAABBAFgAAADAgEACg4BDABE/////////38aCw== +valid valid const.wast:440 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMAAIAmCw== +valid valid const.wast:442 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMAAICmCw== +valid valid const.wast:444 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMBAIAmCw== +valid valid const.wast:446 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMBAICmCw== +valid valid const.wast:464 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMCAIAmCw== +valid valid const.wast:466 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMCAICmCw== +valid valid const.wast:492 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMDAIAmCw== +valid valid const.wast:494 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMDAICmCw== +valid valid const.wast:564 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMAAIBYCw== +valid valid const.wast:566 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMAAIDYCw== +valid valid const.wast:568 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMBAIBYCw== +valid valid const.wast:570 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMBAIDYCw== +valid valid const.wast:588 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMCAIBYCw== +valid valid const.wast:590 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMCAIDYCw== +valid valid const.wast:616 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMDAIBYCw== +valid valid const.wast:618 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMDAIDYCw== +valid valid const.wast:668 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMAAAAACw== +valid valid const.wast:670 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMAAACACw== +valid valid const.wast:672 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMBAAAACw== +valid valid const.wast:674 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMBAACACw== +valid valid const.wast:692 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMCAAAACw== +valid valid const.wast:694 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMCAACACw== +valid valid const.wast:720 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMDAAAACw== +valid valid const.wast:722 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEMDAACACw== +valid valid const.wast:726 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEP//39/Cw== +valid valid const.wast:728 AGFzbQEAAAABBQFgAAF9AwIBAAcFAQFmAAAKCQEHAEP//3//Cw== +valid valid const.wast:740 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAABwGgs= +valid valid const.wast:742 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAABwmgs= +valid valid const.wast:744 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAABwGgs= +valid valid const.wast:746 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAABwmgs= +valid valid const.wast:764 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAABwGgs= +valid valid const.wast:766 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAABwmgs= +valid valid const.wast:788 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAABwGgs= +valid valid const.wast:790 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAABwmgs= +valid valid const.wast:846 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAABgfgs= +valid valid const.wast:848 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAABg/gs= +valid valid const.wast:850 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAABgfgs= +valid valid const.wast:852 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAABg/gs= +valid valid const.wast:858 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAABgfgs= +valid valid const.wast:860 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAABg/gs= +valid valid const.wast:864 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAABwZQs= +valid valid const.wast:866 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAABw5Qs= +valid valid const.wast:868 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAABwZQs= +valid valid const.wast:870 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAABw5Qs= +valid valid const.wast:888 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAABwZQs= +valid valid const.wast:890 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAABw5Qs= +valid valid const.wast:916 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAABwZQs= +valid valid const.wast:918 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAABw5Qs= +valid valid const.wast:921 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAAAARgs= +valid valid const.wast:923 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAAAAxgs= +valid valid const.wast:925 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAAAARgs= +valid valid const.wast:927 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAAAAxgs= +valid valid const.wast:945 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAAAARgs= +valid valid const.wast:947 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAAAAxgs= +valid valid const.wast:973 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAAAARgs= +valid valid const.wast:975 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAAAAxgs= +valid valid const.wast:978 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAACwQws= +valid valid const.wast:980 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAACwwws= +valid valid const.wast:982 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAACwQws= +valid valid const.wast:984 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAACwwws= +valid valid const.wast:990 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAACwQws= +valid valid const.wast:992 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAACwwws= +valid valid const.wast:996 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAAAAAAs= +valid valid const.wast:998 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQAAAAAAAAAgAs= +valid valid const.wast:1000 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAAAAAAs= +valid valid const.wast:1002 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQBAAAAAAAAgAs= +valid valid const.wast:1020 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAAAAAAs= +valid valid const.wast:1022 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQCAAAAAAAAgAs= +valid valid const.wast:1048 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAAAQAAs= +valid valid const.wast:1050 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAEQDAAAAAAAQgAs= +valid valid const.wast:1054 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAET////////vfws= +valid valid const.wast:1056 AGFzbQEAAAABBQFgAAF8AwIBAAcFAQFmAAAKDQELAET////////v/ws= +valid valid conversions.wast:1 AGFzbQEAAAABPQxgAX8BfmABfgF/YAF9AX9gAXwBf2ABfQF+YAF8AX5gAX8BfWABfgF9YAF/AXxgAX4BfGABfQF8YAF8AX0DIiEAAAECAgMDBAQFBQICAwMEBAUFBgcICQYHCAkKCwYJAgUHkQUhEGk2NC5leHRlbmRfaTMyX3MAABBpNjQuZXh0ZW5kX2kzMl91AAEMaTMyLndyYXBfaTY0AAIPaTMyLnRydW5jX2YzMl9zAAMPaTMyLnRydW5jX2YzMl91AAQPaTMyLnRydW5jX2Y2NF9zAAUPaTMyLnRydW5jX2Y2NF91AAYPaTY0LnRydW5jX2YzMl9zAAcPaTY0LnRydW5jX2YzMl91AAgPaTY0LnRydW5jX2Y2NF9zAAkPaTY0LnRydW5jX2Y2NF91AAoTaTMyLnRydW5jX3NhdF9mMzJfcwALE2kzMi50cnVuY19zYXRfZjMyX3UADBNpMzIudHJ1bmNfc2F0X2Y2NF9zAA0TaTMyLnRydW5jX3NhdF9mNjRfdQAOE2k2NC50cnVuY19zYXRfZjMyX3MADxNpNjQudHJ1bmNfc2F0X2YzMl91ABATaTY0LnRydW5jX3NhdF9mNjRfcwARE2k2NC50cnVuY19zYXRfZjY0X3UAEhFmMzIuY29udmVydF9pMzJfcwATEWYzMi5jb252ZXJ0X2k2NF9zABQRZjY0LmNvbnZlcnRfaTMyX3MAFRFmNjQuY29udmVydF9pNjRfcwAWEWYzMi5jb252ZXJ0X2kzMl91ABcRZjMyLmNvbnZlcnRfaTY0X3UAGBFmNjQuY29udmVydF9pMzJfdQAZEWY2NC5jb252ZXJ0X2k2NF91ABoPZjY0LnByb21vdGVfZjMyABsOZjMyLmRlbW90ZV9mNjQAHBNmMzIucmVpbnRlcnByZXRfaTMyAB0TZjY0LnJlaW50ZXJwcmV0X2k2NAAeE2kzMi5yZWludGVycHJldF9mMzIAHxNpNjQucmVpbnRlcnByZXRfZjY0ACAKzwEhBQAgAKwLBQAgAK0LBQAgAKcLBQAgAKgLBQAgAKkLBQAgAKoLBQAgAKsLBQAgAK4LBQAgAK8LBQAgALALBQAgALELBgAgAPwACwYAIAD8AQsGACAA/AILBgAgAPwDCwYAIAD8BAsGACAA/AULBgAgAPwGCwYAIAD8BwsFACAAsgsFACAAtAsFACAAtwsFACAAuQsFACAAswsFACAAtQsFACAAuAsFACAAugsFACAAuwsFACAAtgsFACAAvgsFACAAvwsFACAAvAsFACAAvQs= +invalid invalid conversions.wast:678 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAQwAAAACnCw== +invalid invalid conversions.wast:679 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgCoCw== +invalid invalid conversions.wast:680 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgCpCw== +invalid invalid conversions.wast:681 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgCqCw== +invalid invalid conversions.wast:682 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgCrCw== +invalid invalid conversions.wast:683 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgC8Cw== +invalid invalid conversions.wast:684 AGFzbQEAAAABBQFgAAF+AwIBAAoKAQgAQwAAAACsCw== +invalid invalid conversions.wast:685 AGFzbQEAAAABBQFgAAF+AwIBAAoKAQgAQwAAAACtCw== +invalid invalid conversions.wast:686 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQCuCw== +invalid invalid conversions.wast:687 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQCvCw== +invalid invalid conversions.wast:688 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQCwCw== +invalid invalid conversions.wast:689 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQCxCw== +invalid invalid conversions.wast:690 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQC9Cw== +invalid invalid conversions.wast:691 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCyCw== +invalid invalid conversions.wast:692 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCzCw== +invalid invalid conversions.wast:693 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQQC0Cw== +invalid invalid conversions.wast:694 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQQC1Cw== +invalid invalid conversions.wast:695 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQQC2Cw== +invalid invalid conversions.wast:696 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgC+Cw== +invalid invalid conversions.wast:697 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgC3Cw== +invalid invalid conversions.wast:698 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgC4Cw== +invalid invalid conversions.wast:699 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQQC5Cw== +invalid invalid conversions.wast:700 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQQC6Cw== +invalid invalid conversions.wast:701 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQQC7Cw== +invalid invalid conversions.wast:702 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQQC/Cw== +valid valid custom.wast:1 AGFzbQEAAAAAJBBhIGN1c3RvbSBzZWN0aW9udGhpcyBpcyB0aGUgcGF5bG9hZAAgEGEgY3VzdG9tIHNlY3Rpb250aGlzIGlzIHBheWxvYWQAERBhIGN1c3RvbSBzZWN0aW9uABAAdGhpcyBpcyBwYXlsb2FkAAEAACQQAABjdXN0b20gc2VjdGlvAHRoaXMgaXMgdGhlIHBheWxvYWQAJBDvu79hIGN1c3RvbSBzZWN0dGhpcyBpcyB0aGUgcGF5bG9hZAAkEGEgY3VzdG9tIHNlY3TijKN0aGlzIGlzIHRoZSBwYXlsb2FkAB8WbW9kdWxlIHdpdGhpbiBhIG1vZHVsZQBhc20BAAAA +valid valid custom.wast:14 AGFzbQEAAAAADgZjdXN0b21wYXlsb2FkAA4GY3VzdG9tcGF5bG9hZAEBAAAOBmN1c3RvbXBheWxvYWQADgZjdXN0b21wYXlsb2FkAgEAAA4GY3VzdG9tcGF5bG9hZAAOBmN1c3RvbXBheWxvYWQDAQAADgZjdXN0b21wYXlsb2FkAA4GY3VzdG9tcGF5bG9hZAQBAAAOBmN1c3RvbXBheWxvYWQADgZjdXN0b21wYXlsb2FkBQEAAA4GY3VzdG9tcGF5bG9hZAAOBmN1c3RvbXBheWxvYWQGAQAADgZjdXN0b21wYXlsb2FkAA4GY3VzdG9tcGF5bG9hZAcBAAAOBmN1c3RvbXBheWxvYWQADgZjdXN0b21wYXlsb2FkCQEAAA4GY3VzdG9tcGF5bG9hZAAOBmN1c3RvbXBheWxvYWQKAQAADgZjdXN0b21wYXlsb2FkAA4GY3VzdG9tcGF5bG9hZAsBAAAOBmN1c3RvbXBheWxvYWQADgZjdXN0b21wYXlsb2Fk +valid valid custom.wast:50 AGFzbQEAAAABBwFgAn9/AX8AGgZjdXN0b210aGlzIGlzIHRoZSBwYXlsb2FkAwIBAAcKAQZhZGRUd28AAAoJAQcAIAAgAWoLABsHY3VzdG9tMnRoaXMgaXMgdGhlIHBheWxvYWQ= +malformed invalid custom.wast:61 AGFzbQEAAAAA +malformed invalid custom.wast:69 AGFzbQEAAAAAAA== +malformed invalid custom.wast:77 AGFzbQEAAAAAAAAFAQAHAAA= +malformed invalid custom.wast:85 AGFzbQEAAAAAJhBhIGN1c3RvbSBzZWN0aW9udGhpcyBpcyB0aGUgcGF5bG9hZA== +malformed invalid custom.wast:93 AGFzbQEAAAAAJRBhIGN1c3RvbSBzZWN0aW9udGhpcyBpcyB0aGUgcGF5bG9hZAAkEGEgY3VzdG9tIHNlY3Rpb250aGlzIGlzIHRoZSBwYXlsb2Fk +malformed invalid custom.wast:102 AGFzbQEAAAABBwFgAn9/AX8AJRBhIGN1c3RvbSBzZWN0aW9udGhpcyBpcyB0aGUgcGF5bG9hZAMCAQAKCQEHACAAIAFqCwAbB2N1c3RvbTJ0aGlzIGlzIHRoZSBwYXlsb2Fk +malformed invalid custom.wast:115 AGFzbQEAAAAAYXNtAQAAAA== +malformed invalid custom.wast:123 AGFzbQEAAAAFAwEAAQwBAgsGAQBBAAsA +valid valid data.wast:5 AGFzbQEAAAAFAwEAAQuzARwAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMBAAEEYWJjZABBAAsAAEEBCwRhYmNkAEEACwAAQQALA2FiYwBBAAsAAEEBCwRhYmNkAEEACwAAQQALA2FiYwBBAAsAAEEBCwRhYmNkAEEACwAAQQALA2FiYwEAAQRhYmNk +valid valid data.wast:39 AGFzbQEAAAAFAwEAAQsHAQBBAAsBYQ== +valid valid data.wast:43 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgABCwcBAEEACwFh +valid valid data.wast:48 AGFzbQEAAAAFAwEAAQsiBQBBAAsBYQBBAwsBYgBB5AALA2NkZQBBBQsBeABBAwsBYw== +valid valid data.wast:56 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgABCycGAEEACwFhAEEBCwFiAEECCwNjZGUAQQMLAWYAQQILAWcAQQELAWg= +valid valid data.wast:66 AGFzbQEAAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAUDAQABCwcBACMACwFh +valid valid data.wast:71 AGFzbQEAAAACKwIIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAhzcGVjdGVzdAZtZW1vcnkCAAELBwEAIwALAWE= +invalid valid data.wast:89 AGFzbQEAAAAFAwEAAQYGAX8AQQALCwcBACMACwFh +valid valid data.wast:100 AGFzbQEAAAAFAwEAAQsPAgBBAAsBYQBB//8DCwFi +valid valid data.wast:105 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgABCw8CAEEACwFhAEH//wMLAWI= +valid valid data.wast:111 AGFzbQEAAAAFAwEAAgsJAQBB//8HCwFh +valid valid data.wast:116 AGFzbQEAAAAFAwEAAAsGAQBBAAsA +valid valid data.wast:120 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgAACwYBAEEACwA= +valid valid data.wast:125 AGFzbQEAAAAFBAEBAAALBgEAQQALAA== +valid valid data.wast:130 AGFzbQEAAAAFAwEAAQsIAQBBgIAECwA= +valid valid data.wast:149 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgAACwcBAEEACwFh +valid valid data.wast:154 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEAAwsHAQBBAAsBYQ== +valid valid data.wast:159 AGFzbQEAAAACKwIIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAhzcGVjdGVzdAZtZW1vcnkCAAALBwEAIwALAWE= +valid valid data.wast:165 AGFzbQEAAAACLAIIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAhzcGVjdGVzdAZtZW1vcnkCAQADCwcBACMACwFh +valid valid data.wast:171 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgAACwcBAEEBCwFh +valid valid data.wast:176 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEAAwsHAQBBAQsBYQ== +valid valid data.wast:184 AGFzbQEAAAAFAwEAAAsHAQBBAAsBYQ== +valid valid data.wast:192 AGFzbQEAAAAFBAEBAAALBwEAQQALAWE= +valid valid data.wast:200 AGFzbQEAAAAFBAEBAAELBwEAQQALAWE= +valid valid data.wast:207 AGFzbQEAAAAFAwEAAAsGAQBBAQsA +valid valid data.wast:214 AGFzbQEAAAAFBAEBAAELBgEAQQELAA== +valid valid data.wast:231 AGFzbQEAAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAUDAQAACwcBACMACwFh +valid valid data.wast:240 AGFzbQEAAAAFBAEBAQILCQEAQYCABAsBYQ== +valid valid data.wast:247 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgABCwkBAEGAgAQLAWE= +valid valid data.wast:255 AGFzbQEAAAAFAwEAAgsJAQBBgIAICwFh +valid valid data.wast:263 AGFzbQEAAAAFBAEBAgMLCQEAQYCACAsBYQ== +valid valid data.wast:271 AGFzbQEAAAAFAwEAAQsHAQBBfwsBYQ== +valid valid data.wast:278 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgABCwcBAEF/CwFh +valid valid data.wast:286 AGFzbQEAAAAFAwEAAgsIAQBBnH8LAWE= +valid valid data.wast:293 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgABCwgBAEGcfwsBYQ== +invalid invalid data.wast:303 AGFzbQEAAAALBgEAQQALAA== +invalid invalid data.wast:311 AGFzbQEAAAAFAwEAAAsHAQIBQQALAA== +invalid invalid data.wast:335 AGFzbQEAAAALBwECAUEACwA= +invalid invalid data.wast:347 AGFzbQEAAAAFAwEAAAtFAQIBQQALPgABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9 +invalid invalid data.wast:369 AGFzbQEAAAALRQECAUEACz4AAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PQ== +invalid invalid data.wast:388 AGFzbQEAAAAFAwEAAQsGAQBCAAsA +invalid invalid data.wast:396 AGFzbQEAAAAFAwEAAQsGAQDQcAsA +invalid invalid data.wast:404 AGFzbQEAAAAFAwEAAQsEAQALAA== +invalid invalid data.wast:412 AGFzbQEAAAAFAwEAAQsIAQBBAEEACwA= +invalid invalid data.wast:420 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABQMBAAELCAEAIwAjAAsA +invalid invalid data.wast:429 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABQMBAAELCAEAIwBBAAsA +invalid invalid data.wast:438 AGFzbQEAAAAFAwEAAQsHAQBBAGgLAA== +invalid invalid data.wast:446 AGFzbQEAAAAFAwEAAQsFAQABCwA= +invalid invalid data.wast:454 AGFzbQEAAAAFAwEAAQsHAQABQQALAA== +invalid invalid data.wast:462 AGFzbQEAAAAFAwEAAQsHAQBBAAELAA== +invalid invalid data.wast:470 AGFzbQEAAAACCwEEdGVzdAFnA38BBQMBAAELBgEAIwALAA== +invalid invalid data.wast:479 AGFzbQEAAAAFAwEAAQsGAQAjAAsA +invalid invalid data.wast:487 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABQMBAAELBgEAIwELAA== +invalid invalid data.wast:496 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWkzMgN/AQUDAQABCwYBACMACwA= +valid valid elem.wast:4 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgmkAz0BAAAFcATSAAvSAAvQcAvSAQsBAAABAAQAAAEBAQAABXAE0gAL0gAL0HAL0gELAQAAAQAEAAABAQBBAAsABEEACwLSAAvQcAsAQQALAABBAAsCAAEAQQALAABBAAsCAAEAQQALAABBAAsCAAAAQQALAABBAAsCAAAAQQALAABBAAsCAAAAQQALAABBAAsCAAAAQQALAARBAAsC0gAL0HALAEEACwIAAABBAAsCAAAAQQALAARBAAsC0gAL0HALAEEACwIAAABBAAsCAAAEQQALAtIAC9BwCwBBAAsABEEACwLSAAvQcAsAQQALAABBAAsCAAEAQQALAABBAAsCAAEAQQALAABBAAsCAAAAQQALAABBAAsCAAAAQQALAABBAAsCAAAAQQALAABBAAsCAAAAQQALAARBAAsC0gAL0HALAEEACwIAAABBAAsCAAAAQQALAARBAAsC0gAL0HALAEEACwIAAABBAAsCAAADAAAHcATSAAvSAAvQcAvSAQsDAAADAAQAAAEBAwAAB3AE0gAL0gAL0HAL0gELAwAAAwAEAAABAQoHAgIACwIACw== +valid valid elem.wast:80 AGFzbQEAAAABBAFgAAADAwIAAAQFAXABAwMJDwEEQQALA9IAC9BwC9IBCwoHAgIACwIACw== +valid valid elem.wast:90 AGFzbQEAAAABBAFgAAADAgEABAQBcAAKCQcBAEEACwEACgQBAgAL +valid valid elem.wast:95 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAKAwIBAAkHAQBBAAsBAAoEAQIACw== +valid valid elem.wast:101 AGFzbQEAAAABBAFgAAADAgEABAQBcAAKCR8FAEEACwEAAEEDCwEAAEEHCwEAAEEFCwEAAEEDCwEACgQBAgAL +valid valid elem.wast:110 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAKAwIBAAkfBQBBCQsBAABBAwsBAABBBwsBAABBAwsBAABBBQsBAAoEAQIACw== +valid valid elem.wast:120 AGFzbQEAAAABBAFgAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAMCAQAEBQFwAOgHCQcBACMACwEACgQBAgAL +valid valid elem.wast:134 AGFzbQEAAAABBQFgAAF/AwUEAAAAAAQEAXAACgcTAgZjYWxsLTcAAgZjYWxsLTkAAwkNAgBBBwsBAABBCQsBAQodBAUAQcEACwUAQcIACwcAQQcRAAALBwBBCREAAAs= +valid valid elem.wast:153 AGFzbQEAAAABBQFgAAF/AwUEAAAAAAQEAXAACwcTAgZjYWxsLTcAAgZjYWxsLTkAAwkXAgRBBgsC0HAL0gALBEEJCwLSAQvQcAsKHQQFAEHBAAsFAEHCAAsHAEEHEQAACwcAQQkRAAAL +invalid valid elem.wast:171 AGFzbQEAAAABBAFgAAADAgEABAQBcAABBgYBfwBBAAsJBwEAIwALAQAKBAECAAs= +valid valid elem.wast:182 AGFzbQEAAAABBAFgAAADAgEABAQBcAAKCQcBAEEJCwEACgQBAgAL +valid valid elem.wast:187 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAKAwIBAAkHAQBBCQsBAAoEAQIACw== +valid valid elem.wast:193 AGFzbQEAAAAEBAFwAAAJBgEAQQALAA== +valid valid elem.wast:197 AGFzbQEAAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAACQYBAEEACwA= +valid valid elem.wast:202 AGFzbQEAAAAEBQFwAQAACQYBAEEACwA= +valid valid elem.wast:207 AGFzbQEAAAAEBAFwABQJBgEAQRQLAA== +valid valid elem.wast:212 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAAAwIBAAkHAQBBAAsBAAoEAQIACw== +valid valid elem.wast:218 AGFzbQEAAAABBAFgAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEAZAMCAQAJBwEAQQALAQAKBAECAAs= +valid valid elem.wast:224 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAAAwIBAAkHAQBBAQsBAAoEAQIACw== +valid valid elem.wast:230 AGFzbQEAAAABBAFgAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEAHgMCAQAJBwEAQQELAQAKBAECAAs= +valid valid elem.wast:239 AGFzbQEAAAABBAFgAAADAgEABAQBcAAACQcBAEEACwEACgQBAgAL +valid valid elem.wast:248 AGFzbQEAAAABBAFgAAADAgEABAUBcAEAAAkHAQBBAAsBAAoEAQIACw== +valid valid elem.wast:257 AGFzbQEAAAABBAFgAAADAgEABAUBcAEAAQkHAQBBAAsBAAoEAQIACw== +valid valid elem.wast:266 AGFzbQEAAAAEBAFwAAAJBgEAQQELAA== +valid valid elem.wast:273 AGFzbQEAAAABBAFgAAADAgEABAQBcAAKCQcBAEEKCwEACgQBAgAL +valid valid elem.wast:281 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAKAwIBAAkHAQBBCgsBAAoEAQIACw== +valid valid elem.wast:290 AGFzbQEAAAABBAFgAAADAgEABAUBcAEKFAkHAQBBCgsBAAoEAQIACw== +valid valid elem.wast:307 AGFzbQEAAAABBAFgAAADAgEABAQBcAAKCQcBAEF/CwEACgQBAgAL +valid valid elem.wast:315 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAKAwIBAAkHAQBBfwsBAAoEAQIACw== +valid valid elem.wast:324 AGFzbQEAAAABBAFgAAADAgEABAQBcAAKCQcBAEF2CwEACgQBAgAL +valid valid elem.wast:332 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAKAwIBAAkHAQBBdgsBAAoEAQIACw== +valid valid elem.wast:342 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQRpbml0AAEJBwEAQQALAQAKEQICAAsMAEEAQQBBAfwMAAAL +valid valid elem.wast:352 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQRpbml0AAEJBQEDAAEAChECAgALDABBAEEAQQH8DAAACw== +invalid invalid elem.wast:365 AGFzbQEAAAABBAFgAAADAgEACQcBAEEACwEACgQBAgAL +invalid invalid elem.wast:375 AGFzbQEAAAAEBAFwAAEJBgEAQgALAA== +invalid invalid elem.wast:383 AGFzbQEAAAAEBAFwAAEJBgEA0HALAA== +invalid invalid elem.wast:391 AGFzbQEAAAAEBAFwAAEJBAEACwA= +invalid invalid elem.wast:399 AGFzbQEAAAAEBAFwAAEJCAEAQQBBAAsA +invalid invalid elem.wast:407 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABAQBcAABCQgBACMAIwALAA== +invalid invalid elem.wast:416 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABAQBcAABCQgBACMAQQALAA== +invalid invalid elem.wast:426 AGFzbQEAAAAEBAFwAAEJBwEAQQBoCwA= +invalid invalid elem.wast:434 AGFzbQEAAAAEBAFwAAEJBQEAAQsA +invalid invalid elem.wast:442 AGFzbQEAAAAEBAFwAAEJBwEAAUEACwA= +invalid invalid elem.wast:450 AGFzbQEAAAAEBAFwAAEJBwEAQQABCwA= +invalid invalid elem.wast:458 AGFzbQEAAAACCwEEdGVzdAFnA38BBAQBcAABCQYBACMACwA= +invalid invalid elem.wast:467 AGFzbQEAAAAEBAFwAAEJBgEAIwALAA== +invalid invalid elem.wast:475 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABAQBcAABCQYBACMBCwA= +invalid invalid elem.wast:484 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWkzMgN/AQQEAXAAAQkGAQAjAAsA +invalid invalid elem.wast:495 AGFzbQEAAAAEBAFwAAEJCQEEQQALAdBvCw== +invalid invalid elem.wast:503 AGFzbQEAAAAEBAFwAAEJCwEEQQALAdBw0HAL +invalid invalid elem.wast:511 AGFzbQEAAAAEBAFwAAEJCQEEQQALAUEACw== +invalid invalid elem.wast:527 AGFzbQEAAAABBQFgAAFwAwIBAAQEAXAAAQkJAQRBAAsBEAALCgYBBADQcAs= +valid valid elem.wast:537 AGFzbQEAAAABBQFgAAF/AwQDAAAABAQBcAAKBxQBEGNhbGwtb3ZlcndyaXR0ZW4AAgkNAgBBCQsBAABBCQsBAQoVAwUAQcEACwUAQcIACwcAQQkRAAAL +valid valid elem.wast:550 AGFzbQEAAAABBQFgAAF/AhQBCHNwZWN0ZXN0BXRhYmxlAXAACgMEAwAAAAccARhjYWxsLW92ZXJ3cml0dGVuLWVsZW1lbnQAAgkNAgBBCQsBAABBCQsBAQoVAwUAQcEACwUAQcIACwcAQQkRAAAL +valid valid elem.wast:565 AGFzbQEAAAABBQFgAAF/AwYFAAAAAAAEBAFwAAoHKwQMc2hhcmVkLXRhYmxlAQAGY2FsbC03AAIGY2FsbC04AAMGY2FsbC05AAQJDQIAQQgLAQAAQQkLAQEKJQUFAEHBAAsFAEHCAAsHAEEHEQAACwcAQQgRAAALBwBBCREAAAs= +valid valid elem.wast:589 AGFzbQEAAAABBQFgAAF/AhoBB21vZHVsZTEMc2hhcmVkLXRhYmxlAXAACgMDAgAACQ0CAEEHCwEAAEEICwEBCg0CBQBBwwALBQBBxAAL +valid valid elem.wast:602 AGFzbQEAAAABBQFgAAF/AhoBB21vZHVsZTEMc2hhcmVkLXRhYmxlAXAACgMDAgAACQ0CAEEICwEAAEEJCwEBCg0CBQBBxQALBQBBxgAL +invalid invalid elem.wast:618 AGFzbQEAAAABBAFgAAADAgEABAQBbwABCQcBAEEACwEACgQBAgAL +invalid invalid elem.wast:623 AGFzbQEAAAAEBAFwAAEJCwEGAEEAC28B0G8L +invalid invalid elem.wast:628 AGFzbQEAAAABBAFgAAADAwIAAAQEAW8AAQkFAQEAAQAKEQICAAsMAEEAQQBBAfwMAAAL +invalid invalid elem.wast:637 AGFzbQEAAAABBAFgAAADAgEABAQBcAABCQcBBW8B0G8LCg4BDABBAEEAQQH8DAAACw== +valid valid elem.wast:646 AGFzbQEAAAABCwJgAX8Bb2ACf28AAwMCAAEEBAFvAAIHFQMFdGFibGUBAANnZXQAAANzZXQAAQoRAgYAIAAlAAsIACAAIAEmAAs= +valid valid elem.wast:664 AGFzbQEAAAACFAEIZXhwb3J0ZXIFdGFibGUBbwACCQsBBgBBAAtvAdBvCw== +valid valid elem.wast:673 AGFzbQEAAAABBQFgAAF/AwIBAAYGAXAA0gALBwUBAWYDAAoGAQQAQSoL +valid valid elem.wast:682 AGFzbQEAAAABBQFgAAF/Ag4BB21vZHVsZTQBZgNwAAMCAQAEBAFwAAoHFgESY2FsbF9pbXBvcnRlZF9lbGVtAAAJCQEEQQALASMACwoJAQcAQQARAAAL +valid valid endianness.wast:1 AGFzbQEAAAABJAdgAn9/AGACf34AYAF/AX9gAX8BfmABfgF+YAF9AX1gAXwBfAMYFwAAAQICAwICAgQEBAQEBQYCAgQEBAUGBQMBAAEH4QERDGkzMl9sb2FkMTZfcwAGDGkzMl9sb2FkMTZfdQAHCGkzMl9sb2FkAAgMaTY0X2xvYWQxNl9zAAkMaTY0X2xvYWQxNl91AAoMaTY0X2xvYWQzMl9zAAsMaTY0X2xvYWQzMl91AAwIaTY0X2xvYWQADQhmMzJfbG9hZAAOCGY2NF9sb2FkAA8LaTMyX3N0b3JlMTYAEAlpMzJfc3RvcmUAEQtpNjRfc3RvcmUxNgASC2k2NF9zdG9yZTMyABMJaTY0X3N0b3JlABQJZjMyX3N0b3JlABUJZjY0X3N0b3JlABYK9gIXFgAgACABOgAAIABBAWogAUEIdjoAAAsUACAAIAEQACAAQQJqIAFBEHYQAAsWACAAIAGnEAEgAEEEaiABQiCIpxABCxMAIAAtAAAgAEEBai0AAEEIdHILEQAgABADIABBAmoQA0EQdHILEwAgABAErSAAQQRqEAStQiCGhAsNAEEAIAAQAEEALgEACw0AQQAgABAAQQAvAQALDQBBACAAEAFBACgCAAsOAEEAIACnEABBADIBAAsOAEEAIACnEABBADMBAAsOAEEAIACnEAFBADQCAAsOAEEAIACnEAFBADUCAAsNAEEAIAAQAkEAKQMACw4AQQAgALwQAUEAKgIACw4AQQAgAL0QAkEAKwMACw0AQQAgADsBAEEAEAMLDQBBACAANgIAQQAQBAsOAEEAIAA9AQBBABADrQsOAEEAIAA+AgBBABAErQsNAEEAIAA3AwBBABAFCw4AQQAgADgCAEEAEAS+Cw4AQQAgADkDAEEAEAW/Cw== +valid valid exports.wast:3 AGFzbQEAAAABBAFgAAADAgEABwUBAWEAAAoEAQIACw== +valid valid exports.wast:4 AGFzbQEAAAABBAFgAAADAgEABwkCAWEAAAFiAAAKBAECAAs= +valid valid exports.wast:5 AGFzbQEAAAABBAFgAAADAwIAAAcJAgFhAAABYgABCgcCAgALAgAL +valid valid exports.wast:8 AGFzbQEAAAABBAFgAAADAgEABw0DAWEAAAFiAAABYwAACgQBAgAL +valid valid exports.wast:9 AGFzbQEAAAABBQFgAX8AAwIBAAcJAgFhAAABYgAACgQBAgAL +valid valid exports.wast:16 AGFzbQEAAAABBgFgAX8BfwMCAQAHBQEBZQAACgoBCAAgAEEBag8L +valid valid exports.wast:28 AGFzbQEAAAABBQFgAAF/AwIBAAcNAwFhAAABYgAAAWMAAAoGAQQAQSoL +invalid invalid exports.wast:39 AGFzbQEAAAAHBQEBYQAA +invalid invalid exports.wast:43 AGFzbQEAAAABBAFgAAADAgEABwUBAWEAAQoEAQIACw== +invalid invalid exports.wast:47 AGFzbQEAAAABBQFgAX8AAhYBCHNwZWN0ZXN0CXByaW50X2kzMgAABwUBAWEAAQ== +invalid invalid exports.wast:51 AGFzbQEAAAABBAFgAAADAgEABwkCAWEAAAFhAAAKBAECAAs= +invalid invalid exports.wast:55 AGFzbQEAAAABBAFgAAADAwIAAAcJAgFhAAABYQABCgcCAgALAgAL +invalid invalid exports.wast:59 AGFzbQEAAAABBAFgAAADAgEABgYBfwBBAAsHCQIBYQAAAWEDAAoEAQIACw== +invalid invalid exports.wast:63 AGFzbQEAAAABBAFgAAADAgEABAQBcAAABwkCAWEAAAFhAQAKBAECAAs= +invalid invalid exports.wast:67 AGFzbQEAAAABBAFgAAADAgEABQMBAAAHCQIBYQAAAWECAAoEAQIACw== +valid valid exports.wast:74 AGFzbQEAAAAGBgF/AEEACwcFAQFhAwA= +valid valid exports.wast:75 AGFzbQEAAAAGBgF/AEEACwcJAgFhAwABYgMA +valid valid exports.wast:76 AGFzbQEAAAAGCwJ/AEEAC38AQQALBwkCAWEDAAFiAwE= +valid valid exports.wast:85 AGFzbQEAAAAGBgF/AEEqCwcFAQFlAwA= +invalid invalid exports.wast:96 AGFzbQEAAAAHBQEBYQMA +invalid invalid exports.wast:100 AGFzbQEAAAAGBgF/AEEACwcFAQFhAwE= +invalid invalid exports.wast:104 AGFzbQEAAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAcFAQFhAwE= +invalid invalid exports.wast:108 AGFzbQEAAAAGBgF/AEEACwcJAgFhAwABYQMA +invalid invalid exports.wast:112 AGFzbQEAAAAGCwJ/AEEAC38AQQALBwkCAWEDAAFhAwE= +invalid invalid exports.wast:116 AGFzbQEAAAABBAFgAAADAgEABgYBfwBBAAsHCQIBYQMAAWEAAAoEAQIACw== +invalid invalid exports.wast:120 AGFzbQEAAAAEBAFwAAAGBgF/AEEACwcJAgFhAwABYQEA +invalid invalid exports.wast:124 AGFzbQEAAAAFAwEAAAYGAX8AQQALBwkCAWEDAAFhAgA= +valid valid exports.wast:131 AGFzbQEAAAAEBAFwAAAHBQEBYQEA +valid valid exports.wast:132 AGFzbQEAAAAEBAFwAAAHCQIBYQEAAWIBAA== +valid valid exports.wast:133 AGFzbQEAAAAEBwJwAABwAAAHCQIBYQEAAWIBAQ== +valid valid exports.wast:136 AGFzbQEAAAAEBQFwAQABBwUBAWEBAA== +invalid invalid exports.wast:151 AGFzbQEAAAAHBQEBYQEA +invalid invalid exports.wast:155 AGFzbQEAAAAEBAFwAAAHBQEBYQEB +invalid invalid exports.wast:159 AGFzbQEAAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEKFAcFAQFhAQE= +invalid invalid exports.wast:163 AGFzbQEAAAAEBAFwAAAHCQIBYQEAAWEBAA== +invalid invalid exports.wast:167 AGFzbQEAAAAEBwJwAABwAAAHCQIBYQEAAWEBAQ== +invalid invalid exports.wast:171 AGFzbQEAAAABBAFgAAADAgEABAQBcAAABwkCAWEBAAFhAAAKBAECAAs= +invalid invalid exports.wast:175 AGFzbQEAAAAEBAFwAAAGBgF/AEEACwcJAgFhAQABYQMA +invalid invalid exports.wast:179 AGFzbQEAAAAEBAFwAAAFAwEAAAcJAgFhAQABYQIA +valid valid exports.wast:186 AGFzbQEAAAAFAwEAAAcFAQFhAgA= +valid valid exports.wast:187 AGFzbQEAAAAFAwEAAAcJAgFhAgABYgIA +valid valid exports.wast:192 AGFzbQEAAAAFBAEBAAEHBQEBYQIA +invalid invalid exports.wast:207 AGFzbQEAAAAHBQEBYQIA +invalid invalid exports.wast:211 AGFzbQEAAAAFAwEAAAcFAQFhAgE= +invalid invalid exports.wast:215 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEBAgcFAQFhAgE= +invalid invalid exports.wast:219 AGFzbQEAAAAFAwEAAAcJAgFhAgABYQIA +invalid invalid exports.wast:228 AGFzbQEAAAABBAFgAAADAgEABQMBAAAHCQIBYQIAAWEAAAoEAQIACw== +invalid invalid exports.wast:232 AGFzbQEAAAAFAwEAAAYGAX8AQQALBwkCAWECAAFhAwA= +invalid invalid exports.wast:236 AGFzbQEAAAAEBAFwAAAFAwEAAAcJAgFhAgABYQEA +valid valid f32.wast:5 AGFzbQEAAAABDAJgAn19AX1gAX0BfQMMCwAAAAABAAABAQEBB00LA2FkZAAAA3N1YgABA211bAACA2RpdgADBHNxcnQABANtaW4ABQNtYXgABgRjZWlsAAcFZmxvb3IACAV0cnVuYwAJB25lYXJlc3QACgpPCwcAIAAgAZILBwAgACABkwsHACAAIAGUCwcAIAAgAZULBQAgAJELBwAgACABlgsHACAAIAGXCwUAIACNCwUAIACOCwUAIACPCwUAIACQCw== +invalid invalid f32.wast:2523 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAACSCw== +invalid invalid f32.wast:2524 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAACVCw== +invalid invalid f32.wast:2525 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAACXCw== +invalid invalid f32.wast:2526 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAACWCw== +invalid invalid f32.wast:2527 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAACUCw== +invalid invalid f32.wast:2528 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAACTCw== +invalid invalid f32.wast:2529 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCNCw== +invalid invalid f32.wast:2530 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCOCw== +invalid invalid f32.wast:2531 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCQCw== +invalid invalid f32.wast:2532 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCRCw== +invalid invalid f32.wast:2533 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCPCw== +valid valid f32_bitwise.wast:4 AGFzbQEAAAABDAJgAX0BfWACfX0BfQMEAwAAAQcYAwNhYnMAAANuZWcAAQhjb3B5c2lnbgACChUDBQAgAIsLBQAgAIwLBwAgACABmAs= +invalid invalid f32_bitwise.wast:374 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAACYCw== +invalid invalid f32_bitwise.wast:375 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCLCw== +invalid invalid f32_bitwise.wast:376 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAQgCMCw== +valid valid f32_cmp.wast:4 AGFzbQEAAAABBwFgAn19AX8DBwYAAAAAAAAHHwYCZXEAAAJuZQABAmx0AAICbGUAAwJndAAEAmdlAAUKMQYHACAAIAFbCwcAIAAgAVwLBwAgACABXQsHACAAIAFfCwcAIAAgAV4LBwAgACABYAs= +invalid invalid f32_cmp.wast:2417 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAABbCw== +invalid invalid f32_cmp.wast:2418 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAABgCw== +invalid invalid f32_cmp.wast:2419 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAABeCw== +invalid invalid f32_cmp.wast:2420 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAABfCw== +invalid invalid f32_cmp.wast:2421 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAABdCw== +invalid invalid f32_cmp.wast:2422 AGFzbQEAAAABBQFgAAF9AwIBAAoQAQ4AQgBEAAAAAAAAAABcCw== +valid valid f64.wast:5 AGFzbQEAAAABDAJgAnx8AXxgAXwBfAMMCwAAAAABAAABAQEBB00LA2FkZAAAA3N1YgABA211bAACA2RpdgADBHNxcnQABANtaW4ABQNtYXgABgRjZWlsAAcFZmxvb3IACAV0cnVuYwAJB25lYXJlc3QACgpPCwcAIAAgAaALBwAgACABoQsHACAAIAGiCwcAIAAgAaMLBQAgAJ8LBwAgACABpAsHACAAIAGlCwUAIACbCwUAIACcCwUAIACdCwUAIACeCw== +invalid invalid f64.wast:2523 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAKAL +invalid invalid f64.wast:2524 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAKML +invalid invalid f64.wast:2525 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAKUL +invalid invalid f64.wast:2526 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAKQL +invalid invalid f64.wast:2527 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAKIL +invalid invalid f64.wast:2528 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAKEL +invalid invalid f64.wast:2529 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgCbCw== +invalid invalid f64.wast:2530 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgCcCw== +invalid invalid f64.wast:2531 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgCeCw== +invalid invalid f64.wast:2532 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgCfCw== +invalid invalid f64.wast:2533 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgCdCw== +valid valid f64_bitwise.wast:4 AGFzbQEAAAABDAJgAXwBfGACfHwBfAMEAwAAAQcYAwNhYnMAAANuZWcAAQhjb3B5c2lnbgACChUDBQAgAJkLBQAgAJoLBwAgACABpgs= +invalid invalid f64_bitwise.wast:374 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAKYL +invalid invalid f64_bitwise.wast:375 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgCZCw== +invalid invalid f64_bitwise.wast:376 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgCaCw== +valid valid f64_cmp.wast:4 AGFzbQEAAAABBwFgAnx8AX8DBwYAAAAAAAAHHwYCZXEAAAJuZQABAmx0AAICbGUAAwJndAAEAmdlAAUKMQYHACAAIAFhCwcAIAAgAWILBwAgACABYwsHACAAIAFlCwcAIAAgAWQLBwAgACABZgs= +invalid invalid f64_cmp.wast:2417 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAGEL +invalid invalid f64_cmp.wast:2418 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAGYL +invalid invalid f64_cmp.wast:2419 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAGQL +invalid invalid f64_cmp.wast:2420 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAGUL +invalid invalid f64_cmp.wast:2421 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAGML +invalid invalid f64_cmp.wast:2422 AGFzbQEAAAABBQFgAAF8AwIBAAoMAQoAQgBDAAAAAGIL +valid valid fac.wast:1 AGFzbQEAAAABGgRgAX4BfmABfgJ+fmACfn4Dfn5+YAJ+fgF+AwkIAAAAAAABAgAHSwYHZmFjLXJlYwAADWZhYy1yZWMtbmFtZWQAAQhmYWMtaXRlcgACDmZhYy1pdGVyLW5hbWVkAAMHZmFjLW9wdAAEB2ZhYy1zc2EABwrrAQgXACAAQgBRBH5CAQUgACAAQgF9EAB+CwsXACAAQgBRBH5CAQUgACAAQgF9EAF+CwsvAQJ+IAAhAUIBIQICQANAIAFCAFEEQAwCBSABIAJ+IQIgAUIBfSEBCwwACwsgAgsvAQJ+IAAhAUIBIQICQANAIAFCAFEEQAwCBSABIAJ+IQIgAUIBfSEBCwwACwsgAgssAQF+QgEhAQJAIABCAlMNAANAIAEgAH4hASAAQn98IQAgAEIBVQ0ACwsgAQsGACAAIAALCAAgACABIAALHABCASAAAwMQBhAGfhAGQgF9EAVCAFYNABoPCws= +valid valid float_exprs.wast:6 AGFzbQEAAAABCAFgA3x8fAF8AwIBAAcWARJmNjQubm9fY29udHJhY3Rpb24AAAoMAQoAIAAgAaIgAqAL +valid valid float_exprs.wast:19 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBxsCCmYzMi5ub19mbWEAAApmNjQubm9fZm1hAAEKFwIKACAAIAGUIAKSCwoAIAAgAaIgAqAL +valid valid float_exprs.wast:40 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfYWRkX3plcm8AABRmNjQubm9fZm9sZF9hZGRfemVybwABChsCCgAgAEMAAAAAkgsOACAARAAAAAAAAAAAoAs= +valid valid float_exprs.wast:54 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfemVyb19zdWIAABRmNjQubm9fZm9sZF96ZXJvX3N1YgABChsCCgBDAAAAACAAkwsOAEQAAAAAAAAAACAAoQs= +valid valid float_exprs.wast:68 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfc3ViX3plcm8AABRmNjQubm9fZm9sZF9zdWJfemVybwABChsCCgAgAEMAAAAAkwsOACAARAAAAAAAAAAAoQs= +valid valid float_exprs.wast:80 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfbXVsX3plcm8AABRmNjQubm9fZm9sZF9tdWxfemVybwABChsCCgAgAEMAAAAAlAsOACAARAAAAAAAAAAAogs= +valid valid float_exprs.wast:99 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLQITZjMyLm5vX2ZvbGRfbXVsX29uZQAAE2Y2NC5ub19mb2xkX211bF9vbmUAAQobAgoAIABDAACAP5QLDgAgAEQAAAAAAADwP6IL +valid valid float_exprs.wast:111 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfemVyb19kaXYAABRmNjQubm9fZm9sZF96ZXJvX2RpdgABChsCCgBDAAAAACAAlQsOAEQAAAAAAAAAACAAows= +valid valid float_exprs.wast:129 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLQITZjMyLm5vX2ZvbGRfZGl2X29uZQAAE2Y2NC5ub19mb2xkX2Rpdl9vbmUAAQobAgoAIABDAACAP5ULDgAgAEQAAAAAAADwP6ML +valid valid float_exprs.wast:141 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfZGl2X25lZzEAABRmNjQubm9fZm9sZF9kaXZfbmVnMQABChsCCgAgAEMAAIC/lQsOACAARAAAAAAAAPC/ows= +valid valid float_exprs.wast:153 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfbmVnMF9zdWIAABRmNjQubm9fZm9sZF9uZWcwX3N1YgABChsCCgBDAAAAgCAAkwsOAEQAAAAAAAAAgCAAoQs= +valid valid float_exprs.wast:165 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfbmVnMV9tdWwAABRmNjQubm9fZm9sZF9uZWcxX211bAABChsCCgBDAACAvyAAlAsOAEQAAAAAAADwvyAAogs= +valid valid float_exprs.wast:177 AGFzbQEAAAABCwJgAX0Bf2ABfAF/AwMCAAEHLQITZjMyLm5vX2ZvbGRfZXFfc2VsZgAAE2Y2NC5ub19mb2xkX2VxX3NlbGYAAQoRAgcAIAAgAFsLBwAgACAAYQs= +valid valid float_exprs.wast:189 AGFzbQEAAAABCwJgAX0Bf2ABfAF/AwMCAAEHLQITZjMyLm5vX2ZvbGRfbmVfc2VsZgAAE2Y2NC5ub19mb2xkX25lX3NlbGYAAQoRAgcAIAAgAFwLBwAgACAAYgs= +valid valid float_exprs.wast:201 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfc3ViX3NlbGYAABRmNjQubm9fZm9sZF9zdWJfc2VsZgABChECBwAgACAAkwsHACAAIAChCw== +valid valid float_exprs.wast:215 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfZGl2X3NlbGYAABRmNjQubm9fZm9sZF9kaXZfc2VsZgABChECBwAgACAAlQsHACAAIACjCw== +valid valid float_exprs.wast:233 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHKQIRZjMyLm5vX2ZvbGRfZGl2XzMAABFmNjQubm9fZm9sZF9kaXZfMwABChsCCgAgAEMAAEBAlQsOACAARAAAAAAAAAhAows= +valid valid float_exprs.wast:253 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABByECDWYzMi5ub19mYWN0b3IAAA1mNjQubm9fZmFjdG9yAAEKHQINACAAIAKUIAEgApSSCw0AIAAgAqIgASACoqAL +valid valid float_exprs.wast:273 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBykCEWYzMi5ub19kaXN0cmlidXRlAAARZjY0Lm5vX2Rpc3RyaWJ1dGUAAQoXAgoAIAAgAZIgApQLCgAgACABoCACogs= +valid valid float_exprs.wast:293 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBzMCFmYzMi5ub19yZWdyb3VwX2Rpdl9tdWwAABZmNjQubm9fcmVncm91cF9kaXZfbXVsAAEKFwIKACAAIAEgApWUCwoAIAAgASACo6IL +valid valid float_exprs.wast:313 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBzMCFmYzMi5ub19yZWdyb3VwX211bF9kaXYAABZmNjQubm9fcmVncm91cF9tdWxfZGl2AAEKFwIKACAAIAGUIAKVCwoAIAAgAaIgAqML +valid valid float_exprs.wast:333 AGFzbQEAAAABEQJgBH19fX0BfWAEfHx8fAF8AwMCAAEHMwIWZjMyLm5vX3JlYXNzb2NpYXRlX2FkZAAAFmY2NC5ub19yZWFzc29jaWF0ZV9hZGQAAQodAg0AIAAgAZIgApIgA5ILDQAgACABoCACoCADoAs= +valid valid float_exprs.wast:353 AGFzbQEAAAABEQJgBH19fX0BfWAEfHx8fAF8AwMCAAEHMwIWZjMyLm5vX3JlYXNzb2NpYXRlX211bAAAFmY2NC5ub19yZWFzc29jaWF0ZV9tdWwAAQodAg0AIAAgAZQgApQgA5QLDQAgACABoiACoiADogs= +valid valid float_exprs.wast:373 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHKQIRZjMyLm5vX2ZvbGRfZGl2XzAAABFmNjQubm9fZm9sZF9kaXZfMAABChsCCgAgAEMAAAAAlQsOACAARAAAAAAAAAAAows= +valid valid float_exprs.wast:399 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfZGl2X25lZzAAABRmNjQubm9fZm9sZF9kaXZfbmVnMAABChsCCgAgAEMAAACAlQsOACAARAAAAAAAAACAows= +valid valid float_exprs.wast:425 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQcvAhRmMzIubm9fZm9sZF90b19oeXBvdAAAFGY2NC5ub19mb2xkX3RvX2h5cG90AAEKHwIOACAAIACUIAEgAZSSkQsOACAAIACiIAEgAaKgnws= +valid valid float_exprs.wast:447 AGFzbQEAAAABBgFgAX0BfQMCAQAHIQEdZjMyLm5vX2FwcHJveGltYXRlX3JlY2lwcm9jYWwAAAoMAQoAQwAAgD8gAJUL +valid valid float_exprs.wast:460 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHRAIiZjMyLm5vX2FwcHJveGltYXRlX3JlY2lwcm9jYWxfc3FydAAAG2Y2NC5ub19mdXNlX3JlY2lwcm9jYWxfc3FydAABCh0CCwBDAACAPyAAkZULDwBEAAAAAAAA8D8gAJ+jCw== +valid valid float_exprs.wast:481 AGFzbQEAAAABBgFgAX0BfQMCAQAHJgEiZjMyLm5vX2FwcHJveGltYXRlX3NxcnRfcmVjaXByb2NhbAAACg0BCwBDAACAPyAAlZEL +valid valid float_exprs.wast:494 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQdRBBFpMzIubm9fZm9sZF9mMzJfcwAAEWkzMi5ub19mb2xkX2YzMl91AAERaTY0Lm5vX2ZvbGRfZjY0X3MAAhFpNjQubm9fZm9sZF9mNjRfdQADCh0EBgAgALKoCwYAIACzqQsGACAAubALBgAgALqxCw== +valid valid float_exprs.wast:523 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQctAhNmMzIubm9fZm9sZF9hZGRfc3ViAAATZjY0Lm5vX2ZvbGRfYWRkX3N1YgABChcCCgAgACABkiABkwsKACAAIAGgIAGhCw== +valid valid float_exprs.wast:544 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQctAhNmMzIubm9fZm9sZF9zdWJfYWRkAAATZjY0Lm5vX2ZvbGRfc3ViX2FkZAABChcCCgAgACABkyABkgsKACAAIAGhIAGgCw== +valid valid float_exprs.wast:565 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQctAhNmMzIubm9fZm9sZF9tdWxfZGl2AAATZjY0Lm5vX2ZvbGRfbXVsX2RpdgABChcCCgAgACABlCABlQsKACAAIAGiIAGjCw== +valid valid float_exprs.wast:586 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQctAhNmMzIubm9fZm9sZF9kaXZfbXVsAAATZjY0Lm5vX2ZvbGRfZGl2X211bAABChcCCgAgACABlSABlAsKACAAIAGjIAGiCw== +valid valid float_exprs.wast:607 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHMQIVZjMyLm5vX2ZvbGRfZGl2Ml9tdWwyAAAVZjY0Lm5vX2ZvbGRfZGl2Ml9tdWwyAAEKKwIQACAAQwAAAECVQwAAAECUCxgAIABEAAAAAAAAAECjRAAAAAAAAABAogs= +valid valid float_exprs.wast:619 AGFzbQEAAAABBgFgAXwBfAMCAQAHGgEWbm9fZm9sZF9kZW1vdGVfcHJvbW90ZQAACggBBgAgALa7Cw== +valid valid float_exprs.wast:633 AGFzbQEAAAABBgFgAX0BfQMCAQAHGgEWbm9fZm9sZF9wcm9tb3RlX2RlbW90ZQAACggBBgAgALu2Cw== +valid valid float_exprs.wast:655 AGFzbQEAAAABDQJgAnx9AX1gAn18AX0DAwIAAQc2AhNub19kZW1vdGVfbWl4ZWRfYWRkAAAcbm9fZGVtb3RlX21peGVkX2FkZF9jb21tdXRlZAABChUCCQAgACABu6C2CwkAIAC7IAGgtgs= +valid valid float_exprs.wast:677 AGFzbQEAAAABDQJgAnx9AX1gAn18AX0DAwIAAQc2AhNub19kZW1vdGVfbWl4ZWRfc3ViAAAcbm9fZGVtb3RlX21peGVkX3N1Yl9jb21tdXRlZAABChUCCQAgACABu6G2CwkAIAC7IAGhtgs= +valid valid float_exprs.wast:699 AGFzbQEAAAABDQJgAnx9AX1gAn18AX0DAwIAAQc2AhNub19kZW1vdGVfbWl4ZWRfbXVsAAAcbm9fZGVtb3RlX21peGVkX211bF9jb21tdXRlZAABChUCCQAgACABu6K2CwkAIAC7IAGitgs= +valid valid float_exprs.wast:721 AGFzbQEAAAABDQJgAnx9AX1gAn18AX0DAwIAAQc2AhNub19kZW1vdGVfbWl4ZWRfZGl2AAAcbm9fZGVtb3RlX21peGVkX2Rpdl9jb21tdXRlZAABChUCCQAgACABu6O2CwkAIAC7IAGjtgs= +valid valid float_exprs.wast:742 AGFzbQEAAAABCwJgAX0BfWABfAF8AxEQAAAAAAEBAQEAAAAAAQEBAQfBBBAhZjMyLmkzMi5ub19mb2xkX3RydW5jX3NfY29udmVydF9zAAAhZjMyLmkzMi5ub19mb2xkX3RydW5jX3VfY29udmVydF9zAAEhZjMyLmkzMi5ub19mb2xkX3RydW5jX3NfY29udmVydF91AAIhZjMyLmkzMi5ub19mb2xkX3RydW5jX3VfY29udmVydF91AAMhZjY0LmkzMi5ub19mb2xkX3RydW5jX3NfY29udmVydF9zAAQhZjY0LmkzMi5ub19mb2xkX3RydW5jX3VfY29udmVydF9zAAUhZjY0LmkzMi5ub19mb2xkX3RydW5jX3NfY29udmVydF91AAYhZjY0LmkzMi5ub19mb2xkX3RydW5jX3VfY29udmVydF91AAchZjMyLmk2NC5ub19mb2xkX3RydW5jX3NfY29udmVydF9zAAghZjMyLmk2NC5ub19mb2xkX3RydW5jX3VfY29udmVydF9zAAkhZjMyLmk2NC5ub19mb2xkX3RydW5jX3NfY29udmVydF91AAohZjMyLmk2NC5ub19mb2xkX3RydW5jX3VfY29udmVydF91AAshZjY0Lmk2NC5ub19mb2xkX3RydW5jX3NfY29udmVydF9zAAwhZjY0Lmk2NC5ub19mb2xkX3RydW5jX3VfY29udmVydF9zAA0hZjY0Lmk2NC5ub19mb2xkX3RydW5jX3NfY29udmVydF91AA4hZjY0Lmk2NC5ub19mb2xkX3RydW5jX3VfY29udmVydF91AA8KcRAGACAAqLILBgAgAKmyCwYAIACoswsGACAAqbMLBgAgAKq3CwYAIACrtwsGACAAqrgLBgAgAKu4CwYAIACutAsGACAAr7QLBgAgAK61CwYAIACvtQsGACAAsLkLBgAgALG5CwYAIACwugsGACAAsboL +valid valid float_exprs.wast:817 AGFzbQEAAAABCwJgAn99AGABfwF9AwQDAAABBQQBAQEBBxYDBGluaXQAAANydW4AAQVjaGVjawACCjkDCQAgACABOAIACyUBAX8CQANAIAIgAioCACABlTgCACACQQRqIQIgAiAASQ0ACwsLBwAgACoCAAs= +valid valid float_exprs.wast:852 AGFzbQEAAAABCwJgAn98AGABfwF8AwQDAAABBQQBAQEBBxYDBGluaXQAAANydW4AAQVjaGVjawACCjkDCQAgACABOQMACyUBAX8CQANAIAIgAisDACABozkDACACQQhqIQIgAiAASQ0ACwsLBwAgACsDAAs= +valid valid float_exprs.wast:889 AGFzbQEAAAABDQJgAn19AX9gAnx8AX8DCQgAAAAAAQEBAQdRCAdmMzIudWx0AAAHZjMyLnVsZQABB2YzMi51Z3QAAgdmMzIudWdlAAMHZjY0LnVsdAAEB2Y2NC51bGUABQdmNjQudWd0AAYHZjY0LnVnZQAHCkkICAAgACABYEULCAAgACABXkULCAAgACABX0ULCAAgACABXUULCAAgACABZkULCAAgACABZEULCAAgACABZUULCAAgACABY0UL +valid valid float_exprs.wast:936 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDCQgAAAAAAQEBAQfBAQgVZjMyLm5vX2ZvbGRfbHRfc2VsZWN0AAAVZjMyLm5vX2ZvbGRfbGVfc2VsZWN0AAEVZjMyLm5vX2ZvbGRfZ3Rfc2VsZWN0AAIVZjMyLm5vX2ZvbGRfZ2Vfc2VsZWN0AAMVZjY0Lm5vX2ZvbGRfbHRfc2VsZWN0AAQVZjY0Lm5vX2ZvbGRfbGVfc2VsZWN0AAUVZjY0Lm5vX2ZvbGRfZ3Rfc2VsZWN0AAYVZjY0Lm5vX2ZvbGRfZ2Vfc2VsZWN0AAcKaQgMACAAIAEgACABXRsLDAAgACABIAAgAV8bCwwAIAAgASAAIAFeGwsMACAAIAEgACABYBsLDAAgACABIAAgAWMbCwwAIAAgASAAIAFlGwsMACAAIAEgACABZBsLDAAgACABIAAgAWYbCw== +valid valid float_exprs.wast:983 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDCQgAAAAAAQEBAQehAQgRZjMyLm5vX2ZvbGRfbHRfaWYAABFmMzIubm9fZm9sZF9sZV9pZgABEWYzMi5ub19mb2xkX2d0X2lmAAIRZjMyLm5vX2ZvbGRfZ2VfaWYAAxFmNjQubm9fZm9sZF9sdF9pZgAEEWY2NC5ub19mb2xkX2xlX2lmAAURZjY0Lm5vX2ZvbGRfZ3RfaWYABhFmNjQubm9fZm9sZF9nZV9pZgAHCoEBCA8AIAAgAV0EfSAABSABCwsPACAAIAFfBH0gAAUgAQsLDwAgACABXgR9IAAFIAELCw8AIAAgAWAEfSAABSABCwsPACAAIAFjBHwgAAUgAQsLDwAgACABZQR8IAAFIAELCw8AIAAgAWQEfCAABSABCwsPACAAIAFmBHwgAAUgAQsL +valid valid float_exprs.wast:1062 AGFzbQEAAAABCwJgAX0BfWABfAF8AwkIAAAAAAEBAQEH+QEIHGYzMi5ub19mb2xkX2x0X3NlbGVjdF90b19hYnMAABxmMzIubm9fZm9sZF9sZV9zZWxlY3RfdG9fYWJzAAEcZjMyLm5vX2ZvbGRfZ3Rfc2VsZWN0X3RvX2FicwACHGYzMi5ub19mb2xkX2dlX3NlbGVjdF90b19hYnMAAxxmNjQubm9fZm9sZF9sdF9zZWxlY3RfdG9fYWJzAAQcZjY0Lm5vX2ZvbGRfbGVfc2VsZWN0X3RvX2FicwAFHGY2NC5ub19mb2xkX2d0X3NlbGVjdF90b19hYnMABhxmNjQubm9fZm9sZF9nZV9zZWxlY3RfdG9fYWJzAAcKmQEIEAAgAIwgACAAQwAAAABdGwsQACAAjCAAIABDAAAAgF8bCxAAIAAgAIwgAEMAAACAXhsLEAAgACAAjCAAQwAAAABgGwsUACAAmiAAIABEAAAAAAAAAABjGwsUACAAmiAAIABEAAAAAAAAAIBlGwsUACAAIACaIABEAAAAAAAAAIBkGwsUACAAIACaIABEAAAAAAAAAABmGws= +valid valid float_exprs.wast:1109 AGFzbQEAAAABCwJgAX0BfWABfAF8AwkIAAAAAAEBAQEH2QEIGGYzMi5ub19mb2xkX2x0X2lmX3RvX2FicwAAGGYzMi5ub19mb2xkX2xlX2lmX3RvX2FicwABGGYzMi5ub19mb2xkX2d0X2lmX3RvX2FicwACGGYzMi5ub19mb2xkX2dlX2lmX3RvX2FicwADGGY2NC5ub19mb2xkX2x0X2lmX3RvX2FicwAEGGY2NC5ub19mb2xkX2xlX2lmX3RvX2FicwAFGGY2NC5ub19mb2xkX2d0X2lmX3RvX2FicwAGGGY2NC5ub19mb2xkX2dlX2lmX3RvX2FicwAHCrEBCBMAIABDAAAAAF0EfSAAjAUgAAsLEwAgAEMAAACAXwR9IACMBSAACwsTACAAQwAAAIBeBH0gAAUgAIwLCxMAIABDAAAAAGAEfSAABSAAjAsLFwAgAEQAAAAAAAAAAGMEfCAAmgUgAAsLFwAgAEQAAAAAAAAAgGUEfCAAmgUgAAsLFwAgAEQAAAAAAAAAgGQEfCAABSAAmgsLFwAgAEQAAAAAAAAAAGYEfCAABSAAmgsL +valid valid float_exprs.wast:1189 AGFzbQEAAAABCQJgAAF9YAABfAMDAgABBzcCGGYzMi5pbmNvcnJlY3RfY29ycmVjdGlvbgAAGGY2NC5pbmNvcnJlY3RfY29ycmVjdGlvbgABCkUCGQBDvp+qP0PNzJw/kkO+n6o/k0PNzJw/kwspAESHFtnO91P1P0SamZmZmZnzP6BEhxbZzvdT9T+hRJqZmZmZmfM/oQs= +valid valid float_exprs.wast:1204 AGFzbQEAAAABBQFgAAF9AwIBAAcNAQljYWxjdWxhdGUAAApKAUgBBX1DAEAcQyEAQ1VVUEMhAUMVx+M/IQIgAYwgAJQgACAClCABk5UhAyABjCAAlCAAIAKUIAGTlSEEAkAgAyAEWw0AAAsgBAs= +valid valid float_exprs.wast:1223 AGFzbQEAAAABBQFgAAF8AwIBAAcNAQljYWxjdWxhdGUAAApWAVQBBXxEAAAAAACIY0AhAERLBquqqgpqQCEBRJbQ46riePw/IQIgAZogAKIgACACoiABoaMhAyABmiAAoiAAIAKiIAGhoyEEAkAgAyAEYQ0AAAsgBAs= +valid valid float_exprs.wast:1245 AGFzbQEAAAABBgFgAX0BfQMCAQAHEAEMbGx2bV9wcjI2NzQ2AAAKEgEQAEMAAAAAQwAAAIAgAJOTCw== +valid valid float_exprs.wast:1256 AGFzbQEAAAABBgFgAX8BfQMCAQAHEAEMbGx2bV9wcjI3MTUzAAAKFAESACAAQf////8AcbJDAAAAy5IL +valid valid float_exprs.wast:1267 AGFzbQEAAAABBwFgAn9/AX0DAgEABxABDGxsdm1fcHIyNzAzNgAAChcBFQAgAEHL/4d0crIgAUGAgJAHcbKSCw== +valid valid float_exprs.wast:1284 AGFzbQEAAAABFwNgBHx8fHwBfGADfHx8AXxgA319fQF9AwQDAAECByIDCHRoZXBhc3QwAAAIdGhlcGFzdDEAAQh0aGVwYXN0MgACCiUDDQAgACABoiACIAOiowsKACAAIAGiIAKhCwoAIAAgAZQgApQL +valid valid float_exprs.wast:1305 AGFzbQEAAAABBgFgAX0BfQMCAQAHCwEHaW52ZXJzZQAACgwBCgBDAACAPyAAlQs= +valid valid float_exprs.wast:1316 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHJwIQZjMyX3NxcnRfbWludXNfMgAAEGY2NF9zcXJ0X21pbnVzXzIAAQodAgsAIACRQwAAAECTCw8AIACfRAAAAAAAAABAoQs= +valid valid float_exprs.wast:1331 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHNQIXZjMyLm5vX2ZvbGRfcmVjaXBfcmVjaXAAABdmNjQubm9fZm9sZF9yZWNpcF9yZWNpcAABCisCEABDAACAP0MAAIA/IACVlQsYAEQAAAAAAADwP0QAAAAAAADwPyAAo6ML +valid valid float_exprs.wast:1363 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQc7AhpmMzIubm9fYWxnZWJyYWljX2ZhY3RvcmluZwAAGmY2NC5ub19hbGdlYnJhaWNfZmFjdG9yaW5nAAEKHQINACAAIAGSIAAgAZOUCw0AIAAgAaAgACABoaIL +valid valid float_exprs.wast:1387 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQc7AhpmMzIubm9fYWxnZWJyYWljX2ZhY3RvcmluZwAAGmY2NC5ub19hbGdlYnJhaWNfZmFjdG9yaW5nAAEKHQINACAAIACUIAEgAZSTCw0AIAAgAKIgASABoqEL +valid valid float_exprs.wast:1412 AGFzbQEAAAABDAJgA39/fwBgAX8BfQMDAgABBQQBAQEBByACEWYzMi5zaW1wbGVfeDRfc3VtAAAIZjMyLmxvYWQAAQpuAmQBCH0gACoCACEDIAAqAgQhBCAAKgIIIQUgACoCDCEGIAEqAgAhByABKgIEIQggASoCCCEJIAEqAgwhCiACIAMgB5I4AgAgAiAEIAiSOAIEIAIgBSAJkjgCCCACIAYgCpI4AgwLBwAgACoCAAsLNgEAQQALMAEAAAABAACAAQAAAAEAAIABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== +valid valid float_exprs.wast:1451 AGFzbQEAAAABDAJgA39/fwBgAX8BfAMDAgABBQQBAQEBByACEWY2NC5zaW1wbGVfeDRfc3VtAAAIZjY0LmxvYWQAAQpuAmQBCHwgACsDACEDIAArAwghBCAAKwMQIQUgACsDGCEGIAErAwAhByABKwMIIQggASsDECEJIAErAxghCiACIAMgB6A5AwAgAiAEIAigOQMIIAIgBSAJoDkDECACIAYgCqA5AxgLBwAgACsDAAsLZgEAQQALYAEAAAAAAAAAAQAAAAAAAIABAAAAAAAAAAEAAAAAAACAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== +valid valid float_exprs.wast:1493 AGFzbQEAAAABBwFgAn9/AX0DAwIAAAUEAQEBAQchAg1mMzIua2FoYW5fc3VtAAANZjMyLnBsYWluX3N1bQABCmICNwEDfQJAA0AgAyAAKgIAIASTIgSSIgIgA5MgBJMhBCAAQQRqIQAgAiEDIAFBf2oiAQ0ACwsgAgsoAQF9AkADQCACIAAqAgCSIQIgAEEEaiEAIAFBf2ohASABDQALCyACCwuHCAEAQQALgAjExVckpYTIC224Sy7ydhccykpWHhtucSJdFx5uv80UXMchVVE5nB+yUfCjk9fBLK5+qCg6ASH0CliT+EJ3n4M5al+69wrYUWo0yq3GNA7YJtxMMxztKZCoeA/RznYxI4O4NejyRLDTofy7MuGwumlECdbZff8uwFo2FDMUPqn6h22LvM6dp/3E6YU/3dfhGKZQJnJuP3MP+BKTIzRhdhJIwJsFk+usht6UPlXojOjd5PyVR75WAyEgTOa/e/Z/1bpzHMEUj8QnlrO9M/94QV/AWs72Z25zmhdmcAP4ziejUrKfO7/7ru3TWvg3V/D1bu+xTXA9VKcBmoUISJH1nQxgh1vZVB5RbYiOCIylcTpWCGdGj48TKizsLB+0YitvQQrEZUKiMWssfT67dayGlzDZSM2aH1bExuQSwJ377gKMzhzyHqF4I9vEHkkD03HMCFDF2Fzt1bVlrLXJIdLJKXbe8DAaWzzyO9s6OYI6FghvqPG+aWmZcaYF0xSTKhbyLxHHfiC7kUTu+OQBU8C5f/C/8AOcbbHfokQBbWtxK1yzIRlGXo/bkdN8eGu3EgCP672K9dQuxMEe33NjWUdJAwq3zyTPnA5Eep4U+0K/nTkwnqCrL9GunmqDQ+NVfYW/Y4r4lhAf/m3nIhvhaUaKRMjI+QwrGQelAj7yMBCahYpf74FFoHexAxBzS66YnUe/mi061Q8DZuM9U9lAzh9vMi8hKyMhbGLUpz6ozigxLQA9Z16voM8u0rlrhOtpCDxiNr4S/TZ/iD6tvAvAQcRQtuNQMejO4pZlVZwWRuawLTrogQWwvzT3vBAc+8w88YWXQp/rFI08v9cXiEmdiyuyOoPRTwSeoQ+tCJ1Ur9GCw+wyLwKPBSEtorfk9G8ugSsLnPzL/nQC+dv08+oAqOzRmXQm3dY01SWxRt2cqnH1YLCIyOALWVolTylm+eMu/una5RhPJ2L0zqQhlXTHV2Qnmkz9VH1hzsOsh0ac+v8JynmXZyR0ytQhgyYlGRI3ZBnlZeB0dY7dyO90x9ghK3kEUUZlYANd+tj0ZaSeXSPa14qSgKTeeDzxV0Jtzckv1aSeq0D0yxvXo8r866cBspppTkabGE7deaeqplI5Hu8wzJu9W+5MIW0wAHKwRl8Iz8W54D7CswzcjmTeGUJ5z0PqQ12OiPerFdw/yGcg27hksUcf3vLLP1mf2EaQ3K4vIvniMYnZnBxM06lKV4Scn+osPK48wx6L5U4XASXbNEZfFeoFDHzZRYwZ0HOKlhbdRPkFt1txsOYhNl91iZFzdat9rtNz7DfG6lV17+qri3sR3G0asmrEJc+q459JSYnLN5sKpwFgcNy3yIPhQvW+rWKUrY2h +valid valid float_exprs.wast:1587 AGFzbQEAAAABBwFgAn9/AXwDAwIAAAUEAQEBAQchAg1mNjQua2FoYW5fc3VtAAANZjY0LnBsYWluX3N1bQABCmICNwEDfAJAA0AgAyAAKwMAIAShIgSgIgIgA6EgBKEhBCAAQQhqIQAgAiEDIAFBf2oiAQ0ACwsgAgsoAQF8AkADQCACIAArAwCgIQIgAEEIaiEAIAFBf2ohASABDQALCyACCwuHEAEAQQALgBATBYRCXaIsxkPbVanN2lXjc/xY1rrVAP2DNUKIixNdOEpHDXJzoRrvxEUXV9jJRuCNbOE3cMiDW1VeWi1zHlbI4W1pFHgKilpkOgnHqIfF8NNd5gP8k74mytapkWC9sO2u9zB+kjpvp1mOqn2/Z1gqVPhO/u01WKZRv0LlS2YnJG1/Qi0okhjsCK7nVdqxpmWlclBHG7ipVNemBlsPQliDiheCxhBDoMAubbxahVNyf61EvDA8VbIkmnQ6nuHYD3D8qTrNk0vs437dXSfN+KCdHBHAVy79yBMyzDoafaNBVe3DgkkqBB7vc7kuLuNf9N/msjMMOT9vRGoDwUK5+rHI7aVYmX/ttHKeeev7Q4JFqruV0v8onvahrZXWVZUNb2ARx3g+SfJ+SPSicdATjrPemVLjRXTqdg4bKsjuFAHEUFs2PO+6cqKmCPh7Np357wvHVi1c8J1d3vy4rQ9kDpcVMibCMeYFHu/LFxttFQt0XdMu+GuGtLpzUlOZqXYgRclAgGsU7aH6gEbmJtLmmMRXv8QcpJB6NpQUuhWJbuacN4z03hIiXaF5UGcNPXrp1Koufyp6MD3qXRJI/uEYzaRXooc+tpqL29qdeJzPjbFPkLQ04J32yv5MO3htClwYn2G53bTgD3bgG2kNXlhzcF4OLaF9/yDrkTSSrDhyKh+OcS5q8a/HJ3DZxFf30jwduPDwZM/crr6jzD4ifU5pIWMX7QMCVJoPUE4TWjWhIqTfhsJ0eRa4aWmgUl0RZL1bk/xpoPQT0IFR3foMFcN6yWJ6qR3J5lqzW5cCPGQiEjwikGQtMFRMtKEiCVciXo44KwKorva+DSvyA636EAFxdyowApX2AD7QxI00GVAhCrxQ2jww1joxlI06/u8UV51LkwCWJAxv/bwjdgJs61JygBF+gDoTEjgdOEmVQCeKRHvo3G2MjI48tbMYDvYIGoRBNf+LuJNA6uFRHYmljUJoKeovwXpS65BdTdaA49d1SM7t0wEcjVullA14z/EGEy+YAqRtLmzy1XQpiUz5A/XHGK168Gj4XNZZh27WPwa+hiDjQZEi826L8GgcV6f8sHyemQuWGolf5g18CFGgomeaRwCTa/ko8GjbYvHgZSxTM+CnyhFCMPavAcFlPTIBb6suvtOLvhTD/+z78PnFDAVvAQlr4zQxDB9mpkK8GodJFhaMsJANNIwK4QleEKRrVszwybvcuFzO9syNdX6zB4gEL7ReyeNKI3MZYmyaA3ZEhpxg/Ntyjyeg3bPF2v/57Gqxe9PPUDfJengM5Dq29eb0mG5CfTVzi0XAVpfNbc7PrTGzw1T679XA9GpfVOdJPjMKMDj92QX/pT9XRhS1kRfKa5gjemWzbAK0zHldWNiz1ZSu9G11ZfeSv35HTDzu26zxMl37b0EcNMiDT8JYAb4FPmYWpgRtXU+GCSeCJRLNOs3Oa7zKrCib7molhp5FcMbSvTt9QuUnr8cd9IHIs3aKqDajrirmGOE2Iq32JXKwOYsBmiJ7hMMtX3KkmKwVcOfUGOJ90jB8MwjNysQihYh1gcZKdFiN4Oisxat1WvQoEvAYRVLyl7KTQW+Nf9tw+6NdH6eNmCArIp86AbWLG9LLFAMOFBTSGVofzl7NgXkVAcrec3SMViCfdy0lFvZhUR2kjpuYpcbsqEVXgll4DZC031Gww4KUzLNTCRVtlmw6QEe3SnoFL6EejJ2gIIj7Uref8/O7X+eKYachsaz6CaqkbLwkgLoq6WX/cP/M+mWHdvPFFc7L6EIxAAyRV9ngnTVUJK2k2PkIZ2PIz4HdkKLXxAdK5hBvZ+cn1CNZGPKonV/YlDCqVIZPh52CtSbKppa/z1X5nTcBGUhDxZRs83SXWEw8nQjoBMJYMHbhoPjq6cWuz3ieqQyss0RC4LxdG5xJWEocGUnBOur16zuBqUtwDMyeGtMvt1IvIDvrZFEdoC2yPr4ThUiSMi7bXKHnjEWRNQEKk8LrCc7z0iIk0IzMHZ04yE3jgsxkFQYt5wEvq7u1BEySHHrWP+hfMRUM3OQxtMQlPiqqAJ7I5SF6fynxwK8dXuhjOa34fmzIxX/CqJcnCtn0IWrqAwn795Y7g3lffEswn1Y13rRz1JXwFMN0Lw2jHU6NMSSzGoSFYlp7PBQ5F+Zt6zfCAFhbC+M8imLh+DVLVuKHYIu+pziRd1SpWiQlkJ+lQnfzXDnf/3QHdqHNH2ILgYForwXBwH8m7sCRo2p9KWFFJ+VXiNwNlwQaM6lEitoCEEU/jlWmdoxN4/GJg8jQ+JtQd59H30ycZg2qGLhfT8QBztyErEaeaeF2RWthieRdlLsRg5942ArS9X5dQ+q8EPE6yeJk+1Nl0Me0p/vUBVMl0M0piABWJSR9XbTzQZ/ptfeuZCzjyW3VhDpyErh62RsJ6DjaJk8EzgNxbopEe1yBWZzS5MO6WablKKePmuTVTrnKf8t1uCtDPrMVRrGlvJ2eOBXxvRshqvGCAJX8p3dHOaczQ5LXUkBLBoGKoL3xa5mEQlviO8VeElwoTbYOTshc6AGKxefknULuXZzE6+toCSeSlZoRVHPEEoD7ff7FCGB/NkHgELrWK2zxtBf+JjTjS/io45G+Tyr82oG45/7VJlBH8xplMoHgBbhPMjEmAEpTl8LDDi6hJlSrBY5WL32vIoRopYuX9qT9qMx1QZaG/Sc9KYaNf0zUjnNB9B7i3Vgnl86clM96BC/c7Q== +valid valid float_exprs.wast:1640 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQctAhNmMzIubm9fZm9sZF9uZWdfc3ViAAATZjY0Lm5vX2ZvbGRfbmVnX3N1YgABChMCCAAgACABk4wLCAAgACABoZoL +valid valid float_exprs.wast:1660 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQctAhNmMzIubm9fZm9sZF9uZWdfYWRkAAATZjY0Lm5vX2ZvbGRfbmVnX2FkZAABChMCCAAgACABkowLCAAgACABoJoL +valid valid float_exprs.wast:1680 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQc1AhdmMzIubm9fZm9sZF9hZGRfbmVnX25lZwAAF2Y2NC5ub19mb2xkX2FkZF9uZWdfbmVnAAEKFQIJACAAjCABjJILCQAgAJogAZqgCw== +valid valid float_exprs.wast:1700 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHLQITZjMyLm5vX2ZvbGRfYWRkX25lZwAAE2Y2NC5ub19mb2xkX2FkZF9uZWcAAQoTAggAIACMIACSCwgAIACaIACgCw== +valid valid float_exprs.wast:1720 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHMwIWZjMyLm5vX2ZvbGRfNnhfdmlhX2FkZAAAFmY2NC5ub19mb2xkXzZ4X3ZpYV9hZGQAAQopAhMAIAAgAJIgAJIgAJIgAJIgAJILEwAgACAAoCAAoCAAoCAAoCAAoAs= +valid valid float_exprs.wast:1749 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBy0CE2YzMi5ub19mb2xkX2Rpdl9kaXYAABNmNjQubm9fZm9sZF9kaXZfZGl2AAEKFwIKACAAIAGVIAKVCwoAIAAgAaMgAqML +valid valid float_exprs.wast:1773 AGFzbQEAAAABEQJgBH19fX0BfWAEfHx8fAF8AwMCAAEHLwIUZjMyLm5vX2ZvbGRfbXVsX2RpdnMAABRmNjQubm9fZm9sZF9tdWxfZGl2cwABCh0CDQAgACABlSACIAOVlAsNACAAIAGjIAIgA6OiCw== +valid valid float_exprs.wast:1795 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBy8CFGYzMi5ub19mb2xkX2FkZF9kaXZzAAAUZjY0Lm5vX2ZvbGRfYWRkX2RpdnMAAQodAg0AIAAgApUgASAClZILDQAgACACoyABIAKjoAs= +valid valid float_exprs.wast:1817 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHNQIXZjMyLm5vX2ZvbGRfc3FydF9zcXVhcmUAABdmNjQubm9fZm9sZF9zcXJ0X3NxdWFyZQABChMCCAAgACAAlJELCAAgACAAop8L +valid valid float_exprs.wast:1839 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQcxAhVmMzIubm9fZm9sZF9tdWxfc3FydHMAABVmNjQubm9fZm9sZF9tdWxfc3FydHMAAQoVAgkAIACRIAGRlAsJACAAnyABn6IL +valid valid float_exprs.wast:1861 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQcxAhVmMzIubm9fZm9sZF9kaXZfc3FydHMAABVmNjQubm9fZm9sZF9kaXZfc3FydHMAAQoVAgkAIACRIAGRlQsJACAAnyABn6ML +valid valid float_exprs.wast:1883 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQc3AhhmMzIubm9fZm9sZF9tdWxfc3FydF9kaXYAABhmNjQubm9fZm9sZF9tdWxfc3FydF9kaXYAAQoZAgsAIAAgAZGUIAGVCwsAIAAgAZ+iIAGjCw== +valid valid float_exprs.wast:1906 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABB00CI2YzMi5ub19mbHVzaF9pbnRlcm1lZGlhdGVfc3Vibm9ybWFsAAAjZjY0Lm5vX2ZsdXNoX2ludGVybWVkaWF0ZV9zdWJub3JtYWwAAQoXAgoAIAAgAZQgApQLCgAgACABoiACogs= +valid valid float_exprs.wast:1920 AGFzbQEAAAABEwNgAn19AX9gAnx8AX9gAnx9AX0DCAcAAAABAQECB38HD2YzMi5yZWNvZGluZ19lcQAAD2YzMi5yZWNvZGluZ19sZQABD2YzMi5yZWNvZGluZ19sdAACD2Y2NC5yZWNvZGluZ19lcQADD2Y2NC5yZWNvZGluZ19sZQAED2Y2NC5yZWNvZGluZ19sdAAFD3JlY29kaW5nX2RlbW90ZQAGCkwHCgAgACABlCAAWwsKACAAIAGUIABfCwoAIAAgAZQgAF0LCgAgACABoiAAYQsKACAAIAGiIABlCwoAIAAgAaIgAGMLCAAgALYgAZQL +valid valid float_exprs.wast:1964 AGFzbQEAAAABDwJgA319fQF/YAN8fHwBfwMDAgABB0ECHWYzMi5ub19leHRlbmRlZF9wcmVjaXNpb25fZGl2AAAdZjY0Lm5vX2V4dGVuZGVkX3ByZWNpc2lvbl9kaXYAAQoXAgoAIAAgAZUgAlsLCgAgACABoyACYQs= +valid valid float_exprs.wast:1980 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHNQIXZjMyLm5vX2Rpc3RyaWJ1dGVfZXhhY3QAABdmNjQubm9fZGlzdHJpYnV0ZV9leGFjdAABCjECEwBDAAAAwSAAlEMAAABBIACUkgsbAEQAAAAAAAAgwCAAokQAAAAAAAAgQCAAoqAL +valid valid float_exprs.wast:1994 AGFzbQEAAAABKQZgAX0BfWAEfX19fQF9YAN9fX0BfWABfAF8YAR8fHx8AXxgA3x8fAF8AwsKAAECAgEDBAUFBAe1AQoIZjMyLnNxcnQAAA9mMzIueGtjZF9zcXJ0XzIAAQ9mMzIueGtjZF9zcXJ0XzMAAg9mMzIueGtjZF9zcXJ0XzUAAxZmMzIueGtjZF9iZXR0ZXJfc3FydF81AAQIZjY0LnNxcnQABQ9mNjQueGtjZF9zcXJ0XzIABg9mNjQueGtjZF9zcXJ0XzMABw9mNjQueGtjZF9zcXJ0XzUACBZmNjQueGtjZF9iZXR0ZXJfc3FydF81AAkKiQEKBQAgAJELEAAgACABlSACIAMgApOVkgsKACAAIAGUIAKVCw0AIAAgAZUgAiAAlZILEwAgACABIAKUkiADIAEgApSTlQsFACAAnwsQACAAIAGjIAIgAyACoaOgCwoAIAAgAaIgAqMLDQAgACABoyACIACjoAsTACAAIAEgAqKgIAMgASACoqGjCw== +valid valid float_exprs.wast:2045 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQcpAhFmMzIuY29tcHV0ZV9yYWRpeAAAEWY2NC5jb21wdXRlX3JhZGl4AAEKnQECQwADQCAAIACSIgBDAACAP5IgAJNDAACAv5JDAAAAAFsNAAsDQCAAIAFDAACAP5IiAZIgAJMgAZNDAAAAAFwNAAsgAQtXAANAIAAgAKAiAEQAAAAAAADwP6AgAKFEAAAAAAAA8L+gRAAAAAAAAAAAYQ0ACwNAIAAgAUQAAAAAAADwP6AiAaAgAKEgAaFEAAAAAAAAAABiDQALIAEL +valid valid float_exprs.wast:2129 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQc3AhhmMzIubm9fZm9sZF9zdWIxX211bF9hZGQAABhmNjQubm9fZm9sZF9zdWIxX211bF9hZGQAAQonAhAAIABDAACAP5MgAZQgAZILFAAgAEQAAAAAAADwP6EgAaIgAaAL +valid valid float_exprs.wast:2143 AGFzbQEAAAABDwJgA319fQF/YAN8fHwBfwMFBAAAAQEHiQEEH2YzMi5ub19mb2xkX2FkZF9sZV9tb25vdG9uaWNpdHkAAB9mMzIubm9fZm9sZF9hZGRfZ2VfbW9ub3RvbmljaXR5AAEfZjY0Lm5vX2ZvbGRfYWRkX2xlX21vbm90b25pY2l0eQACH2Y2NC5ub19mb2xkX2FkZF9nZV9tb25vdG9uaWNpdHkAAwo5BA0AIAAgApIgASACkl8LDQAgACACkiABIAKSYAsNACAAIAKgIAEgAqBlCw0AIAAgAqAgASACoGYL +valid valid float_exprs.wast:2164 AGFzbQEAAAABDQJgAn19AX9gAnx8AX8DCQgAAAAAAQEBAQdpCApmMzIubm90X2x0AAAKZjMyLm5vdF9sZQABCmYzMi5ub3RfZ3QAAgpmMzIubm90X2dlAAMKZjY0Lm5vdF9sdAAECmY2NC5ub3RfbGUABQpmNjQubm90X2d0AAYKZjY0Lm5vdF9nZQAHCkkICAAgACABXUULCAAgACABX0ULCAAgACABXkULCAAgACABYEULCAAgACABY0ULCAAgACABZUULCAAgACABZEULCAAgACABZkUL +valid valid float_exprs.wast:2203 AGFzbQEAAAABCQJgAAF9YAABfAMDAgABBx0CC2YzMi5lcHNpbG9uAAALZjY0LmVwc2lsb24AAQpVAh8AQwAAgD9DAABAQEMAAIBAQwAAQECVQwAAgD+TlJMLMwBEAAAAAAAA8D9EAAAAAAAACEBEAAAAAAAAEEBEAAAAAAAACECjRAAAAAAAAPA/oaKhCw== +valid valid float_exprs.wast:2218 AGFzbQEAAAABCQJgAAF9YAABfAMDAgABBx0CC2YzMi5lcHNpbG9uAAALZjY0LmVwc2lsb24AAQpnAioBAn1DAACAPyEAA0AgACIBQwAAAD+UIgBDAACAP5JDAACAP14NAAsgAQs6AQJ8RAAAAAAAAPA/IQADQCAAIgFEAAAAAAAA4D+iIgBEAAAAAAAA8D+gRAAAAAAAAPA/ZA0ACyABCw== +valid valid float_exprs.wast:2272 AGFzbQEAAAABDQJgAn19AX9gAnx8AX8DCQgAAAAAAQEBAQe5AQgUZjMyLm5vX3RyaWNob3RvbXlfbHQAABRmMzIubm9fdHJpY2hvdG9teV9sZQABFGYzMi5ub190cmljaG90b215X2d0AAIUZjMyLm5vX3RyaWNob3RvbXlfZ2UAAxRmNjQubm9fdHJpY2hvdG9teV9sdAAEFGY2NC5ub190cmljaG90b215X2xlAAUUZjY0Lm5vX3RyaWNob3RvbXlfZ3QABhRmNjQubm9fdHJpY2hvdG9teV9nZQAHCnEIDQAgACABXSAAIAFgcgsNACAAIAFfIAAgAV5yCw0AIAAgAV4gACABX3ILDQAgACABYCAAIAFdcgsNACAAIAFjIAAgAWZyCw0AIAAgAWUgACABZHILDQAgACABZCAAIAFlcgsNACAAIAFmIAAgAWNyCw== +valid valid float_exprs.wast:2304 AGFzbQEAAAABFwRgAn9/AX9gAX8Bf2ACfn4BfmABfgF+AxQTAAABAgIDAQEBAQEBAwMDAwMDAQfuAxMdZjMyLmFyaXRobWV0aWNfbmFuX2JpdHBhdHRlcm4AABxmMzIuY2Fub25pY2FsX25hbl9iaXRwYXR0ZXJuAAEgZjMyLm5vbmFyaXRobWV0aWNfbmFuX2JpdHBhdHRlcm4AAh1mNjQuYXJpdGhtZXRpY19uYW5fYml0cGF0dGVybgADHGY2NC5jYW5vbmljYWxfbmFuX2JpdHBhdHRlcm4ABCBmNjQubm9uYXJpdGhtZXRpY19uYW5fYml0cGF0dGVybgAFFGYzMi5ub19mb2xkX3N1Yl96ZXJvAAYUZjMyLm5vX2ZvbGRfbmVnMF9zdWIABxNmMzIubm9fZm9sZF9tdWxfb25lAAgUZjMyLm5vX2ZvbGRfbmVnMV9tdWwACRNmMzIubm9fZm9sZF9kaXZfb25lAAoUZjMyLm5vX2ZvbGRfZGl2X25lZzEACxRmNjQubm9fZm9sZF9zdWJfemVybwAMFGY2NC5ub19mb2xkX25lZzBfc3ViAA0TZjY0Lm5vX2ZvbGRfbXVsX29uZQAOFGY2NC5ub19mb2xkX25lZzFfbXVsAA8TZjY0Lm5vX2ZvbGRfZGl2X29uZQAQFGY2NC5ub19mb2xkX2Rpdl9uZWcxABEWbm9fZm9sZF9wcm9tb3RlX2RlbW90ZQASCpkDExEAIAC+IAG+lbxBgICA/gdxCxEAIAC+IAG+lbxB/////wdxCwcAIAC+jLwLFgAgAL8gAb+jvUKAgICAgICA/P8AgwsWACAAvyABv6O9Qv///////////wCDCwcAIAC/mr0LEwAgAL5DAAAAAJO8QYCAgP4HcQsTAEMAAACAIAC+k7xBgICA/gdxCxMAIAC+QwAAgD+UvEGAgID+B3ELEwBDAACAvyAAvpS8QYCAgP4HcQsTACAAvkMAAIA/lbxBgICA/gdxCxMAIAC+QwAAgL+VvEGAgID+B3ELHAAgAL9EAAAAAAAAAAChvUKAgICAgICA/P8AgwscAEQAAAAAAAAAgCAAv6G9QoCAgICAgID8/wCDCxwAIAC/RAAAAAAAAPA/or1CgICAgICAgPz/AIMLHABEAAAAAAAA8L8gAL+ivUKAgICAgICA/P8AgwscACAAv0QAAAAAAADwP6O9QoCAgICAgID8/wCDCxwAIAC/RAAAAAAAAPC/o71CgICAgICAgPz/AIMLDwAgAL67trxBgICA/gdxCw== +valid valid float_exprs.wast:2420 AGFzbQEAAAABDQFgCHx8fHx8fHx8AXwDAwIAAAcyAhNkb3RfcHJvZHVjdF9leGFtcGxlAAAYd2l0aF9iaW5hcnlfc3VtX2NvbGxhcHNlAAEKNQIZACAAIASiIAEgBaKgIAIgBqKgIAMgB6KgCxkAIAAgBKIgASAFoqAgAiAGoiADIAeioKAL +valid valid float_exprs.wast:2454 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQcnAhBmMzIuY29udHJhY3QyZm1hAAAQZjY0LmNvbnRyYWN0MmZtYQABCh8CDgAgACAAlCABIAGUk5ELDgAgACAAoiABIAGioZ8L +valid valid float_exprs.wast:2475 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBz8CHGYzMi5kaXZpc2lvbl9ieV9zbWFsbF9udW1iZXIAABxmNjQuZGl2aXNpb25fYnlfc21hbGxfbnVtYmVyAAEKFwIKACAAIAEgApWTCwoAIAAgASACo6EL +valid valid float_exprs.wast:2490 AGFzbQEAAAABDwJgA319fQF9YAN8fHwBfAMDAgABBycCEGYzMi5nb2xkZW5fcmF0aW8AABBmNjQuZ29sZGVuX3JhdGlvAAEKGQILACAAIAEgApGSlAsLACAAIAEgAp+gogs= +valid valid float_exprs.wast:2503 AGFzbQEAAAABCwJgAX0BfWABfAF8AwMCAAEHJwIQZjMyLnNpbHZlcl9tZWFucwAAEGY2NC5zaWx2ZXJfbWVhbnMAAQo5AhcAQwAAAD8gACAAIACUQwAAgECSkZKUCx8ARAAAAAAAAOA/IAAgACAAokQAAAAAAAAQQKCfoKIL +valid valid float_exprs.wast:2532 AGFzbQEAAAABBwFgAnx8AX8DAgEABw4BCnBvaW50X2ZvdXIAAAoTAREAIAAgAaNEmpmZmZmZ2T9jCw== +valid valid float_exprs.wast:2542 AGFzbQEAAAABBgFgAX8BfAMCAQAHBwEDdGF1AAAKywEByAEBBHxEAAAAAAAAAAAhAQJAIABBAUgNAEQAAAAAAADwPyECRAAAAAAAAAAAIQMDQCABIAJEAAAAAAAAIEAgA0QAAAAAAAAgQKIiBEQAAAAAAADwP6CjRAAAAAAAABBAIAREAAAAAAAAEECgo6FEAAAAAAAAAEAgBEQAAAAAAAAUQKCjoUQAAAAAAAAAQCAERAAAAAAAABhAoKOhoqAhASADRAAAAAAAAPA/oCEDIAJEAAAAAAAAsD+iIQIgAEF/aiIADQALCyABCw== +valid valid float_exprs.wast:2612 AGFzbQEAAAABDQJgAn19AX1gAnx8AXwDAwIAAQc9AhtmMzIubm9fZm9sZF9jb25kaXRpb25hbF9pbmMAABtmNjQubm9fZm9sZF9jb25kaXRpb25hbF9pbmMAAQo1AhUAIAAgAEMAAIA/kiABQwAAAABdGwsdACAAIABEAAAAAAAA8D+gIAFEAAAAAAAAAABjGws= +valid valid float_literals.wast:3 AGFzbQEAAAABEQRgAAF/YAABfmAAAX1gAAF8A2NiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAgICAgICAgICAgMDAwMDAwMDAwMHmQ5iB2YzMi5uYW4AABBmMzIucG9zaXRpdmVfbmFuAAEQZjMyLm5lZ2F0aXZlX25hbgACDWYzMi5wbGFpbl9uYW4AAyJmMzIuaW5mb3JtYWxseV9rbm93bl9hc19wbGFpbl9zbmFuAAQQZjMyLmFsbF9vbmVzX25hbgAFDGYzMi5taXNjX25hbgAGFWYzMi5taXNjX3Bvc2l0aXZlX25hbgAHFWYzMi5taXNjX25lZ2F0aXZlX25hbgAIDGYzMi5pbmZpbml0eQAJFWYzMi5wb3NpdGl2ZV9pbmZpbml0eQAKFWYzMi5uZWdhdGl2ZV9pbmZpbml0eQALCGYzMi56ZXJvAAwRZjMyLnBvc2l0aXZlX3plcm8ADRFmMzIubmVnYXRpdmVfemVybwAOCGYzMi5taXNjAA8QZjMyLm1pbl9wb3NpdGl2ZQAQDmYzMi5taW5fbm9ybWFsABEOZjMyLm1heF9maW5pdGUAEhFmMzIubWF4X3N1Ym5vcm1hbAATEGYzMi50cmFpbGluZ19kb3QAFAxmMzIubWlzY19pbnQAFQ1mMzIubGFyZ2VfaW50ABYNZjMyLm1pbl9pbnQzMgAXDWYzMi5taW5faW50NjQAGAxmMzJfZGVjLnplcm8AGRVmMzJfZGVjLnBvc2l0aXZlX3plcm8AGhVmMzJfZGVjLm5lZ2F0aXZlX3plcm8AGwxmMzJfZGVjLm1pc2MAHBRmMzJfZGVjLm1pbl9wb3NpdGl2ZQAdEmYzMl9kZWMubWluX25vcm1hbAAeFWYzMl9kZWMubWF4X3N1Ym5vcm1hbAAfEmYzMl9kZWMubWF4X2Zpbml0ZQAgFGYzMl9kZWMudHJhaWxpbmdfZG90ACEQZjMyX2RlYy5taXNjX2ludAAiEWYzMl9kZWMubGFyZ2VfaW50ACMRZjMyX2RlYy5taW5faW50MzIAJBFmMzJfZGVjLm1pbl9pbnQ2NAAlF2YzMl9kZWMucm9vdF9iZWVyX2Zsb2F0ACYHZjY0Lm5hbgAnEGY2NC5wb3NpdGl2ZV9uYW4AKBBmNjQubmVnYXRpdmVfbmFuACkNZjY0LnBsYWluX25hbgAqImY2NC5pbmZvcm1hbGx5X2tub3duX2FzX3BsYWluX3NuYW4AKxBmNjQuYWxsX29uZXNfbmFuACwMZjY0Lm1pc2NfbmFuAC0VZjY0Lm1pc2NfcG9zaXRpdmVfbmFuAC4VZjY0Lm1pc2NfbmVnYXRpdmVfbmFuAC8MZjY0LmluZmluaXR5ADAVZjY0LnBvc2l0aXZlX2luZmluaXR5ADEVZjY0Lm5lZ2F0aXZlX2luZmluaXR5ADIIZjY0Lnplcm8AMxFmNjQucG9zaXRpdmVfemVybwA0EWY2NC5uZWdhdGl2ZV96ZXJvADUIZjY0Lm1pc2MANhBmNjQubWluX3Bvc2l0aXZlADcOZjY0Lm1pbl9ub3JtYWwAOBFmNjQubWF4X3N1Ym5vcm1hbAA5DmY2NC5tYXhfZmluaXRlADoQZjY0LnRyYWlsaW5nX2RvdAA7DGY2NC5taXNjX2ludAA8DWY2NC5sYXJnZV9pbnQAPQ1mNjQubWluX2ludDMyAD4NZjY0Lm1pbl9pbnQ2NAA/DGY2NF9kZWMuemVybwBAFWY2NF9kZWMucG9zaXRpdmVfemVybwBBFWY2NF9kZWMubmVnYXRpdmVfemVybwBCDGY2NF9kZWMubWlzYwBDFGY2NF9kZWMubWluX3Bvc2l0aXZlAEQSZjY0X2RlYy5taW5fbm9ybWFsAEUVZjY0X2RlYy5tYXhfc3Vibm9ybWFsAEYSZjY0X2RlYy5tYXhfZmluaXRlAEcUZjY0X2RlYy50cmFpbGluZ19kb3QASBBmNjRfZGVjLm1pc2NfaW50AEkRZjY0X2RlYy5sYXJnZV9pbnQAShFmNjRfZGVjLm1pbl9pbnQzMgBLEWY2NF9kZWMubWluX2ludDY0AEwXZjY0X2RlYy5yb290X2JlZXJfZmxvYXQATQxmMzItZGVjLXNlcDEATgxmMzItZGVjLXNlcDIATwxmMzItZGVjLXNlcDMAUAxmMzItZGVjLXNlcDQAUQxmMzItZGVjLXNlcDUAUgxmMzItaGV4LXNlcDEAUwxmMzItaGV4LXNlcDIAVAxmMzItaGV4LXNlcDMAVQxmMzItaGV4LXNlcDQAVgxmMzItaGV4LXNlcDUAVwxmNjQtZGVjLXNlcDEAWAxmNjQtZGVjLXNlcDIAWQxmNjQtZGVjLXNlcDMAWgxmNjQtZGVjLXNlcDQAWwxmNjQtZGVjLXNlcDUAXAxmNjQtaGV4LXNlcDEAXQxmNjQtaGV4LXNlcDIAXgxmNjQtaGV4LXNlcDMAXwxmNjQtaGV4LXNlcDQAYAxmNjQtaGV4LXNlcDUAYQqjCGIIAEMAAMB/vAsIAEMAAMB/vAsIAEMAAMD/vAsIAEMAAMB/vAsIAEMAAKB/vAsIAEP/////vAsIAENFI4F/vAsIAENQQLB/vAsIAEPevKr/vAsIAEMAAIB/vAsIAEMAAIB/vAsIAEMAAID/vAsIAEMAAAAAvAsIAEMAAAAAvAsIAEMAAACAvAsIAEPbD8lAvAsIAEMBAAAAvAsIAEMAAIAAvAsIAEP//39/vAsIAEP//38AvAsIAEMAAIBEvAsIAEOAopFHvAsIAEMAAIBnvAsIAEMAAADPvAsIAEMAAADfvAsIAEMAAAAAvAsIAEMAAAAAvAsIAEMAAACAvAsIAEPbD8lAvAsIAEMBAAAAvAsIAEMAAIAAvAsIAEP//38AvAsIAEP//39/vAsIAEP5AhVQvAsIAEMA5EBGvAsIAEPseK1gvAsIAEMAAADPvAsIAEMAAADfvAsIAEMBAIA/vAsMAEQAAAAAAAD4f70LDABEAAAAAAAA+H+9CwwARAAAAAAAAPj/vQsMAEQAAAAAAAD4f70LDABEAAAAAAAA9H+9CwwARP//////////vQsMAES8mnhWNBLwf70LDABECQgHBgUE83+9CwwAREUjAe/Nq/L/vQsMAEQAAAAAAADwf70LDABEAAAAAAAA8H+9CwwARAAAAAAAAPD/vQsMAEQAAAAAAAAAAL0LDABEAAAAAAAAAAC9CwwARAAAAAAAAACAvQsMAEQYLURU+yEZQL0LDABEAQAAAAAAAAC9CwwARAAAAAAAABAAvQsMAET///////8PAL0LDABE////////73+9CwwARAAAAAAAADBGvQsMAEQAAAAAUDTyQL0LDABEAAAAAAAA8ES9CwwARAAAAAAAAODBvQsMAEQAAAAAAADgw70LDABEAAAAAAAAAAC9CwwARAAAAAAAAAAAvQsMAEQAAAAAAAAAgL0LDABEGC1EVPshGUC9CwwARAEAAAAAAAAAvQsMAEQAAAAAAAAQAL0LDABE////////DwC9CwwARP///////+9/vQsMAER9w5QlrUmyVL0LDABEAAAAAIAcyEC9CwwARECMtXgdrxVEvQsMAEQAAAAAAADgwb0LDABEAAAAAAAA4MO9CwwARCSe8R8AAPA/vQsHAEMAJHRJCwcAQwAAekQLBwBDEMl6RAsHAEOWGWFYCwcAQ3SuHW4LBwBDCvAgTQsHAEOAB9VHCwcAQ/H/IEcLBwBDAADwSQsHAEMowKtVCwsARAAAAACAhC5BCwsARAAAAAAAQI9ACwsARACL/PohWY9ACwsAREXMt1tzXNAmCwsARJzeW3LOtcNFCwsARDIzAQAe4CVDCwsARAAAAADwoPpACwsARECzNCj+H+RACwsARAAAAAAAAD5BCwsARABwnw8FeLVCCw== +valid valid float_literals.wast:224 AGFzbQEAAAABhYCAgAABYAABfAOCgICAAAEAB46AgIAAAQo0Mjk0OTY3MjQ5AAAKkYCAgAABi4CAgAAARAAAIPr//+9BCw== +valid valid float_memory.wast:5 AGFzbQEAAAABDANgAAF9YAABf2AAAAMGBQABAgICBQQBAQEBBzcFCGYzMi5sb2FkAAAIaTMyLmxvYWQAAQlmMzIuc3RvcmUAAglpMzIuc3RvcmUAAwVyZXNldAAECjYFBwBBACoCAAsHAEEAKAIACwwAQQBDAACgfzgCAAsNAEEAQYCAgP0HNgIACwkAQQBBADYCAAsLCgEAQQALBAAAoH8= +valid valid float_memory.wast:30 AGFzbQEAAAABDANgAAF8YAABfmAAAAMGBQABAgICBQQBAQEBBzcFCGY2NC5sb2FkAAAIaTY0LmxvYWQAAQlmNjQuc3RvcmUAAglpNjQuc3RvcmUAAwVyZXNldAAECj8FBwBBACsDAAsHAEEAKQMACxAAQQBEAAAAAAAA9H85AwALEgBBAEKAgICAgICA+v8ANwMACwkAQQBCADcDAAsLDgEAQQALCAAAAAAAAPR/ +valid valid float_memory.wast:57 AGFzbQEAAAABDANgAAF9YAABf2AAAAMGBQABAgICBQQBAQEBBzcFCGYzMi5sb2FkAAAIaTMyLmxvYWQAAQlmMzIuc3RvcmUAAglpMzIuc3RvcmUAAwVyZXNldAAECjYFBwBBASoCAAsHAEEBKAIACwwAQQFDAACgfzgCAAsNAEEBQYCAgP0HNgIACwkAQQFBADYCAAsLCwEAQQALBQAAAKB/ +valid valid float_memory.wast:82 AGFzbQEAAAABDANgAAF8YAABfmAAAAMGBQABAgICBQQBAQEBBzcFCGY2NC5sb2FkAAAIaTY0LmxvYWQAAQlmNjQuc3RvcmUAAglpNjQuc3RvcmUAAwVyZXNldAAECj8FBwBBASsDAAsHAEEBKQMACxAAQQFEAAAAAAAA9H85AwALEgBBAUKAgICAgICA+v8ANwMACwkAQQFCADcDAAsLDwEAQQALCQAAAAAAAAD0fw== +valid valid float_memory.wast:109 AGFzbQEAAAABDANgAAF9YAABf2AAAAMGBQABAgICBQQBAQEBBzcFCGYzMi5sb2FkAAAIaTMyLmxvYWQAAQlmMzIuc3RvcmUAAglpMzIuc3RvcmUAAwVyZXNldAAECjYFBwBBACoCAAsHAEEAKAIACwwAQQBDAQDQfzgCAAsNAEEAQYGAwP4HNgIACwkAQQBBADYCAAsLCgEAQQALBAEA0H8= +valid valid float_memory.wast:134 AGFzbQEAAAABDANgAAF8YAABfmAAAAMGBQABAgICBQQBAQEBBzcFCGY2NC5sb2FkAAAIaTY0LmxvYWQAAQlmNjQuc3RvcmUAAglpNjQuc3RvcmUAAwVyZXNldAAECj8FBwBBACsDAAsHAEEAKQMACxAAQQBEAQAAAAAA/H85AwALEgBBAEKBgICAgICA/v8ANwMACwkAQQBCADcDAAsLDgEAQQALCAEAAAAAAPx/ +valid valid float_misc.wast:17 AGFzbQEAAAABFwRgAn19AX1gAX0BfWACfHwBfGABfAF8Ax0cAAAAAAEBAQABAQEBAAACAgICAwMDAgMDAwMCAge3AhwHZjMyLmFkZAAAB2YzMi5zdWIAAQdmMzIubXVsAAIHZjMyLmRpdgADCGYzMi5zcXJ0AAQHZjMyLmFicwAFB2YzMi5uZWcABgxmMzIuY29weXNpZ24ABwhmMzIuY2VpbAAICWYzMi5mbG9vcgAJCWYzMi50cnVuYwAKC2YzMi5uZWFyZXN0AAsHZjMyLm1pbgAMB2YzMi5tYXgADQdmNjQuYWRkAA4HZjY0LnN1YgAPB2Y2NC5tdWwAEAdmNjQuZGl2ABEIZjY0LnNxcnQAEgdmNjQuYWJzABMHZjY0Lm5lZwAUDGY2NC5jb3B5c2lnbgAVCGY2NC5jZWlsABYJZjY0LmZsb29yABcJZjY0LnRydW5jABgLZjY0Lm5lYXJlc3QAGQdmNjQubWluABoHZjY0Lm1heAAbCsUBHAcAIAAgAZILBwAgACABkwsHACAAIAGUCwcAIAAgAZULBQAgAJELBQAgAIsLBQAgAIwLBwAgACABmAsFACAAjQsFACAAjgsFACAAjwsFACAAkAsHACAAIAGWCwcAIAAgAZcLBwAgACABoAsHACAAIAGhCwcAIAAgAaILBwAgACABowsFACAAnwsFACAAmQsFACAAmgsHACAAIAGmCwUAIACbCwUAIACcCwUAIACdCwUAIACeCwcAIAAgAaQLBwAgACABpQs= +valid valid forward.wast:1 AGFzbQEAAAABBgFgAX8BfwMDAgAABw4CBGV2ZW4AAANvZGQAAQorAhQAIABBAEYEf0EBBSAAQQFrEAELCxQAIABBAEYEf0EABSAAQQFrEAALCw== +valid valid func.wast:3 AGFzbQEAAAABvAEcYAAAYAAAYAABf2ABfwBgA398fwF/YAAAYAN/fH4AYAJ/fABgBX99fn98AGAAA398fWAAAn98YAAFf31+f3xgBH99fn8Df35/YAABfmAAAX1gAAF8YAJ/fwF/YAJ+fgF+YAJ9fQF9YAJ8fAF8YAZ9f35/fH8BfGAAA39/f2AAAn9+YAF/AX9gAX8Cf35gAX8Cf39gAAJ/f2ARf359fX98fX9/f318fHx/f30QfH1/f39+fX9/fXx8f31/fANiYQAAAAAAAAAAAAAAAAAAAwMGBwgAAAIJCgsBAgMEAgMEAAUMAAINDg8CDQ4PDxAREhMQERITFAAAAg0ODwoVAAIWAAINDg8KFQIWAAINDg8KFQIWAxcYAxcYAxcZGwINDg8HlAlGAWYAAgFnAAQKdHlwZS11c2UtMQAZCnR5cGUtdXNlLTIAGgp0eXBlLXVzZS0zABsKdHlwZS11c2UtNAAcCnR5cGUtdXNlLTUAHQp0eXBlLXVzZS02AB4KdHlwZS11c2UtNwAfD2xvY2FsLWZpcnN0LWkzMgAkD2xvY2FsLWZpcnN0LWk2NAAlD2xvY2FsLWZpcnN0LWYzMgAmD2xvY2FsLWZpcnN0LWY2NAAnEGxvY2FsLXNlY29uZC1pMzIAKBBsb2NhbC1zZWNvbmQtaTY0ACkQbG9jYWwtc2Vjb25kLWYzMgAqEGxvY2FsLXNlY29uZC1mNjQAKwtsb2NhbC1taXhlZAAsD3BhcmFtLWZpcnN0LWkzMgAtD3BhcmFtLWZpcnN0LWk2NAAuD3BhcmFtLWZpcnN0LWYzMgAvD3BhcmFtLWZpcnN0LWY2NAAwEHBhcmFtLXNlY29uZC1pMzIAMRBwYXJhbS1zZWNvbmQtaTY0ADIQcGFyYW0tc2Vjb25kLWYzMgAzEHBhcmFtLXNlY29uZC1mNjQANAtwYXJhbS1taXhlZAA1BWVtcHR5ADYKdmFsdWUtdm9pZAA3CXZhbHVlLWkzMgA4CXZhbHVlLWk2NAA5CXZhbHVlLWYzMgA6CXZhbHVlLWY2NAA7DXZhbHVlLWkzMi1mNjQAPBF2YWx1ZS1pMzItaTMyLWkzMgA9EHZhbHVlLWJsb2NrLXZvaWQAPg92YWx1ZS1ibG9jay1pMzIAPxN2YWx1ZS1ibG9jay1pMzItaTY0AEAMcmV0dXJuLWVtcHR5AEEKcmV0dXJuLWkzMgBCCnJldHVybi1pNjQAQwpyZXR1cm4tZjMyAEQKcmV0dXJuLWY2NABFDnJldHVybi1pMzItZjY0AEYScmV0dXJuLWkzMi1pMzItaTMyAEcQcmV0dXJuLWJsb2NrLWkzMgBIFHJldHVybi1ibG9jay1pMzItaTY0AEkLYnJlYWstZW1wdHkASglicmVhay1pMzIASwlicmVhay1pNjQATAlicmVhay1mMzIATQlicmVhay1mNjQATg1icmVhay1pMzItZjY0AE8RYnJlYWstaTMyLWkzMi1pMzIAUA9icmVhay1ibG9jay1pMzIAURNicmVhay1ibG9jay1pMzItaTY0AFIRYnJlYWstYnJfaWYtZW1wdHkAUw9icmVhay1icl9pZi1udW0AVBNicmVhay1icl9pZi1udW0tbnVtAFUUYnJlYWstYnJfdGFibGUtZW1wdHkAVhJicmVhay1icl90YWJsZS1udW0AVxZicmVhay1icl90YWJsZS1udW0tbnVtAFgbYnJlYWstYnJfdGFibGUtbmVzdGVkLWVtcHR5AFkZYnJlYWstYnJfdGFibGUtbmVzdGVkLW51bQBaHWJyZWFrLWJyX3RhYmxlLW5lc3RlZC1udW0tbnVtAFsJbGFyZ2Utc2lnAFwOaW5pdC1sb2NhbC1pMzIAXQ5pbml0LWxvY2FsLWk2NABeDmluaXQtbG9jYWwtZjMyAF8OaW5pdC1sb2NhbC1mNjQAYAq3BmECAAsCAAsCAAsCAAsCAAsCAAsCAAsEAQF/CwQBAX8LCAMBfwF8AX4LBgIBfwF8CwwFAX8BfQF+AX8BfAsCAAsCAAsCAAsCAAsCAAsCAAsCAAsCAAsCAAsDAAALAwAACwMAAAsDAAALAgALBABBAAsCAAsEAEEACwQAQQALAgALBABBAAsCAAsCAAsQBgF9AX8BfgF/AXwBfwAACxAGAX0BfwF+AX8BfAF/AAALBgECfyAACwYBAn4gAAsGAQJ9IAALBgECfCAACwYBAn8gAQsGAQJ+IAELBgECfSABCwYBAnwgAQsoBgF9AX8BfgF/AXwBfyAAjBogAUUaIAJQGiADRRogBJoaIAVFGiAECwQAIAALBAAgAAsEACAACwQAIAALBAAgAQsEACABCwQAIAELBAAgAQscACAAjBogAUUaIAJQGiADRRogBJoaIAVFGiAECwIACwQAEAALBQBBzQALBQBC4TwLBwBDZmabQgsLAEThehSuR3FTQAsOAEHNAEQAAAAAAAAcQAsIAEEBQQJBAwsJAAJAEAAQAAsLCgACfxAAQc0ACwsLAAIWEABBAUICCwsDAA8LBgBBzgAPCwYAQsY9DwsIAENmZp1CDwsMAERSuB6F67FTQA8LDwBBzgBEUrgeheuxU0APCwkAQQFBAkEDDwsLAAJ/EABBzQALDwsMAAIWEABBAUICCw8LBAAMAAsHAEHPAAwACwcAQqs+DAALCQBDzcyfQgwACw0ARMP1KFyP8lNADAALEABBzwBEw/UoXI/yU0AMAAsKAEEBQQJBAwwACwwAAn8QAEHNAAsMAAsNAAIWEABBAUICCwwACwYAIAANAAsLAEEyIAANABpBMwsQAEEyQjMgAA0AGhpBM0I0CwkAIAAOAgAAAAsMAEEyIAAOAQAAQTMLEABBMkIzIAAOAQAAQTNCNAsMAAJAIAAOAgABAAsLEwACf0EyIAAOAgABAEEzC0ECagsXAAIaQTJBMyAADgIAAQBBM0F9C2pBNAsiACAFIAIgACAIIAcgASADIAkgBCAGIA0gCyAPIBAgDiAMCwYBAX8gAAsGAQF+IAALBgEBfSAACwYBAXwgAAs= +valid valid func.wast:422 AGFzbQEAAAABDANgAX8AYAABfGAAAAMGBQEAAAECCikFCwBEAAAAAAAAAAALAgALAgALCwBEAAAAAAAAAAALCQBBABACEAMaCw== +invalid invalid func.wast:436 AGFzbQEAAAABCQJgAX8AYAABfAMFBAEAAQIKHwQLAEQAAAAAAAAAAAsCAAsLAEQAAAAAAADwPwsCAAs= +valid valid func.wast:459 AGFzbQEAAAABCgJgAAF/YAF/AX8DBQQBAQEABw0DAWYAAAFnAAIBcAADCiEEBgEBfyABCwYBAX8gAQsGACAAEAELCgEBf0EqIQAgAAs= +valid valid func.wast:488 AGFzbQEAAAABHQRgAABgAABgCH5+fH58fn1/AGAIfH58fnx+fX8AAwwLAAMAAwMCAgAAAAAEBQFwAQcHB3cEGXNpZ25hdHVyZS1leHBsaWNpdC1yZXVzZWQABxlzaWduYXR1cmUtaW1wbGljaXQtcmV1c2VkAAgcc2lnbmF0dXJlLWV4cGxpY2l0LWR1cGxpY2F0ZQAJHHNpZ25hdHVyZS1pbXBsaWNpdC1kdXBsaWNhdGUACgkNAQBBAAsHBAIBBAAFBgqFAgsCAAsCAAsCAAsCAAsCAAsCAAsCAAsMAEEBEQAAQQQRAAALiQEARAAAAAAAAAAAQgBEAAAAAAAAAABCAEQAAAAAAAAAAEIAQwAAAABBAEEAEQMARAAAAAAAAAAAQgBEAAAAAAAAAABCAEQAAAAAAAAAAEIAQwAAAABBAEECEQMARAAAAAAAAAAAQgBEAAAAAAAAAABCAEQAAAAAAAAAAEIAQwAAAABBAEEDEQMACwcAQQERAQALTgBCAEIARAAAAAAAAAAAQgBEAAAAAAAAAABCAEMAAAAAQQBBBRECAEIAQgBEAAAAAAAAAABCAEQAAAAAAAAAAEIAQwAAAABBAEEGEQIACw== +invalid invalid func.wast:634 AGFzbQEAAAABBQFgAAF+AwIBAAoIAQYBAX8gAAs= +invalid invalid func.wast:638 AGFzbQEAAAABBAFgAAADAgEACgkBBwEBfSAARQs= +invalid invalid func.wast:642 AGFzbQEAAAABBAFgAAADAgEACgsBCQIBfAF+IAGaCw== +invalid invalid func.wast:650 AGFzbQEAAAABBgFgAX8BfgMCAQAKBgEEACAACw== +invalid invalid func.wast:654 AGFzbQEAAAABBQFgAX0AAwIBAAoHAQUAIABFCw== +invalid invalid func.wast:658 AGFzbQEAAAABBgFgAnx+AAMCAQAKBwEFACABmgs= +invalid invalid func.wast:666 AGFzbQEAAAABBQFgAAF/AwIBAAoEAQIACw== +invalid invalid func.wast:670 AGFzbQEAAAABBQFgAAF+AwIBAAoEAQIACw== +invalid invalid func.wast:674 AGFzbQEAAAABBQFgAAF9AwIBAAoEAQIACw== +invalid invalid func.wast:678 AGFzbQEAAAABBQFgAAF8AwIBAAoEAQIACw== +invalid invalid func.wast:682 AGFzbQEAAAABBgFgAAJ8fwMCAQAKBAECAAs= +invalid invalid func.wast:687 AGFzbQEAAAABBQFgAAF/AwIBAAoFAQMAAQs= +invalid invalid func.wast:693 AGFzbQEAAAABBgFgAAJ/fwMCAQAKBQEDAAEL +invalid invalid func.wast:699 AGFzbQEAAAABBAFgAAADAgEACgYBBABBAAs= +invalid invalid func.wast:705 AGFzbQEAAAABBAFgAAADAgEACggBBgBBAEIACw== +invalid invalid func.wast:711 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcAQwAAAAAL +invalid invalid func.wast:717 AGFzbQEAAAABBgFgAAJ9fQMCAQAKCQEHAEMAAAAACw== +invalid invalid func.wast:723 AGFzbQEAAAABBQFgAAF9AwIBAAoOAQwAQwAAAABDAAAAAAs= +invalid invalid func.wast:730 AGFzbQEAAAABBQFgAAF/AwIBAAoFAQMADws= +invalid invalid func.wast:736 AGFzbQEAAAABBgFgAAJ/fwMCAQAKBQEDAA8L +invalid invalid func.wast:742 AGFzbQEAAAABBQFgAAF/AwIBAAoGAQQAAQ8L +invalid invalid func.wast:748 AGFzbQEAAAABBgFgAAJ/fgMCAQAKBgEEAAEPCw== +invalid invalid func.wast:754 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgAPCw== +invalid invalid func.wast:760 AGFzbQEAAAABBgFgAAJ+fgMCAQAKBwEFAEIADws= +invalid invalid func.wast:767 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAD0EBCw== +invalid invalid func.wast:773 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCQEHAA9BAUECCw== +invalid invalid func.wast:779 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCQEHAEEBD0ECCw== +invalid invalid func.wast:785 AGFzbQEAAAABBQFgAAF/AwIBAAoIAQYAAQ9BAQs= +invalid invalid func.wast:791 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCAEGAAEPQQEL +invalid invalid func.wast:797 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcAQgEPQQEL +invalid invalid func.wast:803 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCwEJAEIBD0EBQQIL +invalid invalid func.wast:809 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAQgEPQQEPCw== +invalid invalid func.wast:815 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDAEKAEEBD0EBQQIPCw== +invalid invalid func.wast:822 AGFzbQEAAAABBQFgAAF/AwIBAAoGAQQADAAL +invalid invalid func.wast:828 AGFzbQEAAAABBgFgAAJ/fwMCAQAKBgEEAAwACw== +invalid invalid func.wast:834 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAQwAAAAAMAAs= +invalid invalid func.wast:840 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCAEGAEEADAAL +invalid invalid func.wast:846 AGFzbQEAAAABBQFgAAF/AwIBAAoIAQYADABBAQs= +invalid invalid func.wast:852 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCgEIAAwAQQFBAgs= +invalid invalid func.wast:858 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAQgEMAEEBCw== +invalid invalid func.wast:864 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDAEKAEEBDABBAUECCw== +invalid invalid func.wast:870 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgEMAEEBDAAL +invalid invalid func.wast:877 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAAkAMAQtBAQwACw== +invalid invalid func.wast:883 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDwENAAJADAELQQFBAgwACw== +invalid invalid func.wast:889 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAkABDAELQQEMAAs= +invalid invalid func.wast:895 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEAEOAAJAAQwBC0EBQQIMAAs= +invalid invalid func.wast:901 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAkBCAQwBC0EBDAAL +invalid invalid func.wast:907 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEQEPAAJ/QQEMAQtBAUECDAAL +valid valid func_ptrs.wast:1 AGFzbQEAAAABGwdgAABgAABgAABgAAF/YAABf2ABfwF/YAF/AAIWAQhzcGVjdGVzdAlwcmludF9pMzIABgMHBgABBAUFBgccBANvbmUAAwN0d28ABAV0aHJlZQAFBGZvdXIABgojBgIACwIACwQAQQ0LBwAgAEEBagsHACAAQQJrCwYAIAAQAAs= +invalid invalid func_ptrs.wast:32 AGFzbQEAAAAJBgEAQQALAA== +invalid invalid func_ptrs.wast:48 AGFzbQEAAAADAgEqCgQBAgAL +invalid invalid func_ptrs.wast:49 AGFzbQEAAAACFgEIc3BlY3Rlc3QJcHJpbnRfaTMyACs= +valid valid func_ptrs.wast:51 AGFzbQEAAAABDgNgAAF/YAABf2ABfwF/AwgHAAAAAQECAgQFAXABBwcHEQIFY2FsbHQABQVjYWxsdQAGCQ0BAEEACwcAAQIDBAACCioHBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwcAIAARAAALBwAgABEBAAs= +valid valid func_ptrs.wast:93 AGFzbQEAAAABCgJgAAF/YAF/AX8DBAMAAAEEBQFwAQICBwkBBWNhbGx0AAIJCAEAQQALAgABChMDBABBAQsEAEECCwcAIAARAAAL +valid valid global.wast:3 AGFzbQEAAAABNw1gAn9/AX9gAAF/YAABfmAAAW9gAX8AYAF+AGABbwBgAAF9YAABfGABfQBgAXwAYAAAYAF/AX8CLwIIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAhzcGVjdGVzdApnbG9iYWxfaTY0A34AAzMyAQIDAwECAQIEBQYHCAcICQoLAQEBAQEBAQEBAQEBAQABAQELCwEBDAEBCwEMDAEBAQEEBQFwAQEBBQMBAAEGVg1/AEF+C30AQwAAQMALfABEAAAAAAAAEMALfgBCewt/AUF0C30BQwAAUMELfAFEAAAAAAAALMALfgFCcQt/ACMAC34AIwELbwDQbwtvAdBvC3AA0HALB7EFLwVnZXQtYQAABWdldC1iAAEFZ2V0LXIAAgZnZXQtbXIAAwVnZXQteAAEBWdldC15AAUGZ2V0LXoxAAYGZ2V0LXoyAAcFc2V0LXgACAVzZXQteQAJBnNldC1tcgAKBWdldC0zAAsFZ2V0LTQADAVnZXQtNwANBWdldC04AA4Fc2V0LTcADwVzZXQtOAAQD2FzLXNlbGVjdC1maXJzdAASDWFzLXNlbGVjdC1taWQAEw5hcy1zZWxlY3QtbGFzdAAUDWFzLWxvb3AtZmlyc3QAFQthcy1sb29wLW1pZAAWDGFzLWxvb3AtbGFzdAAXD2FzLWlmLWNvbmRpdGlvbgAYCmFzLWlmLXRoZW4AGQphcy1pZi1lbHNlABoOYXMtYnJfaWYtZmlyc3QAGw1hcy1icl9pZi1sYXN0ABwRYXMtYnJfdGFibGUtZmlyc3QAHRBhcy1icl90YWJsZS1sYXN0AB4WYXMtY2FsbF9pbmRpcmVjdC1maXJzdAAgFGFzLWNhbGxfaW5kaXJlY3QtbWlkACEVYXMtY2FsbF9pbmRpcmVjdC1sYXN0ACIOYXMtc3RvcmUtZmlyc3QAIw1hcy1zdG9yZS1sYXN0ACQPYXMtbG9hZC1vcGVyYW5kACUUYXMtbWVtb3J5Lmdyb3ctdmFsdWUAJg1hcy1jYWxsLXZhbHVlACgPYXMtcmV0dXJuLXZhbHVlACkPYXMtZHJvcC1vcGVyYW5kACoLYXMtYnItdmFsdWUAKxJhcy1sb2NhbC5zZXQtdmFsdWUALBJhcy1sb2NhbC50ZWUtdmFsdWUALRNhcy1nbG9iYWwuc2V0LXZhbHVlAC4QYXMtdW5hcnktb3BlcmFuZAAvEWFzLWJpbmFyeS1vcGVyYW5kADASYXMtY29tcGFyZS1vcGVyYW5kADEJBwEAQQALAR8KsAMyBAAjAgsEACMFCwQAIwwLBAAjDQsEACMGCwQAIwkLBAAjCgsEACMLCwYAIAAkBgsGACAAJAkLBgAgACQNCwQAIwMLBAAjBAsEACMHCwQAIwgLBgAgACQHCwYAIAAkCAsCAAsJACMGQQJBAxsLCQBBAiMGQQMbCwkAQQJBAyMGGwsLAAN/IwYQERARCwsLAAN/EBEjBhARCwsLAAN/EBEQESMGCwsQACMGBH8QEUECBRARQQMLCwwAQQEEfyMGBUECCwsMAEEABH9BAgUjBgsLDgACfyMGQQINAEEDDwsLDgACf0ECIwYNAEEDDwsLDQACfyMGQQIOAQAACwsNAAJ/QQIjBg4BAAALCwQAIAALDgACfyMGQQJBABEAAAsLDgACf0ECIwZBABEAAAsLDgACf0ECQQAjBhEAAAsLCQAjBkEBNgIACwkAQQAjBjYCAAsHACMGKAIACwYAIwZAAAsEACAACwYAIwYQJwsFACMGDwsFACMGGgsJAAJ/IwYMAAsLCAAjBiEAIAALBgAjBiIACwgAIwYkBiMGCwUAIwZFCwcAIwYjBmwLBwAjAEEBSws= +invalid invalid global.wast:273 AGFzbQEAAAABBAFgAAADAgEABgkBfQBDAAAAAAsKCwEJAEMAAIA/JAAL +invalid invalid global.wast:278 AGFzbQEAAAABBAFgAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAMCAQAKCAEGAEEBJAAL +valid valid global.wast:283 AGFzbQEAAAAGCQF9AUMAAAAACwcFAQFhAwA= +invalid invalid global.wast:287 AGFzbQEAAAAGCgF9AEMAAAAAjAs= +invalid invalid global.wast:292 AGFzbQEAAAAGBgF9ACAACw== +invalid invalid global.wast:297 AGFzbQEAAAAGCgF9AEMAAIA/jAs= +invalid invalid global.wast:302 AGFzbQEAAAAGBwF/AEEAAQs= +invalid invalid global.wast:307 AGFzbQEAAAAGBwF/AEEAaAs= +invalid invalid global.wast:312 AGFzbQEAAAAGBQF/AAEL +invalid invalid global.wast:317 AGFzbQEAAAAGCQF/AEMAAAAACw== +invalid invalid global.wast:322 AGFzbQEAAAAGCAF/AEEAQQAL +invalid invalid global.wast:327 AGFzbQEAAAAGBAF/AAs= +invalid invalid global.wast:332 AGFzbQEAAAACBgEAAANvAAYGAXAAIwAL +invalid invalid global.wast:337 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABggBfwAjACMACw== +invalid invalid global.wast:342 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABggBfwBBACMACw== +invalid invalid global.wast:347 AGFzbQEAAAAGBgF/ACMACw== +invalid valid global.wast:352 AGFzbQEAAAAGCwJ/AEEAC38AIwAL +invalid invalid global.wast:361 AGFzbQEAAAAGCwJ/ACMBC38AQQAL +invalid invalid global.wast:366 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38ABgYBfwAjAgs= +invalid invalid global.wast:371 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWkzMgN/AQYGAX8AIwAL +valid valid global.wast:375 AGFzbQEAAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AA== +malformed invalid global.wast:379 AGFzbQEAAAACmICAgAABCHNwZWN0ZXN0Cmdsb2JhbF9pMzIDfwQ= +malformed invalid global.wast:392 AGFzbQEAAAACmICAgAABCHNwZWN0ZXN0Cmdsb2JhbF9pMzIDf/8= +valid valid global.wast:405 AGFzbQEAAAAGBgF/AEEACw== +malformed invalid global.wast:409 AGFzbQEAAAAGhoCAgAABfwRBAAs= +malformed invalid global.wast:421 AGFzbQEAAAAGhoCAgAABf/9BAAs= +invalid invalid global.wast:435 AGFzbQEAAAABBQFgAAF/AwIBAAoGAQQAIwAL +invalid invalid global.wast:440 AGFzbQEAAAABBQFgAAF/AwIBAAYGAX8AQQALCgYBBAAjAQs= +invalid invalid global.wast:448 AGFzbQEAAAABBQFgAAF/AhgBCHNwZWN0ZXN0Cmdsb2JhbF9pMzIDfwADAgEACgYBBAAjAQs= +invalid invalid global.wast:456 AGFzbQEAAAABBQFgAAF/AhgBCHNwZWN0ZXN0Cmdsb2JhbF9pMzIDfwADAgEABgYBfwBBAAsKBgEEACMCCw== +invalid invalid global.wast:466 AGFzbQEAAAABBAFgAAADAgEACggBBgBBACQACw== +invalid invalid global.wast:471 AGFzbQEAAAABBAFgAAADAgEABgYBfwBBAAsKCAEGAEEAJAEL +invalid invalid global.wast:479 AGFzbQEAAAABBAFgAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAMCAQAKCAEGAEEAJAEL +invalid invalid global.wast:487 AGFzbQEAAAABBAFgAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAMCAQAGBgF/AEEACwoIAQYAQQAkAgs= +invalid invalid global.wast:497 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKBgEEACQACw== +invalid invalid global.wast:506 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKCwEJAEEAAkAkAAsL +invalid invalid global.wast:516 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKCwEJAEEAA0AkAAsL +invalid invalid global.wast:526 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKDQELAEEAQQAEQCQACws= +invalid invalid global.wast:536 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKEAEOAEEAQQAEf0EABSQACws= +invalid invalid global.wast:546 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKDQELAEEAAkAkAAwACws= +invalid invalid global.wast:556 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKDQELAEEAAkAkAA0ACws= +invalid invalid global.wast:566 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKDgEMAEEAAkAkAA4AAAsL +invalid invalid global.wast:576 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKBwEFACQADws= +invalid invalid global.wast:585 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKCwEJACQAQQFBAhsL +invalid invalid global.wast:594 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBgYBfwFBAAsKDQIGACQAEAELBAAgAAs= +invalid invalid global.wast:604 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQYGAX8BQQALCQcBAEEACwEAChMCBAAgAAsMAAJ/JABBABEAAAsL +valid valid i32.wast:3 AGFzbQEAAAABDAJgAn9/AX9gAX8BfwMgHwAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAH3gEfA2FkZAAAA3N1YgABA211bAACBWRpdl9zAAMFZGl2X3UABAVyZW1fcwAFBXJlbV91AAYDYW5kAAcCb3IACAN4b3IACQNzaGwACgVzaHJfcwALBXNocl91AAwEcm90bAANBHJvdHIADgNjbHoADwNjdHoAEAZwb3BjbnQAEQlleHRlbmQ4X3MAEgpleHRlbmQxNl9zABMDZXF6ABQCZXEAFQJuZQAWBGx0X3MAFwRsdF91ABgEbGVfcwAZBGxlX3UAGgRndF9zABsEZ3RfdQAcBGdlX3MAHQRnZV91AB4K7QEfBwAgACABagsHACAAIAFrCwcAIAAgAWwLBwAgACABbQsHACAAIAFuCwcAIAAgAW8LBwAgACABcAsHACAAIAFxCwcAIAAgAXILBwAgACABcwsHACAAIAF0CwcAIAAgAXULBwAgACABdgsHACAAIAF3CwcAIAAgAXgLBQAgAGcLBQAgAGgLBQAgAGkLBQAgAMALBQAgAMELBQAgAEULBwAgACABRgsHACAAIAFHCwcAIAAgAUgLBwAgACABSQsHACAAIAFMCwcAIAAgAU0LBwAgACABSgsHACAAIAFLCwcAIAAgAU4LBwAgACABTws= +invalid invalid i32.wast:444 AGFzbQEAAAABBAFgAAADAgEACgYBBABFGgs= +invalid invalid i32.wast:452 AGFzbQEAAAABBAFgAAADAgEACgsBCQBBAAJARRoLCw== +invalid invalid i32.wast:461 AGFzbQEAAAABBAFgAAADAgEACgsBCQBBAANARRoLCw== +invalid invalid i32.wast:470 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAEEABEBFGgsL +invalid invalid i32.wast:479 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAEEABH9BAAVFCxoL +invalid invalid i32.wast:488 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAAJARQwAGgsL +invalid invalid i32.wast:497 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAAJARUEBDQAaCws= +invalid invalid i32.wast:506 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAAJARQ4AABoLCw== +invalid invalid i32.wast:515 AGFzbQEAAAABBAFgAAADAgEACgcBBQBFDxoL +invalid invalid i32.wast:523 AGFzbQEAAAABBAFgAAADAgEACgsBCQBFQQFBAhsaCw== +invalid invalid i32.wast:531 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABCg0CBgBFEAEaCwQAIAAL +invalid invalid i32.wast:540 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoTAgQAIAALDAACf0VBABEAABoLCw== +invalid invalid i32.wast:556 AGFzbQEAAAABBAFgAAADAgEACgwBCgEBf0UhACAAGgs= +invalid invalid i32.wast:565 AGFzbQEAAAABBAFgAAADAgEACgoBCAEBf0UiABoL +invalid invalid i32.wast:574 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKCgEIAEUkACMAGgs= +invalid invalid i32.wast:583 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCAEGAEVAABoL +invalid invalid i32.wast:592 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCQEHAEUoAgAaCw== +invalid invalid i32.wast:601 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEVBATYCAAs= +invalid invalid i32.wast:611 AGFzbQEAAAABBAFgAAADAgEACgYBBABqGgs= +invalid invalid i32.wast:619 AGFzbQEAAAABBAFgAAADAgEACggBBgBBAGoaCw== +invalid invalid i32.wast:627 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAEEAAkBqGgsL +invalid invalid i32.wast:636 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAAJAQQBqGgsL +invalid invalid i32.wast:645 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAEEAA0BqGgsL +invalid invalid i32.wast:654 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAANAQQBqGgsL +invalid invalid i32.wast:663 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEAQQBqBEAaCws= +invalid invalid i32.wast:672 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAEEAQQAEQGoFGgsL +invalid invalid i32.wast:681 AGFzbQEAAAABBAFgAAADAgEAChUBEwBBAEEAQQAEf0EABWpBAAsaGgs= +invalid invalid i32.wast:691 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAEEABH9BAAVqCxoL +invalid invalid i32.wast:701 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEAAkBqDAAaCws= +invalid invalid i32.wast:710 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAAJAQQBqDAAaCws= +invalid invalid i32.wast:719 AGFzbQEAAAABBAFgAAADAgEAChEBDwBBAEEAAkBqQQENABoLCw== +invalid invalid i32.wast:728 AGFzbQEAAAABBAFgAAADAgEAChEBDwBBAAJAQQBqQQENABoLCw== +invalid invalid i32.wast:737 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAEEAAkBqDgAAGgsL +invalid invalid i32.wast:746 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAAJAQQBqDgAAGgsL +invalid invalid i32.wast:755 AGFzbQEAAAABBAFgAAADAgEACgcBBQBqDxoL +invalid invalid i32.wast:763 AGFzbQEAAAABBAFgAAADAgEACgkBBwBBAGoPGgs= +invalid invalid i32.wast:771 AGFzbQEAAAABBAFgAAADAgEACgsBCQBqQQFBAhsaCw== +invalid invalid i32.wast:779 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAGpBAUECGxoL +invalid invalid i32.wast:787 AGFzbQEAAAABCgJgAABgAn9/AX8DAwIAAQoNAgYAahABGgsEACAACw== +invalid invalid i32.wast:796 AGFzbQEAAAABCgJgAABgAn9/AX8DAwIAAQoPAggAQQBqEAEaCwQAIAAL +invalid invalid i32.wast:805 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoTAgQAIAALDAACf2pBABEAABoLCw== +invalid invalid i32.wast:821 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoVAgQAIAALDgACf0EAakEAEQAAGgsL +invalid invalid i32.wast:837 AGFzbQEAAAABBAFgAAADAgEACgwBCgEBf2ohACAAGgs= +invalid invalid i32.wast:846 AGFzbQEAAAABBAFgAAADAgEACg4BDAEBf0EAaiEAIAAaCw== +invalid invalid i32.wast:855 AGFzbQEAAAABBAFgAAADAgEACgoBCAEBf2oiABoL +invalid invalid i32.wast:864 AGFzbQEAAAABBAFgAAADAgEACgwBCgEBf0EAaiIAGgs= +invalid invalid i32.wast:873 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKCgEIAGokACMAGgs= +invalid invalid i32.wast:882 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKDAEKAEEAaiQAIwAaCw== +invalid invalid i32.wast:891 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCAEGAGpAABoL +invalid invalid i32.wast:900 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAEEAakAAGgs= +invalid invalid i32.wast:909 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCQEHAGooAgAaCw== +invalid invalid i32.wast:918 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAEEAaigCABoL +invalid invalid i32.wast:927 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAGpBATYCAAs= +invalid invalid i32.wast:936 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDAEKAEEBakEANgIACw== +invalid invalid i32.wast:948 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAGoL +invalid invalid i32.wast:949 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHEL +invalid invalid i32.wast:950 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAG0L +invalid invalid i32.wast:951 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAG4L +invalid invalid i32.wast:952 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAGwL +invalid invalid i32.wast:953 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHIL +invalid invalid i32.wast:954 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAG8L +invalid invalid i32.wast:955 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHAL +invalid invalid i32.wast:956 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHcL +invalid invalid i32.wast:957 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHgL +invalid invalid i32.wast:958 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHQL +invalid invalid i32.wast:959 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHUL +invalid invalid i32.wast:960 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHYL +invalid invalid i32.wast:961 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAGsL +invalid invalid i32.wast:962 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAHML +invalid invalid i32.wast:963 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgBFCw== +invalid invalid i32.wast:964 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgBnCw== +invalid invalid i32.wast:965 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgBoCw== +invalid invalid i32.wast:966 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAQgBpCw== +invalid invalid i32.wast:967 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAEYL +invalid invalid i32.wast:968 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAE4L +invalid invalid i32.wast:969 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAE8L +invalid invalid i32.wast:970 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAEoL +invalid invalid i32.wast:971 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAEsL +invalid invalid i32.wast:972 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAEwL +invalid invalid i32.wast:973 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAE0L +invalid invalid i32.wast:974 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAEgL +invalid invalid i32.wast:975 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAEkL +invalid invalid i32.wast:976 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQgBDAAAAAEcL +valid valid i64.wast:3 AGFzbQEAAAABFwRgAn5+AX5gAX4BfmABfgF/YAJ+fgF/AyEgAAAAAAAAAAAAAAAAAAAAAQEBAQEBAgMDAwMDAwMDAwMH6wEgA2FkZAAAA3N1YgABA211bAACBWRpdl9zAAMFZGl2X3UABAVyZW1fcwAFBXJlbV91AAYDYW5kAAcCb3IACAN4b3IACQNzaGwACgVzaHJfcwALBXNocl91AAwEcm90bAANBHJvdHIADgNjbHoADwNjdHoAEAZwb3BjbnQAEQlleHRlbmQ4X3MAEgpleHRlbmQxNl9zABMKZXh0ZW5kMzJfcwAUA2VxegAVAmVxABYCbmUAFwRsdF9zABgEbHRfdQAZBGxlX3MAGgRsZV91ABsEZ3RfcwAcBGd0X3UAHQRnZV9zAB4EZ2VfdQAfCvMBIAcAIAAgAXwLBwAgACABfQsHACAAIAF+CwcAIAAgAX8LBwAgACABgAsHACAAIAGBCwcAIAAgAYILBwAgACABgwsHACAAIAGECwcAIAAgAYULBwAgACABhgsHACAAIAGHCwcAIAAgAYgLBwAgACABiQsHACAAIAGKCwUAIAB5CwUAIAB6CwUAIAB7CwUAIADCCwUAIADDCwUAIADECwUAIABQCwcAIAAgAVELBwAgACABUgsHACAAIAFTCwcAIAAgAVQLBwAgACABVwsHACAAIAFYCwcAIAAgAVULBwAgACABVgsHACAAIAFZCwcAIAAgAVoL +invalid invalid i64.wast:457 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAHwL +invalid invalid i64.wast:458 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIML +invalid invalid i64.wast:459 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAH8L +invalid invalid i64.wast:460 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIAL +invalid invalid i64.wast:461 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAH4L +invalid invalid i64.wast:462 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIQL +invalid invalid i64.wast:463 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIEL +invalid invalid i64.wast:464 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIIL +invalid invalid i64.wast:465 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIkL +invalid invalid i64.wast:466 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIoL +invalid invalid i64.wast:467 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIYL +invalid invalid i64.wast:468 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIcL +invalid invalid i64.wast:469 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIgL +invalid invalid i64.wast:470 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAH0L +invalid invalid i64.wast:471 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAIUL +invalid invalid i64.wast:472 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQBQCw== +invalid invalid i64.wast:473 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQB5Cw== +invalid invalid i64.wast:474 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQB6Cw== +invalid invalid i64.wast:475 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAQQB7Cw== +invalid invalid i64.wast:476 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFEL +invalid invalid i64.wast:477 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFkL +invalid invalid i64.wast:478 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFoL +invalid invalid i64.wast:479 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFUL +invalid invalid i64.wast:480 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFYL +invalid invalid i64.wast:481 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFcL +invalid invalid i64.wast:482 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFgL +invalid invalid i64.wast:483 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFML +invalid invalid i64.wast:484 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFQL +invalid invalid i64.wast:485 AGFzbQEAAAABBQFgAAF+AwIBAAoMAQoAQQBDAAAAAFIL +valid valid if.wast:3 AGFzbQEAAAABXRBgAn9/AX9gAABgAAF/YAF/AGADf3x/A398f2ABfwF/YAF/An9/YAADf35/YAACf39gAAJ9fWABfwN/f35gAAN/f35gAn9/An9/YAN+fn8Cfn9gAn5+AX5gAX4BfgM2NQEDBQYABQUFBQUFBQUFBQUABQUFAwMFBQUFAwUFBQUFBQAFAAUFBQIFCgUFBQUFBQUNDgEBBAUBcAEBAQUDAQABBgYBfwFBCgsHzgYyBWVtcHR5AAEIc2luZ3VsYXIAAgVtdWx0aQADBm5lc3RlZAAED2FzLXNlbGVjdC1maXJzdAAFDWFzLXNlbGVjdC1taWQABg5hcy1zZWxlY3QtbGFzdAAHDWFzLWxvb3AtZmlyc3QACAthcy1sb29wLW1pZAAJDGFzLWxvb3AtbGFzdAAKD2FzLWlmLWNvbmRpdGlvbgALDmFzLWJyX2lmLWZpcnN0AAwNYXMtYnJfaWYtbGFzdAANEWFzLWJyX3RhYmxlLWZpcnN0AA4QYXMtYnJfdGFibGUtbGFzdAAPFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAERRhcy1jYWxsX2luZGlyZWN0LW1pZAASFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAATDmFzLXN0b3JlLWZpcnN0ABQNYXMtc3RvcmUtbGFzdAAVFGFzLW1lbW9yeS5ncm93LXZhbHVlABYNYXMtY2FsbC12YWx1ZQAYD2FzLXJldHVybi12YWx1ZQAZD2FzLWRyb3Atb3BlcmFuZAAaC2FzLWJyLXZhbHVlABsSYXMtbG9jYWwuc2V0LXZhbHVlABwSYXMtbG9jYWwudGVlLXZhbHVlAB0TYXMtZ2xvYmFsLnNldC12YWx1ZQAeD2FzLWxvYWQtb3BlcmFuZAAfEGFzLXVuYXJ5LW9wZXJhbmQAIBFhcy1iaW5hcnktb3BlcmFuZAAhD2FzLXRlc3Qtb3BlcmFuZAAiEmFzLWNvbXBhcmUtb3BlcmFuZAAjEmFzLWJpbmFyeS1vcGVyYW5kcwAkE2FzLWNvbXBhcmUtb3BlcmFuZHMAJRFhcy1taXhlZC1vcGVyYW5kcwAmCmJyZWFrLWJhcmUAJwticmVhay12YWx1ZQAoEWJyZWFrLW11bHRpLXZhbHVlACkFcGFyYW0AKgZwYXJhbXMAKwlwYXJhbXMtaWQALAtwYXJhbS1icmVhawAtDHBhcmFtcy1icmVhawAuD3BhcmFtcy1pZC1icmVhawAvB2VmZmVjdHMAMBJhZGQ2NF91X3dpdGhfY2FycnkAMRFhZGQ2NF91X3NhdHVyYXRlZAAyCHR5cGUtdXNlADMSYXR5cGljYWwtY29uZGl0aW9uADQJBwEAQQALARAKxgs1AgALFgAgAARACyAABEALIAAEQAsgAARACwsaACAABEABCyAABEABBQELIAAEf0EHBUEICwtnACAABEAQABAAEAALIAAEQAUQABAAEAALIAAEfxAAEABBCBAABRAAEABBCRAACyAABAcQABAAQQEQABAAEABCAhAAEAAQAEEDEAAFEAAQAEF/EAAQABAAQn4QABAAEABBfRAACxoaC1IAIAAEfyABBEAQAAJACwELIAEEQAUQAAJACwELIAEEfxAAQQkFEABBCgsFIAEEQBAAAkALAQsgAQRABRAAAkALAQsgAQR/EABBCgUQAEELCwsLFQAgAAR/EABBAQUQAEEAC0ECQQMbCxUAQQIgAAR/EABBAQUQAEEAC0EDGwsVAEECQQMgAAR/EABBAQUQAEEACxsLFwADfyAABH8QAEEBBRAAQQALEAAQAAsLFwADfxAAIAAEfxAAQQEFEABBAAsQAAsLFwADfxAAEAAgAAR/EABBAQUQAEEACwsLGAAgAAR/QQEFQQALBH8QAEECBRAAQQMLCxoAAn8gAAR/EABBAQUQAEEAC0ECDQBBAw8LCxoAAn9BAiAABH8QAEEBBRAAQQALDQBBAw8LCxkAAn8gAAR/EABBAQUQAEEAC0ECDgEAAAsLGQACf0ECIAAEfxAAQQEFEABBAAsOAQAACwsEACAACxoAAn8gAAR/EABBAQUQAEEAC0ECQQARAAALCxoAAn9BAiAABH8QAEEBBRAAQQALQQARAAALCxoAAn9BAkEAIAAEfxAAQQEFEABBAAsRAAALCxUAIAAEfxAAQQEFEABBAAtBAjYCAAsVAEECIAAEfxAAQQEFEABBAAs2AgALDgAgAAR/QQEFQQALQAALBAAgAAsOACAABH9BAQVBAAsQFwsNACAABH9BAQVBAAsPCw0AIAAEf0EBBUEACxoLEQACfyAABH9BAQVBAAsMAAsLEgEBfyAABH9BAQVBAAshACAACw4AIAAEf0EBBUEACyIACxAAIAAEf0EBBUEACyQAIwALDwAgAAR/QQsFQQoLKAIACxEAIAAEfxAAQQ0FEABBcwtoCx8AIAAEfxAAQQMFEABBfQsgAQR/EABBBAUQAEF7C2wLEQAgAAR/EABBDQUQAEEAC0ULKwAgAAR9EABDAABAQAUQAEMAAEDACyABBH0QAEMAAIBABRAAQwAAgMALXgsZACAABAgQAEEDEABBBAUQAEEDEABBfAtsCyUAIAAECRAAQwAAQEAQAEMAAEBABRAAQwAAAMAQAEMAAEDAC14LHAAgAAQIEABBAxAAQQQFEABBfRAAQXwLQQVqbAtnAEEBBEAMAAALQQEEQAwAAAUAC0EABEAABQwAAAtBAQRAQQENAAALQQEEQEEBDQAABQALQQAEQAAFQQENAAALQQEEQEEADgAAAAtBAQRAQQAOAAAABQALQQAEQAAFQQAOAAAAC0ETCxQAIAAEf0ESDABBEwVBFQwAQRQLCyQAIAAEC0ESQW5CEgwAQRNBbUITBUFuQRJCbgwAQW1BE0JtCwsQAEEBIAAEBUECagVBfmoLCw4AQQFBAiAABABqBWsLCwwAQQFBAiAABAwLagsUAEEBIAAEBUECagwABUF+agwACwsSAEEBQQIgAAQAagwABWsMAAsLDgBBAUECIAAEDAwAC2oLUQEBfwJ/QQEhASAACwRAIAFBA2whASABQQVrIQEgAUEHbCEBDAAgAUHkAGwhAQUgAUEFbCEBIAFBB2shASABQQNsIQEMACABQegHbCEBCyABCxcBAX4gACABfCACrXwhAyADIAMgAFQPCxAAIAAgAUEAEDEEDxpCfwsLVQBBAQRAC0EBBH9BAAVBAgtBAQQDGgUaC0EARAAAAAAAAAAAQQBBAQQECxoaGkEBBH9BAAVBAgtBAQQDGgUaC0EARAAAAAAAAAAAQQBBAQQECxoaGgsNAEEABEALQQFFBEALCw== +invalid invalid if.wast:836 AGFzbQEAAAABBAFgAAADAgEACgsBCQBBAUEABEALCw== +invalid invalid if.wast:844 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcAQQAEQAsL +invalid invalid if.wast:848 AGFzbQEAAAABBQFgAAF+AwIBAAoJAQcAQQAEQAsL +invalid invalid if.wast:852 AGFzbQEAAAABBQFgAAF9AwIBAAoJAQcAQQAEQAsL +invalid invalid if.wast:856 AGFzbQEAAAABBQFgAAF8AwIBAAoJAQcAQQAEQAsL +invalid invalid if.wast:878 AGFzbQEAAAABBAFgAAADAgEACgsBCQBBAQRAQQELCw== +invalid invalid if.wast:890 AGFzbQEAAAABBAFgAAADAgEACgwBCgBBAQRABUEBCws= +invalid invalid if.wast:896 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAQRAQQEFQQELCw== +invalid invalid if.wast:903 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAQRAQQFBAgsL +invalid invalid if.wast:915 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAQRABUEBQQILCw== +invalid invalid if.wast:921 AGFzbQEAAAABBAFgAAADAgEAChIBEABBAQRAQQFBAgVBAkEBCws= +invalid invalid if.wast:928 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQQEEfwVBAAsL +invalid invalid if.wast:934 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAQQEEf0EACws= +invalid invalid if.wast:940 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcAQQEEfwsL +invalid invalid if.wast:947 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDgEMAEEBBAAFQQBBAgsL +invalid invalid if.wast:953 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDQELAEEBBABBAEEBCws= +invalid invalid if.wast:959 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCQEHAEEBBAALCw== +invalid invalid if.wast:966 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAQQEEf0EBCws= +invalid invalid if.wast:972 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDQELAEEBBABBAUEBCws= +invalid invalid if.wast:979 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAQQEEfwEFQQALCw== +invalid invalid if.wast:985 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAQQEEf0EABQELCw== +invalid invalid if.wast:991 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQQEEfwEFAQsL +invalid invalid if.wast:998 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDwENAEEBBAABBUEAQQALCw== +invalid invalid if.wast:1004 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDwENAEEBBABBAEEABQELCw== +invalid invalid if.wast:1010 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDAEKAEEBBAABBQELCw== +invalid invalid if.wast:1017 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAQQEEf0IBBUEBCws= +invalid invalid if.wast:1023 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAQQEEf0EBBUIBCws= +invalid invalid if.wast:1029 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAQQEEf0IBBUIBCws= +invalid invalid if.wast:1036 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEAEOAEEBBABBAQVBAUEBCws= +invalid invalid if.wast:1042 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEAEOAEEBBABBAUEBBUEBCws= +invalid invalid if.wast:1048 AGFzbQEAAAABBgFgAAJ/fwMCAQAKDgEMAEEBBABBAQVBAQsL +invalid invalid if.wast:1055 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEgEQAEEAQQEEAEEBBUEBQQELCw== +invalid invalid if.wast:1062 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEgEQAEEAQQEEAEEBQQEFQQELCw== +invalid invalid if.wast:1069 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEAEOAEEAQQEEAEEBBUEBCws= +invalid invalid if.wast:1077 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AQQEEf0EBQQEFQQELCw== +invalid invalid if.wast:1083 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AQQEEf0EBBUEBQQELCw== +invalid invalid if.wast:1089 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAQQEEf0EBQQEFQQFBAQsL +invalid invalid if.wast:1096 AGFzbQEAAAABBQFgAAF/AwIBAAoVARMAQQEEf0IBBUQAAAAAAADwPwsL +invalid invalid if.wast:1102 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEgEQAEEBBABBAUEBQQEFQQELCw== +invalid invalid if.wast:1109 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AQQAEfgAAABsFQgALCw== +invalid invalid if.wast:1119 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AQQEEfkIABQAAABsLCw== +invalid invalid if.wast:1129 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAQQEEfgAAABsFAAAAGwsL +invalid invalid if.wast:1140 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAQQEEfwwABUEBCws= +invalid invalid if.wast:1146 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAQQEEf0EBBQwACws= +invalid invalid if.wast:1152 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEAEOAEEBBAAMAAVBAUEBCws= +invalid invalid if.wast:1158 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEAEOAEEBBABBAUEBBQwACws= +invalid invalid if.wast:1165 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AQQEEfwwAQQEFQQELCw== +invalid invalid if.wast:1174 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AQQEEf0EBBQwAQQELCw== +invalid invalid if.wast:1183 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFAESAEEBBAAMAEEBQQEFQQFBAQsL +invalid invalid if.wast:1192 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFAESAEEBBABBAUEBBQwAQQFBAQsL +invalid invalid if.wast:1202 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AQQEEfwEMAEEBBUEBCws= +invalid invalid if.wast:1211 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AQQEEf0EBBQEMAEEBCws= +invalid invalid if.wast:1220 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFQETAEEBBAABDABBAUEBBUEBQQELCw== +invalid invalid if.wast:1229 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFQETAEEBBABBAUEBBQEMAEEBQQELCw== +invalid invalid if.wast:1239 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAQQEEf0IBDABBAQVBAQsL +invalid invalid if.wast:1248 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAQQEEf0EBBUIBDABBAQsL +invalid invalid if.wast:1257 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFgEUAEEBBABCAQwAQQFBAQVBAUEBCws= +invalid invalid if.wast:1266 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFgEUAEEBBABBAUEBBUIBDABBAUEBCws= +invalid invalid if.wast:1275 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFAESAEEBQQEEAEIBDABBAQVBAQsL +invalid invalid if.wast:1285 AGFzbQEAAAABBgFgAAJ/fwMCAQAKFAESAEEBQQEEAEEBBUIBDABBAQsL +invalid invalid if.wast:1296 AGFzbQEAAAABBAFgAAADAgEACgcBBQAEQAsL +invalid invalid if.wast:1304 AGFzbQEAAAABBAFgAAADAgEACgwBCgBBAAJABEALCws= +invalid invalid if.wast:1313 AGFzbQEAAAABBAFgAAADAgEACgwBCgBBAANABEALCws= +invalid invalid if.wast:1322 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAEEABEAEQAsLCw== +invalid invalid if.wast:1331 AGFzbQEAAAABBAFgAAADAgEAChQBEgBBAEEABH9BAAUEQAtBAAsaCw== +invalid invalid if.wast:1341 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAAJABEALDAAaCws= +invalid invalid if.wast:1350 AGFzbQEAAAABBAFgAAADAgEAChEBDwBBAAJABEALQQENABoLCw== +invalid invalid if.wast:1359 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAAJABEALDgAAGgsL +invalid invalid if.wast:1368 AGFzbQEAAAABBAFgAAADAgEACgkBBwAEQAsPGgs= +invalid invalid if.wast:1376 AGFzbQEAAAABBAFgAAADAgEACg0BCwAEQAtBAUECGxoL +invalid invalid if.wast:1384 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABCg8CCAAEQAsQARoLBAAgAAs= +invalid invalid if.wast:1393 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQkHAQBBAAsBAAoVAgQAIAALDgACfwRAC0EAEQAAGgsL +invalid invalid if.wast:1409 AGFzbQEAAAABBAFgAAADAgEACg4BDAEBfwRACyEAIAAaCw== +invalid invalid if.wast:1418 AGFzbQEAAAABBAFgAAADAgEACgwBCgEBfwRACyIAGgs= +invalid invalid if.wast:1427 AGFzbQEAAAABBAFgAAADAgEABgYBfwFBAAsKDAEKAARACyQAIwAaCw== +invalid invalid if.wast:1436 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIAARAC0AAGgs= +invalid invalid if.wast:1445 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJAARACygCABoL +invalid invalid if.wast:1454 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDAEKAARAC0EBNgIACw== +invalid invalid if.wast:1464 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoKAQgAQQEEARoLCw== +invalid invalid if.wast:1470 AGFzbQEAAAABCQJgAABgAn98AAMCAQAKCwEJAEEBBAEaGgsL +invalid invalid if.wast:1476 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoPAQ0AQwAAAABBAQQBGgsL +invalid invalid if.wast:1482 AGFzbQEAAAABCQJgAABgAn1/AAMCAQAKEAEOAEMAAAAAQQEEARoaCws= +invalid invalid if.wast:1488 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoNAQsAAkBBAQQBGgsLCw== +invalid invalid if.wast:1494 AGFzbQEAAAABCQJgAABgAn98AAMCAQAKDgEMAAJAQQEEARoaCwsL +invalid invalid if.wast:1500 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoSARAAAkBDAAAAAEEBBAEaCwsL +invalid invalid if.wast:1506 AGFzbQEAAAABCQJgAABgAn1/AAMCAQAKEwERAAJAQwAAAABBAQQBGhoLCws= +valid valid imports.wast:3 AGFzbQEAAAABHgdgAABgAX8AYAF9AGAAAX9gAAF9YAF/AX9gAX4BfgMIBwABAgMEBQYECAJwAApwAQoUBQMBAAIGFAN/AEE3C30AQwAAMEILfgFCwgALB60BDQRmdW5jAAAIZnVuYy1pMzIAAQhmdW5jLWYzMgACCWZ1bmMtPmkzMgADCWZ1bmMtPmYzMgAEDWZ1bmMtaTMyLT5pMzIABQ1mdW5jLWk2NC0+aTY0AAYKZ2xvYmFsLWkzMgMACmdsb2JhbC1mMzIDAQ5nbG9iYWwtbXV0LWk2NAMCDHRhYmxlLTEwLWluZgEAC3RhYmxlLTEwLTIwAQEMbWVtb3J5LTItaW5mAgAKIQcCAAsCAAsCAAsEAEEWCwcAQwAAMEELBAAgAAsEACAACw== +valid valid imports.wast:26 AGFzbQEAAAABJAhgAX8AYAF+AGABfQBgAXwAYAF/AGACf30AYAJ8fABgAX4BfgKDAxIIc3BlY3Rlc3QJcHJpbnRfaTMyAAAIc3BlY3Rlc3QJcHJpbnRfaTY0AAEIc3BlY3Rlc3QJcHJpbnRfaTMyAAAIc3BlY3Rlc3QJcHJpbnRfaTY0AAEIc3BlY3Rlc3QJcHJpbnRfZjMyAAIIc3BlY3Rlc3QJcHJpbnRfZjY0AAMIc3BlY3Rlc3QNcHJpbnRfaTMyX2YzMgAFCHNwZWN0ZXN0DXByaW50X2Y2NF9mNjQABghzcGVjdGVzdAlwcmludF9pMzIAAAhzcGVjdGVzdAlwcmludF9mNjQAAwR0ZXN0DWZ1bmMtaTY0LT5pNjQABwhzcGVjdGVzdAlwcmludF9pMzIAAAhzcGVjdGVzdAlwcmludF9pMzIAAAhzcGVjdGVzdAlwcmludF9pMzIAAAhzcGVjdGVzdAlwcmludF9pMzIAAAhzcGVjdGVzdAlwcmludF9pMzIAAAhzcGVjdGVzdAlwcmludF9pMzIABAhzcGVjdGVzdAlwcmludF9pMzIABAMDAgABBAUBcAECAgczCAJwMQALAnAyAAwCcDMADQJwNAANAnA1AA4CcDYADwdwcmludDMyABIHcHJpbnQ2NAATCQgBAEEACwICBQpoAiwBAX0gALIhASAAEAAgAEEBakMAAChCEAYgABACIAAQCCABEAQgAEEAEQAACzkBAXwgABAKuSEBIAAQASABRAAAAAAAAPA/oEQAAAAAAIBKQBAHIAAQAyABEAUgARAJIAFBAREDAAs= +invalid invalid imports.wast:89 AGFzbQEAAAABBQFgAAF/Ag0BBHRlc3QEZnVuYwAB +valid valid imports.wast:97 AGFzbQEAAAABBQFgAX8AAhYBCHNwZWN0ZXN0CXByaW50X2kzMgAAAwIBAAcNAQlwcmludF9pMzIAAQoIAQYAIAAQAAs= +valid valid imports.wast:107 AGFzbQEAAAABCwJgAX8AYAJ/fwF/AhYBCHNwZWN0ZXN0CXByaW50X2kzMgAAAwIBAQcNAQlwcmludF9pMzIAAQoJAQcAIAAgAWoL +valid valid imports.wast:116 AGFzbQEAAAABBAFgAAACDQEEdGVzdARmdW5jAAA= +valid valid imports.wast:117 AGFzbQEAAAABBQFgAX8AAhEBBHRlc3QIZnVuYy1pMzIAAA== +valid valid imports.wast:118 AGFzbQEAAAABBQFgAX0AAhEBBHRlc3QIZnVuYy1mMzIAAA== +valid valid imports.wast:119 AGFzbQEAAAABBQFgAAF/AhIBBHRlc3QJZnVuYy0+aTMyAAA= +valid valid imports.wast:120 AGFzbQEAAAABBQFgAAF9AhIBBHRlc3QJZnVuYy0+ZjMyAAA= +valid valid imports.wast:121 AGFzbQEAAAABBgFgAX8BfwIWAQR0ZXN0DWZ1bmMtaTMyLT5pMzIAAA== +valid valid imports.wast:122 AGFzbQEAAAABBgFgAX4BfgIWAQR0ZXN0DWZ1bmMtaTY0LT5pNjQAAA== +valid valid imports.wast:125 AGFzbQEAAAABBAFgAAACEAEEdGVzdAd1bmtub3duAAA= +valid valid imports.wast:129 AGFzbQEAAAABBAFgAAACFAEIc3BlY3Rlc3QHdW5rbm93bgAA +valid valid imports.wast:134 AGFzbQEAAAABBQFgAX8AAg0BBHRlc3QEZnVuYwAA +valid valid imports.wast:138 AGFzbQEAAAABBQFgAAF/Ag0BBHRlc3QEZnVuYwAA +valid valid imports.wast:142 AGFzbQEAAAABBgFgAX8BfwINAQR0ZXN0BGZ1bmMAAA== +valid valid imports.wast:146 AGFzbQEAAAABBAFgAAACEQEEdGVzdAhmdW5jLWkzMgAA +valid valid imports.wast:150 AGFzbQEAAAABBQFgAAF/AhEBBHRlc3QIZnVuYy1pMzIAAA== +valid valid imports.wast:154 AGFzbQEAAAABBQFgAX0AAhEBBHRlc3QIZnVuYy1pMzIAAA== +valid valid imports.wast:158 AGFzbQEAAAABBQFgAX4AAhEBBHRlc3QIZnVuYy1pMzIAAA== +valid valid imports.wast:162 AGFzbQEAAAABBgFgAX8BfwIRAQR0ZXN0CGZ1bmMtaTMyAAA= +valid valid imports.wast:166 AGFzbQEAAAABBAFgAAACEgEEdGVzdAlmdW5jLT5pMzIAAA== +valid valid imports.wast:170 AGFzbQEAAAABBQFgAX8AAhIBBHRlc3QJZnVuYy0+aTMyAAA= +valid valid imports.wast:174 AGFzbQEAAAABBQFgAAF9AhIBBHRlc3QJZnVuYy0+aTMyAAA= +valid valid imports.wast:178 AGFzbQEAAAABBQFgAAF+AhIBBHRlc3QJZnVuYy0+aTMyAAA= +valid valid imports.wast:182 AGFzbQEAAAABBgFgAX8BfwISAQR0ZXN0CWZ1bmMtPmkzMgAA +valid valid imports.wast:186 AGFzbQEAAAABBAFgAAACFgEEdGVzdA1mdW5jLWkzMi0+aTMyAAA= +valid valid imports.wast:190 AGFzbQEAAAABBQFgAX8AAhYBBHRlc3QNZnVuYy1pMzItPmkzMgAA +valid valid imports.wast:194 AGFzbQEAAAABBQFgAAF/AhYBBHRlc3QNZnVuYy1pMzItPmkzMgAA +valid valid imports.wast:199 AGFzbQEAAAABBQFgAAF/AhMBBHRlc3QKZ2xvYmFsLWkzMgAA +valid valid imports.wast:203 AGFzbQEAAAABBAFgAAACFQEEdGVzdAx0YWJsZS0xMC1pbmYAAA== +valid valid imports.wast:207 AGFzbQEAAAABBAFgAAACFQEEdGVzdAxtZW1vcnktMi1pbmYAAA== +valid valid imports.wast:211 AGFzbQEAAAABBAFgAAACFwEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgAA +valid valid imports.wast:215 AGFzbQEAAAABBAFgAAACEgEIc3BlY3Rlc3QFdGFibGUAAA== +valid valid imports.wast:219 AGFzbQEAAAABBAFgAAACEwEIc3BlY3Rlc3QGbWVtb3J5AAA= +valid valid imports.wast:226 AGFzbQEAAAABEQRgAAF/YAABfmAAAX1gAAF8AqIBBwhzcGVjdGVzdApnbG9iYWxfaTMyA38ACHNwZWN0ZXN0Cmdsb2JhbF9pMzIDfwAIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAhzcGVjdGVzdApnbG9iYWxfaTMyA38ACHNwZWN0ZXN0Cmdsb2JhbF9pNjQDfgAIc3BlY3Rlc3QKZ2xvYmFsX2YzMgN9AAhzcGVjdGVzdApnbG9iYWxfZjY0A3wAAwgHAAAAAAECAwc5BwVnZXQtMAAABWdldC0xAAEFZ2V0LXgAAgVnZXQteQADBWdldC00AAQFZ2V0LTUABQVnZXQtNgAGCiQHBAAjAAsEACMBCwQAIwILBAAjAwsEACMECwQAIwULBAAjBgs= +valid valid imports.wast:254 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38A +valid valid imports.wast:255 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtZjMyA30A +valid valid imports.wast:256 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWk2NAN+AQ== +valid valid imports.wast:259 AGFzbQEAAAACEQEEdGVzdAd1bmtub3duA38A +valid valid imports.wast:263 AGFzbQEAAAACFQEIc3BlY3Rlc3QHdW5rbm93bgN/AA== +valid valid imports.wast:268 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA34A +valid valid imports.wast:272 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA30A +valid valid imports.wast:276 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA3wA +valid valid imports.wast:280 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyA38B +valid valid imports.wast:284 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtZjMyA38A +valid valid imports.wast:288 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtZjMyA34A +valid valid imports.wast:292 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtZjMyA3wA +valid valid imports.wast:296 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtZjMyA30B +valid valid imports.wast:300 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWk2NAN/AQ== +valid valid imports.wast:304 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWk2NAN9AQ== +valid valid imports.wast:308 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWk2NAN8AQ== +valid valid imports.wast:312 AGFzbQEAAAACGAEEdGVzdA5nbG9iYWwtbXV0LWk2NAN+AA== +valid valid imports.wast:317 AGFzbQEAAAACDgEEdGVzdARmdW5jA38A +valid valid imports.wast:321 AGFzbQEAAAACFgEEdGVzdAx0YWJsZS0xMC1pbmYDfwA= +valid valid imports.wast:325 AGFzbQEAAAACFgEEdGVzdAxtZW1vcnktMi1pbmYDfwA= +valid valid imports.wast:329 AGFzbQEAAAACFwEIc3BlY3Rlc3QJcHJpbnRfaTMyA38A +valid valid imports.wast:333 AGFzbQEAAAACEwEIc3BlY3Rlc3QFdGFibGUDfwA= +valid valid imports.wast:337 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5A38A +valid valid imports.wast:344 AGFzbQEAAAABCgJgAAF/YAF/AX8CFQEIc3BlY3Rlc3QFdGFibGUBcAEKFAMEAwEAAAcIAQRjYWxsAAAJCAEAQQELAgECChMDBwAgABEAAAsEAEELCwQAQRYL +valid valid imports.wast:381 AGFzbQEAAAACJwIIc3BlY3Rlc3QFdGFibGUBcAAACHNwZWN0ZXN0BXRhYmxlAXAAAAQHAnAACnAACg== +valid valid imports.wast:388 AGFzbQEAAAACFwEEdGVzdAx0YWJsZS0xMC1pbmYBcAAK +valid valid imports.wast:389 AGFzbQEAAAACFwEEdGVzdAx0YWJsZS0xMC1pbmYBcAAF +valid valid imports.wast:390 AGFzbQEAAAACFwEEdGVzdAx0YWJsZS0xMC1pbmYBcAAA +valid valid imports.wast:391 AGFzbQEAAAACFgEEdGVzdAt0YWJsZS0xMC0yMAFwAAo= +valid valid imports.wast:392 AGFzbQEAAAACFgEEdGVzdAt0YWJsZS0xMC0yMAFwAAU= +valid valid imports.wast:393 AGFzbQEAAAACFgEEdGVzdAt0YWJsZS0xMC0yMAFwAAA= +valid valid imports.wast:394 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQoU +valid valid imports.wast:395 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQUU +valid valid imports.wast:396 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQAU +valid valid imports.wast:397 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQoZ +valid valid imports.wast:398 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQUZ +valid valid imports.wast:399 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQAZ +valid valid imports.wast:400 AGFzbQEAAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAK +valid valid imports.wast:401 AGFzbQEAAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAF +valid valid imports.wast:402 AGFzbQEAAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAA +valid valid imports.wast:403 AGFzbQEAAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEKFA== +valid valid imports.wast:404 AGFzbQEAAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEFFA== +valid valid imports.wast:405 AGFzbQEAAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEAFA== +valid valid imports.wast:406 AGFzbQEAAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEKGQ== +valid valid imports.wast:407 AGFzbQEAAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEFGQ== +valid valid imports.wast:410 AGFzbQEAAAACEgEEdGVzdAd1bmtub3duAXAACg== +valid valid imports.wast:414 AGFzbQEAAAACFgEIc3BlY3Rlc3QHdW5rbm93bgFwAAo= +valid valid imports.wast:419 AGFzbQEAAAACFwEEdGVzdAx0YWJsZS0xMC1pbmYBcAAM +valid valid imports.wast:423 AGFzbQEAAAACGAEEdGVzdAx0YWJsZS0xMC1pbmYBcAEKFA== +valid valid imports.wast:427 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQwU +valid valid imports.wast:431 AGFzbQEAAAACFwEEdGVzdAt0YWJsZS0xMC0yMAFwAQoS +valid valid imports.wast:435 AGFzbQEAAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAM +valid valid imports.wast:439 AGFzbQEAAAACFQEIc3BlY3Rlc3QFdGFibGUBcAEKDw== +valid valid imports.wast:444 AGFzbQEAAAACDwEEdGVzdARmdW5jAXAACg== +valid valid imports.wast:448 AGFzbQEAAAACFQEEdGVzdApnbG9iYWwtaTMyAXAACg== +valid valid imports.wast:452 AGFzbQEAAAACFwEEdGVzdAxtZW1vcnktMi1pbmYBcAAK +valid valid imports.wast:456 AGFzbQEAAAACGAEIc3BlY3Rlc3QJcHJpbnRfaTMyAXAACg== +valid valid imports.wast:464 AGFzbQEAAAABBgFgAX8BfwIVAQhzcGVjdGVzdAZtZW1vcnkCAQECAwIBAAcIAQRsb2FkAAAKCQEHACAAKAIACwsHAQBBCgsBEA== +invalid valid imports.wast:488 AGFzbQEAAAACCwIAAAIAAQAAAgAB +invalid valid imports.wast:492 AGFzbQEAAAACBgEAAAIAAQUDAQAA +invalid valid imports.wast:496 AGFzbQEAAAAFBQIAAAAA +valid valid imports.wast:500 AGFzbQEAAAACFgEEdGVzdAxtZW1vcnktMi1pbmYCAAI= +valid valid imports.wast:501 AGFzbQEAAAACFgEEdGVzdAxtZW1vcnktMi1pbmYCAAE= +valid valid imports.wast:502 AGFzbQEAAAACFgEEdGVzdAxtZW1vcnktMi1pbmYCAAA= +valid valid imports.wast:503 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgAB +valid valid imports.wast:504 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgAA +valid valid imports.wast:505 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEBAg== +valid valid imports.wast:506 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEAAg== +valid valid imports.wast:507 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEBAw== +valid valid imports.wast:508 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEAAw== +valid valid imports.wast:511 AGFzbQEAAAACEQEEdGVzdAd1bmtub3duAgAB +valid valid imports.wast:515 AGFzbQEAAAACFQEIc3BlY3Rlc3QHdW5rbm93bgIAAQ== +valid valid imports.wast:520 AGFzbQEAAAACFgEEdGVzdAxtZW1vcnktMi1pbmYCAAM= +valid valid imports.wast:524 AGFzbQEAAAACFwEEdGVzdAxtZW1vcnktMi1pbmYCAQID +valid valid imports.wast:528 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgAC +valid valid imports.wast:532 AGFzbQEAAAACFQEIc3BlY3Rlc3QGbWVtb3J5AgEBAQ== +valid valid imports.wast:537 AGFzbQEAAAACEgEEdGVzdAhmdW5jLWkzMgIAAQ== +valid valid imports.wast:541 AGFzbQEAAAACFAEEdGVzdApnbG9iYWwtaTMyAgAB +valid valid imports.wast:545 AGFzbQEAAAACFgEEdGVzdAx0YWJsZS0xMC1pbmYCAAE= +valid valid imports.wast:549 AGFzbQEAAAACFwEIc3BlY3Rlc3QJcHJpbnRfaTMyAgAB +valid valid imports.wast:553 AGFzbQEAAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgIAAQ== +valid valid imports.wast:557 AGFzbQEAAAACEwEIc3BlY3Rlc3QFdGFibGUCAAE= +valid valid imports.wast:570 AGFzbQEAAAABBgFgAX8BfwIVAQhzcGVjdGVzdAZtZW1vcnkCAQADAwIBAAcIAQRncm93AAAKCAEGACAAQAAL +valid valid imports.wast:657 AGFzbQEAAAABKQpgAABgAX8AYAJ/fwBgAX4AYAF9AGABfABgAAF/YAABfmAAAX1gAAF8AugCEAhub3Qgd2FzbQpvdmVybG9hZGVkAAAIbm90IHdhc20Kb3ZlcmxvYWRlZAABCG5vdCB3YXNtCm92ZXJsb2FkZWQAAghub3Qgd2FzbQpvdmVybG9hZGVkAAMIbm90IHdhc20Kb3ZlcmxvYWRlZAAECG5vdCB3YXNtCm92ZXJsb2FkZWQABQhub3Qgd2FzbQpvdmVybG9hZGVkAAYIbm90IHdhc20Kb3ZlcmxvYWRlZAAHCG5vdCB3YXNtCm92ZXJsb2FkZWQACAhub3Qgd2FzbQpvdmVybG9hZGVkAAkIbm90IHdhc20Kb3ZlcmxvYWRlZAN/AAhub3Qgd2FzbQpvdmVybG9hZGVkA34ACG5vdCB3YXNtCm92ZXJsb2FkZWQDfQAIbm90IHdhc20Kb3ZlcmxvYWRlZAN8AAhub3Qgd2FzbQpvdmVybG9hZGVkAXAAAAhub3Qgd2FzbQpvdmVybG9hZGVkAgAA +valid valid inline-module.wast:1 AGFzbQEAAAABBAFgAAADAwIAAAUDAQAABwUBAWYAAQoHAgIACwIACw== +valid valid int_exprs.wast:6 AGFzbQEAAAABDQJgAn9/AX9gAn5+AX8DBQQAAAEBB20EGGkzMi5ub19mb2xkX2NtcF9zX29mZnNldAAAGGkzMi5ub19mb2xkX2NtcF91X29mZnNldAABGGk2NC5ub19mb2xkX2NtcF9zX29mZnNldAACGGk2NC5ub19mb2xkX2NtcF91X29mZnNldAADCjkEDQAgAEEBaiABQQFqSAsNACAAQQFqIAFBAWpJCw0AIABCAXwgAUIBfFMLDQAgAEIBfCABQgF8VAs= +valid valid int_exprs.wast:25 AGFzbQEAAAABBgFgAX4BfgMCAQAHHQEZaTY0Lm5vX2ZvbGRfd3JhcF9leHRlbmRfcwAACggBBgAgAKesCw== +valid valid int_exprs.wast:35 AGFzbQEAAAABBgFgAX4BfgMCAQAHHQEZaTY0Lm5vX2ZvbGRfd3JhcF9leHRlbmRfdQAACggBBgAgAKetCw== +valid valid int_exprs.wast:44 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQdhBBVpMzIubm9fZm9sZF9zaGxfc2hyX3MAABVpMzIubm9fZm9sZF9zaGxfc2hyX3UAARVpNjQubm9fZm9sZF9zaGxfc2hyX3MAAhVpNjQubm9fZm9sZF9zaGxfc2hyX3UAAwotBAoAIABBAXRBAXULCgAgAEEBdEEBdgsKACAAQgGGQgGHCwoAIABCAYZCAYgL +valid valid int_exprs.wast:63 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQdhBBVpMzIubm9fZm9sZF9zaHJfc19zaGwAABVpMzIubm9fZm9sZF9zaHJfdV9zaGwAARVpNjQubm9fZm9sZF9zaHJfc19zaGwAAhVpNjQubm9fZm9sZF9zaHJfdV9zaGwAAwotBAoAIABBAXVBAXQLCgAgAEEBdkEBdAsKACAAQgGHQgGGCwoAIABCAYhCAYYL +valid valid int_exprs.wast:82 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQdhBBVpMzIubm9fZm9sZF9kaXZfc19tdWwAABVpMzIubm9fZm9sZF9kaXZfdV9tdWwAARVpNjQubm9fZm9sZF9kaXZfc19tdWwAAhVpNjQubm9fZm9sZF9kaXZfdV9tdWwAAwotBAoAIABBBm1BBmwLCgAgAEEGbkEGbAsKACAAQgZ/QgZ+CwoAIABCBoBCBn4L +valid valid int_exprs.wast:101 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQdlBBZpMzIubm9fZm9sZF9kaXZfc19zZWxmAAAWaTMyLm5vX2ZvbGRfZGl2X3Vfc2VsZgABFmk2NC5ub19mb2xkX2Rpdl9zX3NlbGYAAhZpNjQubm9fZm9sZF9kaXZfdV9zZWxmAAMKIQQHACAAIABtCwcAIAAgAG4LBwAgACAAfwsHACAAIACACw== +valid valid int_exprs.wast:120 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQdlBBZpMzIubm9fZm9sZF9yZW1fc19zZWxmAAAWaTMyLm5vX2ZvbGRfcmVtX3Vfc2VsZgABFmk2NC5ub19mb2xkX3JlbV9zX3NlbGYAAhZpNjQubm9fZm9sZF9yZW1fdV9zZWxmAAMKIQQHACAAIABvCwcAIAAgAHALBwAgACAAgQsHACAAIACCCw== +valid valid int_exprs.wast:139 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQdhBBVpMzIubm9fZm9sZF9tdWxfZGl2X3MAABVpMzIubm9fZm9sZF9tdWxfZGl2X3UAARVpNjQubm9fZm9sZF9tdWxfZGl2X3MAAhVpNjQubm9fZm9sZF9tdWxfZGl2X3UAAwotBAoAIABBBmxBBm0LCgAgAEEGbEEGbgsKACAAQgZ+QgZ/CwoAIABCBn5CBoAL +valid valid int_exprs.wast:158 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwMCAAEHLQITaTMyLm5vX2ZvbGRfZGl2X3NfMgAAE2k2NC5ub19mb2xkX2Rpdl9zXzIAAQoRAgcAIABBAm0LBwAgAEICfws= +valid valid int_exprs.wast:171 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwMCAAEHLQITaTMyLm5vX2ZvbGRfcmVtX3NfMgAAE2k2NC5ub19mb2xkX3JlbV9zXzIAAQoRAgcAIABBAm8LBwAgAEICgQs= +valid valid int_exprs.wast:184 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQc5BAtpMzIuZGl2X3NfMAAAC2kzMi5kaXZfdV8wAAELaTY0LmRpdl9zXzAAAgtpNjQuZGl2X3VfMAADCiEEBwAgAEEAbQsHACAAQQBuCwcAIABCAH8LBwAgAEIAgAs= +valid valid int_exprs.wast:203 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQc5BAtpMzIuZGl2X3NfMwAAC2kzMi5kaXZfdV8zAAELaTY0LmRpdl9zXzMAAgtpNjQuZGl2X3VfMwADCiEEBwAgAEEDbQsHACAAQQNuCwcAIABCA38LBwAgAEIDgAs= +valid valid int_exprs.wast:226 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQc5BAtpMzIuZGl2X3NfNQAAC2kzMi5kaXZfdV81AAELaTY0LmRpdl9zXzUAAgtpNjQuZGl2X3VfNQADCiEEBwAgAEEFbQsHACAAQQVuCwcAIABCBX8LBwAgAEIFgAs= +valid valid int_exprs.wast:249 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQc5BAtpMzIuZGl2X3NfNwAAC2kzMi5kaXZfdV83AAELaTY0LmRpdl9zXzcAAgtpNjQuZGl2X3VfNwADCiEEBwAgAEEHbQsHACAAQQduCwcAIABCB38LBwAgAEIHgAs= +valid valid int_exprs.wast:272 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQc5BAtpMzIucmVtX3NfMwAAC2kzMi5yZW1fdV8zAAELaTY0LnJlbV9zXzMAAgtpNjQucmVtX3VfMwADCiEEBwAgAEEDbwsHACAAQQNwCwcAIABCA4ELBwAgAEIDggs= +valid valid int_exprs.wast:295 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQc5BAtpMzIucmVtX3NfNQAAC2kzMi5yZW1fdV81AAELaTY0LnJlbV9zXzUAAgtpNjQucmVtX3VfNQADCiEEBwAgAEEFbwsHACAAQQVwCwcAIABCBYELBwAgAEIFggs= +valid valid int_exprs.wast:318 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwUEAAABAQc5BAtpMzIucmVtX3NfNwAAC2kzMi5yZW1fdV83AAELaTY0LnJlbV9zXzcAAgtpNjQucmVtX3VfNwADCiEEBwAgAEEHbwsHACAAQQdwCwcAIABCB4ELBwAgAEIHggs= +valid valid int_exprs.wast:341 AGFzbQEAAAABCwJgAX8Bf2ABfgF+AwMCAAEHLwIUaTMyLm5vX2ZvbGRfZGl2X25lZzEAABRpNjQubm9fZm9sZF9kaXZfbmVnMQABChECBwAgAEF/bQsHACAAQn9/Cw== +valid valid int_literals.wast:1 AGFzbQEAAAABCQJgAAF/YAABfgMfHgAAAAAAAAAAAAAAAQEBAQEBAQEBAQEAAAAAAQEBAQe3Ax4IaTMyLnRlc3QAAAhpMzIudW1heAABCGkzMi5zbWF4AAIMaTMyLm5lZ19zbWF4AAMIaTMyLnNtaW4ABAxpMzIuYWx0X3NtaW4ABQxpMzIuaW5jX3NtaW4ABgxpMzIubmVnX3plcm8ABw1pMzIubm90X29jdGFsAAgUaTMyLnVuc2lnbmVkX2RlY2ltYWwACQ1pMzIucGx1c19zaWduAAoIaTY0LnRlc3QACwhpNjQudW1heAAMCGk2NC5zbWF4AA0MaTY0Lm5lZ19zbWF4AA4IaTY0LnNtaW4ADwxpNjQuYWx0X3NtaW4AEAxpNjQuaW5jX3NtaW4AEQxpNjQubmVnX3plcm8AEg1pNjQubm90X29jdGFsABMUaTY0LnVuc2lnbmVkX2RlY2ltYWwAFA1pNjQucGx1c19zaWduABUMaTMyLWRlYy1zZXAxABYMaTMyLWRlYy1zZXAyABcMaTMyLWhleC1zZXAxABgMaTMyLWhleC1zZXAyABkMaTY0LWRlYy1zZXAxABoMaTY0LWRlYy1zZXAyABsMaTY0LWhleC1zZXAxABwMaTY0LWhleC1zZXAyAB0KlQIeCQBBjaC33QAPCwUAQX8PCwkAQf////8HDwsJAEGBgICAeA8LCQBBgICAgHgPCwkAQYCAgIB4DwsMAEGAgICAeEEBag8LBQBBAA8LBQBBCg8LBQBBfw8LBQBBKg8LDQBC7tSZ3eDN7tUMDwsFAEJ/DwsOAEL///////////8ADwsOAEKBgICAgICAgIB/DwsOAEKAgICAgICAgIB/DwsOAEKAgICAgICAgIB/DwsRAEKAgICAgICAgIB/QgF8DwsFAEIADwsFAEIKDwsFAEJ/DwsFAEIqDwsGAEHAhD0LBQBB6AcLCABBmYG80AALBgBBj9QGCwYAQsCEPQsFAELoBwsLAEKZs4KA8IG8BQsGAEKP1AYL +valid valid labels.wast:1 AGFzbQEAAAABCgJgAAF/YAF/AX8DExIAAAAAAQAAAAABAQAAAAAAAAAHmgESBWJsb2NrAAAFbG9vcDEAAQVsb29wMgACBWxvb3AzAAMFbG9vcDQABAVsb29wNQAFBWxvb3A2AAYCaWYABwNpZjIACAZzd2l0Y2gACQZyZXR1cm4ACgZicl9pZjAACwZicl9pZjEADAZicl9pZjIADQZicl9pZjMADgJicgAPCXNoYWRvd2luZwAQDHJlZGVmaW5pdGlvbgARCoMGEgsAAn9BAQwAQQALCyMBAX9BACEAAn8DfyAAQQFqIQAgAEEFRgRAIAAMAgsMAAsLCzQBAX9BACEAAn8DfyAAQQFqIQAgAEEFRgRADAELIABBCEYEQCAADAILIABBAWohAAwACwsLIwEBf0EAIQACfwN/IABBAWohACAAQQVGBEAgAAwCCyAACwsLIwEBf0EBIQECfwN/IAEgAWohASABIABLBEAgAQwCCwwACwsLCgADf0EBC0EBagsLAAN/QQANAEEDCwuEAQEBf0EAIQACQEEBBEAMAEGaBSEACyAAQQFqIQBBAQRADABBmgUhAAVB+AYhAAsgAEEBaiEAQQEEQAwAQZoFIQAFQfgGIQALIABBAWohAEEABEBB+AYhAAUMAEGaBSEACyAAQQFqIQBBAARAQfgGIQAFDABBmgUhAAsgAEEBaiEACyAAC4QBAQF/QQAhAAJAQQEEQAwAQZoFIQALIABBAWohAEEBBEAMAEGaBSEABUH4BiEACyAAQQFqIQBBAQRADABBmgUhAAVB+AYhAAsgAEEBaiEAQQAEQEH4BiEABQwAQZoFIQALIABBAWohAEEABEBB+AYhAAUMAEGaBSEACyAAQQFqIQALIAALLQACf0EKAn8CQAJAAkACQAJAIAAOBAQAAQIDCwtBAgwDC0EDDAMLC0EFC2wLCxgAAkACQAJAIAAOAQABDAILQQAPCwtBAgtWAQF/QQAhAAJ/AkBBAA0AIABBAXIhAEEBDQAgAEECciEACwJ/IABBBHIhACAAC0EADQAaIABBCHIhAAJ/IABBEHIhACAAC0EBDQAaIABBIHIhACAACwsTAAJ/An9BAQwAC0EBDQAaQQALCxgAAn9BAQRAAn9BAQwAC0EBDQEaC0EACwskAQF/An8Cf0EBIQAgAAsCf0ECIQAgAAsNABpBAAtBAGoaIAALIQACf0EBBEACf0EBDAALDAEFAkACf0EBDAALGgsLQQELCwwAAn9BAQwAQQJzCwsSAAJ/An9BAgsCf0EDDAALagsL +invalid invalid labels.wast:318 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEBDQCMAQsL +invalid invalid labels.wast:322 AGFzbQEAAAABBAFgAAADAgEAChABDgACQEMAAAAAQQENAAsL +valid valid left-to-right.wast:1 AGFzbQEAAAABQA1gAn9/AX9gAn5+AX9gAn19AX9gAnx8AX9gAABgAAF/YAABfmAAAX1gAAF8YAJ/fwBgAn5+AGACfX0AYAJ8fAADhAGCAQAAAQECAgMDBAQFBQUFBQUGBgYFBQcHBwUFCAgIBQUJCgsMBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEBQFwAQgIBQMBAAEHqghfB2kzMl9hZGQAIwdpMzJfc3ViACQHaTMyX211bAAlCWkzMl9kaXZfcwAmCWkzMl9kaXZfdQAnCWkzMl9yZW1fcwAoCWkzMl9yZW1fdQApB2kzMl9hbmQAKgZpMzJfb3IAKwdpMzJfeG9yACwHaTMyX3NobAAtCWkzMl9zaHJfdQAuCWkzMl9zaHJfcwAvBmkzMl9lcQAwBmkzMl9uZQAxCGkzMl9sdF9zADIIaTMyX2xlX3MAMwhpMzJfbHRfdQA0CGkzMl9sZV91ADUIaTMyX2d0X3MANghpMzJfZ2VfcwA3CGkzMl9ndF91ADgIaTMyX2dlX3UAOQlpMzJfc3RvcmUAOgppMzJfc3RvcmU4ADsLaTMyX3N0b3JlMTYAPAhpMzJfY2FsbAA9EWkzMl9jYWxsX2luZGlyZWN0AD4KaTMyX3NlbGVjdAA/B2k2NF9hZGQAQAdpNjRfc3ViAEEHaTY0X211bABCCWk2NF9kaXZfcwBDCWk2NF9kaXZfdQBECWk2NF9yZW1fcwBFCWk2NF9yZW1fdQBGB2k2NF9hbmQARwZpNjRfb3IASAdpNjRfeG9yAEkHaTY0X3NobABKCWk2NF9zaHJfdQBLCWk2NF9zaHJfcwBMBmk2NF9lcQBNBmk2NF9uZQBOCGk2NF9sdF9zAE8IaTY0X2xlX3MAUAhpNjRfbHRfdQBRCGk2NF9sZV91AFIIaTY0X2d0X3MAUwhpNjRfZ2VfcwBUCGk2NF9ndF91AFUIaTY0X2dlX3UAVglpNjRfc3RvcmUAVwppNjRfc3RvcmU4AFgLaTY0X3N0b3JlMTYAWQtpNjRfc3RvcmUzMgBaCGk2NF9jYWxsAFsRaTY0X2NhbGxfaW5kaXJlY3QAXAppNjRfc2VsZWN0AF0HZjMyX2FkZABeB2YzMl9zdWIAXwdmMzJfbXVsAGAHZjMyX2RpdgBhDGYzMl9jb3B5c2lnbgBiBmYzMl9lcQBjBmYzMl9uZQBkBmYzMl9sdABlBmYzMl9sZQBmBmYzMl9ndABnBmYzMl9nZQBoB2YzMl9taW4AaQdmMzJfbWF4AGoJZjMyX3N0b3JlAGsIZjMyX2NhbGwAbBFmMzJfY2FsbF9pbmRpcmVjdABtCmYzMl9zZWxlY3QAbgdmNjRfYWRkAG8HZjY0X3N1YgBwB2Y2NF9tdWwAcQdmNjRfZGl2AHIMZjY0X2NvcHlzaWduAHMGZjY0X2VxAHQGZjY0X25lAHUGZjY0X2x0AHYGZjY0X2xlAHcGZjY0X2d0AHgGZjY0X2dlAHkHZjY0X21pbgB6B2Y2NF9tYXgAewlmNjRfc3RvcmUAfAhmNjRfY2FsbAB9EWY2NF9jYWxsX2luZGlyZWN0AH4KZjY0X3NlbGVjdAB/BWJyX2lmAIABCGJyX3RhYmxlAIEBCQ4BAEEACwgAAQIDBAUGBwqqDYIBBABBfwsEAEF+CwQAQX8LBABBfgsEAEF/CwQAQX4LBABBfwsEAEF+CwkAQQhBADYCAAsnAEELQQotAAA6AABBCkEJLQAAOgAAQQlBCC0AADoAAEEIQX06AAALBwBBCCgCAAsNABAJQQhBAToAAEEACw0AEAlBCEECOgAAQQELDQAQCUEIQQM6AABBAQsNABAJQQhBBDoAAEEACw0AEAlBCEEFOgAAQQALDQAQCUEIQQE6AABCAAsNABAJQQhBAjoAAEIBCw0AEAlBCEEDOgAAQgELDQAQCUEIQQQ6AABBAgsNABAJQQhBBToAAEEACxAAEAlBCEEBOgAAQwAAAAALEAAQCUEIQQI6AABDAACAPwsQABAJQQhBAzoAAEMAAIA/Cw0AEAlBCEEEOgAAQQQLDQAQCUEIQQU6AABBAAsUABAJQQhBAToAAEQAAAAAAAAAAAsUABAJQQhBAjoAAEQAAAAAAADwPwsUABAJQQhBAzoAAEQAAAAAAADwPwsNABAJQQhBBDoAAEEGCw0AEAlBCEEFOgAAQQALAgALAgALAgALAgALDAAQCBALEAxqGhAKCwwAEAgQCxAMaxoQCgsMABAIEAsQDGwaEAoLDAAQCBALEAxtGhAKCwwAEAgQCxAMbhoQCgsMABAIEAsQDG8aEAoLDAAQCBALEAxwGhAKCwwAEAgQCxAMcRoQCgsMABAIEAsQDHIaEAoLDAAQCBALEAxzGhAKCwwAEAgQCxAMdBoQCgsMABAIEAsQDHYaEAoLDAAQCBALEAx1GhAKCwwAEAgQCxAMRhoQCgsMABAIEAsQDEcaEAoLDAAQCBALEAxIGhAKCwwAEAgQCxAMTBoQCgsMABAIEAsQDEkaEAoLDAAQCBALEAxNGhAKCwwAEAgQCxAMShoQCgsMABAIEAsQDE4aEAoLDAAQCBALEAxLGhAKCwwAEAgQCxAMTxoQCgsNABAIEAsQDDYCABAKCw0AEAgQCxAMOgAAEAoLDQAQCBALEAw7AQAQCgsMABAIEAsQDBAfEAoLEAAQCBALEAwQDhEAABoQCgsOABAIEAsQDBAPGxoQCgsMABAIEBAQEXwaEAoLDAAQCBAQEBF9GhAKCwwAEAgQEBARfhoQCgsMABAIEBAQEX8aEAoLDAAQCBAQEBGAGhAKCwwAEAgQEBARgRoQCgsMABAIEBAQEYIaEAoLDAAQCBAQEBGDGhAKCwwAEAgQEBARhBoQCgsMABAIEBAQEYUaEAoLDAAQCBAQEBGGGhAKCwwAEAgQEBARiBoQCgsMABAIEBAQEYcaEAoLDAAQCBAQEBFRGhAKCwwAEAgQEBARUhoQCgsMABAIEBAQEVMaEAoLDAAQCBAQEBFXGhAKCwwAEAgQEBARVBoQCgsMABAIEBAQEVgaEAoLDAAQCBAQEBFVGhAKCwwAEAgQEBARWRoQCgsMABAIEBAQEVYaEAoLDAAQCBAQEBFaGhAKCw0AEAgQCxARNwMAEAoLDQAQCBALEBE8AAAQCgsNABAIEAsQET0BABAKCw0AEAgQCxARPgIAEAoLDAAQCBAQEBEQIBAKCxAAEAgQEBAREBMRAQAaEAoLDgAQCBAQEBEQFBsaEAoLDAAQCBAVEBaSGhAKCwwAEAgQFRAWkxoQCgsMABAIEBUQFpQaEAoLDAAQCBAVEBaVGhAKCwwAEAgQFRAWmBoQCgsMABAIEBUQFlsaEAoLDAAQCBAVEBZcGhAKCwwAEAgQFRAWXRoQCgsMABAIEBUQFl8aEAoLDAAQCBAVEBZeGhAKCwwAEAgQFRAWYBoQCgsMABAIEBUQFpYaEAoLDAAQCBAVEBaXGhAKCw0AEAgQCxAWOAIAEAoLDAAQCBAVEBYQIRAKCxAAEAgQFRAWEBgRAgAaEAoLDgAQCBAVEBYQGRsaEAoLDAAQCBAaEBugGhAKCwwAEAgQGhAboRoQCgsMABAIEBoQG6IaEAoLDAAQCBAaEBujGhAKCwwAEAgQGhAbphoQCgsMABAIEBoQG2EaEAoLDAAQCBAaEBtiGhAKCwwAEAgQGhAbYxoQCgsMABAIEBoQG2UaEAoLDAAQCBAaEBtkGhAKCwwAEAgQGhAbZhoQCgsMABAIEBoQG6QaEAoLDAAQCBAaEBulGhAKCw0AEAgQCxAbOQMAEAoLDAAQCBAaEBsQIhAKCxAAEAgQGhAbEB0RAwAaEAoLDgAQCBAaEBsQHhsaEAoLEwACfxAIEAsQDEEAcQ0AGhAKCwsVAAJ/EAgCfxALEAwOAQEACxoQCgsL +valid valid linking.wast:3 AGFzbQEAAAABBQFgAAF/AwMCAAAHCAEEY2FsbAAACgsCBAAQAQsEAEECCw== +valid valid linking.wast:9 AGFzbQEAAAABBQFgAAF/AgsBAk1mBGNhbGwAAAMEAwAAAAchAwdNZi5jYWxsAAAMY2FsbCBNZi5jYWxsAAEEY2FsbAACChADBAAQAAsEABADCwQAQQML +valid valid linking.wast:22 AGFzbQEAAAABBQFgAX8AAhYBCHNwZWN0ZXN0CXByaW50X2kzMgAABwkBBXByaW50AAA= +valid valid linking.wast:28 AGFzbQEAAAABBQFgAX4AAhQBCnJlZXhwb3J0X2YFcHJpbnQAAA== +valid valid linking.wast:32 AGFzbQEAAAABBgFgAX8BfwIUAQpyZWV4cG9ydF9mBXByaW50AAA= +valid valid linking.wast:39 AGFzbQEAAAABCQJgAAF/YAF/AAMEAwAAAQYMAn8AQSoLfwFBjgELBy0FBGdsb2IDAANnZXQAAAhtdXRfZ2xvYgMBB2dldF9tdXQAAQdzZXRfbXV0AAIKEgMEACMACwQAIwELBgAgACQBCw== +valid valid linking.wast:50 AGFzbQEAAAABCQJgAAF/YAF/AAI+BQJNZwRnbG9iA38AAk1nCG11dF9nbG9iA38BAk1nA2dldAAAAk1nB2dldF9tdXQAAAJNZwdzZXRfbXV0AAEDAgEABgYBfwBBKwsHSQcHTWcuZ2xvYgMABk1nLmdldAAABGdsb2IDAgNnZXQAAwtNZy5tdXRfZ2xvYgMBCk1nLmdldF9tdXQAAQpNZy5zZXRfbXV0AAIKBgEEACMCCw== +valid valid linking.wast:87 AGFzbQEAAAACEAECTWcIbXV0X2dsb2IDfwA= +valid valid linking.wast:91 AGFzbQEAAAACDAECTWcEZ2xvYgN/AQ== +valid valid linking.wast:96 AGFzbQEAAAAGFQRwANBwC3AB0HALbwDQbwtvAdBvCwc9BAxnLWNvbnN0LWZ1bmMDAApnLXZhci1mdW5jAwEOZy1jb25zdC1leHRlcm4DAgxnLXZhci1leHRlcm4DAw== +valid valid linking.wast:104 AGFzbQEAAAACYQQHTXJlZl9leAxnLWNvbnN0LWZ1bmMDcAAHTXJlZl9leA5nLWNvbnN0LWV4dGVybgNvAAdNcmVmX2V4CmctdmFyLWZ1bmMDcAEHTXJlZl9leAxnLXZhci1leHRlcm4DbwE= +valid valid linking.wast:113 AGFzbQEAAAACGwEHTXJlZl9leA5nLWNvbnN0LWV4dGVybgNwAA== +valid valid linking.wast:117 AGFzbQEAAAACGQEHTXJlZl9leAxnLWNvbnN0LWZ1bmMDbwA= +valid valid linking.wast:123 AGFzbQEAAAACFwEHTXJlZl9leApnLXZhci1mdW5jA28B +valid valid linking.wast:127 AGFzbQEAAAACGQEHTXJlZl9leAxnLXZhci1leHRlcm4DcAE= +valid valid linking.wast:134 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8DBAMAAAIEBAFwAAoHEgMDdGFiAQABaAABBGNhbGwAAgkKAQBBAgsEAAAAAAoTAwQAQQQLBABBfAsHACAAEQAACw== +valid valid linking.wast:149 AGFzbQEAAAABDQNgAABgAAF/YAF/AX8CEgICTXQEY2FsbAACAk10AWgAAQMEAwECAgQFAXABBQUHIQMHTXQuY2FsbAAADGNhbGwgTXQuY2FsbAADBGNhbGwABAkLAQBBAAsFAgICAQAKFQMEAEEFCwYAIAAQAAsHACAAEQEACw== +valid valid linking.wast:191 AGFzbQEAAAABCgJgAAF/YAF/AX8CEwICTXQBaAAAAk10A3RhYgFwAAUDAwIAAQcIAQRjYWxsAAIJCAEAQQELAgEACg4CBABBBgsHACAAEQAACw== +valid valid linking.wast:229 AGFzbQEAAAABBAFgAAACDAECTXQDdGFiAXAAAAMCAQAJBwEAQQkLAQAKBAECAAs= +valid valid linking.wast:235 AGFzbQEAAAAGBgF/AEEFCwcFAQFnAwA= +valid valid linking.wast:237 AGFzbQEAAAACCQECRzEBZwN/AAYGAX8AIwALBwUBAWcDAQ== +valid valid linking.wast:244 AGFzbQEAAAABBAFgAAACDAECTXQDdGFiAXAAAAMCAQAJBwEAQQoLAQAKBAECAAs= +valid valid linking.wast:253 AGFzbQEAAAABBQFgAAF/AhYCAk10A3RhYgFwAAoCTXQDbWVtAgABAwIBAAkNAgBBBwsBAABBCQsBAAoGAQQAQQAL +valid valid linking.wast:267 AGFzbQEAAAABBQFgAAF/AgwBAk10A3RhYgFwAAoDAgEACRECAEEHCwEAAEEICwUAAAAAAAoGAQQAQQAL +valid valid linking.wast:279 AGFzbQEAAAABBQFgAAF/AgwBAk10A3RhYgFwAAoDAgEABQMBAAEJBwEAQQcLAQAKBgEEAEEACwsJAQBBgIAECwFk +valid valid linking.wast:291 AGFzbQEAAAAEBwJwAAFvAAEHFQIGdC1mdW5jAQAIdC1leHRlcm4BAQ== +valid valid linking.wast:297 AGFzbQEAAAACLQIJTXRhYmxlX2V4BnQtZnVuYwFwAAEJTXRhYmxlX2V4CHQtZXh0ZXJuAW8AAQ== +valid valid linking.wast:303 AGFzbQEAAAACFgEJTXRhYmxlX2V4BnQtZnVuYwFvAAE= +valid valid linking.wast:307 AGFzbQEAAAACGAEJTXRhYmxlX2V4CHQtZXh0ZXJuAXAAAQ== +valid valid linking.wast:314 AGFzbQEAAAABBgFgAX8BfwMCAQAFBAEBAQUHDgIDbWVtAgAEbG9hZAAACgkBBwAgAC0AAAsLEAEAQQoLCgABAgMEBQYHCAk= +valid valid linking.wast:324 AGFzbQEAAAABBgFgAX8BfwILAQJNbQRsb2FkAAADAgEABQMBAAEHEgIHTW0ubG9hZAAABGxvYWQAAQoJAQcAIAAtAAALCwwBAEEKCwbw8fLz9PU= +valid valid linking.wast:340 AGFzbQEAAAABBgFgAX8BfwILAQJNbQNtZW0CAAEDAgEABwgBBGxvYWQAAAoJAQcAIAAtAAALCw4BAEEFCwigoaKjpKWmpw== +valid valid linking.wast:354 AGFzbQEAAAACCwECTW0DbWVtAgAACwkBAEH//wMLAWE= +valid valid linking.wast:360 AGFzbQEAAAACCwECTW0DbWVtAgAACwkBAEGAgAQLAWE= +valid valid linking.wast:367 AGFzbQEAAAABBgFgAX8BfwIMAQJNbQNtZW0CAQEIAwIBAAcIAQRncm93AAAKCAEGACAAQAAL +valid valid linking.wast:385 AGFzbQEAAAABBAFgAAACJwMIc3BlY3Rlc3QFcHJpbnQAAAJNbQNtZW0CAAECTW0DdGFiAXAAAAsJAQBBAAsDYWJj +valid valid linking.wast:398 AGFzbQEAAAACCwECTW0DbWVtAgABCyICAEEACwNhYmMAQfb/EwsSenp6enp6enp6enp6enp6enp6 +valid valid linking.wast:410 AGFzbQEAAAABBAFgAAACCwECTW0DbWVtAgABAwIBAAQEAXAAAAkHAQBBAAsBAAoEAQIACwsJAQBBAAsDYWJj +valid valid linking.wast:422 AGFzbQEAAAABBQFgAAF/AwMCAAAEBAFwAAEFAwEAAQcxBAZtZW1vcnkCAAV0YWJsZQEADWdldCBtZW1vcnlbMF0AAAxnZXQgdGFibGVbMF0AAQoRAgcAQQAtAAALBwBBABEAAAs= +valid valid linking.wast:436 AGFzbQEAAAABCAJgAAF/YAAAAhsCAk1zBm1lbW9yeQIAAQJNcwV0YWJsZQFwAAEDAwIAAQgBAQkHAQBBAAsBAAoMAgYAQa29AwsDAAALCwsBAEEACwVoZWxsbw== +valid valid load.wast:3 AGFzbQEAAAABFQRgA39/fwF/YAABf2AAAGACf38BfwMnJgECAQECAQEBAQEBAwMBAAEBAQEBAQECAQIBAQICAgIBAQEBAQEBBAUBcAEBAQUDAQABBgYBfwFBAAsHtwUlC2FzLWJyLXZhbHVlAAANYXMtYnJfaWYtY29uZAABDmFzLWJyX2lmLXZhbHVlAAITYXMtYnJfaWYtdmFsdWUtY29uZAADEWFzLWJyX3RhYmxlLWluZGV4AAQRYXMtYnJfdGFibGUtdmFsdWUABRdhcy1icl90YWJsZS12YWx1ZS1pbmRleAAGD2FzLXJldHVybi12YWx1ZQAHCmFzLWlmLWNvbmQACAphcy1pZi10aGVuAAkKYXMtaWYtZWxzZQAKD2FzLXNlbGVjdC1maXJzdAALEGFzLXNlbGVjdC1zZWNvbmQADA5hcy1zZWxlY3QtY29uZAANDWFzLWNhbGwtZmlyc3QADwthcy1jYWxsLW1pZAAQDGFzLWNhbGwtbGFzdAARFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAEhRhcy1jYWxsX2luZGlyZWN0LW1pZAATFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAAUFmFzLWNhbGxfaW5kaXJlY3QtaW5kZXgAFRJhcy1sb2NhbC5zZXQtdmFsdWUAFhJhcy1sb2NhbC50ZWUtdmFsdWUAFxNhcy1nbG9iYWwuc2V0LXZhbHVlABgPYXMtbG9hZC1hZGRyZXNzABkQYXMtbG9hZE4tYWRkcmVzcwAaEGFzLXN0b3JlLWFkZHJlc3MAGw5hcy1zdG9yZS12YWx1ZQAcEWFzLXN0b3JlTi1hZGRyZXNzAB0PYXMtc3RvcmVOLXZhbHVlAB4QYXMtdW5hcnktb3BlcmFuZAAfDmFzLWJpbmFyeS1sZWZ0ACAPYXMtYmluYXJ5LXJpZ2h0ACEPYXMtdGVzdC1vcGVyYW5kACIPYXMtY29tcGFyZS1sZWZ0ACMQYXMtY29tcGFyZS1yaWdodAAkE2FzLW1lbW9yeS5ncm93LXNpemUAJQkHAQBBAAsBDgqGBCYMAAJ/QQAoAgAMAAsLDAACQEEAKAIADQALCxEAAn9BACgCAEEBDQAaQQcLCxEAAn9BBkEAKAIADQAaQQcLCw8AAkBBACgCAA4CAAAACwsTAAJ/QQAoAgBBAQ4CAAAAQQcLCxIAAn9BBkEAKAIADgEAAEEHCwsIAEEAKAIADwsPAEEAKAIABH9BAAVBAQsLDwBBAQR/QQAoAgAFQQALCw8AQQAEf0EABUEAKAIACwsMAEEAKAIAIAAgARsLDAAgAEEAKAIAIAEbCwwAQQBBAUEAKAIAGwsEAEF/Cw0AQQAoAgBBAkEDEA4LDQBBAUEAKAIAQQMQDgsNAEEBQQJBACgCABAOCxAAQQAoAgBBAkEDQQARAAALEABBAUEAKAIAQQNBABEAAAsQAEEBQQJBACgCAEEAEQAACxAAQQFBAkEDQQAoAgARAAALCwEBf0EAKAIAIQALCwEBf0EAKAIAIgALCwEBf0EAKAIAJAALCgBBACgCACgCAAsKAEEAKAIALAAACwwAQQAoAgBBBzYCAAsMAEECQQAoAgA2AgALDABBACwAAEEHOgAACwwAQQJBACgCADsBAAsJAEHkACgCAGcLCwBB5AAoAgBBCmoLCwBBCkHkACgCAGsLCQBB5AAoAgBFCwsAQeQAKAIAQQpMCwsAQQpB5AAoAgBHCwoAQeQAKAIAQAAL +invalid invalid load.wast:312 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAKAIACw== +invalid invalid load.wast:316 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEALAAACw== +invalid invalid load.wast:320 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEALQAACw== +invalid invalid load.wast:324 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEALgEACw== +invalid invalid load.wast:328 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEALwEACw== +invalid invalid load.wast:332 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAKQMACw== +invalid invalid load.wast:336 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAMAAACw== +invalid invalid load.wast:340 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAMQAACw== +invalid invalid load.wast:344 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAMgEACw== +invalid invalid load.wast:348 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAMwEACw== +invalid invalid load.wast:352 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEANAIACw== +invalid invalid load.wast:356 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEANQIACw== +invalid invalid load.wast:360 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAKgIACw== +invalid invalid load.wast:364 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEAKwMACw== +invalid invalid load.wast:371 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABCgwBCgBDAAAAACgCAAs= +invalid invalid load.wast:372 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABCgwBCgBDAAAAACwAAAs= +invalid invalid load.wast:373 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABCgwBCgBDAAAAAC0AAAs= +invalid invalid load.wast:374 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABCgwBCgBDAAAAAC4BAAs= +invalid invalid load.wast:375 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABCgwBCgBDAAAAAC8BAAs= +invalid invalid load.wast:376 AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABCgwBCgBDAAAAACkDAAs= +invalid invalid load.wast:377 AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABCgwBCgBDAAAAADAAAAs= +invalid invalid load.wast:378 AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABCgwBCgBDAAAAADEAAAs= +invalid invalid load.wast:379 AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABCgwBCgBDAAAAADIBAAs= +invalid invalid load.wast:380 AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABCgwBCgBDAAAAADMBAAs= +invalid invalid load.wast:381 AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABCgwBCgBDAAAAADQCAAs= +invalid invalid load.wast:382 AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABCgwBCgBDAAAAADUCAAs= +invalid invalid load.wast:383 AGFzbQEAAAABBQFgAAF9AwIBAAUDAQABCgwBCgBDAAAAACoCAAs= +invalid invalid load.wast:384 AGFzbQEAAAABBQFgAAF8AwIBAAUDAQABCgwBCgBDAAAAACsDAAs= +invalid invalid load.wast:388 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCAEGACgCABoL +invalid invalid load.wast:397 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDQELAEEAAkAoAgAaCws= +invalid invalid load.wast:407 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDQELAEEAA0AoAgAaCws= +invalid invalid load.wast:417 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDwENAEEAQQAEQCgCABoLCw== +invalid invalid load.wast:427 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKEgEQAEEAQQAEf0EABSgCAAsaCw== +invalid invalid load.wast:437 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDwENAEEAAkAoAgAMABoLCw== +invalid invalid load.wast:447 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKEQEPAEEAAkAoAgBBAQ0AGgsL +invalid invalid load.wast:457 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKEAEOAEEAAkAoAgAOAAAaCws= +invalid invalid load.wast:467 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCQEHACgCAA8aCw== +invalid invalid load.wast:476 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDQELACgCAEEBQQIbGgs= +invalid invalid load.wast:485 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQMBAAAKDwIIACgCABABGgsEACAACw== +invalid invalid load.wast:495 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQUDAQAACQcBAEEACwEAChUCBAAgAAsOAAJ/KAIAQQARAAAaCws= +invalid invalid load.wast:512 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDgEMAQF/KAIAIQAgABoL +invalid invalid load.wast:522 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKDAEKAQF/KAIAIgAaCw== +invalid invalid load.wast:532 AGFzbQEAAAABBAFgAAADAgEABQMBAAAGBgF/AUEACwoMAQoAKAIAJAAjABoL +invalid invalid load.wast:542 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCgEIACgCAEAAGgs= +invalid invalid load.wast:551 AGFzbQEAAAABBAFgAAADAgEABQMBAAAKCwEJACgCACgCABoL +invalid invalid load.wast:560 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDAEKACgCAEEBNgIACw== +valid valid local_get.wast:3 AGFzbQEAAAABNgpgAAF/YAABfmAAAX1gAAF8YAF/AX9gAX4BfmABfQF9YAF8AXxgBX59fH9/AGAFfn18f38BfAMUEwABAgMEBQYHCAkEBAQEBAQEBAQHswITDnR5cGUtbG9jYWwtaTMyAAAOdHlwZS1sb2NhbC1pNjQAAQ50eXBlLWxvY2FsLWYzMgACDnR5cGUtbG9jYWwtZjY0AAMOdHlwZS1wYXJhbS1pMzIABA50eXBlLXBhcmFtLWk2NAAFDnR5cGUtcGFyYW0tZjMyAAYOdHlwZS1wYXJhbS1mNjQABwp0eXBlLW1peGVkAAgEcmVhZAAJDmFzLWJsb2NrLXZhbHVlAAoNYXMtbG9vcC12YWx1ZQALC2FzLWJyLXZhbHVlAAwOYXMtYnJfaWYtdmFsdWUADRNhcy1icl9pZi12YWx1ZS1jb25kAA4RYXMtYnJfdGFibGUtdmFsdWUADw9hcy1yZXR1cm4tdmFsdWUAEAphcy1pZi10aGVuABEKYXMtaWYtZWxzZQASCo4CEwYBAX8gAAsGAQF+IAALBgEBfSAACwYBAXwgAAsEACAACwQAIAALBAAgAAsEACAACywDAX0CfgF8IABQGiABjBogApoaIANFGiAERRogBYwaIAZQGiAHUBogCJoaCz8DAX0CfgF8QwAAsEAhBUIGIQZEAAAAAAAAIEAhCCAAuiABuyACIAO4IAS3IAW7IAa6IAe6IAigoKCgoKCgoAsHAAJ/IAALCwcAA38gAAsLCQACfyAADAALCwsAAn8gAEEBDQALCwsAAn8gACAADQALCx0AAkACQAJAIAAOAgABAkEADwtBAQ8LQQIPC0EDCwUAIAAPCwwAIAAEfyAABUEACwsMACAABH9BAQUgAAsL +invalid invalid local_get.wast:153 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcBAX0gAEUL +invalid invalid local_get.wast:157 AGFzbQEAAAABBQFgAAF8AwIBAAoLAQkCAXwBfiABmgs= +invalid invalid local_get.wast:169 AGFzbQEAAAABBgFgAX0BfwMCAQAKBwEFACAARQs= +invalid invalid local_get.wast:173 AGFzbQEAAAABBwFgAnx+AXwDAgEACgcBBQAgAZoL +invalid invalid local_get.wast:181 AGFzbQEAAAABBAFgAAADAgEACggBBgEBfyAACw== +invalid invalid local_get.wast:185 AGFzbQEAAAABBAFgAAADAgEACggBBgEBfiAACw== +invalid invalid local_get.wast:189 AGFzbQEAAAABBAFgAAADAgEACggBBgEBfSAACw== +invalid invalid local_get.wast:193 AGFzbQEAAAABBAFgAAADAgEACggBBgEBfCAACw== +invalid invalid local_get.wast:201 AGFzbQEAAAABBAFgAAADAgEACgsBCQIBfwF+IAMaCw== +invalid invalid local_get.wast:205 AGFzbQEAAAABBAFgAAADAgEACg4BDAIBfwF+IPek6gYaCw== +invalid invalid local_get.wast:210 AGFzbQEAAAABBgFgAn9+AAMCAQAKBwEFACACGgs= +invalid invalid local_get.wast:214 AGFzbQEAAAABBgFgAn9+AAMCAQAKCwEJACD38s7UAhoL +invalid invalid local_get.wast:219 AGFzbQEAAAABBQFgAX8AAwIBAAoLAQkCAX8BfiADGgs= +invalid invalid local_get.wast:223 AGFzbQEAAAABBQFgAX4AAwIBAAoOAQwCAX8BfiD3qJlmGgs= +valid valid local_set.wast:3 AGFzbQEAAAABJQdgAABgAX8AYAF+AGABfQBgAXwAYAV+fXx/fwBgBX59fH9/AX4DFBMAAAAAAQIDBAUGAQEBAQEBAQEBB7QCEw50eXBlLWxvY2FsLWkzMgAADnR5cGUtbG9jYWwtaTY0AAEOdHlwZS1sb2NhbC1mMzIAAg50eXBlLWxvY2FsLWY2NAADDnR5cGUtcGFyYW0taTMyAAQOdHlwZS1wYXJhbS1pNjQABQ50eXBlLXBhcmFtLWYzMgAGDnR5cGUtcGFyYW0tZjY0AAcKdHlwZS1taXhlZAAIBXdyaXRlAAkOYXMtYmxvY2stdmFsdWUACg1hcy1sb29wLXZhbHVlAAsLYXMtYnItdmFsdWUADA5hcy1icl9pZi12YWx1ZQANE2FzLWJyX2lmLXZhbHVlLWNvbmQADhFhcy1icl90YWJsZS12YWx1ZQAPD2FzLXJldHVybi12YWx1ZQAQCmFzLWlmLXRoZW4AEQphcy1pZi1lbHNlABIK0gITCAEBf0EAIQALCAEBfkIAIQALCwEBfUMAAAAAIQALDwEBfEQAAAAAAAAAACEACwYAQQohAAsGAEILIQALCQBDmpkxQSEACw0ARGZmZmZmZihAIQALQAMBfQJ+AXxCACEAQwAAAAAhAUQAAAAAAAAAACECQQAhA0EAIQRDAAAAACEFQgAhBkIAIQdEAAAAAAAAAAAhCAtPAwF9An4BfEOamZm+IQFBKCEDQXkhBEMAALBAIQVCBiEGRAAAAAAAACBAIQggALogAbsgAiADuCAEtyAFuyAGuiAHuiAIoKCgoKCgoKCwCwkAAkBBASEACwsJAANAQQMhAAsLCwACQEEJIQAMAAsLDQACQEEIIQBBAQ0ACwsNAAJAQQZBCSEADQALCw4AAkBBCiEAQQEOAAALCwcAQQchAA8LCwAgAARAQQMhAAsLDAAgAARABUEBIQALCw== +invalid invalid local_set.wast:148 AGFzbQEAAAABBAFgAAADAgEACgkBBwEBfwEhAAs= +invalid invalid local_set.wast:152 AGFzbQEAAAABBAFgAAADAgEACg0BCwEBf0MAAAAAIQAL +invalid invalid local_set.wast:156 AGFzbQEAAAABBAFgAAADAgEAChEBDwEBfUQAAAAAAAAAACEACw== +invalid invalid local_set.wast:160 AGFzbQEAAAABBAFgAAADAgEAChMBEQIBfAF+RAAAAAAAAAAAIQEL +invalid invalid local_set.wast:169 AGFzbQEAAAABBQFgAX8AAwIBAAoHAQUAASEACw== +invalid invalid local_set.wast:173 AGFzbQEAAAABBQFgAX8AAwIBAAoLAQkAQwAAAAAhAAs= +invalid invalid local_set.wast:177 AGFzbQEAAAABBQFgAX0AAwIBAAoPAQ0ARAAAAAAAAAAAIQAL +invalid invalid local_set.wast:181 AGFzbQEAAAABBgFgAnx+AAMCAQAKDwENAEQAAAAAAAAAACEBCw== +invalid invalid local_set.wast:186 AGFzbQEAAAABBQFgAX8AAwIBAAoGAQQAIQAL +invalid invalid local_set.wast:194 AGFzbQEAAAABBQFgAX8AAwIBAAoLAQkAQQACQCEACws= +invalid invalid local_set.wast:203 AGFzbQEAAAABBQFgAX8AAwIBAAoLAQkAQQADQCEACws= +invalid invalid local_set.wast:212 AGFzbQEAAAABBQFgAX8AAwIBAAoNAQsAQQBBAQRAIQALCw== +invalid invalid local_set.wast:221 AGFzbQEAAAABBQFgAX8AAwIBAAoQAQ4AQQBBAAR/QQAFIQALCw== +invalid invalid local_set.wast:230 AGFzbQEAAAABBQFgAX8AAwIBAAoNAQsAQQACQCEADAALCw== +invalid invalid local_set.wast:239 AGFzbQEAAAABBQFgAX8AAwIBAAoNAQsAQQACQCEADQALCw== +invalid invalid local_set.wast:248 AGFzbQEAAAABBQFgAX8AAwIBAAoOAQwAQQACQCEADgAACws= +invalid invalid local_set.wast:257 AGFzbQEAAAABBQFgAX8AAwIBAAoHAQUAIQAPCw== +invalid invalid local_set.wast:265 AGFzbQEAAAABBQFgAX8AAwIBAAoLAQkAIQBBAUECGws= +invalid invalid local_set.wast:273 AGFzbQEAAAABCgJgAX8AYAF/AX8DAwIAAQoNAgYAIQAQAQsEACAACw== +invalid invalid local_set.wast:282 AGFzbQEAAAABCgJgAX8Bf2ABfwADAwIAAQQFAXABAQEJBwEAQQALAQAKEwIEACAACwwAAn8hAEEAEQAACws= +invalid invalid local_set.wast:301 AGFzbQEAAAABBQFgAX0AAwIBAAoNAQsBAX9DAAAAACEBCw== +invalid invalid local_set.wast:305 AGFzbQEAAAABBgFgAn5/AAMCAQAKDQELAQF9QwAAAAAhAQs= +invalid invalid local_set.wast:309 AGFzbQEAAAABBQFgAX4AAwIBAAoMAQoCAXwBfkIAIQEL +invalid invalid local_set.wast:317 AGFzbQEAAAABBgFgAX8BfwMCAQAKCAEGAEEBIQAL +invalid invalid local_set.wast:321 AGFzbQEAAAABBgFgAX4BfgMCAQAKCAEGAEIBIQAL +invalid invalid local_set.wast:325 AGFzbQEAAAABBgFgAX0BfQMCAQAKCwEJAEMAAIA/IQAL +invalid invalid local_set.wast:329 AGFzbQEAAAABBgFgAXwBfAMCAQAKDwENAEQAAAAAAADwPyEACw== +invalid invalid local_set.wast:337 AGFzbQEAAAABBAFgAAADAgEACgwBCgIBfwF+QQAhAws= +invalid invalid local_set.wast:341 AGFzbQEAAAABBAFgAAADAgEACg8BDQIBfwF+QQAh96TqBgs= +invalid invalid local_set.wast:346 AGFzbQEAAAABBgFgAn9+AAMCAQAKCAEGAEEAIQIL +invalid invalid local_set.wast:350 AGFzbQEAAAABBgFgAn9+AAMCAQAKDAEKAEEAIffyztQCCw== +invalid invalid local_set.wast:355 AGFzbQEAAAABBQFgAX8AAwIBAAoMAQoCAX8BfkEAIQML +invalid invalid local_set.wast:359 AGFzbQEAAAABBQFgAX4AAwIBAAoPAQ0CAX8BfkEAIfeomWYL +valid valid local_tee.wast:3 AGFzbQEAAAABWBBgA39/fwF/YAABf2AAAX5gAAF9YAABfGABfwF/YAF+AX5gAX0BfWABfAF8YAV+fXx/fwBgBX59fH9/AX5gBX59fH9/AXxgAABgAX8AYAJ/fwF/YAF+AX8DOjkBAgMEBQYHCAkKCwwFBQUFBQUFDQUFDQUFBQUFBQ4OBQAFBQUFBQUFDAUMBQUNDQ0NBwUFBQUFDwUEBQFwAQEBBQMBAAEGBgF/AUEACwfPBzcOdHlwZS1sb2NhbC1pMzIAAA50eXBlLWxvY2FsLWk2NAABDnR5cGUtbG9jYWwtZjMyAAIOdHlwZS1sb2NhbC1mNjQAAw50eXBlLXBhcmFtLWkzMgAEDnR5cGUtcGFyYW0taTY0AAUOdHlwZS1wYXJhbS1mMzIABg50eXBlLXBhcmFtLWY2NAAHCnR5cGUtbWl4ZWQACAV3cml0ZQAJBnJlc3VsdAAKDmFzLWJsb2NrLWZpcnN0AAwMYXMtYmxvY2stbWlkAA0NYXMtYmxvY2stbGFzdAAODWFzLWxvb3AtZmlyc3QADwthcy1sb29wLW1pZAAQDGFzLWxvb3AtbGFzdAARC2FzLWJyLXZhbHVlABINYXMtYnJfaWYtY29uZAATDmFzLWJyX2lmLXZhbHVlABQTYXMtYnJfaWYtdmFsdWUtY29uZAAVEWFzLWJyX3RhYmxlLWluZGV4ABYRYXMtYnJfdGFibGUtdmFsdWUAFxdhcy1icl90YWJsZS12YWx1ZS1pbmRleAAYD2FzLXJldHVybi12YWx1ZQAZCmFzLWlmLWNvbmQAGgphcy1pZi10aGVuABsKYXMtaWYtZWxzZQAcD2FzLXNlbGVjdC1maXJzdAAdEGFzLXNlbGVjdC1zZWNvbmQAHg5hcy1zZWxlY3QtY29uZAAfDWFzLWNhbGwtZmlyc3QAIQthcy1jYWxsLW1pZAAiDGFzLWNhbGwtbGFzdAAjFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAJBRhcy1jYWxsX2luZGlyZWN0LW1pZAAlFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAAmFmFzLWNhbGxfaW5kaXJlY3QtaW5kZXgAJxJhcy1sb2NhbC5zZXQtdmFsdWUAKBJhcy1sb2NhbC50ZWUtdmFsdWUAKRNhcy1nbG9iYWwuc2V0LXZhbHVlACoPYXMtbG9hZC1hZGRyZXNzACsQYXMtbG9hZE4tYWRkcmVzcwAsEGFzLXN0b3JlLWFkZHJlc3MALQ5hcy1zdG9yZS12YWx1ZQAuEWFzLXN0b3JlTi1hZGRyZXNzAC8PYXMtc3RvcmVOLXZhbHVlADAQYXMtdW5hcnktb3BlcmFuZAAxDmFzLWJpbmFyeS1sZWZ0ADIPYXMtYmluYXJ5LXJpZ2h0ADMPYXMtdGVzdC1vcGVyYW5kADQPYXMtY29tcGFyZS1sZWZ0ADUQYXMtY29tcGFyZS1yaWdodAA2EmFzLWNvbnZlcnQtb3BlcmFuZAA3E2FzLW1lbW9yeS5ncm93LXNpemUAOAkHAQBBAAsBIAqDBzkIAQF/QQAiAAsIAQF+QgAiAAsLAQF9QwAAAAAiAAsPAQF8RAAAAAAAAAAAIgALBgBBCiIACwYAQgsiAAsJAEOamTFBIgALDQBEZmZmZmZmKEAiAAtSAwF9An4BfEIAIgBQGkMAAAAAIgGMGkQAAAAAAAAAACICmhpBACIDRRpBACIERRpDAAAAACIFjBpCACIGUBpCACIHUBpEAAAAAAAAAAAiCJoaC1UDAX0CfgF8Q5qZmb4iARpBKCIDGkF5IgQaQwAAsEAiBRpCBiIGGkQAAAAAAAAgQCIIGiAAuiABuyACIAO4IAS3IAW7IAa6IAe6IAigoKCgoKCgoLALTwMBfQJ+AXxCASIAukMAAABAIgG7RGZmZmZmZgpAIgJBBCIDuEEFIgS3QwAAsEAiBbtCBiIGukIAIge6RAAAAAAAACBAIgigoKCgoKCgoAsCAAsLAAJ/QQEiABALCwsNAAJ/EAtBASIAEAsLCw0AAn8QCxALQQEiAAsLCwADf0EDIgAQCwsLDQADfxALQQQiABALCwsNAAN/EAsQC0EFIgALCwsAAn9BCSIADAALCwsAAkBBASIADQALCxAAAn9BCCIAQQENABpBBwsLEAACf0EGQQkiAA0AGkEHCwsOAAJAQQAiAA4CAAAACwsSAAJ/QQoiAEEBDgIAAABBBwsLEQACf0EGQQsiAA4BAABBBwsLBwBBByIADwsOAEECIgAEf0EABUEBCwsOACAABH9BAyIABSAACwsOACAABH8gAAVBBCIACwsLAEEFIgAgACABGwsLACAAQQYiACABGwsLAEEAQQFBByIAGwsEAEF/CwwAQQwiAEECQQMQIAsMAEEBQQ0iAEEDECALDABBAUECQQ4iABAgCw8AQQEiAEECQQNBABEAAAsPAEEBQQIiAEEDQQARAAALDwBBAUECQQMiAEEAEQAACw8AQQFBAkEDQQAiABEAAAsKAQF/QQEiACEACwgAQQEiACIACwoBAX9BASIAJAALCQBBASIAKAIACwkAQQMiACwAAAsLAEEeIgBBBzYCAAsLAEECQQEiADYCAAsLAEEBIgBBBzoAAAsLAEECQQEiADsBAAsKAEPi8YB/IgCMCwkAQQMiAEEKagsJAEEKQQQiAGsLBwBBACIARQsJAEErIgBBCkwLCQBBCkEqIgBHCwcAQikiAKcLCABBKCIAQAAL +invalid invalid local_tee.wast:371 AGFzbQEAAAABBQFgAAF+AwIBAAoKAQgBAX9BACIACw== +invalid invalid local_tee.wast:375 AGFzbQEAAAABBAFgAAADAgEACg4BDAEBfUMAAAAAIgBFCw== +invalid invalid local_tee.wast:379 AGFzbQEAAAABBAFgAAADAgEACg0BCwIBfAF+QgAiAZoL +invalid invalid local_tee.wast:384 AGFzbQEAAAABBAFgAAADAgEACgkBBwEBfwEiAAs= +invalid invalid local_tee.wast:388 AGFzbQEAAAABBAFgAAADAgEACg0BCwEBf0MAAAAAIgAL +invalid invalid local_tee.wast:392 AGFzbQEAAAABBAFgAAADAgEAChEBDwEBfUQAAAAAAAAAACIACw== +invalid invalid local_tee.wast:396 AGFzbQEAAAABBAFgAAADAgEAChMBEQIBfAF+RAAAAAAAAAAAIgEL +invalid invalid local_tee.wast:417 AGFzbQEAAAABBQFgAX8AAwIBAAoHAQUAASIACw== +invalid invalid local_tee.wast:421 AGFzbQEAAAABBQFgAX8AAwIBAAoLAQkAQwAAAAAiAAs= +invalid invalid local_tee.wast:425 AGFzbQEAAAABBQFgAX0AAwIBAAoPAQ0ARAAAAAAAAAAAIgAL +invalid invalid local_tee.wast:429 AGFzbQEAAAABBgFgAnx+AAMCAQAKDwENAEQAAAAAAAAAACIBCw== +invalid invalid local_tee.wast:434 AGFzbQEAAAABBQFgAX8AAwIBAAoHAQUAIgAaCw== +invalid invalid local_tee.wast:442 AGFzbQEAAAABBQFgAX8AAwIBAAoMAQoAQQACQCIAGgsL +invalid invalid local_tee.wast:451 AGFzbQEAAAABBQFgAX8AAwIBAAoMAQoAQQADQCIAGgsL +invalid invalid local_tee.wast:460 AGFzbQEAAAABBQFgAX8AAwIBAAoOAQwAQQBBAARAIgAaCws= +invalid invalid local_tee.wast:469 AGFzbQEAAAABBQFgAX8AAwIBAAoRAQ8AQQBBAAR/QQAFIgALGgs= +invalid invalid local_tee.wast:478 AGFzbQEAAAABBQFgAX8AAwIBAAoOAQwAQQACQCIADAAaCws= +invalid invalid local_tee.wast:487 AGFzbQEAAAABBQFgAX8AAwIBAAoQAQ4AQQACQCIAQQENABoLCw== +invalid invalid local_tee.wast:496 AGFzbQEAAAABBQFgAX8AAwIBAAoPAQ0AQQACQCIADgAAGgsL +invalid invalid local_tee.wast:505 AGFzbQEAAAABBQFgAX8AAwIBAAoIAQYAIgAPGgs= +invalid invalid local_tee.wast:513 AGFzbQEAAAABBQFgAX8AAwIBAAoMAQoAIgBBAUECGxoL +invalid invalid local_tee.wast:521 AGFzbQEAAAABCgJgAX8AYAF/AX8DAwIAAQoOAgcAIgAQARoLBAAgAAs= +invalid invalid local_tee.wast:530 AGFzbQEAAAABCgJgAX8Bf2ABfwADAwIAAQQFAXABAQEJBwEAQQALAQAKFAIEACAACw0AAn8iAEEAEQAAGgsL +invalid invalid local_tee.wast:546 AGFzbQEAAAABBQFgAX8AAwIBAAoLAQkAIgAhACAAGgs= +invalid invalid local_tee.wast:554 AGFzbQEAAAABBQFgAX8AAwIBAAoJAQcAIgAiABoL +invalid invalid local_tee.wast:562 AGFzbQEAAAABBQFgAX8AAwIBAAYGAX8BQQALCgsBCQAiACQAIwAaCw== +invalid invalid local_tee.wast:571 AGFzbQEAAAABBQFgAX8AAwIBAAUDAQAACgkBBwAiAEAAGgs= +invalid invalid local_tee.wast:580 AGFzbQEAAAABBQFgAX8AAwIBAAUDAQAACgoBCAAiACgCABoL +invalid invalid local_tee.wast:589 AGFzbQEAAAABBQFgAX8AAwIBAAUDAQABCgsBCQAiAEEBNgIACw== +invalid invalid local_tee.wast:599 AGFzbQEAAAABBQFgAX0AAwIBAAoNAQsBAX9DAAAAACIBCw== +invalid invalid local_tee.wast:603 AGFzbQEAAAABBgFgAn5/AAMCAQAKDQELAQF9QwAAAAAiAQs= +invalid invalid local_tee.wast:607 AGFzbQEAAAABBQFgAX4AAwIBAAoMAQoCAXwBfkIAIgEL +invalid invalid local_tee.wast:615 AGFzbQEAAAABBAFgAAADAgEACg0BCwIBfwF+QQAiAxoL +invalid invalid local_tee.wast:619 AGFzbQEAAAABBAFgAAADAgEAChABDgIBfwF+QQAi96TqBhoL +invalid invalid local_tee.wast:624 AGFzbQEAAAABBgFgAn9+AAMCAQAKCQEHAEEAIgIaCw== +invalid invalid local_tee.wast:628 AGFzbQEAAAABBgFgAn9+AAMCAQAKDQELAEEAIvfyztQCGgs= +invalid invalid local_tee.wast:633 AGFzbQEAAAABBQFgAX8AAwIBAAoNAQsCAX8BfkEAIgMaCw== +invalid invalid local_tee.wast:637 AGFzbQEAAAABBQFgAX4AAwIBAAoQAQ4CAX8BfkEAIveomWYaCw== +valid valid loop.wast:3 AGFzbQEAAAABTg5gAn9/AX9gAABgAAF/YAF/AGADf3x/A398f2AAA39+f2ABfwF/YAACf39gAAJ9fWAAA39/fmADf39+AGACf38Cf39gAX4BfmACfX0BfQM5OAEBAgICAgICAgECAgICAgIAAgICAQECBgICAQICAgICAgICAgICAgICCQICAgICAgICAgIMDA0BBAUBcAEBAQUDAQABBgYBfwFBAAsHyAY1BWVtcHR5AAEIc2luZ3VsYXIAAgVtdWx0aQADBm5lc3RlZAAEBGRlZXAABQ9hcy1zZWxlY3QtZmlyc3QABg1hcy1zZWxlY3QtbWlkAAcOYXMtc2VsZWN0LWxhc3QACA9hcy1pZi1jb25kaXRpb24ACQphcy1pZi10aGVuAAoKYXMtaWYtZWxzZQALDmFzLWJyX2lmLWZpcnN0AAwNYXMtYnJfaWYtbGFzdAANEWFzLWJyX3RhYmxlLWZpcnN0AA4QYXMtYnJfdGFibGUtbGFzdAAPFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAERRhcy1jYWxsX2luZGlyZWN0LW1pZAASFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAATDmFzLXN0b3JlLWZpcnN0ABQNYXMtc3RvcmUtbGFzdAAVFGFzLW1lbW9yeS5ncm93LXZhbHVlABYNYXMtY2FsbC12YWx1ZQAYD2FzLXJldHVybi12YWx1ZQAZD2FzLWRyb3Atb3BlcmFuZAAaC2FzLWJyLXZhbHVlABsSYXMtbG9jYWwuc2V0LXZhbHVlABwSYXMtbG9jYWwudGVlLXZhbHVlAB0TYXMtZ2xvYmFsLnNldC12YWx1ZQAeD2FzLWxvYWQtb3BlcmFuZAAfEGFzLXVuYXJ5LW9wZXJhbmQAIBFhcy1iaW5hcnktb3BlcmFuZAAhD2FzLXRlc3Qtb3BlcmFuZAAiEmFzLWNvbXBhcmUtb3BlcmFuZAAjEmFzLWJpbmFyeS1vcGVyYW5kcwAkE2FzLWNvbXBhcmUtb3BlcmFuZHMAJRFhcy1taXhlZC1vcGVyYW5kcwAmCmJyZWFrLWJhcmUAJwticmVhay12YWx1ZQAoEWJyZWFrLW11bHRpLXZhbHVlACkOYnJlYWstcmVwZWF0ZWQAKgticmVhay1pbm5lcgArCmNvbnQtaW5uZXIALAVwYXJhbQAtBnBhcmFtcwAuCXBhcmFtcy1pZAAvC3BhcmFtLWJyZWFrADAMcGFyYW1zLWJyZWFrADEPcGFyYW1zLWlkLWJyZWFrADIHZWZmZWN0cwAzBXdoaWxlADQDZm9yADUHbmVzdGluZwA2CHR5cGUtdXNlADcJBwEAQQALARAKgws4AgALCAADQAsDQAsLCwADQAELA39BBwsLPAADQBAAEAAQABAACwN/EAAQAEEIEAALGgMFEAAQABAAQQgQABAAEAAQAEIHEAAQABAAEABBCRAACxoaCxUAA38DQBAAAkALAQsDfxAAQQkLCwt/AAN/An8DfwJ/A38CfwN/An8DfwJ/A38CfwN/An8DfwJ/A38CfwN/An8DfwJ/A38CfwN/An8DfwJ/A38CfwN/An8DfwJ/A38CfwN/An8DfwJ/EABBlgELCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwwAA39BAQtBAkEDGwsMAEECA39BAQtBAxsLDABBAkEDA39BAQsbCwwAA39BAQsEQBAACwsPAEEBBH8Df0EBCwVBAgsLDwBBAQR/QQIFA39BAQsLCw4AAn8Df0EBC0ECDQALCw4AAn9BAgN/QQELDQALCxAAAn8Df0EBC0ECDgEAAAsLEAACf0ECA39BAQsOAQAACwsEACAACxEAAn8Df0EBC0ECQQARAAALCxEAAn9BAgN/QQELQQARAAALCxEAAn9BAUECA39BAAsRAAALCwwAA39BAQtBATYCAAsMAEEKA39BAQs2AgALCQADf0EBC0AACwQAIAALCQADf0EBCxAXCwgAA39BAQsPCwgAA39BAQsaCwwAAn8Df0EBCwwACwsNAQF/A39BAQshACAACwsBAX8Df0EBCyIACwsAA39BAQskACMACwoAA39BAQsoAgALCgADfxAAQQ0LaAsRAAN/EABBAwsDfxAAQQQLbAsKAAN/EABBDQtFCxcAA30QAEMAAEBACwN9EABDAABAQAteCw4AAwcQAEEDEABBBAtsCxQAAwgQAEMAAEBAEABDAABAQAteCxEAAwcQAEEDEABBBAtBBWpsCzQAAkADQAwBDAAACwsCQANAQQENAQALCwJAA0BBAA4AAQALCwJAA0BBAQ4CAQEBAAsLQRMLFwACf0EAAwMCQEESDAILQRQMAAtBEwsLJwACCUEAQQBCAAMKAkBBEkFuQhIMAgtBFEFsQhQMAAtBE0FtQhMLCzQAAn8Df0ESDAFBEwwBQRRBAA0BGkEUQQENARpBFQwBQRZBAA4AAUEXQQEOAgEBAUEVCwsLZgEBf0EAIQAgAAJ/A38Cf0EBDAILCwtqIQAgAAJ/A38Df0ECDAILCwtqIQAgAAJ/A38CfwN/QQQMAQsLCwtqIQAgAAJ/A39BCAwBaAsLaiEAIAACfwN/A39BEAwCC2gLC2ohACAACzABAX9BACEAIAADfwN/DAELC2ohACAAA38MAGgLaiEAIAADfwN/DAELaAtqIQAgAAsKAEEBAwZBAmoLCwoAQQFBAgMAagsLCgBBAUECAwsLagsVAQF/QQEDBkEEaiIAIABBCkkNAAsLGAEBf0EBQQIDAGoiAEEDIABBCkkNABoLCx4BAX9BACEAQQFBAgMLIABBAWohACAAQQpJDQALagsyAQF/AkADQEEBIQAgAEEDbCEAIABBBWshACAAQQdsIQAMASAAQeQAbCEACwsgAEFyRgslAQF+QgEhAQJAA0AgAFANASAAIAF+IQEgAEIBfSEADAALCyABCysBAn5CASEBQgIhAgJAA0AgAiAAVg0BIAEgAn4hASACQgF8IQIMAAsLIAELWgECfQJAA0AgAEMAAAAAWw0BIAEhAgJAA0AgAkMAAAAAWw0BIAJDAAAAAF0NAyADIAKSIQMgAkMAAABAkyECDAALCyADIACVIQMgAEMAAIA/kyEADAALCyADCz0AA0ALA39BAAsDAxoLQQBEAAAAAAAAAABBAAMECxoaGgN/QQALAwMaC0EARAAAAAAAAAAAQQADBAsaGhoL +invalid invalid loop.wast:601 AGFzbQEAAAABBAFgAAADAgEACgkBBwADQEEACws= +invalid invalid loop.wast:609 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAA0ALCw== +invalid invalid loop.wast:613 AGFzbQEAAAABBQFgAAF+AwIBAAoHAQUAA0ALCw== +invalid invalid loop.wast:617 AGFzbQEAAAABBQFgAAF9AwIBAAoHAQUAA0ALCw== +invalid invalid loop.wast:621 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAA0ALCw== +invalid invalid loop.wast:626 AGFzbQEAAAABBAFgAAADAgEACgkBBwADQEEBCws= +invalid invalid loop.wast:632 AGFzbQEAAAABBAFgAAADAgEACgsBCQADQEEBQQILCw== +invalid invalid loop.wast:638 AGFzbQEAAAABBQFgAAF/AwIBAAoHAQUAA38LCw== +invalid invalid loop.wast:644 AGFzbQEAAAABBgFgAAJ/fwMCAQAKBwEFAAMACws= +invalid invalid loop.wast:650 AGFzbQEAAAABBQFgAAF/AwIBAAoIAQYAA38BCws= +invalid invalid loop.wast:656 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCAEGAAMAAQsL +invalid invalid loop.wast:662 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAA39DAAAAAAsL +invalid invalid loop.wast:668 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCQEHAAMAQQALCw== +invalid invalid loop.wast:674 AGFzbQEAAAABBgFgAAJ/fwMCAQAKCwEJAEEBAwBBAgsL +invalid invalid loop.wast:680 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAA39BAUECCws= +invalid invalid loop.wast:686 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAA34AAAAbCws= +invalid invalid loop.wast:693 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAAJAA38LGgsL +invalid invalid loop.wast:702 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAANAA38LGgsL +invalid invalid loop.wast:711 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEABEADfwsaCws= +invalid invalid loop.wast:721 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoIAQYAAwEaCws= +invalid invalid loop.wast:727 AGFzbQEAAAABCQJgAABgAn98AAMCAQAKCQEHAAMBGhoLCw== +invalid invalid loop.wast:733 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoNAQsAQwAAAAADARoLCw== +invalid invalid loop.wast:739 AGFzbQEAAAABCQJgAABgAn1/AAMCAQAKDgEMAEMAAAAAAwEaGgsL +invalid invalid loop.wast:745 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoLAQkAAkADARoLCws= +invalid invalid loop.wast:751 AGFzbQEAAAABCQJgAABgAn98AAMCAQAKDAEKAAJAAwEaGgsLCw== +invalid invalid loop.wast:757 AGFzbQEAAAABCAJgAABgAX8AAwIBAAoQAQ4AAkBDAAAAAAMBGgsLCw== +invalid invalid loop.wast:763 AGFzbQEAAAABCQJgAABgAn1/AAMCAQAKEQEPAAJAQwAAAAADARoaCwsL +valid valid memory.wast:3 AGFzbQEAAAAFAwEAAA== +valid valid memory.wast:4 AGFzbQEAAAAFAwEAAQ== +valid valid memory.wast:5 AGFzbQEAAAAFBAEBAAA= +valid valid memory.wast:6 AGFzbQEAAAAFBAEBAAE= +valid valid memory.wast:7 AGFzbQEAAAAFBQEBAYAC +valid valid memory.wast:8 AGFzbQEAAAAFBgEBAICABA== +invalid valid memory.wast:11 AGFzbQEAAAACFAEIc3BlY3Rlc3QGbWVtb3J5AgAABQMBAAA= +valid valid memory.wast:13 AGFzbQEAAAABBQFgAAF/AwIBAAUEAQEAAAcLAQdtZW1zaXplAAAKBgEEAD8ACwsGAQBBAAsA +valid valid memory.wast:17 AGFzbQEAAAABBQFgAAF/AwIBAAUEAQEBAQcLAQdtZW1zaXplAAAKBgEEAD8ACwsHAQBBAAsBeA== +invalid invalid memory.wast:22 AGFzbQEAAAALBwEAQQALAXg= +invalid invalid memory.wast:25 AGFzbQEAAAABBAFgAAADAgEACgoBCABBACoCABoL +invalid invalid memory.wast:29 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAEMAAAAAOAIACw== +invalid invalid memory.wast:33 AGFzbQEAAAABBAFgAAADAgEACgoBCABBACwAABoL +invalid invalid memory.wast:37 AGFzbQEAAAABBAFgAAADAgEACgsBCQBBAEEAOgAACw== +invalid invalid memory.wast:41 AGFzbQEAAAABBAFgAAADAgEACgcBBQA/ABoL +invalid invalid memory.wast:45 AGFzbQEAAAABBAFgAAADAgEACgkBBwBBAEAAGgs= +invalid invalid memory.wast:51 AGFzbQEAAAAFBAEBAQA= +invalid invalid memory.wast:55 AGFzbQEAAAAFBQEAgYAE +invalid invalid memory.wast:59 AGFzbQEAAAAFBwEAgICAgAg= +invalid invalid memory.wast:63 AGFzbQEAAAAFBwEA/////w8= +invalid invalid memory.wast:67 AGFzbQEAAAAFBgEBAIGABA== +invalid invalid memory.wast:71 AGFzbQEAAAAFCAEBAICAgIAI +invalid invalid memory.wast:75 AGFzbQEAAAAFCAEBAP////8P +valid valid memory.wast:92 AGFzbQEAAAABEwRgAAF/YAABfGABfwF/YAF+AX4DDQwAAQICAgIDAwMDAwMFAwEAAQehAQwEZGF0YQAABGNhc3QAAQtpMzJfbG9hZDhfcwACC2kzMl9sb2FkOF91AAMMaTMyX2xvYWQxNl9zAAQMaTMyX2xvYWQxNl91AAULaTY0X2xvYWQ4X3MABgtpNjRfbG9hZDhfdQAHDGk2NF9sb2FkMTZfcwAIDGk2NF9sb2FkMTZfdQAJDGk2NF9sb2FkMzJfcwAKDGk2NF9sb2FkMzJfdQALCp8CDE4AQQAtAABBwQBGQQMtAABBpwFGcUEGLQAAQQBGQRMtAABBAEZxcUEULQAAQdcARkEXLQAAQc0ARnFBGC0AAEEARkH/By0AAEEARnFxcQs4AEEIQseffzcDAEEIKwMAQseff79hBEBEAAAAAAAAAAAPC0EJQgA3AABBD0HFgAE7AABBCSsAAAsOAEEIIAA6AABBCCwAAAsOAEEIIAA6AABBCC0AAAsOAEEIIAA7AQBBCC4BAAsOAEEIIAA7AQBBCC8BAAsOAEEIIAA8AABBCDAAAAsOAEEIIAA8AABBCDEAAAsOAEEIIAA9AQBBCDIBAAsOAEEIIAA9AQBBCDMBAAsOAEEIIAA+AgBBCDQCAAsOAEEIIAA+AgBBCDUCAAsLFAIAQQALBUFCQ6dEAEEUCwRXQVNN +valid valid memory.wast:246 AGFzbQEAAAABBgFgAX8BfwMCAQAFBAEBAQEGFgN/AEGQzgALfwBBkM4AC38AQZDOAAsHOgUGbWVtb3J5AgAKX19kYXRhX2VuZAMAC19fc3RhY2tfdG9wAwELX19oZWFwX2Jhc2UDAgRsb2FkAAAKCQEHACAALQAACw== +valid valid memory_copy.wast:6 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABCg0CAwABCwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:48 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABChYCDABBDUECQQP8CgAACwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:90 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABChYCDABBGUEPQQL8CgAACwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:132 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABChYCDABBDUEZQQP8CgAACwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:174 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABChYCDABBFEEWQQT8CgAACwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:216 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABChYCDABBGUEBQQP8CgAACwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:258 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABChYCDABBCkEMQQf8CgAACwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:300 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABChYCDABBDEEKQQf8CgAACwcAIAAtAAALCxQCAEECCwQDAQQBAEEMCwUHBQIDBg== +valid valid memory_copy.wast:342 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxcDA21lbQIAA3J1bgAAB2xvYWQ4X3UAAQoWAgwAIAAgASAC/AoAAAsHACAALQAACwsaAQBBAAsUAAECAwQFBgcICQoLDA0ODxAREhM= +valid valid memory_copy.wast:703 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxcDA21lbQIAA3J1bgAAB2xvYWQ4X3UAAQoWAgwAIAAgASAC/AoAAAsHACAALQAACwsbAQBBAAsVAAECAwQFBgcICQoLDA0ODxAREhMU +valid valid memory_copy.wast:1065 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxcDA21lbQIAA3J1bgAAB2xvYWQ4X3UAAQoWAgwAIAAgASAC/AoAAAsHACAALQAACwscAQBB7P8DCxQAAQIDBAUGBwgJCgsMDQ4PEBESEw== +valid valid memory_copy.wast:1426 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxcDA21lbQIAA3J1bgAAB2xvYWQ4X3UAAQoWAgwAIAAgASAC/AoAAAsHACAALQAACwsdAQBB6/8DCxUAAQIDBAUGBwgJCgsMDQ4PEBESExQ= +valid valid memory_copy.wast:1788 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxcDA21lbQIAA3J1bgAAB2xvYWQ4X3UAAQoWAgwAIAAgASAC/AoAAAsHACAALQAACwscAQBBzv8DCxQAAQIDBAUGBwgJCgsMDQ4PEBESEw== +valid valid memory_copy.wast:2510 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxcDA21lbQIAA3J1bgAAB2xvYWQ4X3UAAQoWAgwAIAAgASAC/AoAAAsHACAALQAACwscAQBB4v8DCxQAAQIDBAUGBwgJCgsMDQ4PEBESEw== +valid valid memory_copy.wast:3593 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQMBAAEHFwMDbWVtAgADcnVuAAAHbG9hZDhfdQABChYCDAAgACABIAL8CgAACwcAIAAtAAALCxwBAEHs/wMLFAABAgMEBQYHCAkKCwwNDg8QERIT +valid valid memory_copy.wast:3954 AGFzbQEAAAABDAJgA39/fwBgAX8BfwMDAgABBQQBAQEBBxcDA21lbQIAA3J1bgAAB2xvYWQ4X3UAAQoWAgwAIAAgASAC/AoAAAsHACAALQAACwscAQBBgOADCxQAAQIDBAUGBwgJCgsMDQ4PEBESEw== +invalid invalid memory_copy.wast:4316 AGFzbQEAAAABBAFgAAADAgEABwoBBnRlc3RmbgAACg4BDABBCkEUQR78CgAACw== +invalid invalid memory_copy.wast:4322 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBBCkEUQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4329 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg4BDABBCkEUQh78CgAACw== +invalid invalid memory_copy.wast:4336 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBBCkEURAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4343 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBBCkMAAKBBQR78CgAACw== +invalid invalid memory_copy.wast:4350 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBBCkMAAKBBQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4357 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBBCkMAAKBBQh78CgAACw== +invalid invalid memory_copy.wast:4364 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBBCkMAAKBBRAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4371 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg4BDABBCkIUQR78CgAACw== +invalid invalid memory_copy.wast:4378 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBBCkIUQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4385 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg4BDABBCkIUQh78CgAACw== +invalid invalid memory_copy.wast:4392 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBBCkIURAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4399 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBBCkQAAAAAAAA0QEEe/AoAAAs= +invalid invalid memory_copy.wast:4406 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBBCkQAAAAAAAA0QEMAAPBB/AoAAAs= +invalid invalid memory_copy.wast:4413 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBBCkQAAAAAAAA0QEIe/AoAAAs= +invalid invalid memory_copy.wast:4420 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChwBGgBBCkQAAAAAAAA0QEQAAAAAAAA+QPwKAAAL +invalid invalid memory_copy.wast:4427 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBDAAAgQUEUQR78CgAACw== +invalid invalid memory_copy.wast:4434 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBDAAAgQUEUQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4441 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBDAAAgQUEUQh78CgAACw== +invalid invalid memory_copy.wast:4448 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBDAAAgQUEURAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4455 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBDAAAgQUMAAKBBQR78CgAACw== +invalid invalid memory_copy.wast:4462 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBDAAAgQUMAAKBBQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4469 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBDAAAgQUMAAKBBQh78CgAACw== +invalid invalid memory_copy.wast:4476 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBDAAAgQUMAAKBBRAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4483 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBDAAAgQUIUQR78CgAACw== +invalid invalid memory_copy.wast:4490 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBDAAAgQUIUQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4497 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBDAAAgQUIUQh78CgAACw== +invalid invalid memory_copy.wast:4504 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBDAAAgQUIURAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4511 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBDAAAgQUQAAAAAAAA0QEEe/AoAAAs= +invalid invalid memory_copy.wast:4518 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBDAAAgQUQAAAAAAAA0QEMAAPBB/AoAAAs= +invalid invalid memory_copy.wast:4525 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBDAAAgQUQAAAAAAAA0QEIe/AoAAAs= +invalid invalid memory_copy.wast:4532 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACh8BHQBDAAAgQUQAAAAAAAA0QEQAAAAAAAA+QPwKAAAL +invalid invalid memory_copy.wast:4539 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg4BDABCCkEUQR78CgAACw== +invalid invalid memory_copy.wast:4546 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBCCkEUQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4553 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg4BDABCCkEUQh78CgAACw== +invalid invalid memory_copy.wast:4560 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBCCkEURAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4567 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBCCkMAAKBBQR78CgAACw== +invalid invalid memory_copy.wast:4574 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBCCkMAAKBBQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4581 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBCCkMAAKBBQh78CgAACw== +invalid invalid memory_copy.wast:4588 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBCCkMAAKBBRAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4595 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg4BDABCCkIUQR78CgAACw== +invalid invalid memory_copy.wast:4602 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChEBDwBCCkIUQwAA8EH8CgAACw== +invalid invalid memory_copy.wast:4609 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg4BDABCCkIUQh78CgAACw== +invalid invalid memory_copy.wast:4616 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBCCkIURAAAAAAAAD5A/AoAAAs= +invalid invalid memory_copy.wast:4623 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBCCkQAAAAAAAA0QEEe/AoAAAs= +invalid invalid memory_copy.wast:4630 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBCCkQAAAAAAAA0QEMAAPBB/AoAAAs= +invalid invalid memory_copy.wast:4637 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBCCkQAAAAAAAA0QEIe/AoAAAs= +invalid invalid memory_copy.wast:4644 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChwBGgBCCkQAAAAAAAA0QEQAAAAAAAA+QPwKAAAL +invalid invalid memory_copy.wast:4651 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBEAAAAAAAAJEBBFEEe/AoAAAs= +invalid invalid memory_copy.wast:4658 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBEAAAAAAAAJEBBFEMAAPBB/AoAAAs= +invalid invalid memory_copy.wast:4665 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBEAAAAAAAAJEBBFEIe/AoAAAs= +invalid invalid memory_copy.wast:4672 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChwBGgBEAAAAAAAAJEBBFEQAAAAAAAA+QPwKAAAL +invalid invalid memory_copy.wast:4679 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBEAAAAAAAAJEBDAACgQUEe/AoAAAs= +invalid invalid memory_copy.wast:4686 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBEAAAAAAAAJEBDAACgQUMAAPBB/AoAAAs= +invalid invalid memory_copy.wast:4693 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBEAAAAAAAAJEBDAACgQUIe/AoAAAs= +invalid invalid memory_copy.wast:4700 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACh8BHQBEAAAAAAAAJEBDAACgQUQAAAAAAAA+QPwKAAAL +invalid invalid memory_copy.wast:4707 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBEAAAAAAAAJEBCFEEe/AoAAAs= +invalid invalid memory_copy.wast:4714 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChgBFgBEAAAAAAAAJEBCFEMAAPBB/AoAAAs= +invalid invalid memory_copy.wast:4721 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChUBEwBEAAAAAAAAJEBCFEIe/AoAAAs= +invalid invalid memory_copy.wast:4728 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChwBGgBEAAAAAAAAJEBCFEQAAAAAAAA+QPwKAAAL +invalid invalid memory_copy.wast:4735 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChwBGgBEAAAAAAAAJEBEAAAAAAAANEBBHvwKAAAL +invalid invalid memory_copy.wast:4742 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACh8BHQBEAAAAAAAAJEBEAAAAAAAANEBDAADwQfwKAAAL +invalid invalid memory_copy.wast:4749 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChwBGgBEAAAAAAAAJEBEAAAAAAAANEBCHvwKAAAL +invalid invalid memory_copy.wast:4756 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACiMBIQBEAAAAAAAAJEBEAAAAAAAANEBEAAAAAAAAPkD8CgAACw== +valid valid memory_copy.wast:4763 AGFzbQEAAAABCwJgAABgA39/fwF/AwMCAAEFBAEBAQEHFQIEdGVzdAAACmNoZWNrUmFuZ2UAAQpAAhYAQQpB1QBBCvwLAEEJQQpBBfwKAAALJwADQCAAIAFGBEBBfw8LIAAtAAAgAkYEQCAAQQFqIQAMAQsLIAAPCw== +valid valid memory_copy.wast:4789 AGFzbQEAAAABCwJgAABgA39/fwF/AwMCAAEFBAEBAQEHFQIEdGVzdAAACmNoZWNrUmFuZ2UAAQpAAhYAQQpB1QBBCvwLAEEQQQ9BBfwKAAALJwADQCAAIAFGBEBBfw8LIAAtAAAgAkYEQCAAQQFqIQAMAQsLIAAPCw== +valid valid memory_copy.wast:4815 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoTAREAQYD+A0GAgAJBgQL8CgAACw== +valid valid memory_copy.wast:4821 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYB+QYCAAUGBAvwKAAAL +valid valid memory_copy.wast:4827 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoTAREAQYCAAkGA/gNBgQL8CgAACw== +valid valid memory_copy.wast:4833 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYCAAUGAfkGBAvwKAAAL +valid valid memory_copy.wast:4839 AGFzbQEAAAABCwJgAABgA39/fwF/AwMCAAEFBAEBAQEHFQIEdGVzdAAACmNoZWNrUmFuZ2UAAQpUAioAQQBB1QBBgIAC/AsAQYCAAkGqAUGAgAL8CwBBgKACQYDgAUEA/AoAAAsnAANAIAAgAUYEQEF/DwsgAC0AACACRgRAIABBAWohAAwBCwsgAA8L +valid valid memory_copy.wast:4863 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYCABEGA4AFBAPwKAAAL +valid valid memory_copy.wast:4869 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYCACEGA4AFBAPwKAAAL +valid valid memory_copy.wast:4875 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYCgAkGAgARBAPwKAAAL +valid valid memory_copy.wast:4881 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYCgAkGAgAhBAPwKAAAL +valid valid memory_copy.wast:4887 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYCABEGAgARBAPwKAAAL +valid valid memory_copy.wast:4893 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwgBBHRlc3QAAAoSARAAQYCACEGAgAhBAPwKAAAL +valid valid memory_copy.wast:4899 AGFzbQEAAAABCwJgAABgA39/fwF/AwMCAAEFBAEBAQEHFQIEdGVzdAAACmNoZWNrUmFuZ2UAAQq3FQKMFQBB54oBQQFBwAr8CwBB6bACQQJBnwj8CwBB0bgDQQNB3Af8CwBByqgCQQRBwgL8CwBBqT5BBUHKD/wLAEG6sQFBBkHcF/wLAEHygwFBB0HEEvwLAEHj0wJBCEHDBvwLAEH8AEEJQfEK/AsAQdQQQQpBxhX8CwBBm8YAQQtBmhj8CwBB55sDQQxB5QX8CwBB9h5BDUGHFvwLAEGzhANBDkGACvwLAEHJiQNBD0G6C/wLAEGNoAFBEEHWGPwLAEGx9AJBEUGgBPwLAEGj4QBBEkHtFPwLAEGlwgFBE0HbFPwLAEGF4gJBFEGiDPwLAEHY0AJBFUGbDfwLAEHeiAJBFkGGBfwLAEGr+wJBF0HCDvwLAEHNoQNBGEHhFPwLAEGb7QFBGUHVB/wLAEHUyABBGkGPDvwLAEGOiANBG0HnA/wLAEGh6gNBHEGSBPwLAEHcmwJBHUGvB/wLAEHwNEEeQf0C/AsAQb6QA0EfQZEY/AsAQcGEA0EgQZIF/AsAQfzbAkEhQaYN/AsAQb6EAkEiQcQI/AsAQf6MA0EjQYIL/AsAQerzAkEkQZwR/AsAQeumA0ElQdoS/AsAQY+vA0EmQfoB/AsAQdywAUEnQbEQ/AsAQeyFAUEoQcAZ/AsAQbuoA0EpQeMZ/AsAQbK0AkEqQewV/AsAQbyaAkErQZYQ/AsAQeyTAkEsQcsV/AsAQdv/AUEtQbgC/AsAQYLyA0EuQcAB/AsAQf7xAUEvQdQE/AsAQfuBAUEwQfUD/AsAQaq9A0ExQa4F/AsAQfuLAkEyQYED/AsAQdHbA0EzQYcH/AsAQYXgA0E0QdYS/AsAQfzuAkE1QaEL/AsAQfXKAUE2QdoY/AsAQb4rQTdB1xD8CwBBiZkCQThBhwT8CwBB3N4CQTlB0Bn8CwBBqO0CQTpBjg38CwBBj+wCQTtB4Bj8CwBBsa8BQTxBoQv8CwBB8ckDQT1BlwX8CwBBhfwBQT5Bhw38CwBB9xdBP0HRBfwLAEHpiQJBwABB1AD8CwBBuoQCQcEAQe0P/AsAQcqfAkHCAEEd/AsAQcuVAUHDAEHaF/wLAEHI4gBBxABBkwj8CwBB5I4BQcUAQfwZ/AsAQZ8kQcYAQcMI/AsAQZ7+AEHHAEHND/wLAEGcjgFByABB0xH8CwBB5IoDQckAQfUY/AsAQZTWAEHKAEGwD/wLAEHa/ABBywBBrwv8CwBB3uICQcwAQZkJ/AsAQfmmA0HNAEGgDPwLAEG7ggJBzgBB6gz8CwBB5NwDQc8AQdQZ/AsAQZGUA0HQAEHfAfwLAEGJIkHRAEH7EPwLAEGqwQNB0gBBqgr8CwBBrLMDQdMAQdgU/AsAQZu8AUHUAEGVCPwLAEGv0QJB1QBBmRj8CwBBs/wBQdYAQewV/AsAQeMdQdcAQdoP/AsAQcisA0HYAEEA/AsAQZWGA0HZAEGVEPwLAEG7nwFB2gBB0Bb8CwBBoogCQdsAQcAB/AsAQbrJAEHcAEGTEfwLAEH94ABB3QBBGPwLAEGL7gBB3gBBwQT8CwBBmtgCQd8AQakQ/AsAQf+eAkHgAEHsGvwLAEH4tQFB4QBBzRX8CwBB+DFB4gBBvgb8CwBBm4QCQeMAQZIP/AsAQbWrAUHkAEG+FfwLAEHOzgNB6KcDQbIQ/AoAAEGy7ANBuLICQeYB/AoAAEH5lANBzbgBQfwR/AoAAEG0NEG8uwFB/wT8CgAAQc42QfeEAkHJCPwKAABBy5cBQezQAEH9GPwKAABBrNUBQYapA0HkAPwKAABB1dQBQaLVAkG1DfwKAABB8NgDQbXDAEH3APwKAABBuy5BhBJBkgX8CgAAQbMlQa+TA0HdEfwKAABByeIAQf2VAUHBBvwKAABBztwAQanrAkHkGfwKAABB8NgAQdTfAkHpEfwKAABBiosCQak0QYwU/AoAAEHIJkGaDUGwCvwKAABBvO0DQdU7QYYN/AoAAEGY3AJBqI8BQSH8CgAAQY7XAkHMrgFBkwv8CgAAQa3sAkGbhQNBmgv8CgAAQcTxA0GzxABBwgb8CgAAQc2FAkGjnQFB9Rn8CgAAQf+8AkGtqANBgRn8CgAAQdTJAUH2zgNBlBP8CgAAQd6ZAUGyvANB2gL8CgAAQez7AEHKmAJB/hL8CgAAQbDcAEH2lQJBrAL8CgAAQaPQA0GF7QBB0Rj8CgAAQfuLAkGy2QNBgQr8CgAAQYTGAEH03wBBrwf8CgAAQYsWQbnRAEHfDvwKAABButECQYbXAkHiBfwKAABBvuwDQYWUAUH6APwKAABB7LsBQdndAkHbDfwKAABB0LABQaPzAEG+BfwKAABBlNgAQdPPAUGmDvwKAABBtLQBQfefAUGoCPwKAABBoL8DQfKrA0HHFPwKAABBlMcBQYEIQakY/AoAAEG0gwNBvNkCQc8H/AoAAEH43AFB+sUCQaAS/AoAAEHp3gNB5gFBuBb8CgAAQdCvAUGamgNBlRH8CgAAQem8AkHqygBBpg/8CgAAQcziAUH+ogFBihH8CgAAQaWeA0Gz1wJBjQj8CgAAQYTHAUHTlgJB8gz8CgAAQZTJA0H75QJBwg/8CgAAQZmrAkGQLUGjD/wKAABB194BQcSwA0HAEvwKAABBm+kDQbyNAUHMCvwKAABB5YcDQaXsAEH+AvwKAABBiIQBQfWbAkHsDvwKAABB4vcCQd7YAEH3FfwKAABB4N4BQaq7AkHDAvwKAABBspUCQdDZAUGGDfwKAABB+usDQdSgA0G9CvwKAABBte4AQejpAkGEBfwKAABB5uIBQYKVAUHwA/wKAABBmN8CQdnzAkHgFfwKAABBh7UCQfXcAkHGCvwKAABB8NAAQdrkAUHDC/wKAABBv+4CQeLoAkG7C/wKAABBqSZBxOABQecO/AoAAEH8qAJBpb8DQdcN/AoAAEHOzgFB19QBQecI/AoAAEHTywNB0cABQacI/AoAAEGs3wNBhq8CQf4F/AoAAEGA2QJB7BFB8Av8CgAAQeT/AUGF8QJBxhf8CgAAQYzXAEGMpgFB8wf8CgAAQfE7Qfz2AUHaF/wKAABB/IwBQbvlAEH4GfwKAABB2r8DQeG0A0G0AvwKAABB48ABQa+DAUGDCfwKAABBvJsBQYPPAEHSBfwKAABB6RZBry5BwhL8CgAAQf/7AUGvhwNB7hb8CgAAQZb2AEGThwFBrxT8CgAAQYfkAkGf3gFB/Q/8CgAAQe2uA0GRmgJBpBT8CgAAQa3eAUGNpwNBkAn8CgAAQc/2AkGJoQNBwRj8CgAAQbbvAUHj4AJB2RT8CgAAQcEnQcchQTT8CgAAQaQ0QYO9AUG5A/wKAABB2IECQe3TAUH1GvwKAABBkv4BQezPA0HhFfwKAABBuYwCQYLGAEHmEvwKAABB5YsBQYqqA0G1GvwKAABBnbEBQffYAkGIAfwKAABB0c0DQaU3QZUI/AoAAEHBzwJB9K0DQdUS/AoAAEGV3QJBqp0BQe0G/AoAAEHKnwJB7MQBQfca/AoAAEGu5QJBkPkBQdYG/AoAAEGsvQFB+vgBQeEK/AoAAEHyhwJBtAVBugz8CgAAQcrZA0GZkQFBqxf8CgAAQcKJA0G3wgJB/gr8CgAACycAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADws= +valid valid memory_fill.wast:6 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQo5AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsPAEGA/gNB1QBBgAL8CwAL +valid valid memory_fill.wast:28 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQo5AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsPAEGA/gNB1QBBgQL8CwAL +valid valid memory_fill.wast:46 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQo4AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsOAEGAfkHVAEGBAvwLAAs= +valid valid memory_fill.wast:64 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQo2AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsMAEESQdUAQQD8CwAL +valid valid memory_fill.wast:84 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQo4AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsOAEGAgARB1QBBAPwLAAs= +valid valid memory_fill.wast:102 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQo4AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsOAEGAgAhB1QBBAPwLAAs= +valid valid memory_fill.wast:120 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQo4AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsOAEEBQaoBQf7/A/wLAAs= +valid valid memory_fill.wast:145 AGFzbQEAAAABCwJgA39/fwF/YAAAAwMCAAEFBAEBAQEHFQIKY2hlY2tSYW5nZQAABHRlc3QAAQpAAicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsWAEESQdUAQQr8CwBBFUGqAUEE/AsACw== +invalid invalid memory_fill.wast:175 AGFzbQEAAAABBAFgAAADAgEABwoBBnRlc3RmbgAACg0BCwBBCkEUQR78CwAL +invalid invalid memory_fill.wast:181 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBBCkEUQwAA8EH8CwAL +invalid invalid memory_fill.wast:188 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg0BCwBBCkEUQh78CwAL +invalid invalid memory_fill.wast:195 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBBCkEURAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:202 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBBCkMAAKBBQR78CwAL +invalid invalid memory_fill.wast:209 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChMBEQBBCkMAAKBBQwAA8EH8CwAL +invalid invalid memory_fill.wast:216 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBBCkMAAKBBQh78CwAL +invalid invalid memory_fill.wast:223 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBBCkMAAKBBRAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:230 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg0BCwBBCkIUQR78CwAL +invalid invalid memory_fill.wast:237 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBBCkIUQwAA8EH8CwAL +invalid invalid memory_fill.wast:244 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg0BCwBBCkIUQh78CwAL +invalid invalid memory_fill.wast:251 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBBCkIURAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:258 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBBCkQAAAAAAAA0QEEe/AsACw== +invalid invalid memory_fill.wast:265 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBBCkQAAAAAAAA0QEMAAPBB/AsACw== +invalid invalid memory_fill.wast:272 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBBCkQAAAAAAAA0QEIe/AsACw== +invalid invalid memory_fill.wast:279 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBBCkQAAAAAAAA0QEQAAAAAAAA+QPwLAAs= +invalid invalid memory_fill.wast:286 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBDAAAgQUEUQR78CwAL +invalid invalid memory_fill.wast:293 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChMBEQBDAAAgQUEUQwAA8EH8CwAL +invalid invalid memory_fill.wast:300 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBDAAAgQUEUQh78CwAL +invalid invalid memory_fill.wast:307 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBDAAAgQUEURAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:314 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChMBEQBDAAAgQUMAAKBBQR78CwAL +invalid invalid memory_fill.wast:321 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChYBFABDAAAgQUMAAKBBQwAA8EH8CwAL +invalid invalid memory_fill.wast:328 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChMBEQBDAAAgQUMAAKBBQh78CwAL +invalid invalid memory_fill.wast:335 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChoBGABDAAAgQUMAAKBBRAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:342 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBDAAAgQUIUQR78CwAL +invalid invalid memory_fill.wast:349 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChMBEQBDAAAgQUIUQwAA8EH8CwAL +invalid invalid memory_fill.wast:356 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBDAAAgQUIUQh78CwAL +invalid invalid memory_fill.wast:363 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBDAAAgQUIURAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:370 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBDAAAgQUQAAAAAAAA0QEEe/AsACw== +invalid invalid memory_fill.wast:377 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChoBGABDAAAgQUQAAAAAAAA0QEMAAPBB/AsACw== +invalid invalid memory_fill.wast:384 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBDAAAgQUQAAAAAAAA0QEIe/AsACw== +invalid invalid memory_fill.wast:391 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACh4BHABDAAAgQUQAAAAAAAA0QEQAAAAAAAA+QPwLAAs= +invalid invalid memory_fill.wast:398 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg0BCwBCCkEUQR78CwAL +invalid invalid memory_fill.wast:405 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBCCkEUQwAA8EH8CwAL +invalid invalid memory_fill.wast:412 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg0BCwBCCkEUQh78CwAL +invalid invalid memory_fill.wast:419 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBCCkEURAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:426 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBCCkMAAKBBQR78CwAL +invalid invalid memory_fill.wast:433 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChMBEQBCCkMAAKBBQwAA8EH8CwAL +invalid invalid memory_fill.wast:440 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBCCkMAAKBBQh78CwAL +invalid invalid memory_fill.wast:447 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBCCkMAAKBBRAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:454 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg0BCwBCCkIUQR78CwAL +invalid invalid memory_fill.wast:461 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChABDgBCCkIUQwAA8EH8CwAL +invalid invalid memory_fill.wast:468 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACg0BCwBCCkIUQh78CwAL +invalid invalid memory_fill.wast:475 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBCCkIURAAAAAAAAD5A/AsACw== +invalid invalid memory_fill.wast:482 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBCCkQAAAAAAAA0QEEe/AsACw== +invalid invalid memory_fill.wast:489 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBCCkQAAAAAAAA0QEMAAPBB/AsACw== +invalid invalid memory_fill.wast:496 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBCCkQAAAAAAAA0QEIe/AsACw== +invalid invalid memory_fill.wast:503 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBCCkQAAAAAAAA0QEQAAAAAAAA+QPwLAAs= +invalid invalid memory_fill.wast:510 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBEAAAAAAAAJEBBFEEe/AsACw== +invalid invalid memory_fill.wast:517 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBEAAAAAAAAJEBBFEMAAPBB/AsACw== +invalid invalid memory_fill.wast:524 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBEAAAAAAAAJEBBFEIe/AsACw== +invalid invalid memory_fill.wast:531 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBEAAAAAAAAJEBBFEQAAAAAAAA+QPwLAAs= +invalid invalid memory_fill.wast:538 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBEAAAAAAAAJEBDAACgQUEe/AsACw== +invalid invalid memory_fill.wast:545 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChoBGABEAAAAAAAAJEBDAACgQUMAAPBB/AsACw== +invalid invalid memory_fill.wast:552 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBEAAAAAAAAJEBDAACgQUIe/AsACw== +invalid invalid memory_fill.wast:559 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACh4BHABEAAAAAAAAJEBDAACgQUQAAAAAAAA+QPwLAAs= +invalid invalid memory_fill.wast:566 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBEAAAAAAAAJEBCFEEe/AsACw== +invalid invalid memory_fill.wast:573 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChcBFQBEAAAAAAAAJEBCFEMAAPBB/AsACw== +invalid invalid memory_fill.wast:580 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChQBEgBEAAAAAAAAJEBCFEIe/AsACw== +invalid invalid memory_fill.wast:587 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBEAAAAAAAAJEBCFEQAAAAAAAA+QPwLAAs= +invalid invalid memory_fill.wast:594 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBEAAAAAAAAJEBEAAAAAAAANEBBHvwLAAs= +invalid invalid memory_fill.wast:601 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACh4BHABEAAAAAAAAJEBEAAAAAAAANEBDAADwQfwLAAs= +invalid invalid memory_fill.wast:608 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAAChsBGQBEAAAAAAAAJEBEAAAAAAAANEBCHvwLAAs= +invalid invalid memory_fill.wast:615 AGFzbQEAAAABBAFgAAADAgEABQQBAQEBBwoBBnRlc3RmbgAACiIBIABEAAAAAAAAJEBEAAAAAAAANEBEAAAAAAAAPkD8CwAL +valid valid memory_fill.wast:621 AGFzbQEAAAABDgJgA39/fwF/YAN/f38AAwMCAAEFBAEBAQEHFAIKY2hlY2tSYW5nZQAAA3J1bgABCjUCJwADQCAAIAFGBEBBfw8LIAAtAAAgAkYEQCAAQQFqIQAMAQsLIAAPCwsAIAAgASAC/AsACw== +valid valid memory_grow.wast:1 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8DBwYAAQABAgAFAwEAAAdXBgxsb2FkX2F0X3plcm8AAA1zdG9yZV9hdF96ZXJvAAERbG9hZF9hdF9wYWdlX3NpemUAAhJzdG9yZV9hdF9wYWdlX3NpemUAAwRncm93AAQEc2l6ZQAFCjUGBwBBACgCAAsJAEEAQQI2AgALCQBBgIAEKAIACwsAQYCABEEDNgIACwYAIABAAAsEAD8ACw== +valid valid memory_grow.wast:36 AGFzbQEAAAABBgFgAX8BfwMCAQAFAwEAAAcIAQRncm93AAAKCAEGACAAQAAL +valid valid memory_grow.wast:50 AGFzbQEAAAABBgFgAX8BfwMCAQAFBAEBAAoHCAEEZ3JvdwAACggBBgAgAEAACw== +valid valid memory_grow.wast:66 AGFzbQEAAAABDAJgAX8Bf2ACf38BfwMDAgABBQMBAAEHHAIEZ3JvdwAAEWNoZWNrLW1lbW9yeS16ZXJvAAEKPAIGACAAQAALMwEBf0EBIQICQANAIAAtAAAhAiACQQBHDQEgACABTw0BIABBAWohACAAIAFNDQALCyACCw== +valid valid memory_grow.wast:101 AGFzbQEAAAABFQRgA39/fwF/YAABf2AAAGACf38BfwMnJgECAQECAQEBAQEBAwMBAAEBAQEBAQECAQIBAQICAgIBAQEBAQEBBAUBcAEBAQUDAQABBgYBfwFBAAsHtwUlC2FzLWJyLXZhbHVlAAANYXMtYnJfaWYtY29uZAABDmFzLWJyX2lmLXZhbHVlAAITYXMtYnJfaWYtdmFsdWUtY29uZAADEWFzLWJyX3RhYmxlLWluZGV4AAQRYXMtYnJfdGFibGUtdmFsdWUABRdhcy1icl90YWJsZS12YWx1ZS1pbmRleAAGD2FzLXJldHVybi12YWx1ZQAHCmFzLWlmLWNvbmQACAphcy1pZi10aGVuAAkKYXMtaWYtZWxzZQAKD2FzLXNlbGVjdC1maXJzdAALEGFzLXNlbGVjdC1zZWNvbmQADA5hcy1zZWxlY3QtY29uZAANDWFzLWNhbGwtZmlyc3QADwthcy1jYWxsLW1pZAAQDGFzLWNhbGwtbGFzdAARFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAEhRhcy1jYWxsX2luZGlyZWN0LW1pZAATFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAAUFmFzLWNhbGxfaW5kaXJlY3QtaW5kZXgAFRJhcy1sb2NhbC5zZXQtdmFsdWUAFhJhcy1sb2NhbC50ZWUtdmFsdWUAFxNhcy1nbG9iYWwuc2V0LXZhbHVlABgPYXMtbG9hZC1hZGRyZXNzABkQYXMtbG9hZE4tYWRkcmVzcwAaEGFzLXN0b3JlLWFkZHJlc3MAGw5hcy1zdG9yZS12YWx1ZQAcEWFzLXN0b3JlTi1hZGRyZXNzAB0PYXMtc3RvcmVOLXZhbHVlAB4QYXMtdW5hcnktb3BlcmFuZAAfDmFzLWJpbmFyeS1sZWZ0ACAPYXMtYmluYXJ5LXJpZ2h0ACEPYXMtdGVzdC1vcGVyYW5kACIPYXMtY29tcGFyZS1sZWZ0ACMQYXMtY29tcGFyZS1yaWdodAAkE2FzLW1lbW9yeS5ncm93LXNpemUAJQkHAQBBAAsBDgraAyYLAAJ/QQBAAAwACwsLAAJAQQBAAA0ACwsQAAJ/QQBAAEEBDQAaQQcLCxAAAn9BBkEAQAANABpBBwsLDgACQEEAQAAOAgAAAAsLEgACf0EAQABBAQ4CAAAAQQcLCxEAAn9BBkEAQAAOAQAAQQcLCwcAQQBAAA8LDgBBAEAABH9BAAVBAQsLDgBBAQR/QQBAAAVBAAsLDgBBAAR/QQAFQQBAAAsLCwBBAEAAIAAgARsLCwAgAEEAQAAgARsLCwBBAEEBQQBAABsLBABBfwsMAEEAQABBAkEDEA4LDABBAUEAQABBAxAOCwwAQQFBAkEAQAAQDgsPAEEAQABBAkEDQQARAAALDwBBAUEAQABBA0EAEQAACw8AQQFBAkEAQABBABEAAAsPAEEBQQJBA0EAQAARAAALCgEBf0EAQAAhAAsKAQF/QQBAACIACwoBAX9BAEAAJAALCQBBAEAAKAIACwkAQQBAACwAAAsLAEEAQABBBzYCAAsLAEECQQBAADYCAAsLAEEAQABBBzoAAAsLAEECQQBAADsBAAsHAEEAQABnCwkAQQBAAEEKagsJAEEKQQBAAGsLBwBBAEAARQsJAEEAQABBCkwLCQBBCkEAQABHCwgAQQBAAEAACw== +valid valid memory_grow.wast:312 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABBxECBm1lbW9yeQIABGdyb3cAAAoIAQYAQQFAAAs= +valid valid memory_grow.wast:318 AGFzbQEAAAABBQFgAAF/AhgBDGdyb3duLW1lbW9yeQZtZW1vcnkCAAIDAgEABxECBm1lbW9yeQIABGdyb3cAAAoIAQYAQQFAAAs= +valid valid memory_grow.wast:325 AGFzbQEAAAABBQFgAAF/AiEBFWdyb3duLWltcG9ydGVkLW1lbW9yeQZtZW1vcnkCAAMDAgEABwgBBHNpemUAAAoGAQQAPwAL +invalid invalid memory_grow.wast:334 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQAACgYBBABAAAs= +invalid invalid memory_grow.wast:343 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQAACgsBCQBBAAJ/QAALCw== +invalid invalid memory_grow.wast:353 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQAACgsBCQBBAAN/QAALCw== +invalid invalid memory_grow.wast:363 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQAACg0BCwBBAEEABH9AAAsL +invalid invalid memory_grow.wast:374 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABCgsBCQBDAAAAAEAACw== +invalid invalid memory_grow.wast:384 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCAEGAEEAQAAL +invalid invalid memory_grow.wast:393 AGFzbQEAAAABBQFgAAF9AwIBAAUDAQABCggBBgBBAEAACw== +valid valid memory_init.wast:6 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABCg0CAwABCwcAIAAtAAALCyEEAEECCwQDAQQBAQQCBwEIAEEMCwUHBQIDBgEFBQkCBwY= +valid valid memory_init.wast:50 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABDAEEChYCDABBB0EAQQT8CAEACwcAIAAtAAALCyEEAEECCwQDAQQBAQQCBwEIAEEMCwUHBQIDBgEFBQkCBwY= +valid valid memory_init.wast:94 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABDAEEChYCDABBD0EBQQP8CAMACwcAIAAtAAALCyEEAEECCwQDAQQBAQQCBwEIAEEMCwUHBQIDBgEFBQkCBwY= +valid valid memory_init.wast:138 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQQBAQEBBxwDB21lbW9yeTACAAR0ZXN0AAAHbG9hZDhfdQABDAEEClgCTgBBB0EAQQT8CAEA/AkBQQ9BAUED/AgDAPwJA0EUQQ9BBfwKAABBFUEdQQH8CgAAQRhBCkEB/AoAAEENQQtBBPwKAABBE0EUQQX8CgAACwcAIAAtAAALCyEEAEECCwQDAQQBAQQCBwEIAEEMCwUHBQIDBgEFBQkCBwY= +invalid invalid memory_init.wast:190 AGFzbQEAAAABBAFgAAADAgEABwgBBHRlc3QAAAoHAQUA/AkACw== +invalid invalid memory_init.wast:196 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCgcBBQD8CQQLCwQBAQE3 +valid valid memory_init.wast:203 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCgoBCAD8CQD8CQALCwQBAQE3 +valid valid memory_init.wast:211 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChIBEAD8CQBB0glBAUEB/AgAAAsLBAEBATc= +valid valid memory_init.wast:219 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg8BDQBB0glBAUEB/AgAAAsLBwEAQQALATc= +invalid invalid memory_init.wast:227 AGFzbQEAAAABBAFgAAADAgEABwgBBHRlc3QAAAoPAQ0AQdIJQQFBAfwIAQAL +invalid invalid memory_init.wast:233 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg8BDQBB0glBAUEB/AgBAAsLBAEBATc= +valid valid memory_init.wast:240 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBBAUEAQQH8CAAAQQFBAEEB/AgAAAsLBAEBATc= +valid valid memory_init.wast:248 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg8BDQBB0glBAEEF/AgAAAsLBAEBATc= +valid valid memory_init.wast:255 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg8BDQBB0glBAkED/AgAAAsLBAEBATc= +valid valid memory_init.wast:262 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChABDgBB/v8DQQFBA/wIAAALCwQBAQE3 +valid valid memory_init.wast:269 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg8BDQBB0glBBEEA/AgAAAsLBAEBATc= +valid valid memory_init.wast:276 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg8BDQBB0glBAUEA/AgAAAsLBAEBATc= +valid valid memory_init.wast:283 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChABDgBBgYAEQQBBAPwIAAALCwQBAQE3 +valid valid memory_init.wast:290 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChABDgBBgIAEQQBBAPwIAAALCwQBAQE3 +valid valid memory_init.wast:297 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChABDgBBgIAEQQFBAPwIAAALCwQBAQE3 +valid valid memory_init.wast:304 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChABDgBBgYAEQQRBAPwIAAALCwQBAQE3 +invalid invalid memory_init.wast:312 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBBAUEBQwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:320 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg4BDABBAUEBQgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:328 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBBAUEBRAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:336 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBBAUMAAIA/QQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:344 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChQBEgBBAUMAAIA/QwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:352 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBBAUMAAIA/QgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:360 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBBAUMAAIA/RAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:368 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg4BDABBAUIBQQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:376 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBBAUIBQwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:384 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg4BDABBAUIBQgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:392 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBBAUIBRAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:400 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBBAUQAAAAAAADwP0EB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:408 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBBAUQAAAAAAADwP0MAAIA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:416 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBBAUQAAAAAAADwP0IB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:424 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChwBGgBBAUQAAAAAAADwP0QAAAAAAADwP/wIAAALCwQBAQE3 +invalid invalid memory_init.wast:432 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBDAACAP0EBQQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:440 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChQBEgBDAACAP0EBQwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:448 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBDAACAP0EBQgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:456 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBDAACAP0EBRAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:464 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChQBEgBDAACAP0MAAIA/QQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:472 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChcBFQBDAACAP0MAAIA/QwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:480 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChQBEgBDAACAP0MAAIA/QgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:488 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChsBGQBDAACAP0MAAIA/RAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:496 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBDAACAP0IBQQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:504 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChQBEgBDAACAP0IBQwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:512 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBDAACAP0IBQgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:520 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBDAACAP0IBRAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:528 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBDAACAP0QAAAAAAADwP0EB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:536 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChsBGQBDAACAP0QAAAAAAADwP0MAAIA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:544 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBDAACAP0QAAAAAAADwP0IB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:552 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCh8BHQBDAACAP0QAAAAAAADwP0QAAAAAAADwP/wIAAALCwQBAQE3 +invalid invalid memory_init.wast:560 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg4BDABCAUEBQQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:568 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBCAUEBQwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:576 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg4BDABCAUEBQgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:584 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBCAUEBRAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:592 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBCAUMAAIA/QQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:600 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChQBEgBCAUMAAIA/QwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:608 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBCAUMAAIA/QgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:616 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBCAUMAAIA/RAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:624 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg4BDABCAUIBQQH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:632 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChEBDwBCAUIBQwAAgD/8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:640 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCg4BDABCAUIBQgH8CAAACwsEAQEBNw== +invalid invalid memory_init.wast:648 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBCAUIBRAAAAAAAAPA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:656 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBCAUQAAAAAAADwP0EB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:664 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBCAUQAAAAAAADwP0MAAIA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:672 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBCAUQAAAAAAADwP0IB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:680 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChwBGgBCAUQAAAAAAADwP0QAAAAAAADwP/wIAAALCwQBAQE3 +invalid invalid memory_init.wast:688 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBEAAAAAAAA8D9BAUEB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:696 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBEAAAAAAAA8D9BAUMAAIA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:704 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBEAAAAAAAA8D9BAUIB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:712 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChwBGgBEAAAAAAAA8D9BAUQAAAAAAADwP/wIAAALCwQBAQE3 +invalid invalid memory_init.wast:720 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBEAAAAAAAA8D9DAACAP0EB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:728 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChsBGQBEAAAAAAAA8D9DAACAP0MAAIA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:736 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBEAAAAAAAA8D9DAACAP0IB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:744 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCh8BHQBEAAAAAAAA8D9DAACAP0QAAAAAAADwP/wIAAALCwQBAQE3 +invalid invalid memory_init.wast:752 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBEAAAAAAAA8D9CAUEB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:760 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChgBFgBEAAAAAAAA8D9CAUMAAIA//AgAAAsLBAEBATc= +invalid invalid memory_init.wast:768 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChUBEwBEAAAAAAAA8D9CAUIB/AgAAAsLBAEBATc= +invalid invalid memory_init.wast:776 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChwBGgBEAAAAAAAA8D9CAUQAAAAAAADwP/wIAAALCwQBAQE3 +invalid invalid memory_init.wast:784 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChwBGgBEAAAAAAAA8D9EAAAAAAAA8D9BAfwIAAALCwQBAQE3 +invalid invalid memory_init.wast:792 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCh8BHQBEAAAAAAAA8D9EAAAAAAAA8D9DAACAP/wIAAALCwQBAQE3 +invalid invalid memory_init.wast:800 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBChwBGgBEAAAAAAAA8D9EAAAAAAAA8D9CAfwIAAALCwQBAQE3 +invalid invalid memory_init.wast:808 AGFzbQEAAAABBAFgAAADAgEABQMBAAEHCAEEdGVzdAAADAEBCiMBIQBEAAAAAAAA8D9EAAAAAAAA8D9EAAAAAAAA8D/8CAAACwsEAQEBNw== +valid valid memory_init.wast:815 AGFzbQEAAAABDQJgA39/fwF/YAJ/fwADAwIAAQUEAQEBAQcUAgpjaGVja1JhbmdlAAADcnVuAAEMAQEKNgInAANAIAAgAUYEQEF/DwsgAC0AACACRgRAIABBAWohAAwBCwsgAA8LDAAgAEEAIAH8CAAACwsTAQEQQkJCQkJCQkJCQkJCQkJCQg== +valid valid memory_init.wast:907 AGFzbQEAAAABDQJgA39/fwF/YAJ/fwADAwIAAQUDAQABBxQCCmNoZWNrUmFuZ2UAAANydW4AAQwBAQo2AicAA0AgACABRgRAQX8PCyAALQAAIAJGBEAgAEEBaiEADAELCyAADwsMACAAQQAgAfwIAAALCxMBARBCQkJCQkJCQkJCQkJCQkJC +valid valid memory_init.wast:954 AGFzbQEAAAABBAFgAAADAgEABQMBAAEMAUEKDgEMAEEAQQBBAPwIQAALC4MBQQEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQA= +valid valid memory_redundancy.wast:5 AGFzbQEAAAABEQRgAABgAAF/YAABfWABfwF/AwcGAAEBAgMBBQQBAQEBB2sGD3plcm9fZXZlcnl0aGluZwAAEnRlc3Rfc3RvcmVfdG9fbG9hZAABE3Rlc3RfcmVkdW5kYW50X2xvYWQAAg90ZXN0X2RlYWRfc3RvcmUAAwZtYWxsb2MABA9tYWxsb2NfYWxpYXNpbmcABQqlAQYeAEEAQQA2AgBBBEEANgIAQQhBADYCAEEMQQA2AgALGABBCEEANgIAQQVDAAAAgDgCAEEIKAIACyIBAn9BCCgCACEAQQVBgICAgHg2AgBBCCgCACEBIAAgAWoLHwEBfUEIQaPGjJkCNgIAQQsqAgAhAEEIQQA2AgAgAAsEAEEQCyMBAn9BBBAEIQBBBBAEIQEgAEEqNgIAIAFBKzYCACAAKAIACw== +valid valid memory_size.wast:1 AGFzbQEAAAABCQJgAAF/YAF/AAMDAgABBQMBAAAHDwIEc2l6ZQAABGdyb3cAAQoOAgQAPwALBwAgAEAAGgs= +valid valid memory_size.wast:15 AGFzbQEAAAABCQJgAAF/YAF/AAMDAgABBQMBAAEHDwIEc2l6ZQAABGdyb3cAAQoOAgQAPwALBwAgAEAAGgs= +valid valid memory_size.wast:29 AGFzbQEAAAABCQJgAAF/YAF/AAMDAgABBQQBAQACBw8CBHNpemUAAARncm93AAEKDgIEAD8ACwcAIABAABoL +valid valid memory_size.wast:47 AGFzbQEAAAABCQJgAAF/YAF/AAMDAgABBQQBAQMIBw8CBHNpemUAAARncm93AAEKDgIEAD8ACwcAIABAABoL +invalid invalid memory_size.wast:69 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKBgEEAD8ACw== +invalid invalid memory_size.wast:78 AGFzbQEAAAABBQFgAAF9AwIBAAUDAQABCgYBBAA/AAs= +valid valid memory_trap.wast:1 AGFzbQEAAAABDwNgAAF/YAJ/fwBgAX8BfwMFBAABAgIFAwEAAQceAwVzdG9yZQABBGxvYWQAAgttZW1vcnkuZ3JvdwADCioECQA/AEGAgARsCwwAEAAgAGogATYCAAsKABAAIABqKAIACwYAIABAAAs= +valid valid memory_trap.wast:35 AGFzbQEAAAABKQhgAX8Bf2ABfwF+YAF/AX1gAX8BfGACf38AYAJ/fgBgAn99AGACf3wAAxgXAAECAwAAAAABAQEBAQEEBQYHBAQFBQUFAwEAAQezAhcIaTMyLmxvYWQAAAhpNjQubG9hZAABCGYzMi5sb2FkAAIIZjY0LmxvYWQAAwtpMzIubG9hZDhfcwAEC2kzMi5sb2FkOF91AAUMaTMyLmxvYWQxNl9zAAYMaTMyLmxvYWQxNl91AAcLaTY0LmxvYWQ4X3MACAtpNjQubG9hZDhfdQAJDGk2NC5sb2FkMTZfcwAKDGk2NC5sb2FkMTZfdQALDGk2NC5sb2FkMzJfcwAMDGk2NC5sb2FkMzJfdQANCWkzMi5zdG9yZQAOCWk2NC5zdG9yZQAPCWYzMi5zdG9yZQAQCWY2NC5zdG9yZQARCmkzMi5zdG9yZTgAEgtpMzIuc3RvcmUxNgATCmk2NC5zdG9yZTgAFAtpNjQuc3RvcmUxNgAVC2k2NC5zdG9yZTMyABYKywEXBwAgACgCAAsHACAAKQMACwcAIAAqAgALBwAgACsDAAsHACAALAAACwcAIAAtAAALBwAgAC4BAAsHACAALwEACwcAIAAwAAALBwAgADEAAAsHACAAMgEACwcAIAAzAQALBwAgADQCAAsHACAANQIACwkAIAAgATYCAAsJACAAIAE3AwALCQAgACABOAIACwkAIAAgATkDAAsJACAAIAE6AAALCQAgACABOwEACwkAIAAgATwAAAsJACAAIAE9AQALCQAgACABPgIACwsdAgBBAAsIYWJjZGVmZ2gAQfj/AwsIYWJjZGVmZ2g= +valid valid names.wast:4 AGFzbQEAAAABBQFgAAF/AwIBAAcHAQNmb28AAAoGAQQAQQAL +valid valid names.wast:12 AGFzbQEAAAABBQFgAAF/AwIBAAcHAQNmb28AAAoGAQQAQQEL +valid valid names.wast:19 AGFzbQEAAAABBQFgAAF/A+ED3wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAerId8DAAAAATAAAQItMAACAV8AAwEkAAQBQAAFIX4hQCMkJV4mKigpXytgLT17fXxbXVw6IjsnPD4/LC4vIAAGA05hTgAHCEluZmluaXR5AAgCaWYACQZtYWxsb2MACgdfbWFsbG9jAAsIX19tYWxsb2MADAFhAA0BQQAOA++7vwAPAsOFABADQcyKABED4oSrABID76yDABMEZu+sgQAUA2ZmaQAVEAABAgMEBQYHCAkKCwwNDg8AFhAQERITFBUWFxgZGhscHR4fABcCIH8AGCDCgMKBwoLCg8KEwoXChsKHwojCicKKwovCjMKNwo7CjwAZIMKQwpHCksKTwpTClcKWwpfCmMKZwprCm8Kcwp3CnsKfABoY77+w77+x77+y77+z77+077+177+277+3ABsY77+477+577+677+777+877+977++77+/ABww4pCA4pCB4pCC4pCD4pCE4pCF4pCG4pCH4pCI4pCJ4pCK4pCL4pCM4pCN4pCO4pCPAB0w4pCQ4pCR4pCS4pCT4pCU4pCV4pCW4pCX4pCY4pCZ4pCa4pCb4pCc4pCd4pCe4pCfAB4G4pCg4pChAB8q77+w77+x77+y77+z77+077+177+277+377+477+577+677+777+877+9ACAD4oCNACED4oCMACICzY8AIwPigaAAJAPitb8AJQTwkYG/ACYD4aCOACcW77+v4oCLwqDCreKBoOGagOKAruKArQAoJ+KAjuKAj+KAkeKAqOKAqeKAquKAq+KArOKAr+KBpuKBp+KBqOKBqQApEuKBquKBq+KBrOKBreKBruKBrwAqDOKBoeKBouKBo+KBpAArDPCQgIDzn7+/9I+/vwAsgQJazI/NhsyGzZvNjMy0zZjNns2HzKvMpcyqzZPNiM2UzY7Ml8yezLrMr8yxzJ7MmcyxzJzMlsygYc2XzajMjsyEzIbNl8y/zaHNn82AzLbNgcylzLDMs8ytzZnMssyxzLnMnc2OzLxszYTNisyazZfNps2EzavMh82BzLbMt82JzKnMucyrzJ3Nls2FzJnMssy8zYfNms2NzK7NjsylzYXMnmfNg8yQzIXNrsyUzJDMjsyCzI/Mvs2KzI3Ni82KzafMgcyGzabNnsy2zZXNlM2azKlvzYvMlM2QzarNqcyhzY/MosynzYHMq8yZzKTMrs2WzZnNk8y6zJzMqcy8zJjMoAAtDOGFn+GFoOOFpO++oAAuA++4gAAvA++4hAAwBPOghIAAMQTzoIevADICzIgAMwEKADQD4pCkADUD4oCoADYBDQA3Ag0KADgCCg0AOQEeADoBCwA7AQwAPALChQA9A+KAqQA+A+KApgA/A+KPjgBAAsKLAEECwowAQgLCjQBDA+KGtQBEA+KGqQBFA+KMpABGA+KktgBHA+KGsgBIA+KuqABJA+KusABKA++/vQBLA++3kABMA++3kQBNA++3kgBOA++3kwBPA++3lABQA++3lQBRA++3lgBSA++3lwBTA++3mABUA++3mQBVA++3mgBWA++3mwBXA++3nABYA++3nQBZA++3ngBaA++3nwBbA++3oABcA++3oQBdA++3ogBeA++3owBfA++3pABgA++3pQBhA++3pgBiA++3pwBjA++3qABkA++3qQBlA++3qgBmA++3qwBnA++3rABoA++3rQBpA++3rgBqA++3rwBrA++/vgBsA++/vwBtBPCfv74AbgTwn7+/AG8E8K+/vgBwBPCvv78AcQTwv7++AHIE8L+/vwBzBPGPv74AdATxj7+/AHUE8Z+/vgB2BPGfv78AdwTxr7++AHgE8a+/vwB5BPG/v74AegTxv7+/AHsE8o+/vgB8BPKPv78AfQTyn7++AH4E8p+/vwB/BPKvv74AgAEE8q+/vwCBAQTyv7++AIIBBPK/v78AgwEE84+/vgCEAQTzj7+/AIUBBPOfv74AhgEE85+/vwCHAQTzr7++AIgBBPOvv78AiQEE87+/vgCKAQTzv7+/AIsBBPSPv74AjAEE9I+/vwCNAQnMiOKAvcyIzIkAjgEDYWJjAI8BBuKArWFiYwCQAQbigK5jYmEAkQEJ4oCtYWJj4oCuAJIBCeKArmNiYeKArQCTAQTwnZGoAJQBBPCdkLQAlQEE8J2YiACWAQTwnZi8AJcBBPCdkIAAmAEE8J2TkACZAQTwnZWsAJoBBPCdl5QAmwEE8J2SnACcAQTwnZSEAJ0BBPCdlLgAngEE8J2WoACfAQTwnZmwAKABA+G0gAChAQPhtKwAogED4pK2AKMBA++8oQCkAQTwn4SQAKUBBPCfhLAApgEE86CBgQCnAQZVKzAwNDEAqAEEQeKAiwCpAQLQkACqAQPqmZYAqwED4re8AKwBA+K3tgCtAQPisa8ArgEE8J+FkACvAQTwn4WwALABA+KwrQCxAQTwkJCCALIBBPCQkIgAswEE8JCSsAC0AQLDgAC1AQLDgQC2AQLDggC3AQLDgwC4AQLDhAC5AQLEgAC6AQLEggC7AQLEhAC8AQLHjQC9AQLHngC+AQLHoAC/AQLHugDAAQLIgADBAQLIggDCAQLIpgDDAQLIugDEAQLTkADFAQLTkgDGAQLfigDHAQPgoKEAyAED4KCiAMkBA+CgowDKAQPgoKQAywED4KClAMwBA+CkhADNAQPgpIUAzgED4KWyAM8BA+CmhQDQAQPgqIUA0QED4KqFANIBA+CshQDTAQPgroUA1AED4LCFANUBA+CyhQDWAQPgtIUA1wED4LiwANgBA+C6sADZAQPgvIEA2gED4L2oANsBA+C+uADcAQPhgKEA3QED4YCiAN4BA+GCnADfAQPhhaEA4AED4YqgAOEBA+GLkADiAQPhjqAA4wED4ZCKAOQBA+GWswDlAQPhmqgA5gED4ZqqAOcBA+GbhgDoAQPhnIAA6QED4ZygAOoBA+GdgADrAQPhnaAA7AED4aCgAO0BA+GihwDuAQPhpKAA7wED4aWjAPABA+GolQDxAQPhqYsA8gED4amhAPMBA+GugwD0AQPhr4AA9QED4a+BAPYBA+GwowD3AQPhuIAA+AED4bqgAPkBA+G6ogD6AQPhuqQA+wED4bqmAPwBA+G6qAD9AQPhuqoA/gED4bqsAP8BA+G6rgCAAgPhurAAgQID4bqyAIICA+G6tACDAgPhurYAhAID44GCAIUCA+OCogCGAgPjhJoAhwID44WPAIgCA+OIjgCJAgPjiI8AigID44iQAIsCA+OIkQCMAgPjiJIAjQID44iTAI4CA+OIlACPAgPjiJUAkAID44iWAJECA+OIlwCSAgPjiJgAkwID44iZAJQCA+OImgCVAgPjiJsAlgID44muAJcCA+OJrwCYAgPjibAAmQID44mxAJoCA+OJsgCbAgPjibMAnAID44m0AJ0CA+OJtQCeAgPjibYAnwID44m3AKACA+OJuAChAgPjibkAogID44m6AKMCA+OJuwCkAgPji5AApQID6oCKAKYCA+qTrgCnAgPqlYkAqAID6pqgAKkCA+qggACqAgPqoKMAqwID6qGdAKwCA+qiggCtAgPqo6oArgID6qSiAK8CA+qlhgCwAgPqpoQAsQID6qiAALICA++9sQCzAgPvv4IAtAIE8JCAgAC1AgTwkIqAALYCBPCQiqAAtwIE8JCMgAC4AgTwkI6gALkCBPCQkpYAugIE8JCUgAC7AgTwkJ2AALwCBPCQoIAAvQIE8JCkoAC+AgTwkKaAAL8CBPCQpqAAwAIE8JCogADBAgTwkKyAAMICBPCQsIAAwwIE8JCwgQDEAgTwkLKAAMUCBPCRgIUAxgIE8JGCgwDHAgTwkYSnAMgCBPCRhZAAyQIE8JGGgwDKAgTwkYiAAMsCBPCRioAAzAIE8JGKsADNAgTwkYyFAM4CBPCRjbAAzwIE8JGQgADQAgTwkZKBANECBPCRloAA0gIE8JGYgADTAgTwkZqAANQCBPCRnJIA1QIE8JGcoADWAgTwkaKhANcCBPCRq5UA2AIE8JGwgADZAgTwkbKPANoCBPCRsq8A2wIE8JKAgADcAgTwlqeVAN0CBPCWqYYA3gIE8JarpwDfAgTwlr2UAOACBPCbsYEA4QIE8JuxpADiAgTwnqCjAOMCBPCfh6YA5AID4rGtAOUCAs6bAOYCA+KxsADnAgLCqgDoAgPiiIAA6QID4oKzAOoCBPCQpIAA6wID4rKAAOwCBPCQjLAA7QICzoYA7gICzpEA7wID4byIAPACA+G8iQDxAgPhvIoA8gID4byLAPMCA+G8jAD0AgPhvI0A9QID4byOAPYCA+G8jwD3AgPhvogA+AID4b6JAPkCA+G+igD6AgPhvosA+wID4b6MAPwCA+G+jQD9AgPhvo4A/gID4b6PAP8CA+G+uACAAwPhvrkAgQMD4b66AIIDA+G+uwCDAwPhvrwAhAME8J2aqACFAwTwnZuiAIYDBPCdnJwAhwME8J2dlgCIAwTwnZ6QAIkDA+KNtgCKAwPijboAiwMD4qmcAIwDA+GXhQCNAwPhjqoAjgOcASnLusu88JSXj/CdhbTwnYW28J2FuPCdhbrigb7igo7inaninavin6/vtL/vuLbvuZrvvInvvaDzoICp4p2z4p214p+n4p+p4p+r4p+t4qaI4qaK4qaW4rij4ril77iY77i477i677i877i+77mA77mC77mE77mI77mc77me77y9772d772j86CBnfOggb3Cu+KAmeKAneKAuuKdrwCPA5wBKMu5y7vwlJeO8J2Fs/CdhbXwnYW38J2FueKBveKCjeKdqOKdquKfru+0vu+4te+5me+8iO+9n/OggKjinbLinbTin6bin6jin6rin6zipofiponippXiuKLiuKTvuJfvuLfvuLnvuLvvuL3vuL/vuYHvuYPvuYfvuZvvuZ3vvLvvvZvvvaLzoIGb86CBu8Kr4oCY4oCc4oC54p2uAJADCPCdqovwnaqkAJEDBPCdqosAkgMCwr0AkwMFMeKBhDIAlAMDMS8yAJUDA+CtswCWAwPgtbQAlwMD4rO9AJgDA+qgsQCZAwTwkIWBAJoDBPCQhbUAmwME8JCFtgCcAwTwkKa9AJ0DBPCQubsAngMD77yCAJ8DAX8AoAMBCAChAwPijKsAogMD4oymAKMDA+KQiACkAwPikKEApQMD4be7AKYDAQ8ApwMD4oaQAKgDA+KMpwCpAwPijZIAqgMD4o2UAKsDA+KNogCsAwPijasArQMBGgCuAwPikKYArwMD4pCaALADA++/vACxAwE/ALIDAsK/ALMDA+GlhQC0AwLNvgC1AwLVngC2AwLYnwC3AwPhjacAuAMD4oGHALkDA+KNsAC6AwPinZMAuwMD4p2UALwDA+KzugC9AwPis7sAvgMD4riuAL8DA+OJhADAAwPqmI8AwQMD6pu3AMIDA++4lgDDAwPvuZYAxAMD77yfAMUDBPCRhYMAxgME8J6lnwDHAwTzoIC/AMgDBPCWoYQAyQMD4q+RAMoDAsK2AMsDA+KBiwDMAwLcgADNAwPhg7sAzgMD4Y2oAM8DA+OAtwDQAwPinaEA0QMD4riPANIDA+K4kADTAwPiuJEA1AMD4riOANUDARQA1gMD4piZANcDA+K4vwDYAwPjgIcA2QMD4LmbANoDA+qZrgDbAwLPkwDcAwLPlADdAwPhupsA3gMK/BXfAwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLBABBCgsEAEELCwQAQQwLBABBDQsEAEEOCwQAQQ8LBABBEAsEAEERCwQAQRILBABBEwsEAEEUCwQAQRULBABBFgsEAEEXCwQAQRgLBABBGQsEAEEaCwQAQRsLBABBHAsEAEEdCwQAQR4LBABBHwsEAEEgCwQAQSELBABBIgsEAEEjCwQAQSQLBABBJQsEAEEmCwQAQScLBABBKAsEAEEpCwQAQSoLBABBKwsEAEEsCwQAQS0LBABBLgsEAEEvCwQAQTALBABBMQsEAEEyCwQAQTMLBABBNAsEAEE1CwQAQTYLBABBNwsEAEE4CwQAQTkLBABBOgsEAEE7CwQAQTwLBABBPQsEAEE+CwQAQT8LBQBBwAALBQBBwQALBQBBwgALBQBBwwALBQBBxAALBQBBxQALBQBBxgALBQBBxwALBQBByAALBQBByQALBQBBygALBQBBywALBQBBzAALBQBBzQALBQBBzgALBQBBzwALBQBB0AALBQBB0QALBQBB0gALBQBB0wALBQBB1AALBQBB1QALBQBB1gALBQBB1wALBQBB2AALBQBB2QALBQBB2gALBQBB2wALBQBB3AALBQBB3QALBQBB3gALBQBB3wALBQBB4AALBQBB4QALBQBB4gALBQBB4wALBQBB5AALBQBB5QALBQBB5gALBQBB5wALBQBB6AALBQBB6QALBQBB6gALBQBB6wALBQBB7AALBQBB7QALBQBB7gALBQBB7wALBQBB8AALBQBB8QALBQBB8gALBQBB8wALBQBB9AALBQBB9QALBQBB9gALBQBB9wALBQBB+AALBQBB+QALBQBB+gALBQBB+wALBQBB/AALBQBB/QALBQBB/gALBQBB/wALBQBBgAELBQBBgQELBQBBggELBQBBgwELBQBBhAELBQBBhQELBQBBhgELBQBBhwELBQBBiAELBQBBiQELBQBBigELBQBBiwELBQBBjAELBQBBjQELBQBBjgELBQBBjwELBQBBkAELBQBBkQELBQBBkgELBQBBkwELBQBBlAELBQBBlQELBQBBlgELBQBBlwELBQBBmAELBQBBmQELBQBBmgELBQBBmwELBQBBnAELBQBBnQELBQBBngELBQBBnwELBQBBoAELBQBBoQELBQBBogELBQBBowELBQBBpAELBQBBpQELBQBBpgELBQBBpwELBQBBqAELBQBBqQELBQBBqgELBQBBqwELBQBBrAELBQBBrQELBQBBrgELBQBBrwELBQBBsAELBQBBsQELBQBBsgELBQBBswELBQBBtAELBQBBtQELBQBBtgELBQBBtwELBQBBuAELBQBBuQELBQBBugELBQBBuwELBQBBvAELBQBBvQELBQBBvgELBQBBvwELBQBBwAELBQBBwQELBQBBwgELBQBBwwELBQBBxAELBQBBxQELBQBBxgELBQBBxwELBQBByAELBQBByQELBQBBygELBQBBywELBQBBzAELBQBBzQELBQBBzgELBQBBzwELBQBB0AELBQBB0QELBQBB0gELBQBB0wELBQBB1AELBQBB1QELBQBB1gELBQBB1wELBQBB2AELBQBB2QELBQBB2gELBQBB2wELBQBB3AELBQBB3QELBQBB3gELBQBB3wELBQBB4AELBQBB4QELBQBB4gELBQBB4wELBQBB5AELBQBB5QELBQBB5gELBQBB5wELBQBB6AELBQBB6QELBQBB6gELBQBB6wELBQBB7AELBQBB7QELBQBB7gELBQBB7wELBQBB8AELBQBB8QELBQBB8gELBQBB8wELBQBB9AELBQBB9QELBQBB9gELBQBB9wELBQBB+AELBQBB+QELBQBB+gELBQBB+wELBQBB/AELBQBB/QELBQBB/gELBQBB/wELBQBBgAILBQBBgQILBQBBggILBQBBgwILBQBBhAILBQBBhQILBQBBhgILBQBBhwILBQBBiAILBQBBiQILBQBBigILBQBBiwILBQBBjAILBQBBjQILBQBBjgILBQBBjwILBQBBkAILBQBBkQILBQBBkgILBQBBkwILBQBBlAILBQBBlQILBQBBlgILBQBBlwILBQBBmAILBQBBmQILBQBBmgILBQBBmwILBQBBnAILBQBBnQILBQBBngILBQBBnwILBQBBoAILBQBBoQILBQBBogILBQBBowILBQBBpAILBQBBpQILBQBBpgILBQBBpwILBQBBqAILBQBBqQILBQBBqgILBQBBqwILBQBBrAILBQBBrQILBQBBrgILBQBBrwILBQBBsAILBQBBsQILBQBBsgILBQBBswILBQBBtAILBQBBtQILBQBBtgILBQBBtwILBQBBuAILBQBBuQILBQBBugILBQBBuwILBQBBvAILBQBBvQILBQBBvgILBQBBvwILBQBBwAILBQBBwQILBQBBwgILBQBBwwILBQBBxAILBQBBxQILBQBBxgILBQBBxwILBQBByAILBQBByQILBQBBygILBQBBywILBQBBzAILBQBBzQILBQBBzgILBQBBzwILBQBB0AILBQBB0QILBQBB0gILBQBB0wILBQBB1AILBQBB1QILBQBB1gILBQBB1wILBQBB2AILBQBB2QILBQBB2gILBQBB2wILBQBB3AILBQBB3QILBQBB3gILBQBB3wILBQBB4AILBQBB4QILBQBB4gILBQBB4wILBQBB5AILBQBB5QILBQBB5gILBQBB5wILBQBB6AILBQBB6QILBQBB6gILBQBB6wILBQBB7AILBQBB7QILBQBB7gILBQBB7wILBQBB8AILBQBB8QILBQBB8gILBQBB8wILBQBB9AILBQBB9QILBQBB9gILBQBB9wILBQBB+AILBQBB+QILBQBB+gILBQBB+wILBQBB/AILBQBB/QILBQBB/gILBQBB/wILBQBBgAMLBQBBgQMLBQBBggMLBQBBgwMLBQBBhAMLBQBBhQMLBQBBhgMLBQBBhwMLBQBBiAMLBQBBiQMLBQBBigMLBQBBiwMLBQBBjAMLBQBBjQMLBQBBjgMLBQBBjwMLBQBBkAMLBQBBkQMLBQBBkgMLBQBBkwMLBQBBlAMLBQBBlQMLBQBBlgMLBQBBlwMLBQBBmAMLBQBBmQMLBQBBmgMLBQBBmwMLBQBBnAMLBQBBnQMLBQBBngMLBQBBnwMLBQBBoAMLBQBBoQMLBQBBogMLBQBBowMLBQBBpAMLBQBBpQMLBQBBpgMLBQBBpwMLBQBBqAMLBQBBqQMLBQBBqgMLBQBBqwMLBQBBrAMLBQBBrQMLBQBBrgMLBQBBrwMLBQBBsAMLBQBBsQMLBQBBsgMLBQBBswMLBQBBtAMLBQBBtQMLBQBBtgMLBQBBtwMLBQBBuAMLBQBBuQMLBQBBugMLBQBBuwMLBQBBvAMLBQBBvQMLBQBBvgMLBQBBvwMLBQBBwAMLBQBBwQMLBQBBwgMLBQBBwwMLBQBBxAMLBQBBxQMLBQBBxgMLBQBBxwMLBQBByAMLBQBByQMLBQBBygMLBQBBywMLBQBBzAMLBQBBzQMLBQBBzgMLBQBBzwMLBQBB0AMLBQBB0QMLBQBB0gMLBQBB0wMLBQBB1AMLBQBB1QMLBQBB1gMLBQBB1wMLBQBB2AMLBQBB2QMLBQBB2gMLBQBB2wMLBQBB3AMLBQBB3QMLBQBB3gML +valid valid names.wast:1095 AGFzbQEAAAABCgJgAX8AYAJ/fwACKwIIc3BlY3Rlc3QJcHJpbnRfaTMyAAAIc3BlY3Rlc3QJcHJpbnRfaTMyAAADAgEBBwsBB3ByaW50MzIAAgoMAQoAIAAQACABEAEL +valid valid nop.wast:3 AGFzbQEAAAABIwdgAn9/AX9gAABgA39/fwF/YAABf2ABfwBgAX8Bf2ACf38AA1RTAQIDAwMDBAQEBQUFBQUDAwMDAwMDAwQEBAUFBQUFBQUFBQUFBQUFAgICAgIFBQUFBQUFBQUFBQUFBQUFBQADAwMDAwUFBQUFBQMDAwUFBQYGBgYEBQFwAQEBBQMBAAEGBgF/AUEACwfqC1ANYXMtZnVuYy1maXJzdAACC2FzLWZ1bmMtbWlkAAMMYXMtZnVuYy1sYXN0AAQSYXMtZnVuYy1ldmVyeXdoZXJlAAUNYXMtZHJvcC1maXJzdAAGDGFzLWRyb3AtbGFzdAAHEmFzLWRyb3AtZXZlcnl3aGVyZQAID2FzLXNlbGVjdC1maXJzdAAJDmFzLXNlbGVjdC1taWQxAAoOYXMtc2VsZWN0LW1pZDIACw5hcy1zZWxlY3QtbGFzdAAMFGFzLXNlbGVjdC1ldmVyeXdoZXJlAA0OYXMtYmxvY2stZmlyc3QADgxhcy1ibG9jay1taWQADw1hcy1ibG9jay1sYXN0ABATYXMtYmxvY2stZXZlcnl3aGVyZQARDWFzLWxvb3AtZmlyc3QAEgthcy1sb29wLW1pZAATDGFzLWxvb3AtbGFzdAAUEmFzLWxvb3AtZXZlcnl3aGVyZQAVD2FzLWlmLWNvbmRpdGlvbgAWCmFzLWlmLXRoZW4AFwphcy1pZi1lbHNlABgLYXMtYnItZmlyc3QAGQphcy1ici1sYXN0ABoQYXMtYnItZXZlcnl3aGVyZQAbDmFzLWJyX2lmLWZpcnN0ABwMYXMtYnJfaWYtbWlkAB0NYXMtYnJfaWYtbGFzdAAeE2FzLWJyX2lmLWV2ZXJ5d2hlcmUAHxFhcy1icl90YWJsZS1maXJzdAAgD2FzLWJyX3RhYmxlLW1pZAAhEGFzLWJyX3RhYmxlLWxhc3QAIhZhcy1icl90YWJsZS1ldmVyeXdoZXJlACMPYXMtcmV0dXJuLWZpcnN0ACQOYXMtcmV0dXJuLWxhc3QAJRRhcy1yZXR1cm4tZXZlcnl3aGVyZQAmDWFzLWNhbGwtZmlyc3QAJwxhcy1jYWxsLW1pZDEAKAxhcy1jYWxsLW1pZDIAKQxhcy1jYWxsLWxhc3QAKhJhcy1jYWxsLWV2ZXJ5d2hlcmUAKw5hcy11bmFyeS1maXJzdAAsDWFzLXVuYXJ5LWxhc3QALRNhcy11bmFyeS1ldmVyeXdoZXJlAC4PYXMtYmluYXJ5LWZpcnN0AC8NYXMtYmluYXJ5LW1pZAAwDmFzLWJpbmFyeS1sYXN0ADEUYXMtYmluYXJ5LWV2ZXJ5d2hlcmUAMg1hcy10ZXN0LWZpcnN0ADMMYXMtdGVzdC1sYXN0ADQSYXMtdGVzdC1ldmVyeXdoZXJlADUQYXMtY29tcGFyZS1maXJzdAA2DmFzLWNvbXBhcmUtbWlkADcPYXMtY29tcGFyZS1sYXN0ADgVYXMtY29tcGFyZS1ldmVyeXdoZXJlADkUYXMtbWVtb3J5Lmdyb3ctZmlyc3QAOhNhcy1tZW1vcnkuZ3Jvdy1sYXN0ADsZYXMtbWVtb3J5Lmdyb3ctZXZlcnl3aGVyZQA8FmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAPhVhcy1jYWxsX2luZGlyZWN0LW1pZDEAPxVhcy1jYWxsX2luZGlyZWN0LW1pZDIAQBVhcy1jYWxsX2luZGlyZWN0LWxhc3QAQRthcy1jYWxsX2luZGlyZWN0LWV2ZXJ5d2hlcmUAQhJhcy1sb2NhbC5zZXQtZmlyc3QAQxFhcy1sb2NhbC5zZXQtbGFzdABEF2FzLWxvY2FsLnNldC1ldmVyeXdoZXJlAEUSYXMtbG9jYWwudGVlLWZpcnN0AEYRYXMtbG9jYWwudGVlLWxhc3QARxdhcy1sb2NhbC50ZWUtZXZlcnl3aGVyZQBIE2FzLWdsb2JhbC5zZXQtZmlyc3QASRJhcy1nbG9iYWwuc2V0LWxhc3QAShhhcy1nbG9iYWwuc2V0LWV2ZXJ5d2hlcmUASw1hcy1sb2FkLWZpcnN0AEwMYXMtbG9hZC1sYXN0AE0SYXMtbG9hZC1ldmVyeXdoZXJlAE4OYXMtc3RvcmUtZmlyc3QATwxhcy1zdG9yZS1taWQAUA1hcy1zdG9yZS1sYXN0AFETYXMtc3RvcmUtZXZlcnl3aGVyZQBSCQcBAEEACwE9CqIHUwIACwoAIAAgASACa2oLBQABQQELBwAQAAFBAgsHABAAQQMBCwsAAQEQAAFBBAEBCwYAASAAGgsGACAAARoLCQABASAAAQEaCwoAASAAIAAgABsLCgAgAAEgACAAGwsKACAAIAABIAAbCwoAIAAgACAAARsLEAABIAABASAAAQEgAAEBGwsIAAJ/AUECCwsKAAJ/EAABQQILCwsAAn8BEABBAwELCw4AAn8BARAAAUEEAQELCwgAA38BQQILCwoAA38QAAFBAgsLCgADfxAAQQMBCwsOAAN/AQEQAAFBBAEBCwsKACAAAQRAEAALCwsAIAAEQAEFEAALCwsAIAAEQBAABQELCwoAAn8BIAAMAAsLCgACfyAAAQwACwsNAAJ/AQEgAAEBDAALCwwAAn8BIAAgAA0ACwsMAAJ/IAABIAANAAsLDAACfyAAIAABDQALCxEAAn8BASAAAQEgAAEBDQALCw4AAn8BIAAgAA4BAAALCw4AAn8gAAEgAA4BAAALCw4AAn8gACAAAQ4BAAALCxMAAn8BASAAAQEgAAEBDgEAAAsLBgABIAAPCwYAIAABDwsJAAEBIAABAQ8LCwABIAAgASACEAELCwAgAAEgASACEAELCwAgACABASACEAELCwAgACABIAIBEAELEgABASAAAQEgAQEBIAIBARABCwYAASAAaAsGACAAAWgLCQABASAAAQFoCwgAASAAIABqCwgAIAABIABqCwgAIAAgAAFqCwwAASAAAQEgAAEBagsGAAEgAEULBgAgAAFFCwkAAQEgAAEBRQsIAAEgACAARwsIACAAASAARwsIACAAIAABSQsMAAEgAAEBIAABAUwLBwABIABAAAsHACAAAUAACwoAAQEgAAEBQAALBAAgAAsPAAJ/AUEBQQJBABEAAAsLDwACf0EBAUECQQARAAALCw8AAn9BAUECAUEAEQAACwsPAAJ/QQFBAkEAAREAAAsLFgACfwEBQQEBAUECAQFBAAEBEQAACwsJAAFBAiEAIAALCQBBAgEhACAACwwAAQFBAgEBIQAgAAsHAAFBAiIACwcAQQIBIgALCgABAUECAQEiAAsJAAFBAiQAIwALCQBBAgEkACMACwwAAQFBAgEBJAAjAAsIAAEgACgCAAsIACAAASgCAAsLAAEBIAABASgCAAsKAAEgACABNgIACwoAIAABIAE2AgALCgAgACABATYCAAsPAAEBIAABASABAQE2AgAL +invalid invalid nop.wast:416 AGFzbQEAAAABBQFgAAF+AwIBAAoFAQMAAQs= +invalid invalid nop.wast:420 AGFzbQEAAAABBQFgAAF9AwIBAAoFAQMAAQs= +invalid invalid nop.wast:424 AGFzbQEAAAABBQFgAAF8AwIBAAoFAQMAAQs= +valid valid ref_func.wast:1 AGFzbQEAAAABBgFgAX8BfwMCAQAHBQEBZgAACgYBBAAgAAs= +valid valid ref_func.wast:6 AGFzbQEAAAABDQNgAX8Bf2AAAGAAAX8CBwEBTQFmAAADDw4AAQEBAQECAgIBAQAAAAQEAXAAAQYaBXAA0gALcADSAQtwAdIAC3AA0gMLcADSBAsHUAgJaXNfbnVsbC1mAAcJaXNfbnVsbC1nAAgJaXNfbnVsbC12AAkFc2V0LWYACgVzZXQtZwALBmNhbGwtZgAMBmNhbGwtZwANBmNhbGwtdgAOCRADAwACAwUDAAIEBgMAAgABCm4OBwAgAEEBagsIANIFGtIGGgsCAAsCAAsCAAsCAAsFANIA0QsFANIB0QsFACMC0QsGANIAJAILBgDSASQCCw8AQQDSACYAIABBABEAAAsPAEEA0gEmACAAQQARAAALDwBBACMCJgAgAEEAEQAACw== +invalid invalid ref_func.wast:69 AGFzbQEAAAABBgFgAX8BfwINAgFNAWYAAAFNAWcAAAYGAXAA0gcL +valid valid ref_func.wast:80 AGFzbQEAAAABBAFgAAADCAcAAAAAAAAABAQBcAABBgYBcADSAAsHBQEBZgABCRUEAEEACwECAEEACwEDAQABBAEAAQUKIwcCAAsCAAsCAAsCAAsCAAsCAAsPANIA0gHSAtID0gTSBQ8L +invalid invalid ref_func.wast:109 AGFzbQEAAAABBAFgAAADAgEACgcBBQDSABoL +invalid invalid ref_func.wast:113 AGFzbQEAAAABBAFgAAADAgEACAEACgcBBQDSABoL +valid valid ref_is_null.wast:1 AGFzbQEAAAABFwVgAXABf2ABbwF/YAAAYAFvAGABfwF/AwgHAAECAwIEBAQHAnAAAm8AAgdHBgdmdW5jcmVmAAAJZXh0ZXJucmVmAAEEaW5pdAADBmRlaW5pdAAEDGZ1bmNyZWYtZWxlbQAFDmV4dGVybnJlZi1lbGVtAAYJBwEAQQELAQIKOgcFACAA0QsFACAA0QsCAAsIAEEBIAAmAQsOAEEB0HAmAEEB0G8mAQsIACAAJQAQAAsIACAAJQEQAQs= +invalid invalid ref_is_null.wast:52 AGFzbQEAAAABBQFgAX8AAwIBAAoHAQUAIADRCw== +invalid invalid ref_is_null.wast:56 AGFzbQEAAAABBAFgAAADAgEACgUBAwDRCw== +valid valid ref_null.wast:1 AGFzbQEAAAABCQJgAAFvYAABcAMDAgABBgsCbwDQbwtwANBwCwcXAglleHRlcm5yZWYAAAdmdW5jcmVmAAEKCwIEANBvCwQA0HAL +valid valid return.wast:3 AGFzbQEAAAABIQdgA39/fwF/YAAAYAABf2AAAX5gAAF9YAABfGACf38BfwM+PQEBAQEBAgMEBQEFAgIBAgEBAQICAgICAQICAwICAwIGBgYGAgACAgICAgICAgICBAMCAgICBAIDAgICAgIEBQFwAQEBBQMBAAEGBgF/AUEACwf4BzsIdHlwZS1pMzIAAQh0eXBlLWk2NAACCHR5cGUtZjMyAAMIdHlwZS1mNjQABA50eXBlLWkzMi12YWx1ZQAFDnR5cGUtaTY0LXZhbHVlAAYOdHlwZS1mMzItdmFsdWUABw50eXBlLWY2NC12YWx1ZQAIB251bGxhcnkACQV1bmFyeQAKDWFzLWZ1bmMtZmlyc3QACwthcy1mdW5jLW1pZAAMDGFzLWZ1bmMtbGFzdAANDWFzLWZ1bmMtdmFsdWUADg5hcy1ibG9jay1maXJzdAAPDGFzLWJsb2NrLW1pZAAQDWFzLWJsb2NrLWxhc3QAEQ5hcy1ibG9jay12YWx1ZQASDWFzLWxvb3AtZmlyc3QAEwthcy1sb29wLW1pZAAUDGFzLWxvb3AtbGFzdAAVC2FzLWJyLXZhbHVlABYNYXMtYnJfaWYtY29uZAAXDmFzLWJyX2lmLXZhbHVlABgTYXMtYnJfaWYtdmFsdWUtY29uZAAZEWFzLWJyX3RhYmxlLWluZGV4ABoRYXMtYnJfdGFibGUtdmFsdWUAGxdhcy1icl90YWJsZS12YWx1ZS1pbmRleAAcD2FzLXJldHVybi12YWx1ZQAdCmFzLWlmLWNvbmQAHgphcy1pZi10aGVuAB8KYXMtaWYtZWxzZQAgD2FzLXNlbGVjdC1maXJzdAAhEGFzLXNlbGVjdC1zZWNvbmQAIg5hcy1zZWxlY3QtY29uZAAjDWFzLWNhbGwtZmlyc3QAJQthcy1jYWxsLW1pZAAmDGFzLWNhbGwtbGFzdAAnFWFzLWNhbGxfaW5kaXJlY3QtZnVuYwAoFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAKRRhcy1jYWxsX2luZGlyZWN0LW1pZAAqFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAArEmFzLWxvY2FsLnNldC12YWx1ZQAsEmFzLWxvY2FsLnRlZS12YWx1ZQAtE2FzLWdsb2JhbC5zZXQtdmFsdWUALg9hcy1sb2FkLWFkZHJlc3MALxBhcy1sb2FkTi1hZGRyZXNzADAQYXMtc3RvcmUtYWRkcmVzcwAxDmFzLXN0b3JlLXZhbHVlADIRYXMtc3RvcmVOLWFkZHJlc3MAMw9hcy1zdG9yZU4tdmFsdWUANBBhcy11bmFyeS1vcGVyYW5kADUOYXMtYmluYXJ5LWxlZnQANg9hcy1iaW5hcnktcmlnaHQANw9hcy10ZXN0LW9wZXJhbmQAOA9hcy1jb21wYXJlLWxlZnQAORBhcy1jb21wYXJlLXJpZ2h0ADoSYXMtY29udmVydC1vcGVyYW5kADsTYXMtbWVtb3J5Lmdyb3ctc2l6ZQA8CQcBAEEACwEkCqkFPQIACwUAD2gaCwUAD3oaCwUAD4waCwUAD5oaCwkAAn9BAQ9oCwsJAAJ+QgIPegsLDAACfUMAAEBAD4wLCxAAAnxEAAAAAAAAEEAPmgsLAwAPCwwARAAAAAAAAAhADwsHAEEBD0ECCwkAEABBAg9BAwsGAAEQAA8LCAABEABBAw8LCAACQA8QAAsLCgACQBAADxAACwsJAAJAARAADwsLCwACfwEQAEECDwsLCgADf0EDD0ECCwsMAAN/EABBBA9BAgsLCwADfwEQAEEFDwsLCgACf0EJDwwACwsIAAJADw0ACwsPAAJ/QQgPQQENABpBBwsLDwACf0EGQQkPDQAaQQcLCw8AAkBCCQ8OAgAAAAtCfwsRAAJ/QQoPQQEOAgAAAEEHCwsQAAJ/QQZBCw8OAQAAQQcLCwYAQgcPDwsNAEECDwR/QQAFQQELCw0AIAAEf0EDDwUgAQsLDQAgAAR/IAEFQQQPCwsKAEEFDyAAIAEbCwoAIABBBg8gARsLCgBBAEEBQQcPGwsEAEF/CwsAQQwPQQJBAxAkCwsAQQFBDQ9BAxAkCwsAQQFBAkEODxAkCw4AQRQPQQFBAkEDEQAACw4AQQBBFQ9BAkEDEQAACw4AQQBBAUEWD0EDEQAACw4AQQBBAUECQRcPEQAACwsBAX1BEQ8hAEF/CwkBAX9BAQ8iAAsHAEEBDyQACwsAQ5qZ2T8PKgIACwgAQh4PMAAACxMAQR4PRAAAAAAAABxAOQMAQX8LDABBAkEfDzcDAEF/CwwAQSAPQQc6AABBfwsMAEECQSEPPQEAQX8LCQBDmplZQA+MCwgAQQMPQQpqCwgAQgpCLQ99CwYAQSwPRQsPAEErD0QAAAAAAAAkQGULCwBDAAAgQUEqD1wLBgBBKQ+nCwcAQSgPQAAL +invalid invalid return.wast:315 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAQQACQA8LCw== +invalid invalid return.wast:324 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAQQADQA8LCw== +invalid invalid return.wast:333 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQQBBAARADwsL +invalid invalid return.wast:342 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AQQBBAAR/QQAFDwsaCw== +invalid invalid return.wast:351 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQQACQA8MAAsL +invalid invalid return.wast:360 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAQQACQA9BAQ0ACws= +invalid invalid return.wast:369 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAQQACQA8OAAALCw== +invalid invalid return.wast:378 AGFzbQEAAAABBQFgAAF/AwIBAAoGAQQADw8L +invalid invalid return.wast:386 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAD0EBQQIbCw== +invalid invalid return.wast:394 AGFzbQEAAAABCgJgAAF/YAF/AX8DAwIAAQoMAgUADxABCwQAIAAL +invalid invalid return.wast:403 AGFzbQEAAAABCgJgAX8Bf2AAAX8DAwIAAQQFAXABAQEJBwEAQQALAQAKEgIEACAACwsAAn8PQQARAAALCw== +invalid invalid return.wast:418 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkBAX8PIQAgAAs= +invalid invalid return.wast:427 AGFzbQEAAAABBQFgAAF/AwIBAAoJAQcBAX8PIgAL +invalid invalid return.wast:436 AGFzbQEAAAABBQFgAAF/AwIBAAYGAX8BQQALCgkBBwAPJAAjAAs= +invalid invalid return.wast:445 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQAACgcBBQAPQAAL +invalid invalid return.wast:454 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQAACggBBgAPKAIACw== +invalid invalid return.wast:463 AGFzbQEAAAABBQFgAAF/AwIBAAUDAQABCgoBCAAPQQE2AgAL +invalid invalid return.wast:472 AGFzbQEAAAABBQFgAAF8AwIBAAoGAQQAAQ8L +invalid invalid return.wast:476 AGFzbQEAAAABBQFgAAF8AwIBAAoHAQUAQgEPCw== +valid valid select.wast:1 AGFzbQEAAAABPQpgAn9/AX9gAABgA39/fwF/YAN+fn8BfmADfX1/AX1gA3x8fwF8YANwcH8BcGADb29/AW9gAX8Bf2ABfwADLy4BAgMEBQIDBAUGBwgICAgICAkICAgICAgACAgICQkICAgICQgICAgICAgICAgIBAkCcAEBAXABAQEFAwEAAQYGAX8BQQoLB/AFKwpzZWxlY3QtaTMyAAEKc2VsZWN0LWk2NAACCnNlbGVjdC1mMzIAAwpzZWxlY3QtZjY0AAQMc2VsZWN0LWkzMi10AAUMc2VsZWN0LWk2NC10AAYMc2VsZWN0LWYzMi10AAcMc2VsZWN0LWY2NC10AAgOc2VsZWN0LWZ1bmNyZWYACRBzZWxlY3QtZXh0ZXJucmVmAAoPYXMtc2VsZWN0LWZpcnN0AAsNYXMtc2VsZWN0LW1pZAAMDmFzLXNlbGVjdC1sYXN0AA0NYXMtbG9vcC1maXJzdAAOC2FzLWxvb3AtbWlkAA8MYXMtbG9vcC1sYXN0ABAPYXMtaWYtY29uZGl0aW9uABEKYXMtaWYtdGhlbgASCmFzLWlmLWVsc2UAEw5hcy1icl9pZi1maXJzdAAUDWFzLWJyX2lmLWxhc3QAFRFhcy1icl90YWJsZS1maXJzdAAWEGFzLWJyX3RhYmxlLWxhc3QAFxZhcy1jYWxsX2luZGlyZWN0LWZpcnN0ABkUYXMtY2FsbF9pbmRpcmVjdC1taWQAGhVhcy1jYWxsX2luZGlyZWN0LWxhc3QAGw5hcy1zdG9yZS1maXJzdAAcDWFzLXN0b3JlLWxhc3QAHRRhcy1tZW1vcnkuZ3Jvdy12YWx1ZQAeDWFzLWNhbGwtdmFsdWUAIA9hcy1yZXR1cm4tdmFsdWUAIQ9hcy1kcm9wLW9wZXJhbmQAIgthcy1ici12YWx1ZQAjEmFzLWxvY2FsLnNldC12YWx1ZQAkEmFzLWxvY2FsLnRlZS12YWx1ZQAlE2FzLWdsb2JhbC5zZXQtdmFsdWUAJg9hcy1sb2FkLW9wZXJhbmQAJxBhcy11bmFyeS1vcGVyYW5kACgRYXMtYmluYXJ5LW9wZXJhbmQAKQ9hcy10ZXN0LW9wZXJhbmQAKg9hcy1jb21wYXJlLWxlZnQAKxBhcy1jb21wYXJlLXJpZ2h0ACwSYXMtY29udmVydC1vcGVyYW5kAC0JDwIAQQALAQACAUEACwABGAqABS4CAAsJACAAIAEgAhsLCQAgACABIAIbCwkAIAAgASACGwsJACAAIAEgAhsLCwAgACABIAIcAX8LCwAgACABIAIcAX4LCwAgACABIAIcAX0LCwAgACABIAIcAXwLCwAgACABIAIcAXALCwAgACABIAIcAW8LDgBBAEEBIAAbQQJBAxsLDgBBAkEAQQEgABtBAxsLDgBBAkEDQQBBASAAGxsLEAADf0ECQQMgABsQABAACwsQAAN/EABBAkEDIAAbEAALCxAAA38QABAAQQJBAyAAGwsLDgBBAkEDIAAbBEAQAAsLEQBBAQR/QQJBAyAAGwVBBAsLEQBBAAR/QQIFQQJBAyAAGwsLEAACf0ECQQMgABtBBA0ACwsQAAJ/QQJBAkEDIAAbDQALCxIAAn9BAkEDIAAbQQIOAQAACwsSAAJ/QQJBAkEDIAAbDgEAAAsLBAAgAAsTAAJ/QQJBAyAAG0EBQQARAAELCxMAAn9BAUECQQMgABtBABEAAQsLEwACf0EBQQRBAkEDIAAbEQABCwsOAEEAQQQgABtBATYCAAsOAEEIQQFBAiAAGzYCAAsLAEEBQQIgABtAAAsEACAACwsAQQFBAiAAGxAfCwoAQQFBAiAAGw8LCgBBAUECIAAbGgsOAAJ/QQFBAiAAGwwACwsPAQF/QQFBAiAAGyEAIAALCwBBAUECIAAbIgALDQBBAUECIAAbJAAjAAsMAEEAQQQgABsoAgALCgBBAEEBIAAbRQsRAEEBQQIgABtBAUECIAAbbAsNAAJ/QQBBASAAG0ULCw8AAn9BAUECIAAbQQFMCwsPAAJ/QQFBAEEBIAAbRwsLDQACf0IBQgAgABunCws= +invalid invalid select.wast:320 AGFzbQEAAAABBAFgAAADAgEACgkBBwABAUEBGws= +invalid invalid select.wast:328 AGFzbQEAAAABBgFgAAJ/fwMCAQAKEgEQAEEAQQBBAEEAQQEcAn9/Cw== +invalid invalid select.wast:340 AGFzbQEAAAABBQFgAW8AAwIBAAoMAQoAIAAgAEEBGxoL +invalid invalid select.wast:347 AGFzbQEAAAABBAFgAAADAgEACgwBCgBBAUIBQQEbGgs= +invalid invalid select.wast:353 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAUMAAIA/QQEbGgs= +invalid invalid select.wast:359 AGFzbQEAAAABBAFgAAADAgEAChMBEQBBAUQAAAAAAADwP0EBGxoL +invalid invalid select.wast:388 AGFzbQEAAAABBAFgAAADAgEACgYBBAAbGgs= +invalid invalid select.wast:396 AGFzbQEAAAABBAFgAAADAgEACggBBgBBABsaCw== +invalid invalid select.wast:404 AGFzbQEAAAABBAFgAAADAgEACgoBCABBAEEAGxoL +invalid invalid select.wast:412 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEAQQACQBsaCws= +invalid invalid select.wast:421 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEAAkBBABsaCws= +invalid invalid select.wast:430 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAAJAQQBBABsaCws= +invalid invalid select.wast:439 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEAQQADQBsaCws= +invalid invalid select.wast:448 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEAA0BBABsaCws= +invalid invalid select.wast:457 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAANAQQBBABsaCws= +invalid invalid select.wast:466 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEAQQAEQBsaCws= +invalid invalid select.wast:475 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAEEABEBBABsaCws= +invalid invalid select.wast:484 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAARAQQBBABsaCws= +invalid invalid select.wast:496 AGFzbQEAAAABBAFgAAADAgEACgwBCgBBAUEBQgEbGgs= +invalid invalid select.wast:500 AGFzbQEAAAABBAFgAAADAgEACg8BDQBBAUEBQwAAgD8bGgs= +invalid invalid select.wast:504 AGFzbQEAAAABBAFgAAADAgEAChMBEQBBAUEBRAAAAAAAAPA/GxoL +invalid invalid select.wast:511 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAQgFCAUEBGws= +valid valid select.wast:518 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAAEBAFwAAEKUAsEAAAbCwUAABsBCwUAABsbCwUAABsbCwYAABsbGwsGAAAcAX8LBgAAHAF/CwcAABwBfxsLCQAAHAF/HAF/CwcAABsRAQALCAAAGxEBABsL +valid valid skip-stack-guard-page.wast:2 AGFzbQEAAAABCAJgAX8AYAAAAwMCAAEFAwEAAQcYARR0ZXN0LWd1YXJkLXBhZ2Utc2tpcAAACuCQAQIUACAAQQBGBEAQAQUgAEEBaxAACwvHkAEBoAh+EAFBACkAACEAQQApAAEhAUEAKQACIQJBACkAAyEDQQApAAQhBEEAKQAFIQVBACkABiEGQQApAAchB0EAKQAIIQhBACkACSEJQQApAAohCkEAKQALIQtBACkADCEMQQApAA0hDUEAKQAOIQ5BACkADyEPQQApABAhEEEAKQARIRFBACkAEiESQQApABMhE0EAKQAUIRRBACkAFSEVQQApABYhFkEAKQAXIRdBACkAGCEYQQApABkhGUEAKQAaIRpBACkAGyEbQQApABwhHEEAKQAdIR1BACkAHiEeQQApAB8hH0EAKQAgISBBACkAISEhQQApACIhIkEAKQAjISNBACkAJCEkQQApACUhJUEAKQAmISZBACkAJyEnQQApACghKEEAKQApISlBACkAKiEqQQApACshK0EAKQAsISxBACkALSEtQQApAC4hLkEAKQAvIS9BACkAMCEwQQApADEhMUEAKQAyITJBACkAMyEzQQApADQhNEEAKQA1ITVBACkANiE2QQApADchN0EAKQA4IThBACkAOSE5QQApADohOkEAKQA7ITtBACkAPCE8QQApAD0hPUEAKQA+IT5BACkAPyE/QQApAEAhQEEAKQBBIUFBACkAQiFCQQApAEMhQ0EAKQBEIURBACkARSFFQQApAEYhRkEAKQBHIUdBACkASCFIQQApAEkhSUEAKQBKIUpBACkASyFLQQApAEwhTEEAKQBNIU1BACkATiFOQQApAE8hT0EAKQBQIVBBACkAUSFRQQApAFIhUkEAKQBTIVNBACkAVCFUQQApAFUhVUEAKQBWIVZBACkAVyFXQQApAFghWEEAKQBZIVlBACkAWiFaQQApAFshW0EAKQBcIVxBACkAXSFdQQApAF4hXkEAKQBfIV9BACkAYCFgQQApAGEhYUEAKQBiIWJBACkAYyFjQQApAGQhZEEAKQBlIWVBACkAZiFmQQApAGchZ0EAKQBoIWhBACkAaSFpQQApAGohakEAKQBrIWtBACkAbCFsQQApAG0hbUEAKQBuIW5BACkAbyFvQQApAHAhcEEAKQBxIXFBACkAciFyQQApAHMhc0EAKQB0IXRBACkAdSF1QQApAHYhdkEAKQB3IXdBACkAeCF4QQApAHkheUEAKQB6IXpBACkAeyF7QQApAHwhfEEAKQB9IX1BACkAfiF+QQApAH8hf0EAKQCAASGAAUEAKQCBASGBAUEAKQCCASGCAUEAKQCDASGDAUEAKQCEASGEAUEAKQCFASGFAUEAKQCGASGGAUEAKQCHASGHAUEAKQCIASGIAUEAKQCJASGJAUEAKQCKASGKAUEAKQCLASGLAUEAKQCMASGMAUEAKQCNASGNAUEAKQCOASGOAUEAKQCPASGPAUEAKQCQASGQAUEAKQCRASGRAUEAKQCSASGSAUEAKQCTASGTAUEAKQCUASGUAUEAKQCVASGVAUEAKQCWASGWAUEAKQCXASGXAUEAKQCYASGYAUEAKQCZASGZAUEAKQCaASGaAUEAKQCbASGbAUEAKQCcASGcAUEAKQCdASGdAUEAKQCeASGeAUEAKQCfASGfAUEAKQCgASGgAUEAKQChASGhAUEAKQCiASGiAUEAKQCjASGjAUEAKQCkASGkAUEAKQClASGlAUEAKQCmASGmAUEAKQCnASGnAUEAKQCoASGoAUEAKQCpASGpAUEAKQCqASGqAUEAKQCrASGrAUEAKQCsASGsAUEAKQCtASGtAUEAKQCuASGuAUEAKQCvASGvAUEAKQCwASGwAUEAKQCxASGxAUEAKQCyASGyAUEAKQCzASGzAUEAKQC0ASG0AUEAKQC1ASG1AUEAKQC2ASG2AUEAKQC3ASG3AUEAKQC4ASG4AUEAKQC5ASG5AUEAKQC6ASG6AUEAKQC7ASG7AUEAKQC8ASG8AUEAKQC9ASG9AUEAKQC+ASG+AUEAKQC/ASG/AUEAKQDAASHAAUEAKQDBASHBAUEAKQDCASHCAUEAKQDDASHDAUEAKQDEASHEAUEAKQDFASHFAUEAKQDGASHGAUEAKQDHASHHAUEAKQDIASHIAUEAKQDJASHJAUEAKQDKASHKAUEAKQDLASHLAUEAKQDMASHMAUEAKQDNASHNAUEAKQDOASHOAUEAKQDPASHPAUEAKQDQASHQAUEAKQDRASHRAUEAKQDSASHSAUEAKQDTASHTAUEAKQDUASHUAUEAKQDVASHVAUEAKQDWASHWAUEAKQDXASHXAUEAKQDYASHYAUEAKQDZASHZAUEAKQDaASHaAUEAKQDbASHbAUEAKQDcASHcAUEAKQDdASHdAUEAKQDeASHeAUEAKQDfASHfAUEAKQDgASHgAUEAKQDhASHhAUEAKQDiASHiAUEAKQDjASHjAUEAKQDkASHkAUEAKQDlASHlAUEAKQDmASHmAUEAKQDnASHnAUEAKQDoASHoAUEAKQDpASHpAUEAKQDqASHqAUEAKQDrASHrAUEAKQDsASHsAUEAKQDtASHtAUEAKQDuASHuAUEAKQDvASHvAUEAKQDwASHwAUEAKQDxASHxAUEAKQDyASHyAUEAKQDzASHzAUEAKQD0ASH0AUEAKQD1ASH1AUEAKQD2ASH2AUEAKQD3ASH3AUEAKQD4ASH4AUEAKQD5ASH5AUEAKQD6ASH6AUEAKQD7ASH7AUEAKQD8ASH8AUEAKQD9ASH9AUEAKQD+ASH+AUEAKQD/ASH/AUEAKQCAAiGAAkEAKQCBAiGBAkEAKQCCAiGCAkEAKQCDAiGDAkEAKQCEAiGEAkEAKQCFAiGFAkEAKQCGAiGGAkEAKQCHAiGHAkEAKQCIAiGIAkEAKQCJAiGJAkEAKQCKAiGKAkEAKQCLAiGLAkEAKQCMAiGMAkEAKQCNAiGNAkEAKQCOAiGOAkEAKQCPAiGPAkEAKQCQAiGQAkEAKQCRAiGRAkEAKQCSAiGSAkEAKQCTAiGTAkEAKQCUAiGUAkEAKQCVAiGVAkEAKQCWAiGWAkEAKQCXAiGXAkEAKQCYAiGYAkEAKQCZAiGZAkEAKQCaAiGaAkEAKQCbAiGbAkEAKQCcAiGcAkEAKQCdAiGdAkEAKQCeAiGeAkEAKQCfAiGfAkEAKQCgAiGgAkEAKQChAiGhAkEAKQCiAiGiAkEAKQCjAiGjAkEAKQCkAiGkAkEAKQClAiGlAkEAKQCmAiGmAkEAKQCnAiGnAkEAKQCoAiGoAkEAKQCpAiGpAkEAKQCqAiGqAkEAKQCrAiGrAkEAKQCsAiGsAkEAKQCtAiGtAkEAKQCuAiGuAkEAKQCvAiGvAkEAKQCwAiGwAkEAKQCxAiGxAkEAKQCyAiGyAkEAKQCzAiGzAkEAKQC0AiG0AkEAKQC1AiG1AkEAKQC2AiG2AkEAKQC3AiG3AkEAKQC4AiG4AkEAKQC5AiG5AkEAKQC6AiG6AkEAKQC7AiG7AkEAKQC8AiG8AkEAKQC9AiG9AkEAKQC+AiG+AkEAKQC/AiG/AkEAKQDAAiHAAkEAKQDBAiHBAkEAKQDCAiHCAkEAKQDDAiHDAkEAKQDEAiHEAkEAKQDFAiHFAkEAKQDGAiHGAkEAKQDHAiHHAkEAKQDIAiHIAkEAKQDJAiHJAkEAKQDKAiHKAkEAKQDLAiHLAkEAKQDMAiHMAkEAKQDNAiHNAkEAKQDOAiHOAkEAKQDPAiHPAkEAKQDQAiHQAkEAKQDRAiHRAkEAKQDSAiHSAkEAKQDTAiHTAkEAKQDUAiHUAkEAKQDVAiHVAkEAKQDWAiHWAkEAKQDXAiHXAkEAKQDYAiHYAkEAKQDZAiHZAkEAKQDaAiHaAkEAKQDbAiHbAkEAKQDcAiHcAkEAKQDdAiHdAkEAKQDeAiHeAkEAKQDfAiHfAkEAKQDgAiHgAkEAKQDhAiHhAkEAKQDiAiHiAkEAKQDjAiHjAkEAKQDkAiHkAkEAKQDlAiHlAkEAKQDmAiHmAkEAKQDnAiHnAkEAKQDoAiHoAkEAKQDpAiHpAkEAKQDqAiHqAkEAKQDrAiHrAkEAKQDsAiHsAkEAKQDtAiHtAkEAKQDuAiHuAkEAKQDvAiHvAkEAKQDwAiHwAkEAKQDxAiHxAkEAKQDyAiHyAkEAKQDzAiHzAkEAKQD0AiH0AkEAKQD1AiH1AkEAKQD2AiH2AkEAKQD3AiH3AkEAKQD4AiH4AkEAKQD5AiH5AkEAKQD6AiH6AkEAKQD7AiH7AkEAKQD8AiH8AkEAKQD9AiH9AkEAKQD+AiH+AkEAKQD/AiH/AkEAKQCAAyGAA0EAKQCBAyGBA0EAKQCCAyGCA0EAKQCDAyGDA0EAKQCEAyGEA0EAKQCFAyGFA0EAKQCGAyGGA0EAKQCHAyGHA0EAKQCIAyGIA0EAKQCJAyGJA0EAKQCKAyGKA0EAKQCLAyGLA0EAKQCMAyGMA0EAKQCNAyGNA0EAKQCOAyGOA0EAKQCPAyGPA0EAKQCQAyGQA0EAKQCRAyGRA0EAKQCSAyGSA0EAKQCTAyGTA0EAKQCUAyGUA0EAKQCVAyGVA0EAKQCWAyGWA0EAKQCXAyGXA0EAKQCYAyGYA0EAKQCZAyGZA0EAKQCaAyGaA0EAKQCbAyGbA0EAKQCcAyGcA0EAKQCdAyGdA0EAKQCeAyGeA0EAKQCfAyGfA0EAKQCgAyGgA0EAKQChAyGhA0EAKQCiAyGiA0EAKQCjAyGjA0EAKQCkAyGkA0EAKQClAyGlA0EAKQCmAyGmA0EAKQCnAyGnA0EAKQCoAyGoA0EAKQCpAyGpA0EAKQCqAyGqA0EAKQCrAyGrA0EAKQCsAyGsA0EAKQCtAyGtA0EAKQCuAyGuA0EAKQCvAyGvA0EAKQCwAyGwA0EAKQCxAyGxA0EAKQCyAyGyA0EAKQCzAyGzA0EAKQC0AyG0A0EAKQC1AyG1A0EAKQC2AyG2A0EAKQC3AyG3A0EAKQC4AyG4A0EAKQC5AyG5A0EAKQC6AyG6A0EAKQC7AyG7A0EAKQC8AyG8A0EAKQC9AyG9A0EAKQC+AyG+A0EAKQC/AyG/A0EAKQDAAyHAA0EAKQDBAyHBA0EAKQDCAyHCA0EAKQDDAyHDA0EAKQDEAyHEA0EAKQDFAyHFA0EAKQDGAyHGA0EAKQDHAyHHA0EAKQDIAyHIA0EAKQDJAyHJA0EAKQDKAyHKA0EAKQDLAyHLA0EAKQDMAyHMA0EAKQDNAyHNA0EAKQDOAyHOA0EAKQDPAyHPA0EAKQDQAyHQA0EAKQDRAyHRA0EAKQDSAyHSA0EAKQDTAyHTA0EAKQDUAyHUA0EAKQDVAyHVA0EAKQDWAyHWA0EAKQDXAyHXA0EAKQDYAyHYA0EAKQDZAyHZA0EAKQDaAyHaA0EAKQDbAyHbA0EAKQDcAyHcA0EAKQDdAyHdA0EAKQDeAyHeA0EAKQDfAyHfA0EAKQDgAyHgA0EAKQDhAyHhA0EAKQDiAyHiA0EAKQDjAyHjA0EAKQDkAyHkA0EAKQDlAyHlA0EAKQDmAyHmA0EAKQDnAyHnA0EAKQDoAyHoA0EAKQDpAyHpA0EAKQDqAyHqA0EAKQDrAyHrA0EAKQDsAyHsA0EAKQDtAyHtA0EAKQDuAyHuA0EAKQDvAyHvA0EAKQDwAyHwA0EAKQDxAyHxA0EAKQDyAyHyA0EAKQDzAyHzA0EAKQD0AyH0A0EAKQD1AyH1A0EAKQD2AyH2A0EAKQD3AyH3A0EAKQD4AyH4A0EAKQD5AyH5A0EAKQD6AyH6A0EAKQD7AyH7A0EAKQD8AyH8A0EAKQD9AyH9A0EAKQD+AyH+A0EAKQD/AyH/A0EAKQCABCGABEEAKQCBBCGBBEEAKQCCBCGCBEEAKQCDBCGDBEEAKQCEBCGEBEEAKQCFBCGFBEEAKQCGBCGGBEEAKQCHBCGHBEEAKQCIBCGIBEEAKQCJBCGJBEEAKQCKBCGKBEEAKQCLBCGLBEEAKQCMBCGMBEEAKQCNBCGNBEEAKQCOBCGOBEEAKQCPBCGPBEEAKQCQBCGQBEEAKQCRBCGRBEEAKQCSBCGSBEEAKQCTBCGTBEEAKQCUBCGUBEEAKQCVBCGVBEEAKQCWBCGWBEEAKQCXBCGXBEEAKQCYBCGYBEEAKQCZBCGZBEEAKQCaBCGaBEEAKQCbBCGbBEEAKQCcBCGcBEEAKQCdBCGdBEEAKQCeBCGeBEEAKQCfBCGfBEEAKQCgBCGgBEEAKQChBCGhBEEAKQCiBCGiBEEAKQCjBCGjBEEAKQCkBCGkBEEAKQClBCGlBEEAKQCmBCGmBEEAKQCnBCGnBEEAKQCoBCGoBEEAKQCpBCGpBEEAKQCqBCGqBEEAKQCrBCGrBEEAKQCsBCGsBEEAKQCtBCGtBEEAKQCuBCGuBEEAKQCvBCGvBEEAKQCwBCGwBEEAKQCxBCGxBEEAKQCyBCGyBEEAKQCzBCGzBEEAKQC0BCG0BEEAKQC1BCG1BEEAKQC2BCG2BEEAKQC3BCG3BEEAKQC4BCG4BEEAKQC5BCG5BEEAKQC6BCG6BEEAKQC7BCG7BEEAKQC8BCG8BEEAKQC9BCG9BEEAKQC+BCG+BEEAKQC/BCG/BEEAKQDABCHABEEAKQDBBCHBBEEAKQDCBCHCBEEAKQDDBCHDBEEAKQDEBCHEBEEAKQDFBCHFBEEAKQDGBCHGBEEAKQDHBCHHBEEAKQDIBCHIBEEAKQDJBCHJBEEAKQDKBCHKBEEAKQDLBCHLBEEAKQDMBCHMBEEAKQDNBCHNBEEAKQDOBCHOBEEAKQDPBCHPBEEAKQDQBCHQBEEAKQDRBCHRBEEAKQDSBCHSBEEAKQDTBCHTBEEAKQDUBCHUBEEAKQDVBCHVBEEAKQDWBCHWBEEAKQDXBCHXBEEAKQDYBCHYBEEAKQDZBCHZBEEAKQDaBCHaBEEAKQDbBCHbBEEAKQDcBCHcBEEAKQDdBCHdBEEAKQDeBCHeBEEAKQDfBCHfBEEAKQDgBCHgBEEAKQDhBCHhBEEAKQDiBCHiBEEAKQDjBCHjBEEAKQDkBCHkBEEAKQDlBCHlBEEAKQDmBCHmBEEAKQDnBCHnBEEAKQDoBCHoBEEAKQDpBCHpBEEAKQDqBCHqBEEAKQDrBCHrBEEAKQDsBCHsBEEAKQDtBCHtBEEAKQDuBCHuBEEAKQDvBCHvBEEAKQDwBCHwBEEAKQDxBCHxBEEAKQDyBCHyBEEAKQDzBCHzBEEAKQD0BCH0BEEAKQD1BCH1BEEAKQD2BCH2BEEAKQD3BCH3BEEAKQD4BCH4BEEAKQD5BCH5BEEAKQD6BCH6BEEAKQD7BCH7BEEAKQD8BCH8BEEAKQD9BCH9BEEAKQD+BCH+BEEAKQD/BCH/BEEAKQCABSGABUEAKQCBBSGBBUEAKQCCBSGCBUEAKQCDBSGDBUEAKQCEBSGEBUEAKQCFBSGFBUEAKQCGBSGGBUEAKQCHBSGHBUEAKQCIBSGIBUEAKQCJBSGJBUEAKQCKBSGKBUEAKQCLBSGLBUEAKQCMBSGMBUEAKQCNBSGNBUEAKQCOBSGOBUEAKQCPBSGPBUEAKQCQBSGQBUEAKQCRBSGRBUEAKQCSBSGSBUEAKQCTBSGTBUEAKQCUBSGUBUEAKQCVBSGVBUEAKQCWBSGWBUEAKQCXBSGXBUEAKQCYBSGYBUEAKQCZBSGZBUEAKQCaBSGaBUEAKQCbBSGbBUEAKQCcBSGcBUEAKQCdBSGdBUEAKQCeBSGeBUEAKQCfBSGfBUEAKQCgBSGgBUEAKQChBSGhBUEAKQCiBSGiBUEAKQCjBSGjBUEAKQCkBSGkBUEAKQClBSGlBUEAKQCmBSGmBUEAKQCnBSGnBUEAKQCoBSGoBUEAKQCpBSGpBUEAKQCqBSGqBUEAKQCrBSGrBUEAKQCsBSGsBUEAKQCtBSGtBUEAKQCuBSGuBUEAKQCvBSGvBUEAKQCwBSGwBUEAKQCxBSGxBUEAKQCyBSGyBUEAKQCzBSGzBUEAKQC0BSG0BUEAKQC1BSG1BUEAKQC2BSG2BUEAKQC3BSG3BUEAKQC4BSG4BUEAKQC5BSG5BUEAKQC6BSG6BUEAKQC7BSG7BUEAKQC8BSG8BUEAKQC9BSG9BUEAKQC+BSG+BUEAKQC/BSG/BUEAKQDABSHABUEAKQDBBSHBBUEAKQDCBSHCBUEAKQDDBSHDBUEAKQDEBSHEBUEAKQDFBSHFBUEAKQDGBSHGBUEAKQDHBSHHBUEAKQDIBSHIBUEAKQDJBSHJBUEAKQDKBSHKBUEAKQDLBSHLBUEAKQDMBSHMBUEAKQDNBSHNBUEAKQDOBSHOBUEAKQDPBSHPBUEAKQDQBSHQBUEAKQDRBSHRBUEAKQDSBSHSBUEAKQDTBSHTBUEAKQDUBSHUBUEAKQDVBSHVBUEAKQDWBSHWBUEAKQDXBSHXBUEAKQDYBSHYBUEAKQDZBSHZBUEAKQDaBSHaBUEAKQDbBSHbBUEAKQDcBSHcBUEAKQDdBSHdBUEAKQDeBSHeBUEAKQDfBSHfBUEAKQDgBSHgBUEAKQDhBSHhBUEAKQDiBSHiBUEAKQDjBSHjBUEAKQDkBSHkBUEAKQDlBSHlBUEAKQDmBSHmBUEAKQDnBSHnBUEAKQDoBSHoBUEAKQDpBSHpBUEAKQDqBSHqBUEAKQDrBSHrBUEAKQDsBSHsBUEAKQDtBSHtBUEAKQDuBSHuBUEAKQDvBSHvBUEAKQDwBSHwBUEAKQDxBSHxBUEAKQDyBSHyBUEAKQDzBSHzBUEAKQD0BSH0BUEAKQD1BSH1BUEAKQD2BSH2BUEAKQD3BSH3BUEAKQD4BSH4BUEAKQD5BSH5BUEAKQD6BSH6BUEAKQD7BSH7BUEAKQD8BSH8BUEAKQD9BSH9BUEAKQD+BSH+BUEAKQD/BSH/BUEAKQCABiGABkEAKQCBBiGBBkEAKQCCBiGCBkEAKQCDBiGDBkEAKQCEBiGEBkEAKQCFBiGFBkEAKQCGBiGGBkEAKQCHBiGHBkEAKQCIBiGIBkEAKQCJBiGJBkEAKQCKBiGKBkEAKQCLBiGLBkEAKQCMBiGMBkEAKQCNBiGNBkEAKQCOBiGOBkEAKQCPBiGPBkEAKQCQBiGQBkEAKQCRBiGRBkEAKQCSBiGSBkEAKQCTBiGTBkEAKQCUBiGUBkEAKQCVBiGVBkEAKQCWBiGWBkEAKQCXBiGXBkEAKQCYBiGYBkEAKQCZBiGZBkEAKQCaBiGaBkEAKQCbBiGbBkEAKQCcBiGcBkEAKQCdBiGdBkEAKQCeBiGeBkEAKQCfBiGfBkEAKQCgBiGgBkEAKQChBiGhBkEAKQCiBiGiBkEAKQCjBiGjBkEAKQCkBiGkBkEAKQClBiGlBkEAKQCmBiGmBkEAKQCnBiGnBkEAKQCoBiGoBkEAKQCpBiGpBkEAKQCqBiGqBkEAKQCrBiGrBkEAKQCsBiGsBkEAKQCtBiGtBkEAKQCuBiGuBkEAKQCvBiGvBkEAKQCwBiGwBkEAKQCxBiGxBkEAKQCyBiGyBkEAKQCzBiGzBkEAKQC0BiG0BkEAKQC1BiG1BkEAKQC2BiG2BkEAKQC3BiG3BkEAKQC4BiG4BkEAKQC5BiG5BkEAKQC6BiG6BkEAKQC7BiG7BkEAKQC8BiG8BkEAKQC9BiG9BkEAKQC+BiG+BkEAKQC/BiG/BkEAKQDABiHABkEAKQDBBiHBBkEAKQDCBiHCBkEAKQDDBiHDBkEAKQDEBiHEBkEAKQDFBiHFBkEAKQDGBiHGBkEAKQDHBiHHBkEAKQDIBiHIBkEAKQDJBiHJBkEAKQDKBiHKBkEAKQDLBiHLBkEAKQDMBiHMBkEAKQDNBiHNBkEAKQDOBiHOBkEAKQDPBiHPBkEAKQDQBiHQBkEAKQDRBiHRBkEAKQDSBiHSBkEAKQDTBiHTBkEAKQDUBiHUBkEAKQDVBiHVBkEAKQDWBiHWBkEAKQDXBiHXBkEAKQDYBiHYBkEAKQDZBiHZBkEAKQDaBiHaBkEAKQDbBiHbBkEAKQDcBiHcBkEAKQDdBiHdBkEAKQDeBiHeBkEAKQDfBiHfBkEAKQDgBiHgBkEAKQDhBiHhBkEAKQDiBiHiBkEAKQDjBiHjBkEAKQDkBiHkBkEAKQDlBiHlBkEAKQDmBiHmBkEAKQDnBiHnBkEAKQDoBiHoBkEAKQDpBiHpBkEAKQDqBiHqBkEAKQDrBiHrBkEAKQDsBiHsBkEAKQDtBiHtBkEAKQDuBiHuBkEAKQDvBiHvBkEAKQDwBiHwBkEAKQDxBiHxBkEAKQDyBiHyBkEAKQDzBiHzBkEAKQD0BiH0BkEAKQD1BiH1BkEAKQD2BiH2BkEAKQD3BiH3BkEAKQD4BiH4BkEAKQD5BiH5BkEAKQD6BiH6BkEAKQD7BiH7BkEAKQD8BiH8BkEAKQD9BiH9BkEAKQD+BiH+BkEAKQD/BiH/BkEAKQCAByGAB0EAKQCBByGBB0EAKQCCByGCB0EAKQCDByGDB0EAKQCEByGEB0EAKQCFByGFB0EAKQCGByGGB0EAKQCHByGHB0EAKQCIByGIB0EAKQCJByGJB0EAKQCKByGKB0EAKQCLByGLB0EAKQCMByGMB0EAKQCNByGNB0EAKQCOByGOB0EAKQCPByGPB0EAKQCQByGQB0EAKQCRByGRB0EAKQCSByGSB0EAKQCTByGTB0EAKQCUByGUB0EAKQCVByGVB0EAKQCWByGWB0EAKQCXByGXB0EAKQCYByGYB0EAKQCZByGZB0EAKQCaByGaB0EAKQCbByGbB0EAKQCcByGcB0EAKQCdByGdB0EAKQCeByGeB0EAKQCfByGfB0EAKQCgByGgB0EAKQChByGhB0EAKQCiByGiB0EAKQCjByGjB0EAKQCkByGkB0EAKQClByGlB0EAKQCmByGmB0EAKQCnByGnB0EAKQCoByGoB0EAKQCpByGpB0EAKQCqByGqB0EAKQCrByGrB0EAKQCsByGsB0EAKQCtByGtB0EAKQCuByGuB0EAKQCvByGvB0EAKQCwByGwB0EAKQCxByGxB0EAKQCyByGyB0EAKQCzByGzB0EAKQC0ByG0B0EAKQC1ByG1B0EAKQC2ByG2B0EAKQC3ByG3B0EAKQC4ByG4B0EAKQC5ByG5B0EAKQC6ByG6B0EAKQC7ByG7B0EAKQC8ByG8B0EAKQC9ByG9B0EAKQC+ByG+B0EAKQC/ByG/B0EAKQDAByHAB0EAKQDBByHBB0EAKQDCByHCB0EAKQDDByHDB0EAKQDEByHEB0EAKQDFByHFB0EAKQDGByHGB0EAKQDHByHHB0EAKQDIByHIB0EAKQDJByHJB0EAKQDKByHKB0EAKQDLByHLB0EAKQDMByHMB0EAKQDNByHNB0EAKQDOByHOB0EAKQDPByHPB0EAKQDQByHQB0EAKQDRByHRB0EAKQDSByHSB0EAKQDTByHTB0EAKQDUByHUB0EAKQDVByHVB0EAKQDWByHWB0EAKQDXByHXB0EAKQDYByHYB0EAKQDZByHZB0EAKQDaByHaB0EAKQDbByHbB0EAKQDcByHcB0EAKQDdByHdB0EAKQDeByHeB0EAKQDfByHfB0EAKQDgByHgB0EAKQDhByHhB0EAKQDiByHiB0EAKQDjByHjB0EAKQDkByHkB0EAKQDlByHlB0EAKQDmByHmB0EAKQDnByHnB0EAKQDoByHoB0EAKQDpByHpB0EAKQDqByHqB0EAKQDrByHrB0EAKQDsByHsB0EAKQDtByHtB0EAKQDuByHuB0EAKQDvByHvB0EAKQDwByHwB0EAKQDxByHxB0EAKQDyByHyB0EAKQDzByHzB0EAKQD0ByH0B0EAKQD1ByH1B0EAKQD2ByH2B0EAKQD3ByH3B0EAKQD4ByH4B0EAKQD5ByH5B0EAKQD6ByH6B0EAKQD7ByH7B0EAKQD8ByH8B0EAKQD9ByH9B0EAKQD+ByH+B0EAKQD/ByH/B0EAKQCACCGACEEAKQCBCCGBCEEAKQCCCCGCCEEAKQCDCCGDCEEAKQCECCGECEEAKQCFCCGFCEEAKQCGCCGGCEEAKQCHCCGHCEEAKQCICCGICEEAKQCJCCGJCEEAKQCKCCGKCEEAKQCLCCGLCEEAKQCMCCGMCEEAKQCNCCGNCEEAKQCOCCGOCEEAKQCPCCGPCEEAKQCQCCGQCEEAKQCRCCGRCEEAKQCSCCGSCEEAKQCTCCGTCEEAKQCUCCGUCEEAKQCVCCGVCEEAKQCWCCGWCEEAKQCXCCGXCEEAKQCYCCGYCEEAKQCZCCGZCEEAKQCaCCGaCEEAKQCbCCGbCEEAKQCcCCGcCEEAKQCdCCGdCEEAKQCeCCGeCEEAKQCfCCGfCEEAIAA3AABBACABNwABQQAgAjcAAkEAIAM3AANBACAENwAEQQAgBTcABUEAIAY3AAZBACAHNwAHQQAgCDcACEEAIAk3AAlBACAKNwAKQQAgCzcAC0EAIAw3AAxBACANNwANQQAgDjcADkEAIA83AA9BACAQNwAQQQAgETcAEUEAIBI3ABJBACATNwATQQAgFDcAFEEAIBU3ABVBACAWNwAWQQAgFzcAF0EAIBg3ABhBACAZNwAZQQAgGjcAGkEAIBs3ABtBACAcNwAcQQAgHTcAHUEAIB43AB5BACAfNwAfQQAgIDcAIEEAICE3ACFBACAiNwAiQQAgIzcAI0EAICQ3ACRBACAlNwAlQQAgJjcAJkEAICc3ACdBACAoNwAoQQAgKTcAKUEAICo3ACpBACArNwArQQAgLDcALEEAIC03AC1BACAuNwAuQQAgLzcAL0EAIDA3ADBBACAxNwAxQQAgMjcAMkEAIDM3ADNBACA0NwA0QQAgNTcANUEAIDY3ADZBACA3NwA3QQAgODcAOEEAIDk3ADlBACA6NwA6QQAgOzcAO0EAIDw3ADxBACA9NwA9QQAgPjcAPkEAID83AD9BACBANwBAQQAgQTcAQUEAIEI3AEJBACBDNwBDQQAgRDcAREEAIEU3AEVBACBGNwBGQQAgRzcAR0EAIEg3AEhBACBJNwBJQQAgSjcASkEAIEs3AEtBACBMNwBMQQAgTTcATUEAIE43AE5BACBPNwBPQQAgUDcAUEEAIFE3AFFBACBSNwBSQQAgUzcAU0EAIFQ3AFRBACBVNwBVQQAgVjcAVkEAIFc3AFdBACBYNwBYQQAgWTcAWUEAIFo3AFpBACBbNwBbQQAgXDcAXEEAIF03AF1BACBeNwBeQQAgXzcAX0EAIGA3AGBBACBhNwBhQQAgYjcAYkEAIGM3AGNBACBkNwBkQQAgZTcAZUEAIGY3AGZBACBnNwBnQQAgaDcAaEEAIGk3AGlBACBqNwBqQQAgazcAa0EAIGw3AGxBACBtNwBtQQAgbjcAbkEAIG83AG9BACBwNwBwQQAgcTcAcUEAIHI3AHJBACBzNwBzQQAgdDcAdEEAIHU3AHVBACB2NwB2QQAgdzcAd0EAIHg3AHhBACB5NwB5QQAgejcAekEAIHs3AHtBACB8NwB8QQAgfTcAfUEAIH43AH5BACB/NwB/QQAggAE3AIABQQAggQE3AIEBQQAgggE3AIIBQQAggwE3AIMBQQAghAE3AIQBQQAghQE3AIUBQQAghgE3AIYBQQAghwE3AIcBQQAgiAE3AIgBQQAgiQE3AIkBQQAgigE3AIoBQQAgiwE3AIsBQQAgjAE3AIwBQQAgjQE3AI0BQQAgjgE3AI4BQQAgjwE3AI8BQQAgkAE3AJABQQAgkQE3AJEBQQAgkgE3AJIBQQAgkwE3AJMBQQAglAE3AJQBQQAglQE3AJUBQQAglgE3AJYBQQAglwE3AJcBQQAgmAE3AJgBQQAgmQE3AJkBQQAgmgE3AJoBQQAgmwE3AJsBQQAgnAE3AJwBQQAgnQE3AJ0BQQAgngE3AJ4BQQAgnwE3AJ8BQQAgoAE3AKABQQAgoQE3AKEBQQAgogE3AKIBQQAgowE3AKMBQQAgpAE3AKQBQQAgpQE3AKUBQQAgpgE3AKYBQQAgpwE3AKcBQQAgqAE3AKgBQQAgqQE3AKkBQQAgqgE3AKoBQQAgqwE3AKsBQQAgrAE3AKwBQQAgrQE3AK0BQQAgrgE3AK4BQQAgrwE3AK8BQQAgsAE3ALABQQAgsQE3ALEBQQAgsgE3ALIBQQAgswE3ALMBQQAgtAE3ALQBQQAgtQE3ALUBQQAgtgE3ALYBQQAgtwE3ALcBQQAguAE3ALgBQQAguQE3ALkBQQAgugE3ALoBQQAguwE3ALsBQQAgvAE3ALwBQQAgvQE3AL0BQQAgvgE3AL4BQQAgvwE3AL8BQQAgwAE3AMABQQAgwQE3AMEBQQAgwgE3AMIBQQAgwwE3AMMBQQAgxAE3AMQBQQAgxQE3AMUBQQAgxgE3AMYBQQAgxwE3AMcBQQAgyAE3AMgBQQAgyQE3AMkBQQAgygE3AMoBQQAgywE3AMsBQQAgzAE3AMwBQQAgzQE3AM0BQQAgzgE3AM4BQQAgzwE3AM8BQQAg0AE3ANABQQAg0QE3ANEBQQAg0gE3ANIBQQAg0wE3ANMBQQAg1AE3ANQBQQAg1QE3ANUBQQAg1gE3ANYBQQAg1wE3ANcBQQAg2AE3ANgBQQAg2QE3ANkBQQAg2gE3ANoBQQAg2wE3ANsBQQAg3AE3ANwBQQAg3QE3AN0BQQAg3gE3AN4BQQAg3wE3AN8BQQAg4AE3AOABQQAg4QE3AOEBQQAg4gE3AOIBQQAg4wE3AOMBQQAg5AE3AOQBQQAg5QE3AOUBQQAg5gE3AOYBQQAg5wE3AOcBQQAg6AE3AOgBQQAg6QE3AOkBQQAg6gE3AOoBQQAg6wE3AOsBQQAg7AE3AOwBQQAg7QE3AO0BQQAg7gE3AO4BQQAg7wE3AO8BQQAg8AE3APABQQAg8QE3APEBQQAg8gE3APIBQQAg8wE3APMBQQAg9AE3APQBQQAg9QE3APUBQQAg9gE3APYBQQAg9wE3APcBQQAg+AE3APgBQQAg+QE3APkBQQAg+gE3APoBQQAg+wE3APsBQQAg/AE3APwBQQAg/QE3AP0BQQAg/gE3AP4BQQAg/wE3AP8BQQAggAI3AIACQQAggQI3AIECQQAgggI3AIICQQAggwI3AIMCQQAghAI3AIQCQQAghQI3AIUCQQAghgI3AIYCQQAghwI3AIcCQQAgiAI3AIgCQQAgiQI3AIkCQQAgigI3AIoCQQAgiwI3AIsCQQAgjAI3AIwCQQAgjQI3AI0CQQAgjgI3AI4CQQAgjwI3AI8CQQAgkAI3AJACQQAgkQI3AJECQQAgkgI3AJICQQAgkwI3AJMCQQAglAI3AJQCQQAglQI3AJUCQQAglgI3AJYCQQAglwI3AJcCQQAgmAI3AJgCQQAgmQI3AJkCQQAgmgI3AJoCQQAgmwI3AJsCQQAgnAI3AJwCQQAgnQI3AJ0CQQAgngI3AJ4CQQAgnwI3AJ8CQQAgoAI3AKACQQAgoQI3AKECQQAgogI3AKICQQAgowI3AKMCQQAgpAI3AKQCQQAgpQI3AKUCQQAgpgI3AKYCQQAgpwI3AKcCQQAgqAI3AKgCQQAgqQI3AKkCQQAgqgI3AKoCQQAgqwI3AKsCQQAgrAI3AKwCQQAgrQI3AK0CQQAgrgI3AK4CQQAgrwI3AK8CQQAgsAI3ALACQQAgsQI3ALECQQAgsgI3ALICQQAgswI3ALMCQQAgtAI3ALQCQQAgtQI3ALUCQQAgtgI3ALYCQQAgtwI3ALcCQQAguAI3ALgCQQAguQI3ALkCQQAgugI3ALoCQQAguwI3ALsCQQAgvAI3ALwCQQAgvQI3AL0CQQAgvgI3AL4CQQAgvwI3AL8CQQAgwAI3AMACQQAgwQI3AMECQQAgwgI3AMICQQAgwwI3AMMCQQAgxAI3AMQCQQAgxQI3AMUCQQAgxgI3AMYCQQAgxwI3AMcCQQAgyAI3AMgCQQAgyQI3AMkCQQAgygI3AMoCQQAgywI3AMsCQQAgzAI3AMwCQQAgzQI3AM0CQQAgzgI3AM4CQQAgzwI3AM8CQQAg0AI3ANACQQAg0QI3ANECQQAg0gI3ANICQQAg0wI3ANMCQQAg1AI3ANQCQQAg1QI3ANUCQQAg1gI3ANYCQQAg1wI3ANcCQQAg2AI3ANgCQQAg2QI3ANkCQQAg2gI3ANoCQQAg2wI3ANsCQQAg3AI3ANwCQQAg3QI3AN0CQQAg3gI3AN4CQQAg3wI3AN8CQQAg4AI3AOACQQAg4QI3AOECQQAg4gI3AOICQQAg4wI3AOMCQQAg5AI3AOQCQQAg5QI3AOUCQQAg5gI3AOYCQQAg5wI3AOcCQQAg6AI3AOgCQQAg6QI3AOkCQQAg6gI3AOoCQQAg6wI3AOsCQQAg7AI3AOwCQQAg7QI3AO0CQQAg7gI3AO4CQQAg7wI3AO8CQQAg8AI3APACQQAg8QI3APECQQAg8gI3APICQQAg8wI3APMCQQAg9AI3APQCQQAg9QI3APUCQQAg9gI3APYCQQAg9wI3APcCQQAg+AI3APgCQQAg+QI3APkCQQAg+gI3APoCQQAg+wI3APsCQQAg/AI3APwCQQAg/QI3AP0CQQAg/gI3AP4CQQAg/wI3AP8CQQAggAM3AIADQQAggQM3AIEDQQAgggM3AIIDQQAggwM3AIMDQQAghAM3AIQDQQAghQM3AIUDQQAghgM3AIYDQQAghwM3AIcDQQAgiAM3AIgDQQAgiQM3AIkDQQAgigM3AIoDQQAgiwM3AIsDQQAgjAM3AIwDQQAgjQM3AI0DQQAgjgM3AI4DQQAgjwM3AI8DQQAgkAM3AJADQQAgkQM3AJEDQQAgkgM3AJIDQQAgkwM3AJMDQQAglAM3AJQDQQAglQM3AJUDQQAglgM3AJYDQQAglwM3AJcDQQAgmAM3AJgDQQAgmQM3AJkDQQAgmgM3AJoDQQAgmwM3AJsDQQAgnAM3AJwDQQAgnQM3AJ0DQQAgngM3AJ4DQQAgnwM3AJ8DQQAgoAM3AKADQQAgoQM3AKEDQQAgogM3AKIDQQAgowM3AKMDQQAgpAM3AKQDQQAgpQM3AKUDQQAgpgM3AKYDQQAgpwM3AKcDQQAgqAM3AKgDQQAgqQM3AKkDQQAgqgM3AKoDQQAgqwM3AKsDQQAgrAM3AKwDQQAgrQM3AK0DQQAgrgM3AK4DQQAgrwM3AK8DQQAgsAM3ALADQQAgsQM3ALEDQQAgsgM3ALIDQQAgswM3ALMDQQAgtAM3ALQDQQAgtQM3ALUDQQAgtgM3ALYDQQAgtwM3ALcDQQAguAM3ALgDQQAguQM3ALkDQQAgugM3ALoDQQAguwM3ALsDQQAgvAM3ALwDQQAgvQM3AL0DQQAgvgM3AL4DQQAgvwM3AL8DQQAgwAM3AMADQQAgwQM3AMEDQQAgwgM3AMIDQQAgwwM3AMMDQQAgxAM3AMQDQQAgxQM3AMUDQQAgxgM3AMYDQQAgxwM3AMcDQQAgyAM3AMgDQQAgyQM3AMkDQQAgygM3AMoDQQAgywM3AMsDQQAgzAM3AMwDQQAgzQM3AM0DQQAgzgM3AM4DQQAgzwM3AM8DQQAg0AM3ANADQQAg0QM3ANEDQQAg0gM3ANIDQQAg0wM3ANMDQQAg1AM3ANQDQQAg1QM3ANUDQQAg1gM3ANYDQQAg1wM3ANcDQQAg2AM3ANgDQQAg2QM3ANkDQQAg2gM3ANoDQQAg2wM3ANsDQQAg3AM3ANwDQQAg3QM3AN0DQQAg3gM3AN4DQQAg3wM3AN8DQQAg4AM3AOADQQAg4QM3AOEDQQAg4gM3AOIDQQAg4wM3AOMDQQAg5AM3AOQDQQAg5QM3AOUDQQAg5gM3AOYDQQAg5wM3AOcDQQAg6AM3AOgDQQAg6QM3AOkDQQAg6gM3AOoDQQAg6wM3AOsDQQAg7AM3AOwDQQAg7QM3AO0DQQAg7gM3AO4DQQAg7wM3AO8DQQAg8AM3APADQQAg8QM3APEDQQAg8gM3APIDQQAg8wM3APMDQQAg9AM3APQDQQAg9QM3APUDQQAg9gM3APYDQQAg9wM3APcDQQAg+AM3APgDQQAg+QM3APkDQQAg+gM3APoDQQAg+wM3APsDQQAg/AM3APwDQQAg/QM3AP0DQQAg/gM3AP4DQQAg/wM3AP8DQQAggAQ3AIAEQQAggQQ3AIEEQQAgggQ3AIIEQQAggwQ3AIMEQQAghAQ3AIQEQQAghQQ3AIUEQQAghgQ3AIYEQQAghwQ3AIcEQQAgiAQ3AIgEQQAgiQQ3AIkEQQAgigQ3AIoEQQAgiwQ3AIsEQQAgjAQ3AIwEQQAgjQQ3AI0EQQAgjgQ3AI4EQQAgjwQ3AI8EQQAgkAQ3AJAEQQAgkQQ3AJEEQQAgkgQ3AJIEQQAgkwQ3AJMEQQAglAQ3AJQEQQAglQQ3AJUEQQAglgQ3AJYEQQAglwQ3AJcEQQAgmAQ3AJgEQQAgmQQ3AJkEQQAgmgQ3AJoEQQAgmwQ3AJsEQQAgnAQ3AJwEQQAgnQQ3AJ0EQQAgngQ3AJ4EQQAgnwQ3AJ8EQQAgoAQ3AKAEQQAgoQQ3AKEEQQAgogQ3AKIEQQAgowQ3AKMEQQAgpAQ3AKQEQQAgpQQ3AKUEQQAgpgQ3AKYEQQAgpwQ3AKcEQQAgqAQ3AKgEQQAgqQQ3AKkEQQAgqgQ3AKoEQQAgqwQ3AKsEQQAgrAQ3AKwEQQAgrQQ3AK0EQQAgrgQ3AK4EQQAgrwQ3AK8EQQAgsAQ3ALAEQQAgsQQ3ALEEQQAgsgQ3ALIEQQAgswQ3ALMEQQAgtAQ3ALQEQQAgtQQ3ALUEQQAgtgQ3ALYEQQAgtwQ3ALcEQQAguAQ3ALgEQQAguQQ3ALkEQQAgugQ3ALoEQQAguwQ3ALsEQQAgvAQ3ALwEQQAgvQQ3AL0EQQAgvgQ3AL4EQQAgvwQ3AL8EQQAgwAQ3AMAEQQAgwQQ3AMEEQQAgwgQ3AMIEQQAgwwQ3AMMEQQAgxAQ3AMQEQQAgxQQ3AMUEQQAgxgQ3AMYEQQAgxwQ3AMcEQQAgyAQ3AMgEQQAgyQQ3AMkEQQAgygQ3AMoEQQAgywQ3AMsEQQAgzAQ3AMwEQQAgzQQ3AM0EQQAgzgQ3AM4EQQAgzwQ3AM8EQQAg0AQ3ANAEQQAg0QQ3ANEEQQAg0gQ3ANIEQQAg0wQ3ANMEQQAg1AQ3ANQEQQAg1QQ3ANUEQQAg1gQ3ANYEQQAg1wQ3ANcEQQAg2AQ3ANgEQQAg2QQ3ANkEQQAg2gQ3ANoEQQAg2wQ3ANsEQQAg3AQ3ANwEQQAg3QQ3AN0EQQAg3gQ3AN4EQQAg3wQ3AN8EQQAg4AQ3AOAEQQAg4QQ3AOEEQQAg4gQ3AOIEQQAg4wQ3AOMEQQAg5AQ3AOQEQQAg5QQ3AOUEQQAg5gQ3AOYEQQAg5wQ3AOcEQQAg6AQ3AOgEQQAg6QQ3AOkEQQAg6gQ3AOoEQQAg6wQ3AOsEQQAg7AQ3AOwEQQAg7QQ3AO0EQQAg7gQ3AO4EQQAg7wQ3AO8EQQAg8AQ3APAEQQAg8QQ3APEEQQAg8gQ3APIEQQAg8wQ3APMEQQAg9AQ3APQEQQAg9QQ3APUEQQAg9gQ3APYEQQAg9wQ3APcEQQAg+AQ3APgEQQAg+QQ3APkEQQAg+gQ3APoEQQAg+wQ3APsEQQAg/AQ3APwEQQAg/QQ3AP0EQQAg/gQ3AP4EQQAg/wQ3AP8EQQAggAU3AIAFQQAggQU3AIEFQQAgggU3AIIFQQAggwU3AIMFQQAghAU3AIQFQQAghQU3AIUFQQAghgU3AIYFQQAghwU3AIcFQQAgiAU3AIgFQQAgiQU3AIkFQQAgigU3AIoFQQAgiwU3AIsFQQAgjAU3AIwFQQAgjQU3AI0FQQAgjgU3AI4FQQAgjwU3AI8FQQAgkAU3AJAFQQAgkQU3AJEFQQAgkgU3AJIFQQAgkwU3AJMFQQAglAU3AJQFQQAglQU3AJUFQQAglgU3AJYFQQAglwU3AJcFQQAgmAU3AJgFQQAgmQU3AJkFQQAgmgU3AJoFQQAgmwU3AJsFQQAgnAU3AJwFQQAgnQU3AJ0FQQAgngU3AJ4FQQAgnwU3AJ8FQQAgoAU3AKAFQQAgoQU3AKEFQQAgogU3AKIFQQAgowU3AKMFQQAgpAU3AKQFQQAgpQU3AKUFQQAgpgU3AKYFQQAgpwU3AKcFQQAgqAU3AKgFQQAgqQU3AKkFQQAgqgU3AKoFQQAgqwU3AKsFQQAgrAU3AKwFQQAgrQU3AK0FQQAgrgU3AK4FQQAgrwU3AK8FQQAgsAU3ALAFQQAgsQU3ALEFQQAgsgU3ALIFQQAgswU3ALMFQQAgtAU3ALQFQQAgtQU3ALUFQQAgtgU3ALYFQQAgtwU3ALcFQQAguAU3ALgFQQAguQU3ALkFQQAgugU3ALoFQQAguwU3ALsFQQAgvAU3ALwFQQAgvQU3AL0FQQAgvgU3AL4FQQAgvwU3AL8FQQAgwAU3AMAFQQAgwQU3AMEFQQAgwgU3AMIFQQAgwwU3AMMFQQAgxAU3AMQFQQAgxQU3AMUFQQAgxgU3AMYFQQAgxwU3AMcFQQAgyAU3AMgFQQAgyQU3AMkFQQAgygU3AMoFQQAgywU3AMsFQQAgzAU3AMwFQQAgzQU3AM0FQQAgzgU3AM4FQQAgzwU3AM8FQQAg0AU3ANAFQQAg0QU3ANEFQQAg0gU3ANIFQQAg0wU3ANMFQQAg1AU3ANQFQQAg1QU3ANUFQQAg1gU3ANYFQQAg1wU3ANcFQQAg2AU3ANgFQQAg2QU3ANkFQQAg2gU3ANoFQQAg2wU3ANsFQQAg3AU3ANwFQQAg3QU3AN0FQQAg3gU3AN4FQQAg3wU3AN8FQQAg4AU3AOAFQQAg4QU3AOEFQQAg4gU3AOIFQQAg4wU3AOMFQQAg5AU3AOQFQQAg5QU3AOUFQQAg5gU3AOYFQQAg5wU3AOcFQQAg6AU3AOgFQQAg6QU3AOkFQQAg6gU3AOoFQQAg6wU3AOsFQQAg7AU3AOwFQQAg7QU3AO0FQQAg7gU3AO4FQQAg7wU3AO8FQQAg8AU3APAFQQAg8QU3APEFQQAg8gU3APIFQQAg8wU3APMFQQAg9AU3APQFQQAg9QU3APUFQQAg9gU3APYFQQAg9wU3APcFQQAg+AU3APgFQQAg+QU3APkFQQAg+gU3APoFQQAg+wU3APsFQQAg/AU3APwFQQAg/QU3AP0FQQAg/gU3AP4FQQAg/wU3AP8FQQAggAY3AIAGQQAggQY3AIEGQQAgggY3AIIGQQAggwY3AIMGQQAghAY3AIQGQQAghQY3AIUGQQAghgY3AIYGQQAghwY3AIcGQQAgiAY3AIgGQQAgiQY3AIkGQQAgigY3AIoGQQAgiwY3AIsGQQAgjAY3AIwGQQAgjQY3AI0GQQAgjgY3AI4GQQAgjwY3AI8GQQAgkAY3AJAGQQAgkQY3AJEGQQAgkgY3AJIGQQAgkwY3AJMGQQAglAY3AJQGQQAglQY3AJUGQQAglgY3AJYGQQAglwY3AJcGQQAgmAY3AJgGQQAgmQY3AJkGQQAgmgY3AJoGQQAgmwY3AJsGQQAgnAY3AJwGQQAgnQY3AJ0GQQAgngY3AJ4GQQAgnwY3AJ8GQQAgoAY3AKAGQQAgoQY3AKEGQQAgogY3AKIGQQAgowY3AKMGQQAgpAY3AKQGQQAgpQY3AKUGQQAgpgY3AKYGQQAgpwY3AKcGQQAgqAY3AKgGQQAgqQY3AKkGQQAgqgY3AKoGQQAgqwY3AKsGQQAgrAY3AKwGQQAgrQY3AK0GQQAgrgY3AK4GQQAgrwY3AK8GQQAgsAY3ALAGQQAgsQY3ALEGQQAgsgY3ALIGQQAgswY3ALMGQQAgtAY3ALQGQQAgtQY3ALUGQQAgtgY3ALYGQQAgtwY3ALcGQQAguAY3ALgGQQAguQY3ALkGQQAgugY3ALoGQQAguwY3ALsGQQAgvAY3ALwGQQAgvQY3AL0GQQAgvgY3AL4GQQAgvwY3AL8GQQAgwAY3AMAGQQAgwQY3AMEGQQAgwgY3AMIGQQAgwwY3AMMGQQAgxAY3AMQGQQAgxQY3AMUGQQAgxgY3AMYGQQAgxwY3AMcGQQAgyAY3AMgGQQAgyQY3AMkGQQAgygY3AMoGQQAgywY3AMsGQQAgzAY3AMwGQQAgzQY3AM0GQQAgzgY3AM4GQQAgzwY3AM8GQQAg0AY3ANAGQQAg0QY3ANEGQQAg0gY3ANIGQQAg0wY3ANMGQQAg1AY3ANQGQQAg1QY3ANUGQQAg1gY3ANYGQQAg1wY3ANcGQQAg2AY3ANgGQQAg2QY3ANkGQQAg2gY3ANoGQQAg2wY3ANsGQQAg3AY3ANwGQQAg3QY3AN0GQQAg3gY3AN4GQQAg3wY3AN8GQQAg4AY3AOAGQQAg4QY3AOEGQQAg4gY3AOIGQQAg4wY3AOMGQQAg5AY3AOQGQQAg5QY3AOUGQQAg5gY3AOYGQQAg5wY3AOcGQQAg6AY3AOgGQQAg6QY3AOkGQQAg6gY3AOoGQQAg6wY3AOsGQQAg7AY3AOwGQQAg7QY3AO0GQQAg7gY3AO4GQQAg7wY3AO8GQQAg8AY3APAGQQAg8QY3APEGQQAg8gY3APIGQQAg8wY3APMGQQAg9AY3APQGQQAg9QY3APUGQQAg9gY3APYGQQAg9wY3APcGQQAg+AY3APgGQQAg+QY3APkGQQAg+gY3APoGQQAg+wY3APsGQQAg/AY3APwGQQAg/QY3AP0GQQAg/gY3AP4GQQAg/wY3AP8GQQAggAc3AIAHQQAggQc3AIEHQQAgggc3AIIHQQAggwc3AIMHQQAghAc3AIQHQQAghQc3AIUHQQAghgc3AIYHQQAghwc3AIcHQQAgiAc3AIgHQQAgiQc3AIkHQQAgigc3AIoHQQAgiwc3AIsHQQAgjAc3AIwHQQAgjQc3AI0HQQAgjgc3AI4HQQAgjwc3AI8HQQAgkAc3AJAHQQAgkQc3AJEHQQAgkgc3AJIHQQAgkwc3AJMHQQAglAc3AJQHQQAglQc3AJUHQQAglgc3AJYHQQAglwc3AJcHQQAgmAc3AJgHQQAgmQc3AJkHQQAgmgc3AJoHQQAgmwc3AJsHQQAgnAc3AJwHQQAgnQc3AJ0HQQAgngc3AJ4HQQAgnwc3AJ8HQQAgoAc3AKAHQQAgoQc3AKEHQQAgogc3AKIHQQAgowc3AKMHQQAgpAc3AKQHQQAgpQc3AKUHQQAgpgc3AKYHQQAgpwc3AKcHQQAgqAc3AKgHQQAgqQc3AKkHQQAgqgc3AKoHQQAgqwc3AKsHQQAgrAc3AKwHQQAgrQc3AK0HQQAgrgc3AK4HQQAgrwc3AK8HQQAgsAc3ALAHQQAgsQc3ALEHQQAgsgc3ALIHQQAgswc3ALMHQQAgtAc3ALQHQQAgtQc3ALUHQQAgtgc3ALYHQQAgtwc3ALcHQQAguAc3ALgHQQAguQc3ALkHQQAgugc3ALoHQQAguwc3ALsHQQAgvAc3ALwHQQAgvQc3AL0HQQAgvgc3AL4HQQAgvwc3AL8HQQAgwAc3AMAHQQAgwQc3AMEHQQAgwgc3AMIHQQAgwwc3AMMHQQAgxAc3AMQHQQAgxQc3AMUHQQAgxgc3AMYHQQAgxwc3AMcHQQAgyAc3AMgHQQAgyQc3AMkHQQAgygc3AMoHQQAgywc3AMsHQQAgzAc3AMwHQQAgzQc3AM0HQQAgzgc3AM4HQQAgzwc3AM8HQQAg0Ac3ANAHQQAg0Qc3ANEHQQAg0gc3ANIHQQAg0wc3ANMHQQAg1Ac3ANQHQQAg1Qc3ANUHQQAg1gc3ANYHQQAg1wc3ANcHQQAg2Ac3ANgHQQAg2Qc3ANkHQQAg2gc3ANoHQQAg2wc3ANsHQQAg3Ac3ANwHQQAg3Qc3AN0HQQAg3gc3AN4HQQAg3wc3AN8HQQAg4Ac3AOAHQQAg4Qc3AOEHQQAg4gc3AOIHQQAg4wc3AOMHQQAg5Ac3AOQHQQAg5Qc3AOUHQQAg5gc3AOYHQQAg5wc3AOcHQQAg6Ac3AOgHQQAg6Qc3AOkHQQAg6gc3AOoHQQAg6wc3AOsHQQAg7Ac3AOwHQQAg7Qc3AO0HQQAg7gc3AO4HQQAg7wc3AO8HQQAg8Ac3APAHQQAg8Qc3APEHQQAg8gc3APIHQQAg8wc3APMHQQAg9Ac3APQHQQAg9Qc3APUHQQAg9gc3APYHQQAg9wc3APcHQQAg+Ac3APgHQQAg+Qc3APkHQQAg+gc3APoHQQAg+wc3APsHQQAg/Ac3APwHQQAg/Qc3AP0HQQAg/gc3AP4HQQAg/wc3AP8HQQAggAg3AIAIQQAggQg3AIEIQQAgggg3AIIIQQAggwg3AIMIQQAghAg3AIQIQQAghQg3AIUIQQAghgg3AIYIQQAghwg3AIcIQQAgiAg3AIgIQQAgiQg3AIkIQQAgigg3AIoIQQAgiwg3AIsIQQAgjAg3AIwIQQAgjQg3AI0IQQAgjgg3AI4IQQAgjwg3AI8IQQAgkAg3AJAIQQAgkQg3AJEIQQAgkgg3AJIIQQAgkwg3AJMIQQAglAg3AJQIQQAglQg3AJUIQQAglgg3AJYIQQAglwg3AJcIQQAgmAg3AJgIQQAgmQg3AJkIQQAgmgg3AJoIQQAgmwg3AJsIQQAgnAg3AJwIQQAgnQg3AJ0IQQAgngg3AJ4IQQAgnwg3AJ8ICw== +valid valid stack.wast:1 AGFzbQEAAAABDQNgAX4BfmAAAX9gAAADCQgAAAAAAAECAQYGAX8BQQALB1cGCGZhYy1leHByAAAJZmFjLXN0YWNrAAENZmFjLXN0YWNrLXJhdwACCWZhYy1taXhlZAADDWZhYy1taXhlZC1yYXcABBBub3QtcXVpdGUtYS10cmVlAAcKjwIILwECfiAAIQFCASECAkADQCABQgBRBEAMAgUgASACfiECIAFCAX0hAQsMAAsLIAILLwECfiAAIQFCASECAkADQCABQgBRBEAMAgUgASACfiECIAFCAX0hAQsMAAsLIAILLwECfiAAIQFCASECAkADQCABQgBRBEAMAgUgASACfiECIAFCAX0hAQsMAAsLIAILLwECfiAAIQFCASECAkADQCABQgBRBEAMAgUgASACfiECIAFCAX0hAQsMAAsLIAILLwECfiAAIQFCASECAkADQCABQgBRBEAMAgUgASACfiECIAFCAX0hAQsMAAsLIAILDQEBf0EBIwBqJAAjAAsFABAFGgsJABAFEAUQBmoL +valid valid stack.wast:156 AGFzbQEAAAABDANgAABgAX8AYAABfwMCAQAEBAFwAAEKtAMBsQMAAkBBABEAAAsDQEEAEQAAC0EABEBBABEAAAtBAARAQQARAAAFQQARAAALAkBBABEAAAsDQEEAEQAAC0EABEBBABEAAAtBAARAQQARAAAFQQARAAALAkBBAEEAEQEACwNAQQBBABEBAAtBAARAQQBBABEBAAtBAARAQQBBABEBAAVBAEEAEQEACwJ/QQARAgALGgN/QQARAgALGkEABH9BABECAAVBABECAAsaAkBBABEAAAsDQEEAEQAAC0EABEBBABEAAAtBAARAQQARAAAFQQARAAALAkBBABEAAAsDQEEAEQAAC0EABEBBABEAAAtBAARAQQARAAAFQQARAAALAkBBABEAAAsDQEEAEQAAC0EABEBBABEAAAtBAARAQQARAAAFQQARAAALAkBBAEEAEQEACwNAQQBBABEBAAtBAARAQQBBABEBAAtBAARAQQBBABEBAAVBAEEAEQEACwJ/QQARAgALGgN/QQARAgALGkEABH9BABECAAVBABECAAsaAkBBABEAAAsDQEEAEQAAC0EABEBBABEAAAtBAARAQQARAAAFQQARAAALQQARAAAL +invalid invalid start.wast:2 AGFzbQEAAAABBAFgAAADAgEACAEBCgQBAgAL +invalid invalid start.wast:7 AGFzbQEAAAABBQFgAAF/AwIBAAgBAAoHAQUAQQAPCw== +invalid invalid start.wast:14 AGFzbQEAAAABBQFgAX8AAwIBAAgBAAoEAQIACw== +valid valid start.wast:21 AGFzbQEAAAABCAJgAABgAAF/AwQDAAEABQQBAQEBBw0CA2luYwAAA2dldAABCAECCiMDDwBBAEEALQAAQQFqOgAACwgAQQAtAAAPCwgAEAAQABAACwsHAQBBAAsBQQ== +valid valid start.wast:80 AGFzbQEAAAABCAJgAX8AYAAAAhYBCHNwZWN0ZXN0CXByaW50X2kzMgAAAwIBAQgBAQoIAQYAQQEQAAs= +valid valid start.wast:86 AGFzbQEAAAABCAJgAX8AYAAAAhYBCHNwZWN0ZXN0CXByaW50X2kzMgAAAwIBAQgBAQoIAQYAQQIQAAs= +valid valid start.wast:92 AGFzbQEAAAABBAFgAAACEgEIc3BlY3Rlc3QFcHJpbnQAAAgBAA== +valid valid start.wast:98 AGFzbQEAAAABBAFgAAADAgEACAEACgUBAwAACw== +valid valid store.wast:3 AGFzbQEAAAABBAFgAAADCgkAAAAAAAAAAAAFAwEAAQeXAQkOYXMtYmxvY2stdmFsdWUAAA1hcy1sb29wLXZhbHVlAAELYXMtYnItdmFsdWUAAg5hcy1icl9pZi12YWx1ZQADE2FzLWJyX2lmLXZhbHVlLWNvbmQABBFhcy1icl90YWJsZS12YWx1ZQAFD2FzLXJldHVybi12YWx1ZQAGCmFzLWlmLXRoZW4ABwphcy1pZi1lbHNlAAgKiAEJDAACQEEAQQE2AgALCwwAA0BBAEEBNgIACwsOAAJAQQBBATYCAAwACwsQAAJAQQBBATYCAEEBDQALCxAAAkBBBkEAQQE2AgANAAsLEQACQEEAQQE2AgBBAQ4AAAsLCgBBAEEBNgIADwsOAEEBBEBBAEEBNgIACwsPAEEABEAFQQBBATYCAAsL +invalid invalid store.wast:112 AGFzbQEAAAABBgFgAX8BfwMCAQAFAwEAAQoLAQkAQQBBATYCAAs= +invalid invalid store.wast:116 AGFzbQEAAAABBgFgAX4BfgMCAQAFAwEAAQoLAQkAQQBCATcDAAs= +invalid invalid store.wast:120 AGFzbQEAAAABBgFgAX0BfQMCAQAFAwEAAQoOAQwAQQBDAACAPzgCAAs= +invalid invalid store.wast:124 AGFzbQEAAAABBgFgAXwBfAMCAQAFAwEAAQoSARAAQQBEAAAAAAAA8D85AwAL +invalid invalid store.wast:128 AGFzbQEAAAABBgFgAX8BfwMCAQAFAwEAAQoLAQkAQQBBAToAAAs= +invalid invalid store.wast:132 AGFzbQEAAAABBgFgAX8BfwMCAQAFAwEAAQoLAQkAQQBBATsBAAs= +invalid invalid store.wast:136 AGFzbQEAAAABBgFgAX4BfgMCAQAFAwEAAQoLAQkAQQBCATwAAAs= +invalid invalid store.wast:140 AGFzbQEAAAABBgFgAX4BfgMCAQAFAwEAAQoLAQkAQQBCAT0BAAs= +invalid invalid store.wast:144 AGFzbQEAAAABBgFgAX4BfgMCAQAFAwEAAQoLAQkAQQBCAT4CAAs= +invalid invalid store.wast:150 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKBwEFADYCAAs= +invalid invalid store.wast:159 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCQEHAEEANgIACw== +invalid invalid store.wast:168 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAQQACQDYCAAsL +invalid invalid store.wast:178 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAAkBBADYCAAsL +invalid invalid store.wast:188 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAQQADQDYCAAsL +invalid invalid store.wast:198 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAA0BBADYCAAsL +invalid invalid store.wast:208 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAQQAEQDYCAAsL +invalid invalid store.wast:218 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEABEBBADYCAAsL +invalid invalid store.wast:228 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAEEAQQAEf0EABTYCAAsL +invalid invalid store.wast:238 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAEEABH9BAAVBADYCAAsL +invalid invalid store.wast:248 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEAEOAEEAQQACQDYCAAwACws= +invalid invalid store.wast:258 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEAEOAEEAAkBBADYCAAwACws= +invalid invalid store.wast:268 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEgEQAEEAQQACQDYCAEEBDQALCw== +invalid invalid store.wast:278 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEgEQAEEAAkBBADYCAEEBDQALCw== +invalid invalid store.wast:288 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAEEAQQACQDYCAA4AAAsL +invalid invalid store.wast:298 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAEEAAkBBADYCAA4AAAsL +invalid invalid store.wast:308 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCAEGADYCAA8L +invalid invalid store.wast:317 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCgEIAEEANgIADws= +invalid invalid store.wast:326 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDAEKADYCAEEBQQIbCw== +invalid invalid store.wast:335 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEANgIAQQFBAhsL +invalid invalid store.wast:344 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQMBAAEKDgIHADYCABABCwQAIAAL +invalid invalid store.wast:354 AGFzbQEAAAABCQJgAABgAX8BfwMDAgABBQMBAAEKEAIJAEEANgIAEAELBAAgAAs= +invalid invalid store.wast:364 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQUDAQABCQcBAEEACwEAChQCBAAgAAsNAAJ/NgIAQQARAAALCw== +invalid invalid store.wast:380 AGFzbQEAAAABCQJgAX8Bf2AAAAMDAgABBAUBcAEBAQUDAQABCQcBAEEACwEAChYCBAAgAAsPAAJ/QQA2AgBBABEAAAsL +invalid invalid store.wast:399 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEMAAAAAQQA2AgAL +invalid invalid store.wast:400 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEMAAAAAQQA6AAAL +invalid invalid store.wast:401 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEMAAAAAQQA7AQAL +invalid invalid store.wast:402 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEMAAAAAQQA3AwAL +invalid invalid store.wast:403 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEMAAAAAQgA8AAAL +invalid invalid store.wast:404 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEMAAAAAQgA9AQAL +invalid invalid store.wast:405 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEMAAAAAQgA+AgAL +invalid invalid store.wast:406 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEQEPAEMAAAAAQwAAAAA4AgAL +invalid invalid store.wast:407 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKFQETAEMAAAAARAAAAAAAAAAAOQMACw== +invalid invalid store.wast:409 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAQwAAAAA2AgAL +invalid invalid store.wast:410 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAQwAAAAA6AAAL +invalid invalid store.wast:411 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAQwAAAAA7AQAL +invalid invalid store.wast:412 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKDgEMAEEAQwAAAAA3AwAL +invalid invalid store.wast:413 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEgEQAEEARAAAAAAAAAAAPAAACw== +invalid invalid store.wast:414 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEgEQAEEARAAAAAAAAAAAPQEACw== +invalid invalid store.wast:415 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKEgEQAEEARAAAAAAAAAAAPgIACw== +invalid invalid store.wast:416 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCwEJAEEAQQA4AgAL +invalid invalid store.wast:417 AGFzbQEAAAABBAFgAAADAgEABQMBAAEKCwEJAEEAQgA5AwAL +valid valid switch.wast:1 AGFzbQEAAAABDwNgAX8Bf2ABfgF+YAABfwMFBAABAAIHHgQEc3RtdAAABGV4cHIAAQNhcmcAAgZjb3JuZXIAAwreAQRXAQF/QeQAIQECQAJAAkACQAJAAkACQAJAAkACQCAADggAAQIDBAUGCAcLIAAPCwELC0EAIABrIQEMBQsMBAtB5QAhAQwDC0HlACEBC0HmACEBCwsgAQ8LTAEBfkLkACEBAn4CQAJAAkACQAJAAkACQAJAAkAgAKcOCAABAgMGBQQIBwsgAA8LAQsLQgAgAH0MBQtC5QAhAQsLCyABDAELQnsLDwsqAAJ/QQoCf0HkAAJ/QegHAn9BAiAAbEEDIABxDgMBAgMAC2oLagtqCw8LDAACQEEADgAAC0EBCw== +invalid invalid switch.wast:150 AGFzbQEAAAABBAFgAAADAgEACgkBBwBBAA4AAws= +invalid invalid table-sub.wast:2 AGFzbQEAAAABBAFgAAADAgEABAcCcAAKbwAKCg4BDABBAEEBQQL8DgABCw== +invalid invalid table-sub.wast:13 AGFzbQEAAAABBAFgAAADAgEABAQBcAAKCQQBBW8ACg4BDABBAEEBQQL8DAAACw== +valid valid table.wast:3 AGFzbQEAAAAEBAFwAAA= +valid valid table.wast:4 AGFzbQEAAAAEBAFwAAE= +valid valid table.wast:5 AGFzbQEAAAAEBQFwAQAA +valid valid table.wast:6 AGFzbQEAAAAEBQFwAQAB +valid valid table.wast:7 AGFzbQEAAAAEBgFwAQGAAg== +valid valid table.wast:8 AGFzbQEAAAAEBwFwAQCAgAQ= +valid valid table.wast:9 AGFzbQEAAAAECQFwAQD/////Dw== +valid valid table.wast:11 AGFzbQEAAAAEBwJwAABwAAA= +valid valid table.wast:12 AGFzbQEAAAACFAEIc3BlY3Rlc3QFdGFibGUBcAAABAQBcAAA +invalid invalid table.wast:19 AGFzbQEAAAAEBQFwAQEA +invalid invalid table.wast:23 AGFzbQEAAAAECQFwAf////8PAA== +valid valid table_copy.wast:6 AGFzbQEAAAABBQFgAAF/AwYFAAAAAAAHHwUDZWYwAAADZWYxAAEDZWYyAAIDZWYzAAMDZWY0AAQKGgUEAEEACwQAQQELBABBAgsEAEEDCwQAQQQL +valid valid table_copy.wast:15 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCi4IBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwMAAQsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:107 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQ1BAkED/A4AAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:199 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQRlBD0EC/A4AAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:291 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQ1BGUED/A4AAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:383 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQRRBFkEE/A4AAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:475 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQRlBAUED/A4AAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:567 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQpBDEEH/A4AAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:659 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQxBCkEH/A4AAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:751 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgIBQQMLAAQBAwEEAgFBCwsABQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQpBAEEU/A4BAAsHACAAEQAACwcAIAARAAEL +valid valid table_copy.wast:843 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCi4IBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwMAAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:935 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQ1BAkED/A4BAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1027 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQRlBD0EC/A4BAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1119 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQ1BGUED/A4BAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1211 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQRRBFkEE/A4BAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1303 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQRlBAUED/A4BAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1395 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQpBDEEH/A4BAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1487 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQxBCkEH/A4BAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1579 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwkIAAAAAAABAgIECQJwAR4ecAEeHgceAwR0ZXN0AAoIY2hlY2tfdDAACwhjaGVja190MQAMCToGAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYAQQMLBAEDAQQAQQsLBQYDAgUHCjcIBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwwAQQpBAEEU/A4AAQsHACAAEQABCwcAIAARAAAL +valid valid table_copy.wast:1671 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEcQQFBA/wOAAAL +valid valid table_copy.wast:1696 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEF+QQFBAvwOAAAL +valid valid table_copy.wast:1721 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQRlBBvwOAAAL +valid valid table_copy.wast:1746 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQX5BAvwOAAAL +valid valid table_copy.wast:1771 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQRlBAPwOAAAL +valid valid table_copy.wast:1796 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEeQQ9BAPwOAAAL +valid valid table_copy.wast:1821 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEfQQ9BAPwOAAAL +valid valid table_copy.wast:1846 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQR5BAPwOAAAL +valid valid table_copy.wast:1871 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQR9BAPwOAAAL +valid valid table_copy.wast:1896 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEeQR5BAPwOAAAL +valid valid table_copy.wast:1921 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEfQR9BAPwOAAAL +valid valid table_copy.wast:1946 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEcQQFBA/wOAQAL +valid valid table_copy.wast:1971 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEF+QQFBAvwOAQAL +valid valid table_copy.wast:1996 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQRlBBvwOAQAL +valid valid table_copy.wast:2021 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQX5BAvwOAQAL +valid valid table_copy.wast:2046 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQRlBAPwOAQAL +valid valid table_copy.wast:2071 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEeQQ9BAPwOAQAL +valid valid table_copy.wast:2096 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEfQQ9BAPwOAQAL +valid valid table_copy.wast:2121 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQR5BAPwOAQAL +valid valid table_copy.wast:2146 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQR9BAPwOAQAL +valid valid table_copy.wast:2171 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEeQR5BAPwOAQAL +valid valid table_copy.wast:2196 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEeHgcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEfQR9BAPwOAQAL +valid valid table_copy.wast:2221 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBQFwASBAB2QSAmYwAAACZjEAAQJmMgACAmYzAAMCZjQABAJmNQAFAmY2AAYCZjcABwJmOAAIAmY5AAkDZjEwAAoDZjExAAsDZjEyAAwDZjEzAA0DZjE0AA4DZjE1AA8EdGVzdAAQA3J1bgARCQ4BAEEACwgAAQIDBAUGBwpmEgQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLBABBCgsEAEELCwQAQQwLBABBDQsEAEEOCwQAQQ8LBwAgABEAAAsMACAAIAEgAvwOAAAL +valid valid table_copy.wast:2282 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBQFwASBAB2QSAmYwAAACZjEAAQJmMgACAmYzAAMCZjQABAJmNQAFAmY2AAYCZjcABwJmOAAIAmY5AAkDZjEwAAoDZjExAAsDZjEyAAwDZjEzAA0DZjE0AA4DZjE1AA8EdGVzdAAQA3J1bgARCQ8BAEEACwkAAQIDBAUGBwgKZhIEAEEACwQAQQELBABBAgsEAEEDCwQAQQQLBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwQAQQoLBABBCwsEAEEMCwQAQQ0LBABBDgsEAEEPCwcAIAARAAALDAAgACABIAL8DgAACw== +valid valid table_copy.wast:2343 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBQFwASBAB2QSAmYwAAACZjEAAQJmMgACAmYzAAMCZjQABAJmNQAFAmY2AAYCZjcABwJmOAAIAmY5AAkDZjEwAAoDZjExAAsDZjEyAAwDZjEzAA0DZjE0AA4DZjE1AA8EdGVzdAAQA3J1bgARCQ4BAEEYCwgAAQIDBAUGBwpmEgQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLBABBCgsEAEELCwQAQQwLBABBDQsEAEEOCwQAQQ8LBwAgABEAAAsMACAAIAEgAvwOAAAL +valid valid table_copy.wast:2404 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBQFwASBAB2QSAmYwAAACZjEAAQJmMgACAmYzAAMCZjQABAJmNQAFAmY2AAYCZjcABwJmOAAIAmY5AAkDZjEwAAoDZjExAAsDZjEyAAwDZjEzAA0DZjE0AA4DZjE1AA8EdGVzdAAQA3J1bgARCQ8BAEEXCwkAAQIDBAUGBwgKZhIEAEEACwQAQQELBABBAgsEAEEDCwQAQQQLBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwQAQQoLBABBCwsEAEEMCwQAQQ0LBABBDgsEAEEPCwcAIAARAAALDAAgACABIAL8DgAACw== +valid valid table_copy.wast:2465 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBQFwASBAB2QSAmYwAAACZjEAAQJmMgACAmYzAAMCZjQABAJmNQAFAmY2AAYCZjcABwJmOAAIAmY5AAkDZjEwAAoDZjExAAsDZjEyAAwDZjEzAA0DZjE0AA4DZjE1AA8EdGVzdAAQA3J1bgARCQ4BAEELCwgAAQIDBAUGBwpmEgQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLBABBCgsEAEELCwQAQQwLBABBDQsEAEEOCwQAQQ8LBwAgABEAAAsMACAAIAEgAvwOAAAL +valid valid table_copy.wast:2587 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBQFwASBAB2QSAmYwAAACZjEAAQJmMgACAmYzAAMCZjQABAJmNQAFAmY2AAYCZjcABwJmOAAIAmY5AAkDZjEwAAoDZjExAAsDZjEyAAwDZjEzAA0DZjE0AA4DZjE1AA8EdGVzdAAQA3J1bgARCQ4BAEEVCwgAAQIDBAUGBwpmEgQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLBABBCgsEAEELCwQAQQwLBABBDQsEAEEOCwQAQQ8LBwAgABEAAAsMACAAIAEgAvwOAAAL +valid valid table_copy.wast:2709 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBQFwASBAB2QSAmYwAAACZjEAAQJmMgACAmYzAAMCZjQABAJmNQAFAmY2AAYCZjcABwJmOAAIAmY5AAkDZjEwAAoDZjExAAsDZjEyAAwDZjEzAA0DZjE0AA4DZjE1AA8EdGVzdAAQA3J1bgARCREBAEEVCwsAAQIDBAUGBwgJCgpmEgQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLBABBCgsEAEELCwQAQQwLBABBDQsEAEEOCwQAQQ8LBwAgABEAAAsMACAAIAEgAvwOAAAL +valid valid table_copy.wast:2770 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBwFwAYABgAEHZBICZjAAAAJmMQABAmYyAAICZjMAAwJmNAAEAmY1AAUCZjYABgJmNwAHAmY4AAgCZjkACQNmMTAACgNmMTEACwNmMTIADANmMTMADQNmMTQADgNmMTUADwR0ZXN0ABADcnVuABEJFwEAQfAACxAAAQIDBAUGBwgJCgsMDQ4PCmYSBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsEAEEKCwQAQQsLBABBDAsEAEENCwQAQQ4LBABBDwsHACAAEQAACwwAIAAgASAC/A4AAAs= +valid valid table_copy.wast:2927 AGFzbQEAAAABEANgAAF/YAF/AX9gA39/fwADExIAAAAAAAAAAAAAAAAAAAAAAQIEBwFwAYABgAEHZBICZjAAAAJmMQABAmYyAAICZjMAAwJmNAAEAmY1AAUCZjYABgJmNwAHAmY4AAgCZjkACQNmMTAACgNmMTEACwNmMTIADANmMTMADQNmMTQADgNmMTUADwR0ZXN0ABADcnVuABEJFgEAQQALEAABAgMEBQYHCAkKCwwNDg8KZhIEAEEACwQAQQELBABBAgsEAEEDCwQAQQQLBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwQAQQoLBABBCwsEAEEMCwQAQQ0LBABBDgsEAEEPCwcAIAARAAALDAAgACABIAL8DgAACw== +valid valid table_fill.wast:1 AGFzbQEAAAABDAJgA39vfwBgAX8BbwMEAwAAAQQEAW8ACgccAwRmaWxsAAALZmlsbC1hYmJyZXYAAQNnZXQAAgogAwsAIAAgASAC/BEACwsAIAAgASAC/BEACwYAIAAlAAs= +invalid invalid table_fill.wast:75 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCgcBBQD8EQAL +invalid invalid table_fill.wast:84 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCgsBCQDQb0EB/BEACw== +invalid invalid table_fill.wast:93 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCgsBCQBBAUEB/BEACw== +invalid invalid table_fill.wast:102 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCgsBCQBBAdBv/BEACw== +invalid invalid table_fill.wast:111 AGFzbQEAAAABBAFgAAADAgEABAQBbwAAChABDgBDAACAP9BvQQH8EQAL +invalid invalid table_fill.wast:120 AGFzbQEAAAABBQFgAW8AAwIBAAQEAXAAAAoNAQsAQQEgAEEB/BEACw== +invalid invalid table_fill.wast:129 AGFzbQEAAAABBAFgAAADAgEABAQBbwAAChABDgBBAdBvQwAAgD/8EQAL +invalid invalid table_fill.wast:139 AGFzbQEAAAABBQFgAW8AAwIBAAQHAm8AAXAAAQoNAQsAQQAgAEEB/BEBCw== +invalid invalid table_fill.wast:150 AGFzbQEAAAABBQFgAAF/AwIBAAQEAW8AAQoNAQsAQQDQb0EB/BEACw== +valid valid table_get.wast:1 AGFzbQEAAAABFwVgAABgAW8AYAF/AW9gAX8BcGABfwF/AwYFAAECAwQEBwJvAAJwAAMHOAQEaW5pdAABDWdldC1leHRlcm5yZWYAAgtnZXQtZnVuY3JlZgADD2lzX251bGwtZnVuY3JlZgAECQkBAgFBAQsAAQAKKwUCAAsQAEEBIAAmAEECQQElASYBCwYAIAAlAAsGACAAJQELBwAgABAD0Qs= +invalid invalid table_get.wast:42 AGFzbQEAAAABBQFgAAFvAwIBAAQEAW8ACgoGAQQAJQAL +invalid invalid table_get.wast:51 AGFzbQEAAAABBQFgAAFvAwIBAAQEAW8ACgoLAQkAQwAAgD8lAAs= +invalid invalid table_get.wast:61 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCggBBgBBACUACw== +invalid invalid table_get.wast:70 AGFzbQEAAAABBQFgAAFwAwIBAAQEAW8ACgoIAQYAQQElAAs= +invalid invalid table_get.wast:80 AGFzbQEAAAABBQFgAAFwAwIBAAQHAnAAAW8AAQoIAQYAQQAlAQs= +valid valid table_grow.wast:1 AGFzbQEAAAABFQRgAX8Bb2ACf28AYAJ/bwF/YAABfwMGBQABAgIDBAQBbwAABykFA2dldAAAA3NldAABBGdyb3cAAgtncm93LWFiYnJldgADBHNpemUABAorBQYAIAAlAAsIACAAIAEmAAsJACABIAD8DwALCQAgASAA/A8ACwUA/BAACw== +valid valid table_grow.wast:42 AGFzbQEAAAABBQFgAAF/AwIBAAQEAXAAEAcIAQRncm93AAAJBQEDAAEACgsBCQDSAEFw/A8ACw== +valid valid table_grow.wast:53 AGFzbQEAAAABBgFgAX8BfwMCAQAEBAFvAAAHCAEEZ3JvdwAACgsBCQDQbyAA/A8ACw== +valid valid table_grow.wast:67 AGFzbQEAAAABBgFgAX8BfwMCAQAEBQFvAQAKBwgBBGdyb3cAAAoLAQkA0G8gAPwPAAs= +valid valid table_grow.wast:84 AGFzbQEAAAABDAJgAX8Bf2ACf38BcAMDAgABBAQBcAAKBxsCBGdyb3cAABBjaGVjay10YWJsZS1udWxsAAEJBQEDAAEBCj0CCQDQcCAA/A8ACzEBAXDSASECAkADQCAAJQAhAiAC0UUNASAAIAFPDQEgAEEBaiEAIAAgAU0NAAsLIAIL +valid valid table_grow.wast:111 AGFzbQEAAAABBQFgAAF/AwIBAAQEAXAAAQcQAgV0YWJsZQEABGdyb3cAAAoLAQkA0HBBAfwPAAs= +valid valid table_grow.wast:117 AGFzbQEAAAABBQFgAAF/AhcBC2dyb3duLXRhYmxlBXRhYmxlAXAAAgMCAQAHEAIFdGFibGUBAARncm93AAAKCwEJANBwQQH8DwAL +valid valid table_grow.wast:124 AGFzbQEAAAABBQFgAAF/AiABFGdyb3duLWltcG9ydGVkLXRhYmxlBXRhYmxlAXAAAwMCAQAHCAEEc2l6ZQAACgcBBQD8EAAL +invalid invalid table_grow.wast:135 AGFzbQEAAAABBQFgAAF/AwIBAAQEAW8AAAoHAQUA/A8ACw== +invalid invalid table_grow.wast:144 AGFzbQEAAAABBQFgAAF/AwIBAAQEAW8AAAoJAQcA0G/8DwAL +invalid invalid table_grow.wast:153 AGFzbQEAAAABBQFgAAF/AwIBAAQEAW8AAAoJAQcAQQH8DwAL +invalid invalid table_grow.wast:162 AGFzbQEAAAABBQFgAAF/AwIBAAQEAW8AAAoOAQwA0G9DAACAP/wPAAs= +invalid invalid table_grow.wast:171 AGFzbQEAAAABBgFgAW8BfwMCAQAEBAFwAAAKCwEJACAAQQH8DwAL +invalid invalid table_grow.wast:181 AGFzbQEAAAABBAFgAAADAgEABAQBbwABCgsBCQDQb0EA/A8ACw== +invalid invalid table_grow.wast:190 AGFzbQEAAAABBQFgAAF9AwIBAAQEAW8AAQoLAQkA0G9BAPwPAAs= +valid valid table_init.wast:15 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwgHAAAAAAABAgQJAnABHh5wAR4eBxACBHRlc3QACgVjaGVjawALCSMEAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgovBwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEHQQBBBPwMAQALBwAgABEAAAs= +valid valid table_init.wast:73 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwgHAAAAAAABAgQJAnABHh5wAR4eBxACBHRlc3QACgVjaGVjawALCSMEAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgovBwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEPQQFBA/wMAwALBwAgABEAAAs= +valid valid table_init.wast:131 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwgHAAAAAAABAgQJAnABHh5wAR4eBxACBHRlc3QACgVjaGVjawALCSMEAEECCwQDAQQBAQAEAgcBCABBDAsFBwUCAwYBAAUFCQIHBgpxBwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQtOAEEHQQBBBPwMAQD8DQFBD0EBQQP8DAMA/A0DQRRBD0EF/A4AAEEVQR1BAfwOAABBGEEKQQH8DgAAQQ1BC0EE/A4AAEETQRRBBfwOAAALBwAgABEAAAs= +valid valid table_init.wast:197 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwgHAAAAAAABAgQJAnABHh5wAR4eBxACBHRlc3QACgVjaGVjawALCScEAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYKLwcEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBB0EAQQT8DAEBCwcAIAARAAEL +valid valid table_init.wast:255 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwgHAAAAAAABAgQJAnABHh5wAR4eBxACBHRlc3QACgVjaGVjawALCScEAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYKLwcEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBD0EBQQP8DAMBCwcAIAARAAEL +valid valid table_init.wast:313 AGFzbQEAAAABDQNgAAF/YAAAYAF/AX8CKQUBYQNlZjAAAAFhA2VmMQAAAWEDZWYyAAABYQNlZjMAAAFhA2VmNAAAAwgHAAAAAAABAgQJAnABHh5wAR4eBxACBHRlc3QACgVjaGVjawALCScEAgFBAgsABAMBBAEBAAQCBwEIAgFBDAsABQcFAgMGAQAFBQkCBwYKcQcEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLTgBBB0EAQQT8DAEB/A0BQQ9BAUED/AwDAfwNA0EUQQ9BBfwOAQFBFUEdQQH8DgEBQRhBCkEB/A4BAUENQQtBBPwOAQFBE0EUQQX8DgEBCwcAIAARAAEL +invalid invalid table_init.wast:379 AGFzbQEAAAABBAFgAAADAgEABwgBBHRlc3QAAAoHAQUA/A0ACw== +invalid invalid table_init.wast:385 AGFzbQEAAAABBAFgAAADAgEABwgBBHRlc3QAAAoOAQwAQQxBAUEB/AwAAAs= +invalid invalid table_init.wast:391 AGFzbQEAAAABCAJgAAF/YAAAAwMCAAEHCAEEdGVzdAABCQUBAQABAAoMAgQAQQALBQD8DQQL +invalid invalid table_init.wast:399 AGFzbQEAAAABCAJgAAF/YAAAAwMCAAEHCAEEdGVzdAABCQUBAQABAAoTAgQAQQALDABBDEEBQQH8DAQACw== +valid valid table_init.wast:407 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCjkLBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsFAPwNAgs= +valid valid table_init.wast:431 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEMQQFBAfwMAgAL +valid valid table_init.wast:455 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkoLBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsWAEEMQQFBAfwMAQBBFUEBQQH8DAEACw== +valid valid table_init.wast:479 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCjwLBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsIAPwNAfwNAQs= +valid valid table_init.wast:503 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkMLBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsPAPwNAUEMQQFBAfwMAQAL +valid valid table_init.wast:527 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEMQQBBBfwMAQAL +valid valid table_init.wast:551 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEMQQJBA/wMAQAL +valid valid table_init.wast:575 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEcQQFBA/wMAQAL +valid valid table_init.wast:599 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEMQQRBAPwMAQAL +valid valid table_init.wast:623 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEMQQVBAPwMAQAL +valid valid table_init.wast:647 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEeQQJBAPwMAQAL +valid valid table_init.wast:671 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEfQQJBAPwMAQAL +valid valid table_init.wast:695 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEeQQRBAPwMAQAL +valid valid table_init.wast:719 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJIwQAQQILBAMBBAEBAAQCBwEIAEEMCwUHBQIDBgEABQUJAgcGCkALBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsMAEEfQQVBAPwMAQAL +valid valid table_init.wast:743 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJJwQCAUECCwAEAwEEAQEABAIHAQgCAUEMCwAFBwUCAwYBAAUFCQIHBgpACwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBGkEBQQP8DAEBCw== +valid valid table_init.wast:767 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJJwQCAUECCwAEAwEEAQEABAIHAQgCAUEMCwAFBwUCAwYBAAUFCQIHBgpACwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBDEEEQQD8DAEBCw== +valid valid table_init.wast:791 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJJwQCAUECCwAEAwEEAQEABAIHAQgCAUEMCwAFBwUCAwYBAAUFCQIHBgpACwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBDEEFQQD8DAEBCw== +valid valid table_init.wast:815 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJJwQCAUECCwAEAwEEAQEABAIHAQgCAUEMCwAFBwUCAwYBAAUFCQIHBgpACwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBHEECQQD8DAEBCw== +valid valid table_init.wast:839 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJJwQCAUECCwAEAwEEAQEABAIHAQgCAUEMCwAFBwUCAwYBAAUFCQIHBgpACwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBHUECQQD8DAEBCw== +valid valid table_init.wast:863 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJJwQCAUECCwAEAwEEAQEABAIHAQgCAUEMCwAFBwUCAwYBAAUFCQIHBgpACwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBHEEEQQD8DAEBCw== +valid valid table_init.wast:887 AGFzbQEAAAABCAJgAAF/YAAAAwwLAAAAAAAAAAAAAAEECQJwAR4ecAEcHAcIAQR0ZXN0AAoJJwQCAUECCwAEAwEEAQEABAIHAQgCAUEMCwAFBwUCAwYBAAUFCQIHBgpACwQAQQALBABBAQsEAEECCwQAQQMLBABBBAsEAEEFCwQAQQYLBABBBwsEAEEICwQAQQkLDABBHUEFQQD8DAEBCw== +invalid invalid table_init.wast:912 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEEBQQFDAACAP/wMAAAL +invalid invalid table_init.wast:921 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKEQICAAsMAEEBQQFCAfwMAAAL +invalid invalid table_init.wast:930 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEEBQQFEAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:939 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEEBQwAAgD9BAfwMAAAL +invalid invalid table_init.wast:948 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFwICAAsSAEEBQwAAgD9DAACAP/wMAAAL +invalid invalid table_init.wast:957 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEEBQwAAgD9CAfwMAAAL +invalid invalid table_init.wast:966 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEEBQwAAgD9EAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:975 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKEQICAAsMAEEBQgFBAfwMAAAL +invalid invalid table_init.wast:984 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEEBQgFDAACAP/wMAAAL +invalid invalid table_init.wast:993 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKEQICAAsMAEEBQgFCAfwMAAAL +invalid invalid table_init.wast:1002 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEEBQgFEAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:1011 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEEBRAAAAAAAAPA/QQH8DAAACw== +invalid invalid table_init.wast:1020 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEEBRAAAAAAAAPA/QwAAgD/8DAAACw== +invalid invalid table_init.wast:1029 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEEBRAAAAAAAAPA/QgH8DAAACw== +invalid invalid table_init.wast:1038 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHwICAAsaAEEBRAAAAAAAAPA/RAAAAAAAAPA//AwAAAs= +invalid invalid table_init.wast:1047 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEMAAIA/QQFBAfwMAAAL +invalid invalid table_init.wast:1056 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFwICAAsSAEMAAIA/QQFDAACAP/wMAAAL +invalid invalid table_init.wast:1065 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEMAAIA/QQFCAfwMAAAL +invalid invalid table_init.wast:1074 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEMAAIA/QQFEAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:1083 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFwICAAsSAEMAAIA/QwAAgD9BAfwMAAAL +invalid invalid table_init.wast:1092 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGgICAAsVAEMAAIA/QwAAgD9DAACAP/wMAAAL +invalid invalid table_init.wast:1101 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFwICAAsSAEMAAIA/QwAAgD9CAfwMAAAL +invalid invalid table_init.wast:1110 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHgICAAsZAEMAAIA/QwAAgD9EAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:1119 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEMAAIA/QgFBAfwMAAAL +invalid invalid table_init.wast:1128 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFwICAAsSAEMAAIA/QgFDAACAP/wMAAAL +invalid invalid table_init.wast:1137 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEMAAIA/QgFCAfwMAAAL +invalid invalid table_init.wast:1146 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEMAAIA/QgFEAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:1155 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEMAAIA/RAAAAAAAAPA/QQH8DAAACw== +invalid invalid table_init.wast:1164 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHgICAAsZAEMAAIA/RAAAAAAAAPA/QwAAgD/8DAAACw== +invalid invalid table_init.wast:1173 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEMAAIA/RAAAAAAAAPA/QgH8DAAACw== +invalid invalid table_init.wast:1182 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKIgICAAsdAEMAAIA/RAAAAAAAAPA/RAAAAAAAAPA//AwAAAs= +invalid invalid table_init.wast:1191 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKEQICAAsMAEIBQQFBAfwMAAAL +invalid invalid table_init.wast:1200 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEIBQQFDAACAP/wMAAAL +invalid invalid table_init.wast:1209 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKEQICAAsMAEIBQQFCAfwMAAAL +invalid invalid table_init.wast:1218 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEIBQQFEAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:1227 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEIBQwAAgD9BAfwMAAAL +invalid invalid table_init.wast:1236 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFwICAAsSAEIBQwAAgD9DAACAP/wMAAAL +invalid invalid table_init.wast:1245 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEIBQwAAgD9CAfwMAAAL +invalid invalid table_init.wast:1254 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEIBQwAAgD9EAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:1263 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKEQICAAsMAEIBQgFBAfwMAAAL +invalid invalid table_init.wast:1272 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKFAICAAsPAEIBQgFDAACAP/wMAAAL +invalid invalid table_init.wast:1281 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKEQICAAsMAEIBQgFCAfwMAAAL +invalid invalid table_init.wast:1290 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEIBQgFEAAAAAAAA8D/8DAAACw== +invalid invalid table_init.wast:1299 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEIBRAAAAAAAAPA/QQH8DAAACw== +invalid invalid table_init.wast:1308 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEIBRAAAAAAAAPA/QwAAgD/8DAAACw== +invalid invalid table_init.wast:1317 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEIBRAAAAAAAAPA/QgH8DAAACw== +invalid invalid table_init.wast:1326 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHwICAAsaAEIBRAAAAAAAAPA/RAAAAAAAAPA//AwAAAs= +invalid invalid table_init.wast:1335 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEQAAAAAAADwP0EBQQH8DAAACw== +invalid invalid table_init.wast:1344 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEQAAAAAAADwP0EBQwAAgD/8DAAACw== +invalid invalid table_init.wast:1353 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEQAAAAAAADwP0EBQgH8DAAACw== +invalid invalid table_init.wast:1362 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHwICAAsaAEQAAAAAAADwP0EBRAAAAAAAAPA//AwAAAs= +invalid invalid table_init.wast:1371 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEQAAAAAAADwP0MAAIA/QQH8DAAACw== +invalid invalid table_init.wast:1380 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHgICAAsZAEQAAAAAAADwP0MAAIA/QwAAgD/8DAAACw== +invalid invalid table_init.wast:1389 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEQAAAAAAADwP0MAAIA/QgH8DAAACw== +invalid invalid table_init.wast:1398 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKIgICAAsdAEQAAAAAAADwP0MAAIA/RAAAAAAAAPA//AwAAAs= +invalid invalid table_init.wast:1407 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEQAAAAAAADwP0IBQQH8DAAACw== +invalid invalid table_init.wast:1416 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGwICAAsWAEQAAAAAAADwP0IBQwAAgD/8DAAACw== +invalid invalid table_init.wast:1425 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKGAICAAsTAEQAAAAAAADwP0IBQgH8DAAACw== +invalid invalid table_init.wast:1434 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHwICAAsaAEQAAAAAAADwP0IBRAAAAAAAAPA//AwAAAs= +invalid invalid table_init.wast:1443 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHwICAAsaAEQAAAAAAADwP0QAAAAAAADwP0EB/AwAAAs= +invalid invalid table_init.wast:1452 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKIgICAAsdAEQAAAAAAADwP0QAAAAAAADwP0MAAIA//AwAAAs= +invalid invalid table_init.wast:1461 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKHwICAAsaAEQAAAAAAADwP0QAAAAAAADwP0IB/AwAAAs= +invalid invalid table_init.wast:1470 AGFzbQEAAAABBAFgAAADAwIAAAQEAXAACgcIAQR0ZXN0AAEJBwEBAAMAAAAKJgICAAshAEQAAAAAAADwP0QAAAAAAADwP0QAAAAAAADwP/wMAAAL +valid valid table_init.wast:1478 AGFzbQEAAAABDwNgAAF/YAF/AX9gAn9/AAMTEgAAAAAAAAAAAAAAAAAAAAABAgQFAXABIEAHZBICZjAAAAJmMQABAmYyAAICZjMAAwJmNAAEAmY1AAUCZjYABgJmNwAHAmY4AAgCZjkACQNmMTAACgNmMTEACwNmMTIADANmMTMADQNmMTQADgNmMTUADwR0ZXN0ABADcnVuABEJFAEBABAAAQIDBAUGBwgJCgsMDQ4PCmYSBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsEAEEKCwQAQQsLBABBDAsEAEENCwQAQQ4LBABBDwsHACAAEQAACwwAIABBACAB/AwAAAs= +valid valid table_init.wast:1602 AGFzbQEAAAABDwNgAAF/YAF/AX9gAn9/AAMTEgAAAAAAAAAAAAAAAAAAAAABAgQHAXABoAHAAgdkEgJmMAAAAmYxAAECZjIAAgJmMwADAmY0AAQCZjUABQJmNgAGAmY3AAcCZjgACAJmOQAJA2YxMAAKA2YxMQALA2YxMgAMA2YxMwANA2YxNAAOA2YxNQAPBHRlc3QAEANydW4AEQkUAQEAEAABAgMEBQYHCAkKCwwNDg8KZhIEAEEACwQAQQELBABBAgsEAEEDCwQAQQQLBABBBQsEAEEGCwQAQQcLBABBCAsEAEEJCwQAQQoLBABBCwsEAEEMCwQAQQ0LBABBDgsEAEEPCwcAIAARAAALDAAgAEEAIAH8DAAACw== +valid valid table_init.wast:1982 AGFzbQEAAAABDwNgAAF/YAF/AX9gAn9/AAMTEgAAAAAAAAAAAAAAAAAAAAABAgQFAXABQEAHZBICZjAAAAJmMQABAmYyAAICZjMAAwJmNAAEAmY1AAUCZjYABgJmNwAHAmY4AAgCZjkACQNmMTAACgNmMTEACwNmMTIADANmMTMADQNmMTQADgNmMTUADwR0ZXN0ABADcnVuABEJFAEBABAAAQIDBAUGBwgJCgsMDQ4PCmYSBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsEAEEKCwQAQQsLBABBDAsEAEENCwQAQQ4LBABBDwsHACAAEQAACwwAIABBACAB/AwAAAs= +valid valid table_init.wast:2076 AGFzbQEAAAABDwNgAAF/YAF/AX9gAn9/AAMTEgAAAAAAAAAAAAAAAAAAAAABAgQFAXABEBAHZBICZjAAAAJmMQABAmYyAAICZjMAAwJmNAAEAmY1AAUCZjYABgJmNwAHAmY4AAgCZjkACQNmMTAACgNmMTEACwNmMTIADANmMTMADQNmMTQADgNmMTUADwR0ZXN0ABADcnVuABEJFAEBABAAAQIDBAUGBwgJCgsMDQ4PCmYSBABBAAsEAEEBCwQAQQILBABBAwsEAEEECwQAQQULBABBBgsEAEEHCwQAQQgLBABBCQsEAEEKCwQAQQsLBABBDAsEAEENCwQAQQ4LBABBDwsHACAAEQAACwwAIABBCCAB/AwAAAs= +valid valid table_init.wast:2122 AGFzbQEAAAABBAFgAAADAgEABAQBcAABCcQBQQEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAEAAAoOAQwAQQBBAEEA/AxAAAs= +valid valid table_set.wast:1 AGFzbQEAAAABIgdgAABgAX8Bb2ABfwFwYAJ/bwBgAn9wAGACf38AYAF/AX8DCAcAAQIDBAUGBAcCbwABcAACB2IGDWdldC1leHRlcm5yZWYAAQtnZXQtZnVuY3JlZgACDXNldC1leHRlcm5yZWYAAwtzZXQtZnVuY3JlZgAEEHNldC1mdW5jcmVmLWZyb20ABQ9pc19udWxsLWZ1bmNyZWYABgkJAQIBQQELAAEACjcHAgALBgAgACUACwYAIAAlAQsIACAAIAEmAAsIACAAIAEmAQsKACAAIAElASYBCwcAIAAQAtEL +invalid invalid table_set.wast:55 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCgYBBAAmAAs= +invalid invalid table_set.wast:64 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCggBBgDQbyYACw== +invalid invalid table_set.wast:73 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCggBBgBBASYACw== +invalid invalid table_set.wast:82 AGFzbQEAAAABBAFgAAADAgEABAQBbwAKCg0BCwBDAACAP9BvJgAL +invalid invalid table_set.wast:91 AGFzbQEAAAABBQFgAW8AAwIBAAQEAXAACgoKAQgAQQEgACYACw== +invalid invalid table_set.wast:101 AGFzbQEAAAABBQFgAW8AAwIBAAQHAm8AAXAAAQoKAQgAQQAgACYBCw== +invalid invalid table_set.wast:112 AGFzbQEAAAABBQFgAAF/AwIBAAQEAW8ACgoKAQgAQQDQbyYACw== +valid valid table_size.wast:1 AGFzbQEAAAABCQJgAAF/YAF/AAMJCAAAAAABAQEBBA8EbwAAbwABbwEAAm8BAwgHUQgHc2l6ZS10MAAAB3NpemUtdDEAAQdzaXplLXQyAAIHc2l6ZS10MwADB2dyb3ctdDAABAdncm93LXQxAAUHZ3Jvdy10MgAGB2dyb3ctdDMABwpFCAUA/BAACwUA/BABCwUA/BACCwUA/BADCwoA0G8gAPwPABoLCgDQbyAA/A8BGgsKANBvIAD8DwIaCwoA0G8gAPwPAxoL +invalid invalid table_size.wast:70 AGFzbQEAAAABBAFgAAADAgEABAQBbwABCgcBBQD8EAAL +invalid invalid table_size.wast:79 AGFzbQEAAAABBQFgAAF9AwIBAAQEAW8AAQoHAQUA/BAACw== +valid valid token.wast:15 AGFzbQEAAAABBAFgAAADAgEACgUBAwABCw== +valid valid token.wast:18 AGFzbQEAAAABBAFgAAADAgEACgYBBAABAQs= +valid valid token.wast:30 AGFzbQEAAAABBAFgAAADAgEACgcBBQAMAAEL +valid valid token.wast:33 AGFzbQEAAAABBAFgAAADAgEABAQBcAABCQcBAEEACwEACgQBAgAL +valid valid token.wast:47 AGFzbQEAAAABBAFgAAACEgEIc3BlY3Rlc3QFcHJpbnQAAA== +valid valid token.wast:54 AGFzbQEAAAABBAFgAAADAgEACgQBAgAL +valid valid token.wast:70 AGFzbQEAAAABBAFgAAADAgEACgYBBAAMAAs= +valid valid token.wast:74 AGFzbQEAAAALBAEBAWE= +valid valid token.wast:82 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEADgEAAAsL +valid valid token.wast:112 AGFzbQEAAAABBAFgAAADAgEACgwBCgACQEEADgAACws= +valid valid token.wast:142 AGFzbQEAAAALBQEBAiBh +valid valid token.wast:152 AGFzbQEAAAALBQEBAmEg +valid valid token.wast:162 AGFzbQEAAAALBgEBA2EgYg== +valid valid token.wast:172 AGFzbQEAAAALCQEBBu+Ymu+SqQ== +valid valid token.wast:182 AGFzbQEAAAALCgEBByDvmJrvkqk= +valid valid token.wast:192 AGFzbQEAAAALCgEBB++Ymu+SqSA= +valid valid token.wast:202 AGFzbQEAAAALBQEBAmFi +valid valid token.wast:232 AGFzbQEAAAALDwEBDO+Ymu+Sqe+Ymu+SqQ== +valid valid token.wast:242 AGFzbQEAAAALEAEBDe+Ymu+SqSDvmJrvkqk= +valid valid traps.wast:5 AGFzbQEAAAABCwJgAn9/AGACfn4AAwUEAAABAQdNBBBub19kY2UuaTMyLmRpdl9zAAAQbm9fZGNlLmkzMi5kaXZfdQABEG5vX2RjZS5pNjQuZGl2X3MAAhBub19kY2UuaTY0LmRpdl91AAMKJQQIACAAIAFtGgsIACAAIAFuGgsIACAAIAF/GgsIACAAIAGAGgs= +valid valid traps.wast:23 AGFzbQEAAAABCwJgAn9/AGACfn4AAwUEAAABAQdNBBBub19kY2UuaTMyLnJlbV9zAAAQbm9fZGNlLmkzMi5yZW1fdQABEG5vX2RjZS5pNjQucmVtX3MAAhBub19kY2UuaTY0LnJlbV91AAMKJQQIACAAIAFvGgsIACAAIAFwGgsIACAAIAGBGgsIACAAIAGCGgs= +valid valid traps.wast:39 AGFzbQEAAAABCQJgAX0AYAF8AAMJCAAAAQEAAAEBB8kBCBZub19kY2UuaTMyLnRydW5jX2YzMl9zAAAWbm9fZGNlLmkzMi50cnVuY19mMzJfdQABFm5vX2RjZS5pMzIudHJ1bmNfZjY0X3MAAhZub19kY2UuaTMyLnRydW5jX2Y2NF91AAMWbm9fZGNlLmk2NC50cnVuY19mMzJfcwAEFm5vX2RjZS5pNjQudHJ1bmNfZjMyX3UABRZub19kY2UuaTY0LnRydW5jX2Y2NF9zAAYWbm9fZGNlLmk2NC50cnVuY19mNjRfdQAHCjkIBgAgAKgaCwYAIACpGgsGACAAqhoLBgAgAKsaCwYAIACuGgsGACAArxoLBgAgALAaCwYAIACxGgs= +valid valid traps.wast:59 AGFzbQEAAAABBQFgAX8AAw8OAAAAAAAAAAAAAAAAAAAFAwEAAQehAg4Pbm9fZGNlLmkzMi5sb2FkAAATbm9fZGNlLmkzMi5sb2FkMTZfcwABE25vX2RjZS5pMzIubG9hZDE2X3UAAhJub19kY2UuaTMyLmxvYWQ4X3MAAxJub19kY2UuaTMyLmxvYWQ4X3UABA9ub19kY2UuaTY0LmxvYWQABRNub19kY2UuaTY0LmxvYWQzMl9zAAYTbm9fZGNlLmk2NC5sb2FkMzJfdQAHE25vX2RjZS5pNjQubG9hZDE2X3MACBNub19kY2UuaTY0LmxvYWQxNl91AAkSbm9fZGNlLmk2NC5sb2FkOF9zAAoSbm9fZGNlLmk2NC5sb2FkOF91AAsPbm9fZGNlLmYzMi5sb2FkAAwPbm9fZGNlLmY2NC5sb2FkAA0Kfw4IACAAKAIAGgsIACAALgEAGgsIACAALwEAGgsIACAALAAAGgsIACAALQAAGgsIACAAKQMAGgsIACAANAIAGgsIACAANQIAGgsIACAAMgEAGgsIACAAMwEAGgsIACAAMAAAGgsIACAAMQAAGgsIACAAKgIAGgsIACAAKwMAGgs= +valid valid type.wast:3 AGFzbQEAAAABiQEXYAAAYAAAYAF/AGABfwBgAAF/YAF/AX9gAX8Bf2ACfXwAYAACfn1gAn9+An18YAJ9fABgAn18AGACfXwAYAJ9fABgAAJ+fWACf34CfXxgAn9+An18YAZ9fH98f38AYAAFfn59fX9gBH9/fn8EfXx8f2ADfXx/AGAAA35+fWAFf39+f38EfXx8fw== +valid valid unreachable.wast:3 AGFzbQEAAAABIAdgA39/fwBgAABgAAF/YAABfmAAAX1gAAF8YAJ/fwF/Az08AQACAwQFAgIBAgICAQICAgIBAgIBAgIBAgICAgMCBgYGBgYCAQEBAQEBAQEEBAQDAQEBAQQCAwICAgICBAUBcAEBAQUDAQABBgkBfQFDAAAAAAsHjAg6CHR5cGUtaTMyAAIIdHlwZS1pNjQAAwh0eXBlLWYzMgAECHR5cGUtZjY0AAUNYXMtZnVuYy1maXJzdAAGC2FzLWZ1bmMtbWlkAAcMYXMtZnVuYy1sYXN0AAgNYXMtZnVuYy12YWx1ZQAJDmFzLWJsb2NrLWZpcnN0AAoMYXMtYmxvY2stbWlkAAsNYXMtYmxvY2stbGFzdAAMDmFzLWJsb2NrLXZhbHVlAA0OYXMtYmxvY2stYnJva2UADg1hcy1sb29wLWZpcnN0AA8LYXMtbG9vcC1taWQAEAxhcy1sb29wLWxhc3QAEQ1hcy1sb29wLWJyb2tlABILYXMtYnItdmFsdWUAEw1hcy1icl9pZi1jb25kABQOYXMtYnJfaWYtdmFsdWUAFRNhcy1icl9pZi12YWx1ZS1jb25kABYRYXMtYnJfdGFibGUtaW5kZXgAFxFhcy1icl90YWJsZS12YWx1ZQAYE2FzLWJyX3RhYmxlLXZhbHVlLTIAGRdhcy1icl90YWJsZS12YWx1ZS1pbmRleAAaG2FzLWJyX3RhYmxlLXZhbHVlLWFuZC1pbmRleAAbD2FzLXJldHVybi12YWx1ZQAcCmFzLWlmLWNvbmQAHQphcy1pZi10aGVuAB4KYXMtaWYtZWxzZQAfEmFzLWlmLXRoZW4tbm8tZWxzZQAgD2FzLXNlbGVjdC1maXJzdAAhEGFzLXNlbGVjdC1zZWNvbmQAIg5hcy1zZWxlY3QtY29uZAAjDWFzLWNhbGwtZmlyc3QAJAthcy1jYWxsLW1pZAAlDGFzLWNhbGwtbGFzdAAmFWFzLWNhbGxfaW5kaXJlY3QtZnVuYwAnFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAKBRhcy1jYWxsX2luZGlyZWN0LW1pZAApFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAAqEmFzLWxvY2FsLnNldC12YWx1ZQArEmFzLWxvY2FsLnRlZS12YWx1ZQAsE2FzLWdsb2JhbC5zZXQtdmFsdWUALQ9hcy1sb2FkLWFkZHJlc3MALhBhcy1sb2FkTi1hZGRyZXNzAC8QYXMtc3RvcmUtYWRkcmVzcwAwDmFzLXN0b3JlLXZhbHVlADERYXMtc3RvcmVOLWFkZHJlc3MAMg9hcy1zdG9yZU4tdmFsdWUAMxBhcy11bmFyeS1vcGVyYW5kADQOYXMtYmluYXJ5LWxlZnQANQ9hcy1iaW5hcnktcmlnaHQANg9hcy10ZXN0LW9wZXJhbmQANw9hcy1jb21wYXJlLWxlZnQAOBBhcy1jb21wYXJlLXJpZ2h0ADkSYXMtY29udmVydC1vcGVyYW5kADoTYXMtbWVtb3J5Lmdyb3ctc2l6ZQA7CQcBAEEACwEBCrUEPAIACwIACwMAAAsDAAALAwAACwMAAAsFAABBfwsHABAAAEF/CwUAEAAACwUAEAAACwgAAn8AQQILCwoAAn8QAABBAgsLCQACQAEQAAALCwkAAn8BEAAACwsMAAJ/EABBAQwAAAsLCAADfwBBAgsLCgADfxAAAEECCwsJAANAARAAAAsLDwACfwN/EABBAQwBAAsLCwgAAn8ADAALCwgAAkAADQALCw0AAn8AQQENABpBBwsLDQACf0EGAA0AGkEHCwsLAAJAAA4CAAAACwsPAAJ/AEEBDgIAAABBBwsLDwACfwJ/AEEBDgEAAQsLCw4AAn9BBgAOAQAAQQcLCwwAAn8ADgEAAEEICwsEAAAPCwsAAAR/QQAFQQELCwsAIAAEfwAFIAELCwsAIAAEfyABBQALCwoAIAAEQAALIAELCAAAIAAgARsLCAAgAAAgARsLCABBAEEBABsLCQAAQQJBAxABCwkAQQEAQQMQAQsJAEEBQQIAEAELDAAAQQFBAkEDEQAACwwAQQAAQQJBAxEAAAsMAEEAQQEAQQMRAAALDABBAEEBQQIAEQAACwcBAX0AIQALBwEBfQAiAAsFAAAkAAsGAAAqAgALBgAAMAAACw8AAEQAAAAAAAAcQDkDAAsIAEECADcDAAsIAABBBzoAAAsIAEECAD0BAAsEAACMCwYAAEEKagsGAEIKAH0LBAAARQsNAABEAAAAAAAAJEBlCwkAQwAAIEEAXAsEAACnCwUAAEAACw== +invalid invalid unreached-invalid.wast:4 AGFzbQEAAAABBAFgAAADAgEACggBBgAAIAAaCw== +invalid invalid unreached-invalid.wast:8 AGFzbQEAAAABBAFgAAADAgEACggBBgAAIwAaCw== +invalid invalid unreached-invalid.wast:12 AGFzbQEAAAABBAFgAAADAgEACgcBBQAAEAEL +invalid invalid unreached-invalid.wast:16 AGFzbQEAAAABBAFgAAADAgEACgcBBQAADAEL +invalid invalid unreached-invalid.wast:21 AGFzbQEAAAABBAFgAAADAgEACgkBBwAAQQBQGgs= +invalid invalid unreached-invalid.wast:27 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAAEIAQQAbCw== +invalid invalid unreached-invalid.wast:33 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAEIAQQAbQQBBABsL +invalid invalid unreached-invalid.wast:42 AGFzbQEAAAABBAFgAAADAgEACgcBBQAAQQAL +invalid invalid unreached-invalid.wast:46 AGFzbQEAAAABBAFgAAADAgEACgYBBAAARQs= +invalid invalid unreached-invalid.wast:50 AGFzbQEAAAABBAFgAAADAgEACggBBgAAQQBqCw== +invalid invalid unreached-invalid.wast:56 AGFzbQEAAAABBAFgAAADAgEACgYBBAAAGws= +invalid invalid unreached-invalid.wast:60 AGFzbQEAAAABBAFgAAADAgEACggBBgAAQQAbCw== +invalid invalid unreached-invalid.wast:64 AGFzbQEAAAABBAFgAAADAgEACgoBCAAAQQBBABsL +invalid invalid unreached-invalid.wast:71 AGFzbQEAAAABBAFgAAADAgEACg8BDQACQAwAAkABRRoLCws= +invalid invalid unreached-invalid.wast:77 AGFzbQEAAAABBAFgAAADAgEAChABDgACQAwAQwAAgD9FGgsL +invalid invalid unreached-invalid.wast:83 AGFzbQEAAAABBAFgAAADAgEAChABDgACQAwAAkBBAVsaCwsL +invalid invalid unreached-invalid.wast:89 AGFzbQEAAAABBAFgAAADAgEAChIBEAACQAwAQQFDAAAAAFsaCws= +invalid invalid unreached-invalid.wast:95 AGFzbQEAAAABBAFgAAADAgEACgsBCQACQAwAQQELCw== +invalid invalid unreached-invalid.wast:101 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn9BAQwAQwAAAAALCw== +invalid invalid unreached-invalid.wast:107 AGFzbQEAAAABBAFgAAADAgEACg4BDAACQANADAFBAQsLCw== +invalid invalid unreached-invalid.wast:113 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AA39BAQwBQwAAAAALCw== +invalid invalid unreached-invalid.wast:119 AGFzbQEAAAABBAFgAAADAgEACggBBgAMAEEBCw== +invalid invalid unreached-invalid.wast:125 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAQQEMAEMAAAAACw== +invalid invalid unreached-invalid.wast:132 AGFzbQEAAAABBAFgAAADAgEACgsBCQAPAkABRRoLCw== +invalid invalid unreached-invalid.wast:138 AGFzbQEAAAABBAFgAAADAgEACgwBCgAPQwAAgD9FGgs= +invalid invalid unreached-invalid.wast:144 AGFzbQEAAAABBAFgAAADAgEACgwBCgAPAkBBAVsaCws= +invalid invalid unreached-invalid.wast:150 AGFzbQEAAAABBAFgAAADAgEACg4BDAAPQQFDAAAAAFsaCw== +invalid invalid unreached-invalid.wast:156 AGFzbQEAAAABBAFgAAADAgEACgoBCAACQA9BAQsL +invalid invalid unreached-invalid.wast:162 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAn9BAUEAD0MAAAAACws= +invalid invalid unreached-invalid.wast:168 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQANAD0EBCwsL +invalid invalid unreached-invalid.wast:174 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AA39BAQ9DAAAAAAsL +invalid invalid unreached-invalid.wast:180 AGFzbQEAAAABBAFgAAADAgEACgcBBQAPQQEL +invalid invalid unreached-invalid.wast:186 AGFzbQEAAAABBQFgAAF/AwIBAAoMAQoAQQEPQwAAAAAL +invalid invalid unreached-invalid.wast:193 AGFzbQEAAAABBAFgAAADAgEACgsBCQAAAkABRRoLCw== +invalid invalid unreached-invalid.wast:199 AGFzbQEAAAABBAFgAAADAgEACgsBCQAAA0ABRRoLCw== +invalid invalid unreached-invalid.wast:205 AGFzbQEAAAABBAFgAAADAgEACgoBCAAAA38BRQsL +invalid invalid unreached-invalid.wast:211 AGFzbQEAAAABBAFgAAADAgEACgwBCgAAQwAAgD9FGgs= +invalid invalid unreached-invalid.wast:217 AGFzbQEAAAABBAFgAAADAgEACgwBCgAAAkBBAVsaCws= +invalid invalid unreached-invalid.wast:223 AGFzbQEAAAABBAFgAAADAgEACg4BDAAAQQFDAAAAAFsaCw== +invalid invalid unreached-invalid.wast:229 AGFzbQEAAAABBAFgAAADAgEACgoBCAACQABBAQsL +invalid invalid unreached-invalid.wast:235 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAn9BAQBDAAAAAAsL +invalid invalid unreached-invalid.wast:241 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQANAAEEBCwsL +invalid invalid unreached-invalid.wast:247 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAA38AQwAAAAALCw== +invalid invalid unreached-invalid.wast:253 AGFzbQEAAAABBAFgAAADAgEACgcBBQAAQQEL +invalid invalid unreached-invalid.wast:259 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAAEMAAAAACw== +invalid invalid unreached-invalid.wast:265 AGFzbQEAAAABBAFgAAADAgEACg0BCwAAQQAEQAFFGgsL +invalid invalid unreached-invalid.wast:271 AGFzbQEAAAABBAFgAAADAgEACg8BDQAAQQAEQAEFAUUaCws= +invalid invalid unreached-invalid.wast:277 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAARAAAUBRRoLCw== +invalid invalid unreached-invalid.wast:284 AGFzbQEAAAABBAFgAAADAgEACg4BDAACQAALAkABRRoLCw== +invalid invalid unreached-invalid.wast:290 AGFzbQEAAAABBAFgAAADAgEACg8BDQACQAALQwAAgD9FGgs= +invalid invalid unreached-invalid.wast:296 AGFzbQEAAAABBAFgAAADAgEACg8BDQACQAALAkBBAVsaCws= +invalid invalid unreached-invalid.wast:302 AGFzbQEAAAABBAFgAAADAgEAChEBDwACQAALQQFDAAAAAFsaCw== +invalid invalid unreached-invalid.wast:308 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQAJAAAtBAQsL +invalid invalid unreached-invalid.wast:314 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAAn9BAQJAAAtDAAAAAAsL +invalid invalid unreached-invalid.wast:321 AGFzbQEAAAABBAFgAAADAgEAChABDgACQANAAkAAC0EBCwsL +invalid invalid unreached-invalid.wast:327 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AA38CQAALQwAAAAALCw== +invalid invalid unreached-invalid.wast:334 AGFzbQEAAAABBAFgAAADAgEACgoBCAACQAALQQEL +invalid invalid unreached-invalid.wast:340 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAAkAAC0MAAAAACw== +invalid invalid unreached-invalid.wast:348 AGFzbQEAAAABBAFgAAADAgEACg8BDQADQAwACwJAAUUaCws= +invalid invalid unreached-invalid.wast:354 AGFzbQEAAAABBAFgAAADAgEAChABDgADQAwAC0MAAIA/RRoL +invalid invalid unreached-invalid.wast:360 AGFzbQEAAAABBAFgAAADAgEAChABDgADQAwACwJAQQFbGgsL +invalid invalid unreached-invalid.wast:366 AGFzbQEAAAABBAFgAAADAgEAChIBEAADQAwAC0EBQwAAAABbGgs= +invalid invalid unreached-invalid.wast:372 AGFzbQEAAAABBAFgAAADAgEACg4BDAACQANADAALQQELCw== +invalid invalid unreached-invalid.wast:378 AGFzbQEAAAABBQFgAAF/AwIBAAoTAREAAn9BAQNADAALQwAAAAALCw== +invalid invalid unreached-invalid.wast:384 AGFzbQEAAAABBAFgAAADAgEAChEBDwACQANAA0AMAAtBAQsLCw== +invalid invalid unreached-invalid.wast:390 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AA38DQAwAC0MAAAAACws= +invalid invalid unreached-invalid.wast:396 AGFzbQEAAAABBAFgAAADAgEACgsBCQADQAwAC0EBCw== +invalid invalid unreached-invalid.wast:402 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAA0AMAAtDAAAAAAs= +invalid invalid unreached-invalid.wast:409 AGFzbQEAAAABBAFgAAADAgEACgwBCgBBAARAAUUaCws= +invalid invalid unreached-invalid.wast:415 AGFzbQEAAAABBAFgAAADAgEAChABDgBBAARAQwAAgD9FGgsL +invalid invalid unreached-invalid.wast:421 AGFzbQEAAAABBAFgAAADAgEACg0BCwBBAARAQQFbGgsL +invalid invalid unreached-invalid.wast:427 AGFzbQEAAAABBAFgAAADAgEAChIBEABBAARAQQFDAAAAAFsaCws= +invalid invalid unreached-invalid.wast:433 AGFzbQEAAAABBAFgAAADAgEACgsBCQBBAARAQQELCw== +invalid invalid unreached-invalid.wast:439 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAQQAEf0MAAAAACws= +invalid invalid unreached-invalid.wast:445 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAARAAkBBAQsLCw== +invalid invalid unreached-invalid.wast:451 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AQQAEfwJ/QwAAAAALCws= +invalid invalid unreached-invalid.wast:457 AGFzbQEAAAABBAFgAAADAgEACg4BDABBAARAA0BBAQsLCw== +invalid invalid unreached-invalid.wast:463 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AQQAEfwN/QwAAAAALCws= +invalid invalid unreached-invalid.wast:470 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AQQEPRAAAAAAAAPA/Dws= +invalid invalid unreached-invalid.wast:477 AGFzbQEAAAABBQFgAAF/AwIBAAoWARQAAn9BAQwARAAAAAAAAPA/DAALCw== +invalid invalid unreached-invalid.wast:484 AGFzbQEAAAABBAFgAAADAgEACg8BDQACQABDAAAAAA0ACws= +invalid invalid unreached-invalid.wast:490 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAn8CQABBAEEADQELCws= +invalid invalid unreached-invalid.wast:498 AGFzbQEAAAABBQFgAAF/AwIBAAoUARIAAn8CfQBBAEEADQELGkEACws= +invalid invalid unreached-invalid.wast:507 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn8AQQBBAA0AQQALCw== +invalid invalid unreached-invalid.wast:515 AGFzbQEAAAABBAFgAAADAgEAChABDgACQABDAACAPw4AAAsL +invalid invalid unreached-invalid.wast:521 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAAn8AQwAAAABBAQ4AAAsL +invalid invalid unreached-invalid.wast:527 AGFzbQEAAAABBAFgAAADAgEAChMBEQACQAJ9AEEBDgIAAQALGgsL +invalid invalid unreached-invalid.wast:540 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEDAkAACwsL +invalid invalid unreached-invalid.wast:546 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAAkACQAALCws= +invalid invalid unreached-invalid.wast:552 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAAn5CAAJAAAsLCw== +invalid invalid unreached-invalid.wast:558 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAkBBAwJAQgEACwtBCQs= +invalid invalid unreached-invalid.wast:565 AGFzbQEAAAABBAFgAAADAgEACg4BDAACQEEDAkAMAQsLCw== +invalid invalid unreached-invalid.wast:571 AGFzbQEAAAABBQFgAAF/AwIBAAoOAQwAAn8CQEEADAELCws= +invalid invalid unreached-invalid.wast:577 AGFzbQEAAAABBQFgAAF/AwIBAAoQAQ4AAn9CAAJAQQAMAQsLCw== +invalid invalid unreached-invalid.wast:584 AGFzbQEAAAABBAFgAAADAgEAChEBDwACQAJAQQMCQAwCCwsLCw== +invalid invalid unreached-invalid.wast:590 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAn8CQAJAQQAMAgsLCws= +invalid invalid unreached-invalid.wast:596 AGFzbQEAAAABBQFgAAF/AwIBAAoTAREAAn8CfkIAAkBBAAwCCwsLCw== +invalid invalid unreached-invalid.wast:604 AGFzbQEAAAABBQFgAAF/AwIBAAoSARAAAkBBAwJAQgEMAQsLQQkL +invalid invalid unreached-invalid.wast:611 AGFzbQEAAAABBAFgAAADAgEACg0BCwACQEEDAkAPCwsL +invalid invalid unreached-invalid.wast:617 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAAkACQEEADwsLCw== +invalid invalid unreached-invalid.wast:623 AGFzbQEAAAABBQFgAAF/AwIBAAoPAQ0AAn5CAAJAQQAPCwsL +invalid invalid unreached-invalid.wast:629 AGFzbQEAAAABBQFgAAF/AwIBAAoTAREAAkBBAwJAQgFBAA8LC0EJCw== +invalid invalid unreached-invalid.wast:637 AGFzbQEAAAABBAFgAAADAgEACg0BCwADQEEDAkAACwsL +invalid invalid unreached-invalid.wast:643 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAA0ACQAALCws= +invalid invalid unreached-invalid.wast:649 AGFzbQEAAAABBQFgAAF/AwIBAAoNAQsAA35CAAJAAAsLCw== +invalid invalid unreached-invalid.wast:656 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAA0ABDAALCw== +invalid invalid unreached-invalid.wast:662 AGFzbQEAAAABBQFgAAF/AwIBAAoLAQkAA0BBAAwACws= +invalid invalid unreached-invalid.wast:669 AGFzbQEAAAABBAFgAAADAgEACgkBBwEBfwAiAAs= +invalid invalid unreached-invalid.wast:676 AGFzbQEAAAABBQFgAAF/AwIBAAoRAQ8AAn8CQABBAA0BC0EACws= +invalid invalid unreached-invalid.wast:687 AGFzbQEAAAABBQFgAAF+AwIBAAoIAQYAAA0ArQs= +invalid invalid unreached-invalid.wast:699 AGFzbQEAAAABBAFgAAADAgEACg0BCwAAQQFCAUEBGxoL +invalid invalid unreached-invalid.wast:704 AGFzbQEAAAABBAFgAAADAgEACg0BCwAAQgFBAUEBGxoL +invalid invalid unreached-invalid.wast:710 AGFzbQEAAAABBAFgAAADAgEACg0BCwAAQQFBAUIBGxoL +invalid invalid unreached-invalid.wast:715 AGFzbQEAAAABBAFgAAADAgEACgsBCQAAQQFCARsaCw== +invalid invalid unreached-invalid.wast:720 AGFzbQEAAAABBAFgAAADAgEACgkBBwAAQgEbGgs= +invalid invalid unreached-invalid.wast:726 AGFzbQEAAAABBQFgAAF/AwIBAAoKAQgAAEIBQQEbCw== +invalid invalid unreached-invalid.wast:738 AGFzbQEAAAABBgFgAX8BbwMCAQAKFgEUAAJvAn/QbyAADgIAAQALGtBvCws= +valid valid unreached-valid.wast:1 AGFzbQEAAAABEQRgAX8Bf2AAAGAAAX9gAAF+AwgHAAABAgMBAQeXAQcQc2VsZWN0LXRyYXAtbGVmdAAAEXNlbGVjdC10cmFwLXJpZ2h0AAEQc2VsZWN0LXVucmVhY2hlZAACGXNlbGVjdF91bnJlYWNoZWRfcmVzdWx0XzEAAxlzZWxlY3RfdW5yZWFjaGVkX3Jlc3VsdF8yAAQPdW5yZWFjaGFibGUtbnVtAAUPdW5yZWFjaGFibGUtcmVmAAYKUgcIAABBACAAGwsIAEEAACAAGwsgAAAbAEEAGwBBAEEAGwBBAEEAQQAbAEMAAAAAQQAbAAsFAAAbagsJAABCAEEAG3wLBgAAG0UaCwYAABvRGgs= +valid valid unreached-valid.wast:49 AGFzbQEAAAABBAFgAAADAgEABw8BC21lZXQtYm90dG9tAAAKHQEbAAJ8An0AQQEOAgABAQsaRAAAAAAAAAAACxoL +valid valid unwind.wast:3 AGFzbQEAAAABCAJgAABgAAF/AzIxAAABAAEAAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQeUCjEaZnVuYy11bndpbmQtYnktdW5yZWFjaGFibGUAABFmdW5jLXVud2luZC1ieS1icgABF2Z1bmMtdW53aW5kLWJ5LWJyLXZhbHVlAAIUZnVuYy11bndpbmQtYnktYnJfaWYAAxpmdW5jLXVud2luZC1ieS1icl9pZi12YWx1ZQAEF2Z1bmMtdW53aW5kLWJ5LWJyX3RhYmxlAAUdZnVuYy11bndpbmQtYnktYnJfdGFibGUtdmFsdWUABhVmdW5jLXVud2luZC1ieS1yZXR1cm4ABxtibG9jay11bndpbmQtYnktdW5yZWFjaGFibGUACBJibG9jay11bndpbmQtYnktYnIACRhibG9jay11bndpbmQtYnktYnItdmFsdWUAChVibG9jay11bndpbmQtYnktYnJfaWYACxtibG9jay11bndpbmQtYnktYnJfaWYtdmFsdWUADBhibG9jay11bndpbmQtYnktYnJfdGFibGUADR5ibG9jay11bndpbmQtYnktYnJfdGFibGUtdmFsdWUADhZibG9jay11bndpbmQtYnktcmV0dXJuAA8iYmxvY2stbmVzdGVkLXVud2luZC1ieS11bnJlYWNoYWJsZQAQGWJsb2NrLW5lc3RlZC11bndpbmQtYnktYnIAER9ibG9jay1uZXN0ZWQtdW53aW5kLWJ5LWJyLXZhbHVlABIcYmxvY2stbmVzdGVkLXVud2luZC1ieS1icl9pZgATImJsb2NrLW5lc3RlZC11bndpbmQtYnktYnJfaWYtdmFsdWUAFB9ibG9jay1uZXN0ZWQtdW53aW5kLWJ5LWJyX3RhYmxlABUlYmxvY2stbmVzdGVkLXVud2luZC1ieS1icl90YWJsZS12YWx1ZQAWHWJsb2NrLW5lc3RlZC11bndpbmQtYnktcmV0dXJuABcXdW5hcnktYWZ0ZXItdW5yZWFjaGFibGUAGA51bmFyeS1hZnRlci1icgAZEXVuYXJ5LWFmdGVyLWJyX2lmABoUdW5hcnktYWZ0ZXItYnJfdGFibGUAGxJ1bmFyeS1hZnRlci1yZXR1cm4AHBhiaW5hcnktYWZ0ZXItdW5yZWFjaGFibGUAHQ9iaW5hcnktYWZ0ZXItYnIAHhJiaW5hcnktYWZ0ZXItYnJfaWYAHxViaW5hcnktYWZ0ZXItYnJfdGFibGUAIBNiaW5hcnktYWZ0ZXItcmV0dXJuACEYc2VsZWN0LWFmdGVyLXVucmVhY2hhYmxlACIPc2VsZWN0LWFmdGVyLWJyACMSc2VsZWN0LWFmdGVyLWJyX2lmACQVc2VsZWN0LWFmdGVyLWJyX3RhYmxlACUTc2VsZWN0LWFmdGVyLXJldHVybgAmHWJsb2NrLXZhbHVlLWFmdGVyLXVucmVhY2hhYmxlACcUYmxvY2stdmFsdWUtYWZ0ZXItYnIAKBdibG9jay12YWx1ZS1hZnRlci1icl9pZgApGmJsb2NrLXZhbHVlLWFmdGVyLWJyX3RhYmxlACoYYmxvY2stdmFsdWUtYWZ0ZXItcmV0dXJuACscbG9vcC12YWx1ZS1hZnRlci11bnJlYWNoYWJsZQAsE2xvb3AtdmFsdWUtYWZ0ZXItYnIALRZsb29wLXZhbHVlLWFmdGVyLWJyX2lmAC4ZbG9vcC12YWx1ZS1hZnRlci1icl90YWJsZQAvF2xvb3AtdmFsdWUtYWZ0ZXItcmV0dXJuADAKvAYxBwBBA0IBAAsIAEEDQgEMAAsKAEEDQgFBCQwACwwAQQNCAUEBDQAaGgsOAEEDQgFBCUEBDQAaGgsLAEEDQgFBAA4AAAsNAEEDQgFBCUEADgAACwkAQQNCAUEJDwsKAAJAQQNCAQALCw0AAkBBA0IBDAALQQkLDQACf0EDQgFBCQwACwsRAAJAQQNCAUEBDQAaGgtBCQsRAAJ/QQNCAUEJQQENABoaCwsQAAJAQQNCAUEADgAAC0EJCxAAAn9BA0IBQQlBAA4AAAsLDAACf0EDQgFBCQ8LCw0AAn9BAwJAQgEACwsLEQACQEEDAkBCAQwBCxoLQQkLEAACf0EDAkBCAUEJDAELCwsUAAJAQQMCQEIBQQENARoLGgtBCQsUAAJ/QQMCQEIBQQlBAQ0BGhoLCwsUAAJAQQMCQEIBQQEOAAELGgtBCQsTAAJ/QQMCQEIBQQlBAQ4AAQsLCw8AAn9BAwJAQgFBCQ8LCwsJAEMAAAAAAFALDwACf0MAAAAAQQkMAFALCw8AAn9CAEEJQQENABpQCwsTAAJ/QwAAAABBCUEADgEAAFALCwsAQwAAAABBCQ9QCxIAQwAAAABEAAAAAAAA8D8AUQsYAAJ/QwAAAABEAAAAAAAA8D9BCQwAUQsLEQACf0IAQgFBCUEBDQAaUQsLGwACf0MAAAAARAAAAAAAAPA/QQlBAA4AAFELCxQAQwAAAABEAAAAAAAA8D9BCQ9RCxQAQwAAAABEAAAAAAAA8D9CAAAbCxoAAn9DAAAAAEQAAAAAAADwP0IAQQkMABsLCxMAAn9BAEEBQQBBCUEBDQAaGwsLHQACf0MAAAAARAAAAAAAAPA/QgBBCUEADgAAGwsLFgBDAAAAAEQAAAAAAADwP0IBQQkPGwsLAAJ/QwAAAAAACwsOAAJ/QwAAAABBCQwACwsOAAJ/QQBBCUEBDQAaCwsSAAJ/QwAAAABBCUEADgEAAAsLDQACf0MAAAAAQQkPCwsLAAN/QwAAAAAACwsRAAJ/A39DAAAAAEEJDAELCwsRAAJ/A39BAEEJQQENARoLCwsVAAJ/A39DAAAAAEEJQQAOAQEBCwsLDQADf0MAAAAAQQkPCws= +malformed invalid utf8-custom-section-id.wast:7 AGFzbQEAAAAAAgGA +malformed invalid utf8-custom-section-id.wast:17 AGFzbQEAAAAAAgGP +malformed invalid utf8-custom-section-id.wast:27 AGFzbQEAAAAAAgGQ +malformed invalid utf8-custom-section-id.wast:37 AGFzbQEAAAAAAgGf +malformed invalid utf8-custom-section-id.wast:47 AGFzbQEAAAAAAgGg +malformed invalid utf8-custom-section-id.wast:57 AGFzbQEAAAAAAgG/ +malformed invalid utf8-custom-section-id.wast:69 AGFzbQEAAAAABAPCgIA= +malformed invalid utf8-custom-section-id.wast:79 AGFzbQEAAAAAAgHC +malformed invalid utf8-custom-section-id.wast:89 AGFzbQEAAAAAAwLCLg== +malformed invalid utf8-custom-section-id.wast:101 AGFzbQEAAAAAAwLAgA== +malformed invalid utf8-custom-section-id.wast:111 AGFzbQEAAAAAAwLAvw== +malformed invalid utf8-custom-section-id.wast:121 AGFzbQEAAAAAAwLBgA== +malformed invalid utf8-custom-section-id.wast:131 AGFzbQEAAAAAAwLBvw== +malformed invalid utf8-custom-section-id.wast:141 AGFzbQEAAAAAAwLCAA== +malformed invalid utf8-custom-section-id.wast:151 AGFzbQEAAAAAAwLCfw== +malformed invalid utf8-custom-section-id.wast:161 AGFzbQEAAAAAAwLCwA== +malformed invalid utf8-custom-section-id.wast:171 AGFzbQEAAAAAAwLC/Q== +malformed invalid utf8-custom-section-id.wast:181 AGFzbQEAAAAAAwLfAA== +malformed invalid utf8-custom-section-id.wast:191 AGFzbQEAAAAAAwLffw== +malformed invalid utf8-custom-section-id.wast:201 AGFzbQEAAAAAAwLfwA== +malformed invalid utf8-custom-section-id.wast:211 AGFzbQEAAAAAAwLf/Q== +malformed invalid utf8-custom-section-id.wast:223 AGFzbQEAAAAABQThgICA +malformed invalid utf8-custom-section-id.wast:233 AGFzbQEAAAAAAwLhgA== +malformed invalid utf8-custom-section-id.wast:243 AGFzbQEAAAAABAPhgC4= +malformed invalid utf8-custom-section-id.wast:253 AGFzbQEAAAAAAgHh +malformed invalid utf8-custom-section-id.wast:263 AGFzbQEAAAAAAwLhLg== +malformed invalid utf8-custom-section-id.wast:275 AGFzbQEAAAAABAPgAKA= +malformed invalid utf8-custom-section-id.wast:285 AGFzbQEAAAAABAPgf6A= +malformed invalid utf8-custom-section-id.wast:295 AGFzbQEAAAAABAPggIA= +malformed invalid utf8-custom-section-id.wast:305 AGFzbQEAAAAABAPggKA= +malformed invalid utf8-custom-section-id.wast:315 AGFzbQEAAAAABAPgn6A= +malformed invalid utf8-custom-section-id.wast:325 AGFzbQEAAAAABAPgn78= +malformed invalid utf8-custom-section-id.wast:335 AGFzbQEAAAAABAPgwKA= +malformed invalid utf8-custom-section-id.wast:345 AGFzbQEAAAAABAPg/aA= +malformed invalid utf8-custom-section-id.wast:355 AGFzbQEAAAAABAPhAIA= +malformed invalid utf8-custom-section-id.wast:365 AGFzbQEAAAAABAPhf4A= +malformed invalid utf8-custom-section-id.wast:375 AGFzbQEAAAAABAPhwIA= +malformed invalid utf8-custom-section-id.wast:385 AGFzbQEAAAAABAPh/YA= +malformed invalid utf8-custom-section-id.wast:395 AGFzbQEAAAAABAPsAIA= +malformed invalid utf8-custom-section-id.wast:405 AGFzbQEAAAAABAPsf4A= +malformed invalid utf8-custom-section-id.wast:415 AGFzbQEAAAAABAPswIA= +malformed invalid utf8-custom-section-id.wast:425 AGFzbQEAAAAABAPs/YA= +malformed invalid utf8-custom-section-id.wast:435 AGFzbQEAAAAABAPtAIA= +malformed invalid utf8-custom-section-id.wast:445 AGFzbQEAAAAABAPtf4A= +malformed invalid utf8-custom-section-id.wast:455 AGFzbQEAAAAABAPtoIA= +malformed invalid utf8-custom-section-id.wast:465 AGFzbQEAAAAABAPtoL8= +malformed invalid utf8-custom-section-id.wast:475 AGFzbQEAAAAABAPtv4A= +malformed invalid utf8-custom-section-id.wast:485 AGFzbQEAAAAABAPtv78= +malformed invalid utf8-custom-section-id.wast:495 AGFzbQEAAAAABAPtwIA= +malformed invalid utf8-custom-section-id.wast:505 AGFzbQEAAAAABAPt/YA= +malformed invalid utf8-custom-section-id.wast:515 AGFzbQEAAAAABAPuAIA= +malformed invalid utf8-custom-section-id.wast:525 AGFzbQEAAAAABAPuf4A= +malformed invalid utf8-custom-section-id.wast:535 AGFzbQEAAAAABAPuwIA= +malformed invalid utf8-custom-section-id.wast:545 AGFzbQEAAAAABAPu/YA= +malformed invalid utf8-custom-section-id.wast:555 AGFzbQEAAAAABAPvAIA= +malformed invalid utf8-custom-section-id.wast:565 AGFzbQEAAAAABAPvf4A= +malformed invalid utf8-custom-section-id.wast:575 AGFzbQEAAAAABAPvwIA= +malformed invalid utf8-custom-section-id.wast:585 AGFzbQEAAAAABAPv/YA= +malformed invalid utf8-custom-section-id.wast:597 AGFzbQEAAAAABAPgoAA= +malformed invalid utf8-custom-section-id.wast:607 AGFzbQEAAAAABAPgoH8= +malformed invalid utf8-custom-section-id.wast:617 AGFzbQEAAAAABAPgoMA= +malformed invalid utf8-custom-section-id.wast:627 AGFzbQEAAAAABAPgoP0= +malformed invalid utf8-custom-section-id.wast:637 AGFzbQEAAAAABAPhgAA= +malformed invalid utf8-custom-section-id.wast:647 AGFzbQEAAAAABAPhgH8= +malformed invalid utf8-custom-section-id.wast:657 AGFzbQEAAAAABAPhgMA= +malformed invalid utf8-custom-section-id.wast:667 AGFzbQEAAAAABAPhgP0= +malformed invalid utf8-custom-section-id.wast:677 AGFzbQEAAAAABAPsgAA= +malformed invalid utf8-custom-section-id.wast:687 AGFzbQEAAAAABAPsgH8= +malformed invalid utf8-custom-section-id.wast:697 AGFzbQEAAAAABAPsgMA= +malformed invalid utf8-custom-section-id.wast:707 AGFzbQEAAAAABAPsgP0= +malformed invalid utf8-custom-section-id.wast:717 AGFzbQEAAAAABAPtgAA= +malformed invalid utf8-custom-section-id.wast:727 AGFzbQEAAAAABAPtgH8= +malformed invalid utf8-custom-section-id.wast:737 AGFzbQEAAAAABAPtgMA= +malformed invalid utf8-custom-section-id.wast:747 AGFzbQEAAAAABAPtgP0= +malformed invalid utf8-custom-section-id.wast:757 AGFzbQEAAAAABAPugAA= +malformed invalid utf8-custom-section-id.wast:767 AGFzbQEAAAAABAPugH8= +malformed invalid utf8-custom-section-id.wast:777 AGFzbQEAAAAABAPugMA= +malformed invalid utf8-custom-section-id.wast:787 AGFzbQEAAAAABAPugP0= +malformed invalid utf8-custom-section-id.wast:797 AGFzbQEAAAAABAPvgAA= +malformed invalid utf8-custom-section-id.wast:807 AGFzbQEAAAAABAPvgH8= +malformed invalid utf8-custom-section-id.wast:817 AGFzbQEAAAAABAPvgMA= +malformed invalid utf8-custom-section-id.wast:827 AGFzbQEAAAAABAPvgP0= +malformed invalid utf8-custom-section-id.wast:839 AGFzbQEAAAAABgXxgICAgA== +malformed invalid utf8-custom-section-id.wast:849 AGFzbQEAAAAABAPxgIA= +malformed invalid utf8-custom-section-id.wast:859 AGFzbQEAAAAABQTxgIAj +malformed invalid utf8-custom-section-id.wast:869 AGFzbQEAAAAAAwLxgA== +malformed invalid utf8-custom-section-id.wast:879 AGFzbQEAAAAABAPxgCM= +malformed invalid utf8-custom-section-id.wast:889 AGFzbQEAAAAAAgHx +malformed invalid utf8-custom-section-id.wast:899 AGFzbQEAAAAAAwLxIw== +malformed invalid utf8-custom-section-id.wast:911 AGFzbQEAAAAABQTwAJCQ +malformed invalid utf8-custom-section-id.wast:921 AGFzbQEAAAAABQTwf5CQ +malformed invalid utf8-custom-section-id.wast:931 AGFzbQEAAAAABQTwgICA +malformed invalid utf8-custom-section-id.wast:941 AGFzbQEAAAAABQTwgJCQ +malformed invalid utf8-custom-section-id.wast:951 AGFzbQEAAAAABQTwj5CQ +malformed invalid utf8-custom-section-id.wast:961 AGFzbQEAAAAABQTwj7+/ +malformed invalid utf8-custom-section-id.wast:971 AGFzbQEAAAAABQTwwJCQ +malformed invalid utf8-custom-section-id.wast:981 AGFzbQEAAAAABQTw/ZCQ +malformed invalid utf8-custom-section-id.wast:991 AGFzbQEAAAAABQTxAICA +malformed invalid utf8-custom-section-id.wast:1001 AGFzbQEAAAAABQTxf4CA +malformed invalid utf8-custom-section-id.wast:1011 AGFzbQEAAAAABQTxwICA +malformed invalid utf8-custom-section-id.wast:1021 AGFzbQEAAAAABQTx/YCA +malformed invalid utf8-custom-section-id.wast:1031 AGFzbQEAAAAABQTzAICA +malformed invalid utf8-custom-section-id.wast:1041 AGFzbQEAAAAABQTzf4CA +malformed invalid utf8-custom-section-id.wast:1051 AGFzbQEAAAAABQTzwICA +malformed invalid utf8-custom-section-id.wast:1061 AGFzbQEAAAAABQTz/YCA +malformed invalid utf8-custom-section-id.wast:1071 AGFzbQEAAAAABQT0AICA +malformed invalid utf8-custom-section-id.wast:1081 AGFzbQEAAAAABQT0f4CA +malformed invalid utf8-custom-section-id.wast:1091 AGFzbQEAAAAABQT0kICA +malformed invalid utf8-custom-section-id.wast:1101 AGFzbQEAAAAABQT0v4CA +malformed invalid utf8-custom-section-id.wast:1111 AGFzbQEAAAAABQT0wICA +malformed invalid utf8-custom-section-id.wast:1121 AGFzbQEAAAAABQT0/YCA +malformed invalid utf8-custom-section-id.wast:1131 AGFzbQEAAAAABQT1gICA +malformed invalid utf8-custom-section-id.wast:1141 AGFzbQEAAAAABQT3gICA +malformed invalid utf8-custom-section-id.wast:1151 AGFzbQEAAAAABQT3v7+/ +malformed invalid utf8-custom-section-id.wast:1163 AGFzbQEAAAAABQTwkACQ +malformed invalid utf8-custom-section-id.wast:1173 AGFzbQEAAAAABQTwkH+Q +malformed invalid utf8-custom-section-id.wast:1183 AGFzbQEAAAAABQTwkMCQ +malformed invalid utf8-custom-section-id.wast:1193 AGFzbQEAAAAABQTwkP2Q +malformed invalid utf8-custom-section-id.wast:1203 AGFzbQEAAAAABQTxgACA +malformed invalid utf8-custom-section-id.wast:1213 AGFzbQEAAAAABQTxgH+A +malformed invalid utf8-custom-section-id.wast:1223 AGFzbQEAAAAABQTxgMCA +malformed invalid utf8-custom-section-id.wast:1233 AGFzbQEAAAAABQTxgP2A +malformed invalid utf8-custom-section-id.wast:1243 AGFzbQEAAAAABQTzgACA +malformed invalid utf8-custom-section-id.wast:1253 AGFzbQEAAAAABQTzgH+A +malformed invalid utf8-custom-section-id.wast:1263 AGFzbQEAAAAABQTzgMCA +malformed invalid utf8-custom-section-id.wast:1273 AGFzbQEAAAAABQTzgP2A +malformed invalid utf8-custom-section-id.wast:1283 AGFzbQEAAAAABQT0gACA +malformed invalid utf8-custom-section-id.wast:1293 AGFzbQEAAAAABQT0gH+A +malformed invalid utf8-custom-section-id.wast:1303 AGFzbQEAAAAABQT0gMCA +malformed invalid utf8-custom-section-id.wast:1313 AGFzbQEAAAAABQT0gP2A +malformed invalid utf8-custom-section-id.wast:1325 AGFzbQEAAAAABQTwkJAA +malformed invalid utf8-custom-section-id.wast:1335 AGFzbQEAAAAABQTwkJB/ +malformed invalid utf8-custom-section-id.wast:1345 AGFzbQEAAAAABQTwkJDA +malformed invalid utf8-custom-section-id.wast:1355 AGFzbQEAAAAABQTwkJD9 +malformed invalid utf8-custom-section-id.wast:1365 AGFzbQEAAAAABQTxgIAA +malformed invalid utf8-custom-section-id.wast:1375 AGFzbQEAAAAABQTxgIB/ +malformed invalid utf8-custom-section-id.wast:1385 AGFzbQEAAAAABQTxgIDA +malformed invalid utf8-custom-section-id.wast:1395 AGFzbQEAAAAABQTxgID9 +malformed invalid utf8-custom-section-id.wast:1405 AGFzbQEAAAAABQTzgIAA +malformed invalid utf8-custom-section-id.wast:1415 AGFzbQEAAAAABQTzgIB/ +malformed invalid utf8-custom-section-id.wast:1425 AGFzbQEAAAAABQTzgIDA +malformed invalid utf8-custom-section-id.wast:1435 AGFzbQEAAAAABQTzgID9 +malformed invalid utf8-custom-section-id.wast:1445 AGFzbQEAAAAABQT0gIAA +malformed invalid utf8-custom-section-id.wast:1455 AGFzbQEAAAAABQT0gIB/ +malformed invalid utf8-custom-section-id.wast:1465 AGFzbQEAAAAABQT0gIDA +malformed invalid utf8-custom-section-id.wast:1475 AGFzbQEAAAAABQT0gID9 +malformed invalid utf8-custom-section-id.wast:1487 AGFzbQEAAAAABwb4gICAgIA= +malformed invalid utf8-custom-section-id.wast:1497 AGFzbQEAAAAABQT4gICA +malformed invalid utf8-custom-section-id.wast:1507 AGFzbQEAAAAABgX4gICAIw== +malformed invalid utf8-custom-section-id.wast:1517 AGFzbQEAAAAABAP4gIA= +malformed invalid utf8-custom-section-id.wast:1527 AGFzbQEAAAAABQT4gIAj +malformed invalid utf8-custom-section-id.wast:1537 AGFzbQEAAAAAAwL4gA== +malformed invalid utf8-custom-section-id.wast:1547 AGFzbQEAAAAABAP4gCM= +malformed invalid utf8-custom-section-id.wast:1557 AGFzbQEAAAAAAgH4 +malformed invalid utf8-custom-section-id.wast:1567 AGFzbQEAAAAAAwL4Iw== +malformed invalid utf8-custom-section-id.wast:1579 AGFzbQEAAAAABgX4gICAgA== +malformed invalid utf8-custom-section-id.wast:1589 AGFzbQEAAAAABgX7v7+/vw== +malformed invalid utf8-custom-section-id.wast:1601 AGFzbQEAAAAACAf8gICAgICA +malformed invalid utf8-custom-section-id.wast:1611 AGFzbQEAAAAABgX8gICAgA== +malformed invalid utf8-custom-section-id.wast:1621 AGFzbQEAAAAABwb8gICAgCM= +malformed invalid utf8-custom-section-id.wast:1631 AGFzbQEAAAAABQT8gICA +malformed invalid utf8-custom-section-id.wast:1641 AGFzbQEAAAAABgX8gICAIw== +malformed invalid utf8-custom-section-id.wast:1651 AGFzbQEAAAAABAP8gIA= +malformed invalid utf8-custom-section-id.wast:1661 AGFzbQEAAAAABQT8gIAj +malformed invalid utf8-custom-section-id.wast:1671 AGFzbQEAAAAAAwL8gA== +malformed invalid utf8-custom-section-id.wast:1681 AGFzbQEAAAAABAP8gCM= +malformed invalid utf8-custom-section-id.wast:1691 AGFzbQEAAAAAAgH8 +malformed invalid utf8-custom-section-id.wast:1701 AGFzbQEAAAAAAwL8Iw== +malformed invalid utf8-custom-section-id.wast:1713 AGFzbQEAAAAABwb8gICAgIA= +malformed invalid utf8-custom-section-id.wast:1723 AGFzbQEAAAAABwb9v7+/v78= +malformed invalid utf8-custom-section-id.wast:1735 AGFzbQEAAAAAAgH+ +malformed invalid utf8-custom-section-id.wast:1745 AGFzbQEAAAAAAgH/ +malformed invalid utf8-custom-section-id.wast:1755 AGFzbQEAAAAAAwL+/w== +malformed invalid utf8-custom-section-id.wast:1765 AGFzbQEAAAAABQQAAP7/ +malformed invalid utf8-custom-section-id.wast:1775 AGFzbQEAAAAAAwL//g== +malformed invalid utf8-custom-section-id.wast:1785 AGFzbQEAAAAABQT//gAA +malformed invalid utf8-import-field.wast:7 AGFzbQEAAAACCwEBgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:22 AGFzbQEAAAACCwEBjwR0ZXN0A38A +malformed invalid utf8-import-field.wast:37 AGFzbQEAAAACCwEBkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:52 AGFzbQEAAAACCwEBnwR0ZXN0A38A +malformed invalid utf8-import-field.wast:67 AGFzbQEAAAACCwEBoAR0ZXN0A38A +malformed invalid utf8-import-field.wast:82 AGFzbQEAAAACCwEBvwR0ZXN0A38A +malformed invalid utf8-import-field.wast:99 AGFzbQEAAAACDQEDwoCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:114 AGFzbQEAAAACCwEBwgR0ZXN0A38A +malformed invalid utf8-import-field.wast:129 AGFzbQEAAAACDAECwi4EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:146 AGFzbQEAAAACDAECwIAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:161 AGFzbQEAAAACDAECwL8EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:176 AGFzbQEAAAACDAECwYAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:191 AGFzbQEAAAACDAECwb8EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:206 AGFzbQEAAAACDAECwgAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:221 AGFzbQEAAAACDAECwn8EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:236 AGFzbQEAAAACDAECwsAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:251 AGFzbQEAAAACDAECwv0EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:266 AGFzbQEAAAACDAEC3wAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:281 AGFzbQEAAAACDAEC338EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:296 AGFzbQEAAAACDAEC38AEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:311 AGFzbQEAAAACDAEC3/0EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:328 AGFzbQEAAAACDgEE4YCAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:343 AGFzbQEAAAACDAEC4YAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:358 AGFzbQEAAAACDQED4YAuBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:373 AGFzbQEAAAACCwEB4QR0ZXN0A38A +malformed invalid utf8-import-field.wast:388 AGFzbQEAAAACDAEC4S4EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:405 AGFzbQEAAAACDQED4ACgBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:420 AGFzbQEAAAACDQED4H+gBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:435 AGFzbQEAAAACDQED4ICABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:450 AGFzbQEAAAACDQED4ICgBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:465 AGFzbQEAAAACDQED4J+gBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:480 AGFzbQEAAAACDQED4J+/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:495 AGFzbQEAAAACDQED4MCgBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:510 AGFzbQEAAAACDQED4P2gBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:525 AGFzbQEAAAACDQED4QCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:540 AGFzbQEAAAACDQED4X+ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:555 AGFzbQEAAAACDQED4cCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:570 AGFzbQEAAAACDQED4f2ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:585 AGFzbQEAAAACDQED7ACABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:600 AGFzbQEAAAACDQED7H+ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:615 AGFzbQEAAAACDQED7MCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:630 AGFzbQEAAAACDQED7P2ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:645 AGFzbQEAAAACDQED7QCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:660 AGFzbQEAAAACDQED7X+ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:675 AGFzbQEAAAACDQED7aCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:690 AGFzbQEAAAACDQED7aC/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:705 AGFzbQEAAAACDQED7b+ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:720 AGFzbQEAAAACDQED7b+/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:735 AGFzbQEAAAACDQED7cCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:750 AGFzbQEAAAACDQED7f2ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:765 AGFzbQEAAAACDQED7gCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:780 AGFzbQEAAAACDQED7n+ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:795 AGFzbQEAAAACDQED7sCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:810 AGFzbQEAAAACDQED7v2ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:825 AGFzbQEAAAACDQED7wCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:840 AGFzbQEAAAACDQED73+ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:855 AGFzbQEAAAACDQED78CABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:870 AGFzbQEAAAACDQED7/2ABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:887 AGFzbQEAAAACDQED4KAABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:902 AGFzbQEAAAACDQED4KB/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:917 AGFzbQEAAAACDQED4KDABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:932 AGFzbQEAAAACDQED4KD9BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:947 AGFzbQEAAAACDQED4YAABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:962 AGFzbQEAAAACDQED4YB/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:977 AGFzbQEAAAACDQED4YDABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:992 AGFzbQEAAAACDQED4YD9BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1007 AGFzbQEAAAACDQED7IAABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1022 AGFzbQEAAAACDQED7IB/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1037 AGFzbQEAAAACDQED7IDABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1052 AGFzbQEAAAACDQED7ID9BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1067 AGFzbQEAAAACDQED7YAABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1082 AGFzbQEAAAACDQED7YB/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1097 AGFzbQEAAAACDQED7YDABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1112 AGFzbQEAAAACDQED7YD9BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1127 AGFzbQEAAAACDQED7oAABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1142 AGFzbQEAAAACDQED7oB/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1157 AGFzbQEAAAACDQED7oDABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1172 AGFzbQEAAAACDQED7oD9BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1187 AGFzbQEAAAACDQED74AABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1202 AGFzbQEAAAACDQED74B/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1217 AGFzbQEAAAACDQED74DABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1232 AGFzbQEAAAACDQED74D9BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1249 AGFzbQEAAAACDwEF8YCAgIAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:1264 AGFzbQEAAAACDQED8YCABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1279 AGFzbQEAAAACDgEE8YCAIwR0ZXN0A38A +malformed invalid utf8-import-field.wast:1294 AGFzbQEAAAACDAEC8YAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:1309 AGFzbQEAAAACDQED8YAjBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:1324 AGFzbQEAAAACCwEB8QR0ZXN0A38A +malformed invalid utf8-import-field.wast:1339 AGFzbQEAAAACDAEC8SMEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:1356 AGFzbQEAAAACDgEE8ACQkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1371 AGFzbQEAAAACDgEE8H+QkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1386 AGFzbQEAAAACDgEE8ICAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1401 AGFzbQEAAAACDgEE8ICQkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1416 AGFzbQEAAAACDgEE8I+QkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1431 AGFzbQEAAAACDgEE8I+/vwR0ZXN0A38A +malformed invalid utf8-import-field.wast:1446 AGFzbQEAAAACDgEE8MCQkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1461 AGFzbQEAAAACDgEE8P2QkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1476 AGFzbQEAAAACDgEE8QCAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1491 AGFzbQEAAAACDgEE8X+AgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1506 AGFzbQEAAAACDgEE8cCAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1521 AGFzbQEAAAACDgEE8f2AgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1536 AGFzbQEAAAACDgEE8wCAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1551 AGFzbQEAAAACDgEE83+AgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1566 AGFzbQEAAAACDgEE88CAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1581 AGFzbQEAAAACDgEE8/2AgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1596 AGFzbQEAAAACDgEE9ACAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1611 AGFzbQEAAAACDgEE9H+AgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1626 AGFzbQEAAAACDgEE9JCAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1641 AGFzbQEAAAACDgEE9L+AgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1656 AGFzbQEAAAACDgEE9MCAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1671 AGFzbQEAAAACDgEE9P2AgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1686 AGFzbQEAAAACDgEE9YCAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1701 AGFzbQEAAAACDgEE94CAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1716 AGFzbQEAAAACDgEE97+/vwR0ZXN0A38A +malformed invalid utf8-import-field.wast:1733 AGFzbQEAAAACDgEE8JAAkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1748 AGFzbQEAAAACDgEE8JB/kAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1763 AGFzbQEAAAACDgEE8JDAkAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1778 AGFzbQEAAAACDgEE8JD9kAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1793 AGFzbQEAAAACDgEE8YAAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1808 AGFzbQEAAAACDgEE8YB/gAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1823 AGFzbQEAAAACDgEE8YDAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1838 AGFzbQEAAAACDgEE8YD9gAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1853 AGFzbQEAAAACDgEE84AAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1868 AGFzbQEAAAACDgEE84B/gAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1883 AGFzbQEAAAACDgEE84DAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1898 AGFzbQEAAAACDgEE84D9gAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1913 AGFzbQEAAAACDgEE9IAAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1928 AGFzbQEAAAACDgEE9IB/gAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1943 AGFzbQEAAAACDgEE9IDAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1958 AGFzbQEAAAACDgEE9ID9gAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1975 AGFzbQEAAAACDgEE8JCQAAR0ZXN0A38A +malformed invalid utf8-import-field.wast:1990 AGFzbQEAAAACDgEE8JCQfwR0ZXN0A38A +malformed invalid utf8-import-field.wast:2005 AGFzbQEAAAACDgEE8JCQwAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2020 AGFzbQEAAAACDgEE8JCQ/QR0ZXN0A38A +malformed invalid utf8-import-field.wast:2035 AGFzbQEAAAACDgEE8YCAAAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2050 AGFzbQEAAAACDgEE8YCAfwR0ZXN0A38A +malformed invalid utf8-import-field.wast:2065 AGFzbQEAAAACDgEE8YCAwAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2080 AGFzbQEAAAACDgEE8YCA/QR0ZXN0A38A +malformed invalid utf8-import-field.wast:2095 AGFzbQEAAAACDgEE84CAAAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2110 AGFzbQEAAAACDgEE84CAfwR0ZXN0A38A +malformed invalid utf8-import-field.wast:2125 AGFzbQEAAAACDgEE84CAwAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2140 AGFzbQEAAAACDgEE84CA/QR0ZXN0A38A +malformed invalid utf8-import-field.wast:2155 AGFzbQEAAAACDgEE9ICAAAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2170 AGFzbQEAAAACDgEE9ICAfwR0ZXN0A38A +malformed invalid utf8-import-field.wast:2185 AGFzbQEAAAACDgEE9ICAwAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2200 AGFzbQEAAAACDgEE9ICA/QR0ZXN0A38A +malformed invalid utf8-import-field.wast:2217 AGFzbQEAAAACEAEG+ICAgICABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2232 AGFzbQEAAAACDgEE+ICAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2247 AGFzbQEAAAACDwEF+ICAgCMEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2262 AGFzbQEAAAACDQED+ICABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2277 AGFzbQEAAAACDgEE+ICAIwR0ZXN0A38A +malformed invalid utf8-import-field.wast:2292 AGFzbQEAAAACDAEC+IAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2307 AGFzbQEAAAACDQED+IAjBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2322 AGFzbQEAAAACCwEB+AR0ZXN0A38A +malformed invalid utf8-import-field.wast:2337 AGFzbQEAAAACDAEC+CMEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2354 AGFzbQEAAAACDwEF+ICAgIAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2369 AGFzbQEAAAACDwEF+7+/v78EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2386 AGFzbQEAAAACEQEH/ICAgICAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2401 AGFzbQEAAAACDwEF/ICAgIAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2416 AGFzbQEAAAACEAEG/ICAgIAjBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2431 AGFzbQEAAAACDgEE/ICAgAR0ZXN0A38A +malformed invalid utf8-import-field.wast:2446 AGFzbQEAAAACDwEF/ICAgCMEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2461 AGFzbQEAAAACDQED/ICABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2476 AGFzbQEAAAACDgEE/ICAIwR0ZXN0A38A +malformed invalid utf8-import-field.wast:2491 AGFzbQEAAAACDAEC/IAEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2506 AGFzbQEAAAACDQED/IAjBHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2521 AGFzbQEAAAACCwEB/AR0ZXN0A38A +malformed invalid utf8-import-field.wast:2536 AGFzbQEAAAACDAEC/CMEdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2553 AGFzbQEAAAACEAEG/ICAgICABHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2568 AGFzbQEAAAACEAEG/b+/v7+/BHRlc3QDfwA= +malformed invalid utf8-import-field.wast:2585 AGFzbQEAAAACCwEB/gR0ZXN0A38A +malformed invalid utf8-import-field.wast:2600 AGFzbQEAAAACCwEB/wR0ZXN0A38A +malformed invalid utf8-import-field.wast:2615 AGFzbQEAAAACDAEC/v8EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2630 AGFzbQEAAAACDgEEAAD+/wR0ZXN0A38A +malformed invalid utf8-import-field.wast:2645 AGFzbQEAAAACDAEC//4EdGVzdAN/AA== +malformed invalid utf8-import-field.wast:2660 AGFzbQEAAAACDgEE//4AAAR0ZXN0A38A +malformed invalid utf8-import-module.wast:7 AGFzbQEAAAACCwEEdGVzdAGAA38A +malformed invalid utf8-import-module.wast:22 AGFzbQEAAAACCwEEdGVzdAGPA38A +malformed invalid utf8-import-module.wast:37 AGFzbQEAAAACCwEEdGVzdAGQA38A +malformed invalid utf8-import-module.wast:52 AGFzbQEAAAACCwEEdGVzdAGfA38A +malformed invalid utf8-import-module.wast:67 AGFzbQEAAAACCwEEdGVzdAGgA38A +malformed invalid utf8-import-module.wast:82 AGFzbQEAAAACCwEEdGVzdAG/A38A +malformed invalid utf8-import-module.wast:99 AGFzbQEAAAACDQEEdGVzdAPCgIADfwA= +malformed invalid utf8-import-module.wast:114 AGFzbQEAAAACCwEEdGVzdAHCA38A +malformed invalid utf8-import-module.wast:129 AGFzbQEAAAACDAEEdGVzdALCLgN/AA== +malformed invalid utf8-import-module.wast:146 AGFzbQEAAAACDAEEdGVzdALAgAN/AA== +malformed invalid utf8-import-module.wast:161 AGFzbQEAAAACDAEEdGVzdALAvwN/AA== +malformed invalid utf8-import-module.wast:176 AGFzbQEAAAACDAEEdGVzdALBgAN/AA== +malformed invalid utf8-import-module.wast:191 AGFzbQEAAAACDAEEdGVzdALBvwN/AA== +malformed invalid utf8-import-module.wast:206 AGFzbQEAAAACDAEEdGVzdALCAAN/AA== +malformed invalid utf8-import-module.wast:221 AGFzbQEAAAACDAEEdGVzdALCfwN/AA== +malformed invalid utf8-import-module.wast:236 AGFzbQEAAAACDAEEdGVzdALCwAN/AA== +malformed invalid utf8-import-module.wast:251 AGFzbQEAAAACDAEEdGVzdALC/QN/AA== +malformed invalid utf8-import-module.wast:266 AGFzbQEAAAACDAEEdGVzdALfAAN/AA== +malformed invalid utf8-import-module.wast:281 AGFzbQEAAAACDAEEdGVzdALffwN/AA== +malformed invalid utf8-import-module.wast:296 AGFzbQEAAAACDAEEdGVzdALfwAN/AA== +malformed invalid utf8-import-module.wast:311 AGFzbQEAAAACDAEEdGVzdALf/QN/AA== +malformed invalid utf8-import-module.wast:328 AGFzbQEAAAACDgEEdGVzdAThgICAA38A +malformed invalid utf8-import-module.wast:343 AGFzbQEAAAACDAEEdGVzdALhgAN/AA== +malformed invalid utf8-import-module.wast:358 AGFzbQEAAAACDQEEdGVzdAPhgC4DfwA= +malformed invalid utf8-import-module.wast:373 AGFzbQEAAAACCwEEdGVzdAHhA38A +malformed invalid utf8-import-module.wast:388 AGFzbQEAAAACDAEEdGVzdALhLgN/AA== +malformed invalid utf8-import-module.wast:405 AGFzbQEAAAACDQEEdGVzdAPgAKADfwA= +malformed invalid utf8-import-module.wast:420 AGFzbQEAAAACDQEEdGVzdAPgf6ADfwA= +malformed invalid utf8-import-module.wast:435 AGFzbQEAAAACDQEEdGVzdAPggIADfwA= +malformed invalid utf8-import-module.wast:450 AGFzbQEAAAACDQEEdGVzdAPggKADfwA= +malformed invalid utf8-import-module.wast:465 AGFzbQEAAAACDQEEdGVzdAPgn6ADfwA= +malformed invalid utf8-import-module.wast:480 AGFzbQEAAAACDQEEdGVzdAPgn78DfwA= +malformed invalid utf8-import-module.wast:495 AGFzbQEAAAACDQEEdGVzdAPgwKADfwA= +malformed invalid utf8-import-module.wast:510 AGFzbQEAAAACDQEEdGVzdAPg/aADfwA= +malformed invalid utf8-import-module.wast:525 AGFzbQEAAAACDQEEdGVzdAPhAIADfwA= +malformed invalid utf8-import-module.wast:540 AGFzbQEAAAACDQEEdGVzdAPhf4ADfwA= +malformed invalid utf8-import-module.wast:555 AGFzbQEAAAACDQEEdGVzdAPhwIADfwA= +malformed invalid utf8-import-module.wast:570 AGFzbQEAAAACDQEEdGVzdAPh/YADfwA= +malformed invalid utf8-import-module.wast:585 AGFzbQEAAAACDQEEdGVzdAPsAIADfwA= +malformed invalid utf8-import-module.wast:600 AGFzbQEAAAACDQEEdGVzdAPsf4ADfwA= +malformed invalid utf8-import-module.wast:615 AGFzbQEAAAACDQEEdGVzdAPswIADfwA= +malformed invalid utf8-import-module.wast:630 AGFzbQEAAAACDQEEdGVzdAPs/YADfwA= +malformed invalid utf8-import-module.wast:645 AGFzbQEAAAACDQEEdGVzdAPtAIADfwA= +malformed invalid utf8-import-module.wast:660 AGFzbQEAAAACDQEEdGVzdAPtf4ADfwA= +malformed invalid utf8-import-module.wast:675 AGFzbQEAAAACDQEEdGVzdAPtoIADfwA= +malformed invalid utf8-import-module.wast:690 AGFzbQEAAAACDQEEdGVzdAPtoL8DfwA= +malformed invalid utf8-import-module.wast:705 AGFzbQEAAAACDQEEdGVzdAPtv4ADfwA= +malformed invalid utf8-import-module.wast:720 AGFzbQEAAAACDQEEdGVzdAPtv78DfwA= +malformed invalid utf8-import-module.wast:735 AGFzbQEAAAACDQEEdGVzdAPtwIADfwA= +malformed invalid utf8-import-module.wast:750 AGFzbQEAAAACDQEEdGVzdAPt/YADfwA= +malformed invalid utf8-import-module.wast:765 AGFzbQEAAAACDQEEdGVzdAPuAIADfwA= +malformed invalid utf8-import-module.wast:780 AGFzbQEAAAACDQEEdGVzdAPuf4ADfwA= +malformed invalid utf8-import-module.wast:795 AGFzbQEAAAACDQEEdGVzdAPuwIADfwA= +malformed invalid utf8-import-module.wast:810 AGFzbQEAAAACDQEEdGVzdAPu/YADfwA= +malformed invalid utf8-import-module.wast:825 AGFzbQEAAAACDQEEdGVzdAPvAIADfwA= +malformed invalid utf8-import-module.wast:840 AGFzbQEAAAACDQEEdGVzdAPvf4ADfwA= +malformed invalid utf8-import-module.wast:855 AGFzbQEAAAACDQEEdGVzdAPvwIADfwA= +malformed invalid utf8-import-module.wast:870 AGFzbQEAAAACDQEEdGVzdAPv/YADfwA= +malformed invalid utf8-import-module.wast:887 AGFzbQEAAAACDQEEdGVzdAPgoAADfwA= +malformed invalid utf8-import-module.wast:902 AGFzbQEAAAACDQEEdGVzdAPgoH8DfwA= +malformed invalid utf8-import-module.wast:917 AGFzbQEAAAACDQEEdGVzdAPgoMADfwA= +malformed invalid utf8-import-module.wast:932 AGFzbQEAAAACDQEEdGVzdAPgoP0DfwA= +malformed invalid utf8-import-module.wast:947 AGFzbQEAAAACDQEEdGVzdAPhgAADfwA= +malformed invalid utf8-import-module.wast:962 AGFzbQEAAAACDQEEdGVzdAPhgH8DfwA= +malformed invalid utf8-import-module.wast:977 AGFzbQEAAAACDQEEdGVzdAPhgMADfwA= +malformed invalid utf8-import-module.wast:992 AGFzbQEAAAACDQEEdGVzdAPhgP0DfwA= +malformed invalid utf8-import-module.wast:1007 AGFzbQEAAAACDQEEdGVzdAPsgAADfwA= +malformed invalid utf8-import-module.wast:1022 AGFzbQEAAAACDQEEdGVzdAPsgH8DfwA= +malformed invalid utf8-import-module.wast:1037 AGFzbQEAAAACDQEEdGVzdAPsgMADfwA= +malformed invalid utf8-import-module.wast:1052 AGFzbQEAAAACDQEEdGVzdAPsgP0DfwA= +malformed invalid utf8-import-module.wast:1067 AGFzbQEAAAACDQEEdGVzdAPtgAADfwA= +malformed invalid utf8-import-module.wast:1082 AGFzbQEAAAACDQEEdGVzdAPtgH8DfwA= +malformed invalid utf8-import-module.wast:1097 AGFzbQEAAAACDQEEdGVzdAPtgMADfwA= +malformed invalid utf8-import-module.wast:1112 AGFzbQEAAAACDQEEdGVzdAPtgP0DfwA= +malformed invalid utf8-import-module.wast:1127 AGFzbQEAAAACDQEEdGVzdAPugAADfwA= +malformed invalid utf8-import-module.wast:1142 AGFzbQEAAAACDQEEdGVzdAPugH8DfwA= +malformed invalid utf8-import-module.wast:1157 AGFzbQEAAAACDQEEdGVzdAPugMADfwA= +malformed invalid utf8-import-module.wast:1172 AGFzbQEAAAACDQEEdGVzdAPugP0DfwA= +malformed invalid utf8-import-module.wast:1187 AGFzbQEAAAACDQEEdGVzdAPvgAADfwA= +malformed invalid utf8-import-module.wast:1202 AGFzbQEAAAACDQEEdGVzdAPvgH8DfwA= +malformed invalid utf8-import-module.wast:1217 AGFzbQEAAAACDQEEdGVzdAPvgMADfwA= +malformed invalid utf8-import-module.wast:1232 AGFzbQEAAAACDQEEdGVzdAPvgP0DfwA= +malformed invalid utf8-import-module.wast:1249 AGFzbQEAAAACDwEEdGVzdAXxgICAgAN/AA== +malformed invalid utf8-import-module.wast:1264 AGFzbQEAAAACDQEEdGVzdAPxgIADfwA= +malformed invalid utf8-import-module.wast:1279 AGFzbQEAAAACDgEEdGVzdATxgIAjA38A +malformed invalid utf8-import-module.wast:1294 AGFzbQEAAAACDAEEdGVzdALxgAN/AA== +malformed invalid utf8-import-module.wast:1309 AGFzbQEAAAACDQEEdGVzdAPxgCMDfwA= +malformed invalid utf8-import-module.wast:1324 AGFzbQEAAAACCwEEdGVzdAHxA38A +malformed invalid utf8-import-module.wast:1339 AGFzbQEAAAACDAEEdGVzdALxIwN/AA== +malformed invalid utf8-import-module.wast:1356 AGFzbQEAAAACDgEEdGVzdATwAJCQA38A +malformed invalid utf8-import-module.wast:1371 AGFzbQEAAAACDgEEdGVzdATwf5CQA38A +malformed invalid utf8-import-module.wast:1386 AGFzbQEAAAACDgEEdGVzdATwgICAA38A +malformed invalid utf8-import-module.wast:1401 AGFzbQEAAAACDgEEdGVzdATwgJCQA38A +malformed invalid utf8-import-module.wast:1416 AGFzbQEAAAACDgEEdGVzdATwj5CQA38A +malformed invalid utf8-import-module.wast:1431 AGFzbQEAAAACDgEEdGVzdATwj7+/A38A +malformed invalid utf8-import-module.wast:1446 AGFzbQEAAAACDgEEdGVzdATwwJCQA38A +malformed invalid utf8-import-module.wast:1461 AGFzbQEAAAACDgEEdGVzdATw/ZCQA38A +malformed invalid utf8-import-module.wast:1476 AGFzbQEAAAACDgEEdGVzdATxAICAA38A +malformed invalid utf8-import-module.wast:1491 AGFzbQEAAAACDgEEdGVzdATxf4CAA38A +malformed invalid utf8-import-module.wast:1506 AGFzbQEAAAACDgEEdGVzdATxwICAA38A +malformed invalid utf8-import-module.wast:1521 AGFzbQEAAAACDgEEdGVzdATx/YCAA38A +malformed invalid utf8-import-module.wast:1536 AGFzbQEAAAACDgEEdGVzdATzAICAA38A +malformed invalid utf8-import-module.wast:1551 AGFzbQEAAAACDgEEdGVzdATzf4CAA38A +malformed invalid utf8-import-module.wast:1566 AGFzbQEAAAACDgEEdGVzdATzwICAA38A +malformed invalid utf8-import-module.wast:1581 AGFzbQEAAAACDgEEdGVzdATz/YCAA38A +malformed invalid utf8-import-module.wast:1596 AGFzbQEAAAACDgEEdGVzdAT0AICAA38A +malformed invalid utf8-import-module.wast:1611 AGFzbQEAAAACDgEEdGVzdAT0f4CAA38A +malformed invalid utf8-import-module.wast:1626 AGFzbQEAAAACDgEEdGVzdAT0kICAA38A +malformed invalid utf8-import-module.wast:1641 AGFzbQEAAAACDgEEdGVzdAT0v4CAA38A +malformed invalid utf8-import-module.wast:1656 AGFzbQEAAAACDgEEdGVzdAT0wICAA38A +malformed invalid utf8-import-module.wast:1671 AGFzbQEAAAACDgEEdGVzdAT0/YCAA38A +malformed invalid utf8-import-module.wast:1686 AGFzbQEAAAACDgEEdGVzdAT1gICAA38A +malformed invalid utf8-import-module.wast:1701 AGFzbQEAAAACDgEEdGVzdAT3gICAA38A +malformed invalid utf8-import-module.wast:1716 AGFzbQEAAAACDgEEdGVzdAT3v7+/A38A +malformed invalid utf8-import-module.wast:1733 AGFzbQEAAAACDgEEdGVzdATwkACQA38A +malformed invalid utf8-import-module.wast:1748 AGFzbQEAAAACDgEEdGVzdATwkH+QA38A +malformed invalid utf8-import-module.wast:1763 AGFzbQEAAAACDgEEdGVzdATwkMCQA38A +malformed invalid utf8-import-module.wast:1778 AGFzbQEAAAACDgEEdGVzdATwkP2QA38A +malformed invalid utf8-import-module.wast:1793 AGFzbQEAAAACDgEEdGVzdATxgACAA38A +malformed invalid utf8-import-module.wast:1808 AGFzbQEAAAACDgEEdGVzdATxgH+AA38A +malformed invalid utf8-import-module.wast:1823 AGFzbQEAAAACDgEEdGVzdATxgMCAA38A +malformed invalid utf8-import-module.wast:1838 AGFzbQEAAAACDgEEdGVzdATxgP2AA38A +malformed invalid utf8-import-module.wast:1853 AGFzbQEAAAACDgEEdGVzdATzgACAA38A +malformed invalid utf8-import-module.wast:1868 AGFzbQEAAAACDgEEdGVzdATzgH+AA38A +malformed invalid utf8-import-module.wast:1883 AGFzbQEAAAACDgEEdGVzdATzgMCAA38A +malformed invalid utf8-import-module.wast:1898 AGFzbQEAAAACDgEEdGVzdATzgP2AA38A +malformed invalid utf8-import-module.wast:1913 AGFzbQEAAAACDgEEdGVzdAT0gACAA38A +malformed invalid utf8-import-module.wast:1928 AGFzbQEAAAACDgEEdGVzdAT0gH+AA38A +malformed invalid utf8-import-module.wast:1943 AGFzbQEAAAACDgEEdGVzdAT0gMCAA38A +malformed invalid utf8-import-module.wast:1958 AGFzbQEAAAACDgEEdGVzdAT0gP2AA38A +malformed invalid utf8-import-module.wast:1975 AGFzbQEAAAACDgEEdGVzdATwkJAAA38A +malformed invalid utf8-import-module.wast:1990 AGFzbQEAAAACDgEEdGVzdATwkJB/A38A +malformed invalid utf8-import-module.wast:2005 AGFzbQEAAAACDgEEdGVzdATwkJDAA38A +malformed invalid utf8-import-module.wast:2020 AGFzbQEAAAACDgEEdGVzdATwkJD9A38A +malformed invalid utf8-import-module.wast:2035 AGFzbQEAAAACDgEEdGVzdATxgIAAA38A +malformed invalid utf8-import-module.wast:2050 AGFzbQEAAAACDgEEdGVzdATxgIB/A38A +malformed invalid utf8-import-module.wast:2065 AGFzbQEAAAACDgEEdGVzdATxgIDAA38A +malformed invalid utf8-import-module.wast:2080 AGFzbQEAAAACDgEEdGVzdATxgID9A38A +malformed invalid utf8-import-module.wast:2095 AGFzbQEAAAACDgEEdGVzdATzgIAAA38A +malformed invalid utf8-import-module.wast:2110 AGFzbQEAAAACDgEEdGVzdATzgIB/A38A +malformed invalid utf8-import-module.wast:2125 AGFzbQEAAAACDgEEdGVzdATzgIDAA38A +malformed invalid utf8-import-module.wast:2140 AGFzbQEAAAACDgEEdGVzdATzgID9A38A +malformed invalid utf8-import-module.wast:2155 AGFzbQEAAAACDgEEdGVzdAT0gIAAA38A +malformed invalid utf8-import-module.wast:2170 AGFzbQEAAAACDgEEdGVzdAT0gIB/A38A +malformed invalid utf8-import-module.wast:2185 AGFzbQEAAAACDgEEdGVzdAT0gIDAA38A +malformed invalid utf8-import-module.wast:2200 AGFzbQEAAAACDgEEdGVzdAT0gID9A38A +malformed invalid utf8-import-module.wast:2217 AGFzbQEAAAACEAEEdGVzdAb4gICAgIADfwA= +malformed invalid utf8-import-module.wast:2232 AGFzbQEAAAACDgEEdGVzdAT4gICAA38A +malformed invalid utf8-import-module.wast:2247 AGFzbQEAAAACDwEEdGVzdAX4gICAIwN/AA== +malformed invalid utf8-import-module.wast:2262 AGFzbQEAAAACDQEEdGVzdAP4gIADfwA= +malformed invalid utf8-import-module.wast:2277 AGFzbQEAAAACDgEEdGVzdAT4gIAjA38A +malformed invalid utf8-import-module.wast:2292 AGFzbQEAAAACDAEEdGVzdAL4gAN/AA== +malformed invalid utf8-import-module.wast:2307 AGFzbQEAAAACDQEEdGVzdAP4gCMDfwA= +malformed invalid utf8-import-module.wast:2322 AGFzbQEAAAACCwEEdGVzdAH4A38A +malformed invalid utf8-import-module.wast:2337 AGFzbQEAAAACDAEEdGVzdAL4IwN/AA== +malformed invalid utf8-import-module.wast:2354 AGFzbQEAAAACDwEEdGVzdAX4gICAgAN/AA== +malformed invalid utf8-import-module.wast:2369 AGFzbQEAAAACDwEEdGVzdAX7v7+/vwN/AA== +malformed invalid utf8-import-module.wast:2386 AGFzbQEAAAACEQEEdGVzdAf8gICAgICAA38A +malformed invalid utf8-import-module.wast:2401 AGFzbQEAAAACDwEEdGVzdAX8gICAgAN/AA== +malformed invalid utf8-import-module.wast:2416 AGFzbQEAAAACEAEEdGVzdAb8gICAgCMDfwA= +malformed invalid utf8-import-module.wast:2431 AGFzbQEAAAACDgEEdGVzdAT8gICAA38A +malformed invalid utf8-import-module.wast:2446 AGFzbQEAAAACDwEEdGVzdAX8gICAIwN/AA== +malformed invalid utf8-import-module.wast:2461 AGFzbQEAAAACDQEEdGVzdAP8gIADfwA= +malformed invalid utf8-import-module.wast:2476 AGFzbQEAAAACDgEEdGVzdAT8gIAjA38A +malformed invalid utf8-import-module.wast:2491 AGFzbQEAAAACDAEEdGVzdAL8gAN/AA== +malformed invalid utf8-import-module.wast:2506 AGFzbQEAAAACDQEEdGVzdAP8gCMDfwA= +malformed invalid utf8-import-module.wast:2521 AGFzbQEAAAACCwEEdGVzdAH8A38A +malformed invalid utf8-import-module.wast:2536 AGFzbQEAAAACDAEEdGVzdAL8IwN/AA== +malformed invalid utf8-import-module.wast:2553 AGFzbQEAAAACEAEEdGVzdAb8gICAgIADfwA= +malformed invalid utf8-import-module.wast:2568 AGFzbQEAAAACEAEEdGVzdAb9v7+/v78DfwA= +malformed invalid utf8-import-module.wast:2585 AGFzbQEAAAACCwEEdGVzdAH+A38A +malformed invalid utf8-import-module.wast:2600 AGFzbQEAAAACCwEEdGVzdAH/A38A +malformed invalid utf8-import-module.wast:2615 AGFzbQEAAAACDAEEdGVzdAL+/wN/AA== +malformed invalid utf8-import-module.wast:2630 AGFzbQEAAAACDgEEdGVzdAQAAP7/A38A +malformed invalid utf8-import-module.wast:2645 AGFzbQEAAAACDAEEdGVzdAL//gN/AA== +malformed invalid utf8-import-module.wast:2660 AGFzbQEAAAACDgEEdGVzdAT//gAAA38A +valid valid extended-const/global.wast:3 AGFzbQEAAAABNw1gAn9/AX9gAAF/YAABfmAAAW9gAX8AYAF+AGABbwBgAAF9YAABfGABfQBgAXwAYAAAYAF/AX8CLwIIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAhzcGVjdGVzdApnbG9iYWxfaTY0A34AAzc2AQIDAwECAQIBAgECBAUGBwgHCAkKCwEBAQEBAQEBAQEBAQEAAQEBCwsBAQwBAQsBDAwBAQEBBAUBcAEBAQUDAQABBoIBEX8AQX4LfQBDAABAwAt8AEQAAAAAAAAQwAt+AEJ7C38BQXQLfQFDAABQwQt8AUQAAAAAAAAswAt+AUJxC38AIwALfgAjAQt/AEEUQQJsQQJrQQRqC34AQhRCAn5CAn1CBXwLfwAjAEEqagt+ACMBQip8C28A0G8LbwHQbwtwANBwCwfVBTMFZ2V0LWEAAAVnZXQtYgABBWdldC1yAAIGZ2V0LW1yAAMFZ2V0LXgABAVnZXQteQAFBmdldC16MQAGBmdldC16MgAHBmdldC16MwAIBmdldC16NAAJBmdldC16NQAKBmdldC16NgALBXNldC14AAwFc2V0LXkADQZzZXQtbXIADgVnZXQtMwAPBWdldC00ABAFZ2V0LTcAEQVnZXQtOAASBXNldC03ABMFc2V0LTgAFA9hcy1zZWxlY3QtZmlyc3QAFg1hcy1zZWxlY3QtbWlkABcOYXMtc2VsZWN0LWxhc3QAGA1hcy1sb29wLWZpcnN0ABkLYXMtbG9vcC1taWQAGgxhcy1sb29wLWxhc3QAGw9hcy1pZi1jb25kaXRpb24AHAphcy1pZi10aGVuAB0KYXMtaWYtZWxzZQAeDmFzLWJyX2lmLWZpcnN0AB8NYXMtYnJfaWYtbGFzdAAgEWFzLWJyX3RhYmxlLWZpcnN0ACEQYXMtYnJfdGFibGUtbGFzdAAiFmFzLWNhbGxfaW5kaXJlY3QtZmlyc3QAJBRhcy1jYWxsX2luZGlyZWN0LW1pZAAlFWFzLWNhbGxfaW5kaXJlY3QtbGFzdAAmDmFzLXN0b3JlLWZpcnN0ACcNYXMtc3RvcmUtbGFzdAAoD2FzLWxvYWQtb3BlcmFuZAApFGFzLW1lbW9yeS5ncm93LXZhbHVlACoNYXMtY2FsbC12YWx1ZQAsD2FzLXJldHVybi12YWx1ZQAtD2FzLWRyb3Atb3BlcmFuZAAuC2FzLWJyLXZhbHVlAC8SYXMtbG9jYWwuc2V0LXZhbHVlADASYXMtbG9jYWwudGVlLXZhbHVlADETYXMtZ2xvYmFsLnNldC12YWx1ZQAyEGFzLXVuYXJ5LW9wZXJhbmQAMxFhcy1iaW5hcnktb3BlcmFuZAA0EmFzLWNvbXBhcmUtb3BlcmFuZAA1CQcBAEEACwEjCsQDNgQAIwILBAAjBQsEACMQCwQAIxELBAAjBgsEACMJCwQAIwoLBAAjCwsEACMMCwQAIw0LBAAjDgsEACMPCwYAIAAkBgsGACAAJAkLBgAgACQRCwQAIwMLBAAjBAsEACMHCwQAIwgLBgAgACQHCwYAIAAkCAsCAAsJACMGQQJBAxsLCQBBAiMGQQMbCwkAQQJBAyMGGwsLAAN/IwYQFRAVCwsLAAN/EBUjBhAVCwsLAAN/EBUQFSMGCwsQACMGBH8QFUECBRAVQQMLCwwAQQEEfyMGBUECCwsMAEEABH9BAgUjBgsLDgACfyMGQQINAEEDDwsLDgACf0ECIwYNAEEDDwsLDQACfyMGQQIOAQAACwsNAAJ/QQIjBg4BAAALCwQAIAALDgACfyMGQQJBABEAAAsLDgACf0ECIwZBABEAAAsLDgACf0ECQQAjBhEAAAsLCQAjBkEBNgIACwkAQQAjBjYCAAsHACMGKAIACwYAIwZAAAsEACAACwYAIwYQKwsFACMGDwsFACMGGgsJAAJ/IwYMAAsLCAAjBiEAIAALBgAjBiIACwgAIwYkBiMGCwUAIwZFCwcAIwYjBmwLBwAjAEEBSws= +malformed invalid extended-const/global.wast:391 AGFzbQEAAAACmICAgAABCHNwZWN0ZXN0Cmdsb2JhbF9pMzIDfwI= +malformed invalid extended-const/global.wast:421 AGFzbQEAAAAGhoCAgAABfwJBAAs= +valid valid extended-const/data.wast:5 AGFzbQEAAAAFAwEAAQujARgAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmMAQQALAABBAQsEYWJjZABBAAsAAEEACwNhYmM= +valid valid extended-const/data.wast:179 AGFzbQEAAAAFAwEAAQsJAQBBAEEqagsA +valid valid extended-const/data.wast:184 AGFzbQEAAAAFAwEAAQsJAQBBKkEAawsA +valid valid extended-const/data.wast:189 AGFzbQEAAAAFAwEAAQsJAQBBAUECbAsA +valid valid extended-const/data.wast:196 AGFzbQEAAAACGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAUDAQABCw8BAEECIwBBAWtBAmpsCwA= +invalid invalid extended-const/elem.wast:536 AGFzbQEAAAABBQFgAAF/AwIBAAQEAXAAAQkJAQRBAAsBEAALCgYBBABBCQs= +valid valid extended-const/elem.wast:705 AGFzbQEAAAABCgJgAAF/YAF/AX8DAwIAAQQEAXAACgcRAQ1jYWxsX2luX3RhYmxlAAEJCgEAQQFBAmoLAQAKDgIEAEEqCwcAIAARAAAL +valid valid extended-const/elem.wast:716 AGFzbQEAAAABCgJgAAF/YAF/AX8DAwIAAQQEAXAACgcRAQ1jYWxsX2luX3RhYmxlAAEJCgEAQQJBAWsLAQAKDgIEAEEqCwcAIAARAAAL +valid valid extended-const/elem.wast:727 AGFzbQEAAAABCgJgAAF/YAF/AX8DAwIAAQQEAXAACgcRAQ1jYWxsX2luX3RhYmxlAAEJCgEAQQJBAmwLAQAKDgIEAEEqCwcAIAARAAAL +valid valid extended-const/elem.wast:740 AGFzbQEAAAABCgJgAAF/YAF/AX8CGAEIc3BlY3Rlc3QKZ2xvYmFsX2kzMgN/AAMDAgABBAQBcAAKBxEBDWNhbGxfaW5fdGFibGUAAQkRAQBBAiMAQZkFa0ECamwLAQAKDgIEAEEqCwcAIAARAAAL diff --git a/packages/compiler/test/wasm-spec.test.ts b/packages/compiler/test/wasm-spec.test.ts new file mode 100644 index 000000000..fe935e418 --- /dev/null +++ b/packages/compiler/test/wasm-spec.test.ts @@ -0,0 +1,168 @@ +/* The WebAssembly decoder (src/wasm/binary.ts) against the specification's + * test suite, against V8 on mutations of those modules, and on the inline + * modules of real packages in this workspace. V8 (the Node running the + * test) is the oracle: scriptc must accept exactly the modules Node + * accepts, within the translatable feature level. */ +import { globSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { analyzeWasm } from "../src/wasm/binary.js"; +import { decodeBase64 } from "../src/wasm/discover.js"; + +/** V8's verdict (the compiler's type world declares no WebAssembly value). */ +const v8Validate = (bytes: Uint8Array): boolean => + ( + globalThis as unknown as { WebAssembly: { validate(bytes: Uint8Array): boolean } } + ).WebAssembly.validate(bytes); + +interface SpecCase { + suite: string; + v8: string; + where: string; + bytes: Uint8Array; +} + +const cases: SpecCase[] = readFileSync(join(import.meta.dirname, "wasm-spec-cases.txt"), "utf8") + .split("\n") + .filter((line) => line !== "" && !line.startsWith("#")) + .map((line) => { + const [suite, v8, where, b64] = line.split(" "); + return { + suite: suite!, + v8: v8!, + where: where!, + bytes: new Uint8Array(Buffer.from(b64!, "base64")), + }; + }); + +const accepted = (status: string): boolean => status === "valid" || status === "unsupported"; + +describe("WebAssembly spec test suite", () => { + test("covers the 2.0 core suite and extended constant expressions", () => { + expect(cases.length).toBeGreaterThan(3000); + const extended = cases.filter((c) => c.where.startsWith("extended-const/")); + expect(extended.length).toBeGreaterThan(10); + expect( + extended.filter((c) => analyzeWasm(c.bytes).features.includes("extended-const")).length, + ).toBeGreaterThan(5); + }); + + test("every module gets V8's verdict and needs no other proposal", () => { + const wrong: string[] = []; + for (const c of cases) { + const s = analyzeWasm(c.bytes); + const v8 = v8Validate(c.bytes); + if ((v8 ? "valid" : "invalid") !== c.v8) wrong.push(`${c.where}: V8 changed its verdict`); + // Multiple memories are the one proposal outside the translatable set + // that 2.0 tests exercise (as invalid; V8 accepts them). + const allowed = s.status !== "unsupported" || s.unsupported.join() === "multi-memory"; + if (accepted(s.status) !== v8 || !allowed) { + wrong.push(`${c.where}: ${s.status} ${s.error ?? ""} (V8 ${v8 ? "accepts" : "rejects"})`); + } + } + expect(wrong).toEqual([]); + }); + + test("differs from the 2.0 suite only where WebAssembly 3.0 and V8 relaxed it", () => { + // Multiple memories became valid, and memory.size/grow's reserved byte + // a memory index, so a long LEB128 zero is accepted (multi-memory); + // global initializers and segment offsets may read defined immutable + // globals (GC proposal). + const relaxed = cases.filter((c) => c.suite !== "valid" && c.v8 === "valid"); + expect(relaxed.length).toBeGreaterThan(0); + for (const c of relaxed) { + expect(c.where).toMatch( + /^(?:extended-const\/)?(?:binary|data|elem|global|imports|memory)\.wast:\d+$/, + ); + } + expect(relaxed.length).toBeLessThan(30); + }); +}); + +describe("mutations of the suite's valid modules", () => { + test("never disagree with V8 (deterministic sample)", () => { + const seeds = cases.filter((c) => c.v8 === "valid").map((c) => c.bytes); + let state = 0x9e3779b9; + const random = (n: number): number => { + state ^= state << 13; + state >>>= 0; + state ^= state >>> 17; + state ^= state << 5; + state >>>= 0; + return state % n; + }; + const disagreements: string[] = []; + for (let i = 0; i < 8000; i++) { + const seed = seeds[random(seeds.length)]!; + const at = 8 + random(Math.max(1, seed.length - 8)); + let bytes: Uint8Array; + switch (random(4)) { + case 0: + bytes = Uint8Array.from(seed); + bytes[at] = bytes[at]! ^ (1 << random(8)); + break; + case 1: + bytes = Uint8Array.from(seed); + bytes[at] = random(256); + break; + case 2: + bytes = new Uint8Array([...seed.subarray(0, at), ...seed.subarray(at + 1)]); + break; + default: + bytes = new Uint8Array([...seed.subarray(0, at), random(256), ...seed.subarray(at)]); + } + const s = analyzeWasm(bytes); + if (s.status === "unsupported") continue; + if (accepted(s.status) !== v8Validate(bytes)) { + disagreements.push( + `${s.status} ${s.error ?? ""}: ${Buffer.from(bytes).toString("base64")}`, + ); + } + } + expect(disagreements).toEqual([]); + }); +}); + +describe("modules inlined by packages in this workspace", () => { + // Lockfile-pinned installs: undici's llhttp (with and without SIMD) and + // es-module-lexer, both inlined as base64 string literals. + const root = join(import.meta.dirname, "../../.."); + const inline = (pattern: string): Uint8Array[] => + globSync(join(root, pattern)) + .sort() + .flatMap((file) => + [...readFileSync(file, "utf8").matchAll(/AGFzbQ[A-Za-z0-9+/=]*/g)].map((m) => + decodeBase64(m[0])!, + ), + ); + + test("undici's llhttp: the scalar module translates, the SIMD one needs SIMD", () => { + const scalar = inline( + "node_modules/.pnpm/undici@7.*/node_modules/undici/lib/llhttp/llhttp-wasm.js", + ); + const simd = inline( + "node_modules/.pnpm/undici@7.*/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js", + ); + expect(scalar.length).toBeGreaterThan(0); + expect(simd.length).toBeGreaterThan(0); + for (const bytes of scalar) { + expect(analyzeWasm(bytes)).toMatchObject({ status: "valid", importModules: ["env"] }); + expect(v8Validate(bytes)).toBe(true); + } + for (const bytes of simd) { + expect(analyzeWasm(bytes)).toMatchObject({ status: "unsupported", unsupported: ["simd"] }); + expect(v8Validate(bytes)).toBe(true); + } + }); + + test("es-module-lexer's module is plain MVP", () => { + const modules = inline( + "node_modules/.pnpm/es-module-lexer@1.*/node_modules/es-module-lexer/dist/lexer.js", + ); + expect(modules.length).toBeGreaterThan(0); + for (const bytes of modules) { + expect(analyzeWasm(bytes)).toMatchObject({ status: "valid", imports: 0 }); + expect(v8Validate(bytes)).toBe(true); + } + }); +}); diff --git a/tests/coverage-fixtures/wasm/add.wasm b/tests/coverage-fixtures/wasm/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 diff --git a/tests/coverage-fixtures/wasm/build/generated.wasm b/tests/coverage-fixtures/wasm/build/generated.wasm new file mode 100644 index 0000000000000000000000000000000000000000..3697e05bcd25722e5a7171aae99545ca37196181 GIT binary patch literal 43 lcmZQbEY4+QU|?WmWlUgTtY&6nWZ)8G6k+(w0~AC7+yG~s19boZ literal 0 HcmV?d00001 diff --git a/tests/coverage-fixtures/wasm/glue.js b/tests/coverage-fixtures/wasm/glue.js new file mode 100644 index 000000000..631560632 --- /dev/null +++ b/tests/coverage-fixtures/wasm/glue.js @@ -0,0 +1,18 @@ +import { existsSync } from "node:fs"; + +// (module (func (export "answer") (result i32) i32.const 42)), inlined the +// way undici inlines llhttp. +const ANSWER = "AGFzbQEAAAABBQFgAAF/AwIBAAcKAQZhbnN3ZXIAAAoGAQQAQSoL"; + +export function hasModule() { + return existsSync(new URL("./add.wasm", import.meta.url).pathname); +} + +export function inlineModule() { + return ANSWER; +} + +export async function instantiate(bytes) { + const { instance } = await WebAssembly.instantiate(bytes, {}); + return instance.exports; +} diff --git a/tests/coverage-fixtures/wasm/main.ts b/tests/coverage-fixtures/wasm/main.ts new file mode 100644 index 000000000..a2d80b6b3 --- /dev/null +++ b/tests/coverage-fixtures/wasm/main.ts @@ -0,0 +1,9 @@ +// The WebAssembly modules this program can load are visible to the build in +// four ways (wasm/discover.ts): a path the compiler resolves (glue.js), an +// inline base64 literal (glue.js), a .wasm file shipped by a package whose +// code mentions WebAssembly (wasm-fixture-pkg), and the scriptc.wasm list in +// package.json (build/generated.wasm). +import { hasModule, inlineModule } from "./glue.js"; +import { describeModule } from "wasm-fixture-pkg"; + +console.log(hasModule(), inlineModule().length, describeModule()); diff --git a/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.d.ts b/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.d.ts new file mode 100644 index 000000000..2de8deed5 --- /dev/null +++ b/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.d.ts @@ -0,0 +1 @@ +export declare function describeModule(path?: string): string; diff --git a/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.js b/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.js new file mode 100644 index 000000000..ecb29cd32 --- /dev/null +++ b/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/index.js @@ -0,0 +1,6 @@ +// Loads lib/core.wasm from a path computed at run time, so only the +// package scan finds the module. +export function describeModule(path) { + const where = path ?? "lib/core.wasm"; + return typeof WebAssembly === "object" ? `wasm at ${where}` : "no wasm"; +} diff --git a/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm b/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm new file mode 100644 index 0000000000000000000000000000000000000000..dd711f0953bf8f81c8ff43bc955fb401f1c98803 GIT binary patch literal 27 icmZQbEY4+QU|?WmWlUgTtY>CoWZ+_BWMOb(;06FLCjycH literal 0 HcmV?d00001 diff --git a/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/package.json b/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/package.json new file mode 100644 index 000000000..af753b539 --- /dev/null +++ b/tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/package.json @@ -0,0 +1,7 @@ +{ + "name": "wasm-fixture-pkg", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/tests/coverage-fixtures/wasm/package.json b/tests/coverage-fixtures/wasm/package.json new file mode 100644 index 000000000..a68fcd213 --- /dev/null +++ b/tests/coverage-fixtures/wasm/package.json @@ -0,0 +1,8 @@ +{ + "name": "wasm-coverage-fixture", + "private": true, + "type": "module", + "scriptc": { + "wasm": ["build/generated.wasm"] + } +} diff --git a/tests/harness/__snapshots__/coverage-wasm.txt b/tests/harness/__snapshots__/coverage-wasm.txt new file mode 100644 index 000000000..5b5bea36a --- /dev/null +++ b/tests/harness/__snapshots__/coverage-wasm.txt @@ -0,0 +1,31 @@ +scriptc coverage tests/coverage-fixtures/wasm/main.ts + + statements analyzed 8 + compile statically 6 (75%) + in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them) + + npm package compilation: + wasm-fixture-pkg static + + WebAssembly modules: (found at build time; none compile to native code yet) + inline module 39 bytes valid + 1 function, 0 imports, 1 export + found: inline at tests/coverage-fixtures/wasm/glue.js:5 + tests/coverage-fixtures/wasm/add.wasm 41 bytes valid + 1 function, 0 imports, 1 export + found: path at tests/coverage-fixtures/wasm/glue.js:8 + tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm 27 bytes invalid: type mismatch (at byte 27) + 1 function, 0 imports, 0 exports + found: shipped by the package at tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg + tests/coverage-fixtures/wasm/build/generated.wasm 43 bytes uses simd + 1 function, 0 imports, 0 exports + features: simd + found: configured by tests/coverage-fixtures/wasm/package.json + + in unreached code (never lowered — cannot fail a build) + ×1 SC2004 uses of 'instance' inherit the blocker on its declaration + at tests/coverage-fixtures/wasm/glue.js:17:10 + hint: the declaration of 'instance' did not compile — fix the diagnostic reported there and these sites clear with it + ×1 SC2020 'WebAssembly.instantiate' is not supported yet: scriptc does not compile WebAssembly modules to native code yet + at tests/coverage-fixtures/wasm/glue.js:16:30 + hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 420651418..f1347bf23 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -43,6 +43,36 @@ test("unreached blockers report in their own group", async () => { ); }); +test("WebAssembly modules the build can see report with their sources", async () => { + // A resolved path and an inline literal (glue.js), a packaged .wasm + // (wasm-fixture-pkg), and package.json's scriptc.wasm list. + await expect(report(fixture("wasm/main.ts"))).toMatchFileSnapshot( + "__snapshots__/coverage-wasm.txt", + ); + const { coverage } = analyze(fixture("wasm/main.ts"), { wasm: [fixture("wasm/add.wasm")] }); + const modules = coverage.wasmModules ?? []; + expect( + modules.map((m) => [ + m.path?.replace(repoRoot + "/", "") ?? "inline", + m.status, + m.unsupported, + m.sources.map((s) => s.kind), + ]), + ).toEqual([ + ["inline", "valid", [], ["inline"]], + ["tests/coverage-fixtures/wasm/add.wasm", "valid", [], ["path", "config"]], + [ + "tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm", + "invalid", + [], + ["package"], + ], + ["tests/coverage-fixtures/wasm/build/generated.wasm", "unsupported", ["simd"], ["config"]], + ]); + const envelope = coverageEnvelope(coverage, { compilerVersion: "test" }); + expect(envelope.wasmModules?.map((m) => m.sha256.length)).toEqual([64, 64, 64, 64]); +}); + test("divergences report as warnings, never as blockers", async () => { const { coverage } = analyze(fixture("divergences.ts")); expect(coverage.diagnostics).toEqual([]); From b119b4cf0352fb95e173c14d9f66f50b9845eaff Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 17:32:17 -0500 Subject: [PATCH 070/120] Stringify class instances through the runtime JSON traversal JSON.stringify of a class instance, or of a record, array or union that holds one, boxes the value and serializes it with the runtime JSON traversal. The output matches Node: own enumerable properties in creation order, properties added after construction, toJSON on the instance or a base class, and no methods, accessors, #private fields or static fields. A class whose declared field cannot be read through the checked-dynamic property view, such as a Date field, is refused at compile time with SC1090 naming the field. Boxed Map and Set values now stringify as {}, as Node does, instead of throwing. --- .../language-features/class-reflection.mdx | 2 +- .../src/frontend/lowering/builtins/json.ts | 70 ++++++++++ packages/compiler/src/ir/ir.test.ts | 11 ++ packages/compiler/src/ir/ir.ts | 15 +- ...000-json-stringify-class-instances.ts.json | 6 + ...-json-stringify-js-class-instances.js.json | 6 + .../diagnostics/json-class-fields.ts.json | 6 + packages/runtime/src/scr_json.c | 22 ++- .../6000-json-stringify-class-instances.ts | 131 ++++++++++++++++++ .../6001-json-stringify-js-class-instances.js | 49 +++++++ tests/diagnostics/json-class-fields.ts | 15 ++ .../__snapshots__/json-class-fields.ts.txt | 13 ++ tests/harness/corpus-costs.json | 4 + 13 files changed, 340 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6000-json-stringify-class-instances.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6001-json-stringify-js-class-instances.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/json-class-fields.ts.json create mode 100644 tests/corpus/6000-json-stringify-class-instances.ts create mode 100644 tests/corpus/6001-json-stringify-js-class-instances.js create mode 100644 tests/diagnostics/json-class-fields.ts create mode 100644 tests/harness/__snapshots__/json-class-fields.ts.txt diff --git a/docs/content/limitations/language-features/class-reflection.mdx b/docs/content/limitations/language-features/class-reflection.mdx index 4e2b9f299..8b0738cc7 100644 --- a/docs/content/limitations/language-features/class-reflection.mdx +++ b/docs/content/limitations/language-features/class-reflection.mdx @@ -1 +1 @@ -- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. They raise an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values. +- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. `JSON.stringify` writes Map and Set fields as `{}`, as Node.js does, and refuses classes with Date fields with `SC1090`. `Object.keys` raises an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values. diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index a73c9d5e7..c99a99c75 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -8,6 +8,10 @@ import { isJsonStringifyDynamicType, BOOL, DYN, + type IrType, + classDynViewSupported, + isClassOwnEnumerableFieldName, + isDynTypedRefType, type IrExpr, STRING, type SrcLoc, @@ -157,6 +161,16 @@ export function lowerJsonMethodCall( (id) => lowerer.unions.get(id), ) ) { + const unreadable = classFieldWithoutJsonView(lowerer, value.type, new Set()); + if (unreadable !== null) { + lowerer.unsupported( + "SC1090", + argNode, + `JSON.stringify of '${lowerer.fmt(value.type)}' values (field '${unreadable.field}' of class ` + + `'${unreadable.className}' has type '${lowerer.fmt(unreadable.type)}', which the runtime JSON ` + + `traversal cannot read)`, + ); + } // Keep the original containers live while the runtime visits them: // a toJSON callback can mutate a later field, and shared/cyclic // references must retain their identities throughout traversal. @@ -276,6 +290,62 @@ function lowerOptionalStringifyRoot( return { kind: "call", callee: helper, args: [value], type: resultT, loc }; } +/** The first class field reachable from a stringify input that the class's + * checked-dynamic property view cannot represent. The runtime JSON traversal + * reads class instances through that view, so such a class is refused at + * compile time instead of throwing when the view is built. */ +function classFieldWithoutJsonView( + lowerer: Lowerer, + t: IrType, + visiting: Set, +): { className: string; field: string; type: IrType } | null { + const getRecord = (id: string) => lowerer.shapes.get(id); + const getUnion = (id: string) => lowerer.unions.get(id); + switch (t.kind) { + case "array": + return classFieldWithoutJsonView(lowerer, t.elem, visiting); + case "record": { + if (visiting.has(t.shapeId)) return null; + visiting.add(t.shapeId); + const shape = getRecord(t.shapeId); + for (const field of shape?.fields ?? []) { + const found = classFieldWithoutJsonView(lowerer, field.type, visiting); + if (found) return found; + } + return shape?.indexValue + ? classFieldWithoutJsonView(lowerer, shape.indexValue, visiting) + : null; + } + case "union": { + if (visiting.has(t.unionId)) return null; + visiting.add(t.unionId); + for (const arm of getUnion(t.unionId)?.arms ?? []) { + const found = classFieldWithoutJsonView(lowerer, arm, visiting); + if (found) return found; + } + return null; + } + case "object": { + if (!isDynTypedRefType(t) || visiting.has(t.className)) return null; + visiting.add(t.className); + const info = lowerer.classes.get(t.className); + for (const field of info?.def.fields ?? []) { + if (!isClassOwnEnumerableFieldName(field.name)) continue; + if (!classDynViewSupported([field], getRecord, getUnion)) { + const className = + info?.def.jsName ?? info?.decl?.name?.text ?? t.className.replace(/^%/, ""); + return { className, field: field.name, type: field.type }; + } + const found = classFieldWithoutJsonView(lowerer, field.type, visiting); + if (found) return found; + } + return null; + } + default: + return null; + } +} + /** Only explicit null/undefined select the no-callback path. */ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean { if (ts.isParenthesizedExpression(node)) return jsonNullishArgument(lowerer, node.expression); diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 60ecc9ad0..52ef9b9c1 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -159,6 +159,17 @@ describe("checked records with opaque payloads", () => { isJsonStringifyDynamicType(arrayOf({ kind: "union", unionId: "optional" }), record, union), ).toBe(true); }); + + test("class instances stringify through the runtime JSON traversal only", () => { + const point: IrType = { kind: "object", className: "Point" }; + const failure: IrType = { kind: "object", className: "%Error" }; + expect(isJsonStringifyDynamicType(point, record, union)).toBe(true); + expect(isJsonStringifyDynamicType(arrayOf(point), record, union)).toBe(true); + expect(isJsonStringifyDynamicType(failure, record, union)).toBe(false); + expect(isJsonStringifySafeType(point, record, union)).toBe(false); + expect(isJsonSafeType(point, record, union)).toBe(false); + expect(canDynCheckTo(arrayOf(point), record, union)).toBe(true); + }); }); describe("native typed-array boundaries", () => { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..8c5299a23 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3717,9 +3717,10 @@ export function isJsonStringifySafeType( return isJsonSafeAt(t, getRecord, getUnion, true, false, new Set()); } -/** A JSON-shaped container with unknown payloads needs the runtime JSON - * traversal. Its declared structure can be boxed, but payloads may carry - * omitted values, toJSON methods, or cycles. This predicate does not widen +/** A JSON-shaped container with unknown payloads, or a class instance at + * any position, needs the runtime JSON traversal. Its declared structure can + * be boxed, but payloads may carry omitted values, toJSON methods, added + * properties, or cycles. This predicate does not widen * the type-directed serializer, checked-value boundary, or root-undefined * contract; callers must select the checked runtime traversal explicitly. */ export function isJsonStringifyDynamicType( @@ -3728,7 +3729,7 @@ export function isJsonStringifyDynamicType( getUnion: (unionId: string) => IrUnionDef | undefined, ): boolean { return ( - (t.kind === "record" || t.kind === "array" || t.kind === "union") && + (t.kind === "record" || t.kind === "array" || t.kind === "union" || isDynTypedRefType(t)) && isJsonSafeAt(t, getRecord, getUnion, true, false, new Set(), true) && !isJsonStringifySafeType(t, getRecord, getUnion) ); @@ -3838,8 +3839,12 @@ function isJsonSafeAt( return nativeFields && !stringify; case "bytes": return nativeFields; - case "func": + // Class instances stringify through the runtime JSON traversal, which + // reads their own enumerable properties and calls toJSON. case "object": + if (nativeFields && stringify && isDynTypedRefType(t)) return true; + return false; + case "func": // Class values stringify as "{}" husks in Node (own enumerable statics // aside — not representable type-directedly); rejected like Maps. case "classval": diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6000-json-stringify-class-instances.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6000-json-stringify-class-instances.ts.json new file mode 100644 index 000000000..93c106c07 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6000-json-stringify-class-instances.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6000-json-stringify-class-instances.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6001-json-stringify-js-class-instances.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6001-json-stringify-js-class-instances.js.json new file mode 100644 index 000000000..8ecb709f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6001-json-stringify-js-class-instances.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6001-json-stringify-js-class-instances.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/json-class-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/json-class-fields.ts.json new file mode 100644 index 000000000..ee3ed6bad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/json-class-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/json-class-fields.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b96a19757..0c45d958e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -12818,8 +12818,15 @@ static ScrDyn *scr_native_set_pack(void *ptr) { ScrDyn *scr_dyn_native_set(ScrMap *value) { static const ScrDynHandleOps ops = { - "Set", &scr_map_retain_v, &scr_map_release_v, &scr_native_set_invoke, - &scr_native_set_get, &scr_weak_map_write, NULL, &scr_native_set_pack, + .cls = "Set", + .retain = &scr_map_retain_v, + .release = &scr_map_release_v, + .invoke = &scr_native_set_invoke, + .get = &scr_native_set_get, + .set = &scr_weak_map_write, + .iter_pack = &scr_native_set_pack, + /* Entries live in internal slots: JSON.stringify writes {}, as Node. */ + .json_empty_object = true, }; scr_dyn_handle_install(SCR_DYNH_SET, &ops); return scr_dyn_new_handle(value, SCR_DYNH_SET); @@ -12928,8 +12935,15 @@ static ScrDyn *scr_native_map_pack(void *ptr) { ScrDyn *scr_dyn_native_map(ScrMap *value) { static const ScrDynHandleOps ops = { - "Map", &scr_map_retain_v, &scr_map_release_v, &scr_native_map_invoke, - &scr_native_map_get, &scr_weak_map_write, NULL, &scr_native_map_pack, + .cls = "Map", + .retain = &scr_map_retain_v, + .release = &scr_map_release_v, + .invoke = &scr_native_map_invoke, + .get = &scr_native_map_get, + .set = &scr_weak_map_write, + .iter_pack = &scr_native_map_pack, + /* Entries live in internal slots: JSON.stringify writes {}, as Node. */ + .json_empty_object = true, }; scr_dyn_handle_install(SCR_DYNH_MAP, &ops); return scr_dyn_new_handle(value, SCR_DYNH_MAP); diff --git a/tests/corpus/6000-json-stringify-class-instances.ts b/tests/corpus/6000-json-stringify-class-instances.ts new file mode 100644 index 000000000..a73168b40 --- /dev/null +++ b/tests/corpus/6000-json-stringify-class-instances.ts @@ -0,0 +1,131 @@ +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + norm(): number { + return Math.hypot(this.x, this.y); + } +} + +class Account { + static count = 0; + #secret = "hidden"; + private balance = 10; + protected owner = "ada"; + label: string | undefined = undefined; + tags: string[] = ["a", "b"]; + get secret(): string { + return this.#secret; + } +} + +class Stamp { + at = 5; + toJSON(key: string): string { + return `stamp(${key})`; + } +} + +class Base { + kind = "base"; + toJSON(): { kind: string; via: string } { + return { kind: this.kind, via: "Base.toJSON" }; + } +} +class Child extends Base { + extra = 1; +} + +class Labeled extends Point { + name = "p"; +} + +class Box { + value: T; + constructor(value: T) { + this.value = value; + } +} + +class Registry { + entries = new Map([["a", 1]]); + count = 2; +} + +class Cycle { + name = "loop"; + next: Cycle | null = null; +} + +class Big { + n = 10n; +} + +const tag = Symbol("tag"); + +class Handlers { + name = "h"; + onEvent = (n: number): number => n + 1; + payload: unknown = { a: [1, "two", null] }; + [tag] = "symbol-keyed"; +} + +class Failure extends Error { + code = "E_FAIL"; +} + +class Empty {} + +class Tagged { + s = new Set([1, 2]); + count = 2; +} + +const p = new Point(3, 4); +console.log(JSON.stringify(p)); +console.log(JSON.stringify(new Account())); +console.log(JSON.stringify(new Stamp())); +console.log(JSON.stringify({ first: new Stamp(), list: [new Stamp()] })); +console.log(JSON.stringify(new Child())); +console.log(JSON.stringify(new Labeled(1, 2))); +console.log(JSON.stringify(new Box("text")), JSON.stringify(new Box(new Point(0, 1)))); +console.log(JSON.stringify(new Registry())); +console.log(JSON.stringify([p, null, new Point(5, 6)])); +console.log(JSON.stringify({ point: p, points: [p, p] }, null, 2)); +console.log(JSON.stringify(p, null, "--")); +console.log(JSON.stringify(p, ["x"])); +console.log(JSON.stringify(p, (key: string, value: unknown) => (key === "y" ? undefined : value))); + +const maybe: Point | null = Math.random() < 2 ? p : null; +console.log(JSON.stringify(maybe)); + +const loop = new Cycle(); +loop.next = loop; +try { + JSON.stringify(loop); + console.log("no error"); +} catch (e) { + console.log(e instanceof TypeError); +} + +try { + JSON.stringify(new Big()); + console.log("no error"); +} catch (e) { + console.log(e instanceof TypeError); +} + +p.x = 30; +console.log(JSON.stringify(p)); + +const h = new Handlers(); +console.log(JSON.stringify(h)); +console.log(JSON.stringify(new Empty())); +console.log(JSON.stringify(new Failure("boom"))); +h.payload = 7; +console.log(JSON.stringify({ handlers: [h] })); +const u: unknown = new Map([["a", 1]]); +console.log(JSON.stringify(new Tagged()), JSON.stringify(u)); diff --git a/tests/corpus/6001-json-stringify-js-class-instances.js b/tests/corpus/6001-json-stringify-js-class-instances.js new file mode 100644 index 000000000..d270a632f --- /dev/null +++ b/tests/corpus/6001-json-stringify-js-class-instances.js @@ -0,0 +1,49 @@ +class Box { + constructor(v) { + this.v = v; + } +} + +class Plain {} + +class Late { + constructor() { + this.a = 1; + } + setB() { + this.b = 2; + } + toString() { + return "late"; + } +} + +class Shaped { + constructor(n) { + this.n = n; + } + toJSON() { + return { shaped: this.n }; + } +} + +const b = new Box(1); +console.log(JSON.stringify(b)); +b.extra = "e"; +console.log(JSON.stringify(b)); + +const q = new Plain(); +console.log(JSON.stringify(q)); +q.a = 1; +q.inner = new Box(2); +q.list = [new Box(3), null]; +console.log(JSON.stringify(q)); +console.log(JSON.stringify([q, b], null, 1)); + +const late = new Late(); +console.log(JSON.stringify(late)); +late.c = 3; +late.setB(); +console.log(JSON.stringify(late)); + +console.log(JSON.stringify({ s: new Shaped(4), arr: [new Shaped(5)] })); diff --git a/tests/diagnostics/json-class-fields.ts b/tests/diagnostics/json-class-fields.ts new file mode 100644 index 000000000..81012c161 --- /dev/null +++ b/tests/diagnostics/json-class-fields.ts @@ -0,0 +1,15 @@ +// JSON.stringify reads class instances through their checked-dynamic +// property view. A class whose declared field has no representation in +// that view is refused at compile time, naming the field. +class Schedule { + name = "weekly"; + next = new Date(Date.UTC(2024, 0, 1)); +} + +class Calendar { + title = "team"; + schedules: Schedule[] = [new Schedule()]; +} + +console.log(JSON.stringify(new Schedule())); +console.log(JSON.stringify({ calendar: new Calendar() })); diff --git a/tests/harness/__snapshots__/json-class-fields.ts.txt b/tests/harness/__snapshots__/json-class-fields.ts.txt new file mode 100644 index 000000000..568cfa47f --- /dev/null +++ b/tests/harness/__snapshots__/json-class-fields.ts.txt @@ -0,0 +1,13 @@ +json-class-fields.ts:14:28 - error SC1090: JSON.stringify of 'Schedule' values (field 'next' of class 'Schedule' has type 'Date', which the runtime JSON traversal cannot read) are not supported yet + + 13 | + 14 | console.log(JSON.stringify(new Schedule())); + | ^~~~~~~~~~~~~~ + 15 | console.log(JSON.stringify({ calendar: new Calendar() })); + +json-class-fields.ts:15:28 - error SC1090: JSON.stringify of '{ calendar: Calendar }' values (field 'next' of class 'Schedule' has type 'Date', which the runtime JSON traversal cannot read) are not supported yet + + 14 | console.log(JSON.stringify(new Schedule())); + 15 | console.log(JSON.stringify({ calendar: new Calendar() })); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 16 | \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 18147374d..2dd8a90db 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2201,6 +2201,8 @@ "5995-array-dense-loop-boundaries.ts": 2596, "5996-rejections-between-macrotasks.cjs": 3034, "600-closures-basic.ts": 2349, + "6000-json-stringify-class-instances.ts": 7567, + "6001-json-stringify-js-class-instances.js": 4668, "601-closures-loops.ts": 1788, "602-closures-identity-recursion.ts": 2372, "6020-class-extends-function-constructor.js": 4300, @@ -5241,6 +5243,8 @@ "5995-array-dense-loop-boundaries.ts": 4171, "5996-rejections-between-macrotasks.cjs": 22172, "600-closures-basic.ts": 2534, + "6000-json-stringify-class-instances.ts": 12107, + "6001-json-stringify-js-class-instances.js": 7469, "601-closures-loops.ts": 2297, "602-closures-identity-recursion.ts": 2427, "6020-class-extends-function-constructor.js": 7500, From 2f40bcb8d4237764aaa9c43bf06b632161c8ad4b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 17:46:31 -0500 Subject: [PATCH 071/120] Lower iterator helpers on generators and Map and Set iterators map, filter, take, drop, flatMap, toArray, forEach, reduce, some, every and find compiled only as one chain from arr.values() or arr.entries() that ended in a consuming method. Every other form was refused, and in JavaScript the refusal was thrown at run time. IteratorObject now maps to the generator type. A consuming method over a chain of stages fuses into one loop over the source: a generator driven like for-of, a Map or Set walked live, or an array by index. A stage on its own builds a helper object: a generator over its source that supports next(), for...of, spreads, Array.from and yield*. A for...of whose head is a chain of stages without flatMap runs the stages inside the loop. The source is closed where Node closes it: when take reaches its count, when some, every or find has its answer, when a callback or the loop body throws, when the loop exits early, and before the RangeError of an invalid take or drop budget. It is not closed when it is exhausted or its own next() throws. return() on a helper object before its first next() closes the source through a new runtime hook, scr_gen_set_unstarted_return. When closing an iterator throws while another error propagates, the first error reaches the caller, as in Node. This applies to for...of over a generator too, where the error from the close used to replace the body's error. The try statement takes a dropFinallyErrors flag, whose cleanup handler calls the new scr_exc_keep_first. throw() lowers only on values typed as generators. Iterator helpers and the built-in iterators have no throw method, and a value typed as an iterator may hold one. Callbacks in JavaScript bind their unannotated parameters to the element type, and flatMap callbacks may return generators. A generator whose record yields share checked storage with its return channel has each value converted to the declared element type as it is pulled. --- docs/content/docs/limitations.mdx | 2 + .../builtin-apis/iterator-helpers.mdx | 1 + .../limitations/builtin-apis/map-and-set.mdx | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 47 +- packages/compiler/src/backend/mangle.ts | 5 + .../compiler/src/coverage/surface-manifest.ts | 4 +- .../frontend/lowering/containers/for-of.ts | 2 +- .../lowering/containers/iterator-helpers.ts | 2025 +++++++++++++---- .../src/frontend/lowering/lower-calls.ts | 26 +- .../src/frontend/lowering/lower-generators.ts | 33 +- .../src/frontend/lowering/lower-stmts.ts | 7 + packages/compiler/src/frontend/type-mapper.ts | 5 +- packages/compiler/src/ir/ir.ts | 15 +- packages/compiler/surface-manifest.json | 12 +- .../5908-iterator-helper-objects.ts.json | 6 + .../5909-iterator-helper-objects-js.js.json | 6 + packages/runtime/src/scr_async.c | 11 +- packages/runtime/src/scr_exception.c | 11 + packages/runtime/src/scr_exception.h | 4 + packages/runtime/src/scr_generator.h | 3 + tests/corpus/5908-iterator-helper-objects.ts | 214 ++ .../corpus/5909-iterator-helper-objects-js.js | 58 + .../collection-iterator-helpers.ts | 17 +- .../collection-iterator-helpers.ts.txt | 42 +- .../__snapshots__/es2025-fences.ts.txt | 11 +- .../string-iterator-helpers.ts.txt | 11 +- tests/harness/corpus-costs.json | 4 + 27 files changed, 2003 insertions(+), 581 deletions(-) create mode 100644 docs/content/limitations/builtin-apis/iterator-helpers.mdx create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json create mode 100644 tests/corpus/5908-iterator-helper-objects.ts create mode 100644 tests/corpus/5909-iterator-helper-objects-js.js diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 6f21e2ce3..c15ff67cc 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -120,6 +120,8 @@ title: "Limitations" ../limitations/builtin-apis/synchronous-iteration.mdx +../limitations/builtin-apis/iterator-helpers.mdx + ../limitations/builtin-apis/regular-expressions.mdx ../limitations/builtin-apis/object-freezing.mdx diff --git a/docs/content/limitations/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx new file mode 100644 index 000000000..01f779cb6 --- /dev/null +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -0,0 +1 @@ +- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported. diff --git a/docs/content/limitations/builtin-apis/map-and-set.mdx b/docs/content/limitations/builtin-apis/map-and-set.mdx index 04ebcbfca..12be44e06 100644 --- a/docs/content/limitations/builtin-apis/map-and-set.mdx +++ b/docs/content/limitations/builtin-apis/map-and-set.mdx @@ -1 +1 @@ -- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map` and `Set`, and promises requiring payload adapters, cannot pass into these slots. `Promise` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. +- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map` and `Set`, and promises requiring payload adapters, cannot pass into these slots. `Promise` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 84614b6cb..bac2818be 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -165,6 +165,7 @@ import { mangleFnClosure, mangleFunction, mangleGenDrop, + mangleGenSourceClose, mangleGenSpawn, mangleGlobal, mangleLocal, @@ -4623,6 +4624,25 @@ export class LlEmitter { dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``); for (const line of dr) out.push(line); + // An iterator helper's return() before its first next() closes the + // source generator in its first argument slot. + const closesSource = fn.generator.closesSource === true; + if (closesSource) { + const field = lifted ? 1 : 0; + this.declare(`declare void @scr_gen_close(ptr)`); + this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`); + out.push( + `define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`, + `entry:`, + ` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`, + ` %sv = load ptr, ptr %sp`, + ` call void @scr_gen_close(ptr %sv)`, + ` ret void`, + `}`, + ``, + ); + } + // Spawn wrapper: pack the args (+1 moves in), allocate the // SUSPENDED fiber — nothing runs until the first .next(). if (lifted) { @@ -4639,6 +4659,11 @@ export class LlEmitter { settleAsync === null ? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, + ...(closesSource + ? [ + ` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`, + ] + : []), ` ret ptr %gg`, `}`, ``, @@ -9517,14 +9542,15 @@ export class LlEmitter { B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`); B.line(`store ptr ${stash}, ptr ${stashSlot}`); this.scopes.push([{ slot: stashSlot, type: CAUGHT }]); - const suppressHandler = s.suppressFinallyErrors - ? { - label: B.newLabel("try.fs"), - used: false, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - } - : null; + const suppressHandler = + s.suppressFinallyErrors || s.dropFinallyErrors + ? { + label: B.newLabel("try.fs"), + used: false, + frameDepth: this.frames.length, + scopeDepth: this.scopes.length, + } + : null; if (suppressHandler) this.tryStack.push(suppressHandler); this.emitBlock(s.finallyBody!); if (suppressHandler) this.tryStack.pop(); @@ -9534,8 +9560,9 @@ export class LlEmitter { this.emitUnwind(); if (suppressHandler?.used) { B.startBlock(suppressHandler.label); - this.declare(`declare void @scr_exc_suppress(ptr)`); - B.line(`call void @scr_exc_suppress(ptr ${stash}) ; consumes stash`); + const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress"; + this.declare(`declare void @${cleanup}(ptr)`); + B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`); this.emitUnwind(); } } diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index 0bc428964..43c9cea71 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -204,6 +204,11 @@ export function mangleGenSpawn(fnName: string): string { export function mangleGenDrop(fnName: string): string { return `sc_gd_${sanitize(fnName)}`; } +/** The never-started return() hook of a generator that closes its source + * argument (an iterator helper). */ +export function mangleGenSourceClose(fnName: string): string { + return `sc_gc_${sanitize(fnName)}`; +} /** Emitted ref-kind resolve thunk, interned per inner-type key. */ export function mangleResolveThunk(n: number): string { return `sc_rslv_${n}`; diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 45814cb8b..3477e1847 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -275,7 +275,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes status: "static", ...(COLLECTION_ITERATOR_METHODS.has(name) ? { - note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused", + note: "live native iterators support next(), for...of, drains, and the iterator helper methods", } : {}), }); @@ -288,7 +288,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes status: "static", ...(COLLECTION_ITERATOR_METHODS.has(name) ? { - note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused", + note: "live native iterators support next(), for...of, drains, and the iterator helper methods", } : {}), }); diff --git a/packages/compiler/src/frontend/lowering/containers/for-of.ts b/packages/compiler/src/frontend/lowering/containers/for-of.ts index 4aa24b0b3..6aa838dea 100644 --- a/packages/compiler/src/frontend/lowering/containers/for-of.ts +++ b/packages/compiler/src/frontend/lowering/containers/for-of.ts @@ -747,7 +747,7 @@ function lowerForOfMapOrSet( * Recurses through every nested statement list; nested functions are * lifted elsewhere and never appear here, so every return found exits * THIS function through the active iteration. */ -function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] { +export function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] { const walk = (list: IrStmt[]): IrStmt[] => list.flatMap((s): IrStmt[] => { switch (s.kind) { diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index f0928d2a1..7cb3753ac 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -4,7 +4,10 @@ import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { BOOL, + CAUGHT, F64, + type IrMapIntrinsicMethod, + type IrSetIntrinsicMethod, type IrExpr, type IrFunction, type IrLocal, @@ -16,11 +19,22 @@ import { UNDEFINED_T, VOID, arrayOf, + isUnitType, typeEquals, } from "../../../ir/ir.js"; import { locOf } from "../../program.js"; -import { typeKey } from "../../type-mapper.js"; +import { genResultRecord, typeKey } from "../../type-mapper.js"; import { concatStrings, throwTypeError } from "./dynamic-receivers.js"; +import { + type GenType, + generatorLoopBinding, + genStep, + genValue, + stepDrivable, +} from "../lower-generators.js"; +import { exitBeforeReturns } from "./for-of.js"; +import { iteratorSlotAdapter } from "../iterator-adapters.js"; +import { lowerArrayCallback } from "./callback-arguments.js"; /* ── iterator helpers (ES2025) ─────────────────────────────────────────── */ @@ -37,9 +51,9 @@ import { concatStrings, throwTypeError } from "./dynamic-receivers.js"; * raw value in the message) through an interned checker call inserted * at the argument's evaluation position. The source array is iterated * LIVE by index against a re-read length, exactly the array iterator's - * contract. Everything else — iterator objects stored in bindings, - * generator/Set/Map receivers, Iterator.from — keeps the lib fences. - * Null when the callee isn't a terminal over such a chain. */ + * contract. Chains over generators and other iterator objects lower in + * lowerGeneratorHelperCall. Null when the callee isn't a terminal over + * such a chain. */ export function lowerIteratorHelperCall( lowerer: Lowerer, call: ts.CallExpression, @@ -51,7 +65,7 @@ export function lowerIteratorHelperCall( // Walk receiver: stage* ← values()/entries() ← array-typed source // (entries seeds the chain with [index, element] pairs — the // checker's own tuple type). - const stages: { name: string; argNode: ts.Expression }[] = []; + const stages: StageNode[] = []; let cur: ts.Expression = access.expression; let source: ts.Expression | null = null; let srcProj: "values" | "entries" = "values"; @@ -73,16 +87,14 @@ export function lowerIteratorHelperCall( ts.isSpreadElement(cur.arguments[0]!) ) return null; - stages.push({ name, argNode: cur.arguments[0]! }); + stages.push({ name, argNode: cur.arguments[0]!, access: cur.expression }); cur = cur.expression.expression; } stages.reverse(); // source order for (const s of stages) { - const acc = findStageAccess(call, s); - if (acc !== null && !lowerer.isStdlibMember(acc)) return null; + if (!lowerer.isStdlibMember(s.access)) return null; } if (!lowerer.isStdlibMember(access)) return null; - const loc = locOf(call); // Lower everything in SOURCE ORDER (the chain's own evaluation // order): the source array, each stage argument, the terminal's. const items = lowerer.lowerExpr(source); @@ -90,81 +102,502 @@ export function lowerIteratorHelperCall( // The entries() seed: the interned [number, T] tuple — the same shape // the checker's own [number, T] maps to, so stage callbacks typed // against the lib's pairs intern equal. - let elem: IrType = - srcProj === "entries" - ? { - kind: "record", - shapeId: lowerer.shapes.intern( - [ - { name: "0", type: F64 }, - { name: "1", type: items.type.elem }, - ], - true, - ), - } - : items.type.elem; - type StageIr = - | { - kind: "map" | "filter" | "flatMap"; - fn: IrExpr & { type: IrType & { kind: "func" } }; - out: IrType; - } - | { kind: "take" | "drop"; budget: IrExpr }; - const stageIrs: StageIr[] = []; - for (const s of stages) { - if (s.name === "take" || s.name === "drop") { - const n = lowerer.lowerExpr(s.argNode); - if (n.type.kind !== "f64") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); - // Node validates at the take()/drop() call — the checked budget - // rides an interned helper call AT this argument position. - stageIrs.push({ + const elem: IrType = + srcProj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem; + return lowerFusedChain( + lowerer, + call, + terminal, + stages, + { kind: "array", type: items.type, proj: srcProj }, + items, + elem, + ); +} + +/** Iterator-helper calls whose receiver is an iterator object: a + * generator, a stored array iterator, a helper result (IteratorObject), + * an `Iterator` value, or a Map/Set iterator (adapted into a + * generator over the live collection). + * + * - A terminal (toArray/forEach/reduce/some/every/find) fuses the chain + * of helper stages before it into one loop over the source generator, + * with the helpers' pull order and closing: the source is closed (its + * `return()` runs) when take's budget is spent, a short-circuit + * terminal hits, or a callback throws, and is not closed when it is + * exhausted or its own `next()` throws. + * - A stage (map/filter/take/drop/flatMap) outside such a chain builds + * an iterator helper object: a generator over its source that pulls + * lazily, closes the source on early exit, and closes it on a + * `return()` before the first `next()` (`closesSource`). + * + * Budgets and callbacks validate at the call, as in Node. Null when the + * call is not such a helper call. */ +export function lowerGeneratorHelperCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (call.questionDotToken || access.questionDotToken) return null; + const name = access.name.text; + const isTerminal = ITER_TERMINALS.has(name); + if (!isTerminal && !ITER_STAGES.has(name)) return null; + if (!isHelperReceiver(lowerer, access.expression)) return null; + if (!lowerer.isStdlibMember(access)) return null; + const loc = locOf(call); + if (!isTerminal) { + const argNode = call.arguments[0]; + if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + lowerer.noLowering(`.${name} with ${call.arguments.length} arguments`, call); + } + const src = lowerHelperSource(lowerer, access.expression); + if (name !== "take" && name !== "drop") { + const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc); + return lowerHelperObject(lowerer, call, src.gen, src.elem, stage, out, loc); + } + // The budget check closes the source, so the source is bound first. + const bound = bindSource(lowerer, src.gen, loc); + const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc, bound.ref); + const helper = lowerHelperObject(lowerer, call, bound.ref, src.elem, stage, out, loc); + return { kind: "seqExpr", stmts: [bound.decl], result: helper, type: helper.type, loc }; + } + // The stages before the terminal, back to the first receiver that is + // not itself a helper stage. + const stages: StageNode[] = []; + let cur: ts.Expression = access.expression; + while (isStageCall(lowerer, cur)) { + const stageAccess = cur.expression as ts.PropertyAccessExpression; + stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + cur = stageAccess.expression; + } + stages.reverse(); // source order + // A chain from m.keys()/values()/entries() (or a Set's) walks the + // collection itself, live, as for-of does. + const collection = collectionIteratorCall(lowerer, cur); + if (collection !== null) { + const items = lowerer.lowerExpr(collection.receiver); + if (items.type.kind !== "map" && items.type.kind !== "set") + lowerer.badType(collection.receiver, lowerer.typeOf(collection.receiver)); + const source: ChainSource = { kind: "collection", type: items.type, proj: collection.proj }; + return lowerFusedChain( + lowerer, + call, + name, + stages, + source, + items, + collectionSeed(lowerer, source), + ); + } + const src = lowerHelperSource(lowerer, cur); + return lowerFusedChain( + lowerer, + call, + name, + stages, + { kind: "generator", type: src.gen.type, elem: src.elem }, + src.gen, + src.elem, + ); +} + +const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]); +const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]); +/** Native iterators that ride checked runtime values and adapt into + * generators for the helpers. */ +const ADAPTED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator"]); + +/** `for (x of )` where the head is a run of map/filter/take/drop + * stages over an array iterator, a Map or Set iterator, or a generator: + * one loop over the source with the stages nested in it and the loop body + * innermost, instead of a helper object per stage. The pull order and the + * closing are the helpers': `break`, `return` and throws from the body + * close a generator source (run its `return()`), and take closes it on + * the pass after its last delivery. Chains with flatMap keep the helper + * objects (a `break` there leaves an inner loop). Null when the head is + * not such a chain. */ +export function lowerForOfHelperChain( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + labels: string[] | undefined, +): IrStmt | null { + if (stmt.awaitModifier) return null; + if ( + ts.isVariableDeclarationList(stmt.initializer) && + (stmt.initializer.flags & ts.NodeFlags.Using) !== 0 + ) + return null; + let head: ts.Expression = stmt.expression; + while (ts.isParenthesizedExpression(head)) head = head.expression; + const stages: StageNode[] = []; + let cur: ts.Expression = head; + while (isStageCall(lowerer, cur)) { + const stageAccess = cur.expression as ts.PropertyAccessExpression; + if (stageAccess.name.text === "flatMap") return null; + stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + cur = stageAccess.expression; + } + if (stages.length === 0) return null; + stages.reverse(); // source order + const arrayRoot = arrayIteratorCall(lowerer, cur); + const collection = arrayRoot === null ? collectionIteratorCall(lowerer, cur) : null; + if (arrayRoot === null && collection === null) { + const mapped = lowerer.mapTypeOf(lowerer.typeOf(cur)); + if (mapped?.kind !== "generator" || !stepDrivable(mapped)) return null; + } + const loc = locOf(stmt); + lowerer.scopes.push(new Map()); + try { + const frame = new HostFrame(lowerer, loc); + let items: IrExpr; + let source: ChainSource; + let elem: IrType; + if (arrayRoot !== null) { + items = lowerer.lowerExpr(arrayRoot.receiver); + if (items.type.kind !== "array") + lowerer.badType(arrayRoot.receiver, lowerer.typeOf(arrayRoot.receiver)); + source = { kind: "array", type: items.type, proj: arrayRoot.proj }; + elem = + arrayRoot.proj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem; + } else if (collection !== null) { + items = lowerer.lowerExpr(collection.receiver); + if (items.type.kind !== "map" && items.type.kind !== "set") + lowerer.badType(collection.receiver, lowerer.typeOf(collection.receiver)); + source = { kind: "collection", type: items.type, proj: collection.proj }; + elem = collectionSeed(lowerer, source); + } else { + const src = lowerHelperSource(lowerer, cur); + items = src.gen; + source = { kind: "generator", type: src.gen.type, elem: src.elem }; + elem = src.elem; + } + const itemsId = frame.local("items", items.type, false); + const out: IrStmt[] = [{ kind: "varDecl", localId: itemsId, init: items, loc }]; + // Stage arguments evaluate in chain order, before the first pull. + const slots: StageSlot[] = []; + for (const s of stages) { + const { stage, out: next } = lowerStage( + lowerer, + s, + elem, + loc, + source.kind === "generator" + ? (varRef(itemsId, source.type, loc) as IrExpr & { type: GenType }) + : null, + ); + const arg = "budget" in stage ? stage.budget : stage.fn; + const argId = frame.local("budget" in stage ? "n" : "f", arg.type, false); + out.push({ kind: "varDecl", localId: argId, init: arg, loc }); + slots.push(stageSlot(frame, stage, argId)); + elem = next; + } + const doneId = frame.local("done", BOOL, true); + out.push(...stagePrologue(slots, doneId, loc)); + // Each delivered value lands in a hidden local the loop binding reads. + const valueId = frame.local("hv", elem, false); + const binds = generatorLoopBinding(lowerer, stmt, varRef(valueId, elem, loc), elem); + let body = lowerer.inCtl( + "loop", + () => lowerer.lowerScopedBlock(stmt.statement), + source.kind === "collection" ? undefined : labels, + ); + if (source.kind === "collection") { + // A return from the body ends the live walk first, as for-of does. + const t = source.type; + body = exitBeforeReturns(body, () => ({ + kind: "exprStmt", + expr: + t.kind === "map" + ? { + kind: "mapIntrinsic", + method: "iterExit", + receiver: varRef(itemsId, t, loc), + args: [], + type: VOID, + loc, + } + : { + kind: "setIntrinsic", + method: "iterExit", + receiver: varRef(itemsId, t, loc), + args: [], + type: VOID, + loc, + }, + loc, + })); + } + const emit = foldStages( + lowerer, + frame, + slots, + (value) => [{ kind: "varDecl", localId: valueId, init: value, loc }, ...binds, ...body], + doneId, + loc, + ); + out.push( + ...sourceLoop( + lowerer, + frame, + itemsId, + source, + doneId, + emit, + loc, + source.kind === "collection" ? undefined : labels, + ), + ); + return { kind: "block", body: out, loc }; + } finally { + lowerer.scopes.pop(); + } +} + +/** `arr.values()` or `arr.entries()` on an array value, as the root of a + * helper chain. */ +function arrayIteratorCall( + lowerer: Lowerer, + expr: ts.Expression, +): { receiver: ts.Expression; proj: "values" | "entries" } | null { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; + if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + return null; + const proj = expr.expression.name.text; + if (proj !== "values" && proj !== "entries") return null; + if (lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression))?.kind !== "array") return null; + if (!lowerer.isStdlibMember(expr.expression)) return null; + return { receiver: expr.expression.expression, proj }; +} + +/** One helper stage call in a chain, in source order. */ +interface StageNode { + name: string; + argNode: ts.Expression; + access: ts.PropertyAccessExpression; +} + +/** A lowered stage: its callback, or its validated budget. */ +type StageIr = + | { + kind: "map" | "filter" | "flatMap"; + fn: IrExpr & { type: IrType & { kind: "func" } }; + } + | { kind: "take" | "drop"; budget: IrExpr }; + +/** Where a fused chain's elements come from: a live array (by index), or + * a synchronous generator driven like for-of. */ +type ChainSource = + | { kind: "array"; type: IrType & { kind: "array" }; proj: "values" | "entries" } + | { kind: "generator"; type: GenType; elem: IrType } + | { + kind: "collection"; + type: (IrType & { kind: "map" }) | (IrType & { kind: "set" }); + proj: "keys" | "values" | "entries"; + }; + +/** `m.keys()`, `m.values()` or `m.entries()` (or a Set's) on a Map or + * Set value, as the root of a helper chain. */ +function collectionIteratorCall( + lowerer: Lowerer, + expr: ts.Expression, +): { receiver: ts.Expression; proj: "keys" | "values" | "entries" } | null { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; + if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + return null; + const proj = expr.expression.name.text; + if (proj !== "keys" && proj !== "values" && proj !== "entries") return null; + const kind = lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression))?.kind; + if (kind !== "map" && kind !== "set") return null; + if (!lowerer.isStdlibMember(expr.expression)) return null; + return { receiver: expr.expression.expression, proj }; +} + +/** The element a collection chain starts from: a key (a Set's element), + * a Map's value, or the interned [key, value] pair (a Set's [v, v]). */ +function collectionSeed(lowerer: Lowerer, source: ChainSource & { kind: "collection" }): IrType { + const t = source.type; + const keyT = t.kind === "map" ? t.key : t.elem; + if (source.proj === "keys" || (t.kind === "set" && source.proj === "values")) return keyT; + if (source.proj === "values") return (t as IrType & { kind: "map" }).value; + return { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: keyT }, + { name: "1", type: t.kind === "map" ? t.value : keyT }, + ], + true, + ), + }; +} + +/** The interned [number, T] pair an array's entries() yields. */ +function entryPairType(lowerer: Lowerer, elem: IrType): IrType { + return { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: F64 }, + { name: "1", type: elem }, + ], + true, + ), + }; +} + +/** Whether `expr` is an iterator object the helpers lower over: a + * synchronous generator-typed value, or a Map/Set iterator. */ +function isHelperReceiver(lowerer: Lowerer, expr: ts.Expression): boolean { + const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (mapped?.kind === "generator") return mapped.async !== true; + const symbol = lowerer.typeOf(expr).getSymbol(); + return ( + mapped?.kind === "dyn" && + symbol !== undefined && + ADAPTED_ITERATOR_TYPES.has(symbol.name) && + lowerer.isStdlibSymbol(symbol) + ); +} + +/** Whether `expr` is a stage call (`src.map(f)`) on a helper receiver. */ +function isStageCall(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpression { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return false; + if (expr.questionDotToken || expr.expression.questionDotToken) return false; + if (!ITER_STAGES.has(expr.expression.name.text)) return false; + if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) return false; + return ( + isHelperReceiver(lowerer, expr.expression.expression) && lowerer.isStdlibMember(expr.expression) + ); +} + +/** The source of a helper call as a synchronous generator — the receiver + * itself, or a Map/Set iterator adapted into a generator over the live + * collection — and its element type. A generator whose yield channel is + * checked-dynamic beside a declared element type (`Generator

` boxes a + * record yield next to its `any` return channel) has each value converted + * to the declared type as it is pulled. */ +function lowerHelperSource( + lowerer: Lowerer, + expr: ts.Expression, +): { gen: IrExpr & { type: GenType }; elem: IrType } { + const tsType = lowerer.typeOf(expr); + const mapped = lowerer.mapTypeOf(tsType); + const arg = lowerer.checker.getTypeArguments(tsType as ts.TypeReference)[0]; + if (mapped?.kind === "generator") { + const gen = lowerer.lowerExpr(expr); + if (gen.type.kind !== "generator" || gen.type.async) lowerer.badType(expr, tsType); + const genT = gen.type; + const declared = genT.yieldT.kind === "dyn" && arg ? lowerer.mapTypeOf(arg) : null; + const elem = + declared !== null && + declared.kind !== "dyn" && + declared.kind !== "void" && + !isUnitType(declared) + ? declared + : genT.yieldT; + return { gen: gen as IrExpr & { type: GenType }, elem }; + } + const yieldT = arg ? lowerer.mapTypeOf(arg) : null; + if ( + !yieldT || + yieldT.kind === "void" || + !genResultRecord(yieldT, VOID, lowerer.shapes, lowerer.unions) + ) + lowerer.badType(expr, tsType); + const target: GenType = { kind: "generator", yieldT, retT: VOID, nextT: UNDEFINED_T }; + const adapted = iteratorSlotAdapter(lowerer, lowerer.lowerExpr(expr), target); + if (adapted === null || adapted.type.kind !== "generator") lowerer.badType(expr, tsType); + return { gen: adapted as IrExpr & { type: GenType }, elem: yieldT }; +} + +/** Lower one stage's argument against its input element type: the + * callback (value) or (value, counter), or the take/drop budget with + * Node's eager RangeError check at the argument's position. `out` is + * the stage's output element type. */ +function lowerStage( + lowerer: Lowerer, + s: StageNode, + elem: IrType, + loc: SrcLoc, + source: (IrExpr & { type: GenType }) | null = null, +): { stage: StageIr; out: IrType } { + if (s.name === "take" || s.name === "drop") { + const n = lowerer.lowerExpr(s.argNode); + if (n.type.kind !== "f64") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); + // Node validates at the take()/drop() call — the checked budget + // rides an interned helper call AT this argument position. A + // generator source is closed before the RangeError, as Node closes + // the iterator the helper was called on. + return { + stage: { kind: s.name, budget: { kind: "call", - callee: internBudgetChecker(lowerer, loc), - args: [n], + callee: internBudgetChecker(lowerer, loc, source?.type ?? null), + args: source === null ? [n] : [n, source], type: F64, loc, }, - }); - continue; - } - const f = lowerer.lowerExpr(s.argNode); - if ( - f.type.kind !== "func" || - f.type.params.length > 2 || - (f.type.params.length >= 1 && !typeEquals(f.type.params[0]!, elem)) || - (f.type.params.length === 2 && f.type.params[1]!.kind !== "f64") - ) { - lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); - } - const fn = f as IrExpr & { type: IrType & { kind: "func" } }; - if (s.name === "map") { - stageIrs.push({ kind: "map", fn, out: fn.type.ret }); - elem = fn.type.ret; - } else if (s.name === "filter") { - if (fn.type.ret.kind !== "bool") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); - stageIrs.push({ kind: "filter", fn, out: elem }); - } else { - if (fn.type.ret.kind !== "array") { - lowerer.noLowering( - `.flatMap over a callback returning '${lowerer.fmt(fn.type.ret)}'`, - s.argNode, - "an array-returning callback is the supported form (iterators/strings have no lowering here)", - ); - } - stageIrs.push({ kind: "flatMap", fn, out: fn.type.ret.elem }); - elem = fn.type.ret.elem; - } + }, + out: elem, + }; } - // The terminal's own arguments. + // A flatMap callback returns its own array or generator type (an + // unannotated JavaScript arrow would otherwise return checked values). + let expectedReturn: IrType | undefined; + if (s.name === "flatMap") { + const sig = lowerer.checker.getCallSignatures(lowerer.typeOf(s.argNode))[0]; + const ret = sig ? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(sig)) : null; + if (ret?.kind === "array" || ret?.kind === "generator") expectedReturn = ret; + } + // Unannotated JavaScript parameters bind the element type. + const f = lowerArrayCallback( + lowerer, + s.argNode, + [elem], + UNDEFINED_T, + false, + expectedReturn, + ).fnArg; + if ( + f.type.kind !== "func" || + f.type.params.length > 2 || + (f.type.params.length >= 1 && !typeEquals(f.type.params[0]!, elem)) || + (f.type.params.length === 2 && f.type.params[1]!.kind !== "f64") + ) { + lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); + } + const fn = f as IrExpr & { type: IrType & { kind: "func" } }; + if (s.name === "map") return { stage: { kind: "map", fn }, out: fn.type.ret }; + if (s.name === "filter") { + if (fn.type.ret.kind !== "bool") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); + return { stage: { kind: "filter", fn }, out: elem }; + } + const ret = fn.type.ret; + if (ret.kind === "array") return { stage: { kind: "flatMap", fn }, out: ret.elem }; + if (ret.kind === "generator" && ret.async !== true && stepDrivable(ret)) + return { stage: { kind: "flatMap", fn }, out: ret.yieldT }; + lowerer.noLowering( + `.flatMap over a callback returning '${lowerer.fmt(ret)}'`, + s.argNode, + "callbacks returning an array or a generator are the supported forms", + ); +} + +/** The terminal's arguments and result type over elements of `elem`. */ +function lowerTerminal( + lowerer: Lowerer, + call: ts.CallExpression, + terminal: string, + elem: IrType, +): { termArgs: IrExpr[]; resultT: IrType } { const termArgs: IrExpr[] = []; - let resultT: IrType; if (terminal === "toArray") { if (call.arguments.length !== 0) lowerer.noLowering(`.toArray with ${call.arguments.length} arguments`, call); - resultT = arrayOf(elem); - } else if ( + return { termArgs, resultT: arrayOf(elem) }; + } + if ( terminal === "forEach" || terminal === "some" || terminal === "every" || @@ -174,7 +607,7 @@ export function lowerIteratorHelperCall( if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { lowerer.noLowering(`.${terminal} with ${call.arguments.length} arguments`, call); } - const f = lowerer.lowerExpr(argNode); + const f = lowerArrayCallback(lowerer, argNode, [elem], UNDEFINED_T).fnArg; if ( f.type.kind !== "func" || f.type.params.length > 2 || @@ -185,43 +618,84 @@ export function lowerIteratorHelperCall( lowerer.badType(argNode, lowerer.typeOf(argNode)); } termArgs.push(f); - resultT = - terminal === "forEach" ? VOID : terminal === "find" ? lowerer.withUndefinedArm(elem) : BOOL; - } else { - // reduce(f[, initial]) - const fNode = call.arguments[0]; - if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - fNode === undefined || - call.arguments.some((a) => ts.isSpreadElement(a)) - ) { - lowerer.noLowering(`.reduce with ${call.arguments.length} arguments`, call); - } - const f = lowerer.lowerExpr(fNode); - if ( - f.type.kind !== "func" || - f.type.params.length < 2 || - f.type.params.length > 3 || - !typeEquals(f.type.params[1]!, elem) || - (f.type.params.length === 3 && f.type.params[2]!.kind !== "f64") || - !typeEquals(f.type.params[0]!, f.type.ret) - ) { - lowerer.badType(fNode, lowerer.typeOf(fNode)); - } - const accT = (f.type as IrType & { kind: "func" }).ret; - if (call.arguments.length === 1 && !typeEquals(accT, elem)) { - lowerer.noLowering( - ".reduce without an initial value where the accumulator's type differs from the elements'", - call, - "pass the initial value: .reduce(f, init)", - ); - } - termArgs.push(f); - if (call.arguments[1] !== undefined) - termArgs.push(lowerer.lowerExprExpecting(call.arguments[1], accT)); - resultT = accT; + return { + termArgs, + resultT: + terminal === "forEach" ? VOID : terminal === "find" ? lowerer.withUndefinedArm(elem) : BOOL, + }; } + // reduce(f[, initial]) + const fNode = call.arguments[0]; + if ( + call.arguments.length < 1 || + call.arguments.length > 2 || + fNode === undefined || + call.arguments.some((a) => ts.isSpreadElement(a)) + ) { + lowerer.noLowering(`.reduce with ${call.arguments.length} arguments`, call); + } + // The accumulator binds the checker's result type. + const accLead = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? elem; + const f = lowerArrayCallback(lowerer, fNode, [accLead, elem], UNDEFINED_T).fnArg; + if ( + f.type.kind !== "func" || + f.type.params.length < 2 || + f.type.params.length > 3 || + !typeEquals(f.type.params[1]!, elem) || + (f.type.params.length === 3 && f.type.params[2]!.kind !== "f64") || + !typeEquals(f.type.params[0]!, f.type.ret) + ) { + lowerer.badType(fNode, lowerer.typeOf(fNode)); + } + const accT = (f.type as IrType & { kind: "func" }).ret; + if (call.arguments.length === 1 && !typeEquals(accT, elem)) { + lowerer.noLowering( + ".reduce without an initial value where the accumulator's type differs from the elements'", + call, + "pass the initial value: .reduce(f, init)", + ); + } + termArgs.push(f); + if (call.arguments[1] !== undefined) + termArgs.push(lowerer.lowerExprExpecting(call.arguments[1], accT)); + return { termArgs, resultT: accT }; +} + +/** A terminal over a chain of stages from `source`: one interned fused + * function, called with the source, each stage's budget or callback and + * the terminal's arguments (lowered in that, the chain's own, order). */ +function lowerFusedChain( + lowerer: Lowerer, + call: ts.CallExpression, + terminal: string, + stages: StageNode[], + source: ChainSource, + items: IrExpr, + seed: IrType, +): IrExpr { + const loc = locOf(call); + if (source.kind === "generator" && !stepDrivable(source.type)) { + lowerer.noLowering( + `.${terminal} over an iterator yielding '${lowerer.fmt(source.type.yieldT)}'`, + call, + "helpers over iterators of numbers, booleans, strings, arrays, records and class instances are the supported forms", + ); + } + // A take/drop budget check closes a generator source, so the source is + // bound before the stage arguments. + const bound = + source.kind === "generator" && stages.some((s) => s.name === "take" || s.name === "drop") + ? bindSource(lowerer, items as IrExpr & { type: GenType }, loc) + : null; + if (bound !== null) items = bound.ref; + let elem = seed; + const stageIrs: StageIr[] = []; + for (const s of stages) { + const lowered = lowerStage(lowerer, s, elem, loc, bound?.ref ?? null); + stageIrs.push(lowered.stage); + elem = lowered.out; + } + const { termArgs, resultT } = lowerTerminal(lowerer, call, terminal, elem); // The checker's own result type must agree (annotation drift fences). const checkerT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (terminal !== "forEach" && (checkerT === null || !typeEquals(checkerT, resultT))) { @@ -231,11 +705,14 @@ export function lowerIteratorHelperCall( "the chain's element and result types must be representable — annotate the callbacks' types", ); } - const stageKeys = stageIrs.map((s) => { - if (!s) throw new InternalCompilerError("missing iterator stage"); - return "budget" in s ? s.kind : `${s.kind}:${typeKey(s.fn.type)}`; - }); - const key = `iter:${srcProj}:${typeKey(items.type)}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; + const stageKeys = stageIrs.map((s) => + "budget" in s ? s.kind : `${s.kind}:${typeKey(s.fn.type)}`, + ); + const srcKey = + source.kind === "generator" + ? `${typeKey(source.type)}:${typeKey(source.elem)}` + : `${source.proj}:${typeKey(source.type)}`; + const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; @@ -244,8 +721,7 @@ export function lowerIteratorHelperCall( buildIterChainFn( lowerer, helper, - items.type, - srcProj, + source, stageIrs, terminal, termArgs.map((a) => a.type), @@ -254,35 +730,67 @@ export function lowerIteratorHelperCall( ), ); } - const budgetOrFn = stageIrs.map((s): IrExpr => { - if (!s) throw new InternalCompilerError("missing iterator stage"); - return "budget" in s ? s.budget : s.fn; - }); - return { + const budgetOrFn = stageIrs.map((s): IrExpr => ("budget" in s ? s.budget : s.fn)); + const fused: IrExpr = { kind: "call", callee: helper, args: [items, ...budgetOrFn, ...termArgs], type: resultT, loc, }; + return bound === null + ? fused + : { kind: "seqExpr", stmts: [bound.decl], result: fused, type: resultT, loc }; } -const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]); -const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]); - -/** The ts.PropertyAccessExpression of stage `s` inside the chain under - * `call` — for the stdlib-membership check. Walks the same spine the - * parser walked; null only on shape drift (checked defensively). */ -function findStageAccess( +/** An iterator helper object for one stage over `src`: an interned + * generator `%iter..N(src, f | n)` typed as the call's checker + * type (IteratorObject). */ +function lowerHelperObject( + lowerer: Lowerer, call: ts.CallExpression, - s: { name: string; argNode: ts.Expression }, -): ts.PropertyAccessExpression | null { - let cur: ts.Expression = (call.expression as ts.PropertyAccessExpression).expression; - while (ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) { - if (cur.arguments[0] === s.argNode) return cur.expression; - cur = cur.expression.expression; + src: IrExpr & { type: GenType }, + srcElem: IrType, + stage: StageIr, + out: IrType, + loc: SrcLoc, +): IrExpr { + const outT = lowerer.mapTypeOf(lowerer.typeOf(call)); + const resultType = + outT?.kind === "generator" + ? genResultRecord(outT.yieldT, outT.retT, lowerer.shapes, lowerer.unions) + : null; + if ( + outT?.kind !== "generator" || + outT.async === true || + !typeEquals(outT.yieldT, out) || + outT.retT.kind !== "void" || + resultType === null + ) { + lowerer.noLowering( + `.${stage.kind} at this result type`, + call, + "the helper's element type must be representable — annotate the callback's types", + ); } - return null; + if (!stepDrivable(src.type)) { + lowerer.noLowering( + `.${stage.kind} over an iterator yielding '${lowerer.fmt(src.type.yieldT)}'`, + call, + "helpers over iterators of numbers, booleans, strings, arrays, records and class instances are the supported forms", + ); + } + const arg = "budget" in stage ? stage.budget : stage.fn; + const key = `iterHelper:${stage.kind}:${typeKey(src.type)}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; + let name = lowerer.arrHofHelpers.get(key); + if (!name) { + name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + lowerer.liftedFns.push( + buildHelperGenFn(lowerer, name, src.type, srcElem, stage, arg.type, outT, resultType, loc), + ); + } + return { kind: "call", callee: name, args: [src, arg], type: outT, loc }; } /** The eager take/drop budget check, interned once per program: @@ -292,11 +800,11 @@ function findStageAccess( * Node's exact behavior: NaN and negative INTEGER budgets throw (the * message carries the RAW argument — take(-1.5) says "-1.5"), -0.5 * truncates to 0 and passes, Infinity passes through. */ -function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc): string { - const key = "iter:budget"; +function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null): string { + const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = "%iter.budget"; + name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); @@ -351,13 +859,52 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc): string { }, loc, }; + // The source closes first; an error from its return() is dropped, so + // the RangeError is what propagates. + const closeSource: IrStmt[] = + srcT === null + ? [] + : [ + { + kind: "tryCatch", + tryBody: [ + { + kind: "exprStmt", + expr: { + kind: "genResume", + mode: "close", + gen: varRef("src.0", srcT, loc), + arg: null, + type: VOID, + loc, + }, + loc, + }, + ], + catchBody: [], + catchLocalId: "exc.0", + finallyBody: null, + loc, + }, + ]; lowerer.liftedFns.push({ name, - params: [{ localId: "v.0", name: "v", type: F64 }], + params: [ + { localId: "v.0", name: "v", type: F64 }, + ...(srcT === null ? [] : [{ localId: "src.0", name: "src", type: srcT }]), + ], returnType: F64, - locals: [{ id: "v.0", name: "v", type: F64, mutable: true }], + locals: [ + { id: "v.0", name: "v", type: F64, mutable: true }, + ...(srcT === null + ? [] + : [ + { id: "src.0", name: "src", type: srcT, mutable: false }, + { id: "exc.0", name: "exc", type: CAUGHT, mutable: false }, + ]), + ], body: [ - { kind: "if", cond: bad, then: [throwStmt], else_: null, loc }, + { kind: "if", cond: bad, then: [...closeSource, throwStmt], else_: null, loc }, { kind: "return", value: t(), loc }, ], loc, @@ -365,97 +912,30 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc): string { return name; } -/** The fused chain body. One loop over the LIVE source (length re-read - * per pass), a `done` flag every loop condition carries (take closing - * the pipeline, short-circuit terminals), stage code nested inside in - * source order, flatMap as an inner loop over its returned array. - * Pre-loop, a zero take budget marks done immediately — Node pulls - * nothing at all through a take(0). */ -function buildIterChainFn( +/** A hidden local holding `gen`, and its declaration. */ +function bindSource( lowerer: Lowerer, - name: string, - itemsT: IrType & { kind: "array" }, - srcProj: "values" | "entries", - stages: { kind: string; fn?: IrExpr & { type: IrType & { kind: "func" } } }[], - terminal: string, - termArgTs: IrType[], - resultT: IrType, + gen: IrExpr & { type: GenType }, loc: SrcLoc, -): IrFunction { - const locals: IrLocal[] = [{ id: "items.0", name: "items", type: itemsT, mutable: true }]; - const params: IrParam[] = [{ localId: "items.0", name: "items", type: itemsT }]; - let n = 0; - const addLocal = (base: string, type: IrType, mutable: boolean): string => { - const id = `${base}.${n++}`; - locals.push({ id, name: base, type, mutable }); - return id; +): { ref: IrExpr & { type: GenType }; decl: IrStmt } { + const local = lowerer.declareHiddenLocal("%helperSource", gen.type); + return { + ref: varRef(local.id, gen.type, loc) as IrExpr & { type: GenType }, + decl: { kind: "varDecl", localId: local.id, init: gen, loc }, }; - const addParam = (base: string, type: IrType): string => { - const id = `${base}.${n++}`; - locals.push({ id, name: base, type, mutable: true }); - params.push({ localId: id, name: base, type }); - return id; - }; - const doneId = addLocal("done", BOOL, true); - const done = (): IrExpr => varRef(doneId, BOOL, loc); - const setDone: IrStmt = { kind: "assign", localId: doneId, value: boolLit(true, loc), loc }; - // Parameters in call order: per-stage budget/callback, then terminal's. - interface StageSlot { - kind: string; - paramId: string; - fnT?: (IrType & { kind: "func" }) | undefined; - cntId?: string | undefined; - elem: IrType; - } - // The entries() seed builds each pass's [index, element] pair — the - // same interned tuple the caller computed its stage types against. - const seedT: IrType = - srcProj === "entries" - ? { - kind: "record", - shapeId: lowerer.shapes.intern( - [ - { name: "0", type: F64 }, - { name: "1", type: itemsT.elem }, - ], - true, - ), - } - : itemsT.elem; - let elem: IrType = seedT; - const slots: StageSlot[] = []; - for (const s of stages) { - if (s.kind === "take" || s.kind === "drop") { - const paramId = addParam("n", F64); - const cntId = addLocal(s.kind === "take" ? "taken" : "dropped", F64, true); - slots.push({ kind: s.kind, paramId, cntId, elem }); - continue; - } - const fnT = s.fn!.type; - const paramId = addParam("f", fnT); - const cntId = fnT.params.length === 2 ? addLocal("c", F64, true) : undefined; - const out = - s.kind === "map" - ? fnT.ret - : s.kind === "flatMap" - ? (fnT.ret as IrType & { kind: "array" }).elem - : elem; - slots.push({ kind: s.kind, paramId, fnT, cntId, elem: out }); - elem = out; - } - const termIds = termArgTs.map((t, i) => addParam(i === 0 ? "tf" : "init", t)); - const termFnT = - termArgTs[0]?.kind === "func" ? (termArgTs[0] as IrType & { kind: "func" }) : null; - const termCntId = - (terminal === "reduce" && termFnT !== null && termFnT.params.length === 3) || - (terminal !== "reduce" && - terminal !== "toArray" && - termFnT !== null && - termFnT.params.length === 2) || - terminal === "reduce" - ? addLocal("k", F64, true) - : undefined; - const bump = (id: string): IrStmt => ({ +} + +/** Where chain code allocates its locals: a lifted helper function's own + * frame, or hidden locals of the function being lowered (a fused for-of). */ +interface ChainFrame { + local(base: string, type: IrType, mutable: boolean): string; + /** `id = id + 1` over a number local. */ + bump(id: string): IrStmt; +} + +/** `id = id + 1` over a number local. */ +function bumpLocal(id: string, loc: SrcLoc): IrStmt { + return { kind: "assign", localId: id, value: { @@ -467,18 +947,633 @@ function buildIterChainFn( loc, }, loc, - }); - const callWith = ( - fnT: IrType & { kind: "func" }, - fnId: string, - value: IrExpr, - cntId: string | undefined, - ): IrExpr => { - const args: IrExpr[] = []; - if (fnT.params.length >= 1) args.push(value); - if (cntId !== undefined && fnT.params.length >= 2) args.push(varRef(cntId, F64, loc)); - return { kind: "callValue", callee: varRef(fnId, fnT, loc), args, type: fnT.ret, loc }; }; +} + +/** Hidden locals of the function being lowered. */ +class HostFrame implements ChainFrame { + constructor( + private readonly lowerer: Lowerer, + private readonly loc: SrcLoc, + ) {} + local(base: string, type: IrType, mutable: boolean): string { + const local = this.lowerer.declareHiddenLocal(`%${base}`, type); + local.mutable = mutable; + return local.id; + } + bump(id: string): IrStmt { + return bumpLocal(id, this.loc); + } +} + +/** The locals and parameters of a lifted helper function under + * construction. */ +class LiftedFrame implements ChainFrame { + readonly locals: IrLocal[] = []; + readonly params: IrParam[] = []; + private n = 0; + constructor(private readonly loc: SrcLoc) {} + local(base: string, type: IrType, mutable: boolean): string { + const id = `${base}.${this.n++}`; + this.locals.push({ id, name: base, type, mutable }); + return id; + } + param(base: string, type: IrType): string { + const id = `${base}.${this.n++}`; + this.locals.push({ id, name: base, type, mutable: true }); + this.params.push({ localId: id, name: base, type }); + return id; + } + bump(id: string): IrStmt { + return bumpLocal(id, this.loc); + } +} + +/** Statements that drive the synchronous generator in local `genId` the + * way for-of does. Each pass runs `pre` (which may `break`), steps the + * generator, binds the yielded value and runs `body` over it (`continue` + * moves to the next pass). Leaving the loop while the generator is + * unfinished closes it: a `break` in `pre` or `body`, a throw from the + * body, or a `return()` at a yield inside it. Exhaustion and a throwing + * step leave it unclosed, as in Node. The generator counts as unfinished + * before the first step, so a `pre` that breaks at once (take(0)) closes + * a source it never pulled. `genT` must be stepDrivable. */ +function driveGenerator( + lowerer: Lowerer, + frame: ChainFrame, + genId: string, + genT: GenType, + elemT: IrType, + pre: IrStmt[], + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, + labels?: string[], +): IrStmt[] { + const g = (): IrExpr & { type: GenType } => + varRef(genId, genT, loc) as IrExpr & { type: GenType }; + const openId = frame.local("open", BOOL, true); + const setOpen = (value: boolean): IrStmt => ({ + kind: "assign", + localId: openId, + value: boolLit(value, loc), + loc, + }); + const vId = frame.local("gv", genT.yieldT, false); + // A checked-dynamic yield converts once into the element type. + const converted = typeEquals(elemT, genT.yieldT) + ? null + : lowerer.coerceToExpected(varRef(vId, genT.yieldT, loc), elemT); + if (converted !== null && !typeEquals(converted.type, elemT)) + throw new InternalCompilerError("lowerer bug: helper source element has no conversion"); + const eId = converted === null ? vId : frame.local("ev", elemT, false); + return [ + { kind: "varDecl", localId: openId, init: boolLit(true, loc), loc }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "while", + cond: boolLit(true, loc), + body: [ + ...pre, + setOpen(false), + { + kind: "if", + cond: genStep(g(), loc), + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setOpen(true), + { kind: "varDecl", localId: vId, init: genValue(g(), loc), loc }, + ...(converted === null + ? [] + : [{ kind: "varDecl" as const, localId: eId, init: converted, loc }]), + ...body(varRef(eId, elemT, loc)), + ], + ...(labels && { labels }), + loc, + }, + ], + catchBody: null, + catchLocalId: null, + // After a throw, an error from closing the source is dropped. + dropFinallyErrors: true, + finallyBody: [ + { + kind: "if", + cond: varRef(openId, BOOL, loc), + then: [ + { + kind: "exprStmt", + expr: { kind: "genResume", mode: "close", gen: g(), arg: null, type: VOID, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + }, + ]; +} + +/** `for (j = 0; j < arr.length && !stop; j++) body(arr[j])` over a live + * array local (the array iterator's per-step length read). */ +function walkArray( + frame: ChainFrame, + arrId: string, + arrT: IrType & { kind: "array" }, + stop: IrExpr | null, + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, +): IrStmt { + const jId = frame.local("j", F64, true); + const wId = frame.local("w", arrT.elem, false); + const inBounds: IrExpr = { + kind: "bin", + op: "<", + left: varRef(jId, F64, loc), + right: { + kind: "arrIntrinsic", + method: "length", + receiver: varRef(arrId, arrT, loc), + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }; + return { + kind: "for", + init: { kind: "varDecl", localId: jId, init: numLit(0, loc), loc }, + cond: + stop === null + ? inBounds + : { + kind: "logical", + op: "&&", + left: inBounds, + right: { kind: "unary", op: "!", operand: stop, type: BOOL, loc }, + type: BOOL, + loc, + }, + update: frame.bump(jId), + body: [ + { + kind: "varDecl", + localId: wId, + init: { + kind: "arrayGet", + arr: varRef(arrId, arrT, loc), + index: varRef(jId, F64, loc), + type: arrT.elem, + loc, + }, + loc, + }, + ...body(varRef(wId, arrT.elem, loc)), + ], + loc, + }; +} + +/** The live walk over the Map or Set in local `collId` that for-of does: + * iterEnter, then each live index from iterStart below the current + * iterCount until `stop`, then iterExit on every exit (a throw exits + * before rethrowing). The body sees the projected element. */ +function walkCollection( + lowerer: Lowerer, + frame: ChainFrame, + collId: string, + source: ChainSource & { kind: "collection" }, + stop: IrExpr, + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, + labels?: string[], +): IrStmt[] { + const t = source.type; + const iId = frame.local("i", F64, true); + const iter = (method: string, args: IrExpr[], type: IrType): IrExpr => + t.kind === "map" + ? { + kind: "mapIntrinsic", + method: method as IrMapIntrinsicMethod, + receiver: varRef(collId, t, loc), + args, + type, + loc, + } + : { + kind: "setIntrinsic", + method: method as IrSetIntrinsicMethod, + receiver: varRef(collId, t, loc), + args, + type, + loc, + }; + const keyT = t.kind === "map" ? t.key : t.elem; + const keyRead = (): IrExpr => iter("iterKey", [varRef(iId, F64, loc)], keyT); + const valRead = (): IrExpr => + t.kind === "map" ? iter("iterValue", [varRef(iId, F64, loc)], t.value) : keyRead(); + const seedT = collectionSeed(lowerer, source); + const seed: IrExpr = + source.proj === "entries" + ? { + kind: "recordLit", + fields: [ + { name: "0", value: keyRead() }, + { name: "1", value: valRead() }, + ], + type: seedT, + loc, + } + : source.proj === "values" && t.kind === "map" + ? valRead() + : keyRead(); + const vId = frame.local("v", seedT, false); + const exit = (): IrStmt => ({ kind: "exprStmt", expr: iter("iterExit", [], VOID), loc }); + const caughtId = frame.local("exc", CAUGHT, false); + const loop: IrStmt = { + kind: "for", + init: { kind: "varDecl", localId: iId, init: iter("iterStart", [], F64), loc }, + cond: { + kind: "logical", + op: "&&", + left: { + kind: "bin", + op: "<", + left: varRef(iId, F64, loc), + right: iter("iterCount", [], F64), + type: BOOL, + loc, + }, + right: { kind: "unary", op: "!", operand: stop, type: BOOL, loc }, + type: BOOL, + loc, + }, + update: frame.bump(iId), + body: [ + { + kind: "if", + cond: iter("iterLive", [varRef(iId, F64, loc)], BOOL), + then: [ + { kind: "varDecl", localId: vId, init: seed, loc }, + ...body(varRef(vId, seedT, loc)), + ], + else_: null, + loc, + }, + ], + ...(labels && { labels }), + loc, + }; + return [ + { kind: "exprStmt", expr: iter("iterEnter", [], VOID), loc }, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: [exit(), { kind: "rethrow", localId: caughtId, loc }], + catchLocalId: caughtId, + finallyBody: null, + loc, + }, + exit(), + ]; +} + +/** A flatMap stage's inner walk over the callback's result in local + * `innerId`: an array by index, or a generator driven (and closed on + * early exit) like the source. `stop` ends the walk early. */ +function walkInner( + lowerer: Lowerer, + frame: ChainFrame, + innerId: string, + innerT: IrType, + stop: IrExpr | null, + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, +): IrStmt[] { + if (innerT.kind === "array") return [walkArray(frame, innerId, innerT, stop, body, loc)]; + if (innerT.kind !== "generator") + throw new InternalCompilerError("lowerer bug: flatMap over a non-iterable result"); + const pre: IrStmt[] = + stop === null + ? [] + : [{ kind: "if", cond: stop, then: [{ kind: "break", loc }], else_: null, loc }]; + return driveGenerator(lowerer, frame, innerId, innerT, innerT.yieldT, pre, body, loc); +} + +/** A stage inside fused chain code: the local holding its budget or + * callback, and its counter when the callback reads one (take/drop count + * in theirs). */ +interface StageSlot { + kind: string; + argId: string; + fnT?: (IrType & { kind: "func" }) | undefined; + cntId?: string | undefined; +} + +/** A stage callback call with (value) or (value, counter), as declared. */ +function callStage( + fnT: IrType & { kind: "func" }, + fnId: string, + value: IrExpr, + cntId: string | undefined, + loc: SrcLoc, +): IrExpr { + const args: IrExpr[] = []; + if (fnT.params.length >= 1) args.push(value); + if (cntId !== undefined && fnT.params.length >= 2) args.push(varRef(cntId, F64, loc)); + return { kind: "callValue", callee: varRef(fnId, fnT, loc), args, type: fnT.ret, loc }; +} + +/** The slot for `stage` whose budget or callback is in local `argId`. */ +function stageSlot(frame: ChainFrame, stage: StageIr, argId: string): StageSlot { + if ("budget" in stage) { + return { + kind: stage.kind, + argId, + cntId: frame.local(stage.kind === "take" ? "taken" : "dropped", F64, true), + }; + } + const fnT = stage.fn.type; + return { + kind: stage.kind, + argId, + fnT, + cntId: fnT.params.length === 2 ? frame.local("c", F64, true) : undefined, + }; +} + +/** The declarations fused chain code starts with: the `done` flag — + * already true when a take budget is zero, so nothing pulls — and each + * stage counter at zero. */ +function stagePrologue(slots: StageSlot[], doneId: string, loc: SrcLoc): IrStmt[] { + let doneInit: IrExpr = boolLit(false, loc); + for (const s of slots) { + if (s.kind !== "take") continue; + const isZero: IrExpr = { + kind: "bin", + op: "===", + left: varRef(s.argId, F64, loc), + right: numLit(0, loc), + type: BOOL, + loc, + }; + doneInit = + doneInit.kind === "boolLit" + ? isZero + : { kind: "logical", op: "||", left: doneInit, right: isZero, type: BOOL, loc }; + } + const out: IrStmt[] = [{ kind: "varDecl", localId: doneId, init: doneInit, loc }]; + for (const s of slots) { + if (s.cntId !== undefined) + out.push({ kind: "varDecl", localId: s.cntId, init: numLit(0, loc), loc }); + } + return out; +} + +/** The per-element code of a chain: each stage nested in source order + * around `last` (the terminal's statements, or a for-of body), folded + * from the end outward. filter and drop skip with `continue`; take spends + * its budget before delivery and sets `done`, so the nth element flows on + * and nothing pulls after it (every pass tests done); flatMap walks its + * callback's array or generator in an inner loop that also tests done. */ +function foldStages( + lowerer: Lowerer, + frame: ChainFrame, + slots: StageSlot[], + last: (value: IrExpr) => IrStmt[], + doneId: string, + loc: SrcLoc, +): (value: IrExpr) => IrStmt[] { + const done = (): IrExpr => varRef(doneId, BOOL, loc); + const setDone: IrStmt = { kind: "assign", localId: doneId, value: boolLit(true, loc), loc }; + let emit = last; + for (let si = slots.length - 1; si >= 0; si--) { + const s = slots[si]!; + const inner = emit; + if (s.kind === "map") { + emit = (value) => { + const vId = frame.local("m", s.fnT!.ret, false); + return [ + { + kind: "varDecl", + localId: vId, + init: callStage(s.fnT!, s.argId, value, s.cntId, loc), + loc, + }, + ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), + ...inner(varRef(vId, s.fnT!.ret, loc)), + ]; + }; + } else if (s.kind === "filter") { + emit = (value) => { + const okId = frame.local("ok", BOOL, false); + return [ + { + kind: "varDecl", + localId: okId, + init: callStage(s.fnT!, s.argId, value, s.cntId, loc), + loc, + }, + ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), + { + kind: "if", + cond: { kind: "unary", op: "!", operand: varRef(okId, BOOL, loc), type: BOOL, loc }, + then: [{ kind: "continue", loc }], + else_: null, + loc, + }, + ...inner(value), + ]; + }; + } else if (s.kind === "drop") { + emit = (value) => [ + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: varRef(s.cntId!, F64, loc), + right: varRef(s.argId, F64, loc), + type: BOOL, + loc, + }, + then: [frame.bump(s.cntId!), { kind: "continue", loc }], + else_: null, + loc, + }, + ...inner(value), + ]; + } else if (s.kind === "take") { + emit = (value) => [ + frame.bump(s.cntId!), + { + kind: "if", + cond: { + kind: "bin", + op: ">=", + left: varRef(s.cntId!, F64, loc), + right: varRef(s.argId, F64, loc), + type: BOOL, + loc, + }, + then: [setDone], + else_: null, + loc, + }, + ...inner(value), + ]; + } else { + emit = (value) => { + const innerT = s.fnT!.ret; + const innerId = frame.local("fm", innerT, false); + return [ + { + kind: "varDecl", + localId: innerId, + init: callStage(s.fnT!, s.argId, value, s.cntId, loc), + loc, + }, + ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), + ...walkInner(lowerer, frame, innerId, innerT, done(), inner, loc), + ]; + }; + } + } + return emit; +} + +/** The loop over a chain's source in local `itemsId`, running `emit` per + * element until `done`: a live array (length re-read per pass), a Map or + * Set walked live, or a generator driven like for-of (closed when the + * loop leaves it unfinished). `labels` name the loop for a fused for-of + * body's labeled jumps. */ +function sourceLoop( + lowerer: Lowerer, + frame: ChainFrame, + itemsId: string, + source: ChainSource, + doneId: string, + emit: (value: IrExpr) => IrStmt[], + loc: SrcLoc, + labels?: string[], +): IrStmt[] { + const done = (): IrExpr => varRef(doneId, BOOL, loc); + if (source.kind === "collection") + return walkCollection(lowerer, frame, itemsId, source, done(), emit, loc, labels); + if (source.kind === "generator") { + return driveGenerator( + lowerer, + frame, + itemsId, + source.type, + source.elem, + [{ kind: "if", cond: done(), then: [{ kind: "break", loc }], else_: null, loc }], + emit, + loc, + labels, + ); + } + const itemsT = source.type; + const seedT = source.proj === "entries" ? entryPairType(lowerer, itemsT.elem) : itemsT.elem; + const iId = frame.local("i", F64, true); + const vId = frame.local("v", seedT, false); + const elemRead = (): IrExpr => ({ + kind: "arrayGet", + arr: varRef(itemsId, itemsT, loc), + index: varRef(iId, F64, loc), + type: itemsT.elem, + loc, + }); + const seedInit: IrExpr = + source.proj === "entries" + ? { + kind: "recordLit", + fields: [ + { name: "0", value: varRef(iId, F64, loc) }, + { name: "1", value: elemRead() }, + ], + type: seedT, + loc, + } + : elemRead(); + return [ + { + kind: "for", + init: { kind: "varDecl", localId: iId, init: numLit(0, loc), loc }, + cond: { + kind: "logical", + op: "&&", + left: { + kind: "bin", + op: "<", + left: varRef(iId, F64, loc), + right: { + kind: "arrIntrinsic", + method: "length", + receiver: varRef(itemsId, itemsT, loc), + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + right: { kind: "unary", op: "!", operand: done(), type: BOOL, loc }, + type: BOOL, + loc, + }, + update: frame.bump(iId), + body: [ + { kind: "varDecl", localId: vId, init: seedInit, loc }, + ...emit(varRef(vId, seedT, loc)), + ], + ...(labels && { labels }), + loc, + }, + ]; +} + +/** The fused chain function: the source, each stage's budget or callback + * and the terminal's arguments as parameters; one loop over the source + * with the stages nested in it and the terminal's statements innermost. + * A zero take budget marks done before the first pass: Node pulls nothing + * through a take(0), and closes a generator source. */ +function buildIterChainFn( + lowerer: Lowerer, + name: string, + source: ChainSource, + stages: StageIr[], + terminal: string, + termArgTs: IrType[], + resultT: IrType, + loc: SrcLoc, +): IrFunction { + const frame = new LiftedFrame(loc); + const itemsId = frame.param("items", source.type); + const doneId = frame.local("done", BOOL, true); + const setDone: IrStmt = { kind: "assign", localId: doneId, value: boolLit(true, loc), loc }; + // Parameters in call order: per-stage budget/callback, then terminal's. + const slots = stages.map((s) => + stageSlot(frame, s, frame.param("budget" in s ? "n" : "f", "budget" in s ? F64 : s.fn.type)), + ); + const termIds = termArgTs.map((t, i) => frame.param(i === 0 ? "tf" : "init", t)); + const termFnT = + termArgTs[0]?.kind === "func" ? (termArgTs[0] as IrType & { kind: "func" }) : null; + const termCntId = + (terminal === "reduce" && termFnT !== null && termFnT.params.length === 3) || + (terminal !== "reduce" && + terminal !== "toArray" && + termFnT !== null && + termFnT.params.length === 2) || + terminal === "reduce" + ? frame.local("k", F64, true) + : undefined; // Terminal state. const preLoop: IrStmt[] = []; const postLoop: IrStmt[] = []; @@ -487,7 +1582,7 @@ function buildIterChainFn( let hasAccId = ""; let resId = ""; if (terminal === "toArray") { - outId = addLocal("out", resultT, false); + outId = frame.local("out", resultT, false); preLoop.push({ kind: "varDecl", localId: outId, @@ -496,7 +1591,7 @@ function buildIterChainFn( }); postLoop.push({ kind: "return", value: varRef(outId, resultT, loc), loc }); } else if (terminal === "some" || terminal === "every") { - resId = addLocal("res", BOOL, true); + resId = frame.local("res", BOOL, true); preLoop.push({ kind: "varDecl", localId: resId, @@ -505,14 +1600,14 @@ function buildIterChainFn( }); postLoop.push({ kind: "return", value: varRef(resId, BOOL, loc), loc }); } else if (terminal === "find") { - resId = addLocal("res", resultT, true); + resId = frame.local("res", resultT, true); const undef = lowerer.wrappedUndefined(resultT, loc); if (undef === null) throw new InternalCompilerError("lowerer bug: find result union lacks undefined"); preLoop.push({ kind: "varDecl", localId: resId, init: undef, loc }); postLoop.push({ kind: "return", value: varRef(resId, resultT, loc), loc }); } else if (terminal === "reduce") { - accId = addLocal("acc", resultT, true); + accId = frame.local("acc", resultT, true); if (termIds.length === 2) { preLoop.push({ kind: "varDecl", @@ -521,7 +1616,7 @@ function buildIterChainFn( loc, }); } else { - hasAccId = addLocal("hasAcc", BOOL, true); + hasAccId = frame.local("hasAcc", BOOL, true); preLoop.push({ kind: "varDecl", localId: hasAccId, init: boolLit(false, loc), loc }); postLoop.push({ kind: "if", @@ -547,28 +1642,7 @@ function buildIterChainFn( } if (termCntId !== undefined) preLoop.push({ kind: "varDecl", localId: termCntId, init: numLit(0, loc), loc }); - // done starts true when any take budget is zero (nothing pulls). - let doneInit: IrExpr = boolLit(false, loc); - for (const s of slots) { - if (s.kind !== "take") continue; - const isZero: IrExpr = { - kind: "bin", - op: "===", - left: varRef(s.paramId, F64, loc), - right: numLit(0, loc), - type: BOOL, - loc, - }; - doneInit = - doneInit.kind === "boolLit" - ? isZero - : { kind: "logical", op: "||", left: doneInit, right: isZero, type: BOOL, loc }; - } - preLoop.unshift({ kind: "varDecl", localId: doneId, init: doneInit, loc }); - for (const s of slots) { - if (s.cntId !== undefined) - preLoop.push({ kind: "varDecl", localId: s.cntId, init: numLit(0, loc), loc }); - } + preLoop.unshift(...stagePrologue(slots, doneId, loc)); // The terminal's per-element statements over `value`. const terminalBody = (value: IrExpr): IrStmt[] => { const out: IrStmt[] = []; @@ -586,9 +1660,13 @@ function buildIterChainFn( loc, }); } else if (terminal === "forEach") { - out.push({ kind: "exprStmt", expr: callWith(termFnT!, termIds[0]!, value, termCntId), loc }); + out.push({ + kind: "exprStmt", + expr: callStage(termFnT!, termIds[0]!, value, termCntId, loc), + loc, + }); } else if (terminal === "some" || terminal === "every") { - const hit = callWith(termFnT!, termIds[0]!, value, termCntId); + const hit = callStage(termFnT!, termIds[0]!, value, termCntId, loc); const cond: IrExpr = terminal === "some" ? hit : { kind: "unary", op: "!", operand: hit, type: BOOL, loc }; out.push({ @@ -610,7 +1688,7 @@ function buildIterChainFn( ); out.push({ kind: "if", - cond: callWith(termFnT!, termIds[0]!, value, termCntId), + cond: callStage(termFnT!, termIds[0]!, value, termCntId, loc), then: [ { kind: "assign", @@ -658,230 +1736,203 @@ function buildIterChainFn( out.push({ kind: "assign", localId: accId, value: reducer(), loc }); } } - if (termCntId !== undefined) out.push(bump(termCntId)); + if (termCntId !== undefined) out.push(frame.bump(termCntId)); return out; }; - // Fold stages from the terminal outward. - let emit: (value: IrExpr) => IrStmt[] = terminalBody; - for (let si = slots.length - 1; si >= 0; si--) { - const s = slots[si]!; - const inner = emit; - if (s.kind === "map") { - emit = (value) => { - const vId = addLocal("m", s.fnT!.ret, false); - return [ - { kind: "varDecl", localId: vId, init: callWith(s.fnT!, s.paramId, value, s.cntId), loc }, - ...(s.cntId !== undefined ? [bump(s.cntId)] : []), - ...inner(varRef(vId, s.fnT!.ret, loc)), - ]; - }; - } else if (s.kind === "filter") { - emit = (value) => { - const okId = addLocal("ok", BOOL, false); - return [ - { - kind: "varDecl", - localId: okId, - init: callWith(s.fnT!, s.paramId, value, s.cntId), - loc, - }, - ...(s.cntId !== undefined ? [bump(s.cntId)] : []), - { - kind: "if", - cond: { kind: "unary", op: "!", operand: varRef(okId, BOOL, loc), type: BOOL, loc }, - then: [{ kind: "continue", loc }], - else_: null, - loc, - }, - ...inner(value), - ]; - }; - } else if (s.kind === "drop") { - emit = (value) => [ - { - kind: "if", - cond: { - kind: "bin", - op: "<", - left: varRef(s.cntId!, F64, loc), - right: varRef(s.paramId, F64, loc), - type: BOOL, - loc, - }, - then: [bump(s.cntId!), { kind: "continue", loc }], - else_: null, - loc, - }, - ...inner(value), - ]; - } else if (s.kind === "take") { - // Budget consumed BEFORE delivery: the nth element flows on with - // done already set, so no later element pulls upstream — and a - // downstream `continue` can't skip the close (the loop conditions - // carry !done). - emit = (value) => [ - bump(s.cntId!), - { - kind: "if", - cond: { - kind: "bin", - op: ">=", - left: varRef(s.cntId!, F64, loc), - right: varRef(s.paramId, F64, loc), - type: BOOL, - loc, - }, - then: [setDone], - else_: null, - loc, - }, - ...inner(value), - ]; - } else { - // flatMap: an inner loop over the returned array, `!done` carried. - emit = (value) => { - const innerT = s.fnT!.ret as IrType & { kind: "array" }; - const arrId = addLocal("fm", innerT, false); - const jId = addLocal("j", F64, true); - const wId = addLocal("w", innerT.elem, false); - return [ - { - kind: "varDecl", - localId: arrId, - init: callWith(s.fnT!, s.paramId, value, s.cntId), - loc, - }, - ...(s.cntId !== undefined ? [bump(s.cntId)] : []), - { - kind: "for", - init: { kind: "varDecl", localId: jId, init: numLit(0, loc), loc }, - cond: { - kind: "logical", - op: "&&", - left: { - kind: "bin", - op: "<", - left: varRef(jId, F64, loc), - right: { - kind: "arrIntrinsic", - method: "length", - receiver: varRef(arrId, innerT, loc), - args: [], - type: F64, - loc, - }, - type: BOOL, - loc, - }, - right: { kind: "unary", op: "!", operand: done(), type: BOOL, loc }, - type: BOOL, - loc, - }, - update: { - kind: "assign", - localId: jId, - value: { - kind: "bin", - op: "+", - left: varRef(jId, F64, loc), - right: numLit(1, loc), - type: F64, - loc, - }, - loc, - }, - body: [ - { - kind: "varDecl", - localId: wId, - init: { - kind: "arrayGet", - arr: varRef(arrId, innerT, loc), - index: varRef(jId, F64, loc), - type: innerT.elem, - loc, - }, - loc, - }, - ...inner(varRef(wId, innerT.elem, loc)), - ], - loc, - }, - ]; - }; - } - } - const iId = addLocal("i", F64, true); - const vId = addLocal("v", seedT, false); - const elemRead = (): IrExpr => ({ - kind: "arrayGet", - arr: varRef("items.0", itemsT, loc), - index: varRef(iId, F64, loc), - type: itemsT.elem, - loc, - }); - const seedInit: IrExpr = - srcProj === "entries" - ? { - kind: "recordLit", - fields: [ - { name: "0", value: varRef(iId, F64, loc) }, - { name: "1", value: elemRead() }, - ], - type: seedT, - loc, - } - : elemRead(); - const loop: IrStmt = { - kind: "for", - init: { kind: "varDecl", localId: iId, init: numLit(0, loc), loc }, - cond: { - kind: "logical", - op: "&&", - left: { - kind: "bin", - op: "<", - left: varRef(iId, F64, loc), - right: { - kind: "arrIntrinsic", - method: "length", - receiver: varRef("items.0", itemsT, loc), - args: [], - type: F64, - loc, - }, - type: BOOL, - loc, - }, - right: { kind: "unary", op: "!", operand: done(), type: BOOL, loc }, - type: BOOL, - loc, - }, - update: { - kind: "assign", - localId: iId, - value: { - kind: "bin", - op: "+", - left: varRef(iId, F64, loc), - right: numLit(1, loc), - type: F64, - loc, - }, - loc, - }, + const emit = foldStages(lowerer, frame, slots, terminalBody, doneId, loc); + return { + name, + params: frame.params, + returnType: resultT, + locals: frame.locals, body: [ - { kind: "varDecl", localId: vId, init: seedInit, loc }, - ...emit(varRef(vId, seedT, loc)), + ...preLoop, + ...sourceLoop(lowerer, frame, itemsId, source, doneId, emit, loc), + ...postLoop, ], loc, }; +} + +/** An iterator helper object's generator for one stage over `src`: + * + * map: for (v of src) yield f(v, c++) + * filter: for (v of src) if (f(v, c++)) yield v + * take: for (;;) { if (rem === 0) { close src; return } rem--; pull; yield v } + * drop: for (v of src) { if (dropped < n) { dropped++; continue } yield v } + * flatMap: for (v of src) for (w of f(v, c++)) yield w + * + * The source drive closes `src` on every early exit, including a + * `return()` at one of the yields; `closesSource` closes it on a + * `return()` before the first `next()`. */ +function buildHelperGenFn( + lowerer: Lowerer, + name: string, + srcT: GenType, + srcElem: IrType, + stage: StageIr, + argT: IrType, + outT: GenType, + resultType: IrType & { kind: "record" }, + loc: SrcLoc, +): IrFunction { + const frame = new LiftedFrame(loc); + const srcId = frame.param("src", srcT); + const argId = frame.param(stage.kind === "take" || stage.kind === "drop" ? "n" : "f", argT); + const yieldOf = (value: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "yieldExpr", + value, + type: outT.nextT.kind === "undefinedT" ? VOID : outT.nextT, + loc, + }, + loc, + }); + const fnT = argT.kind === "func" ? argT : null; + const counted = fnT !== null && fnT.params.length === 2; + const cntId = counted ? frame.local("c", F64, true) : null; + const callF = (value: IrExpr): IrExpr => { + const args: IrExpr[] = []; + if (fnT!.params.length >= 1) args.push(value); + if (cntId !== null) args.push(varRef(cntId, F64, loc)); + return { kind: "callValue", callee: varRef(argId, fnT!, loc), args, type: fnT!.ret, loc }; + }; + const body: IrStmt[] = []; + if (cntId !== null) body.push({ kind: "varDecl", localId: cntId, init: numLit(0, loc), loc }); + const bumpCount = (): IrStmt[] => (cntId !== null ? [frame.bump(cntId)] : []); + const n = (): IrExpr => varRef(argId, F64, loc); + if (stage.kind === "map") { + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => { + const mId = frame.local("m", fnT!.ret, false); + return [ + { kind: "varDecl", localId: mId, init: callF(value), loc }, + ...bumpCount(), + yieldOf(varRef(mId, fnT!.ret, loc)), + ]; + }, + loc, + ), + ); + } else if (stage.kind === "filter") { + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => { + const okId = frame.local("ok", BOOL, false); + return [ + { kind: "varDecl", localId: okId, init: callF(value), loc }, + ...bumpCount(), + { kind: "if", cond: varRef(okId, BOOL, loc), then: [yieldOf(value)], else_: null, loc }, + ]; + }, + loc, + ), + ); + } else if (stage.kind === "take") { + // The budget is spent before each pull; a spent budget closes the + // source on the next pass instead of pulling. + const pre: IrStmt[] = [ + { + kind: "if", + cond: { kind: "bin", op: "===", left: n(), right: numLit(0, loc), type: BOOL, loc }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + { + kind: "assign", + localId: argId, + value: { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc }, + loc, + }, + ]; + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + pre, + (value) => [yieldOf(value)], + loc, + ), + ); + } else if (stage.kind === "drop") { + const droppedId = frame.local("dropped", F64, true); + body.push({ kind: "varDecl", localId: droppedId, init: numLit(0, loc), loc }); + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => [ + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: varRef(droppedId, F64, loc), + right: n(), + type: BOOL, + loc, + }, + then: [frame.bump(droppedId), { kind: "continue", loc }], + else_: null, + loc, + }, + yieldOf(value), + ], + loc, + ), + ); + } else { + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => { + const innerT = fnT!.ret; + const innerId = frame.local("fm", innerT, false); + return [ + { kind: "varDecl", localId: innerId, init: callF(value), loc }, + ...bumpCount(), + ...walkInner(lowerer, frame, innerId, innerT, null, (w) => [yieldOf(w)], loc), + ]; + }, + loc, + ), + ); + } + body.push({ kind: "return", value: null, loc }); return { name, - params, - returnType: resultT, - locals, - body: [...preLoop, loop, ...postLoop], + params: frame.params, + returnType: VOID, + locals: frame.locals, + body, + generator: { yieldT: outT.yieldT, nextT: outT.nextT, resultType, closesSource: true }, loc, }; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 9ae66f859..a936b9c78 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -83,7 +83,10 @@ import { import { lowerArrayFromAsyncCall } from "./containers/array-from-async.js"; import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/array-callbacks.js"; import { lowerGroupByStaticCall } from "./containers/collection-grouping.js"; -import { lowerIteratorHelperCall } from "./containers/iterator-helpers.js"; +import { + lowerGeneratorHelperCall, + lowerIteratorHelperCall, +} from "./containers/iterator-helpers.js"; import { lowerObjectAssignIndexShape, lowerObjectConstructor, @@ -1425,6 +1428,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // array method paths (the terminal names collide with array // methods, but only iterator-typed receivers reach this). lowerIteratorHelperCall(lowerer, expr, expr.expression) ?? + // Helper calls on generators and other iterator objects. + lowerGeneratorHelperCall(lowerer, expr, expr.expression) ?? lowerIteratorStaticFence(lowerer, expr, expr.expression) ?? lowerObjectStaticCall(lowerer, expr, expr.expression) ?? lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ?? @@ -3078,8 +3083,9 @@ function lowerOptionalStringNumber( return { kind: "call", callee: helper, args: [arg], type: F64, loc }; } -/** Iterator types whose helper calls have no lowering on any path (the - * array iterator's chains lower in lowerIteratorHelperCall). */ +/** Iterator types with checked runtime receivers: Map/Set iterators' + * helper calls lower in lowerGeneratorHelperCall; the string iterator's + * have no lowering. */ const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]); const ITERATOR_HELPER_METHODS = new Set([ "map", @@ -3146,6 +3152,8 @@ function lowerDynReceiverMethodCall( lowerer.isStdlibMember(access) ) { if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") { + const helper = lowerGeneratorHelperCall(lowerer, call, access); + if (helper) return helper; lowerer.noLowering( `collection iterator .${access.name.text}()`, call, @@ -3156,7 +3164,7 @@ function lowerDynReceiverMethodCall( lowerer.noLowering( `${recvSymbol.name}.${access.name.text}`, call, - "iterator helpers lower on chains from arr.values() or arr.entries() that end in a terminal (toArray, forEach, reduce, some, every, find) — spread the string first: [...s].values()", + "iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()", ); } const arrayReceiver = lowerer.checker.isArrayType(recvTs); @@ -6546,9 +6554,9 @@ function recordAssignHelper( } /** The `Iterator` global's statics (ES2025 — Iterator.from, and the - * abstract constructor as a value): no first-class iterator objects - * exist here, so every member fences with the working spelling named - * instead of the generic-method fence's monomorphization wording. */ + * abstract constructor as a value) have no lowering: every member fences + * with the working spelling named instead of the generic-method fence's + * monomorphization wording. */ function lowerIteratorStaticFence( lowerer: Lowerer, call: ts.CallExpression, @@ -6559,8 +6567,8 @@ function lowerIteratorStaticFence( lowerer.noLowering( `Iterator.${access.name.text}`, call, - "first-class iterator objects have no lowering — iterator helpers compile as one chain on an " + - "array iterator, consumed in place: arr.values().map(f).take(n).toArray()", + "iterator helpers apply directly to generators and to array, Map and Set iterators: " + + "arr.values().map(f).take(n).toArray()", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 2eda4c4ce..a9b467a6a 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -115,6 +115,16 @@ export function lowerGenMethodCall( if (call.arguments.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments"); } + // Iterator helpers and the built-in iterators have no throw method; an + // iterator-typed value may hold one, so only a generator's own member + // lowers. + if (name === "throw" && !isGeneratorMember(lowerer, access)) { + lowerer.noLowering( + `${lowerer.checker.typeToString(lowerer.typeOf(access.expression))}.throw`, + call, + "throw() lowers on values typed as generators (iterator helpers and built-in iterators have no throw method)", + ); + } const loc = locOf(call); const gen = lowerer.lowerExpr(access.expression); if (gen.type.kind !== "generator") return null; @@ -187,6 +197,23 @@ export function lowerGenMethodCall( return { kind: "genResume", mode: name, gen, arg, type: resultT, loc }; } +/** Whether `access` names a member the lib declares on Generator or + * AsyncGenerator itself. */ +function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + const member = lowerer.checker.getSymbolAtLocation(access.name); + return ( + member !== undefined && + lowerer.checker + .declarationsOf(member) + .some( + (d) => + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && + lowerer.isStdlibFile(d.getSourceFile()), + ) + ); +} + /** Extraction of a suspended resume's yield value out of the result * record's V slot, typed back to the yield channel: V IS the channel for * dyn (and for a union channel already equal to V); a non-union channel @@ -319,7 +346,7 @@ export function genValue(gen: IrExpr & { type: GenType }, loc: SrcLoc): IrExpr { return { kind: "genResume", mode: "value", gen, arg: null, type: gen.type.yieldT, loc }; } -function generatorLoopBinding( +export function generatorLoopBinding( lowerer: Lowerer, stmt: ts.ForOfStatement, value: IrExpr, @@ -572,7 +599,9 @@ export function lowerForOfGenerator( catchBody: null, catchLocalId: null, // IteratorClose: every exit that leaves the generator unfinished - // closes it — finallys run; the .return() result is dropped. + // closes it — finallys run; the .return() result is dropped, and + // after a throw from the body an error from the close is too. + dropFinallyErrors: true, finallyBody: [ { kind: "if", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 7f3be4d44..b8ccd958d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -97,6 +97,7 @@ import { lowerForOfSearchParams, lowerForOfSet, } from "./containers/for-of.js"; +import { lowerForOfHelperChain } from "./containers/iterator-helpers.js"; import { objectIterOverIndexShape } from "./containers/indexed-objects.js"; import { strCharsCall } from "./containers/array-construction.js"; import { @@ -8303,6 +8304,12 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { } } } + // A head that is a chain of iterator-helper stages runs as one loop + // over the chain's source. + { + const fused = lowerForOfHelperChain(lowerer, stmt, labels); + if (fused) return fused; + } // `for (const k of m.keys())` / `.values()` / `.entries()`: the // iterator methods consumed DIRECTLY by a for-of head ride the // container's live walk — exactly `for..of m` with the projection diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index c6e70577c..417fb5b52 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3224,7 +3224,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { isStdlibInterface("IterableIterator") || // An Iterator annotation holds a single-pass iterator object: a // generator, or a native iterator adapted into one. - isStdlibInterface("Iterator") + isStdlibInterface("Iterator") || + // IteratorObject is what the iterator helpers return: a helper + // generator over its source (containers/iterator-helpers.ts). + isStdlibInterface("IteratorObject") ) { const args = checker.getTypeArguments(widened as ts.TypeReference); const channels = genChannels(args[0], args[1], args[2], ctx); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..a77ea2e4b 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1651,7 +1651,15 @@ export interface IrFunction { * out, `nextT` what `.next(v)` sends in (the yield expression's result * type). `async` alongside this field marks an async generator: its * resume methods queue requests and return promises. */ - generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } }; + generator?: { + yieldT: IrType; + nextT: IrType; + resultType: IrType & { kind: "record" }; + /** The first parameter is a generator this one reads from (an + * iterator helper's source): `.return()` before the first `.next()` + * closes it. */ + closesSource?: true; + }; /** Source function name for native error stack frames. */ sourceName?: string; /** Ordinary authored function declarations/expressions own a prototype. */ @@ -1924,6 +1932,11 @@ export type IrStmt = /** Resource-management cleanup: if the guarded body and cleanup both * throw, raise a SuppressedError instead of replacing the first error. */ suppressFinallyErrors?: true; + /** Iterator closing: if the guarded body threw and the cleanup throws + * too, the cleanup's error is dropped and the first one propagates. + * After a generator .return() the cleanup's error replaces it, and on + * normal exits it propagates. */ + dropFinallyErrors?: true; loc: SrcLoc; }; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 6dc871976..aad2e4861 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4464,7 +4464,7 @@ "kind": "stdlib", "name": "Map.prototype.entries", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.map.forEach", @@ -4489,7 +4489,7 @@ "kind": "stdlib", "name": "Map.prototype.keys", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.map.set", @@ -4502,7 +4502,7 @@ "kind": "stdlib", "name": "Map.prototype.values", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.math.E", @@ -5371,7 +5371,7 @@ "kind": "stdlib", "name": "Set.prototype.entries", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.set.forEach", @@ -5418,7 +5418,7 @@ "kind": "stdlib", "name": "Set.prototype.keys", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.set.symmetricDifference", @@ -5439,7 +5439,7 @@ "kind": "stdlib", "name": "Set.prototype.values", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.sharedArrayBuffer", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json new file mode 100644 index 000000000..5aa23f5d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5908-iterator-helper-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json new file mode 100644 index 000000000..fd3454670 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5909-iterator-helper-objects-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 48f3a5329..f630ee5f9 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4890,6 +4890,8 @@ struct ScrGen { /* The never-started teardown: drops the packed (already-retained) * arguments the spawn wrapper built. Emitted per generator function. */ void (*drop_args)(void *); + /* scr_gen_set_unstarted_return's hook; NULL for most generators. */ + void (*unstarted_return)(void *); bool is_async; void (*settle_async)(ScrGen *, ScrPromise *); ScrAsyncGenRequest *requests_head; @@ -5449,6 +5451,8 @@ void scr_gen_resume(ScrGen *g) { } } +void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)) { g->unstarted_return = hook; } + void scr_gen_close(ScrGen *g) { scr_gen_slot_reset(&g->ret); scr_gen_resume_return(g); @@ -5483,13 +5487,16 @@ void scr_gen_resume_return(ScrGen *g) { return; case SCR_GEN_UNSTARTED: /* The body never runs: tear the fiber down cleanly (drop the packed - * arguments) and complete with the parked value. */ + * arguments) and complete with the parked value. The generator is + * done before the hook runs, so a resume from inside it answers done; + * an exception the hook raises stays pending for the caller. */ + g->state = SCR_GEN_DONE; + if (g->unstarted_return != NULL) g->unstarted_return(g->fiber->argpack); if (g->drop_args != NULL) g->drop_args(g->fiber->argpack); else free(g->fiber->argpack); scr_fiber_destroy(g->fiber); g->fiber = NULL; scr_fibers_live--; - g->state = SCR_GEN_DONE; scr_gen_ret_to_out(g); return; default: diff --git a/packages/runtime/src/scr_exception.c b/packages/runtime/src/scr_exception.c index fb152e078..3a79cf85b 100644 --- a/packages/runtime/src/scr_exception.c +++ b/packages/runtime/src/scr_exception.c @@ -389,6 +389,17 @@ void scr_exc_suppressed_forget(const void *error_object) { } } +void scr_exc_keep_first(ScrCaught *first) { + if (first->kind == SCR_EXC_GENRET) { + scr_caught_release(first); + return; + } + ScrCaught *dropped = scr_exc_take(); + if (dropped) scr_caught_release(dropped); + scr_rethrow(first); + scr_caught_release(first); +} + void scr_exc_suppress(ScrCaught *suppressed) { if (suppressed->kind == SCR_EXC_GENRET) { /* A generator .return() is a return completion, not a thrown error. diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index 80fb98957..29460a1b9 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -172,6 +172,10 @@ void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx); * when scr_json.c builds its dyn view, and scr_exc_suppressed_forget * releases them. */ void scr_exc_suppress(ScrCaught *suppressed); +/* An iterator close that threw while `first` was propagating: the close's + * error is dropped and `first` is raised again (consumed). A generator + * .return() in flight is replaced by the close's error instead. */ +void scr_exc_keep_first(ScrCaught *first); bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error, const ScrCaught **suppressed); void scr_exc_suppressed_forget(const void *error_object); diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index 2c2c0e953..f0ecb9d85 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -60,6 +60,9 @@ void scr_gen_coro_finish(ScrFiber *self); ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, void (*drop_args)(void *), void (*settle)(ScrGen *, ScrPromise *)); +/* Runs when return() reaches the generator before its first resume, with + * the packed arguments still alive: an iterator helper closes its source. */ +void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ void *scr_gen_retain_v(void *g); diff --git a/tests/corpus/5908-iterator-helper-objects.ts b/tests/corpus/5908-iterator-helper-objects.ts new file mode 100644 index 000000000..5e57b9ddf --- /dev/null +++ b/tests/corpus/5908-iterator-helper-objects.ts @@ -0,0 +1,214 @@ +// Iterator helpers over generators, stored array iterators and Map/Set +// iterators: chains that end in a consuming method, helper objects driven +// by next(), for...of, spreads, Array.from and yield*, and the points at +// which a helper closes its source (runs its return()). +const log: string[] = []; +function* count(label: string, n: number): Generator { + try { + for (let i = 1; i <= n; i++) { + log.push(`${label}${i}`); + yield i; + } + } finally { + log.push(`${label}:closed`); + } +} +function flush(what: unknown): void { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +// Consuming methods over generator chains. +flush(count("a", 5).map((x) => x * 10).filter((x) => x !== 20).toArray()); +flush(count("b", 4).reduce((acc, x) => acc + x)); +flush(count("c", 4).reduce((acc, x, i) => acc + x * i, 100)); +flush(count("d", 5).some((x) => x === 2)); +flush(count("e", 5).every((x) => x < 3)); +flush(count("f", 5).find((x) => x > 3) ?? -1); +flush(count("g", 2).find((x) => x > 3) ?? -1); +let each = ""; +count("h", 3).forEach((x, i) => { + each += `${x}@${i} `; +}); +flush(each.trim()); + +// take, drop and their budgets. +flush(count("i", 9).drop(2).take(3).toArray()); +flush(count("j", 3).take(Infinity).toArray()); +flush([count("k", 4).take(2.9).toArray(), count("l", 4).drop(1.5).toArray()]); +for (const budget of [-1, NaN, -0.5]) { + try { + console.log(count("m", 1).take(budget).toArray()); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } +} +try { + count("n", 0).reduce((a, b) => a + b); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +flush("budgets"); + +// An invalid budget closes a started source before the RangeError. +const invalid = count("bad", 3); +invalid.next(); +try { + invalid.take(-1); +} catch (e) { + log.push((e as Error).message); +} +flush(invalid.next().done); + +// A started source closes through take(0) without another pull. +const started = count("o", 3); +started.next(); +flush([started.take(0).toArray(), started.next().done]); + +// Helper objects step lazily, one source value per delivered value. +const h = count("p", 4) + .map((x) => x * 2) + .filter((x) => x !== 4); +log.push("made"); +console.log(h.next(), h.next()); +console.log(h.next(), h.next()); +flush("next"); +const words = count("q", 3).map((x, i) => "w".repeat(x) + i); +flush([...words]); +flush(Array.from(count("r", 3).drop(1))); +function* wrap(): Generator { + yield 0; + yield* count("s", 2).map((x) => -x); + yield 9; +} +flush([...wrap()]); +function sum(it: Iterator): number { + let total = 0; + for (let r = it.next(); !r.done; r = it.next()) total += r.value; + return total; +} +flush(sum(count("t", 4).filter((x) => x % 2 === 0))); + +// next() from inside the helper's own callback. +const self: IteratorObject = count("re", 2).map((x) => { + if (x === 2) { + try { + self.next(); + } catch (e) { + log.push(`${(e as Error).name}: ${(e as Error).message}`); + } + } + return x; +}); +flush(self.toArray()); + +// Leaving a for...of over a helper closes the helper and its source. +for (const x of count("u", 9).map((v) => v * v)) { + if (x > 4) break; + log.push(`got${x}`); +} +flush("break"); +try { + count("v", 3).forEach((x) => { + if (x === 2) throw new Error("callback threw"); + }); +} catch (e) { + log.push((e as Error).message); +} +flush("throw"); +function* failing(): Generator { + try { + yield 1; + throw new Error("source threw"); + } finally { + log.push("failing:finally"); + } +} +try { + failing().map((x) => x).toArray(); +} catch (e) { + log.push((e as Error).message); +} +flush("source"); +// When closing the source throws while another error propagates, the +// first error is the one that reaches the caller. +function* closeThrows(): Generator { + try { + yield 1; + } finally { + log.push("closeThrows:finally"); + throw new Error("close threw"); + } +} +try { + closeThrows() + .map((x): number => { + if (x > 0) throw new Error("callback threw"); + return x; + }) + .toArray(); +} catch (e) { + log.push((e as Error).message); +} +try { + for (const x of closeThrows().filter(() => true)) throw new Error(`body threw at ${x}`); +} catch (e) { + log.push((e as Error).message); +} +try { + for (const x of closeThrows()) throw new Error(`plain loop threw at ${x}`); +} catch (e) { + log.push((e as Error).message); +} +flush("close errors"); + +// flatMap over arrays and generators; take stops mid-way through an inner +// generator and closes it before the outer source. +flush(count("w", 2).flatMap((x) => [x, -x]).toArray()); +flush(count("x", 3).flatMap((x) => count(`y${x}-`, 2)).take(3).toArray()); + +// Map and Set iterators, observed live. +const set = new Set([1, 2, 3]); +console.log( + set + .values() + .map((x) => { + if (x === 1) set.add(4); + if (x === 2) set.delete(3); + return x * 100; + }) + .toArray(), +); +const map = new Map([ + ["a", 1], + ["b", 2], + ["c", 3], +]); +const pairs = map.entries().filter(([, v]) => v !== 2); +map.set("d", 4); +console.log([...pairs].map(([k, v]) => k + v).join(",")); +console.log(map.keys().map((k) => k.toUpperCase()).toArray(), map.values().reduce((a, b) => a + b, 0)); +console.log(set.keys().some((x) => x > 3), set.entries().find(([k]) => k === 2)?.[1]); + +// Array iterators as helper sources. +const nums = [5, 6, 7]; +const doubled = nums.values().map((x) => x * 2); +nums.push(8); +console.log([...doubled], nums.keys().filter((i) => i > 0).toArray(), [...nums.entries().drop(2)]); + +// Records and strings flow through the stages by reference. +interface Item { + name: string; + qty: number; +} +const items: Item[] = [ + { name: "pen", qty: 3 }, + { name: "ink", qty: 0 }, + { name: "pad", qty: 7 }, +]; +function* stock(): Generator { + yield* items; +} +const inStock = stock().filter((it) => it.qty > 0); +console.log(inStock.next().value === items[0], stock().find((it) => it.name === "pad")?.qty); +console.log(stock().map((it) => `${it.name}x${it.qty}`).toArray().join(" ")); diff --git a/tests/corpus/5909-iterator-helper-objects-js.js b/tests/corpus/5909-iterator-helper-objects-js.js new file mode 100644 index 000000000..759cc2321 --- /dev/null +++ b/tests/corpus/5909-iterator-helper-objects-js.js @@ -0,0 +1,58 @@ +// Iterator helper objects in JavaScript: inferred callback parameters, +// return() on a helper (before its first next(), mid-way and after +// completion), and chains over Map/Set iterators. +const log = []; +function* count(label, n) { + try { + for (let i = 1; i <= n; i++) { + log.push(label + i); + yield i; + } + } finally { + log.push(label + ":closed"); + } +} +function flush(what) { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +flush(count("a", 4).map((x) => x + 0.5).toArray()); +flush(count("b", 6).filter((x) => x % 2 === 1).map((x, i) => x * 10 + i).take(2).toArray()); +flush(count("c", 3).reduce((acc, x) => acc + x, "")); + +// return() before the first next() closes a source that has started. +const started = count("d", 3); +started.next(); +const mapped = started.map((x) => x * 2); +flush([mapped.return(), started.next()]); +// ... and leaves an unstarted source without running its body. +const fresh = count("e", 3); +const filtered = fresh.filter(() => true); +flush([filtered.return(), fresh.next(), filtered.next()]); +// return() at a yield closes the source; later calls report done. +const stepping = count("f", 5).take(4); +flush([stepping.next(), stepping.return(), stepping.next(), stepping.return()]); +// A chain of helpers closes through to the source. +const chained = count("g", 5); +chained.next(); +const chain = chained.map((x) => x).filter(() => true).take(3); +flush([chain.return(), chained.next()]); + +// Map and Set iterators. +const scores = new Map([ + ["ann", 3], + ["bob", 9], + ["cy", 5], +]); +console.log(scores.entries().filter(([, v]) => v > 4).map(([k]) => k).toArray()); +console.log(scores.values().every((v) => v > 2), scores.keys().find((k) => k.length === 2)); +const tags = new Set(["x", "y", "z"]); +const upper = tags.values().map((t) => t.toUpperCase()); +tags.add("w"); +console.log([...upper], tags.keys().drop(3).toArray()); +for (const t of tags.values().flatMap((t) => [t, t + t])) { + if (t === "yy") break; + log.push(t); +} +flush("flat"); diff --git a/tests/diagnostics/collection-iterator-helpers.ts b/tests/diagnostics/collection-iterator-helpers.ts index 9d264ec51..127b5c64b 100644 --- a/tests/diagnostics/collection-iterator-helpers.ts +++ b/tests/diagnostics/collection-iterator-helpers.ts @@ -1,10 +1,9 @@ -// Stored collection cursors support next/iteration, but helper methods are -// refused explicitly instead of dispatching to array helpers at runtime. -const map = new Map(); -const values = map.values(); -values.map(value => value + 1); -map.keys().toArray(); -const set = new Set(); -set.values().filter(value => value > 0); -set.entries().take(1); +// Iterator helpers lower over Map/Set iterators, generators and array +// iterators. These helper forms are refused explicitly. +const set = new Set([1, 2]); +// flatMap callbacks return arrays or generators. +set.values().flatMap((value) => new Set([value, value + 1])); +// return() on a helper object. +const mapped = set.values().map((value) => value * 2); +mapped.return!(); // Each helper call must produce a diagnostic. diff --git a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt index b6996b3c1..87470b621 100644 --- a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt +++ b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt @@ -1,35 +1,17 @@ -collection-iterator-helpers.ts:5:1 - error SC2020: 'collection iterator .map()' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:5:22 - error SC2020: '.flatMap over a callback returning 'Set'' is part of the standard library types but has no scriptc lowering yet - 4 | const values = map.values(); - 5 | values.map(value => value + 1); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | map.keys().toArray(); + 4 | // flatMap callbacks return arrays or generators. + 5 | set.values().flatMap((value) => new Set([value, value + 1])); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 6 | // return() on a helper object. - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering + hint: callbacks returning an array or a generator are the supported forms -collection-iterator-helpers.ts:6:1 - error SC2020: 'collection iterator .toArray()' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:8:1 - error SC2020: 'IteratorObject.return' is part of the standard library types but has no scriptc lowering yet - 5 | values.map(value => value + 1); - 6 | map.keys().toArray(); - | ^~~~~~~~~~~~~~~~~~~~ - 7 | const set = new Set(); + 7 | const mapped = set.values().map((value) => value * 2); + 8 | mapped.return!(); + | ^~~~~~~~~~~~~ + 9 | // Each helper call must produce a diagnostic. - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering - -collection-iterator-helpers.ts:8:1 - error SC2020: 'collection iterator .filter()' is part of the standard library types but has no scriptc lowering yet - - 7 | const set = new Set(); - 8 | set.values().filter(value => value > 0); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 | set.entries().take(1); - - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering - -collection-iterator-helpers.ts:9:1 - error SC2020: 'collection iterator .take()' is part of the standard library types but has no scriptc lowering yet - - 8 | set.values().filter(value => value > 0); - 9 | set.entries().take(1); - | ^~~~~~~~~~~~~~~~~~~~~ - 10 | // Each helper call must produce a diagnostic. - - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering \ No newline at end of file + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt index e2dc5f40f..d7eda27b6 100644 --- a/tests/harness/__snapshots__/es2025-fences.ts.txt +++ b/tests/harness/__snapshots__/es2025-fences.ts.txt @@ -43,15 +43,6 @@ es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standa hint: use integer load, store, exchange, compareExchange, add, sub, and, or, xor, wait or notify -es2025-fences.ts:25:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet - - 24 | // First-class iterator objects. - 25 | const it = Iterator.from([1, 2, 3].values()); - | ^~ - 26 | - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet 24 | // First-class iterator objects. @@ -59,7 +50,7 @@ es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard l | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | - hint: first-class iterator objects have no lowering — iterator helpers compile as one chain on an array iterator, consumed in place: arr.values().map(f).take(n).toArray() + hint: iterator helpers apply directly to generators and to array, Map and Set iterators: arr.values().map(f).take(n).toArray() es2025-fences.ts:28:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt b/tests/harness/__snapshots__/string-iterator-helpers.ts.txt index 4d9d1ad8d..ef768934e 100644 --- a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt +++ b/tests/harness/__snapshots__/string-iterator-helpers.ts.txt @@ -1,12 +1,3 @@ -string-iterator-helpers.ts:3:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet - - 2 | // refused instead of failing at run time. - 3 | const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase()); - | ^~~~~ - 4 | - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - string-iterator-helpers.ts:3:15 - error SC2020: 'StringIterator.map' is part of the standard library types but has no scriptc lowering yet 2 | // refused instead of failing at run time. @@ -14,4 +5,4 @@ string-iterator-helpers.ts:3:15 - error SC2020: 'StringIterator.map' is part of | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | - hint: iterator helpers lower on chains from arr.values() or arr.entries() that end in a terminal (toArray, forEach, reduce, some, every, find) — spread the string first: [...s].values() \ No newline at end of file + hint: iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values() \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index b34eeecb0..852efc369 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2132,6 +2132,8 @@ "5905-array-from-async-generator.ts": 2750, "5906-array-from-async-generator-js.js": 2950, "5907-regex-groups-match-rows.ts": 3150, + "5908-iterator-helper-objects.ts": 6350, + "5909-iterator-helper-objects-js.js": 4850, "5910-set-prototype-of-primitive-object-kinds.ts": 2719, "5911-dyn-class-to-primitive.ts": 2850, "5912-dyn-class-conversion-methods.ts": 3251, @@ -5173,6 +5175,8 @@ "5905-array-from-async-generator.ts": 3500, "5906-array-from-async-generator-js.js": 3750, "5907-regex-groups-match-rows.ts": 3750, + "5908-iterator-helper-objects.ts": 5950, + "5909-iterator-helper-objects-js.js": 3750, "5910-set-prototype-of-primitive-object-kinds.ts": 5033, "5911-dyn-class-to-primitive.ts": 4732, "5912-dyn-class-conversion-methods.ts": 4424, From f4a8511dca71301bca190291caa0c742046401b1 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 15:58:57 -0700 Subject: [PATCH 072/120] Compile maybe-promise unions, unknown[] rest parameters, and function-valued Maps and Sets A union may now hold one promise beside data members (`T | Promise`, `Promise | R | void`). The union tag is the `instanceof Promise` test, and every operation keeps Node's microtask timing: `await` awaits the promise arm or takes the one non-thenable turn, an async `return` (block bodies, concise arrows, then handlers) adopts the promise arm after the function's own finally blocks, `Promise.resolve` returns the promise arm as is, and `typeof (u as any).then` reads the tag. Data arms that declare `then`, two promise arms, and function arms beside a promise stay refused with diagnostics. Inferred JavaScript literals that mix promises and values keep the checked-dynamic array. Rest parameters typed `unknown[]` or `any[]` in TypeScript use the checked-dynamic variadic ABI that JavaScript rest parameters already use, including spread forwarding (`(...args) => handler(...args)`). Functions are now Map values, Map keys and Set elements, compared by closure identity. Two pre-existing bugs in the same area are fixed: an async lambda in a `Promise | void` slot got a `Promise` ABI and threw when called through the slot, and `return cb()` of such a callback in an async function dropped the promise instead of adopting it. The retired-engine 2251 and 2790 diagnostics fixtures (tagged templates with `any[]` rest tags) now compile and match Node, so they move to the corpus as 6248 and 6249. Co-Authored-By: Claude Opus 5.5 --- .../limitations/builtin-apis/map-and-set.mdx | 2 +- .../null-and-optional-values.mdx | 2 +- .../types-and-class-definitions/unions.mdx | 2 +- .../frontend/lowering/builtins/promises.ts | 29 +- .../src/frontend/lowering/call-arguments.ts | 8 +- .../src/frontend/lowering/call-signatures.ts | 42 ++- .../lowering/coercions/function-adapters.ts | 6 + .../src/frontend/lowering/lower-calls.ts | 31 +- .../src/frontend/lowering/lower-classes.ts | 11 +- .../src/frontend/lowering/lower-exprs.ts | 99 +++++- .../src/frontend/lowering/lower-functions.ts | 18 +- .../compiler/src/frontend/lowering/lowerer.ts | 92 +++++- .../src/frontend/lowering/maybe-promise.ts | 294 ++++++++++++++++++ packages/compiler/src/frontend/type-mapper.ts | 79 ++++- packages/compiler/src/ir/ir.ts | 34 +- packages/compiler/src/ir/validate.test.ts | 30 +- packages/compiler/src/ir/validate.ts | 8 +- .../6240-maybe-promise-await-order.ts.json | 6 + .../6241-maybe-promise-handlers.ts.json | 6 + .../6242-maybe-promise-rejections.ts.json | 6 + .../6243-maybe-promise-narrowing.ts.json | 6 + .../6244-maybe-promise-async-return.ts.json | 6 + .../corpus/6245-rest-unknown-handlers.ts.json | 6 + .../6246-map-set-function-values.ts.json | 6 + .../6247-next-proxy-maybe-promise.ts.json | 6 + .../6248-tagged-template-any-rest.ts.json | 6 + ...ged-template-missing-substitutions.ts.json | 6 + .../diagnostics/maybe-promise-fences.ts.json | 6 + ...d-engine-2251-tagged-templates-dyn.ts.json | 6 - ...array-missing-tagged-substitutions.ts.json | 6 - .../corpus/6240-maybe-promise-await-order.ts | 60 ++++ tests/corpus/6241-maybe-promise-handlers.ts | 77 +++++ tests/corpus/6242-maybe-promise-rejections.ts | 77 +++++ tests/corpus/6243-maybe-promise-narrowing.ts | 61 ++++ .../corpus/6244-maybe-promise-async-return.ts | 61 ++++ tests/corpus/6245-rest-unknown-handlers.ts | 46 +++ tests/corpus/6246-map-set-function-values.ts | 52 ++++ tests/corpus/6247-next-proxy-maybe-promise.ts | 35 +++ .../6248-tagged-template-any-rest.ts} | 0 ...-tagged-template-missing-substitutions.ts} | 0 tests/diagnostics/component-fences.ts | 6 +- tests/diagnostics/maps.ts | 2 +- tests/diagnostics/maybe-promise-fences.ts | 37 +++ .../__snapshots__/component-fences.ts.txt | 30 +- tests/harness/__snapshots__/maps.ts.txt | 16 - .../__snapshots__/maybe-promise-fences.ts.txt | 65 ++++ ...ed-engine-2251-tagged-templates-dyn.ts.txt | 44 --- ...-array-missing-tagged-substitutions.ts.txt | 17 - 48 files changed, 1388 insertions(+), 163 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/maybe-promise.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6240-maybe-promise-await-order.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6241-maybe-promise-handlers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6242-maybe-promise-rejections.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6243-maybe-promise-narrowing.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6244-maybe-promise-async-return.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6245-rest-unknown-handlers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6246-map-set-function-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6247-next-proxy-maybe-promise.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6248-tagged-template-any-rest.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6249-tagged-template-missing-substitutions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/maybe-promise-fences.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts.json create mode 100644 tests/corpus/6240-maybe-promise-await-order.ts create mode 100644 tests/corpus/6241-maybe-promise-handlers.ts create mode 100644 tests/corpus/6242-maybe-promise-rejections.ts create mode 100644 tests/corpus/6243-maybe-promise-narrowing.ts create mode 100644 tests/corpus/6244-maybe-promise-async-return.ts create mode 100644 tests/corpus/6245-rest-unknown-handlers.ts create mode 100644 tests/corpus/6246-map-set-function-values.ts create mode 100644 tests/corpus/6247-next-proxy-maybe-promise.ts rename tests/{diagnostics/retired-engine-2251-tagged-templates-dyn.ts => corpus/6248-tagged-template-any-rest.ts} (100%) rename tests/{diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts => corpus/6249-tagged-template-missing-substitutions.ts} (100%) create mode 100644 tests/diagnostics/maybe-promise-fences.ts create mode 100644 tests/harness/__snapshots__/maybe-promise-fences.ts.txt delete mode 100644 tests/harness/__snapshots__/retired-engine-2251-tagged-templates-dyn.ts.txt delete mode 100644 tests/harness/__snapshots__/retired-engine-2790-array-missing-tagged-substitutions.ts.txt diff --git a/docs/content/limitations/builtin-apis/map-and-set.mdx b/docs/content/limitations/builtin-apis/map-and-set.mdx index 04ebcbfca..1c9c28c20 100644 --- a/docs/content/limitations/builtin-apis/map-and-set.mdx +++ b/docs/content/limitations/builtin-apis/map-and-set.mdx @@ -1 +1 @@ -- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map` and `Set`, and promises requiring payload adapters, cannot pass into these slots. `Promise` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. +- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, functions, symbols, server handles, and unions of these reference types by identity. Map values may also be functions. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map` and `Set`, and promises requiring payload adapters, cannot pass into these slots. `Promise` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. diff --git a/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx b/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx index f3d4e6acf..afb8ec9cf 100644 --- a/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx +++ b/docs/content/limitations/types-and-class-definitions/null-and-optional-values.mdx @@ -1 +1 @@ -- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile. +- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments. A function value with fixed parameters cannot be stored where such a variadic function type is expected. diff --git a/docs/content/limitations/types-and-class-definitions/unions.mdx b/docs/content/limitations/types-and-class-definitions/unions.mdx index 5d3bae3c3..403c9be2e 100644 --- a/docs/content/limitations/types-and-class-definitions/unions.mdx +++ b/docs/content/limitations/types-and-class-definitions/unions.mdx @@ -1 +1 @@ -- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union. +- **Unions:** Narrow a union before an operation whose members require incompatible native representations, such as reading `u.length` on `string | string[]`. Some union-to-union widening and unions combining function and data members are unsupported. Same-representation methods on class unions, `map` and `forEach` on array unions, and shared or joinable field reads compile. `console.log(u)` supports printing a whole union. A union may hold one promise beside other members, such as `T | Promise` or `Promise | R | void`: `await`, an async function's `return`, `Promise.resolve`, and `instanceof Promise` act on the member it holds, with Node's microtask timing. A promise member beside a member that declares `then`, two promise members in one union, and `console.log` of a union holding a promise are unsupported. diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 2dc212f81..da680a86e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -2,6 +2,7 @@ import { checkedPromiseAll } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; +import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { DYN, @@ -402,6 +403,22 @@ export function lowerPromiseStaticCall( if (v.type.kind !== "promise") lowerer.badType(argNode, argT); return v; } + const maybePromise = argIr ? maybePromiseUnionOf(lowerer, argIr) : null; + if (maybePromise) { + const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + const v = lowerer.lowerExpr(argNode); + const mp = maybePromiseUnionOf(lowerer, v.type); + const resolved = + resultT?.kind === "promise" && mp + ? maybePromiseResolve(lowerer, v, mp, resultT, loc) + : null; + if (resolved) return resolved; + lowerer.noLowering( + `Promise.resolve over '${lowerer.fmt(v.type)}' at this result type`, + argNode, + "the promise arm passes through identically, so it must already be the result's promise type", + ); + } if ( argIr?.kind === "union" && (lowerer.unions.get(argIr.unionId)?.arms ?? []).some((a) => a.kind === "promise") @@ -423,7 +440,17 @@ export function lowerPromiseStaticCall( } } } - const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + let resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + // A maybe-promise context (`return Promise.resolve(v)` from a function + // returning `Promise | R | void`) names its promise arm: the + // fresh promise takes that payload type directly. + if (argNode !== undefined && resultT?.kind === "promise") { + const ctxT = lowerer.checker.getContextualType(call); + const contextual = ctxT?.isUnionType() ? lowerer.mapTypeOf(ctxT) : null; + const mp = contextual ? maybePromiseUnionOf(lowerer, contextual) : null; + if (mp && !typeEquals(mp.promise, resultT) && mp.promise.inner.kind !== "void") + resultT = mp.promise; + } if (resultT?.kind !== "promise") { lowerer.noLowering( "Promise.resolve at this type", diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index d1d695920..3119fa2ab 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -546,7 +546,13 @@ export function lowerSpreadArgsCall( callee: IrExpr, loc: SrcLoc, ): IrExpr | null { - if (!isJsSourceFile(expr.getSourceFile())) return null; + // TypeScript reaches this lane only for checked-dynamic variadic callees + // (`(...args: unknown[]) => R`), whose ABI is the JS one. + if ( + !isJsSourceFile(expr.getSourceFile()) && + !(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined) + ) + return null; if (!expr.arguments.some((a) => ts.isSpreadElement(a))) return null; if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null; const calleeName = diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 8e30e8b82..ae59be3b2 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -13,7 +13,7 @@ import { typeEquals, } from "../../ir/ir.js"; import { isJsSourceFile, isNodeEsmFile } from "../program.js"; -import { genResultRecord, jsOpenObjectType } from "../type-mapper.js"; +import { genResultRecord, isAnyArrayType, jsOpenObjectType } from "../type-mapper.js"; import { dynFallbackType } from "./lowerer.js"; import { builtinModuleFnOf } from "./surfaces.js"; import { mixinFnShapeOf } from "./lower-mixins.js"; @@ -286,10 +286,17 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa return { type: DYN, mode: "dynRest" }; } } + // `...args: unknown[]` / `...args: any[]` in TypeScript: the binding is + // the checked-dynamic array (how `unknown[]` maps everywhere), so the + // ABI is the JS variadic dyn form above — the function type marks it + // with `rest` and no typed slot (the type mapper agrees). + const restTs = lowerer.typeOf(param.name); + if (isAnyArrayType(restTs, lowerer.checker)) return { type: DYN, mode: "dynRest" }; const type = lowerer.runtimeOptionalBindingType(param.name, lowerer.irTypeOf(param.name)); + if (type.kind === "dyn") return { type: DYN, mode: "dynRest" }; // Tuple-typed rest params don't map to an array; generic rest is the // generic path's business. Anything non-array here is unmappable. - if (type.kind !== "array") lowerer.badType(param.name, lowerer.typeOf(param.name)); + if (type.kind !== "array") lowerer.badType(param.name, restTs); return { type, mode: "rest" }; } if (param.initializer) { @@ -843,13 +850,34 @@ export function lambdaSignature( ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile ? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!)) : null; - const ctxRet = isAsyncLike && ctxRetRaw?.kind === "promise" ? ctxRetRaw.inner : ctxRetRaw; + // An async lambda always produces a promise, so a slot returning a + // promise-armed union (`Promise | R | void`, `Promise + // | void`) is met through that promise arm: its payload is the ABI. + const ctxPromiseArm = + isAsyncLike && ctxRetRaw?.kind === "union" + ? lowerer.unions.get(ctxRetRaw.unionId)?.arms.filter((arm) => arm.kind === "promise") + : undefined; + const ctxRet = + isAsyncLike && ctxRetRaw?.kind === "promise" + ? ctxRetRaw.inner + : ctxPromiseArm?.length === 1 && ctxPromiseArm[0]!.kind === "promise" + ? ctxPromiseArm[0]!.inner + : ctxRetRaw; + // A TypeScript variadic lambda (`(...args) => handler(...args)`) in a + // checked-dynamic variadic slot returning `unknown` adopts that result + // too: rest closures have no function-value adapter, so the closure + // must match the slot's ABI exactly. + const dynVariadicSlot = + ctxRet?.kind === "dyn" && + !isJsSourceFile(node.getSourceFile()) && + shapes.some((shape) => shape.mode === "dynRest"); if ( - (ctxRet?.kind === "union" || ctxRet?.kind === "array" || ctxRet?.kind === "record") && - (innerRet.kind !== "void" || - (ctxRet.kind === "union" && lowerer.armTag(ctxRet.unionId, UNDEFINED_T) >= 0)) + dynVariadicSlot || + ((ctxRet?.kind === "union" || ctxRet?.kind === "array" || ctxRet?.kind === "record") && + (innerRet.kind !== "void" || + (ctxRet.kind === "union" && lowerer.armTag(ctxRet.unionId, UNDEFINED_T) >= 0))) ) { - ret = isAsyncLike ? { kind: "promise", inner: ctxRet } : ctxRet; + ret = isAsyncLike ? { kind: "promise", inner: ctxRet! } : ctxRet!; } // A VOID slot discards the callback's result (TS's void-returning // assignability rule; JS ignores the value), so an UNANNOTATED diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index f60fc32b8..b99c24a15 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -13,6 +13,7 @@ import { } from "../../../ir/ir.js"; import { typeKey } from "../../type-mapper.js"; import type { Lowerer } from "../lowerer.js"; +import { maybePromiseUnionOf } from "../maybe-promise.js"; /** Wrap a zero-argument function whose array result needs an element * conversion. Each invocation creates a fresh converted array; the factory @@ -96,6 +97,11 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool if (dst.kind === "union") { if (src.kind === "union") return lowerer.unionRetagMappable(src.unionId, dst.unionId); if (src.kind === "void") return lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0; + if (src.kind === "promise") { + // A maybe-promise union's promise arm accepts payload widening. + const mp = maybePromiseUnionOf(lowerer, dst); + if (mp) return lowerer.promisePayloadWidens(src, mp.promise); + } return !isUnitType(src) && lowerer.armTag(dst.unionId, src) >= 0; } if (src.kind === "union") { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2037ba17c..fc7069b41 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -207,6 +207,7 @@ import { } from "./call-arguments.js"; import { isThisParameter, type ParamShape } from "./call-signatures.js"; import { checkedArmExtraction } from "./coercions/unions.js"; +import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js"; import { requireObjLitGenericReceiver, objLitGenericFnNodeOf, @@ -5217,6 +5218,8 @@ export function lowerPromiseMethodCall( const resultOf = (value: IrExpr, returnsPromise = false): IrStmt[] => { if (value.type.kind === "promise") return [lowerer.adoptingReturn(value, R, (e) => lowerer.coerceInto(call, e, R))]; + const flattened = maybePromiseHandlerResult(lowerer, call, value, R); + if (flattened) return [flattened]; if (returnsPromise) { // A boxed handler's promise result adopts too: the then job's // turn after the call, then the await of the returned value. @@ -5567,7 +5570,10 @@ export function lowerPromiseMethodCall( // The handler's result: promise returns flatten exactly like // `return p` in any async body (the spec's thenable adoption); // everything else coerces into R. - if (handlerCall.type.kind === "promise" && R.kind !== "promise") { + const flattened = maybePromiseHandlerResult(lowerer, call, handlerCall, R); + if (flattened) { + body.push(flattened); + } else if (handlerCall.type.kind === "promise" && R.kind !== "promise") { body.push(lowerer.adoptingReturn(handlerCall, R, (e) => lowerer.coerceInto(call, e, R))); } else if (R.kind === "void") { body.push({ kind: "exprStmt", expr: handlerCall, loc }); @@ -9634,3 +9640,26 @@ export function lowerStaticNumberGlobal( } return null; } + +/** A then handler's promise-armed result (`.then(() => cached ?? load())` + * typed `T | Promise`, or a `Promise | void` callback's result): + * the lifted async body adopts the promise arm and fulfills with the + * others, like `return u` in an async function. Null for other results. */ +function maybePromiseHandlerResult( + lowerer: Lowerer, + call: ts.CallExpression, + value: IrExpr, + R: IrType, +): IrStmt | null { + const mp = promiseArmedUnionOf(lowerer, value.type); + if (!mp || promiseArmedUnionOf(lowerer, R)) return null; + const stmt = maybePromiseReturnStmt(lowerer, value, mp, R, (e) => lowerer.coerceInto(call, e, R)); + if (stmt === null) { + lowerer.unsupported( + "SC1090", + call, + `then handlers returning '${lowerer.fmt(value.type)}' into '${lowerer.fmt(R)}' (an arm does not convert to the result)`, + ); + } + return stmt; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index f3a4830c3..d09f40ed8 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1566,7 +1566,14 @@ function inferredEmptyCollectionFieldType( const mappedArgument = (argument: ts.Expression): IrType | null => { const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(argument)); const mapped = lowerer.mapTypeOf(type); - if (!mapped || mapped.kind === "dyn" || mapped.kind === "void" || isUnitType(mapped)) + // Inferred JS collection fields keep functions in checked slots. + if ( + !mapped || + mapped.kind === "dyn" || + mapped.kind === "void" || + mapped.kind === "func" || + isUnitType(mapped) + ) return null; return mapped; }; @@ -9231,7 +9238,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { "SC1090", expr, `Map values of type '${lowerer.checker.typeToString(targs[1])}' ` + - `(Map values must be number, string, boolean, records, class instances, ` + + `(Map values must be number, string, boolean, records, class instances, functions, ` + `typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`, ); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 149c25248..53cb26093 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -45,6 +45,11 @@ import { constituentTypes } from "../ts7/checker.js"; import { dirname } from "node:path"; import * as posix from "node:path/posix"; import type { Lowerer } from "./lowerer.js"; +import { + lowerMaybePromiseAwait, + maybePromiseInstanceOf, + maybePromiseUnionOf, +} from "./maybe-promise.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js"; import { checkedArmExtraction } from "./coercions/unions.js"; import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js"; @@ -1766,6 +1771,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { } return { kind: "awaitUnionExpr", value, promiseTag, type, loc }; } + const maybePromise = maybePromiseUnionOf(lowerer, value.type); + if (maybePromise) return lowerMaybePromiseAwait(lowerer, expr, value, maybePromise, loc); } if (value.type.kind !== "promise") { // A CHECKED-DYNAMIC operand (`await v` where v rode an untyped @@ -4699,12 +4706,61 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr { ); } +/** `typeof (u as any).then` / `if ((u as Promise).then)` over a + * maybe-promise union `u` (the cast is erasure in JS): a promise arm has + * the prototype's `then` function, and no data arm is thenable (the type + * mapper's rule), so the union tag answers. Null for every other shape. */ +function maybePromiseThenPresence( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + result: IrExpr, +): IrExpr | null { + let receiver: ts.Expression = access.expression; + let cast = false; + for (;;) { + if (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression; + else if (ts.isAsExpression(receiver) || ts.isTypeAssertion(receiver)) { + receiver = receiver.expression; + cast = true; + } else break; + } + if (!cast) return null; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(receiver)); + if (!mapped || !maybePromiseUnionOf(lowerer, mapped)) return null; + const value = lowerer.lowerExpr(receiver); + const mp = maybePromiseUnionOf(lowerer, value.type); + if (!mp) return null; + const loc = locOf(access); + const absent: IrExpr = + result.type.kind === "bool" + ? { kind: "boolLit", value: false, type: BOOL, loc } + : { kind: "strLit", value: "undefined", type: STRING, loc }; + return { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: mp.def.id, + tag: mp.promiseTag, + negated: false, + value, + type: BOOL, + loc, + }, + then: result, + else_: absent, + type: result.type, + loc, + }; +} + function lowerPromiseThenPresence( lowerer: Lowerer, access: ts.PropertyAccessExpression, result: IrExpr, ): IrExpr | null { if (access.name.text !== "then") return null; + const maybePromise = maybePromiseThenPresence(lowerer, access, result); + if (maybePromise !== null) return maybePromise; const mapped = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); const promiseType = mapped?.kind === "promise" @@ -5888,6 +5944,11 @@ export function lowerArrayLiteral( const elem = mapped.elem; const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem]; if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN; + // Inferred JavaScript literals mixing promises and plain values + // (`[Promise.resolve(1), 2]`) keep the checked-dynamic array: their + // consumers (Readable.from, Promise.all over iterators) inspect each + // element at run time. + if (maybePromiseUnionOf(lowerer, elem)) mapped = DYN; } if ( !expected && @@ -12607,6 +12668,38 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc }; } } + // `x instanceof Promise` over a promise-armed union (the maybe-promise + // `T | Promise` dispatch): a union tag test — the data arms are + // never promises (scriptc promises are all native, and no data arm + // can hold one). A promise-typed operand answers true after its + // evaluation; a checked-dynamic one asks the runtime. + if ( + ts.isIdentifier(expr.right) && + lowerer.isStdlibGlobal(expr.right, "Promise") && + !lowerer.caughtLocalOf(expr.left) + ) { + const left = lowerer.lowerExpr(expr.left); + const tagTest = maybePromiseInstanceOf(lowerer, left, loc); + if (tagTest) return tagTest; + if (left.type.kind === "dyn") + return { kind: "dynTest", test: "promise", value: left, type: BOOL, loc }; + if (left.type.kind === "promise") { + const yes: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + if (isSafeToDiscard(left)) return yes; + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: left, loc }], + result: yes, + type: BOOL, + loc, + }; + } + lowerer.unsupported( + "SC1090", + expr, + `'instanceof Promise' on '${lowerer.fmt(left.type)}' values (only promises, promise-armed unions, and checked-dynamic values have a lowering)`, + ); + } // `x instanceof RegExp` over a union with a regex arm (the // skip-utility `string | RegExp` dispatch): a union tag test — the // socket-narrowing shape; the checker types the branches and @@ -15778,7 +15871,11 @@ export function lowerPromiseRejectCall( ); } const ctxT = lowerer.checker.getContextualType(call); - const type = ctxT ? lowerer.mapTypeOf(ctxT) : null; + const contextual = ctxT ? lowerer.mapTypeOf(ctxT) : null; + // A maybe-promise context (`return Promise.reject(e)` from a function + // returning `T | Promise`) names its one promise arm. + const maybePromise = contextual ? maybePromiseUnionOf(lowerer, contextual) : null; + const type = maybePromise ? maybePromise.promise : contextual; const reasonNode = call.arguments[0]!; // A CHECKED-DYNAMIC reason (`Promise.reject(value)` where value rode // an untyped binding — the tracingChannel suite's shape): the checked-dynamic tree diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 2991c0d39..c7e293707 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -345,9 +345,12 @@ export function lowerLambda( } else { const value = lowerer.lowerExpr(bodyExpr); // `async () => p` with a void result still resolves the - // function's promise with p (its settlement, rejections included). - body = - value.kind === "unitLit" + // function's promise with p (its settlement, rejections included); + // a maybe-promise value adopts only its promise arm. + const flattened = isAsync ? lowerer.maybePromiseReturn(bodyExpr, value, VOID) : null; + body = flattened + ? [flattened] + : value.kind === "unitLit" ? [] : isAsync && value.type.kind === "promise" ? [lowerer.adoptingReturn(value, VOID, (e) => e)] @@ -367,8 +370,13 @@ export function lowerLambda( if (isAsync && value.type.kind === "promise" && bodyReturn.kind !== "promise") { value = lowerer.returnedPromiseAwait(value); } - body = - value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!)) + const flattened = + isAsync && !fnCtx.inferReturn + ? lowerer.maybePromiseReturn(bodyExpr, value, bodyReturn) + : null; + body = flattened + ? [flattened] + : value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!)) ? [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }] : [ { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a1ded6aee..f7b5febcc 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -596,6 +596,11 @@ import { lowerRecordFieldCall, lowerObjectMethodCall } from "./lower-calls.js"; import { fenceCrossBlockNsRef, nsPathPrefix } from "./lower-namespaces.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { lowerTernary } from "./lower-exprs.js"; +import { + maybePromiseReturnStmt, + maybePromiseUnionOf, + promiseArmedUnionOf, +} from "./maybe-promise.js"; /** Entry function name. '%' cannot appear in a TS identifier, so a user * function can never collide with it (mangling is injective per prefix). */ @@ -1275,17 +1280,20 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir if (args.length === 2) { const key = lowerer.mapTypeOf(args[0]!); const value = lowerer.mapTypeOf(args[1]!); + // Inferred JS function types keep the checked-dynamic slot (their + // closures carry JS arity, not a typed closure ABI). return { kind: "map", key: key && + key.kind !== "func" && isSupportedMapKey( key, key.kind === "union" ? lowerer.unions.get(key.unionId)?.arms : undefined, ) ? key : DYN, - value: value && isSupportedMapValue(value) ? value : DYN, + value: value && value.kind !== "func" && isSupportedMapValue(value) ? value : DYN, }; } } @@ -8204,6 +8212,18 @@ export class Lowerer { // undefined/null literals store the dyn unit values. Types outside the // dyn's domain (bytes, classes, Maps, ...) fall through to // requireExactShape's SC1101 fence. + // A promise entering a maybe-promise union whose promise arm has a + // wider payload (`Promise` into `Promise | R | void`) + // takes the payload widening below first, then wraps into that arm. + if (expected.kind === "union" && expr.type.kind === "promise") { + const mp = maybePromiseUnionOf(this, expected); + if ( + mp && + !typeEquals(mp.promise, expr.type) && + this.promisePayloadWidens(expr.type, mp.promise) + ) + expr = this.coerceToExpected(expr, mp.promise); + } // Promise payloads may widen at a checked return boundary, including // arrays inferred as tuple results by Promise.all. Keep both payload // ABIs explicit: box settlement, then validate the destination payload. @@ -9402,6 +9422,25 @@ export class Lowerer { return type; } + /** Whether a promise of `from` converts to a promise of `to` through the + * checked payload widening (coerceToExpected's boxed-settlement rule, + * which keeps the promise's identity and settlement turn). */ + promisePayloadWidens( + from: IrType & { kind: "promise" }, + to: IrType & { kind: "promise" }, + ): boolean { + if (typeEquals(from, to)) return true; + if (to.inner.kind === "void") return from.inner.kind !== "void"; + return ( + this.dynConvertible(from) && + canDynCheckTo( + to, + (id) => this.shapes.get(id), + (id) => this.unions.get(id), + ) + ); + } + /** True when a static type converts to a dyn value (the dynFrom * walker's domain): JSON-safe, bytes (Uint8Array/Buffer — the checked-dynamic tree's * bytes kind, payload copied; stdin chunks into unknown-typed helpers), @@ -9441,16 +9480,26 @@ export class Lowerer { // storage directly, just like arguments and variable initializers. // Contextual types (such as Proxy descriptors' symbol-valued fields) // must not force an intermediate record with incompatible field slots. + if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected); + return this.asyncReturnValueOf(node, this.lowerExpr(node), expected); + } + + /** A fresh JS object literal returned into a checked-native (dyn) result, + * which lowers straight into that storage. */ + private returnsLiteralIntoDyn(node: ts.Expression, expected: IrType): boolean { let literal = node; while (ts.isParenthesizedExpression(literal)) literal = literal.expression; - if ( + return ( expected.kind === "dyn" && isJsSourceFile(node.getSourceFile()) && ts.isObjectLiteralExpression(literal) - ) { - return this.lowerExprExpecting(node, expected); - } - const e = this.lowerExpr(node); + ); + } + + /** The value of `return ` (already lowered untyped) in an async or + * void context: a promise flattens through the returned-promise await, + * everything else converts into the declared result. */ + private asyncReturnValueOf(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr | null { if (expected.kind === "void" && e.kind === "unitLit") return null; if (this.ctx.isAsync && e.type.kind === "promise" && expected.kind !== "promise") { const awaited = this.returnedPromiseAwait(e); @@ -9516,10 +9565,32 @@ export class Lowerer { return s; } + /** `return u` of a promise-armed union (maybe-promise, or promise-or- + * unit) in an async function: adopt the promise arm, fulfill with the + * others (maybe-promise.ts). Null when the return is not that shape. */ + maybePromiseReturn(node: ts.Expression, e: IrExpr, expected: IrType): IrStmt | null { + if (!this.adoptsReturnedPromise()) return null; + const mp = promiseArmedUnionOf(this, e.type); + if (!mp || promiseArmedUnionOf(this, expected)) return null; + const stmt = maybePromiseReturnStmt(this, e, mp, expected, (x) => + this.coerceInto(node, x, expected), + ); + if (stmt === null) { + this.unsupported( + "SC1090", + node, + `returning '${this.fmt(e.type)}' from an async function whose result is '${this.fmt(expected)}' (an arm does not convert to the result)`, + ); + } + return stmt; + } + private lowerReturnOf(node: ts.Expression, loc: SrcLoc): IrStmt { const expected = this.ctx.returnType; if (expected.kind === "void") { let e = this.lowerExpr(node); + const flattened = this.maybePromiseReturn(node, e, expected); + if (flattened) return flattened; if (this.ctx.isAsync && e.type.kind === "promise") { if (this.adoptsReturnedPromise()) return this.adoptingReturn(e, VOID, (x) => x); e = this.returnedPromiseAwait(e); @@ -9535,6 +9606,15 @@ export class Lowerer { loc, }; } + if (this.adoptsReturnedPromise() && !this.returnsLiteralIntoDyn(node, expected)) { + // The async return lowers its operand untyped first (a promise + // adopts before its payload converts); a maybe-promise union needs + // the statement form, which branches between adoption and fulfillment. + const e = this.lowerExpr(node); + const flattened = this.maybePromiseReturn(node, e, expected); + if (flattened) return flattened; + return { kind: "return", value: this.asyncReturnValueOf(node, e, expected), loc }; + } return { kind: "return", value: this.lowerReturnValue(node), loc }; } diff --git a/packages/compiler/src/frontend/lowering/maybe-promise.ts b/packages/compiler/src/frontend/lowering/maybe-promise.ts new file mode 100644 index 000000000..8323c2e91 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/maybe-promise.ts @@ -0,0 +1,294 @@ +import { varRef } from "../../ir/build.js"; +import { BOOL, isUnitType, typeEquals, VOID } from "../../ir/ir.js"; +import type { IrExpr, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js"; +import type * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** A MAYBE-PROMISE union (`T | Promise`, `Promise | R | + * void`): one promise arm beside at least one data arm. The type mapper + * mints these only when no data arm is thenable, so the union tag decides + * exactly what JS's await decides: the promise arm settles, every other + * arm is a non-thenable that answers itself. Promise-or-unit unions keep + * their own lowerings (awaitUnionExpr). */ +export interface MaybePromiseUnion { + def: IrUnionDef; + promiseTag: number; + promise: IrType & { kind: "promise" }; +} + +export function maybePromiseUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null { + const armed = promiseArmedUnionOf(lowerer, t); + if (!armed || armed.def.arms.every((a, i) => i === armed.promiseTag || isUnitType(a))) + return null; + return armed; +} + +/** Any union with exactly one promise arm: a maybe-promise union or a + * promise-or-unit union (`Promise | void`). An async `return` + * resolves both the same way: adopt the promise arm, fulfill otherwise. */ +export function promiseArmedUnionOf(lowerer: Lowerer, t: IrType): MaybePromiseUnion | null { + if (t.kind !== "union") return null; + const def = lowerer.unions.get(t.unionId); + if (!def) return null; + const tags = def.arms.flatMap((a, i) => (a.kind === "promise" ? [i] : [])); + if (tags.length !== 1) return null; + const promiseTag = tags[0]!; + const promise = def.arms[promiseTag]!; + if (promise.kind !== "promise") return null; + return { def, promiseTag, promise }; +} + +function unionTagTest(mp: MaybePromiseUnion, value: IrExpr, tag: number, loc: SrcLoc): IrExpr { + return { + kind: "unionIsTag", + unionId: mp.def.id, + tag, + negated: false, + value, + type: BOOL, + loc, + }; +} + +/** The value of `ref` (a repeatable read of the maybe-promise union) as + * `target`, given that it does not hold the promise arm: each remaining + * arm extracts and converts. Null when some arm cannot convert. */ +function nonPromiseArmsAs( + lowerer: Lowerer, + mp: MaybePromiseUnion, + ref: IrExpr, + target: IrType, + loc: SrcLoc, +): IrExpr | null { + const pieces: { tag: number; value: IrExpr }[] = []; + for (let tag = 0; tag < mp.def.arms.length; tag++) { + if (tag === mp.promiseTag) continue; + const arm = mp.def.arms[tag]!; + const source: IrExpr = + arm.kind === "undefinedT" || arm.kind === "nullT" + ? { + kind: "unitLit", + unit: arm.kind === "undefinedT" ? "undefined" : "null", + type: arm, + loc, + } + : { kind: "unionNarrow", unionId: mp.def.id, tag, value: ref, type: arm, loc }; + const value = lowerer.coerceToExpected(source, target); + if (!typeEquals(value.type, target)) return null; + pieces.push({ tag, value }); + } + let result: IrExpr | null = null; + for (let i = pieces.length - 1; i >= 0; i--) { + const piece = pieces[i]!; + result = + result === null + ? piece.value + : { + kind: "ternary", + cond: unionTagTest(mp, ref, piece.tag, loc), + then: piece.value, + else_: result, + type: target, + loc, + }; + } + return result; +} + +function hopStmt(loc: SrcLoc): IrStmt { + return { + kind: "exprStmt", + expr: { kind: "libCall", fn: "async.hop", args: [], type: VOID, loc }, + loc, + }; +} + +/** `await u` over a maybe-promise union. JS's await resolves the operand: + * a native promise is awaited directly (resumption one turn after it + * settles; a rejection throws here), and any other non-thenable value + * resumes after exactly one microtask turn with the value itself. The + * result type is the checker's type of the await expression (`Awaited` + * of the union), which holds the promise's settled type and every data + * arm. */ +export function lowerMaybePromiseAwait( + lowerer: Lowerer, + expr: ts.AwaitExpression, + value: IrExpr, + mp: MaybePromiseUnion, + loc: SrcLoc, +): IrExpr { + const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (!resultT || resultT.kind === "void" || resultT.kind === "dyn" || isUnitType(resultT)) { + lowerer.unsupported( + "SC1090", + expr, + `awaiting '${lowerer.fmt(value.type)}' (the awaited result type has no compiled representation)`, + ); + } + const local = lowerer.declareHiddenLocal("%maybePromise", value.type); + const ref = varRef(local.id, value.type, loc); + const inner = mp.promise.inner; + const awaited: IrExpr = { + kind: "awaitExpr", + value: { + kind: "unionNarrow", + unionId: mp.def.id, + tag: mp.promiseTag, + value: ref, + type: mp.promise, + loc, + }, + type: inner, + loc, + }; + let settled: IrExpr | null; + if (inner.kind === "void") { + const undef = lowerer.wrappedUndefined(resultT, loc); + settled = + undef === null + ? null + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: awaited, loc }], + result: undef, + type: resultT, + loc, + }; + } else { + settled = lowerer.coerceToExpected(awaited, resultT); + if (!typeEquals(settled.type, resultT)) settled = null; + } + const rest = nonPromiseArmsAs(lowerer, mp, ref, resultT, loc); + if (settled === null || rest === null) { + lowerer.unsupported( + "SC1090", + expr, + `awaiting '${lowerer.fmt(value.type)}' (its arms do not all convert to the awaited result '${lowerer.fmt(resultT)}')`, + ); + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result: { + kind: "ternary", + cond: unionTagTest(mp, ref, mp.promiseTag, loc), + then: settled, + else_: { kind: "seqExpr", stmts: [hopStmt(loc)], result: rest, type: resultT, loc }, + type: resultT, + loc, + }, + type: resultT, + loc, + }; +} + +/** `return u` of a promise-armed union from an async function (or a then + * handler's lifted body) whose settled type is `R`: the promise arm is + * adopted exactly like `return p` (the spec's thenable resolution, with + * its then-job turn), and the other arms fulfill directly. Null when an + * arm cannot convert to R. */ +export function maybePromiseReturnStmt( + lowerer: Lowerer, + value: IrExpr, + mp: MaybePromiseUnion, + R: IrType, + coerce: (awaited: IrExpr) => IrExpr, +): IrStmt | null { + const loc = value.loc; + const local = lowerer.declareHiddenLocal("%maybePromise", value.type); + const ref = varRef(local.id, value.type, loc); + const promise: IrExpr = { + kind: "unionNarrow", + unionId: mp.def.id, + tag: mp.promiseTag, + value: ref, + type: mp.promise, + loc, + }; + let otherwise: IrStmt[]; + if (R.kind === "void") { + otherwise = [{ kind: "return", value: null, loc }]; + } else { + const rest = nonPromiseArmsAs(lowerer, mp, ref, R, loc); + if (rest === null) return null; + otherwise = [{ kind: "return", value: rest, loc }]; + } + return { + kind: "block", + body: [ + { kind: "varDecl", localId: local.id, init: value, loc }, + { + kind: "if", + cond: unionTagTest(mp, ref, mp.promiseTag, loc), + then: [lowerer.adoptingReturn(promise, R, coerce)], + else_: otherwise, + loc, + }, + ], + loc, + }; +} + +/** `Promise.resolve(u)` over a maybe-promise union: a promise argument is + * returned as is (every scriptc promise is native, so the spec's + * constructor check answers identity), any other value fulfills a fresh + * promise. Null when the promise arm is not already the result type (no + * identity-preserving payload adapter exists) or a data arm cannot convert. */ +export function maybePromiseResolve( + lowerer: Lowerer, + value: IrExpr, + mp: MaybePromiseUnion, + resultT: IrType & { kind: "promise" }, + loc: SrcLoc, +): IrExpr | null { + const local = lowerer.declareHiddenLocal("%maybePromise", value.type); + const ref = varRef(local.id, value.type, loc); + let promise: IrExpr = { + kind: "unionNarrow", + unionId: mp.def.id, + tag: mp.promiseTag, + value: ref, + type: mp.promise, + loc, + }; + if (!typeEquals(mp.promise, resultT)) { + if (resultT.inner.kind !== "void") return null; + promise = { kind: "promiseVoidWiden", value: promise, type: resultT, loc }; + } + let fresh: IrExpr; + if (resultT.inner.kind === "void") { + fresh = { kind: "intrinsic", name: "promise.resolve", args: [], type: resultT, loc }; + } else { + const rest = nonPromiseArmsAs(lowerer, mp, ref, resultT.inner, loc); + if (rest === null) return null; + fresh = { kind: "intrinsic", name: "promise.resolve", args: [rest], type: resultT, loc }; + } + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], + result: { + kind: "ternary", + cond: unionTagTest(mp, ref, mp.promiseTag, loc), + then: promise, + else_: fresh, + type: resultT, + loc, + }, + type: resultT, + loc, + }; +} + +/** `u instanceof Promise` over a maybe-promise (or promise-or-unit) union: + * the union tag test. */ +export function maybePromiseInstanceOf( + lowerer: Lowerer, + value: IrExpr, + loc: SrcLoc, +): IrExpr | null { + if (value.type.kind !== "union") return null; + const def = lowerer.unions.get(value.type.unionId); + const tag = def ? def.arms.findIndex((a) => a.kind === "promise") : -1; + if (!def || tag < 0) return null; + return { kind: "unionIsTag", unionId: def.id, tag, negated: false, value, type: BOOL, loc }; +} diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 7ecf44659..dc7aca503 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -34,6 +34,7 @@ import { SYMBOL_T, typeEquals, typeKey, + maybePromiseUnionArmsOk, unionContainerArmsOk, UNDEFINED_T, VOID, @@ -3942,6 +3943,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } const params: IrType[] = []; let typedRest = false; + let dynRest = false; for (const p of sig.getParameters()) { const decl = checker.valueDeclarationOf(p); const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined; @@ -4004,6 +4006,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (!armed) return null; pt = armed; } + // `...args: unknown[]` / `...args: any[]`: the checked-dynamic + // variadic ABI (no typed slot; the callee's hidden trailing dyn + // array receives every surplus argument, as for JS rest params). + if ( + rest && + (pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker))) + ) { + dynRest = true; + continue; + } if (!pt) return null; if (rest) { if (pt.kind !== "array") return null; @@ -4064,7 +4076,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (!ret) return null; return typedRest ? { kind: "func", params, ret, rest: true, restAbi: "typed" } - : funcOf(params, ret); + : dynRest + ? { kind: "func", params, ret, rest: true } + : funcOf(params, ret); } // Records: object types whose members are all data properties (shorthand // methods in type position count — they're func-typed fields) with @@ -4312,7 +4326,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { a.kind === "dyn" || a.kind === "generator", ) || - !unionContainerArmsOk(arms) + !maybePromiseUnionOk(parts, arms, ctx) ) { return null; } @@ -6077,9 +6091,49 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { } } +/** `any[]` (or `readonly any[]`): an array type whose element is `any`. */ +export function isAnyArrayType(t: ts.Type, checker: ts.TypeChecker): boolean { + if (!checker.isArrayType(t)) return false; + const elem = checker.getTypeArguments(t as ts.TypeReference)[0]; + return elem !== undefined && (elem.flags & ts.TypeFlags.Any) !== 0; +} + +/** The maybe-promise union rule (mapTypeInner's union rule; IR + * maybePromiseUnionArmsOk): at most one promise arm, and when it has data + * siblings none of them may be thenable — `await` and async `return` + * dispatch on the union tag, so a data arm whose type declares `then` + * would skip the thenable adoption JS performs. */ +function maybePromiseUnionOk( + parts: readonly ts.Type[], + arms: IrType[], + ctx: TypeMapperCtx, +): boolean { + if (unionContainerArmsOk(arms)) return true; + if (!maybePromiseUnionArmsOk(arms)) return false; + // A function beside a promise (`Promise | (() => Promise)`, the + // assert.rejects argument) keeps its existing representation: callers + // dispatch on callability, not on a then-able promise. + if (arms.some((arm) => arm.kind === "func")) return false; + return maybePromiseThenableArm(parts, ctx) === null; +} + +/** The first non-promise constituent of a promise-armed union whose type + * has a `then` member (a thenable data arm), or null. */ +function maybePromiseThenableArm(parts: readonly ts.Type[], ctx: TypeMapperCtx): ts.Type | null { + const { checker } = ctx; + for (const part of parts) { + if (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null)) continue; + if (mapType(part, ctx)?.kind === "promise") continue; + // Primitive arms answer through their wrapper prototypes, none of + // which has `then`; object arms answer through their own members. + if (checker.getPropertyOfType(part, "then")) return part; + } + return null; +} + /** Arm kinds with no home in a compiled union (mapTypeInner's union rule): * no runtime narrowing test exists against sibling data arms. */ -function armHasUnionHome(arm: IrType, siblingCount: number): boolean { +function armHasUnionHome(arm: IrType): boolean { switch (arm.kind) { case "void": case "union": @@ -6088,9 +6142,6 @@ function armHasUnionHome(arm: IrType, siblingCount: number): boolean { case "generator": case "dyn": return false; - // Promise data siblings still lack a supported narrowing contract. - case "promise": - return siblingCount === 0; default: return true; } @@ -6215,10 +6266,24 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): if (!mapped) { return `the union shape is supported, but its arm '${text(part)}' does not compile`; } - if (!armHasUnionHome(mapped, dataArms.length - 1)) { + if (!armHasUnionHome(mapped)) { return `the union shape is supported, but '${text(part)}' arms have no home in a compiled union yet (no runtime narrowing test exists against sibling arms)`; } } + const promiseArms = mappedArms.filter(({ mapped }) => mapped?.kind === "promise"); + if (promiseArms.length > 1) { + return `the union shape is supported, but it has several promise arms ('${text(promiseArms[0]!.p)}' and '${text(promiseArms[1]!.p)}'), and a compiled union holds at most one (the union tag is the 'instanceof Promise' test)`; + } + if (promiseArms.length === 1 && dataArms.length > 1) { + const callable = mappedArms.find(({ mapped }) => mapped?.kind === "func"); + if (callable) { + return `the union shape is supported, but its function arm '${text(callable.p)}' sits beside a promise arm, and a maybe-promise union holds only data arms beside its promise`; + } + const thenable = maybePromiseThenableArm(parts, ctx); + if (thenable) { + return `the union shape is supported, but its arm '${text(thenable)}' declares 'then' beside a promise arm, so 'await' would have to adopt it as a thenable (a maybe-promise union's data arms must not be thenable)`; + } + } return null; } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..be6519a9d 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -667,7 +667,12 @@ export function isIdentityCollectionKey(t: IrType): boolean { t.kind === "object" || t.kind === "array" || t.kind === "netServer" || - t.kind === "symbol" + t.kind === "symbol" || + // A function value is one closure for its whole life (top-level + // functions intern one immortal closure, inner closures allocate once + // per evaluation), so pointer identity is JS function identity. + // Closures are cycle-headered and traced like records. + t.kind === "func" ); } @@ -713,8 +718,8 @@ export function isSupportedSetElem(t: IrType, unionArms?: IrType[]): boolean { return isSupportedMapKey(t, unionArms); } -/** The Map VALUE fence: scalars, supported native references and checked - * values. Functions require checked-value boxing. +/** The Map VALUE fence: scalars, supported native references, closures + * and checked values. * Record/object/union values can point back at the map holding them, which * is exactly why ref-valued maps are cycle-capable (see the backend's * cycle analysis and docs/memory.md). Shared frontend/validator. */ @@ -768,6 +773,12 @@ export function isSupportedMapValue(t: IrType): boolean { // a regex holds no references), map form. case "regex": return true; + // A closure (Map U> — the factory/handler registry): + // the index-signature overflow story (scr_closure adapters; closures + // are cycle-headered and traced, so a handler capturing its own + // registry collects). + case "func": + return true; default: return false; } @@ -783,7 +794,7 @@ export function isSupportedMapValue(t: IrType): boolean { * Nested index-signature RECORDS ride the record kind like any other. * Shared frontend (mapType) / validator. */ export function isSupportedIndexValue(t: IrType): boolean { - return t.kind === "dyn" || t.kind === "func" || isSupportedMapValue(t); + return t.kind === "dyn" || isSupportedMapValue(t); } export function funcOf(params: IrType[], ret: IrType): IrType { @@ -791,14 +802,25 @@ export function funcOf(params: IrType[], ret: IrType): IrType { } /** Maps and sets use tagged union storage and retain their native identity. - * Promises still require nullable-only unions until their data-arm narrowing - * and await boundary have a matching contract. */ + * Joined unions (inferred joins of member reads and array elements) keep + * promises nullable-only; declared maybe-promise unions are admitted by + * maybePromiseUnionArmsOk. */ export function unionContainerArmsOk(arms: IrType[]): boolean { return arms.every( (a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)), ); } +/** The MAYBE-PROMISE union (`T | Promise`, `Promise | R | + * void`): at most one promise arm, beside unit or data arms. The union tag + * is the `instanceof Promise` test, and `await` / async `return` branch on + * it (the promise arm awaits or adopts, the other arms take the + * non-thenable path). Data arms must not be thenable; the type mapper + * checks that against the checker types before minting such a union. */ +export function maybePromiseUnionArmsOk(arms: IrType[]): boolean { + return arms.filter((a) => a.kind === "promise").length <= 1; +} + /** Canonical, injective text form of an IrType — the building block of * shape/union identity keys, generic-function instantiation keys, and the * backend's per-type helper interning (jsonStringify/dynCheck walkers). diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index 8086cc176..e4f423411 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -286,17 +286,25 @@ test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as }, ); -test.each([{ kind: "promise", inner: F64 } as IrType])( - "%j payloads still refuse unrelated data siblings", - (type) => { - const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [ - { id: "mixed", arms: [type, STRING, UNDEFINED_T] }, - ]); - expect(validateModule(mod).map((error) => error.message)).toContain( - `union mixed: ${type.kind} arm 0 beside non-unit arms`, - ); - }, -); +test("a maybe-promise union holds one promise arm beside data arms", () => { + const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [ + { id: "maybe", arms: [{ kind: "promise", inner: F64 }, STRING, UNDEFINED_T] }, + ]); + expect(validateModule(mod)).toEqual([]); + expect(deserializeModule(serializeModule(mod))).toEqual(mod); +}); + +test("a union refuses a second promise arm", () => { + const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [ + { + id: "mixed", + arms: [BOOL, { kind: "promise", inner: F64 }, { kind: "promise", inner: STRING }], + }, + ]); + expect(validateModule(mod).map((error) => error.message)).toContain( + "union mixed: promise arm 1 beside another promise arm", + ); +}); test("differently typed collection payloads preserve separate union tags", () => { const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [ diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 09ab7d951..73c819ed9 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -53,7 +53,7 @@ import { STRING, typeEquals, typeKey, - unionContainerArmsOk, + maybePromiseUnionArmsOk, VOID, } from "./ir.js"; import { DYN_CLASS_PROPERTIES } from "./ir.js"; @@ -821,7 +821,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { u.arms.forEach((arm, i) => { // The unit kinds (undefinedT/nullT) are valid arms — union membership // is the ONLY place they may appear. Container payloads preserve - // identity; promise data siblings retain their separate fence. + // identity; a union carries at most one promise arm (maybe-promise). if ( arm.kind === "void" || arm.kind === "union" || @@ -831,9 +831,9 @@ export function validateModule(mod: IrModule): IrValidationError[] { ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); } - if (arm.kind === "promise" && !unionContainerArmsOk(u.arms)) { + if (arm.kind === "promise" && !maybePromiseUnionArmsOk(u.arms)) { errors.push({ - message: `union ${u.id}: ${arm.kind} arm ${i} beside non-unit arms`, + message: `union ${u.id}: ${arm.kind} arm ${i} beside another promise arm`, loc: noLoc, }); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6240-maybe-promise-await-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6240-maybe-promise-await-order.ts.json new file mode 100644 index 000000000..87429c80b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6240-maybe-promise-await-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6240-maybe-promise-await-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6241-maybe-promise-handlers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6241-maybe-promise-handlers.ts.json new file mode 100644 index 000000000..3b5e1fbed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6241-maybe-promise-handlers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6241-maybe-promise-handlers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6242-maybe-promise-rejections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6242-maybe-promise-rejections.ts.json new file mode 100644 index 000000000..b8bec7cac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6242-maybe-promise-rejections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6242-maybe-promise-rejections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6243-maybe-promise-narrowing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6243-maybe-promise-narrowing.ts.json new file mode 100644 index 000000000..555b5e987 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6243-maybe-promise-narrowing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6243-maybe-promise-narrowing.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6244-maybe-promise-async-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6244-maybe-promise-async-return.ts.json new file mode 100644 index 000000000..275f06bef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6244-maybe-promise-async-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6244-maybe-promise-async-return.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6245-rest-unknown-handlers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6245-rest-unknown-handlers.ts.json new file mode 100644 index 000000000..d367e5eb5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6245-rest-unknown-handlers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6245-rest-unknown-handlers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6246-map-set-function-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6246-map-set-function-values.ts.json new file mode 100644 index 000000000..ee4654507 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6246-map-set-function-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6246-map-set-function-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6247-next-proxy-maybe-promise.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6247-next-proxy-maybe-promise.ts.json new file mode 100644 index 000000000..83a605ab0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6247-next-proxy-maybe-promise.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6247-next-proxy-maybe-promise.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6248-tagged-template-any-rest.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6248-tagged-template-any-rest.ts.json new file mode 100644 index 000000000..e5cf59339 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6248-tagged-template-any-rest.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6248-tagged-template-any-rest.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6249-tagged-template-missing-substitutions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6249-tagged-template-missing-substitutions.ts.json new file mode 100644 index 000000000..c01942ee0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6249-tagged-template-missing-substitutions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6249-tagged-template-missing-substitutions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/maybe-promise-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/maybe-promise-fences.ts.json new file mode 100644 index 000000000..2e463b920 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/maybe-promise-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/maybe-promise-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts.json deleted file mode 100644 index 2af7d599f..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts.json deleted file mode 100644 index 571eaa53d..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts" - ], - "diags": [] -} diff --git a/tests/corpus/6240-maybe-promise-await-order.ts b/tests/corpus/6240-maybe-promise-await-order.ts new file mode 100644 index 000000000..728278348 --- /dev/null +++ b/tests/corpus/6240-maybe-promise-await-order.ts @@ -0,0 +1,60 @@ +// `await` on a maybe-promise union (`T | Promise`): the promise arm +// resumes one turn after it settles, every other arm is a non-thenable that +// resumes after exactly one microtask turn with the value itself. A ticker +// chain interleaves with each await so the turn counts are observable. + +type MaybeString = string | Promise; + +const log: string[] = []; +let tick = 0; +function ticker(turns: number): void { + if (turns === 0) return; + Promise.resolve().then(() => { + tick++; + log.push("T" + tick); + ticker(turns - 1); + }); +} + +const settled: Promise = Promise.resolve("settled"); +function choose(kind: number): MaybeString { + if (kind === 0) return "plain"; + if (kind === 1) return settled; + // Settles two turns from now. + return new Promise((resolve) => { + Promise.resolve().then(() => resolve("late")); + }); +} + +async function awaitOnce(kind: number): Promise { + log.push("start" + kind); + const value = await choose(kind); + log.push("got" + kind + ":" + value); +} + +async function awaitTwice(): Promise { + const first = await choose(0); + const second = await choose(1); + log.push("twice:" + first + "+" + second); +} + +// A union with unit arms beside the data arm: `undefined` is a +// non-thenable too. +type MaybeCount = number | undefined | Promise; +function count(kind: number): MaybeCount { + return kind === 0 ? 7 : kind === 1 ? undefined : Promise.resolve(kind === 2 ? 9 : undefined); +} +async function counts(): Promise { + for (let kind = 0; kind < 4; kind++) { + const value = await count(kind); + log.push("count" + kind + ":" + String(value)); + } +} + +ticker(12); +awaitOnce(0); +awaitOnce(1); +awaitOnce(2); +awaitTwice(); +counts(); +setTimeout(() => console.log(log.join(" ")), 5); diff --git a/tests/corpus/6241-maybe-promise-handlers.ts b/tests/corpus/6241-maybe-promise-handlers.ts new file mode 100644 index 000000000..49549eaa4 --- /dev/null +++ b/tests/corpus/6241-maybe-promise-handlers.ts @@ -0,0 +1,77 @@ +// Handlers typed `Promise | R | void` (an extension event +// handler) and members typed `(p: string) => boolean | Promise` +// (a tool's filesystem operations): sync and async implementations share +// one slot, and the caller awaits whatever comes back. + +interface Ops { + exists: (absolutePath: string) => boolean | Promise; +} +const syncOps: Ops = { exists: (p) => p.length > 3 }; +const asyncOps: Ops = { exists: async (p) => p.startsWith("/") }; + +interface Result { + block: boolean; + reason: string; +} +type Handler = (n: number) => Promise | Result | void; + +const handlers: Handler[] = []; +handlers.push((n) => { + if (n > 5) return { block: true, reason: "sync big" }; +}); +handlers.push(async (n) => { + if (n === 3) return { block: true, reason: "async three" }; +}); +handlers.push((n) => (n === 4 ? Promise.resolve({ block: false, reason: "promise four" }) : undefined)); + +// Named functions flow into the slot through their declared types. +async function asyncNamed(n: number): Promise { + return { block: n === 2, reason: "async named" }; +} +function syncNamed(n: number): Result { + return { block: n === 1, reason: "sync named" }; +} +function voidNamed(n: number): void { + console.log("void handler saw", n); +} +async function asyncVoid(n: number): Promise { + console.log("async void handler saw", n); +} +const named: Handler[] = [asyncNamed, syncNamed, voidNamed, asyncVoid]; + +async function run(list: Handler[], n: number): Promise { + for (const h of list) { + const r = await h(n); + if (r && r.block) return r.reason; + } + return "none"; +} + +// A class-instance arm beside null and the promise arm. +class Response { + status: number; + constructor(status: number) { + this.status = status; + } +} +type Route = (path: string) => Response | null | Promise; +const routes: Route[] = [ + (path) => (path === "/sync" ? new Response(200) : null), + async (path) => (path === "/async" ? new Response(202) : null), +]; +async function route(path: string): Promise { + for (const r of routes) { + const res = await r(path); + if (res instanceof Response) return path + " " + res.status; + } + return path + " 404"; +} + +async function main(): Promise { + console.log(await syncOps.exists("/tmp/x"), await syncOps.exists("/a")); + console.log(await asyncOps.exists("/tmp/x"), await asyncOps.exists("tmp")); + for (const n of [1, 3, 4, 7]) console.log(n, await run(handlers, n)); + for (const n of [1, 2, 9]) console.log(n, await run(named, n)); + for (const path of ["/sync", "/async", "/missing"]) console.log(await route(path)); +} +main(); diff --git a/tests/corpus/6242-maybe-promise-rejections.ts b/tests/corpus/6242-maybe-promise-rejections.ts new file mode 100644 index 000000000..6323d6242 --- /dev/null +++ b/tests/corpus/6242-maybe-promise-rejections.ts @@ -0,0 +1,77 @@ +// Rejections through maybe-promise values: a rejected promise arm throws at +// the await, a synchronous throw never reaches the await, an async return +// adopts the promise arm (so its rejection settles the caller's promise +// after the function's own handlers), and a `Promise | void` +// callback's promise is adopted by an async function returning it. + +type MaybeBool = boolean | Promise; +function check(kind: number): MaybeBool { + if (kind === 0) return true; + if (kind === 1) return Promise.reject(new Error("rejected " + kind)); + if (kind === 2) throw new Error("thrown " + kind); + return Promise.resolve(false); +} + +async function awaitEach(): Promise { + for (let kind = 0; kind < 4; kind++) { + try { + console.log(kind, await check(kind)); + } catch (e) { + console.log(kind, "caught", (e as Error).message); + } + } +} + +// `return check(1)` inside try/catch/finally: JS completes the return first +// (the finally runs), and only then does the async function's promise adopt +// the rejected arm, so this function's own catch never sees it. +async function returnInTry(kind: number): Promise { + try { + return check(kind); + } catch (e) { + console.log("inner catch", (e as Error).message); + return false; + } finally { + console.log("finally", kind); + } +} + +type Callback = () => Promise | void; +const callbacks: Callback[] = [ + () => { + console.log("sync callback"); + }, + async () => { + throw new Error("async callback failed"); + }, +]; +async function runCallback(cb: Callback): Promise { + return cb(); +} + +async function main(): Promise { + await awaitEach(); + for (const kind of [0, 1, 2]) { + try { + console.log("returned", await returnInTry(kind)); + } catch (e) { + console.log("outer catch", (e as Error).message); + } + } + for (const cb of callbacks) { + try { + await runCallback(cb); + console.log("callback ok"); + } catch (e) { + console.log("callback caught", (e as Error).message); + } + } + // A then handler returning a rejected promise arm rejects the chain. + await Promise.resolve(1) + .then(() => check(1)) + .then( + (v) => console.log("then value", v), + (e: Error) => console.log("then rejected", e.message), + ); +} +main(); diff --git a/tests/corpus/6243-maybe-promise-narrowing.ts b/tests/corpus/6243-maybe-promise-narrowing.ts new file mode 100644 index 000000000..7c1fe6fd2 --- /dev/null +++ b/tests/corpus/6243-maybe-promise-narrowing.ts @@ -0,0 +1,61 @@ +// Telling the arms of a maybe-promise union apart: `instanceof Promise`, +// the `then` probe through a cast, typeof, truthiness, equality, and +// Promise.resolve (which hands a promise back as is and wraps a value). + +type MaybeString = string | Promise; +function make(i: number): MaybeString { + return i % 2 === 1 ? "s" + i : Promise.resolve("p" + i); +} + +interface Box { + v: number; +} +type MaybeBox = Box | Promise; +function box(i: number): MaybeBox { + return i % 2 === 0 ? { v: i } : Promise.resolve({ v: i * 10 }); +} +function isThenable(x: MaybeBox): boolean { + return typeof (x as any).then === "function"; +} + +type Slot = { name: string; value: string | Promise | null }; +const slots: Slot[] = [ + { name: "a", value: "x" }, + { name: "b", value: Promise.resolve("y") }, + { name: "c", value: null }, +]; + +async function main(): Promise { + for (let i = 0; i < 4; i++) { + const v = make(i); + if (v instanceof Promise) console.log(i, "promise", await v); + else console.log(i, "value", v.toUpperCase()); + console.log(typeof v); + } + for (let i = 0; i < 3; i++) { + const b = box(i); + console.log(i, isThenable(b), typeof (b as Promise).then); + if ((b as Promise).then) console.log("has then"); + console.log((await b).v); + } + for (const s of slots) { + const v = await s.value; + console.log(s.name, v === null ? "null" : v, s.value === null, s.value instanceof Promise, typeof s.value); + if (s.value) console.log("truthy", s.name); + } + console.log(slots[1]!.value === slots[1]!.value); + + // Promise.resolve keeps a promise's identity and wraps a value. + const p = Promise.resolve(7); + const get = (b: boolean): number | Promise => (b ? p : 3); + const fromPromise = Promise.resolve(get(true)); + const fromValue = Promise.resolve(get(false)); + console.log(fromPromise === p, fromValue === p); + const order: string[] = []; + fromPromise.then((v) => order.push("promise" + v)); + fromValue.then((v) => order.push("value" + v)); + Promise.resolve().then(() => order.push("other")); + await new Promise((resolve) => setTimeout(resolve, 1)); + console.log(order.join(",")); +} +main(); diff --git a/tests/corpus/6244-maybe-promise-async-return.ts b/tests/corpus/6244-maybe-promise-async-return.ts new file mode 100644 index 000000000..9c38a8f7d --- /dev/null +++ b/tests/corpus/6244-maybe-promise-async-return.ts @@ -0,0 +1,61 @@ +// Returning a maybe-promise value from async code flattens it: the +// promise arm is adopted (the extra then-job turns of a returned promise), +// any other arm fulfills directly. Covers block bodies, concise arrow +// bodies, `return await`, and then handlers. + +type MaybeString = string | Promise; +const settled: Promise = Promise.resolve("settled"); +function choose(kind: number): MaybeString { + if (kind === 0) return "plain"; + if (kind === 1) return settled; + return new Promise((resolve) => setTimeout(() => resolve("timer"), 1)); +} + +async function viaReturn(kind: number): Promise { + return choose(kind); +} +async function viaReturnAwait(kind: number): Promise { + return await choose(kind); +} +const viaArrow = async (kind: number): Promise => choose(kind); + +const log: string[] = []; +let tick = 0; +function ticker(turns: number): void { + if (turns === 0) return; + Promise.resolve().then(() => { + tick++; + log.push("T" + tick); + ticker(turns - 1); + }); +} + +// A then handler returning a maybe-promise value: a cache that answers +// synchronously once loaded. +const cache = new Map(); +function load(key: string): string | Promise { + const hit = cache.get(key); + if (hit !== undefined) return hit; + return new Promise((resolve) => + setTimeout(() => { + cache.set(key, key.toUpperCase()); + resolve(key.toUpperCase()); + }, 1), + ); +} + +ticker(10); +for (let kind = 0; kind < 3; kind++) { + viaReturn(kind).then((v) => log.push("ret" + kind + ":" + v)); + viaReturnAwait(kind).then((v) => log.push("await" + kind + ":" + v)); + viaArrow(kind).then((v) => log.push("arrow" + kind + ":" + v)); +} +const start = Promise.resolve("k"); +start + .then((key) => load(key)) + .then((v) => { + log.push("first:" + v); + return start.then((key) => load(key)); + }) + .then((v) => log.push("second:" + v)); +setTimeout(() => console.log(log.join(" ")), 20); diff --git a/tests/corpus/6245-rest-unknown-handlers.ts b/tests/corpus/6245-rest-unknown-handlers.ts new file mode 100644 index 000000000..664c6160d --- /dev/null +++ b/tests/corpus/6245-rest-unknown-handlers.ts @@ -0,0 +1,46 @@ +// Rest parameters typed `unknown[]` or `any[]` in function types: the +// handler registry of an extension host, with forwarding wrappers and +// spread calls. + +type HandlerFn = (...args: unknown[]) => Promise; +const handlers = new Map(); +function on(event: string, handler: HandlerFn): () => void { + const registered: HandlerFn = (...args) => handler(...args); + const list = handlers.get(event) ?? []; + list.push(registered); + handlers.set(event, list); + return () => { + const current = handlers.get(event); + if (!current) return; + const index = current.indexOf(registered); + if (index !== -1) current.splice(index, 1); + }; +} +async function emit(event: string, ...args: unknown[]): Promise { + const out: unknown[] = []; + for (const h of handlers.get(event) ?? []) out.push(await h(...args)); + return out; +} + +on("x", async (...args) => args.length); +const off = on("x", async (...args: unknown[]) => typeof args[0] + ":" + String(args[1])); +on("y", async (...args) => "no args " + args.length); + +function sum(...nums: unknown[]): number { + let total = 0; + for (const n of nums) if (typeof n === "number") total += n; + return total; +} +const stored: (...values: unknown[]) => number = sum; +const logAll: (...values: any[]) => void = (...values: any[]) => console.log(values.length, values[0]); + +async function main(): Promise { + console.log(JSON.stringify(await emit("x", 1, "two", true))); + off(); + console.log(JSON.stringify(await emit("x", 5))); + console.log(JSON.stringify(await emit("y"))); + console.log(sum(1, 2, "x", 4), stored(5, 6), stored()); + logAll("p", "q"); + logAll(); +} +main(); diff --git a/tests/corpus/6246-map-set-function-values.ts b/tests/corpus/6246-map-set-function-values.ts new file mode 100644 index 000000000..fc9aa187d --- /dev/null +++ b/tests/corpus/6246-map-set-function-values.ts @@ -0,0 +1,52 @@ +// Functions as Map values, Map keys and Set elements: factory and listener +// registries keep each closure's identity, and a handler that captures its +// own registry is collected with it. + +interface Extension { + name: string; + run(x: number): number; +} +type ExtensionFactory = (api: { log: (s: string) => void }) => Extension; +const factories = new Map(); +factories.set("double", (api) => ({ + name: "double", + run: (x) => { + api.log("double " + x); + return x * 2; + }, +})); +factories.set("inc", () => ({ name: "inc", run: (x) => x + 1 })); +const lines: string[] = []; +const api = { log: (s: string) => lines.push(s) }; +for (const [key, make] of factories) { + const ext = make(api); + console.log(key, ext.name, ext.run(10)); +} +console.log(factories.get("missing") === undefined, typeof factories.get("inc"), factories.has("inc")); +const inc = factories.get("inc"); +if (inc) console.log(inc(api).run(41)); +factories.delete("inc"); +console.log([...factories.keys()].join(","), factories.size, lines.join(";")); + +type Listener = (v: string) => void; +const listeners = new Set(); +const a: Listener = (v) => console.log("a", v); +const b: Listener = (v) => console.log("b", v); +listeners.add(a); +listeners.add(b); +listeners.add(a); +console.log(listeners.size, listeners.has(a), listeners.has((v) => console.log(v))); +for (const l of listeners) l("x"); +listeners.delete(a); +listeners.forEach((l) => l("y")); + +const counts = new Map(); +counts.set(a, 1); +counts.set(b, 2); +counts.set(a, 3); +console.log(counts.size, counts.get(a), counts.get(b)); + +const registry = new Map number>(); +registry.set("self", () => registry.size); +registry.set("double", () => registry.size * 2); +console.log([...registry.values()].map((f) => f()).join(",")); diff --git a/tests/corpus/6247-next-proxy-maybe-promise.ts b/tests/corpus/6247-next-proxy-maybe-promise.ts new file mode 100644 index 000000000..264742e2b --- /dev/null +++ b/tests/corpus/6247-next-proxy-maybe-promise.ts @@ -0,0 +1,35 @@ +// The Next.js proxy signature: `(request, event) => NextMiddlewareResult | +// Promise` where the result is a response class, null, +// undefined or void. Sync and async proxies run through one host. + +class NextResponse { + status: number; + constructor(status: number) { + this.status = status; + } +} +type NextMiddlewareResult = NextResponse | null | undefined | void; +type NextProxy = (request: string, event: number) => NextMiddlewareResult | Promise; + +const proxy: NextProxy = (request) => { + if (request === "/health") return new NextResponse(200); + if (request === "/async") return Promise.resolve(new NextResponse(202)); + if (request === "/null") return null; +}; +const asyncProxy: NextProxy = async (request) => (request === "/a" ? new NextResponse(201) : undefined); + +async function handle(p: NextProxy, request: string): Promise { + const result = await p(request, 0); + return result ? String(result.status) : String(result); +} + +// The repro shape: Promise.resolve over the handler's result. +type Handler = (n: number) => NextResponse | null | Promise; +const h: Handler = () => new NextResponse(204); +Promise.resolve(h(1)).then((r) => console.log(r ? r.status : "none")); + +async function main(): Promise { + for (const request of ["/health", "/async", "/null", "/other"]) console.log(request, await handle(proxy, request)); + for (const request of ["/a", "/b"]) console.log(request, await handle(asyncProxy, request)); +} +main(); diff --git a/tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts b/tests/corpus/6248-tagged-template-any-rest.ts similarity index 100% rename from tests/diagnostics/retired-engine-2251-tagged-templates-dyn.ts rename to tests/corpus/6248-tagged-template-any-rest.ts diff --git a/tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts b/tests/corpus/6249-tagged-template-missing-substitutions.ts similarity index 100% rename from tests/diagnostics/retired-engine-2790-array-missing-tagged-substitutions.ts rename to tests/corpus/6249-tagged-template-missing-substitutions.ts diff --git a/tests/diagnostics/component-fences.ts b/tests/diagnostics/component-fences.ts index 267102086..6d88f9af5 100644 --- a/tests/diagnostics/component-fences.ts +++ b/tests/diagnostics/component-fences.ts @@ -5,9 +5,9 @@ const byFlag = new Map(); console.log(byFlag); -// Map values have no slot for functions. -const listeners = new Map void>(); -console.log(listeners); +// Map values have no slot for Dates. +const stamps = new Map(); +console.log(stamps); // Boolean Set elements remain outside the supported element domain. const flags = new Set(); diff --git a/tests/diagnostics/maps.ts b/tests/diagnostics/maps.ts index eaa2508d3..fcdef411d 100644 --- a/tests/diagnostics/maps.ts +++ b/tests/diagnostics/maps.ts @@ -4,7 +4,7 @@ // Boolean keys compile alongside the remaining value and method fences. const byFlag = new Map(); -// Values exclude functions (no closure story in the uniform value slot yet). +// Function values compile (closures keep their identity in the slot). const handlers = new Map void>(); // Typed nested Maps retain their identity and are supported. diff --git a/tests/diagnostics/maybe-promise-fences.ts b/tests/diagnostics/maybe-promise-fences.ts new file mode 100644 index 000000000..efe541c9e --- /dev/null +++ b/tests/diagnostics/maybe-promise-fences.ts @@ -0,0 +1,37 @@ +// Maybe-promise unions (`T | Promise`) compile when the union tag can +// answer what JS's await decides. These forms stay refused. + +// A data arm that declares `then` is a thenable: await would adopt it. +interface Thenish { + then(cb: (v: number) => void): void; +} +function thenable(): Thenish | Promise { + return Promise.resolve(1); +} +console.log(typeof thenable); + +// Two promise arms: the union tag cannot tell them apart by instanceof. +function twoPromises(): Promise | Promise | boolean { + return true; +} +console.log(typeof twoPromises); + +// Promises have no inspect lowering, so neither does a union holding one. +function maybe(b: boolean): string | Promise { + return b ? "s" : Promise.resolve("p"); +} +console.log(maybe(true)); + +// Promise.resolve hands the promise arm back as is, so the result type +// must be that arm's own promise type. +const widened = Promise.resolve(maybe(false)); +console.log(typeof widened); + +// `instanceof Promise` on a value that is never a promise. +const record = { v: 1 }; +console.log(record instanceof Promise); + +// A variadic `unknown[]` slot takes closures with the same variadic ABI. +type HandlerFn = (...args: unknown[]) => Promise; +const fixed: HandlerFn = async (first: unknown) => first; +fixed(1, 2).then((v) => console.log(v)); diff --git a/tests/harness/__snapshots__/component-fences.ts.txt b/tests/harness/__snapshots__/component-fences.ts.txt index 498846004..5ed6ad6b9 100644 --- a/tests/harness/__snapshots__/component-fences.ts.txt +++ b/tests/harness/__snapshots__/component-fences.ts.txt @@ -1,27 +1,27 @@ -component-fences.ts:9:7 - error SC2009: values of type 'Map void>' cannot be compiled: the Map shape is supported, but '() => void' values have no Map slot yet +component-fences.ts:9:7 - error SC2009: values of type 'Map' cannot be compiled: the Map shape is supported, but 'Date' values have no Map slot yet - 8 | // Map values have no slot for functions. - 9 | const listeners = new Map void>(); - | ^~~~~~~~~ - 10 | console.log(listeners); + 8 | // Map values have no slot for Dates. + 9 | const stamps = new Map(); + | ^~~~~~ + 10 | console.log(stamps); hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -component-fences.ts:9:19 - error SC1090: Map values of type '() => void' (Map values must be number, string, boolean, records, class instances, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet +component-fences.ts:9:16 - error SC1090: Map values of type 'Date' (Map values must be number, string, boolean, records, class instances, functions, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet - 8 | // Map values have no slot for functions. - 9 | const listeners = new Map void>(); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(listeners); + 8 | // Map values have no slot for Dates. + 9 | const stamps = new Map(); + | ^~~~~~~~~~~~~~~~~~~~~~~ + 10 | console.log(stamps); -component-fences.ts:10:13 - error SC2004: uses of 'listeners' inherit the blocker on its declaration +component-fences.ts:10:13 - error SC2004: uses of 'stamps' inherit the blocker on its declaration - 9 | const listeners = new Map void>(); - 10 | console.log(listeners); - | ^~~~~~~~~ + 9 | const stamps = new Map(); + 10 | console.log(stamps); + | ^~~~~~ 11 | - hint: the declaration of 'listeners' did not compile — fix the diagnostic reported there and these sites clear with it + hint: the declaration of 'stamps' did not compile — fix the diagnostic reported there and these sites clear with it component-fences.ts:35:7 - error SC2009: values of type '() => WeakRef' cannot be compiled: the function shape is supported, but its return type 'WeakRef' does not compile diff --git a/tests/harness/__snapshots__/maps.ts.txt b/tests/harness/__snapshots__/maps.ts.txt index fe64f3787..7d02a345c 100644 --- a/tests/harness/__snapshots__/maps.ts.txt +++ b/tests/harness/__snapshots__/maps.ts.txt @@ -1,19 +1,3 @@ -maps.ts:8:7 - error SC2009: values of type 'Map void>' cannot be compiled: the Map shape is supported, but '() => void' values have no Map slot yet - - 7 | // Values exclude functions (no closure story in the uniform value slot yet). - 8 | const handlers = new Map void>(); - | ^~~~~~~~ - 9 | - - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -maps.ts:8:18 - error SC1090: Map values of type '() => void' (Map values must be number, string, boolean, records, class instances, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet - - 7 | // Values exclude functions (no closure story in the uniform value slot yet). - 8 | const handlers = new Map void>(); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 | - maps.ts:29:28 - error SC1090: JSON.stringify of 'Map' values (only number, string, boolean, records, arrays, unions of those, and 'unknown' stringify) are not supported yet 28 | const m = new Map(); diff --git a/tests/harness/__snapshots__/maybe-promise-fences.ts.txt b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt new file mode 100644 index 000000000..58fb23517 --- /dev/null +++ b/tests/harness/__snapshots__/maybe-promise-fences.ts.txt @@ -0,0 +1,65 @@ +maybe-promise-fences.ts:8:10 - error SC2009: values of type 'Promise | Thenish' cannot be compiled: the union shape is supported, but its arm 'Thenish' declares 'then' beside a promise arm, so 'await' would have to adopt it as a thenable (a maybe-promise union's data arms must not be thenable) + + 7 | } + 8 | function thenable(): Thenish | Promise { + | ^~~~~~~~ + 9 | return Promise.resolve(1); + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +maybe-promise-fences.ts:11:20 - error SC2004: uses of 'thenable' inherit the blocker on its declaration + + 10 | } + 11 | console.log(typeof thenable); + | ^~~~~~~~ + 12 | + + hint: the declaration of 'thenable' did not compile — fix the diagnostic reported there and these sites clear with it + +maybe-promise-fences.ts:14:10 - error SC2009: values of type 'boolean | Promise | Promise' cannot be compiled: the union shape is supported, but it has several promise arms ('Promise' and 'Promise'), and a compiled union holds at most one (the union tag is the 'instanceof Promise' test) + + 13 | // Two promise arms: the union tag cannot tell them apart by instanceof. + 14 | function twoPromises(): Promise | Promise | boolean { + | ^~~~~~~~~~~ + 15 | return true; + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +maybe-promise-fences.ts:17:20 - error SC2004: uses of 'twoPromises' inherit the blocker on its declaration + + 16 | } + 17 | console.log(typeof twoPromises); + | ^~~~~~~~~~~ + 18 | + + hint: the declaration of 'twoPromises' did not compile — fix the diagnostic reported there and these sites clear with it + +maybe-promise-fences.ts:23:13 - error SC1090: console.log of 'Promise | string' values ('Promise' values have no inspect lowering) are not supported yet + + 22 | } + 23 | console.log(maybe(true)); + | ^~~~~~~~~~~ + 24 | + +maybe-promise-fences.ts:27:50 - error SC2020: 'Promise.resolve over 'Promise | string' at this result type' is part of the standard library types but has no scriptc lowering yet + + 26 | // must be that arm's own promise type. + 27 | const widened = Promise.resolve(maybe(false)); + | ^~~~~~~~~~~~ + 28 | console.log(typeof widened); + + hint: the promise arm passes through identically, so it must already be the result's promise type + +maybe-promise-fences.ts:32:13 - error SC1090: 'instanceof Promise' on '{ v: number }' values (only promises, promise-armed unions, and checked-dynamic values have a lowering) are not supported yet + + 31 | const record = { v: 1 }; + 32 | console.log(record instanceof Promise); + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 33 | + +maybe-promise-fences.ts:36:26 - error SC1090: '(unknown) => Promise' values where '() => Promise' is expected is not supported yet + + 35 | type HandlerFn = (...args: unknown[]) => Promise; + 36 | const fixed: HandlerFn = async (first: unknown) => first; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 37 | fixed(1, 2).then((v) => console.log(v)); \ No newline at end of file diff --git a/tests/harness/__snapshots__/retired-engine-2251-tagged-templates-dyn.ts.txt b/tests/harness/__snapshots__/retired-engine-2251-tagged-templates-dyn.ts.txt deleted file mode 100644 index 8d2f13b3e..000000000 --- a/tests/harness/__snapshots__/retired-engine-2251-tagged-templates-dyn.ts.txt +++ /dev/null @@ -1,44 +0,0 @@ -retired-engine-2251-tagged-templates-dyn.ts:6:15 - error SC2009: values of type 'any[]' cannot be compiled: the array shape is supported, but its element type 'any' does not compile - - 5 | // non-function tag throws Node's catchable TypeError). - 6 | function f(...args: any[]): string { - | ^~~~ - 7 | return args.length > 1 - - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -retired-engine-2251-tagged-templates-dyn.ts:11:13 - error SC2004: uses of 'f' inherit the blocker on its declaration - - 10 | } - 11 | console.log(f`a${1}b${2}c`); - | ^ - 12 | console.log(f`plain`); - - hint: the declaration of 'f' did not compile — fix the diagnostic reported there and these sites clear with it - -retired-engine-2251-tagged-templates-dyn.ts:12:13 - error SC2004: uses of 'f' inherit the blocker on its declaration - - 11 | console.log(f`a${1}b${2}c`); - 12 | console.log(f`plain`); - | ^ - 13 | - - hint: the declaration of 'f' did not compile — fix the diagnostic reported there and these sites clear with it - -retired-engine-2251-tagged-templates-dyn.ts:14:29 - error SC2009: values of type 'any[]' cannot be compiled: the array shape is supported, but its element type 'any' does not compile - - 13 | - 14 | function declare(x: any, ...ys: any[]): string { - | ^~ - 15 | return `${String(x)}|${ys.length}`; - - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -retired-engine-2251-tagged-templates-dyn.ts:17:13 - error SC2004: uses of 'declare' inherit the blocker on its declaration - - 16 | } - 17 | console.log(declare`Hello ${0} world!`); - | ^~~~~~~ - 18 | - - hint: the declaration of 'declare' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/__snapshots__/retired-engine-2790-array-missing-tagged-substitutions.ts.txt b/tests/harness/__snapshots__/retired-engine-2790-array-missing-tagged-substitutions.ts.txt deleted file mode 100644 index 6f03a0bfc..000000000 --- a/tests/harness/__snapshots__/retired-engine-2790-array-missing-tagged-substitutions.ts.txt +++ /dev/null @@ -1,17 +0,0 @@ -retired-engine-2790-array-missing-tagged-substitutions.ts:11:25 - error SC2009: values of type 'any[]' cannot be compiled: the array shape is supported, but its element type 'any' does not compile - - 10 | - 11 | function dynamicRest(...args: any[]): string { - | ^~~~ - 12 | return `${args.length}:${String(args[1])}`; - - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -retired-engine-2790-array-missing-tagged-substitutions.ts:14:13 - error SC2004: uses of 'dynamicRest' inherit the blocker on its declaration - - 13 | } - 14 | console.log(dynamicRest`a${values[9]}b`); - | ^~~~~~~~~~~ - 15 | - - hint: the declaration of 'dynamicRest' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file From 52303baf4b01f7f0fb3946ca36a178782776a833 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:01:49 -0500 Subject: [PATCH 073/120] Keep the promise of async generic functions whose body reads a missing element The instance result of an async generic function whose body returns an element or index-signature read that can miss is Promise: the promise is always present and settles with undefined. The call site widened the whole promise instead, so the instance and its calls had different signatures and every call failed to compile. --- .../src/frontend/lowering/generic-functions.ts | 7 +++++-- tests/corpus/5954-generic-optional-returns.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 90f83ec2c..1502f7fc5 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -298,9 +298,12 @@ export function genericCallInstance( lowerer.badType(expr, retTs); } // A body that can return an indexed read that misses returns undefined - // there, as in JavaScript, whatever the declared result says. + // there, as in JavaScript, whatever the declared result says. An async + // body's promise is always present and settles with undefined there. const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl) - ? lowerer.runtimeOptionalType(mappedReturn) + ? mappedReturn.kind === "promise" + ? { kind: "promise" as const, inner: lowerer.runtimeOptionalType(mappedReturn.inner) } + : lowerer.runtimeOptionalType(mappedReturn) : mappedReturn; // keyof-constrained type parameters (`K extends keyof T`): the bound diff --git a/tests/corpus/5954-generic-optional-returns.ts b/tests/corpus/5954-generic-optional-returns.ts index 9aaec5889..3f8309c9d 100644 --- a/tests/corpus/5954-generic-optional-returns.ts +++ b/tests/corpus/5954-generic-optional-returns.ts @@ -49,3 +49,18 @@ function second(rows: T[][], row: number, column: number): T { return rows[row][column]; } console.log(second([[1, 2]], 0, 1), second([[1, 2]], 0, 9), second(pairs, 0, 1)); + +// An async generic function's promise is always present; its settled +// value is undefined for a missing element. +async function firstAsync(items: T[]): Promise { + return items[0]; +} +const lookupAsync = async (table: Record, key: string): Promise => table[key]; +console.log(await firstAsync([1, 2]), await firstAsync([]), (await firstAsync([])) === undefined); +console.log(await lookupAsync({ a: "A" }, "a"), await lookupAsync({ a: "A" }, "b")); +async function settle(): Promise { + const found = await firstAsync(["x"]); + const missing = await firstAsync([]); + console.log(found, missing, missing ?? "none"); +} +await settle(); From 05b63df116c9ee10091f75656b96b56b630f3e1f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:05:09 -0500 Subject: [PATCH 074/120] Check Buffer.concat's length before the elements of an untyped list Node's Buffer.concat(list, length) validates length (after returning early for an empty list) before it checks that every element is a Uint8Array. The untyped-list path checked the elements first, so a bad element and a bad length threw ERR_INVALID_ARG_TYPE where Node throws the length's ERR_OUT_OF_RANGE. --- packages/runtime/src/scr_bytes_io.c | 12 ++++++++---- tests/corpus/5973-buffer-concat-untyped.js | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index fc7b3bbb8..4d7ef5f67 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1249,10 +1249,10 @@ static ScrBytes *scr_buffer_from_array_like(const ScrDyn *value, double length) return out; } -/* Buffer.concat over an untyped list: Node's checks (an Array whose every - * element is a Uint8Array, ERR_INVALID_ARG_TYPE otherwise), then the typed - * concat over the elements. total < 0 selects the one-argument form. NULL - * with the exception pending. */ +/* Buffer.concat over an untyped list: Node's checks in Node's order (an + * Array, then the total of the two-argument form unless the list is empty, + * then every element a Uint8Array, ERR_INVALID_ARG_TYPE otherwise), then the + * typed concat over the elements. NULL with the exception pending. */ static ScrBytes *scr_buffer_concat_dyn_impl(const ScrDyn *list, double total, bool has_total) { ScrDyn *materialized = NULL; if (list->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(list) != NULL) { @@ -1265,6 +1265,10 @@ static ScrBytes *scr_buffer_concat_dyn_impl(const ScrDyn *list, double total, bo return NULL; } size_t n = list->v.arr.len; + if (has_total && n > 0 && !scr_bytes_validate_off("length", total, 9007199254740991.0)) { + scr_dyn_release(materialized); + return NULL; + } ScrArr *parts = scr_arr_new(SCR_ELEM_BYTES, (double)n); for (size_t i = 0; i < n; i++) { ScrDyn *item = scr_dyn_index_get(list, (double)i); diff --git a/tests/corpus/5973-buffer-concat-untyped.js b/tests/corpus/5973-buffer-concat-untyped.js index 54256c935..116ca0a2b 100644 --- a/tests/corpus/5973-buffer-concat-untyped.js +++ b/tests/corpus/5973-buffer-concat-untyped.js @@ -1,6 +1,8 @@ // Buffer.concat over an untyped list (JavaScript's `const chunks = []`): // Uint8Array elements concatenate, the length form pads or truncates, and -// a non-array list or a non-Uint8Array element throws Node's TypeError. +// a non-array list or a non-Uint8Array element throws Node's TypeError. The +// length form checks the length before the elements, and an empty list +// returns before either. const chunks = []; chunks.push(Buffer.from("ab")); chunks.push(new Uint8Array([99, 100])); @@ -18,3 +20,19 @@ for (const list of lists) { console.log(e.name, e.code, e.message); } } + +const withLength = [ + [[1], -1], + [[Buffer.from("a"), "b"], NaN], + [[1], 2], + [[], -1], + [[Buffer.from("a")], 1.5], + [[Buffer.from("a"), Buffer.from("b")], 2], +]; +for (const [list, length] of withLength) { + try { + console.log("length", Buffer.concat(list, length).length); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} From 14372c827483a30b94968fbe8e4303bbfc26486c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:09:08 -0500 Subject: [PATCH 075/120] Build "is not a function" errors for Hash, Hmac and KeyObject handles at any length The Hash/Hmac and KeyObject handle dispatchers formatted " is not a function" into a 160-byte stack buffer and passed snprintf's return value as the message length. A callee text longer than about 140 bytes made that length exceed the buffer, so the error copied bytes past it: the message ended in stack contents, and sanitized builds aborted. Both dispatchers now build the message in a growable buffer, as the shared dynamic-call path does, so the TypeError carries the whole callee text, as in Node. Corpus 5774 calls a missing method on a Hash, an Hmac and a KeyObject through callee texts longer than 160 bytes, and through a short one, and corpus-costs.json records its plain and sanitized costs. --- .../5774-crypto-handle-long-callee.cjs.json | 6 +++ packages/runtime/src/scr_lib.c | 18 +++++--- .../corpus/5774-crypto-handle-long-callee.cjs | 46 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json create mode 100644 tests/corpus/5774-crypto-handle-long-callee.cjs diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json new file mode 100644 index 000000000..d2f21dbe2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5774-crypto-handle-long-callee.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5774-crypto-handle-long-callee.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index b0a269a92..90601a0db 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -8076,6 +8076,16 @@ void scr_crypto_hash_release(ScrCryptoHash *hash) { void *scr_crypto_hash_retain_v(void *p) { return scr_crypto_hash_retain(p); } void scr_crypto_hash_release_v(void *p) { scr_crypto_hash_release(p); } +/* Node's TypeError for calling a missing member: " is not a + * function", where `what` is the callee's source text, of any length. */ +static void scr_lib_throw_not_a_function(const char *what) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); +} + /* Hash and Hmac values seen as 'unknown' are native handles. update(), * digest() and Hash's copy() dispatch through the handle, called or read * as values; the other members Node defines on a Hash or Hmac (the @@ -8213,9 +8223,7 @@ static ScrDyn *scr_crypto_hash_dyn_invoke(void *h, ScrDyn *self, const char *met scr_crypto_hash_dyn_unsupported(hash); return NULL; } - char message[160]; - int n = snprintf(message, sizeof message, "%s is not a function", what); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_lib_throw_not_a_function(what); return NULL; } @@ -9501,9 +9509,7 @@ static ScrDyn *scr_key_object_invoke(void *h, ScrDyn *self, const char *method, scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return NULL; } - char message[160]; - int n = snprintf(message, sizeof message, "%s is not a function", what); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_lib_throw_not_a_function(what); return NULL; } diff --git a/tests/corpus/5774-crypto-handle-long-callee.cjs b/tests/corpus/5774-crypto-handle-long-callee.cjs new file mode 100644 index 000000000..89a5fbbbf --- /dev/null +++ b/tests/corpus/5774-crypto-handle-long-callee.cjs @@ -0,0 +1,46 @@ +// A missing method called on a Hash, Hmac or KeyObject held in an untyped +// value throws Node's TypeError with the whole callee text, also when the +// text is longer than a fixed-size message buffer. +const { createHash, createHmac, createSecretKey } = require("node:crypto"); + +const holder = { + aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes: + createHash("sha256"), + anHmacValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes: + createHmac("sha256", "key"), + aSecretKeyObjectStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixty: + createSecretKey(Buffer.alloc(16, 1)), +}; + +function report(label, run) { + try { + run(); + console.log(label, "did not throw"); + } catch (e) { + console.log(label, e instanceof TypeError, e.name, e.message.length); + console.log(e.message); + } +} + +report("hash", () => + holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes.missingMethodOnTheHashValue(), +); +report("hmac", () => + holder.anHmacValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes.missingMethodOnTheHmacValue(), +); +report("key", () => + holder.aSecretKeyObjectStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixty.missingMethodOnTheKeyObject(), +); + +// A short callee text through an untyped parameter, for comparison. +function callMissing(value) { + return value.nope(); +} +report("short", () => + callMissing(holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes), +); +console.log( + holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes + .update("abc") + .digest("hex"), +); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index a6bb392cb..39dd56b8b 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2041,6 +2041,7 @@ "5771-guarded-downcasts.ts": 3555, "5772-guarded-catch-narrows.ts": 2601, "5773-stale-narrowing-inherited.ts": 2486, + "5774-crypto-handle-long-callee.cjs": 1820, "5776-power-of-two-remainder-integer-path.ts": 3263, "5780-readline-history-options.ts": 2909, "5781-readline-history-options.cjs": 3288, @@ -5066,6 +5067,7 @@ "5771-guarded-downcasts.ts": 3443, "5772-guarded-catch-narrows.ts": 3695, "5773-stale-narrowing-inherited.ts": 3758, + "5774-crypto-handle-long-callee.cjs": 2420, "5776-power-of-two-remainder-integer-path.ts": 3584, "5780-readline-history-options.ts": 3008, "5781-readline-history-options.cjs": 5419, From 923952116039f792c23faddeca70560d460291bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:15:01 -0500 Subject: [PATCH 076/120] Answer instanceof Writable for Duplex streams as Node does Node's Writable[Symbol.hasInstance] answers true for any stream with a writable side, so a Duplex, Transform, PassThrough or zlib stream (and user subclasses of Duplex) is an instance of Writable without inheriting from it. `x instanceof Writable` tested only Writable's own interval and answered false for them. It now also tests Duplex's interval for object, union and catch-binding operands, and the static folds count Duplex subclasses. Subclasses of Writable keep the prototype test, as in Node. --- .../src/frontend/lowering/lower-exprs.ts | 130 +++++++++++++----- .../5979-writable-instanceof-duplex.ts.json | 6 + .../corpus/5979-writable-instanceof-duplex.ts | 75 ++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 180 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5979-writable-instanceof-duplex.ts.json create mode 100644 tests/corpus/5979-writable-instanceof-duplex.ts diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d49ce3bb6..5f09342e8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -12810,42 +12810,68 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "'instanceof' right-hand sides other than classes declared in the program", ); } + // `x instanceof Writable`: Node's Writable[Symbol.hasInstance] also + // answers true for streams with a writable side that do not inherit from + // Writable, which are Duplex and its subclasses (Transform, PassThrough, + // the zlib streams). Subclasses of Writable keep the prototype test. + const duplex = + target.def.name === "%Writable" && target.builtinStream !== undefined + ? lowerer.classes.get("%Duplex") + : undefined; + // The classes whose interval test can answer true for a value of static + // class `className`: Writable and Duplex are disjoint, so a value typed + // as one side needs only that side's test. + const runtimeTests = (className: string): ClassInfo[] => { + if (!duplex) return [target]; + const mayHold = (info: ClassInfo): boolean => + className === info.def.name || + lowerer.isSubclassOf(className, info.def.name) || + lowerer.isSubclassOf(info.def.name, className); + const tests = [target, duplex].filter(mayHold); + return tests.length > 0 ? tests : [target]; + }; // A catch binding on the left: the runtime test against the class's // preorder interval (false for non-hierarchy-object payloads). tsc's // narrowing then types the branches; reads bridge through caughtRead. const caughtLocal = lowerer.caughtLocalOf(expr.left); if (caughtLocal) { - if (!lowerer.inHierarchy(target)) { - const value = varRef(caughtLocal.id, CAUGHT, loc); - return { - kind: "ternary", - cond: { - kind: "caughtTest", - value, - test: "instanceof", - className: target.def.name, + const caughtInstanceOf = (info: ClassInfo): IrExpr => { + if (!lowerer.inHierarchy(info)) { + const value = varRef(caughtLocal.id, CAUGHT, loc); + return { + kind: "ternary", + cond: { + kind: "caughtTest", + value, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }, + then: { kind: "boolLit", value: true, type: BOOL, loc }, + else_: classInstanceOf( + lowerer, + { kind: "caughtToDyn", value, type: DYN, loc }, + info, + loc, + ), type: BOOL, loc, - }, - then: { kind: "boolLit", value: true, type: BOOL, loc }, - else_: classInstanceOf( - lowerer, - { kind: "caughtToDyn", value, type: DYN, loc }, - target, - loc, - ), + }; + } + return { + kind: "caughtTest", + value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc }, + test: "instanceof", + className: info.def.name, type: BOOL, loc, }; - } - return { - kind: "caughtTest", - value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc }, - test: "instanceof", - className: target.def.name, - type: BOOL, - loc, }; + const test = caughtInstanceOf(target); + return duplex + ? { kind: "logical", op: "||", left: test, right: caughtInstanceOf(duplex), type: BOOL, loc } + : test; } const left = lowerer.lowerExpr(expr.left); // `u instanceof Error` on an `unknown` value: the checked-dynamic tree's error encoding @@ -12925,23 +12951,33 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const lhsInfo = lowerer.classes.get(arm.className); if (!lhsInfo) throw new InternalCompilerError(`lowerer bug: unknown class ${arm.className}`); - const result: IrExpr = - lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target) + const armTest = (info: ClassInfo): IrExpr => + lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(info) ? { kind: "instanceOf", value: { kind: "unionNarrow", unionId, tag, value, type: arm, loc }, - className: target.def.name, + className: info.def.name, type: BOOL, loc, } : { kind: "boolLit", value: - arm.className === target.def.name || - lowerer.isSubclassOf(arm.className, target.def.name), + arm.className === info.def.name || + lowerer.isSubclassOf(arm.className, info.def.name), type: BOOL, loc, }; + const result = runtimeTests(arm.className) + .map(armTest) + .reduce((left: IrExpr, right: IrExpr): IrExpr => ({ + kind: "logical", + op: "||", + left, + right, + type: BOOL, + loc, + })); body.push({ kind: "if", cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc }, @@ -12987,11 +13023,39 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (!lhsInfo) throw new InternalCompilerError(`lowerer bug: unknown class ${left.type.className}`); if (lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target)) { - return { kind: "instanceOf", value: left, className: target.def.name, type: BOOL, loc }; + const tests = runtimeTests(left.type.className); + if (tests.length === 1) { + return { kind: "instanceOf", value: left, className: tests[0]!.def.name, type: BOOL, loc }; + } + const local = lowerer.declareHiddenLocal("%writableTest", left.type); + const ref = varRef(local.id, left.type, loc); + const test = (className: string): IrExpr => ({ + kind: "instanceOf", + value: ref, + className, + type: BOOL, + loc, + }); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: left, loc }], + result: { + kind: "logical", + op: "||", + left: test(tests[0]!.def.name), + right: test(tests[1]!.def.name), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; } + const leftClass = left.type.className; + const isInstance = (className: string): boolean => + leftClass === className || lowerer.isSubclassOf(leftClass, className); const value = - left.type.className === target.def.name || - lowerer.isSubclassOf(left.type.className, target.def.name); + isInstance(target.def.name) || (duplex !== undefined && isInstance(duplex.def.name)); if (left.kind === "varRef") { return readsClassAfter(lowerer, target, expr.right, [], { kind: "boolLit", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5979-writable-instanceof-duplex.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5979-writable-instanceof-duplex.ts.json new file mode 100644 index 000000000..eca80e7cf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5979-writable-instanceof-duplex.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5979-writable-instanceof-duplex.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5979-writable-instanceof-duplex.ts b/tests/corpus/5979-writable-instanceof-duplex.ts new file mode 100644 index 000000000..f525004ab --- /dev/null +++ b/tests/corpus/5979-writable-instanceof-duplex.ts @@ -0,0 +1,75 @@ +// `x instanceof Writable` answers true for Duplex streams and their +// subclasses, as Node's Writable[Symbol.hasInstance] does. Subclasses of +// Writable keep the prototype test. +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; +import * as zlib from "node:zlib"; +import { EventEmitter } from "node:events"; + +class Sink extends Writable { + override _write(_chunk: Buffer, _encoding: BufferEncoding, callback: () => void): void { + callback(); + } +} +class Pipe extends Duplex { + override _read(): void {} + override _write(_chunk: Buffer, _encoding: BufferEncoding, callback: () => void): void { + callback(); + } +} +class Upper extends Transform { + override _transform( + chunk: Buffer, + _encoding: BufferEncoding, + callback: (error: Error | null, data?: string) => void, + ): void { + callback(null, chunk.toString().toUpperCase()); + } +} + +const writable = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, +}); +const duplex = new Duplex({ + read() {}, + write(_chunk, _encoding, callback) { + callback(); + }, +}); +const pass = new PassThrough(); +const transform = new Transform({ + transform(chunk, _encoding, callback) { + callback(null, chunk); + }, +}); +const readable = new Readable({ read() {} }); + +console.log("writable", writable instanceof Writable, new Sink() instanceof Writable); +console.log( + "duplex family", + duplex instanceof Writable, + pass instanceof Writable, + transform instanceof Writable, + new Pipe() instanceof Writable, + new Upper() instanceof Writable, + zlib.createGzip() instanceof Writable, +); +console.log("readable", readable instanceof Writable, readable instanceof Duplex); +console.log("subclass of writable", pass instanceof Sink, duplex instanceof Sink, new Sink() instanceof Sink); + +const asReadable: Readable = pass; +const asEmitter: EventEmitter = new Pipe(); +const plainEmitter: EventEmitter = new EventEmitter(); +console.log("base-typed", asReadable instanceof Writable, asEmitter instanceof Writable, plainEmitter instanceof Writable); + +function describe(value: Readable | string): string { + return value instanceof Writable ? "writable side" : "no writable side"; +} +console.log("union", describe(duplex), describe(readable), describe("text")); + +try { + throw transform; +} catch (err) { + console.log("caught", err instanceof Writable, err instanceof Readable); +} diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index a6222ef3d..4240b8303 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2195,6 +2195,7 @@ "5976-builtin-class-aliases.ts": 3100, "5977-builtin-class-aliases.cjs": 2950, "5978-builtin-class-alias-modules/main.ts": 2750, + "5979-writable-instanceof-duplex.ts": 2700, "5990-worker-uncloneable.mjs": 3283, "5991-worker-async-dispose.mjs": 5699, "5992-worker-thread-name-lifetime.mjs": 2687, @@ -5245,6 +5246,7 @@ "5976-builtin-class-aliases.ts": 5450, "5977-builtin-class-aliases.cjs": 4300, "5978-builtin-class-alias-modules/main.ts": 4450, + "5979-writable-instanceof-duplex.ts": 6000, "5990-worker-uncloneable.mjs": 22432, "5991-worker-async-dispose.mjs": 25768, "5992-worker-thread-name-lifetime.mjs": 28932, From 1b40157fed505e821eb017a554827e3a4cc6b544 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:25:56 -0500 Subject: [PATCH 077/120] Keep pending match metadata in the array's metadata slot A pending match result now stores its shared match source as a dynamic handle in ScrArrExt.metadata, next to its start index, instead of in a separate field with its own release hook. Array teardown already releases metadata, so scr_arr_ext_free and the publish check keep their previous code, and a static hello world links the same array code as on main. --- packages/runtime/src/scr_array.c | 8 +- packages/runtime/src/scr_array.h | 24 +++--- packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_publish.c | 2 +- packages/runtime/src/scr_regex.c | 126 ++++++++++++++++------------- 5 files changed, 87 insertions(+), 74 deletions(-) diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index fa494d1e5..0c5b4acc2 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -41,7 +41,7 @@ static void scr_arr_oom(void) { * sparse entry, noncanonical property or metadata allocates it. */ static const ScrArrExt scr_arr_no_ext; -SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks = NULL; +SCR_TL bool (*scr_arr_match_copy)(ScrArr *a, ScrDyn *target) = NULL; #define SCR_AX(a) ((const ScrArrExt *)((a)->ext ? (a)->ext : &scr_arr_no_ext)) ScrArrExt *scr_arr_ext(ScrArr *a) { @@ -1070,7 +1070,6 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { * arrays have none. */ static void scr_arr_ext_free(ScrArr *a) { ScrArrExt *x = a->ext; - if (x->match_source) scr_arr_match_hooks->release(x->match_source); scr_arr_sparse_free(x->sparse); for (size_t i = 0; i < x->prop_len; i++) free(x->props[i].key); free(x->props); @@ -4426,7 +4425,10 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { } void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { - if (SCR_AX(a)->match_source && scr_arr_match_hooks->copy_metadata(a, target)) return; + const ScrDyn *metadata = SCR_AX(a)->metadata; + if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE && + scr_arr_match_copy(a, target)) + return; if (!SCR_AX(a)->metadata) return; for (size_t i = 0; i < SCR_AX(a)->metadata->v.obj.len; i++) { ScrDynEntry *entry = &SCR_AX(a)->metadata->v.obj.entries[i]; diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 616aa4c75..5289f8c13 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -105,11 +105,11 @@ typedef struct ScrArrExt { struct ScrArrProp *props; /* noncanonical numeric properties */ size_t prop_len; size_t prop_cap; - struct ScrDyn *metadata; /* heterogeneous named properties of match results */ - /* A match result's metadata before its first use: the match's shared - * subject and regex (scr_regex.c builds `metadata` from them) and its - * UTF-16 start index. */ - struct ScrRegexMatchSource *match_source; + /* Heterogeneous named properties of match results. Until a result's + * metadata is first needed, this is the match source handle that the + * results of one exec or matchAll call share (SCR_DYNH_MATCH_SOURCE, + * scr_regex.c), and match_index is the result's UTF-16 start index. */ + struct ScrDyn *metadata; double match_index; } ScrArrExt; /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read @@ -212,15 +212,11 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { * the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); -/* The match-source operations, installed by scr_regex.c when it records a - * pending match source (ScrArrExt.match_source); this unit links without - * the regex runtime. copy_metadata writes the pending metadata into the - * dynamic copy or materializes it, answering whether it wrote. */ -typedef struct ScrArrMatchHooks { - void (*release)(struct ScrRegexMatchSource *src); - bool (*copy_metadata)(ScrArr *a, struct ScrDyn *target); -} ScrArrMatchHooks; -extern SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks; +/* Installed by scr_regex.c when it records a pending match source; this + * unit links without the regex runtime. Writes the pending metadata of + * `a` into its dynamic copy or materializes it, answering whether it + * wrote. */ +extern SCR_TL bool (*scr_arr_match_copy)(ScrArr *a, struct ScrDyn *target); /* The array's out-of-line storage, allocated (zeroed) on first use. */ ScrArrExt *scr_arr_ext(ScrArr *a); /* Moves inline dense slots (kept in the header block of a small array) to diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 8ebe11265..3e9adbf88 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -171,6 +171,7 @@ typedef enum { SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */ SCR_DYNH_MESSAGE_EVENT, /* ScrEvent with data — BroadcastChannel's 'message' */ SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */ + SCR_DYNH_MATCH_SOURCE, /* a match result's pending metadata (scr_regex.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index a974e82ff..9dedc6844 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -188,7 +188,7 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; - if (a->ext && (a->ext->props || a->ext->metadata || a->ext->match_source)) { + if (a->ext && (a->ext->props || a->ext->metadata)) { scr_pub_refuse(ctx, "Cannot publish an array with non-index properties"); return; } diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 40e843cac..97c924d6d 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -612,11 +612,13 @@ static ScrStr *scr_regex_span(const ScrRegexInput *input, int start, int end) { /* ── match metadata ───────────────────────────────────────────────────── */ -/* Results of one exec or matchAll call share their subject and regex. - * Each result records the source and its start index; the metadata - * object (index, input, groups) is built when a result is first - * converted to a dynamic value, so results read only through their - * elements never allocate it. */ +/* Results of one exec or matchAll call share their subject and regex + * through one match source handle, which each result's metadata slot + * holds with its start index until the metadata object (index, input, + * groups) is first needed: when the result is converted to a dynamic + * value or its groups are read. Results read only through their elements + * never allocate it, and releasing a result releases the handle with the + * rest of its metadata. */ typedef struct ScrRegexMatchSource { size_t rc; ScrStr *input; @@ -624,57 +626,59 @@ typedef struct ScrRegexMatchSource { ScrDyn *input_value; /* `input` as a dynamic value, built on first use */ } ScrRegexMatchSource; -static void scr_regex_materialize_metadata(ScrArr *a); static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target); -static ScrRegexMatchSource *scr_regex_source_new(ScrStr *s, ScrRegex *re) { - ScrRegexMatchSource *src = malloc(sizeof *src); - if (!src) scr_regex_oom(); - src->rc = 1; - src->input = scr_str_retain(s); - src->re = scr_regex_retain(re); - src->input_value = NULL; - return src; +static void *scr_regex_source_retain_v(void *p) { + ((ScrRegexMatchSource *)p)->rc++; + return p; } -static void scr_regex_source_release(ScrRegexMatchSource *src) { - if (!src || --src->rc != 0) return; +static void scr_regex_source_release_v(void *p) { + ScrRegexMatchSource *src = p; + if (--src->rc != 0) return; scr_str_release(src->input); scr_regex_release(src->re); scr_dyn_release(src->input_value); free(src); } -/* Records the match at UTF-16 `index` of `src` as the row's pending - * metadata. */ -static void scr_regex_defer_metadata(ScrArr *row, ScrRegexMatchSource *src, int index) { - static const ScrArrMatchHooks hooks = { scr_regex_source_release, scr_regex_copy_pending_metadata }; - scr_arr_match_hooks = &hooks; +/* The +1 match source handle of one exec or matchAll call. */ +static ScrDyn *scr_regex_source_new(ScrStr *s, ScrRegex *re) { + static const ScrDynHandleOps ops = { + "MatchSource", &scr_regex_source_retain_v, &scr_regex_source_release_v, NULL, NULL, NULL, NULL, + }; + scr_dyn_handle_install(SCR_DYNH_MATCH_SOURCE, &ops); + scr_arr_match_copy = &scr_regex_copy_pending_metadata; + ScrRegexMatchSource *src = malloc(sizeof *src); + if (!src) scr_regex_oom(); + src->rc = 0; + src->input = scr_str_retain(s); + src->re = scr_regex_retain(re); + src->input_value = NULL; + return scr_dyn_new_handle(src, SCR_DYNH_MATCH_SOURCE); +} + +/* Records the match at UTF-16 `index` as the row's pending metadata. */ +static void scr_regex_defer_metadata(ScrArr *row, ScrDyn *source, int index) { ScrArrExt *x = scr_arr_ext(row); - src->rc++; - x->match_source = src; + x->metadata = scr_dyn_retain(source); x->match_index = (double)index; } +/* The pending match source of a result, or NULL once its metadata exists. */ +static ScrRegexMatchSource *scr_regex_pending(const ScrArr *a) { + const ScrDyn *metadata = a->ext ? a->ext->metadata : NULL; + if (!metadata || metadata->kind != SCR_DYN_HANDLE || metadata->v.handle.tag != SCR_DYNH_MATCH_SOURCE) + return NULL; + return metadata->v.handle.ptr; +} + /* The subject as a dynamic value, shared by the source's results. +1. */ static ScrDyn *scr_regex_source_input(ScrRegexMatchSource *src) { if (!src->input_value) src->input_value = scr_dyn_new_str(src->input); return scr_dyn_retain(src->input_value); } -ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key) { - ScrArrExt *x = a->ext; - if (x && x->match_source && !x->metadata) { - if (key->len == 5 && memcmp(key->data, "index", 5) == 0) - return scr_dyn_new_num(x->match_index); - if (key->len == 5 && memcmp(key->data, "input", 5) == 0) - return scr_regex_source_input(x->match_source); - scr_regex_materialize_metadata(a); - } - ScrDyn *value = x && x->metadata ? scr_dyn_obj_get(x->metadata, key->data, key->len) : NULL; - return scr_dyn_retain(value ? value : scr_dyn_undefined()); -} - /* groups is a null-prototype object mapping each group name to its first * participating capture (the row's own string, shared) or undefined, and * undefined itself for a pattern without named groups. The row holds each @@ -700,34 +704,44 @@ static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { } static void scr_regex_materialize_metadata(ScrArr *a) { - ScrArrExt *x = scr_arr_ext(a); - ScrRegexMatchSource *src = x->match_source; + ScrRegexMatchSource *src = scr_regex_pending(a); if (!src) return; - x->match_source = NULL; - if (!x->metadata) { - ScrDyn *props = scr_dyn_new_obj(); - x->metadata = props; - scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num(x->match_index)); - scr_dyn_obj_set(props, "input", 5, scr_regex_source_input(src)); - scr_dyn_obj_set(props, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); - } - scr_regex_source_release(src); + ScrArrExt *x = a->ext; + ScrDyn *source = x->metadata; + ScrDyn *props = scr_dyn_new_obj(); + scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_obj_set(props, "input", 5, scr_regex_source_input(src)); + scr_dyn_obj_set(props, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); + x->metadata = props; + scr_dyn_release(source); } static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target) { - ScrArrExt *x = scr_arr_ext(a); - ScrRegexMatchSource *src = x->match_source; + ScrRegexMatchSource *src = scr_regex_pending(a); if (target->kind != SCR_DYN_ARR || target->v.arr.properties || target->non_extensible || target->v.arr.frozen) { scr_regex_materialize_metadata(a); return false; } - scr_dyn_arr_named_put(target, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_arr_named_put(target, "index", 5, scr_dyn_new_num(a->ext->match_index)); scr_dyn_arr_named_put(target, "input", 5, scr_regex_source_input(src)); scr_dyn_arr_named_put(target, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); return true; } +ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key) { + ScrRegexMatchSource *src = scr_regex_pending(a); + if (src) { + if (key->len == 5 && memcmp(key->data, "index", 5) == 0) + return scr_dyn_new_num(a->ext->match_index); + if (key->len == 5 && memcmp(key->data, "input", 5) == 0) return scr_regex_source_input(src); + scr_regex_materialize_metadata(a); + } + ScrDyn *metadata = a->ext ? a->ext->metadata : NULL; + ScrDyn *value = metadata ? scr_dyn_obj_get(metadata, key->data, key->len) : NULL; + return scr_dyn_retain(value ? value : scr_dyn_undefined()); +} + /* ECMA-262 AdvanceStringIndex: past a whole surrogate pair under /u, one * code unit otherwise. ASCII cannot contain a surrogate pair. */ static int scr_advance(const ScrRegexInput *input, int i, bool unicode) { @@ -863,9 +877,9 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool out->filled = (uint32_t)filled; out->len = n; if (metadata) { - ScrRegexMatchSource *src = scr_regex_source_new(s, re); - scr_regex_defer_metadata(out, src, scr_capture_index(&input, capture[0])); - scr_regex_source_release(src); + ScrDyn *source = scr_regex_source_new(s, re); + scr_regex_defer_metadata(out, source, scr_capture_index(&input, capture[0])); + scr_dyn_release(source); } } scr_regex_input_dispose(&input); @@ -1015,7 +1029,7 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices } bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; int capture_count = lre_get_capture_count(bc); - ScrRegexMatchSource *src = metadata ? scr_regex_source_new(s, re) : NULL; + ScrDyn *source = metadata ? scr_regex_source_new(s, re) : NULL; ScrRegexInput input; scr_regex_input_init(&input, re, bc, s); int len = input.len; @@ -1048,12 +1062,12 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices while (filled < (size_t)capture_count && present[filled] == SCR_ARR_VALUE) filled++; row->filled = (uint32_t)filled; row->len = (size_t)capture_count; - if (src) scr_regex_defer_metadata(row, src, start); + if (source) scr_regex_defer_metadata(row, source, start); scr_arr_push_ref(out, row); if (indices != NULL) scr_arr_push_f64(indices, (double)start); pos = end == start ? scr_advance(&input, end, unicode) : end; } - scr_regex_source_release(src); + scr_dyn_release(source); scr_regex_input_dispose(&input); return out; /* +1 (possibly empty — Node's no-match drain is []) */ } From 8d94e5d99396a2a1fac9e949708a9aee56b2a12e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:34:09 -0500 Subject: [PATCH 078/120] Refuse RegExp and Promise class fields in JSON.stringify A Date field reads through the class's checked-dynamic property view and writes its toJSON text, so json-class-fields.ts compiled and matched Node while the diagnostics test expected a refusal. The Date cases move to 6000-json-stringify-class-instances.ts. RegExp and Promise fields read through the view but the runtime JSON traversal has no writer for them: a RegExp field threw at run time where Node writes {}, and a Promise field failed to link. Both are now refused at compile time naming the field, also inside a field's arrays, records and unions, and the fixture covers them. --- .../language-features/class-reflection.mdx | 2 +- .../src/frontend/lowering/builtins/json.ts | 50 ++++++++++++++++--- .../6000-json-stringify-class-instances.ts | 12 +++++ tests/diagnostics/json-class-fields.ts | 25 ++++++---- .../__snapshots__/json-class-fields.ts.txt | 27 ++++++---- 5 files changed, 90 insertions(+), 26 deletions(-) diff --git a/docs/content/limitations/language-features/class-reflection.mdx b/docs/content/limitations/language-features/class-reflection.mdx index 8b0738cc7..f2b6dc2c8 100644 --- a/docs/content/limitations/language-features/class-reflection.mdx +++ b/docs/content/limitations/language-features/class-reflection.mdx @@ -1 +1 @@ -- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. `JSON.stringify` writes Map and Set fields as `{}`, as Node.js does, and refuses classes with Date fields with `SC1090`. `Object.keys` raises an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values. +- **Class reflection:** `JSON.stringify` and `Object.keys` require every declared field to have a representation that can pass through runtime-checked storage. `JSON.stringify` writes Date fields as their ISO text and Map and Set fields as `{}`, as Node.js does, and refuses classes with RegExp or Promise fields with `SC1090`. `Object.keys` raises an error for classes with opaque fields such as native Maps. Class fields, inherited accessors, and added properties retain identity through untyped values. diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index c99a99c75..013b16e7c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -168,7 +168,7 @@ export function lowerJsonMethodCall( argNode, `JSON.stringify of '${lowerer.fmt(value.type)}' values (field '${unreadable.field}' of class ` + `'${unreadable.className}' has type '${lowerer.fmt(unreadable.type)}', which the runtime JSON ` + - `traversal cannot read)`, + `traversal cannot ${unreadable.reason})`, ); } // Keep the original containers live while the runtime visits them: @@ -290,15 +290,47 @@ function lowerOptionalStringifyRoot( return { kind: "call", callee: helper, args: [value], type: resultT, loc }; } +/** True when a class field type holds a RegExp or a Promise outside a + * nested class instance (whose own fields are checked separately): Node + * writes both as {}, and the runtime JSON traversal has no writer for them. */ +function holdsUnwritable(lowerer: Lowerer, t: IrType, visiting: Set): boolean { + switch (t.kind) { + case "regex": + case "promise": + return true; + case "array": + return holdsUnwritable(lowerer, t.elem, visiting); + case "record": { + if (visiting.has(t.shapeId)) return false; + visiting.add(t.shapeId); + const shape = lowerer.shapes.get(t.shapeId); + return ( + (shape?.fields ?? []).some((field) => holdsUnwritable(lowerer, field.type, visiting)) || + (shape?.indexValue !== undefined && holdsUnwritable(lowerer, shape.indexValue, visiting)) + ); + } + case "union": { + if (visiting.has(t.unionId)) return false; + visiting.add(t.unionId); + return (lowerer.unions.get(t.unionId)?.arms ?? []).some((arm) => + holdsUnwritable(lowerer, arm, visiting), + ); + } + default: + return false; + } +} + /** The first class field reachable from a stringify input that the class's - * checked-dynamic property view cannot represent. The runtime JSON traversal + * checked-dynamic property view cannot represent, or that holds a RegExp or + * a Promise, which the runtime JSON traversal cannot write. The traversal * reads class instances through that view, so such a class is refused at - * compile time instead of throwing when the view is built. */ + * compile time instead of throwing when the view is built or written. */ function classFieldWithoutJsonView( lowerer: Lowerer, t: IrType, visiting: Set, -): { className: string; field: string; type: IrType } | null { +): { className: string; field: string; type: IrType; reason: "read" | "write" } | null { const getRecord = (id: string) => lowerer.shapes.get(id); const getUnion = (id: string) => lowerer.unions.get(id); switch (t.kind) { @@ -331,10 +363,16 @@ function classFieldWithoutJsonView( const info = lowerer.classes.get(t.className); for (const field of info?.def.fields ?? []) { if (!isClassOwnEnumerableFieldName(field.name)) continue; - if (!classDynViewSupported([field], getRecord, getUnion)) { + const unreadable = !classDynViewSupported([field], getRecord, getUnion); + if (unreadable || holdsUnwritable(lowerer, field.type, new Set())) { const className = info?.def.jsName ?? info?.decl?.name?.text ?? t.className.replace(/^%/, ""); - return { className, field: field.name, type: field.type }; + return { + className, + field: field.name, + type: field.type, + reason: unreadable ? "read" : "write", + }; } const found = classFieldWithoutJsonView(lowerer, field.type, visiting); if (found) return found; diff --git a/tests/corpus/6000-json-stringify-class-instances.ts b/tests/corpus/6000-json-stringify-class-instances.ts index a73168b40..cc72a0b82 100644 --- a/tests/corpus/6000-json-stringify-class-instances.ts +++ b/tests/corpus/6000-json-stringify-class-instances.ts @@ -129,3 +129,15 @@ h.payload = 7; console.log(JSON.stringify({ handlers: [h] })); const u: unknown = new Map([["a", 1]]); console.log(JSON.stringify(new Tagged()), JSON.stringify(u)); + +// Date fields write their toJSON text, also in nested instances. +class Schedule { + name = "weekly"; + next = new Date(Date.UTC(2024, 0, 1)); +} +class Calendar { + title = "team"; + schedules: Schedule[] = [new Schedule()]; +} +console.log(JSON.stringify(new Schedule())); +console.log(JSON.stringify({ calendar: new Calendar() })); diff --git a/tests/diagnostics/json-class-fields.ts b/tests/diagnostics/json-class-fields.ts index 81012c161..d4d3f8e73 100644 --- a/tests/diagnostics/json-class-fields.ts +++ b/tests/diagnostics/json-class-fields.ts @@ -1,15 +1,22 @@ // JSON.stringify reads class instances through their checked-dynamic -// property view. A class whose declared field has no representation in -// that view is refused at compile time, naming the field. -class Schedule { - name = "weekly"; - next = new Date(Date.UTC(2024, 0, 1)); +// property view. A class whose declared field holds a RegExp or a Promise, +// which the runtime JSON traversal cannot write, is refused at compile +// time, naming the field. +class Pattern { + name = "digits"; + source = /\d+/g; } -class Calendar { +class Job { + title = "fetch"; + pending = Promise.resolve(1); +} + +class Catalog { title = "team"; - schedules: Schedule[] = [new Schedule()]; + patterns: Pattern[] = [new Pattern()]; } -console.log(JSON.stringify(new Schedule())); -console.log(JSON.stringify({ calendar: new Calendar() })); +console.log(JSON.stringify(new Pattern())); +console.log(JSON.stringify(new Job())); +console.log(JSON.stringify({ catalog: new Catalog() })); diff --git a/tests/harness/__snapshots__/json-class-fields.ts.txt b/tests/harness/__snapshots__/json-class-fields.ts.txt index 568cfa47f..5e6c30fdf 100644 --- a/tests/harness/__snapshots__/json-class-fields.ts.txt +++ b/tests/harness/__snapshots__/json-class-fields.ts.txt @@ -1,13 +1,20 @@ -json-class-fields.ts:14:28 - error SC1090: JSON.stringify of 'Schedule' values (field 'next' of class 'Schedule' has type 'Date', which the runtime JSON traversal cannot read) are not supported yet +json-class-fields.ts:20:28 - error SC1090: JSON.stringify of 'Pattern' values (field 'source' of class 'Pattern' has type 'RegExp', which the runtime JSON traversal cannot write) are not supported yet - 13 | - 14 | console.log(JSON.stringify(new Schedule())); - | ^~~~~~~~~~~~~~ - 15 | console.log(JSON.stringify({ calendar: new Calendar() })); + 19 | + 20 | console.log(JSON.stringify(new Pattern())); + | ^~~~~~~~~~~~~ + 21 | console.log(JSON.stringify(new Job())); -json-class-fields.ts:15:28 - error SC1090: JSON.stringify of '{ calendar: Calendar }' values (field 'next' of class 'Schedule' has type 'Date', which the runtime JSON traversal cannot read) are not supported yet +json-class-fields.ts:21:28 - error SC1090: JSON.stringify of 'Job' values (field 'pending' of class 'Job' has type 'Promise', which the runtime JSON traversal cannot write) are not supported yet - 14 | console.log(JSON.stringify(new Schedule())); - 15 | console.log(JSON.stringify({ calendar: new Calendar() })); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 16 | \ No newline at end of file + 20 | console.log(JSON.stringify(new Pattern())); + 21 | console.log(JSON.stringify(new Job())); + | ^~~~~~~~~ + 22 | console.log(JSON.stringify({ catalog: new Catalog() })); + +json-class-fields.ts:22:28 - error SC1090: JSON.stringify of '{ catalog: Catalog }' values (field 'source' of class 'Pattern' has type 'RegExp', which the runtime JSON traversal cannot write) are not supported yet + + 21 | console.log(JSON.stringify(new Job())); + 22 | console.log(JSON.stringify({ catalog: new Catalog() })); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ + 23 | \ No newline at end of file From 550e00549c1e3bbec8127adf086a4ad89bd71236 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:40:52 -0500 Subject: [PATCH 079/120] Move callSiteReturnType into ir.ts for devirtualization Lowering may not import ir/validate.ts, and devirtualize.ts imported callSiteReturnType from it. The function only reads an IrFunction's types, so it moves next to typeEquals in ir.ts, which both the validator and lowering import. The Date formatter fence's hint now lists toUTCString(), which this branch lowers, so the date-formatters and stdlib-fence snapshots record the new hint. toString() and toLocaleString() still have no lowering, and the fixtures keep them. --- .../src/frontend/lowering/devirtualize.ts | 9 ++++-- packages/compiler/src/ir/ir.ts | 27 +++++++++++++++++ packages/compiler/src/ir/validate.ts | 29 +------------------ .../__snapshots__/date-formatters.ts.txt | 4 +-- .../harness/__snapshots__/stdlib-fence.ts.txt | 2 +- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index 04a53aeb1..b71080a3f 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -1,7 +1,12 @@ import * as ts from "../ts7/adapter.js"; -import { type IrExpr, type IrFunction, type IrType, typeEquals } from "../../ir/ir.js"; +import { + type IrExpr, + type IrFunction, + type IrType, + callSiteReturnType, + typeEquals, +} from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; -import { callSiteReturnType } from "../../ir/validate.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..12b02d139 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -857,6 +857,33 @@ export function typeKey(t: IrType): string { } } +/** The type a CALL SITE (or function value) receives from a module + * function: an async body's returnType is the promise's INNER type, so + * calls and closures over it see Promise; a generator body's is the + * TReturn channel, so call sites see the generator type. The one place + * the body/call-site split is spelled out, for the validator and lowering. */ +export function callSiteReturnType(fn: IrFunction): IrType { + if (fn.async && fn.generator !== undefined) { + return { + kind: "generator", + async: true, + yieldT: fn.generator.yieldT, + retT: fn.returnType, + nextT: fn.generator.nextT, + }; + } + if (fn.async) return { kind: "promise", inner: fn.returnType }; + if (fn.generator !== undefined) { + return { + kind: "generator", + yieldT: fn.generator.yieldT, + retT: fn.returnType, + nextT: fn.generator.nextT, + }; + } + return fn.returnType; +} + export function typeEquals(a: IrType, b: IrType): boolean { // Dispatch once: a chain of negative narrowing checks repeatedly retags // the remaining variants when this comparator runs in the native compiler. diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 979b70115..2526dafa1 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -56,7 +56,7 @@ import { unionContainerArmsOk, VOID, } from "./ir.js"; -import { DYN_CLASS_PROPERTIES } from "./ir.js"; +import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js"; import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; @@ -85,33 +85,6 @@ export interface IrValidationError { loc: SrcLoc; } -/** The type a CALL SITE (or function value) receives from a module - * function: an async body's returnType is the promise's INNER type, so - * calls and closures over it see Promise; a generator body's is the - * TReturn channel, so call sites see the generator type. The one place - * the body/call-site split is spelled out in the validator. */ -export function callSiteReturnType(fn: IrFunction): IrType { - if (fn.async && fn.generator !== undefined) { - return { - kind: "generator", - async: true, - yieldT: fn.generator.yieldT, - retT: fn.returnType, - nextT: fn.generator.nextT, - }; - } - if (fn.async) return { kind: "promise", inner: fn.returnType }; - if (fn.generator !== undefined) { - return { - kind: "generator", - yieldT: fn.generator.yieldT, - retT: fn.returnType, - nextT: fn.generator.nextT, - }; - } - return fn.returnType; -} - interface VirtualCallInfo { declared: boolean; hasOverride: boolean; diff --git a/tests/harness/__snapshots__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 66d98d861..90928dae7 100644 --- a/tests/harness/__snapshots__/date-formatters.ts.txt +++ b/tests/harness/__snapshots__/date-formatters.ts.txt @@ -5,7 +5,7 @@ date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the | ^~~~~~~~~~~~~~~ 5 | date.toLocaleString(); - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet @@ -14,4 +14,4 @@ date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part o | ^~~~~~~~~~~~~~~~~~~~~ 6 | - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 337354eec..95fbe1fff 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet From 3de2fb56a06fea1f28c37fbc53a134af4e347a42 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 19:28:55 -0500 Subject: [PATCH 080/120] Cancel running subtests when their parent test times out node:test changes, matching Node 24.22: - When a test's { timeout } elapses, its subtests that are still running are cancelled, the innermost first. Each reports its duration so far and fails with "test did not finish before its parent and was cancelled", before the parent's result. - A cancelled subtest's body keeps running unobserved: when it settles it reports nothing and leaves the runner's state alone. Before, it reported its result after the run's summary. The timeout-subtests fixture compares the results, counts and failing section with Node, plain and with SCRIPTC_SAN=1. --- .../limitations/builtin-apis/test-runner.mdx | 1 + packages/runtime/src/scr_test.c | 28 +++++++++++++++++++ .../node-test/cases/timeout-subtests.ts | 23 +++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 tests/fixtures/node-test/cases/timeout-subtests.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 6583a2873..77d3d9a84 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -3,3 +3,4 @@ - **TestContext values:** `t.filePath` is the entry file's path when the program was compiled. `t.attempt` is `0`, since a test runs once. `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks. `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node. `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is. - **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`. - **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named ``. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged. +- **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index cc52513a6..4217169b8 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -125,6 +125,7 @@ struct ScrTestCtx { struct ScrMockTimers *mock_timers; /* t.mock.timers, made on first use; freed at exit */ bool passed; /* t.passed: set when the body passes, as Node's pass() does */ bool listed; /* a suite listed in the failing section (a failing root before) */ + double start_ms; /* when the test started running; NaN before */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -230,6 +231,7 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode, t->plan = -1; t->timeout = INFINITY; t->concurrency = 1; + t->start_ms = NAN; #ifdef SCR_RC_AUDIT scr_testctx_live++; #endif @@ -597,6 +599,23 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) { scr_test_report_result(c); } +/* A test that timed out (Node): its subtests that are still running are + * cancelled, innermost first, with their durations so far. Their bodies + * keep running unobserved and report nothing when they settle. */ +static void scr_test_cancel_running(ScrTestCtx *t) { + for (size_t i = 0; i < t->nchildren; i++) { + ScrTestCtx *c = t->children[i]; + if (c->is_suite || c->state != SCR_TS_PENDING || isnan(c->start_ms)) continue; + scr_test_cancel_running(c); + static const char msg[] = "test did not finish before its parent and was cancelled"; + scr_throw_str(scr_str_new(msg, sizeof msg - 1)); + c->err = scr_exc_take(); + c->dur_ms = scr_now_ms() - c->start_ms; + c->state = SCR_TS_CANCELLED; + scr_test_report_result(c); + } +} + static void scr_test_fail_unrun(ScrTestCtx *c, ScrCaught *err) { double start = scr_now_ms(); c->err = scr_caught_retain(err); @@ -689,6 +708,7 @@ static void scr_test_run_node(ScrTestCtx *t) { * afterEach chain. */ ScrTestCtx *prev_parent = scr_test_parent_get(); scr_test_parent_set(t); + t->start_ms = start; if (t->concurrency > 1) scr_test_defer++; bool hooked = scr_test_run_each_hooks(t, SCR_HOOK_BEFORE_EACH, true); /* A concurrent subtest resumes after its beforeEach hooks as many turns @@ -703,10 +723,18 @@ static void scr_test_run_node(ScrTestCtx *t) { failed = true; } else if (t->cb) { scr_test_call(t->cb, t->flags, t, t); + if (t->state != SCR_TS_PENDING) { + /* Its parent timed out and cancelled it while the body ran: the + * result is reported, and the runner has moved on. */ + if (scr_exc_pending()) scr_exc_clear(); + if (t->concurrency > 1) scr_test_defer--; + return; + } if (scr_exc_pending()) { t->err = scr_exc_take(); failed = true; } + if (failed && t->timed_out) scr_test_cancel_running(t); } /* Children a passing body registered without running (bare test() in * the body): run them now, before the parent completes — Node holds diff --git a/tests/fixtures/node-test/cases/timeout-subtests.ts b/tests/fixtures/node-test/cases/timeout-subtests.ts new file mode 100644 index 000000000..91a5417e7 --- /dev/null +++ b/tests/fixtures/node-test/cases/timeout-subtests.ts @@ -0,0 +1,23 @@ +// @exit: 1 +// A test that times out cancels its subtests that are still running, the +// innermost first, with "test did not finish before its parent and was +// cancelled". Subtests that finished keep their results, and the +// cancelled bodies report nothing when they settle later. +import { test } from "node:test"; + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +test("parent", { timeout: 40 }, async (t) => { + await t.test("finished first", () => {}); + await t.test("running", async (t) => { + await t.test("inner done", () => {}); + await t.test("inner running", async () => { + await sleep(300); + throw new Error("never reported"); + }); + }); +}); + +test("next", async () => { + await sleep(400); +}); From 6a2d43b462f8171f888c91f247981a919709d496 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 17:16:44 -0700 Subject: [PATCH 081/120] Keep declaration-file rest callbacks on their existing paths Library callback slots such as String#replace's (substring, ...args: any[]) have intrinsic lowerings that inspect the unmapped function type, so only rest parameters declared in program source take the checked-dynamic variadic ABI. Found by the zero-cost gate on corpus 6165 after merging main. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/type-mapper.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 236b2a6c0..1aa5b1d1b 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4317,11 +4317,16 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if (!armed) return null; pt = armed; } - // `...args: unknown[]` / `...args: any[]`: the checked-dynamic - // variadic ABI (no typed slot; the callee's hidden trailing dyn - // array receives every surplus argument, as for JS rest params). + // `...args: unknown[]` / `...args: any[]` declared in program source: + // the checked-dynamic variadic ABI (no typed slot; the callee's hidden + // trailing dyn array receives every surplus argument, as for JS rest + // params). Declaration files keep their existing paths: library + // callback slots such as String#replace's `(substring, ...args: any[])` + // have intrinsic lowerings that inspect the unmapped type. if ( rest && + decl !== undefined && + !decl.getSourceFile().isDeclarationFile && (pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker))) ) { dynRest = true; From ec79cf27897af17e5ed035b9bee9c17ef6a0be9b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 19:42:27 -0500 Subject: [PATCH 082/120] Run iterator helpers on untyped iterators In JavaScript, a receiver without an inferred type, such as an untyped parameter's xs.values(), is the runtime's checked iterator. Its only method was next(), so xs.values().filter(f), .map(f) and the other iterator helpers threw "TypeError: ... is not a function" where Node runs them, and [].values().reduce(f) threw the same instead of Node's "Reduce of a done iterator with no initial value". The native array, string, Map and Set iterators now answer the Iterator.prototype methods. map, filter, take, drop and flatMap make helper objects (SCR_DYNH_ITERATOR_HELPER) that pull lazily and support next(), return(), for...of and spreads, print as "Object [Iterator Helper] {}" and tag as "[object Iterator Helper]". toArray, forEach, reduce, some, every and find drive the iterator. The helpers close the iterator they were made from where Node does: when a callback throws, when take's budget is spent, on return(), and before the error for an invalid argument. flatMap closes its inner iterator first. An error from a close that runs because of another error is dropped. Invalid arguments throw Node's messages, such as "number 5 is not a function" and "x must be positive". --- .../6070-iterator-helpers-untyped.js.json | 6 + packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_json.c | 691 +++++++++++++++++- tests/corpus/6070-iterator-helpers-untyped.js | 89 +++ tests/harness/corpus-costs.json | 2 + 5 files changed, 786 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6070-iterator-helpers-untyped.js.json create mode 100644 tests/corpus/6070-iterator-helpers-untyped.js diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6070-iterator-helpers-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6070-iterator-helpers-untyped.js.json new file mode 100644 index 000000000..41ecbaab7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6070-iterator-helpers-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6070-iterator-helpers-untyped.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index ca21a0685..bccd8d55c 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -173,6 +173,7 @@ typedef enum { SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */ SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */ SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */ + SCR_DYNH_ITERATOR_HELPER, /* an iterator helper object over a checked iterator (scr_json.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 0567d0b15..401933904 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -75,7 +75,7 @@ static bool scr_builtin_iterable(const ScrDyn *value) { return value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES || (scr_dyn_generator(value) && value->v.typed_ref.type_key[0] != 'a') || (value->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(value)) || - (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_ITERATOR || value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET || + (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_ITERATOR || value->v.handle.tag == SCR_DYNH_ITERATOR_HELPER || value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET || scr_dyn_handle_ops_of(value)->iter_pack || scr_dyn_handle_ops_of(value)->iter_step)); } static bool scr_iterator_object(const ScrDyn *value) { @@ -4657,8 +4657,16 @@ bool scr_dyn_iterator_step_done(const ScrDyn *value) { return ((ScrNativeIterator *)value->v.handle.ptr)->step_done; } +static int scr_ih_method_index(const char *name, size_t len); +static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc); +static ScrDyn *scr_ih_method_function(int index); + static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) { - if (len != 4 || memcmp(key, "next", 4)) return NULL; + if (len != 4 || memcmp(key, "next", 4)) { + /* The Iterator.prototype helper methods. */ + int index = scr_ih_method_index(key, len); + return index >= 0 ? scr_ih_method_function(index) : NULL; + } unsigned kind = ((ScrNativeIterator *)ptr)->kind; if (kind == 4) return scr_iterator_function(10, &scr_native_handle_iterator_next_call, "next"); return kind == 3 ? scr_iterator_function(8, &scr_native_set_iterator_next_call, "next") @@ -4669,8 +4677,9 @@ static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) { static ScrDyn *scr_native_iterator_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - (void)self; if (!strcmp(method, "next")) return scr_native_iterator_next(ptr); + int index = scr_ih_method_index(method, strlen(method)); + if (index >= 0) return scr_iterator_helper_method(self, index, args, argc); ScrDyn *missing = scr_dyn_undefined(); return scr_dyn_call(missing, args, argc, what); } @@ -4700,6 +4709,682 @@ static ScrDyn *scr_native_collection_iterator(ScrDyn *source, unsigned selection return result; } +/* ── iterator helpers on checked iterators (ES2025) ───────────────────────── + * The Iterator.prototype methods over a checked iterator object: a native + * array, string, Map or Set iterator, or a helper object made here. Typed + * code lowers its helpers statically; these answer untyped JavaScript + * receivers. The stages (map, filter, take, drop, flatMap) make helper + * objects that pull lazily; the consuming methods (toArray, forEach, reduce, + * some, every, find) drive the iterator to completion or to their answer. + * Closing follows IteratorClose: a helper closes the iterator it was made + * from when a callback throws, when its budget is spent, and on return(); + * an error from a close that runs because of another error is dropped. */ + +static ScrDyn *scr_iterator_call(const ScrDyn *method, const ScrDyn *receiver, const char *name); +static ScrDyn *scr_iterator_method(const ScrDyn *source); + +enum { SCR_IH_MAP, SCR_IH_FILTER, SCR_IH_TAKE, SCR_IH_DROP, SCR_IH_FLATMAP }; +enum { SCR_IH_START, SCR_IH_YIELDED, SCR_IH_RUNNING, SCR_IH_DONE }; + +typedef struct { + size_t rc; + unsigned kind; + unsigned state; + ScrDyn *iterated; /* the iterator the helper was made from */ + ScrDyn *next; /* its next method, read when the helper was made */ + ScrDyn *fn; /* the map, filter or flatMap callback */ + double remaining; /* the take or drop budget */ + double counter; + ScrDyn *inner; /* flatMap's current inner iterator */ + ScrDyn *inner_next; +} ScrIterHelper; + +static const char *const scr_ih_method_names[] = { + "map", "filter", "take", "drop", "flatMap", "reduce", "toArray", "forEach", "some", "every", "find", +}; +#define SCR_IH_METHOD_COUNT (sizeof scr_ih_method_names / sizeof scr_ih_method_names[0]) + +/* The index of an Iterator.prototype helper method name, or -1. */ +static int scr_ih_method_index(const char *name, size_t len) { + for (size_t i = 0; i < SCR_IH_METHOD_COUNT; i++) { + if (strlen(scr_ih_method_names[i]) == len && !memcmp(scr_ih_method_names[i], name, len)) return (int)i; + } + return -1; +} + +/* V8's description of a value in " is not a function": the type, + * with the value for primitives that print one. */ +static void scr_ih_describe_type(ScrJsonBuf *b, const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_UNDEF: scr_jb_puts(b, "undefined"); return; + case SCR_DYN_NULL: scr_jb_puts(b, "object null"); return; + case SCR_DYN_BOOL: scr_jb_puts(b, v->v.b ? "boolean true" : "boolean false"); return; + case SCR_DYN_NUM: { + scr_jb_puts(b, "number "); + ScrStr *n = scr_f64_to_scrstr(v->v.num); + scr_jb_write(b, n->data, n->len); + scr_str_release(n); + return; + } + case SCR_DYN_STR: + scr_jb_puts(b, "string \""); + scr_jb_write(b, v->v.str->data, v->v.str->len); + scr_jb_putc(b, '"'); + return; + case SCR_DYN_SYMBOL: scr_jb_puts(b, "symbol"); return; + case SCR_DYN_BIGINT: scr_jb_puts(b, "bigint"); return; + default: scr_jb_puts(b, "object"); return; + } +} + +static bool scr_ih_require_callable(const ScrDyn *fn) { + if (scr_dyn_is_callable(fn)) return true; + ScrJsonBuf b; + scr_jb_init(&b); + scr_ih_describe_type(&b, fn); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return false; +} + +/* V8's text for the value in " must be positive": numbers, strings + * raw, undefined by name, + * arrays and objects by their default string or constructor name. */ +static void scr_ih_describe_value(ScrJsonBuf *b, const ScrDyn *v) { + if (v->kind == SCR_DYN_ARR) { + scr_jb_puts(b, "[object Array]"); + return; + } + if (v->kind == SCR_DYN_OBJ) { + scr_jb_puts(b, "#"); + return; + } + if (v->kind == SCR_DYN_HANDLE) { + scr_jb_puts(b, "#<"); + scr_jb_puts(b, scr_dyn_handle_ops_of(v)->cls); + scr_jb_putc(b, '>'); + return; + } + if (v->kind == SCR_DYN_STR) { + scr_jb_write(b, v->v.str->data, v->v.str->len); + return; + } + if (v->kind == SCR_DYN_NUM) { + ScrStr *n = scr_f64_to_scrstr(v->v.num); + scr_jb_write(b, n->data, n->len); + scr_str_release(n); + return; + } + scr_jb_puts(b, v->kind == SCR_DYN_UNDEF ? "undefined" : "NaN"); +} + +/* ToIntegerOrInfinity of a take/drop budget with Node's RangeError for NaN + * and negative budgets. Answers false with the exception pending. */ +static bool scr_ih_budget(const ScrDyn *limit, double *out) { + double n = scr_dyn_number_coerce(limit); + if (scr_exc_pending()) return false; + if (n != n) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_ih_describe_value(&b, limit); + scr_jb_puts(&b, " must be positive"); + scr_throw_error(SCR_ERR_RANGE, scr_jb_finish(&b)); + return false; + } + double t = n < 9007199254740992.0 && n > -9007199254740992.0 ? n - fmod(n, 1.0) : n; + if (t < 0) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_ih_describe_value(&b, limit); + scr_jb_puts(&b, " must be positive"); + scr_throw_error(SCR_ERR_RANGE, scr_jb_finish(&b)); + return false; + } + *out = t == 0 ? 0 : t; + return true; +} + +/* IteratorClose(iterator, completion). `abrupt` is a throw completion: the + * pending error stays and anything the return() call raises is dropped. + * Otherwise errors from return() propagate, and a non-object result is + * Node's TypeError. Answers false when an exception is pending after. */ +static bool scr_ih_close(ScrDyn *iterator, bool abrupt) { + ScrCaught *first = abrupt ? scr_exc_take() : NULL; + ScrDyn *method = scr_iterator_read(iterator, "return", 6); + bool ok = method != NULL; + if (ok && method->kind != SCR_DYN_UNDEF && method->kind != SCR_DYN_NULL) { + ScrDyn *result = scr_iterator_call(method, iterator, "return"); + if (!result) ok = false; + else { + if (!abrupt && !scr_iterator_object(result)) { + static const char message[] = "Iterator result is not an object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + ok = false; + } + scr_dyn_release(result); + } + } + scr_dyn_release(method); + if (first) { + if (scr_exc_pending()) { + ScrCaught *dropped = scr_exc_take(); + if (dropped) scr_caught_release(dropped); + } + scr_rethrow(first); + scr_caught_release(first); + return false; + } + return ok && !scr_exc_pending(); +} + +/* IteratorStepValue over (iterator, next): +1 value with *done false, or + * *done true (the answer is then undefined); NULL with the exception + * pending when next() throws or answers a non-object. */ +static ScrDyn *scr_ih_step(ScrDyn *iterator, ScrDyn *next, bool *done) { + *done = false; + if (scr_dyn_iterator_can_step(iterator, next)) { + ScrDyn *value = scr_dyn_iterator_step(iterator); + if (!value) return NULL; + *done = scr_dyn_iterator_step_done(iterator); + return value; + } + ScrDyn *raw = scr_iterator_call(next, iterator, "next"); + if (!raw) return NULL; + ScrDyn *result = scr_dyn_iterator_result(raw); + scr_dyn_release(raw); + if (!result) return NULL; + ScrDyn *finished = scr_iterator_read(result, "done", 4); + if (!finished) { + scr_dyn_release(result); + return NULL; + } + *done = scr_dyn_truthy(finished); + scr_dyn_release(finished); + ScrDyn *value = *done ? scr_dyn_retain(scr_dyn_undefined()) : scr_iterator_read(result, "value", 5); + scr_dyn_release(result); + return value; +} + +/* GetIteratorDirect: the receiver must be an object; its next method is + * read once. +1 next, or NULL with the TypeError pending. */ +static ScrDyn *scr_ih_direct(const ScrDyn *self, const char *method) { + if (!scr_iterator_object(self)) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Iterator.prototype."); + scr_jb_puts(&b, method); + scr_jb_puts(&b, " called on non-object"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; + } + return scr_iterator_read(self, "next", 4); +} + +static ScrDyn *scr_ih_result(ScrDyn *value, bool done) { + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, value); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done)); + return result; +} + +static void *scr_ih_retain(void *ptr) { + ((ScrIterHelper *)ptr)->rc++; + return ptr; +} + +static void scr_ih_release(void *ptr) { + ScrIterHelper *h = ptr; + if (--h->rc) return; + scr_dyn_release(h->iterated); + scr_dyn_release(h->next); + if (h->fn) scr_dyn_release(h->fn); + if (h->inner) scr_dyn_release(h->inner); + if (h->inner_next) scr_dyn_release(h->inner_next); + free(h); +} + +static void scr_ih_clear_inner(ScrIterHelper *h) { + if (h->inner) scr_dyn_release(h->inner); + if (h->inner_next) scr_dyn_release(h->inner_next); + h->inner = NULL; + h->inner_next = NULL; +} + +/* GetIteratorFlattenable(value, reject-primitives) for flatMap's callback + * result: +1 iterator with *next_out +1, or NULL with the TypeError pending. */ +static ScrDyn *scr_ih_flattenable(ScrDyn *value, ScrDyn **next_out) { + if (!scr_iterator_object(value)) { + static const char message[] = "Iterator.prototype.flatMap called on non-object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrDyn *method = scr_iterator_method(value); + if (!method) return NULL; + ScrDyn *iterator; + if (method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL) { + iterator = scr_dyn_retain(value); + } else { + iterator = scr_iterator_call(method, value, "value[Symbol.iterator]"); + if (iterator && !scr_iterator_object(iterator)) { + scr_dyn_release(iterator); + static const char message[] = "Iterator result is not an object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + iterator = NULL; + } + } + scr_dyn_release(method); + if (!iterator) return NULL; + ScrDyn *next = scr_iterator_read(iterator, "next", 4); + if (!next) { + scr_dyn_release(iterator); + return NULL; + } + *next_out = next; + return iterator; +} + +/* Calls a stage callback with (value, counter); +1 result or NULL. */ +static ScrDyn *scr_ih_callback(ScrDyn *fn, ScrDyn *value, double counter) { + ScrDyn *index = scr_dyn_new_num(counter); + ScrDyn *args[2] = { value, index }; + ScrDyn *undefined = scr_dyn_retain(scr_dyn_undefined()); + scr_dyn_this_push_dyn(undefined); + ScrDyn *result = scr_dyn_call(fn, args, 2, "callback"); + scr_dyn_this_pop(); + scr_dyn_release(undefined); + scr_dyn_release(index); + return result; +} + +/* The helper's next value: +1 value with *done false, *done true, or NULL + * with the exception pending (the helper is then done). */ +static ScrDyn *scr_ih_advance(ScrIterHelper *h, bool *done) { + *done = false; + for (;;) { + if (h->kind == SCR_IH_FLATMAP && h->inner) { + bool inner_done; + ScrDyn *value = scr_ih_step(h->inner, h->inner_next, &inner_done); + if (!value) { + scr_ih_close(h->iterated, true); + return NULL; + } + if (!inner_done) return value; + scr_dyn_release(value); + scr_ih_clear_inner(h); + continue; + } + if (h->kind == SCR_IH_TAKE) { + if (h->remaining == 0) { + *done = true; + if (!scr_ih_close(h->iterated, false)) return NULL; + return scr_dyn_retain(scr_dyn_undefined()); + } + if (h->remaining != INFINITY) h->remaining--; + } + if (h->kind == SCR_IH_DROP) { + while (h->remaining > 0) { + if (h->remaining != INFINITY) h->remaining--; + bool skipped_done; + ScrDyn *skipped = scr_ih_step(h->iterated, h->next, &skipped_done); + if (!skipped) return NULL; + scr_dyn_release(skipped); + if (skipped_done) { + *done = true; + return scr_dyn_retain(scr_dyn_undefined()); + } + } + } + bool source_done; + ScrDyn *value = scr_ih_step(h->iterated, h->next, &source_done); + if (!value) return NULL; + if (source_done) { + *done = true; + return value; + } + if (h->kind == SCR_IH_TAKE || h->kind == SCR_IH_DROP) return value; + ScrDyn *mapped = scr_ih_callback(h->fn, value, h->counter); + if (!mapped) { + scr_dyn_release(value); + scr_ih_close(h->iterated, true); + return NULL; + } + h->counter++; + if (h->kind == SCR_IH_MAP) { + scr_dyn_release(value); + return mapped; + } + if (h->kind == SCR_IH_FILTER) { + bool keep = scr_dyn_truthy(mapped); + scr_dyn_release(mapped); + if (keep) return value; + scr_dyn_release(value); + continue; + } + /* flatMap */ + scr_dyn_release(value); + ScrDyn *inner_next = NULL; + ScrDyn *inner = scr_ih_flattenable(mapped, &inner_next); + scr_dyn_release(mapped); + if (!inner) { + scr_ih_close(h->iterated, true); + return NULL; + } + h->inner = inner; + h->inner_next = inner_next; + } +} + +static void scr_ih_running_error(void) { + static const char message[] = "Generator is already running"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); +} + +static ScrDyn *scr_ih_next(ScrIterHelper *h) { + if (h->state == SCR_IH_RUNNING) { + scr_ih_running_error(); + return NULL; + } + if (h->state == SCR_IH_DONE) return scr_ih_result(scr_dyn_retain(scr_dyn_undefined()), true); + h->state = SCR_IH_RUNNING; + scr_ih_retain(h); + bool done; + ScrDyn *value = scr_ih_advance(h, &done); + ScrDyn *result = NULL; + if (!value || done) { + h->state = SCR_IH_DONE; + scr_ih_clear_inner(h); + if (value) { + scr_dyn_release(value); + result = scr_ih_result(scr_dyn_retain(scr_dyn_undefined()), true); + } + } else { + h->state = SCR_IH_YIELDED; + result = scr_ih_result(value, false); + } + scr_ih_release(h); + return result; +} + +/* %IteratorHelperPrototype%.return: an unstarted or suspended helper + * completes and closes the iterator it was made from (flatMap closes its + * inner iterator first). */ +static ScrDyn *scr_ih_return(ScrIterHelper *h) { + if (h->state == SCR_IH_RUNNING) { + scr_ih_running_error(); + return NULL; + } + if (h->state != SCR_IH_DONE) { + h->state = SCR_IH_DONE; + scr_ih_retain(h); + bool ok = true; + if (h->inner) { + ScrDyn *inner = scr_dyn_retain(h->inner); + scr_ih_clear_inner(h); + if (!scr_ih_close(inner, false)) { + scr_ih_close(h->iterated, true); + ok = false; + } + scr_dyn_release(inner); + } + if (ok) ok = scr_ih_close(h->iterated, false); + scr_ih_release(h); + if (!ok) return NULL; + } + return scr_ih_result(scr_dyn_retain(scr_dyn_undefined()), true); +} + +static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc); + +static ScrDyn *scr_ih_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + if (!strcmp(method, "next")) return scr_ih_next(ptr); + if (!strcmp(method, "return")) return scr_ih_return(ptr); + int index = scr_ih_method_index(method, strlen(method)); + if (index >= 0) return scr_iterator_helper_method(self, index, args, argc); + return scr_dyn_call(scr_dyn_undefined(), args, argc, what); +} + +static ScrDyn *scr_ih_method_function(int index); + +static ScrDyn *scr_ih_next_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)clo; (void)args; (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_ITERATOR_HELPER) { + result = scr_ih_next(self->v.handle.ptr); + } else { + static const char message[] = "Method Iterator Helper.prototype.next called on incompatible receiver"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(self); + return result; +} + +static ScrDyn *scr_ih_return_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)clo; (void)args; (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_ITERATOR_HELPER) { + result = scr_ih_return(self->v.handle.ptr); + } else { + static const char message[] = "Method Iterator Helper.prototype.return called on incompatible receiver"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(self); + return result; +} + +static SCR_TL ScrDyn *scr_ih_functions[SCR_IH_METHOD_COUNT + 2]; +static SCR_TL bool scr_ih_functions_registered; + +static void scr_ih_functions_cleanup(void) { + for (size_t i = 0; i < SCR_IH_METHOD_COUNT + 2; i++) { + scr_dyn_release(scr_ih_functions[i]); + scr_ih_functions[i] = NULL; + } +} + +static ScrDyn *scr_ih_function(size_t slot, ScrDynThunk thunk, const char *name) { + if (!scr_ih_functions_registered) { + scr_ih_functions_registered = true; + scr_atexit(scr_ih_functions_cleanup); + } + if (!scr_ih_functions[slot]) + scr_ih_functions[slot] = scr_dyn_new_func(scr_closure_new(NULL, 0), thunk, 0, "native:iterator", name); + return scr_dyn_retain(scr_ih_functions[slot]); +} + +static ScrDyn *scr_ih_get(void *ptr, const char *key, size_t len) { + (void)ptr; + if (len == 4 && !memcmp(key, "next", 4)) return scr_ih_function(SCR_IH_METHOD_COUNT, &scr_ih_next_call, "next"); + if (len == 6 && !memcmp(key, "return", 6)) + return scr_ih_function(SCR_IH_METHOD_COUNT + 1, &scr_ih_return_call, "return"); + int index = scr_ih_method_index(key, len); + return index >= 0 ? scr_ih_method_function(index) : NULL; +} + +static ScrDyn *scr_ih_inspect_view(void *ptr) { + (void)ptr; + return scr_dyn_new_obj(); +} + +static ScrDyn *scr_ih_new(unsigned kind, ScrDyn *iterated, ScrDyn *next, ScrDyn *fn, double remaining) { + static const ScrDynHandleOps ops = { + .cls = "Object", + .retain = &scr_ih_retain, + .release = &scr_ih_release, + .invoke = &scr_ih_invoke, + .get = &scr_ih_get, + .inspect_view = &scr_ih_inspect_view, + .to_string_tag = "Iterator Helper", + .inspect_view_empty = true, + }; + scr_dyn_handle_install(SCR_DYNH_ITERATOR_HELPER, &ops); + ScrIterHelper *h = malloc(sizeof *h); + if (!h) scr_json_oom(); + *h = (ScrIterHelper){ .rc = 1, .kind = kind, .state = SCR_IH_START, .iterated = scr_dyn_retain(iterated), + .next = next, .fn = fn ? scr_dyn_retain(fn) : NULL, .remaining = remaining }; + ScrDyn *result = scr_dyn_new_handle(h, SCR_DYNH_ITERATOR_HELPER); + scr_ih_release(h); + return result; +} + +/* The consuming methods: drive (self, next) with `fn`. */ +static ScrDyn *scr_ih_consume(ScrDyn *self, ScrDyn *next, int index, ScrDyn *fn, ScrDyn *const *args, + size_t argc) { + const char *name = scr_ih_method_names[index]; + bool reduce = !strcmp(name, "reduce"); + ScrDyn *out = !strcmp(name, "toArray") ? scr_dyn_new_arr() : NULL; + ScrDyn *acc = NULL; + double counter = 0; + if (reduce && argc >= 2) acc = scr_dyn_retain(args[1]); + else if (reduce) { + bool done; + ScrDyn *first = scr_ih_step(self, next, &done); + if (!first) return NULL; + if (done) { + scr_dyn_release(first); + static const char message[] = "Reduce of a done iterator with no initial value"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + acc = first; + counter = 1; + } + for (;;) { + bool done; + ScrDyn *value = scr_ih_step(self, next, &done); + if (!value) { + if (out) scr_dyn_release(out); + if (acc) scr_dyn_release(acc); + return NULL; + } + if (done) { + scr_dyn_release(value); + break; + } + if (out) { + scr_dyn_arr_push(out, value); + continue; + } + ScrDyn *answer; + if (reduce) { + ScrDyn *index_value = scr_dyn_new_num(counter); + ScrDyn *call_args[3] = { acc, value, index_value }; + ScrDyn *undefined = scr_dyn_retain(scr_dyn_undefined()); + scr_dyn_this_push_dyn(undefined); + answer = scr_dyn_call(fn, call_args, 3, "callback"); + scr_dyn_this_pop(); + scr_dyn_release(undefined); + scr_dyn_release(index_value); + } else { + answer = scr_ih_callback(fn, value, counter); + } + counter++; + if (!answer) { + scr_dyn_release(value); + if (acc) scr_dyn_release(acc); + scr_ih_close(self, true); + return NULL; + } + if (reduce) { + scr_dyn_release(acc); + acc = answer; + scr_dyn_release(value); + continue; + } + bool truthy = scr_dyn_truthy(answer); + scr_dyn_release(answer); + if (!strcmp(name, "forEach")) { + scr_dyn_release(value); + continue; + } + bool stop = !strcmp(name, "every") ? !truthy : truthy; + if (!stop) { + scr_dyn_release(value); + continue; + } + if (!scr_ih_close(self, false)) { + scr_dyn_release(value); + return NULL; + } + if (!strcmp(name, "find")) return value; + scr_dyn_release(value); + return scr_dyn_new_bool(!strcmp(name, "some")); + } + if (out) return out; + if (reduce) return acc; + if (!strcmp(name, "some") || !strcmp(name, "every")) return scr_dyn_new_bool(!strcmp(name, "every")); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* An Iterator.prototype helper method called on `self` (borrowed). */ +static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc) { + const char *name = scr_ih_method_names[index]; + ScrDyn *next = scr_ih_direct(self, name); + if (!next) return NULL; + ScrDyn *arg = argc > 0 ? args[0] : scr_dyn_undefined(); + if (!strcmp(name, "take") || !strcmp(name, "drop")) { + double budget; + if (!scr_ih_budget(arg, &budget)) { + scr_dyn_release(next); + scr_ih_close(self, true); + return NULL; + } + return scr_ih_new(!strcmp(name, "take") ? SCR_IH_TAKE : SCR_IH_DROP, self, next, NULL, budget); + } + if (!strcmp(name, "toArray")) { + ScrDyn *result = scr_ih_consume(self, next, index, NULL, args, argc); + scr_dyn_release(next); + return result; + } + if (!scr_ih_require_callable(arg)) { + scr_dyn_release(next); + scr_ih_close(self, true); + return NULL; + } + if (index < 5) { + unsigned kind = !strcmp(name, "map") ? SCR_IH_MAP : !strcmp(name, "filter") ? SCR_IH_FILTER : SCR_IH_FLATMAP; + return scr_ih_new(kind, self, next, arg, 0); + } + ScrDyn *result = scr_ih_consume(self, next, index, arg, args, argc); + scr_dyn_release(next); + return result; +} + +static ScrDyn *scr_ih_method_call(int index, ScrDyn *const *args, size_t argc) { + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = scr_iterator_helper_method(self, index, args, argc); + scr_dyn_release(self); + return result; +} + +#define SCR_IH_THUNK(i) \ + static ScrDyn *scr_ih_method_call_##i(ScrClosure *clo, ScrDyn *const *args, size_t argc) { \ + (void)clo; \ + return scr_ih_method_call(i, args, argc); \ + } +SCR_IH_THUNK(0) +SCR_IH_THUNK(1) +SCR_IH_THUNK(2) +SCR_IH_THUNK(3) +SCR_IH_THUNK(4) +SCR_IH_THUNK(5) +SCR_IH_THUNK(6) +SCR_IH_THUNK(7) +SCR_IH_THUNK(8) +SCR_IH_THUNK(9) +SCR_IH_THUNK(10) + +static ScrDyn *scr_ih_method_function(int index) { + static const ScrDynThunk thunks[SCR_IH_METHOD_COUNT] = { + &scr_ih_method_call_0, &scr_ih_method_call_1, &scr_ih_method_call_2, &scr_ih_method_call_3, + &scr_ih_method_call_4, &scr_ih_method_call_5, &scr_ih_method_call_6, &scr_ih_method_call_7, + &scr_ih_method_call_8, &scr_ih_method_call_9, &scr_ih_method_call_10, + }; + return scr_ih_function((size_t)index, thunks[index], scr_ih_method_names[index]); +} + ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection) { return scr_native_collection_iterator(source, selection); } diff --git a/tests/corpus/6070-iterator-helpers-untyped.js b/tests/corpus/6070-iterator-helpers-untyped.js new file mode 100644 index 000000000..4f528f0d3 --- /dev/null +++ b/tests/corpus/6070-iterator-helpers-untyped.js @@ -0,0 +1,89 @@ +// Iterator helpers on untyped iterators in JavaScript: the receivers come +// from parameters without inferred types, so the helpers run on the +// runtime's iterator objects. +const log = []; +function flush(what) { + console.log(what, "|", log.join(" ")); + log.length = 0; +} +function values(xs) { + return xs.values(); +} +function keysOf(m) { + return m.keys(); +} +function charsOf(s) { + return s[Symbol.iterator](); +} + +// Stages and consuming methods. +const evens = values([1, 2, 3, 4, 6]).filter((x) => x % 2 === 0); +console.log(evens.next(), evens.take(1).toArray(), [...evens]); +console.log(values([1, 2, 3]).map((x, i) => x * 10 + i).toArray(), values([]).map((x) => x).toArray()); +console.log(values([1, 2, 3]).reduce((a, b) => a + b), values([1, 2]).reduce((a, b, i) => a + b * i, 100)); +console.log(values([5, 6, 7]).drop(1).toArray(), values([5, 6, 7]).take(Infinity).toArray()); +console.log(values(new Set([1, 3, 5])).find((x) => x > 2), values(new Set()).find(() => true)); +console.log(values([1, 2, 3]).some((x) => x === 2), values([1, 2, 3]).every((x) => x < 3)); +console.log(keysOf(new Map([["a", 1], ["b", 2]])).map((k) => k.toUpperCase()).toArray()); +console.log(charsOf("a-b-c").filter((c) => c !== "-").toArray().join("")); +const seen = []; +values(["p", "q"]).forEach((v, i) => seen.push(v + i)); +console.log(seen.join(",")); + +// Helper objects. +const doubled = values([1, 2, 3]).map((x) => x * 2); +console.log(typeof doubled.next, typeof doubled.return, typeof doubled.map, typeof doubled.throw); +console.log(doubled.next(), doubled.return(), doubled.next()); +for (const x of values([1, 2, 3]).drop(1)) log.push(`of${x}`); +flush("for...of"); +console.log([...values([4, 5]).flatMap((x) => [x, x])]); +console.log(Object.prototype.toString.call(doubled), String(doubled)); +console.log(doubled); +let self = null; +self = values([1, 2]).map((x) => { + if (x === 2) { + try { + self.next(); + } catch (e) { + log.push(`${e.name}: ${e.message}`); + } + } + return x; +}); +flush(self.toArray()); + +// flatMap closes an inner iterator it leaves early. +function inner(label) { + let n = 0; + return { + next() { + n++; + return { value: `${label}${n}`, done: n > 3 }; + }, + return() { + log.push(`${label}:closed`); + return { done: true }; + }, + }; +} +flush(values([1, 2]).flatMap((x) => inner(`i${x}`)).take(2).toArray()); +const stepping = values([1]).flatMap(() => inner("j")); +stepping.next(); +flush([stepping.return(), stepping.next()]); + +// Node's errors for invalid arguments. +function attempt(f) { + try { + console.log(f()); + } catch (e) { + console.log(`${e.name}: ${e.message}`); + } +} +attempt(() => values([1]).take("x")); +attempt(() => values([1]).drop(-1)); +attempt(() => values([1]).map(5)); +attempt(() => values([1]).filter(null)); +attempt(() => values([1]).reduce(undefined)); +attempt(() => values([1]).forEach("f")); +attempt(() => values([]).reduce((a, b) => a)); +attempt(() => values([1, 2]).flatMap((x) => x).toArray()); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 48b392237..8131c1cd2 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2222,6 +2222,7 @@ "6043-string-index-narrowed-union.ts": 2983, "605-closures-forward-capture-tdz.ts": 3277, "6051-generic-class-factory-results.ts": 4363, + "6070-iterator-helpers-untyped.js": 4050, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5280,6 +5281,7 @@ "6043-string-index-narrowed-union.ts": 3627, "605-closures-forward-capture-tdz.ts": 4638, "6051-generic-class-factory-results.ts": 46581, + "6070-iterator-helpers-untyped.js": 6900, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From b6637c882d60fc36a381d9c275b381319ea1a889 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 15:43:21 -0700 Subject: [PATCH 083/120] Check record, array and string fields that construction can read early A field without an initializer that the constructor assigns only after handing `this` to other code is undefined in Node until that assignment. Class-typed fields in that position already take the undefined-armed deferred-init slot whose reads check it. Record, array and string fields kept a plain pointer slot, so a method the constructor calls first could read null and crash with a segfault (`this.internal.n` read from a method before `this.internal = ...`). Those field kinds now take the same checked slot when a method or accessor the constructor calls through `this` (transitively, over the class and its bases) reads them. Fields only exposed in other ways keep their plain slot, so constructors whose early calls never touch the field compile as before. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 69 ++++++++++++++++++- ...class-field-read-before-assignment.ts.json | 6 ++ ...6191-class-field-read-before-assignment.ts | 44 ++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json create mode 100644 tests/corpus/6191-class-field-read-before-assignment.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 39b3242d6..6dadb029d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -3608,11 +3608,26 @@ export function collectClassShapeInner( } if (escape.assigned && !inst) { + let earlyReads: Set | null = null; for (const f of fieldOrder) { if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue; if (paramProps.some((pp) => pp.name === f.name)) continue; const declared = fields.get(f.name); - if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime) + // Records, arrays and strings are pointer slots that start empty + // too, but only take the checked slot when a method the + // constructor calls actually reads them: their other early + // exposures keep the plain slot. + if ( + declared?.kind === "record" || + declared?.kind === "array" || + declared?.kind === "string" + ) { + earlyReads ??= constructorMethodReads(decl, base, ctor ?? undefined); + if (!earlyReads.has(f.name)) continue; + } else if ( + declared?.kind !== "object" || + lowerer.classes.get(declared.className)?.def.runtime + ) continue; const armed = lowerer.withUndefinedArm(declared); if (armed === null || armed.kind !== "union") continue; @@ -4838,6 +4853,58 @@ export function staticShadowBelow(info: ClassInfo, name: string): boolean { ); } +/** The `this` members read by the methods and accessors a constructor + * calls through `this` (transitively, over the class and its bases), + * outside nested functions. The constructor's own reads are not included: + * they run where the constructor's statements put them. */ +function constructorMethodReads( + decl: ts.ClassLikeDeclaration, + base: ClassInfo | null, + ctor: ts.ConstructorDeclaration | undefined, +): Set { + const reads = new Set(); + const visited = new Set(); + const bodies = (name: string): ts.Node[] => { + const out: ts.Node[] = []; + let owner: ts.ClassLikeDeclaration | undefined = decl; + for (let next = base; owner; owner = next?.decl ?? undefined, next = next?.base ?? null) { + for (const member of owner.members) { + if ( + (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && + member.body && + (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && + member.name.text === name + ) + out.push(member.body); + } + } + return out; + }; + const enter = (name: string): void => { + if (visited.has(name)) return; + visited.add(name); + for (const body of bodies(name)) scan(body, true); + }; + // Nested functions (callbacks handed elsewhere) are not followed: only + // the straight-line code of the constructor and the methods it calls. + const scan = (node: ts.Node, inMethod: boolean): void => { + ts.walkPreorder(node, (n) => { + if (n !== node && ts.isFunctionLike(n)) return "skip"; + if ( + ts.isPropertyAccessExpression(n) && + n.expression.kind === ts.SyntaxKind.ThisKeyword && + (ts.isIdentifier(n.name) || ts.isPrivateIdentifier(n.name)) + ) { + if (inMethod) reads.add(n.name.text); + enter(n.name.text); + } + return undefined; + }); + }; + if (ctor?.body) scan(ctor.body, false); + return reads; +} + /** The nearest GENERIC static declaration of `name` at/above `info` — * findStaticOn's twin over the genericStatics tables. */ export function findGenericStaticOn( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json new file mode 100644 index 000000000..3813f38bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6191-class-field-read-before-assignment.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6191-class-field-read-before-assignment.ts b/tests/corpus/6191-class-field-read-before-assignment.ts new file mode 100644 index 000000000..92cb75b02 --- /dev/null +++ b/tests/corpus/6191-class-field-read-before-assignment.ts @@ -0,0 +1,44 @@ +// Fields without initializers that a method reads while the constructor +// has not assigned them yet (the constructor calls the method first). +// Node reads undefined there, so member access on it throws a TypeError; +// after the assignment the same reads see the values. + +class Settings { + internal: { n: number }; + items: number[]; + label: string; + + constructor() { + console.log(this.describe()); + this.internal = { n: 1 }; + this.items = [1, 2]; + this.label = "ready"; + console.log(this.describe()); + } + + describe(): string { + let internal: string; + let items: string; + let label: string; + try { + internal = String(this.internal.n); + } catch (e) { + internal = e instanceof TypeError ? "TypeError" : "other"; + } + try { + items = String(this.items.length); + } catch (e) { + items = e instanceof TypeError ? "TypeError" : "other"; + } + try { + label = this.label.toUpperCase(); + } catch (e) { + label = e instanceof TypeError ? "TypeError" : "other"; + } + return `${internal} ${items} ${label}`; + } +} + +const settings = new Settings(); +settings.items.push(3); +console.log(settings.internal.n, settings.items.length, settings.label); From f8fa60afecbdf26a6a4154774a9b2e907b0ad0f6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 15:43:21 -0700 Subject: [PATCH 084/120] Link the promise adapter when a boxed class instance holds a promise Boxing a class instance as a dynamic value (Object.keys, spread, inspection) boxes its fields, and a promise field goes through scr_dyn_new_promise_adapting in scr_async_dyn.c. The link-unit scan followed promises through records, arrays, unions and functions but not class instances, so `Object.keys(job)` over a class with an optional `Promise` field failed to link. The scan now follows a class instance's fields. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/ir/runtime-features.ts | 15 +++++++++++++-- .../6192-optional-promise-field-keys.ts.json | 6 ++++++ tests/corpus/6192-optional-promise-field-keys.ts | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json create mode 100644 tests/corpus/6192-optional-promise-field-keys.ts diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index d16edfabd..57331d830 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1,4 +1,4 @@ -import type { IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } from "./ir.js"; +import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } from "./ir.js"; import type { IrLibFn } from "./builtin-signatures.js"; import { isDirentShape, @@ -132,10 +132,11 @@ export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures { return scanRuntimeFeatures(mod); } -/** The module's record shapes and unions by id. */ +/** The module's record shapes, unions and class layouts by id. */ interface TypeTables { records: Map; unions: Map; + classes: Map; } /** Whether boxing a value of type `t` as dyn boxes a promise, which goes @@ -166,6 +167,15 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean const def = tables.unions.get(t.unionId); return def?.arms.some((a) => boxesPromise(a, tables, seen)) ?? false; } + case "object": { + // A boxed class instance exposes its fields as dyn values (Object.keys, + // spread, inspection), boxing a promise any field holds. + const key = `class:${t.className}`; + if (seen.has(key)) return false; + seen.add(key); + const def = tables.classes.get(t.className); + return def?.fields.some((f) => boxesPromise(f.type, tables, seen)) ?? false; + } default: return false; } @@ -222,6 +232,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run (tables ??= { records: new Map((mod.records ?? []).map((r) => [r.id, r])), unions: new Map((mod.unions ?? []).map((u) => [u.id, u])), + classes: new Map((mod.classes ?? []).map((c) => [c.name, c])), }); const expr = (node: IrExpr): boolean => { if (node.kind === "libCall") { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json new file mode 100644 index 000000000..dbae046c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6192-optional-promise-field-keys.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6192-optional-promise-field-keys.ts b/tests/corpus/6192-optional-promise-field-keys.ts new file mode 100644 index 000000000..98cec870c --- /dev/null +++ b/tests/corpus/6192-optional-promise-field-keys.ts @@ -0,0 +1,12 @@ +// Object.keys over an instance whose optional field holds a promise: +// boxing the instance links the promise adapter of the runtime. + +class Job { + done?: Promise; + name = "job"; +} + +const job = new Job(); +console.log(Object.keys(job).join(",")); +job.done = Promise.resolve(); +console.log(Object.keys(job).join(","), typeof job.done); From 20f15f89fdbab54dc12342a87d38893bb738d139 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 15:43:22 -0700 Subject: [PATCH 085/120] Let implicit JS instances that fall through return undefined A JavaScript function specialized per call site takes its declared return type from JSDoc. bcryptjs's `hash` and `compare` document `@returns {!Promise}` but return nothing when a callback is passed, so their instances ended with the "the checker proved every path returns" trap and every callback-style call aborted with SC9002. Non-generic JavaScript functions already widen such a return with an undefined arm; implicit instances now do the same. The widened `Promise | undefined` result is then awaited by TypeScript callers, which was refused: an await of a promise-of-dynamic or unit union now goes through the dynamic await, which adopts the promise or hops once for the unit, as Node does. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/generic-functions.ts | 11 ++++++++ .../src/frontend/lowering/lower-exprs.ts | 12 +++++++++ .../main.ts.json | 6 +++++ .../main.ts | 9 +++++++ .../node_modules/callback-hash/index.d.ts | 6 +++++ .../node_modules/callback-hash/index.js | 26 +++++++++++++++++++ .../node_modules/callback-hash/package.json | 1 + 7 files changed, 71 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index bd785a256..11265be8b 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -13,6 +13,7 @@ import { } from "../../ir/ir.js"; import { isJsSourceFile, locOf } from "../program.js"; import { typeKey } from "../type-mapper.js"; +import { functionCanReturnUndefined } from "../function-completion.js"; import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { findGenericMethodOn, type ClassInfo } from "./lower-classes.js"; @@ -1428,6 +1429,16 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | return null; if (mapped !== null && promiseCarriesDyn(lowerer, mapped) && !hasExplicitJsDocReturn(info.decl)) return null; + // A JavaScript body that can complete without returning yields + // undefined there, whatever its JSDoc claims (bcryptjs's `hash` + // returns a promise only when no callback is passed), exactly as + // declaredReturnType widens non-generic JavaScript functions. + if ( + mapped !== null && + isJsSourceFile(info.decl.getSourceFile()) && + functionCanReturnUndefined(info.decl) + ) + return lowerer.withUndefinedArmOf(mapped) ?? mapped; return mapped; } catch (e) { if (!(e instanceof PoisonError)) throw e; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index c54341df8..6ec10e8d4 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1722,6 +1722,18 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const promiseArm = def.arms[promiseTag]!; const inner = promiseArm.kind === "promise" ? promiseArm.inner : VOID; const units = def.arms.filter(isUnitType); + // A checked-dynamic inner: the whole operand is a dyn value (the + // promise boxes by reference, the unit as itself), and the dyn + // await adopts the promise or hops once for the unit. + if (inner.kind === "dyn" && lowerer.dynConvertible(value.type)) { + return { + kind: "libCall", + fn: "async.awaitDyn", + args: [lowerer.coerceToExpected(value, DYN)], + type: DYN, + loc, + }; + } if (inner.kind === "union" || inner.kind === "dyn") { // A union inner would need an arm-wise re-tag into the result diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json new file mode 100644 index 000000000..39de1b842 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts new file mode 100644 index 000000000..fb61368f7 --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts @@ -0,0 +1,9 @@ +// A package function whose JSDoc promises a Promise but that returns +// nothing when a callback is passed (bcryptjs's hash and compare). The +// call that falls through completes normally, as in Node. +import { hash } from "callback-hash"; + +hash("pw", 4, (err, value) => { + console.log("callback", err, value); +}); +console.log("promise", await hash("pw", "salt")); diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts new file mode 100644 index 000000000..819294565 --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts @@ -0,0 +1,6 @@ +export declare function hash(input: string, salt: number | string): Promise; +export declare function hash( + input: string, + salt: number | string, + callback: (err: Error | null, value?: string) => void, +): void; diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js new file mode 100644 index 000000000..2661ea6ab --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js @@ -0,0 +1,26 @@ +/** + * @param {string} input + * @param {number|string} salt + * @param {function(Error, string=)=} callback + * @param {function(number)=} progress + * @returns {!Promise} + */ +export function hash(input, salt, callback, progress) { + function run(done) { + if (progress) progress(1); + done(null, `${input}:${salt}`); + } + if (callback) { + if (typeof callback !== "function") throw Error("Illegal callback: " + typeof callback); + run(callback); + } else + return new Promise(function (resolve, reject) { + run(function (err, res) { + if (err) { + reject(err); + return; + } + resolve(res); + }); + }); +} diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json new file mode 100644 index 000000000..d3553db20 --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json @@ -0,0 +1 @@ +{ "name": "callback-hash", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } From f761f7d75aa088881ddeccc5a7a9e2c551d5426c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 19:54:32 -0500 Subject: [PATCH 086/120] Count idle pooled sockets in agent.totalSocketCount agent.totalSocketCount counted only the requests an agent was serving, so the idle sockets of a keepAlive agent in agent.freeSockets were left out, and a second request that dialed a new socket while the first was idle counted 1 instead of 2. As in Node, the count is now the number of sockets the agent created that have not closed: a socket joins it when its dial starts and leaves it when it closes, or before the request's 'close' when it does not go back to the pool. A request that takes a free socket keeps the count. Corpus 5593 covers reuse, a second socket, agent.destroy() and an agent without keepAlive. --- ...5593-http-agent-total-socket-count.ts.json | 6 ++ packages/runtime/src/scr_http.c | 58 ++++++++++++++--- .../5593-http-agent-total-socket-count.ts | 63 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5593-http-agent-total-socket-count.ts.json create mode 100644 tests/corpus/5593-http-agent-total-socket-count.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5593-http-agent-total-socket-count.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5593-http-agent-total-socket-count.ts.json new file mode 100644 index 000000000..9e67f9bd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5593-http-agent-total-socket-count.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5593-http-agent-total-socket-count.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index f4e44bf6e..f34cc18bb 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -2386,6 +2386,19 @@ typedef enum { SCR_HTTP_BODY_EOF, /* RESPONSE mode: no framing header — body until EOF */ } ScrHttpParseState; +/* agent.totalSocketCount: the sockets an agent dialed that have not + * closed yet, idle pooled ones included. The agent and each counted + * connection hold the cell (+1), so a socket that closes after the agent + * is gone still lowers a live count. */ +typedef struct ScrHttpSockCount { + size_t rc; + size_t n; +} ScrHttpSockCount; + +static void scr_http_sock_count_release(ScrHttpSockCount *k) { + if (k != NULL && --k->rc == 0) free(k); +} + typedef struct ScrHttpConn { ScrNetSocket *sock; /* BORROWED: the ctx lives and dies with the socket * (a retained ref here would be a cycle the RC @@ -2408,6 +2421,8 @@ typedef struct ScrHttpConn { bool pumping; /* inside scr_http_conn_pump */ bool keep_alive_timer; /* the socket's idle timer is the keep-alive timeout */ size_t max_header_size; /* maxHeaderSize option; 0 = SCR_HTTP_MAX_HEADER_SIZE */ + struct ScrHttpSockCount *sock_count; /* client mode: the agent count that includes + * this socket (+1), or NULL */ struct ScrHttpAgent *pool_agent; /* client mode: the agent whose free list holds * this idle socket (borrowed), or NULL */ bool header_overflow; /* client mode: the response head exceeded a bound */ @@ -2420,6 +2435,23 @@ typedef struct ScrHttpConn { struct ScrHttpClientReq *client; /* +1 in client mode; released at close */ } ScrHttpConn; +/* The connection's socket joins an agent's totalSocketCount when it starts + * to dial, and leaves it when it closes. */ +static void scr_http_conn_count(ScrHttpConn *conn, ScrHttpSockCount *k) { + if (conn == NULL || conn->sock_count != NULL) return; + k->rc++; + k->n++; + conn->sock_count = k; +} + +static void scr_http_conn_uncount(ScrHttpConn *conn) { + ScrHttpSockCount *k = conn != NULL ? conn->sock_count : NULL; + if (k == NULL) return; + conn->sock_count = NULL; + k->n--; + scr_http_sock_count_release(k); +} + /* Client-mode forward declarations (implementations live with the client * below the server parser). */ static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len); @@ -3878,6 +3910,10 @@ static void scr_http_client_settle(struct ScrHttpClientReq *c) { c->destroyed = true; scr_http_client_signal_detach(c); scr_http_client_agent_detach(c); /* usually already detached at socket close */ + /* a socket that does not go back to the pool leaves totalSocketCount + * before the request's 'close', as Node's agent removes it at the + * socket's 'close' (c->sock keeps c->conn alive) */ + if (c->sock != NULL && !c->sock_pooled) scr_http_conn_uncount(c->conn); scr_net_fire0(&c->close_ls); scr_net_ls_drop(&c->resp_ls); scr_net_ls_drop(&c->err_ls); @@ -4857,6 +4893,7 @@ static void scr_http_client_agent_detach(ScrHttpClientReq *c) { } static void scr_http_client_closed(ScrHttpConn *conn) { + scr_http_conn_uncount(conn); if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, conn); ScrHttpClientReq *c = conn->client; if (!c) return; @@ -4931,6 +4968,7 @@ static void scr_http_client_sock_established(void *ctx) { } static void scr_http_client_conn_free(ScrHttpConn *conn) { + scr_http_conn_uncount(conn); if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, conn); if (conn->req) { scr_http_req_finish(conn->req, false); @@ -5138,6 +5176,7 @@ typedef struct ScrHttpAgent { bool destroyed; ScrHttpAgentEnt *ents; /* append order — Node's FIFO queue */ struct ScrHttpAgentFree *free_socks; /* keepAlive: idle sockets, most recent first */ + ScrHttpSockCount *sock_count; /* totalSocketCount (+1) */ bool in_registry; struct ScrHttpAgent *next; } ScrHttpAgent; @@ -5183,6 +5222,7 @@ static void scr_http_agent_release(ScrHttpAgent *a) { free(e); e = next; } + scr_http_sock_count_release(a->sock_count); free(a); } @@ -5276,6 +5316,7 @@ static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, boo scr_net_sock_set_native_established(old, NULL); scr_net_sock_move_writes(old, f->sock); if (oldconn != NULL) { + scr_http_conn_uncount(oldconn); if (oldconn->client) scr_http_client_release(oldconn->client); free(oldconn->buf); free(oldconn); @@ -5413,7 +5454,10 @@ static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpCl scr_http_client_rebind(e->client, f, false); } else if (scr_http_agent_active(ag, name) < ag->max_sockets) { e->queued = false; - if (e->client->sock) scr_net_sock_dial_start(e->client->sock); + if (e->client->sock) { + scr_http_conn_count(e->client->conn, ag->sock_count); + scr_net_sock_dial_start(e->client->sock); + } } break; } @@ -5463,6 +5507,9 @@ ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs, ScrHttpAgent *a = calloc(1, sizeof *a); if (!a) scr_http_oom(); a->rc = 1; + a->sock_count = calloc(1, sizeof *a->sock_count); + if (!a->sock_count) scr_http_oom(); + a->sock_count->rc = 1; a->secure = secure; a->keep_alive = keep_alive; a->fifo = fifo; @@ -5601,6 +5648,7 @@ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double po scr_net_sock_set_timeout(c->sock, ag->timeout_ms); } if (reuse != NULL) scr_http_client_rebind(c, reuse, true); + else if (!queue) scr_http_conn_count(c->conn, ag->sock_count); /* dialed now */ c->agent = scr_http_agent_retain(ag); ScrHttpAgentEnt *e = calloc(1, sizeof *e); if (!e) scr_http_oom(); @@ -6772,13 +6820,7 @@ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) } return obj; } - if (strcmp(key, "totalSocketCount") == 0) { - size_t n = 0; - for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) { - if (!e->queued) n++; - } - return scr_dyn_new_num((double)n); - } + if (strcmp(key, "totalSocketCount") == 0) return scr_dyn_new_num((double)a->sock_count->n); { static const char *const known[] = { "options", "maxTotalSockets", NULL }; if (scr_http_dynh_in(key, known)) { diff --git a/tests/corpus/5593-http-agent-total-socket-count.ts b/tests/corpus/5593-http-agent-total-socket-count.ts new file mode 100644 index 000000000..6e5f0fc18 --- /dev/null +++ b/tests/corpus/5593-http-agent-total-socket-count.ts @@ -0,0 +1,63 @@ +// agent.totalSocketCount counts the sockets an agent created that have not +// closed: the sockets serving requests and, for a keepAlive agent, the idle +// ones in agent.freeSockets. A request that reuses a free socket keeps the +// count, a request that dials a new one raises it, and agent.destroy() +// lowers it as each socket closes. Without keepAlive, a socket counts until +// it closes after its response. +import * as http from "node:http"; + +const server = http.createServer((req, res) => res.end("ok")); + +function freeCount(agent: http.Agent): number { + let n = 0; + for (const list of Object.values(agent.freeSockets)) n += list ? list.length : 0; + return n; +} + +// Logs the count when the request is created, at its response, at the +// response's end and once the socket is idle, unless `quiet` is set. +function get(agent: http.Agent, port: number, tag: string, quiet: boolean): Promise { + return new Promise((resolve) => { + const req = http.get({ host: "127.0.0.1", port, agent }, (res) => { + if (!quiet) console.log(tag, "response", agent.totalSocketCount); + res.resume(); + res.on("end", () => { + if (!quiet) console.log(tag, "end", agent.totalSocketCount); + setImmediate(() => { + if (!quiet) console.log(tag, "idle", agent.totalSocketCount, freeCount(agent)); + resolve(); + }); + }); + }); + console.log(tag, "created", agent.totalSocketCount, req.reusedSocket); + }); +} + +server.listen(0, "127.0.0.1", async () => { + const port = (server.address() as { port: number }).port; + const keepAlive = new http.Agent({ keepAlive: true }); + await get(keepAlive, port, "first", false); + await get(keepAlive, port, "reuse", false); + // Two requests at once: one reuses the free socket, one dials a second. + await Promise.all([get(keepAlive, port, "pair a", true), get(keepAlive, port, "pair b", true)]); + console.log("pair idle", keepAlive.totalSocketCount, freeCount(keepAlive)); + keepAlive.destroy(); + setTimeout(() => { + console.log("after destroy", keepAlive.totalSocketCount, freeCount(keepAlive)); + // Without keepAlive the socket closes after the response, and leaves + // the count before the request's 'close'. + const plain = new http.Agent(); + const req = http.get({ host: "127.0.0.1", port, agent: plain }, (res) => { + res.resume(); + res.on("end", () => console.log("plain end", plain.totalSocketCount)); + }); + console.log("plain created", plain.totalSocketCount); + req.on("close", () => { + console.log("plain request close", plain.totalSocketCount); + setTimeout(() => { + console.log("plain closed", plain.totalSocketCount); + server.close(); + }, 50); + }); + }, 50); +}); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index a79e12d05..e74058abb 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1945,6 +1945,7 @@ "5570-fs-cp-async.ts": 3348, "5571-fs-cp-deep.ts": 4817, "5585-record-results-inlined.ts": 2922, + "5593-http-agent-total-socket-count.ts": 3212, "5594-http-agent-header-limit.ts": 2622, "5595-net-ref-unref.ts": 2593, "5596-http-agent-keep-alive.ts": 3194, @@ -4990,6 +4991,7 @@ "5570-fs-cp-async.ts": 4176, "5571-fs-cp-deep.ts": 5306, "5585-record-results-inlined.ts": 3681, + "5593-http-agent-total-socket-count.ts": 3075, "5594-http-agent-header-limit.ts": 2691, "5595-net-ref-unref.ts": 3036, "5596-http-agent-keep-alive.ts": 3568, From 18033dc42dbb72f30b8e6b5b9ea061e277152329 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 17:45:49 -0700 Subject: [PATCH 087/120] Compile derived constructors whose super() call leads a comma statement Minifiers join a derived constructor's statements into one comma expression (`super(message, options), this.name = ..., ...;`), which jose 6.2.12 ships in its error classes. The statement lowering already runs each operand of a statement-level comma as its own statement, so super() is accepted when only parentheses and comma operators separate it from its expression statement. Every other expression position keeps the SC1090 refusal. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 20 +++- .../corpus/super-comma-jose-errors.js.json | 6 ++ .../corpus/super-comma-sequence-js.js.json | 6 ++ .../tests/corpus/super-comma-sequence.ts.json | 6 ++ .../diagnostics/super-comma-nested.ts.json | 6 ++ tests/corpus/super-comma-jose-errors.js | 84 +++++++++++++++ tests/corpus/super-comma-sequence-js.js | 49 +++++++++ tests/corpus/super-comma-sequence.ts | 101 ++++++++++++++++++ tests/diagnostics/super-comma-nested.ts | 24 +++++ .../__snapshots__/super-comma-nested.ts.txt | 27 +++++ 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json create mode 100644 tests/corpus/super-comma-jose-errors.js create mode 100644 tests/corpus/super-comma-sequence-js.js create mode 100644 tests/corpus/super-comma-sequence.ts create mode 100644 tests/diagnostics/super-comma-nested.ts create mode 100644 tests/harness/__snapshots__/super-comma-nested.ts.txt diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index cddc536c6..3838f067f 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7705,6 +7705,24 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca return out; } +/** Whether a super() call's value is discarded by its statement: the call + * is the expression statement itself, or an operand of a comma chain that + * is (`super(m, o), this.code = c;`, as minifiers join statements). The + * statement lowering runs each comma operand as its own statement in + * source order, so the chain lowers exactly like `super(m, o); this.code = + * c;`. Parentheses are transparent to both. */ +function superCallInStatementPosition(call: ts.CallExpression): boolean { + let node: ts.Node = call; + for (;;) { + const parent = node.parent; + if (ts.isExpressionStatement(parent)) return true; + const comma = + ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken; + if (!comma && !ts.isParenthesizedExpression(parent)) return false; + node = parent; + } +} + /** Super statements initialize the base and then the derived fields on * whichever control-flow path executes. Repeated calls are explicitly * refused until native layouts can allocate a second receiver. */ @@ -7722,7 +7740,7 @@ export function lowerDerivedCtorBody( const previousInitialized = context.superInitialized; context.superInitialized = initialized; context.superCall = (superCall) => { - if (!ts.isExpressionStatement(superCall.parent)) + if (!superCallInStatementPosition(superCall)) lowerer.unsupported("SC1090", superCall, "super() in an expression position"); superSeen = true; const loc = locOf(superCall); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json new file mode 100644 index 000000000..0ed94bfd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/super-comma-jose-errors.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json new file mode 100644 index 000000000..5698bfbed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/super-comma-sequence-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json new file mode 100644 index 000000000..3954c5766 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/super-comma-sequence.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json new file mode 100644 index 000000000..558b0ad78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/super-comma-nested.ts" + ], + "diags": [] +} diff --git a/tests/corpus/super-comma-jose-errors.js b/tests/corpus/super-comma-jose-errors.js new file mode 100644 index 000000000..a194fca93 --- /dev/null +++ b/tests/corpus/super-comma-jose-errors.js @@ -0,0 +1,84 @@ +// jose 6.2.12's error classes (dist/webapi/util/errors.js), verbatim in +// shape: minifiers join a derived constructor's statements into one comma +// expression that opens with super(...). Each comma operand runs as its +// own statement, so the base constructor and the class field initializers +// finish before the trailing assignments. +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} + +// The stack is read where the receiver's class is known: a read through an +// untyped parameter does not see the stack (a documented scriptc gap). +function describe(label, error, stack) { + console.log(label, error.name, error.code, JSON.stringify(error.message)); + console.log(" instanceof", error instanceof JOSEError, error instanceof Error); + console.log(" stack head", String(stack).split("\n")[0]); +} + +const generic = new JOSEError("generic failure"); +describe("generic", generic, generic.stack); +console.log(" static", JOSEError.code, "cause" in generic); + +const withCause = new JOSEError("wrapped", { cause: "root" }); +describe("wrapped", withCause, withCause.stack); +console.log(" cause", withCause.cause); + +const claims = { sub: "user", exp: 1 }; +const failed = new JWTClaimValidationFailed('"iss" claim mismatch', claims, "iss", "check_failed"); +describe("claim", failed, failed.stack); +console.log(" fields", failed.claim, failed.reason, failed.payload === claims); +console.log(" cause", failed.cause.claim, failed.cause.reason, failed.cause.payload === claims); +console.log(" chain", failed instanceof JWTClaimValidationFailed, failed instanceof JWTExpired); + +const defaults = new JWTClaimValidationFailed("defaults", claims); +console.log("defaults", defaults.claim, defaults.reason); + +const expired = new JWTExpired('"exp" claim timestamp check failed', claims, "exp"); +describe("expired", expired, expired.stack); +console.log(" static", JWTExpired.code, expired.reason); + +const alg = new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); +describe("alg", alg, alg.stack); +const sig = new JWSSignatureVerificationFailed(); +describe("sig", sig, sig.stack); + +try { + throw new JWTExpired("thrown", claims, "exp", "check_failed"); +} catch (error) { + if (error instanceof JOSEError) console.log("caught", error.name, error.code, error.claim); +} diff --git a/tests/corpus/super-comma-sequence-js.js b/tests/corpus/super-comma-sequence-js.js new file mode 100644 index 000000000..073bcd97d --- /dev/null +++ b/tests/corpus/super-comma-sequence-js.js @@ -0,0 +1,49 @@ +// The JavaScript side of super-comma-sequence.ts: a statement-level comma +// expression containing super(...) runs its operands in source order, with +// the base constructor and the derived field initializers at the super() +// call, as the separate statements a minifier joined would. +function log(label, value) { + console.log(" " + label); + return value; +} + +class Base { + constructor(origin) { + console.log(" Base body", origin); + this.origin = origin; + } +} + +class Joined extends Base { + first = log("field first", 1); + second = this.first + 1; + constructor(origin) { + super(log("super argument", origin)), log("after super", 0), this.tag = `${this.origin}:${this.second}`; + } +} + +class Leading extends Base { + value = log("field value", "v"); + constructor(origin) { + log("before super", 0), super(origin), log("after super " + this.value, 0); + } +} + +class Again extends Joined { + extra = log("field extra", "e"); + constructor() { + super("again"), this.tag = this.tag + "!", this.extra = this.extra + this.tag; + } +} + +console.log("joined:"); +const joined = new Joined("j"); +console.log(joined.origin, joined.first, joined.second, joined.tag); + +console.log("leading:"); +const leading = new Leading("l"); +console.log(leading.origin, leading.value); + +console.log("again:"); +const again = new Again(); +console.log(again.origin, again.tag, again.extra, again instanceof Joined, again instanceof Base); diff --git a/tests/corpus/super-comma-sequence.ts b/tests/corpus/super-comma-sequence.ts new file mode 100644 index 000000000..46e23a509 --- /dev/null +++ b/tests/corpus/super-comma-sequence.ts @@ -0,0 +1,101 @@ +// @transform-types +// super(...) as an operand of a statement-level comma expression lowers +// like the separate statements a minifier joined: operands run in source +// order, the base constructor and then the derived field initializers run +// at the super() call, and the trailing operands see the initialized +// instance. +function log(label: string, value: T): T { + console.log(" " + label); + return value; +} + +class Base { + readonly origin: string; + constructor(origin: string) { + console.log(" Base body", origin); + this.origin = origin; + } +} + +class Joined extends Base { + first = log("field first", 1); + second: number = this.first + 1; + tag: string; + constructor(origin: string) { + super(log("super argument", origin)), log("after super", 0), this.tag = `${this.origin}:${this.second}`; + } +} + +class Leading extends Base { + value = log("field value", "v"); + constructor(origin: string) { + log("before super", 0), super(origin), log("after super " + this.value, 0); + } +} + +class Parenthesized extends Base { + count = 0; + constructor() { + (super("paren"), this.count++, this.count++); + } +} + +class Nested extends Base { + steps: string[] = []; + constructor() { + super("nested"), (this.steps.push("a"), this.steps.push("b")), this.steps.push("c"); + } +} + +class Branching extends Base { + side: string; + constructor(left: boolean) { + if (left) super("left"), this.side = "L"; + else super("right"), this.side = "R"; + } +} + +class Point { + constructor( + public x: number, + public y: number, + ) {} +} + +class Point3 extends Point { + label = "p3"; + constructor( + x: number, + y: number, + public z: number, + ) { + super(x, y), console.log(" Point3", this.x, this.y, this.z, this.label); + } +} + +class Point4 extends Point3 { + w: number; + constructor(x: number, y: number, z: number, w: number) { + super(x, y, z), this.w = w, this.label = "p4"; + } +} + +console.log("joined:"); +const joined = new Joined("j"); +console.log(joined.origin, joined.first, joined.second, joined.tag); + +console.log("leading:"); +const leading = new Leading("l"); +console.log(leading.origin, leading.value); + +const paren = new Parenthesized(); +console.log("parenthesized:", paren.origin, paren.count); + +const nested = new Nested(); +console.log("nested:", nested.origin, nested.steps.join(",")); + +console.log("branching:", new Branching(true).side, new Branching(false).side); + +console.log("chain:"); +const p4 = new Point4(1, 2, 3, 4); +console.log(p4.x, p4.y, p4.z, p4.w, p4.label, p4 instanceof Point3, p4 instanceof Point); diff --git a/tests/diagnostics/super-comma-nested.ts b/tests/diagnostics/super-comma-nested.ts new file mode 100644 index 000000000..799436ff9 --- /dev/null +++ b/tests/diagnostics/super-comma-nested.ts @@ -0,0 +1,24 @@ +// super(...) as an operand of a statement-level comma expression compiles +// (super-comma-sequence.ts). Any other expression position still refuses: +// the field initializers that run at super() would be misordered. +class Base { + constructor(readonly origin: string) {} +} + +class Selected extends Base { + constructor(left: boolean) { + left ? super("left") : super("right"); + } +} + +function keep(value: T): T { + return value; +} + +class Argument extends Base { + constructor() { + keep((super("argument"), 1)); + } +} + +console.log(new Selected(true).origin, new Argument().origin); diff --git a/tests/harness/__snapshots__/super-comma-nested.ts.txt b/tests/harness/__snapshots__/super-comma-nested.ts.txt new file mode 100644 index 000000000..e7b7f275a --- /dev/null +++ b/tests/harness/__snapshots__/super-comma-nested.ts.txt @@ -0,0 +1,27 @@ +super-comma-nested.ts:9:3 - error SC1090: derived constructors without a lowered super() call is not supported yet + + 8 | class Selected extends Base { + 9 | constructor(left: boolean) { + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | left ? super("left") : super("right"); + +super-comma-nested.ts:10:12 - error SC1090: super() in an expression position is not supported yet + + 9 | constructor(left: boolean) { + 10 | left ? super("left") : super("right"); + | ^~~~~~~~~~~~~ + 11 | } + +super-comma-nested.ts:19:3 - error SC1090: derived constructors without a lowered super() call is not supported yet + + 18 | class Argument extends Base { + 19 | constructor() { + | ^~~~~~~~~~~~~~~ + 20 | keep((super("argument"), 1)); + +super-comma-nested.ts:20:11 - error SC1090: super() in an expression position is not supported yet + + 19 | constructor() { + 20 | keep((super("argument"), 1)); + | ^~~~~~~~~~~~~~~~~ + 21 | } \ No newline at end of file From 705c197a713407c19d986656cbe65df3b17ff1fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:20:05 -0500 Subject: [PATCH 088/120] Format Dates with Node's local time zone names Date.prototype.toString, toDateString and toTimeString, String(date), template literals and string concatenation print local time with the GMT offset and the zone's long name, as Node does. The runtime identifies the host zone from TZ or /etc/localtime, again after each write to process.env.TZ, and looks its name up in a table generated from Node's ICU data. Dates held in untyped values convert to strings with toString() and to numbers with their time value. --- .../content/limitations/builtin-apis/date.mdx | 2 +- .../limitations/dates-and-timezones.mdx | 2 + .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/frontend/lowering/builtins/dates.ts | 12 +- .../src/frontend/lowering/lower-exprs.ts | 4 + .../src/frontend/lowering/surfaces.ts | 19 + .../compiler/src/ir/builtin-signatures.ts | 6 + packages/compiler/surface-manifest.json | 19 + .../tests/corpus/5963-date-to-string.ts.json | 6 + .../5964-date-dynamic-conversions.js.json | 6 + packages/runtime/src/scr_json.c | 35 +- packages/runtime/src/scr_lib.c | 381 +++- packages/runtime/src/scr_number.h | 9 + packages/runtime/src/scr_timezone_data.h | 1555 +++++++++++++++++ scripts/gen-timezone-names.mjs | 292 ++++ tests/corpus/5963-date-to-string.ts | 93 + tests/corpus/5964-date-dynamic-conversions.js | 29 + tests/diagnostics/date-formatters.ts | 2 +- .../__snapshots__/date-formatters.ts.txt | 12 +- .../harness/__snapshots__/stdlib-fence.ts.txt | 2 +- tests/harness/corpus-costs.json | 4 + 21 files changed, 2476 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json create mode 100644 packages/runtime/src/scr_timezone_data.h create mode 100644 scripts/gen-timezone-names.mjs create mode 100644 tests/corpus/5963-date-to-string.ts create mode 100644 tests/corpus/5964-date-dynamic-conversions.js diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index db5278c53..81d37ebc3 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1 +1 @@ -- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions. +- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, `toUTCString`, and locale formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions. diff --git a/docs/content/limitations/dates-and-timezones.mdx b/docs/content/limitations/dates-and-timezones.mdx index ad830eb9a..e88cfb462 100644 --- a/docs/content/limitations/dates-and-timezones.mdx +++ b/docs/content/limitations/dates-and-timezones.mdx @@ -1,3 +1,5 @@ String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs. Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms. Local calendar getters use the operating system's timezone database. Historical results can differ from Node.js when timezone data differs. For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule. + +`toString`, `toTimeString`, `String(date)` and template literals print the zone's long name after the GMT offset, as Node does (`GMT-0500 (Central Daylight Time)`). scriptc identifies the zone the way Node does, from `TZ` when it names a zone, else from `/etc/localtime`. Outside Windows, a write to `process.env.TZ` changes the zone for later conversions and getters. The names are the en-US names for the IANA zones; Node prints them in the language of the locale that `LC_ALL`, `LC_MESSAGES` or `LANG` selects, so with a non-English locale the name differs from Node's. A `TZ` value that is a file path or a POSIX rule string (such as `EST5EDT,M3.2.0,M11.1.0`) gives offsets and names that differ from Node's, which ignores those forms. On Windows, the name is the `GMT+hh:mm` form unless `TZ` names a zone. On `wasm32-wasi`, local time is UTC and the name is `Coordinated Universal Time`. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 67cb77047..87acd391d 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -325,6 +325,9 @@ export const LIB_FN_SYMS: Record = { "date.getMilliseconds": "scr_date_get_milliseconds", "date.getUTCMilliseconds": "scr_date_get_milliseconds", "date.getTimezoneOffset": "scr_date_get_timezone_offset", + "date.toString": "scr_date_to_string", + "date.toDateString": "scr_date_to_date_string", + "date.toTimeString": "scr_date_to_time_string", "fs.existsSync": "scr_fs_exists", // ── the throwing slice (MAY_THROW_LIB_FNS members): the generic path // emits the standard pending check after each — exprs.ts's finish. diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index fa880bdc9..896eeaf28 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -33,8 +33,14 @@ const DATE_GETTER_FNS: Readonly> = { getTimezoneOffset: "date.getTimezoneOffset", }; +const DATE_TEXT_FNS: ReadonlyMap = new Map([ + ["toString", "date.toString"], + ["toDateString", "date.toDateString"], + ["toTimeString", "date.toTimeString"], +]); + const DATE_METHOD_HINT = - "getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering"; + "getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering"; /** The Date slice: statics plus read-only Date values backed by one * TimeClip'd millisecond scalar. Construction lives in lowerNew; @@ -117,6 +123,10 @@ export function lowerDateCall( if (fn !== undefined) { return { kind: "libCall", fn, args: [receiver], type: F64, loc }; } + const textFn = DATE_TEXT_FNS.get(name); + if (textFn !== undefined) { + return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc }; + } lowerer.noLowering( `Date.prototype.${name}`, call, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d72faa936..a19c8e4ae 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -8863,6 +8863,10 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr if (e.type.kind === "string") return e; if (e.type.kind === "url") return { kind: "libCall", fn: "url.href", args: [e], type: STRING, loc: e.loc }; + // String(date) / `${date}` / "" + date: Date's ToPrimitive prefers + // toString(). + if (e.type.kind === "date") + return { kind: "libCall", fn: "date.toString", args: [e], type: STRING, loc: e.loc }; if (e.type.kind === "regex") { return { kind: "regexIntrinsic", diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44ed37e3c..0dead1c35 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2321,6 +2321,25 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ name: "Date.prototype.getTimezoneOffset", fns: ["date.getTimezoneOffset"], }, + { + id: "stdlib.date.toString", + kind: "stdlib", + name: "Date.prototype.toString", + fns: ["date.toString"], + note: "local time with the host zone's long English name; String(date) and template literals use it", + }, + { + id: "stdlib.date.toDateString", + kind: "stdlib", + name: "Date.prototype.toDateString", + fns: ["date.toDateString"], + }, + { + id: "stdlib.date.toTimeString", + kind: "stdlib", + name: "Date.prototype.toTimeString", + fns: ["date.toTimeString"], + }, // ── perf_hooks (lowerPerfHooksCall): the monotonic clock. { id: "node-builtin.perf_hooks.performance.now", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e61bb2594..a87e40659 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4208,6 +4208,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, + /** Date.prototype.toString / toDateString / toTimeString and String(date): + * V8's local-time formats with the host zone's long name, "Invalid Date" + * for an invalid date. Owned (+1) string; never throws. */ + "date.toString": { argTypes: [DATE_T], result: STRING }, + "date.toDateString": { argTypes: [DATE_T], result: STRING }, + "date.toTimeString": { argTypes: [DATE_T], result: STRING }, /** The composed `new Date(dateString).getTime()` read: one borrowed * string, f64 milliseconds since epoch. The parsed grammar is BOUNDED * (documented divergence — V8's parser accepts far more): the ASN.1 diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index a167254c2..637ecc2a0 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4209,6 +4209,12 @@ "status": "static", "note": "one date string, using the bounded parser shared with new Date(dateString).getTime()" }, + { + "id": "stdlib.date.toDateString", + "kind": "stdlib", + "name": "Date.prototype.toDateString", + "status": "static" + }, { "id": "stdlib.date.toISOString", "kind": "stdlib", @@ -4216,6 +4222,19 @@ "status": "static", "note": "UTC ISO formatting over constructed and stored Date values" }, + { + "id": "stdlib.date.toString", + "kind": "stdlib", + "name": "Date.prototype.toString", + "status": "static", + "note": "local time with the host zone's long English name; String(date) and template literals use it" + }, + { + "id": "stdlib.date.toTimeString", + "kind": "stdlib", + "name": "Date.prototype.toTimeString", + "status": "static" + }, { "id": "stdlib.date.valueOf", "kind": "stdlib", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json new file mode 100644 index 000000000..b6743d677 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5963-date-to-string.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json new file mode 100644 index 000000000..1609e844d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5964-date-dynamic-conversions.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 401933904..f1814e914 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -6805,6 +6805,15 @@ static bool scr_dyn_error_encoding(const ScrDyn *d) { return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL; } +static bool scr_dyn_is_date(const ScrDyn *d) { + return d->kind == SCR_DYN_HANDLE && d->v.handle.tag == SCR_DYNH_DATE; +} + +/* A Date's valueOf(): its time value as a number. +1. */ +static ScrDyn *scr_dyn_date_value_of(const ScrDyn *d) { + return scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "valueOf", NULL, 0, "valueOf"); +} + /* Receiver-kind-dispatched toString() on a checked-dynamic value (the * dyn method surface — a stream's 'data'/for-await chunk is the common * receiver): bytes decode per the encoding (Node's Buffer.toString, @@ -6843,7 +6852,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { if (scr_dyn_error_encoding(d) && !scr_dyn_property_owner(d, "toString", 8)) return scr_dyn_error_string(d); return scr_str_new("[object Object]", 15); case SCR_DYN_HANDLE: - if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL) { + if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL || d->v.handle.tag == SCR_DYNH_DATE) { ScrDyn *text = scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "toString", NULL, 0, "RegExp.toString"); if (!text) return NULL; ScrStr *out = scr_str_retain(text->v.str); @@ -7267,6 +7276,14 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) { return false; } default: { + /* A Date's valueOf answers its time value. */ + if (scr_dyn_is_date(d)) { + ScrDyn *time = scr_dyn_date_value_of(d); + if (!time) return false; + *out = time->v.num; + scr_dyn_release(time); + return true; + } /* Arrays, byte views, functions, promises, and native handles inherit a * valueOf that returns the receiver, then use their existing JS-exact * string rendering for the numeric conversion. */ @@ -7287,8 +7304,14 @@ double scr_dyn_number_coerce(const ScrDyn *d) { static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d); +/* ToPrimitive with the number hint, which differs from the default hint + * only for a Date: its time value. */ +static ScrDyn *scr_dyn_number_primitive(const ScrDyn *d) { + return scr_dyn_is_date(d) ? scr_dyn_date_value_of(d) : scr_dyn_add_primitive(d); +} + double scr_dyn_number_constructor(const ScrDyn *d) { - ScrDyn *primitive = scr_dyn_add_primitive(d); + ScrDyn *primitive = scr_dyn_number_primitive(d); if (!primitive) return NAN; double result = primitive->kind == SCR_DYN_BIGINT ? scr_bigint_to_f64(primitive->v.bigint) : scr_dyn_number_coerce(primitive); @@ -7297,7 +7320,7 @@ double scr_dyn_number_constructor(const ScrDyn *d) { } static ScrBigInt *scr_dyn_bigint_convert(const ScrDyn *d, bool allow_number) { - ScrDyn *primitive = scr_dyn_add_primitive(d); + ScrDyn *primitive = scr_dyn_number_primitive(d); if (!primitive) return NULL; ScrBigInt *result = NULL; switch (primitive->kind) { @@ -7430,7 +7453,7 @@ ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right) { } ScrDyn *scr_dyn_to_numeric(const ScrDyn *value) { - ScrDyn *primitive = scr_dyn_add_primitive(value); + ScrDyn *primitive = scr_dyn_number_primitive(value); if (!primitive) return NULL; if (primitive->kind == SCR_DYN_BIGINT) return primitive; double number; @@ -7454,9 +7477,9 @@ ScrDyn *scr_dyn_increment(const ScrDyn *value, bool increment) { bool scr_dyn_compare(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation) { // ToPrimitive is observable and always visits the original left operand // first, including the > and <= spellings of relational comparison. - ScrDyn *l = scr_dyn_add_primitive(left); + ScrDyn *l = scr_dyn_number_primitive(left); if (!l) return false; - ScrDyn *r = scr_dyn_add_primitive(right); + ScrDyn *r = scr_dyn_number_primitive(right); if (!r) { scr_dyn_release(l); return false; } double order = 2; if (l->kind == SCR_DYN_STR && r->kind == SCR_DYN_STR) { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index af57ab057..1d45de4d4 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10682,6 +10682,9 @@ double scr_date_utc(double y, double mo, double d, typedef struct ScrDateParts { long long year; int month, date, day, hours, minutes, seconds, milliseconds; + /* Local fields: tm_isdst at the instant (or its surrogate). It fills the + * padding before timezone_offset, so the struct keeps its size. */ + bool isdst; double timezone_offset; } ScrDateParts; @@ -10718,6 +10721,7 @@ static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) { out->seconds = (int)(msday / 1000 % 60); out->milliseconds = (int)(msday % 1000); out->timezone_offset = 0; + out->isdst = false; } static bool scr_date_utc_parts(double ms, ScrDateParts *out) { @@ -10801,6 +10805,7 @@ static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out) { scr_date_utc_parts_unchecked(clipped + local_offset * 1000.0, out); double timezone_offset = trunc(-local_offset / 60.0); out->timezone_offset = timezone_offset == 0 ? 0 : timezone_offset; + out->isdst = tmv.tm_isdst > 0; return true; } @@ -10840,6 +10845,364 @@ double scr_date_get_timezone_offset(double ms) { return scr_date_local_parts(ms, &p) ? p.timezone_offset : NAN; } +/* ── Date time zone names ─────────────────────────────────────────────── + * Long time zone names for Date.prototype.toString and toTimeString. + * + * Node prints "GMT-0500 (Central Daylight Time)": the name comes from ICU + * for the host zone, in standard or daylight form. ICU identifies the host + * zone from TZ when it looks like a zone ID, else from /etc/localtime (the + * symlink's target, or the zoneinfo file with the same contents), else + * from the libc zone abbreviation. The runtime follows the same steps and + * looks the ID up in the names Node prints for each tzdata zone + * (scr_timezone_data.h, generated). An ID without a row prints Node's + * fallback for a zone without a name, "GMT+hh:mm" with the zone's standard + * offset. + * + * Whether an instant is in daylight time follows V8: instants before 1970 + * or after 2038-01-19 use the instant with the same month, day and time in + * an equivalent year between 2008 and 2037 (same leap-year status and + * weekday of January 1), and tm_isdst decides. A few zones carry Node's + * own daylight-time changes instead: those whose tzdata rules use a + * negative daylight saving (Europe/Dublin's winter time, Morocco's Ramadan + * time), where ICU's data calls the larger offset daylight time, and those + * Node shows in daylight time through 2038 (British Columbia's permanent + * UTC-7, which tzdata names standard time). */ +#include "scr_timezone_data.h" + +#define SCR_TZ_ROWS (sizeof scr_tz_names / sizeof scr_tz_names[0]) + +/* Text that no other code in this file uses lives in named arrays: string + * literals would join the merged read-only strings of every program that + * links this file. */ +static const char scr_tz_posix_ids[4][8] = { "PST8PDT", "MST7MDT", "CST6CDT", "EST5EDT" }; +static const char scr_tz_prefixes[2][7] = { "posix/", "right/" }; +static const char scr_tz_env_name[] = "TZ"; +static const char scr_tz_utc[] = "UTC"; +static const char scr_tz_localtime_path[] = "/etc/localtime"; +static const char scr_tz_zoneinfo_tail[] = "/zoneinfo/"; +static const char scr_tz_zoneinfo_dir[] = "/usr/share/zoneinfo/"; +static const char scr_tz_skipped[4][11] = { ".", "..", "posixrules", "localtime" }; +static const char scr_tz_fallback_format[] = "GMT%c%02lld:%02lld"; +static const char scr_date_invalid_text[] = "Invalid Date"; + +/* The thread's host zone, detected again after a write to process.env.TZ + * (scr_date_tz_generation). */ +typedef struct { + unsigned generation; + const ScrTzNames *row; /* NULL: no row, use the fallback */ + bool resolved; + bool fallback_ready; + char fallback[16]; /* "GMT+hh:mm" */ +} ScrTzState; + +static SCR_TL ScrTzState scr_tz_state; + +/* The local UTC offset (seconds) and tm_isdst at an epoch second. */ +static bool scr_tz_offset_at(double sec, double *offset, bool *isdst) { + struct tm tmv; + if (!scr_date_localtime(sec, &tmv)) return false; + double local = + scr_days_from_civil((long long)tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday) * 86400.0 + + tmv.tm_hour * 3600.0 + tmv.tm_min * 60.0 + tmv.tm_sec; + *offset = local - sec; + *isdst = tmv.tm_isdst > 0; + return true; +} + +/* ICU's isValidOlsonID: letters and punctuation up to an optional run of at + * most two digits ending the string ("GMT+11"), or one of the four POSIX + * zone IDs tzdata also names. */ +static bool scr_tz_valid_id(const char *id) { + size_t i = 0; + while (id[i] && !(id[i] >= '0' && id[i] <= '9') && id[i] != ',') i++; + size_t max = i + 2; + while (id[i] && id[i] >= '0' && id[i] <= '9' && i < max) i++; + if (id[i] == 0) return true; + for (size_t k = 0; k < 4; k++) { + if (!strcmp(id, scr_tz_posix_ids[k])) return true; + } + return false; +} + +static const char *scr_tz_skip_prefix(const char *id) { + return !strncmp(id, scr_tz_prefixes[0], 6) || !strncmp(id, scr_tz_prefixes[1], 6) ? id + 6 : id; +} + +static const ScrTzNames *scr_tz_find(const char *id) { + size_t lo = 0, hi = SCR_TZ_ROWS; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int c = strcmp(id, scr_tz_ids + scr_tz_names[mid].id); + if (c == 0) return &scr_tz_names[mid]; + if (c < 0) hi = mid; + else lo = mid + 1; + } + return NULL; +} + +#if !defined(_WIN32) && !defined(__wasi__) + +/* The bytes of a file, or NULL. */ +static char *scr_tz_read_file(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + size_t cap = 4096, n = 0; + char *data = malloc(cap); + while (data) { + n += fread(data + n, 1, cap - n, f); + if (n < cap) break; + char *grown = realloc(data, cap * 2); + if (!grown) { + free(data); + data = NULL; + break; + } + data = grown; + cap *= 2; + } + fclose(f); + *len = n; + return data; +} + +static bool scr_tz_same_file(const char *path, const char *data, size_t len) { + struct stat st; + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode) || (size_t)st.st_size != len) return false; + size_t other_len; + char *other = scr_tz_read_file(path, &other_len); + bool same = other && other_len == len && memcmp(other, data, len) == 0; + free(other); + return same; +} + +/* ICU's search for a zoneinfo file with /etc/localtime's contents, in + * directory order, skipping "posixrules" and "localtime". Writes the + * path below the zoneinfo directory into `out`. */ +static bool scr_tz_search(char *dir, size_t dir_len, const char *data, size_t len, char *out, size_t cap) { + DIR *d = opendir(dir); + if (!d) return false; + bool found = false; + struct dirent *entry; + while (!found && (entry = readdir(d)) != NULL) { + const char *name = entry->d_name; + bool skipped = false; + for (size_t k = 0; k < 4; k++) skipped = skipped || !strcmp(name, scr_tz_skipped[k]); + if (skipped) continue; + size_t name_len = strlen(name); + if (dir_len + name_len + 2 > PATH_MAX) continue; + memcpy(dir + dir_len, name, name_len + 1); + DIR *sub = opendir(dir); + if (sub) { + closedir(sub); + dir[dir_len + name_len] = '/'; + dir[dir_len + name_len + 1] = 0; + found = scr_tz_search(dir, dir_len + name_len + 1, data, len, out, cap); + } else if (scr_tz_same_file(dir, data, len)) { + const char *id = scr_tz_skip_prefix(dir + strlen(scr_tz_zoneinfo_dir)); + size_t id_len = strlen(id); + if (id_len < cap) { + memcpy(out, id, id_len + 1); + found = true; + } + } + dir[dir_len] = 0; + } + closedir(d); + return found; +} +#endif + +/* The host zone's row, or NULL when ICU would not find a zone name. */ +static const ScrTzNames *scr_tz_detect(void) { +#if defined(__wasi__) + /* WASI's libc keeps every local time in UTC. */ + return scr_tz_find(scr_tz_utc); +#else + const char *tz = getenv(scr_tz_env_name); + if (tz && scr_tz_valid_id(tz)) { + if (tz[0] == ':') tz++; + return scr_tz_find(scr_tz_skip_prefix(tz)); + } +#ifdef _WIN32 + return NULL; +#else + /* The zone ID is the path below a zoneinfo directory of the + * /etc/localtime symlink's target, or of a zoneinfo file with the same + * contents when /etc/localtime is a regular file. */ + char path[PATH_MAX]; + if (realpath(scr_tz_localtime_path, path) && strcmp(path, scr_tz_localtime_path) != 0) { + const char *tail = strstr(path, scr_tz_zoneinfo_tail); + if (tail) { + tail = scr_tz_skip_prefix(tail + strlen(scr_tz_zoneinfo_tail)); + if (scr_tz_valid_id(tail)) return scr_tz_find(tail); + } + } else { + size_t len; + DIR *zoneinfo = opendir(scr_tz_zoneinfo_dir); + char *data = zoneinfo ? scr_tz_read_file(scr_tz_localtime_path, &len) : NULL; + if (zoneinfo) closedir(zoneinfo); + if (data) { + char dir[PATH_MAX]; + memcpy(dir, scr_tz_zoneinfo_dir, sizeof scr_tz_zoneinfo_dir); + char id[PATH_MAX]; + bool found = scr_tz_search(dir, strlen(dir), data, len, id, sizeof id); + free(data); + if (found && scr_tz_valid_id(id)) return scr_tz_find(id); + } + } + /* Otherwise the standard-time abbreviation names the zone ("UTC" when + * there is no /etc/localtime). */ + tzset(); + return scr_tz_find(tzname[0]); +#endif +#endif +} + +static ScrTzState *scr_tz_current(void) { + ScrTzState *s = &scr_tz_state; + unsigned generation = scr_date_tz_generation; + if (s->generation != generation || !s->resolved) { + s->generation = generation; + s->row = scr_tz_detect(); + s->resolved = true; + s->fallback_ready = false; + } + return s; +} + +/* "GMT+hh:mm" for the zone's current standard offset: the name Node prints + * for a zone ID it does not know. */ +static const char *scr_tz_fallback(ScrTzState *s) { + if (!s->fallback_ready) { + double offset = 0; + bool isdst = false; + long long seconds = 0; + if (scr_tz_offset_at((double)time(NULL), &offset, &isdst)) seconds = (long long)offset - (isdst ? 3600 : 0); + long long minutes = (seconds < 0 ? -seconds : seconds) / 60; + snprintf(s->fallback, sizeof s->fallback, scr_tz_fallback_format, seconds < 0 ? '-' : '+', + minutes / 60, minutes % 60); + s->fallback_ready = true; + } + return s->fallback; +} + +/* V8's EquivalentTime: the same month, day and time of day in a year + * between 2008 and 2037 with the same leap-year status and weekday of + * January 1. */ +static double scr_tz_equivalent_time(double ms) { + ScrDateParts p; + scr_date_utc_parts_unchecked(ms, &p); + double within = ms - floor(ms / 86400000.0) * 86400000.0; + long long year = p.year; + bool leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; + long long week_day = ((long long)scr_days_from_civil(year, 1, 1) + 4) % 7; + if (week_day < 0) week_day += 7; + long long recent = (leap ? 1956 : 1967) + (week_day * 12) % 28; + long long equivalent = 2008 + (recent + 3 * 28 - 2008) % 28; + return (scr_days_from_civil(equivalent, p.month + 1, 1) + p.date - 1) * 86400000.0 + within; +} + +static bool scr_tz_is_daylight(double ms, int isdst_hint, const ScrTzNames *row) { + /* V8 names instants in [0, 2^31 - 1] seconds directly. */ + if (ms < 0 || (long long)ms > 2147483647000LL) { + ms = scr_tz_equivalent_time(ms); + isdst_hint = -1; + } + double sec = floor(ms / 1000.0); + if (row->change_count > 0) { + /* Daylight time after an odd number of changes. */ + const int32_t *changes = scr_tz_daylight_changes + row->first_change; + size_t lo = 0, hi = row->change_count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + if (changes[mid] <= sec) lo = mid + 1; + else hi = mid; + } + return (lo & 1) != 0; + } + if (isdst_hint >= 0) return isdst_hint != 0; + double offset; + bool isdst; + return scr_tz_offset_at(sec, &offset, &isdst) && isdst; +} + +const char *scr_date_zone_name(double ms, int isdst) { + ScrTzState *s = scr_tz_current(); + if (!s->row) return scr_tz_fallback(s); + bool daylight = scr_tz_is_daylight(ms, isdst, s->row); + return scr_tz_name_text + (daylight ? s->row->daylight : s->row->standard); +} + +/* Date.prototype.toString, toDateString and toTimeString: V8's formats over + * the local calendar fields ("Sat Oct 18 2026 09:12:00 GMT-0500 (Central + * Daylight Time)"), with years before 1 as "-0001", and "Invalid Date" for + * an invalid date. */ +enum { SCR_DATE_TEXT_FULL, SCR_DATE_TEXT_DATE, SCR_DATE_TEXT_TIME }; + +/* `value` (>= 0) in decimal, zero-padded to `width` digits. */ +static char *scr_date_put_digits(char *p, long long value, int width) { + char digits[24]; + int n = 0; + do { + digits[n++] = (char)('0' + value % 10); + value /= 10; + } while (value > 0); + while (n < width) digits[n++] = '0'; + while (n > 0) *p++ = digits[--n]; + return p; +} + +static ScrStr *scr_date_text(double ms, int mode) { + static const char week_days[7][3] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + static const char months[12][3] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + ScrDateParts p; + if (!scr_date_local_parts(ms, &p)) return scr_str_new(scr_date_invalid_text, sizeof scr_date_invalid_text - 1); + char buf[160]; + char *o = buf; + if (mode != SCR_DATE_TEXT_TIME) { + memcpy(o, week_days[p.day], 3); + o[3] = ' '; + memcpy(o + 4, months[p.month], 3); + o[7] = ' '; + o = scr_date_put_digits(o + 8, p.date, 2); + *o++ = ' '; + /* "%04d", or "%05d" (a sign and four digits) before year 0. */ + if (p.year < 0) *o++ = '-'; + o = scr_date_put_digits(o, p.year < 0 ? -p.year : p.year, 4); + } + if (mode == SCR_DATE_TEXT_FULL) *o++ = ' '; + if (mode != SCR_DATE_TEXT_DATE) { + int offset = -(int)p.timezone_offset; + int magnitude = offset < 0 ? -offset : offset; + o = scr_date_put_digits(o, p.hours, 2); + *o++ = ':'; + o = scr_date_put_digits(o, p.minutes, 2); + *o++ = ':'; + o = scr_date_put_digits(o, p.seconds, 2); + *o++ = ' '; + *o++ = 'G'; + *o++ = 'M'; + *o++ = 'T'; + *o++ = offset < 0 ? '-' : '+'; + o = scr_date_put_digits(o, magnitude / 60, 2); + o = scr_date_put_digits(o, magnitude % 60, 2); + *o++ = ' '; + *o++ = '('; + /* Zone names are at most 64 bytes (scr_timezone_data.h). */ + const char *zone = scr_date_zone_name(ms, p.isdst); + size_t zone_len = strlen(zone); + memcpy(o, zone, zone_len); + o += zone_len; + *o++ = ')'; + } + return scr_str_new(buf, (size_t)(o - buf)); +} + +ScrStr *scr_date_to_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_FULL); } +ScrStr *scr_date_to_date_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_DATE); } +ScrStr *scr_date_to_time_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_TIME); } + /* ── Number statics ──────────────────────────────────────────────────── * JS-exact: the ES2015 Number statics never coerce (unlike the global * isNaN/isFinite), and the compiler routes only number-typed arguments @@ -11176,11 +11539,25 @@ double scr_dyn_native_date_value(const ScrDyn *value) { return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; } +/* Method names without another use in this file, in named arrays (see + * scr_tz_posix_ids). */ +static const char scr_date_to_date_string_name[] = "toDateString"; +static const char scr_date_to_time_string_name[] = "toTimeString"; + static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)self; (void)args; (void)argc; (void)what; double ms = ((ScrNativeDate *)ptr)->milliseconds; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); + if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || + !strcmp(method, scr_date_to_time_string_name)) { + ScrStr *text = !strcmp(method, "toString") ? scr_date_to_string(ms) + : !strcmp(method, scr_date_to_date_string_name) ? scr_date_to_date_string(ms) + : scr_date_to_time_string(ms); + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; + } if (!strcmp(method, "toISOString") || !strcmp(method, "toJSON")) { if (!strcmp(method, "toJSON") && !isfinite(ms)) return scr_dyn_new_null(); ScrStr *text = scr_date_to_iso(ms); @@ -11228,7 +11605,9 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { /* The Date.prototype methods scr_native_date_invoke answers. */ static const char *const scr_native_date_methods[] = { - "getTime", "valueOf", "toISOString", "toJSON", "getFullYear", "getUTCFullYear", "getMonth", + "getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name, + scr_date_to_time_string_name, + "getFullYear", "getUTCFullYear", "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", "getUTCMilliseconds", "getTimezoneOffset", diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index f980daeb5..ac96db8e7 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -120,6 +120,15 @@ double scr_date_get_minutes(double ms, bool utc); double scr_date_get_seconds(double ms, bool utc); double scr_date_get_milliseconds(double ms); double scr_date_get_timezone_offset(double ms); +/* Date.prototype.toString, toDateString and toTimeString with V8's + * formats; "Invalid Date" for NaN. +1. */ +ScrStr *scr_date_to_string(double ms); +ScrStr *scr_date_to_date_string(double ms); +ScrStr *scr_date_to_time_string(double ms); +/* The long time zone name Node prints after the GMT offset for the host + * zone at time value `ms`; `isdst` is tm_isdst at `ms`, or -1 when not + * known. Borrowed; valid until the next call on the same thread. */ +const char *scr_date_zone_name(double ms, int isdst); /* One-argument ABI wrappers used by the LLVM lib-call table. */ double scr_date_get_full_year_local(double ms); double scr_date_get_full_year_utc(double ms); diff --git a/packages/runtime/src/scr_timezone_data.h b/packages/runtime/src/scr_timezone_data.h new file mode 100644 index 000000000..3503b84f9 --- /dev/null +++ b/packages/runtime/src/scr_timezone_data.h @@ -0,0 +1,1555 @@ +/* Generated by scripts/gen-timezone-names.mjs from Node v24.22.0 + * (ICU 78.3, CLDR 48.0) and tzdata 2026c. Do not edit. + * Each IANA zone or link ID maps to the long English names Node prints for + * its standard and daylight time. A row with change_count > 0 takes + * daylight time from scr_tz_daylight_changes[first_change ...]: the epoch + * seconds at which daylight time starts or ends between 1970 and + * 2038-01-19 (INT32_MIN first when 1970 starts in daylight time). Rows are + * sorted by ID. */ +#ifndef SCR_TIMEZONE_DATA_H +#define SCR_TIMEZONE_DATA_H +#include +typedef struct { uint16_t id, standard, daylight, first_change, change_count; } ScrTzNames; +static const char scr_tz_ids[] = + "Africa/Abidjan\0" + "Africa/Accra\0" + "Africa/Addis_Ababa\0" + "Africa/Algiers\0" + "Africa/Asmara\0" + "Africa/Asmera\0" + "Africa/Bamako\0" + "Africa/Bangui\0" + "Africa/Banjul\0" + "Africa/Bissau\0" + "Africa/Blantyre\0" + "Africa/Brazzaville\0" + "Africa/Bujumbura\0" + "Africa/Cairo\0" + "Africa/Casablanca\0" + "Africa/Ceuta\0" + "Africa/Conakry\0" + "Africa/Dakar\0" + "Africa/Dar_es_Salaam\0" + "Africa/Djibouti\0" + "Africa/Douala\0" + "Africa/El_Aaiun\0" + "Africa/Freetown\0" + "Africa/Gaborone\0" + "Africa/Harare\0" + "Africa/Johannesburg\0" + "Africa/Juba\0" + "Africa/Kampala\0" + "Africa/Khartoum\0" + "Africa/Kigali\0" + "Africa/Kinshasa\0" + "Africa/Lagos\0" + "Africa/Libreville\0" + "Africa/Lome\0" + "Africa/Luanda\0" + "Africa/Lubumbashi\0" + "Africa/Lusaka\0" + "Africa/Malabo\0" + "Africa/Maputo\0" + "Africa/Maseru\0" + "Africa/Mbabane\0" + "Africa/Mogadishu\0" + "Africa/Monrovia\0" + "Africa/Nairobi\0" + "Africa/Ndjamena\0" + "Africa/Niamey\0" + "Africa/Nouakchott\0" + "Africa/Ouagadougou\0" + "Africa/Porto-Novo\0" + "Africa/Sao_Tome\0" + "Africa/Timbuktu\0" + "Africa/Tripoli\0" + "Africa/Tunis\0" + "Africa/Windhoek\0" + "America/Adak\0" + "America/Anchorage\0" + "America/Anguilla\0" + "America/Antigua\0" + "America/Araguaina\0" + "America/Argentina/Buenos_Aires\0" + "America/Argentina/Catamarca\0" + "America/Argentina/ComodRivadavia\0" + "America/Argentina/Cordoba\0" + "America/Argentina/Jujuy\0" + "America/Argentina/La_Rioja\0" + "America/Argentina/Mendoza\0" + "America/Argentina/Rio_Gallegos\0" + "America/Argentina/Salta\0" + "America/Argentina/San_Juan\0" + "America/Argentina/San_Luis\0" + "America/Argentina/Tucuman\0" + "America/Argentina/Ushuaia\0" + "America/Aruba\0" + "America/Asuncion\0" + "America/Atikokan\0" + "America/Atka\0" + "America/Bahia\0" + "America/Bahia_Banderas\0" + "America/Barbados\0" + "America/Belem\0" + "America/Belize\0" + "America/Blanc-Sablon\0" + "America/Boa_Vista\0" + "America/Bogota\0" + "America/Boise\0" + "America/Buenos_Aires\0" + "America/Cambridge_Bay\0" + "America/Campo_Grande\0" + "America/Cancun\0" + "America/Caracas\0" + "America/Catamarca\0" + "America/Cayenne\0" + "America/Cayman\0" + "America/Chicago\0" + "America/Chihuahua\0" + "America/Ciudad_Juarez\0" + "America/Coral_Harbour\0" + "America/Cordoba\0" + "America/Costa_Rica\0" + "America/Coyhaique\0" + "America/Creston\0" + "America/Cuiaba\0" + "America/Curacao\0" + "America/Danmarkshavn\0" + "America/Dawson\0" + "America/Dawson_Creek\0" + "America/Denver\0" + "America/Detroit\0" + "America/Dominica\0" + "America/Edmonton\0" + "America/Eirunepe\0" + "America/El_Salvador\0" + "America/Ensenada\0" + "America/Fort_Nelson\0" + "America/Fort_Wayne\0" + "America/Fortaleza\0" + "America/Glace_Bay\0" + "America/Godthab\0" + "America/Goose_Bay\0" + "America/Grand_Turk\0" + "America/Grenada\0" + "America/Guadeloupe\0" + "America/Guatemala\0" + "America/Guayaquil\0" + "America/Guyana\0" + "America/Halifax\0" + "America/Havana\0" + "America/Hermosillo\0" + "America/Indiana/Indianapolis\0" + "America/Indiana/Knox\0" + "America/Indiana/Marengo\0" + "America/Indiana/Petersburg\0" + "America/Indiana/Tell_City\0" + "America/Indiana/Vevay\0" + "America/Indiana/Vincennes\0" + "America/Indiana/Winamac\0" + "America/Indianapolis\0" + "America/Inuvik\0" + "America/Iqaluit\0" + "America/Jamaica\0" + "America/Jujuy\0" + "America/Juneau\0" + "America/Kentucky/Louisville\0" + "America/Kentucky/Monticello\0" + "America/Knox_IN\0" + "America/Kralendijk\0" + "America/La_Paz\0" + "America/Lima\0" + "America/Los_Angeles\0" + "America/Louisville\0" + "America/Lower_Princes\0" + "America/Maceio\0" + "America/Managua\0" + "America/Manaus\0" + "America/Marigot\0" + "America/Martinique\0" + "America/Matamoros\0" + "America/Mazatlan\0" + "America/Mendoza\0" + "America/Menominee\0" + "America/Merida\0" + "America/Metlakatla\0" + "America/Mexico_City\0" + "America/Miquelon\0" + "America/Moncton\0" + "America/Monterrey\0" + "America/Montevideo\0" + "America/Montreal\0" + "America/Montserrat\0" + "America/Nassau\0" + "America/New_York\0" + "America/Nipigon\0" + "America/Nome\0" + "America/Noronha\0" + "America/North_Dakota/Beulah\0" + "America/North_Dakota/Center\0" + "America/North_Dakota/New_Salem\0" + "America/Nuuk\0" + "America/Ojinaga\0" + "America/Panama\0" + "America/Pangnirtung\0" + "America/Paramaribo\0" + "America/Phoenix\0" + "America/Port-au-Prince\0" + "America/Port_of_Spain\0" + "America/Porto_Acre\0" + "America/Porto_Velho\0" + "America/Puerto_Rico\0" + "America/Punta_Arenas\0" + "America/Rainy_River\0" + "America/Rankin_Inlet\0" + "America/Recife\0" + "America/Regina\0" + "America/Resolute\0" + "America/Rio_Branco\0" + "America/Rosario\0" + "America/Santa_Isabel\0" + "America/Santarem\0" + "America/Santiago\0" + "America/Santo_Domingo\0" + "America/Sao_Paulo\0" + "America/Scoresbysund\0" + "America/Shiprock\0" + "America/Sitka\0" + "America/St_Barthelemy\0" + "America/St_Johns\0" + "America/St_Kitts\0" + "America/St_Lucia\0" + "America/St_Thomas\0" + "America/St_Vincent\0" + "America/Swift_Current\0" + "America/Tegucigalpa\0" + "America/Thule\0" + "America/Thunder_Bay\0" + "America/Tijuana\0" + "America/Toronto\0" + "America/Tortola\0" + "America/Vancouver\0" + "America/Virgin\0" + "America/Whitehorse\0" + "America/Winnipeg\0" + "America/Yakutat\0" + "America/Yellowknife\0" + "Antarctica/Casey\0" + "Antarctica/Davis\0" + "Antarctica/DumontDUrville\0" + "Antarctica/Macquarie\0" + "Antarctica/Mawson\0" + "Antarctica/McMurdo\0" + "Antarctica/Palmer\0" + "Antarctica/Rothera\0" + "Antarctica/South_Pole\0" + "Antarctica/Syowa\0" + "Antarctica/Troll\0" + "Antarctica/Vostok\0" + "Arctic/Longyearbyen\0" + "Asia/Aden\0" + "Asia/Almaty\0" + "Asia/Amman\0" + "Asia/Anadyr\0" + "Asia/Aqtau\0" + "Asia/Aqtobe\0" + "Asia/Ashgabat\0" + "Asia/Ashkhabad\0" + "Asia/Atyrau\0" + "Asia/Baghdad\0" + "Asia/Bahrain\0" + "Asia/Baku\0" + "Asia/Bangkok\0" + "Asia/Barnaul\0" + "Asia/Beirut\0" + "Asia/Bishkek\0" + "Asia/Brunei\0" + "Asia/Calcutta\0" + "Asia/Chita\0" + "Asia/Choibalsan\0" + "Asia/Chongqing\0" + "Asia/Chungking\0" + "Asia/Colombo\0" + "Asia/Dacca\0" + "Asia/Damascus\0" + "Asia/Dhaka\0" + "Asia/Dili\0" + "Asia/Dubai\0" + "Asia/Dushanbe\0" + "Asia/Famagusta\0" + "Asia/Gaza\0" + "Asia/Harbin\0" + "Asia/Hebron\0" + "Asia/Ho_Chi_Minh\0" + "Asia/Hong_Kong\0" + "Asia/Hovd\0" + "Asia/Irkutsk\0" + "Asia/Istanbul\0" + "Asia/Jakarta\0" + "Asia/Jayapura\0" + "Asia/Jerusalem\0" + "Asia/Kabul\0" + "Asia/Kamchatka\0" + "Asia/Karachi\0" + "Asia/Kashgar\0" + "Asia/Kathmandu\0" + "Asia/Katmandu\0" + "Asia/Khandyga\0" + "Asia/Kolkata\0" + "Asia/Krasnoyarsk\0" + "Asia/Kuala_Lumpur\0" + "Asia/Kuching\0" + "Asia/Kuwait\0" + "Asia/Macao\0" + "Asia/Macau\0" + "Asia/Magadan\0" + "Asia/Makassar\0" + "Asia/Manila\0" + "Asia/Muscat\0" + "Asia/Nicosia\0" + "Asia/Novokuznetsk\0" + "Asia/Novosibirsk\0" + "Asia/Omsk\0" + "Asia/Oral\0" + "Asia/Phnom_Penh\0" + "Asia/Pontianak\0" + "Asia/Pyongyang\0" + "Asia/Qatar\0" + "Asia/Qostanay\0" + "Asia/Qyzylorda\0" + "Asia/Rangoon\0" + "Asia/Riyadh\0" + "Asia/Saigon\0" + "Asia/Sakhalin\0" + "Asia/Samarkand\0" + "Asia/Seoul\0" + "Asia/Shanghai\0" + "Asia/Singapore\0" + "Asia/Srednekolymsk\0" + "Asia/Taipei\0" + "Asia/Tashkent\0" + "Asia/Tbilisi\0" + "Asia/Tehran\0" + "Asia/Tel_Aviv\0" + "Asia/Thimbu\0" + "Asia/Thimphu\0" + "Asia/Tokyo\0" + "Asia/Tomsk\0" + "Asia/Ujung_Pandang\0" + "Asia/Ulaanbaatar\0" + "Asia/Ulan_Bator\0" + "Asia/Urumqi\0" + "Asia/Ust-Nera\0" + "Asia/Vientiane\0" + "Asia/Vladivostok\0" + "Asia/Yakutsk\0" + "Asia/Yangon\0" + "Asia/Yekaterinburg\0" + "Asia/Yerevan\0" + "Atlantic/Azores\0" + "Atlantic/Bermuda\0" + "Atlantic/Canary\0" + "Atlantic/Cape_Verde\0" + "Atlantic/Faeroe\0" + "Atlantic/Faroe\0" + "Atlantic/Jan_Mayen\0" + "Atlantic/Madeira\0" + "Atlantic/Reykjavik\0" + "Atlantic/South_Georgia\0" + "Atlantic/St_Helena\0" + "Atlantic/Stanley\0" + "Australia/ACT\0" + "Australia/Adelaide\0" + "Australia/Brisbane\0" + "Australia/Broken_Hill\0" + "Australia/Canberra\0" + "Australia/Currie\0" + "Australia/Darwin\0" + "Australia/Eucla\0" + "Australia/Hobart\0" + "Australia/LHI\0" + "Australia/Lindeman\0" + "Australia/Lord_Howe\0" + "Australia/Melbourne\0" + "Australia/NSW\0" + "Australia/North\0" + "Australia/Perth\0" + "Australia/Queensland\0" + "Australia/South\0" + "Australia/Sydney\0" + "Australia/Tasmania\0" + "Australia/Victoria\0" + "Australia/West\0" + "Australia/Yancowinna\0" + "Brazil/Acre\0" + "Brazil/DeNoronha\0" + "Brazil/East\0" + "Brazil/West\0" + "CET\0" + "CST6CDT\0" + "Canada/Atlantic\0" + "Canada/Central\0" + "Canada/Eastern\0" + "Canada/Mountain\0" + "Canada/Newfoundland\0" + "Canada/Pacific\0" + "Canada/Saskatchewan\0" + "Canada/Yukon\0" + "Chile/Continental\0" + "Chile/EasterIsland\0" + "Cuba\0" + "EET\0" + "EST\0" + "EST5EDT\0" + "Egypt\0" + "Eire\0" + "Etc/GMT\0" + "Etc/GMT+0\0" + "Etc/GMT+1\0" + "Etc/GMT+10\0" + "Etc/GMT+11\0" + "Etc/GMT+12\0" + "Etc/GMT+2\0" + "Etc/GMT+3\0" + "Etc/GMT+4\0" + "Etc/GMT+5\0" + "Etc/GMT+6\0" + "Etc/GMT+7\0" + "Etc/GMT+8\0" + "Etc/GMT+9\0" + "Etc/GMT-0\0" + "Etc/GMT-1\0" + "Etc/GMT-10\0" + "Etc/GMT-11\0" + "Etc/GMT-12\0" + "Etc/GMT-13\0" + "Etc/GMT-14\0" + "Etc/GMT-2\0" + "Etc/GMT-3\0" + "Etc/GMT-4\0" + "Etc/GMT-5\0" + "Etc/GMT-6\0" + "Etc/GMT-7\0" + "Etc/GMT-8\0" + "Etc/GMT-9\0" + "Etc/GMT0\0" + "Etc/Greenwich\0" + "Etc/UCT\0" + "Etc/UTC\0" + "Etc/Universal\0" + "Etc/Zulu\0" + "Europe/Amsterdam\0" + "Europe/Andorra\0" + "Europe/Astrakhan\0" + "Europe/Athens\0" + "Europe/Belfast\0" + "Europe/Belgrade\0" + "Europe/Berlin\0" + "Europe/Bratislava\0" + "Europe/Brussels\0" + "Europe/Bucharest\0" + "Europe/Budapest\0" + "Europe/Busingen\0" + "Europe/Chisinau\0" + "Europe/Copenhagen\0" + "Europe/Dublin\0" + "Europe/Gibraltar\0" + "Europe/Guernsey\0" + "Europe/Helsinki\0" + "Europe/Isle_of_Man\0" + "Europe/Istanbul\0" + "Europe/Jersey\0" + "Europe/Kaliningrad\0" + "Europe/Kiev\0" + "Europe/Kirov\0" + "Europe/Kyiv\0" + "Europe/Lisbon\0" + "Europe/Ljubljana\0" + "Europe/London\0" + "Europe/Luxembourg\0" + "Europe/Madrid\0" + "Europe/Malta\0" + "Europe/Mariehamn\0" + "Europe/Minsk\0" + "Europe/Monaco\0" + "Europe/Moscow\0" + "Europe/Nicosia\0" + "Europe/Oslo\0" + "Europe/Paris\0" + "Europe/Podgorica\0" + "Europe/Prague\0" + "Europe/Riga\0" + "Europe/Rome\0" + "Europe/Samara\0" + "Europe/San_Marino\0" + "Europe/Sarajevo\0" + "Europe/Saratov\0" + "Europe/Simferopol\0" + "Europe/Skopje\0" + "Europe/Sofia\0" + "Europe/Stockholm\0" + "Europe/Tallinn\0" + "Europe/Tirane\0" + "Europe/Tiraspol\0" + "Europe/Ulyanovsk\0" + "Europe/Uzhgorod\0" + "Europe/Vaduz\0" + "Europe/Vatican\0" + "Europe/Vienna\0" + "Europe/Vilnius\0" + "Europe/Volgograd\0" + "Europe/Warsaw\0" + "Europe/Zagreb\0" + "Europe/Zaporozhye\0" + "Europe/Zurich\0" + "GB\0" + "GB-Eire\0" + "GMT\0" + "GMT+0\0" + "GMT-0\0" + "GMT0\0" + "Greenwich\0" + "HST\0" + "Hongkong\0" + "Iceland\0" + "Indian/Antananarivo\0" + "Indian/Chagos\0" + "Indian/Christmas\0" + "Indian/Cocos\0" + "Indian/Comoro\0" + "Indian/Kerguelen\0" + "Indian/Mahe\0" + "Indian/Maldives\0" + "Indian/Mauritius\0" + "Indian/Mayotte\0" + "Indian/Reunion\0" + "Iran\0" + "Israel\0" + "Jamaica\0" + "Japan\0" + "Kwajalein\0" + "Libya\0" + "MET\0" + "MST\0" + "MST7MDT\0" + "Mexico/BajaNorte\0" + "Mexico/BajaSur\0" + "Mexico/General\0" + "NZ\0" + "NZ-CHAT\0" + "Navajo\0" + "PRC\0" + "PST8PDT\0" + "Pacific/Apia\0" + "Pacific/Auckland\0" + "Pacific/Bougainville\0" + "Pacific/Chatham\0" + "Pacific/Chuuk\0" + "Pacific/Easter\0" + "Pacific/Efate\0" + "Pacific/Enderbury\0" + "Pacific/Fakaofo\0" + "Pacific/Fiji\0" + "Pacific/Funafuti\0" + "Pacific/Galapagos\0" + "Pacific/Gambier\0" + "Pacific/Guadalcanal\0" + "Pacific/Guam\0" + "Pacific/Honolulu\0" + "Pacific/Johnston\0" + "Pacific/Kanton\0" + "Pacific/Kiritimati\0" + "Pacific/Kosrae\0" + "Pacific/Kwajalein\0" + "Pacific/Majuro\0" + "Pacific/Marquesas\0" + "Pacific/Midway\0" + "Pacific/Nauru\0" + "Pacific/Niue\0" + "Pacific/Norfolk\0" + "Pacific/Noumea\0" + "Pacific/Pago_Pago\0" + "Pacific/Palau\0" + "Pacific/Pitcairn\0" + "Pacific/Pohnpei\0" + "Pacific/Ponape\0" + "Pacific/Port_Moresby\0" + "Pacific/Rarotonga\0" + "Pacific/Saipan\0" + "Pacific/Samoa\0" + "Pacific/Tahiti\0" + "Pacific/Tarawa\0" + "Pacific/Tongatapu\0" + "Pacific/Truk\0" + "Pacific/Wake\0" + "Pacific/Wallis\0" + "Pacific/Yap\0" + "Poland\0" + "Portugal\0" + "ROC\0" + "ROK\0" + "Singapore\0" + "Turkey\0" + "UCT\0" + "US/Alaska\0" + "US/Aleutian\0" + "US/Arizona\0" + "US/Central\0" + "US/East-Indiana\0" + "US/Eastern\0" + "US/Hawaii\0" + "US/Indiana-Starke\0" + "US/Michigan\0" + "US/Mountain\0" + "US/Pacific\0" + "US/Samoa\0" + "UTC\0" + "Universal\0" + "W-SU\0" + "WET\0" + "Zulu\0"; +static const char scr_tz_name_text[] = + "Greenwich Mean Time\0" + "East Africa Time\0" + "Central European Standard Time\0" + "Central European Summer Time\0" + "West Africa Time\0" + "Central Africa Time\0" + "Eastern European Standard Time\0" + "Eastern European Summer Time\0" + "Western European Standard Time\0" + "Western European Summer Time\0" + "South Africa Standard Time\0" + "GMT+02:00\0" + "GMT+01:00\0" + "Hawaii-Aleutian Standard Time\0" + "Hawaii-Aleutian Daylight Time\0" + "Alaska Standard Time\0" + "Alaska Daylight Time\0" + "Atlantic Standard Time\0" + "Brasilia Standard Time\0" + "Brasilia Summer Time\0" + "Argentina Standard Time\0" + "Argentina Summer Time\0" + "Paraguay Standard Time\0" + "Paraguay Summer Time\0" + "Eastern Standard Time\0" + "Central Standard Time\0" + "Central Daylight Time\0" + "Atlantic Daylight Time\0" + "Amazon Standard Time\0" + "Amazon Summer Time\0" + "Colombia Standard Time\0" + "Colombia Summer Time\0" + "Mountain Standard Time\0" + "Mountain Daylight Time\0" + "Eastern Daylight Time\0" + "Venezuela Time\0" + "French Guiana Time\0" + "GMT-03:00\0" + "GMT+00:00\0" + "Yukon Time\0" + "GMT-07:00\0" + "Acre Standard Time\0" + "Acre Summer Time\0" + "Pacific Standard Time\0" + "Pacific Daylight Time\0" + "Greenland Standard Time\0" + "Greenland Summer Time\0" + "Ecuador Time\0" + "GMT-05:00\0" + "Guyana Time\0" + "Cuba Standard Time\0" + "Cuba Daylight Time\0" + "Mexican Pacific Standard Time\0" + "Mexican Pacific Daylight Time\0" + "Bolivia Time\0" + "Peru Standard Time\0" + "Peru Summer Time\0" + "St. Pierre & Miquelon Standard Time\0" + "St. Pierre & Miquelon Daylight Time\0" + "Uruguay Standard Time\0" + "Uruguay Summer Time\0" + "Fernando de Noronha Standard Time\0" + "Fernando de Noronha Summer Time\0" + "Suriname Time\0" + "Chile Standard Time\0" + "Chile Summer Time\0" + "Newfoundland Standard Time\0" + "Newfoundland Daylight Time\0" + "Australian Western Standard Time\0" + "Davis Time\0" + "Dumont d’Urville Time\0" + "Australian Eastern Standard Time\0" + "Australian Eastern Daylight Time\0" + "Mawson Time\0" + "New Zealand Standard Time\0" + "New Zealand Daylight Time\0" + "Rothera Time\0" + "Syowa Time\0" + "Vostok Time\0" + "Arabian Standard Time\0" + "Kazakhstan Time\0" + "GMT+05:00\0" + "GMT+03:00\0" + "Kamchatka Standard Time\0" + "Kamchatka Summer Time\0" + "Turkmenistan Standard Time\0" + "Turkmenistan Summer Time\0" + "Arabian Daylight Time\0" + "Azerbaijan Standard Time\0" + "Azerbaijan Summer Time\0" + "Indochina Time\0" + "Krasnoyarsk Standard Time\0" + "Krasnoyarsk Summer Time\0" + "Kyrgyzstan Time\0" + "GMT+06:00\0" + "Brunei Time\0" + "India Standard Time\0" + "Yakutsk Standard Time\0" + "Yakutsk Summer Time\0" + "Ulaanbaatar Standard Time\0" + "Ulaanbaatar Summer Time\0" + "China Standard Time\0" + "China Daylight Time\0" + "Bangladesh Standard Time\0" + "Bangladesh Summer Time\0" + "Timor-Leste Time\0" + "Gulf Standard Time\0" + "Tajikistan Time\0" + "Hong Kong Standard Time\0" + "Hong Kong Summer Time\0" + "Khovd Standard Time\0" + "Khovd Summer Time\0" + "Irkutsk Standard Time\0" + "Irkutsk Summer Time\0" + "Türkiye Standard Time\0" + "Türkiye Summer Time\0" + "Western Indonesia Time\0" + "Eastern Indonesia Time\0" + "Israel Standard Time\0" + "Israel Daylight Time\0" + "Afghanistan Time\0" + "Pakistan Standard Time\0" + "Pakistan Summer Time\0" + "Nepal Time\0" + "Malaysia Time\0" + "Magadan Standard Time\0" + "Magadan Summer Time\0" + "Central Indonesia Time\0" + "Philippine Standard Time\0" + "Philippine Summer Time\0" + "Omsk Standard Time\0" + "Omsk Summer Time\0" + "Korean Standard Time\0" + "Myanmar Time\0" + "Uzbekistan Standard Time\0" + "Uzbekistan Summer Time\0" + "Korean Daylight Time\0" + "Singapore Standard Time\0" + "Taiwan Standard Time\0" + "Taiwan Daylight Time\0" + "Georgia Standard Time\0" + "Georgia Summer Time\0" + "Iran Standard Time\0" + "Iran Daylight Time\0" + "Bhutan Time\0" + "Japan Standard Time\0" + "Vladivostok Standard Time\0" + "Vladivostok Summer Time\0" + "Yekaterinburg Standard Time\0" + "Yekaterinburg Summer Time\0" + "Armenia Standard Time\0" + "Armenia Summer Time\0" + "Azores Standard Time\0" + "Azores Summer Time\0" + "Cape Verde Standard Time\0" + "South Georgia Time\0" + "Falkland Islands Standard Time\0" + "Falkland Islands Summer Time\0" + "Australian Central Standard Time\0" + "Australian Central Daylight Time\0" + "Australian Central Western Standard Time\0" + "Australian Central Western Daylight Time\0" + "Lord Howe Standard Time\0" + "Lord Howe Daylight Time\0" + "Australian Western Daylight Time\0" + "Easter Island Standard Time\0" + "Easter Island Summer Time\0" + "GMT-01:00\0" + "GMT-10:00\0" + "GMT-11:00\0" + "GMT-12:00\0" + "GMT-02:00\0" + "GMT-04:00\0" + "GMT-06:00\0" + "GMT-08:00\0" + "GMT-09:00\0" + "GMT+10:00\0" + "GMT+11:00\0" + "GMT+12:00\0" + "GMT+13:00\0" + "GMT+14:00\0" + "GMT+04:00\0" + "GMT+07:00\0" + "GMT+08:00\0" + "GMT+09:00\0" + "Coordinated Universal Time\0" + "Samara Standard Time\0" + "Samara Summer Time\0" + "British Summer Time\0" + "Irish Standard Time\0" + "Moscow Standard Time\0" + "Moscow Summer Time\0" + "Indian Ocean Time\0" + "Christmas Island Time\0" + "Cocos Islands Time\0" + "French Southern & Antarctic Time\0" + "Seychelles Time\0" + "Maldives Time\0" + "Mauritius Standard Time\0" + "Mauritius Summer Time\0" + "Réunion Time\0" + "Marshall Islands Time\0" + "Chatham Standard Time\0" + "Chatham Daylight Time\0" + "Samoa Standard Time\0" + "Samoa Daylight Time\0" + "Chuuk Time\0" + "Vanuatu Standard Time\0" + "Vanuatu Summer Time\0" + "Phoenix Islands Time\0" + "Tokelau Time\0" + "Fiji Standard Time\0" + "Fiji Summer Time\0" + "Tuvalu Time\0" + "Galapagos Time\0" + "Gambier Time\0" + "Solomon Islands Time\0" + "Chamorro Standard Time\0" + "Line Islands Time\0" + "Kosrae Time\0" + "Marquesas Time\0" + "American Samoa Standard Time\0" + "Nauru Time\0" + "Niue Time\0" + "Norfolk Island Standard Time\0" + "Norfolk Island Daylight Time\0" + "New Caledonia Standard Time\0" + "New Caledonia Summer Time\0" + "Palau Time\0" + "Pitcairn Time\0" + "Pohnpei Time\0" + "Papua New Guinea Time\0" + "Cook Islands Standard Time\0" + "Cook Islands Summer Time\0" + "Tahiti Time\0" + "Gilbert Islands Time\0" + "Tonga Standard Time\0" + "Tonga Summer Time\0" + "Wake Island Time\0" + "Wallis & Futuna Time\0"; +static const int32_t scr_tz_daylight_changes[] = { + 141264000, 147222000, 199756800, 207702000, 231292800, 244249200, 265507200, 271033200, + 1212278400, 1220223600, 1243814400, 1250809200, 1272758400, 1281222000, 1301788800, 1312066800, + 1335664800, 1342749600, 1345428000, 1348970400, 1367114400, 1373162400, 1376100000, 1382839200, + 1396144800, 1403920800, 1406944800, 1414288800, 1427594400, 1434247200, 1437271200, 1445738400, + 1459044000, 1465092000, 1468116000, 1477792800, 1490493600, 1495332000, 1498960800, 1509242400, + 1521943200, 1526176800, 1529200800, 1557021600, 1560045600, 1587261600, 1590890400, 1618106400, + 1621130400, 1648346400, 1651975200, 1679191200, 1682215200, 1710036000, 1713060000, 1740276000, + 1743904800, 1771120800, 1774144800, 1789866000, 199756800, 207702000, 231292800, 244249200, + 265507200, 271033200, 1212278400, 1220223600, 1243814400, 1250809200, 1272758400, 1281222000, + 1301788800, 1312066800, 1335664800, 1342749600, 1345428000, 1348970400, 1367114400, 1373162400, + 1376100000, 1382839200, 1396144800, 1403920800, 1406944800, 1414288800, 1427594400, 1434247200, + 1437271200, 1445738400, 1459044000, 1465092000, 1468116000, 1477792800, 1490493600, 1495332000, + 1498960800, 1509242400, 1521943200, 1526176800, 1529200800, 1557021600, 1560045600, 1587261600, + 1590890400, 1618106400, 1621130400, 1648346400, 1651975200, 1679191200, 1682215200, 1710036000, + 1713060000, 1740276000, 1743904800, 1771120800, 1774144800, 1789866000, 778640400, 796780800, + 810090000, 828835200, 841539600, 860284800, 873594000, 891734400, 905043600, 923184000, 936493200, + 954633600, 967942800, 986083200, 999392400, 1018137600, 1030842000, 1049587200, 1062896400, + 1081036800, 1094346000, 1112486400, 1125795600, 1143936000, 1157245200, 1175385600, 1188694800, + 1207440000, 1220749200, 1238889600, 1252198800, 1270339200, 1283648400, 1301788800, 1315098000, + 1333238400, 1346547600, 1365292800, 1377997200, 1396742400, 1410051600, 1428192000, 1441501200, + 1459641600, 1472950800, 1491091200, 1504400400, 1508796000, 73472400, 89193600, 104922000, + 120643200, 136371600, 152092800, 167821200, 183542400, 199270800, 215596800, 230720400, 247046400, + 262774800, 278496000, 294224400, 309945600, 325674000, 341395200, 357123600, 372844800, 388573200, + 404899200, 420022800, 436348800, 452077200, 467798400, 483526800, 499248000, 514976400, 530697600, + 544611600, 562147200, 576061200, 594201600, 607510800, 625651200, 638960400, 657100800, 671014800, + 688550400, 702464400, 720000000, 733914000, 752054400, 765363600, 783504000, 796813200, 814953600, + 828867600, 846403200, 860317200, 877852800, 891766800, 909302400, 923216400, 941356800, 954666000, + 972806400, 986115600, 1004256000, 1018170000, 1035705600, 1049619600, 1067155200, 1081069200, + 1099209600, 1112518800, 1130659200, 1143968400, 1162108800, 1173603600, 1194163200, 1205053200, + 1225612800, 1236502800, 1257062400, 1268557200, 1289116800, 1300006800, 1320566400, 1331456400, + 1352016000, 1362906000, 1383465600, 1394355600, 1414915200, 1425805200, 1446364800, 1457859600, + 1478419200, 1489309200, 1509868800, 1520758800, 1541318400, 1552208400, 1572768000, 1583658000, + 1604217600, 1615712400, 1636272000, 1647162000, 1667721600, 1678611600, 1699171200, 1710061200, + 1730620800, 1741510800, 1762070400, 1772960400, 9972000, 25693200, 41421600, 57747600, 73476000, + 89197200, 104925600, 120646800, 136375200, 152096400, 167824800, 183546000, 199274400, 215600400, + 230724000, 247050000, 262778400, 278499600, 294228000, 309949200, 325677600, 341398800, 357127200, + 372848400, 388576800, 404902800, 420026400, 436352400, 452080800, 467802000, 483530400, 499251600, + 514980000, 530701200, 544615200, 562150800, 576064800, 594205200, 607514400, 625654800, 638964000, + 657104400, 671018400, 688554000, 702468000, 720003600, 733917600, 752058000, 765367200, 783507600, + 796816800, 814957200, 828871200, 846406800, 860320800, 877856400, 891770400, 909306000, 923220000, + 941360400, 954669600, 972810000, 986119200, 1004259600, 1018173600, 1035709200, 1049623200, + 1067158800, 1081072800, 1099213200, 1112522400, 1130662800, 1143972000, 1162112400, 1173607200, + 1194166800, 1205056800, 1225616400, 1236506400, 1257066000, 1268560800, 1289120400, 1300010400, + 1320570000, 1331460000, 1352019600, 1362909600, 1383469200, 1394359200, 1414918800, 1425808800, + 1446368400, 1457863200, 1478422800, 1489312800, 1509872400, 1520762400, 1541322000, 1552212000, + 1572771600, 1583661600, 1604221200, 1615716000, 1636275600, 1647165600, 1667725200, 1678615200, + 1699174800, 1710064800, 1730624400, 1741514400, 1762074000, 1772964000, 73472400, 89193600, + 104922000, 120643200, 136371600, 152092800, 167821200, 183542400, 199270800, 215596800, 230720400, + 247046400, 262774800, 278496000, 294224400, 309945600, 325674000, 341395200, 357123600, 372844800, + 388573200, 404899200, 420022800, 436348800, 452077200, 467798400, 483526800, 499248000, 514976400, + 530697600, 544611600, 562147200, 576061200, 594201600, 607510800, 625651200, 638960400, 657100800, + 671014800, 688550400, 702464400, 720000000, 733914000, 752054400, 765363600, 783504000, 796813200, + 814953600, 828867600, 846403200, 860317200, 877852800, 891766800, 909302400, 923216400, 941356800, + 954666000, 972806400, 986115600, 1004256000, 1018170000, 1035705600, 1049619600, 1067155200, + 1081069200, 1099209600, 1112518800, 1130659200, 1143968400, 1162108800, 1173603600, 1194163200, + 1205053200, 1225612800, 1236502800, 1257062400, 1268557200, 1289116800, 1300006800, 1320566400, + 1331456400, 1352016000, 1362906000, 1383465600, 1394355600, 1414915200, 1425805200, 1446364800, + 1457859600, 1478419200, 1489309200, 1509868800, 1520758800, 1541318400, 1552208400, 1572768000, + 1583658000, 1604217600, 1615712400, 1636272000, 1647162000, 1667721600, 1678611600, 1699171200, + 1710061200, 1730620800, 1741510800, 1762070400, 1772960400, 73472400, 89193600, 104922000, + 120643200, 136371600, 152092800, 167821200, 183542400, 199270800, 215596800, 230720400, 247046400, + 262774800, 278496000, 294224400, 309945600, 325674000, 341395200, 357123600, 372844800, 388573200, + 404899200, 420022800, 436348800, 452077200, 467798400, 483526800, 499248000, 514976400, 530697600, + 544611600, 562147200, 576061200, 594201600, 607510800, 625651200, 638960400, 657100800, 671014800, + 688550400, 702464400, 720000000, 733914000, 752054400, 765363600, 783504000, 796813200, 814953600, + 828867600, 846403200, 860317200, 877852800, 891766800, 909302400, 923216400, 941356800, 954666000, + 972806400, 986115600, 1004256000, 1018170000, 1035705600, 1049619600, 1067155200, 1081069200, + 1099209600, 1112518800, 1130659200, 1143968400, 1162108800, 1173603600, 1194163200, 1205053200, + 1225612800, 1236502800, 1257062400, 1268557200, 1289116800, 1300006800, 1320566400, 1331456400, + 1352016000, 1362906000, 1383465600, 1394355600, 1414915200, 1425805200, 1446364800, 1457859600, + 1478419200, 1489309200, 1509868800, 1520758800, 1541318400, 1552208400, 1572768000, 1583658000, + 1604217600, 1615712400, 1636272000, 1647162000, 1667721600, 1678611600, 1699171200, 1710061200, + 1730620800, 1741510800, 1762070400, 1772960400, 9972000, 25693200, 41421600, 57747600, 73476000, + 89197200, 104925600, 120646800, 136375200, 152096400, 167824800, 183546000, 199274400, 215600400, + 230724000, 247050000, 262778400, 278499600, 294228000, 309949200, 325677600, 341398800, 357127200, + 372848400, 388576800, 404902800, 420026400, 436352400, 452080800, 467802000, 483530400, 499251600, + 514980000, 530701200, 544615200, 562150800, 576064800, 594205200, 607514400, 625654800, 638964000, + 657104400, 671018400, 688554000, 702468000, 720003600, 733917600, 752058000, 765367200, 783507600, + 796816800, 814957200, 828871200, 846406800, 860320800, 877856400, 891770400, 909306000, 923220000, + 941360400, 954669600, 972810000, 986119200, 1004259600, 1018173600, 1035709200, 1049623200, + 1067158800, 1081072800, 1099213200, 1112522400, 1130662800, 1143972000, 1162112400, 1173607200, + 1194166800, 1205056800, 1225616400, 1236506400, 1257066000, 1268560800, 1289120400, 1300010400, + 1320570000, 1331460000, 1352019600, 1362909600, 1383469200, 1394359200, 1414918800, 1425808800, + 1446368400, 1457863200, 1478422800, 1489312800, 1509872400, 1520762400, 1541322000, 1552212000, + 1572771600, 1583661600, 1604221200, 1615716000, 1636275600, 1647165600, 1667725200, 1678615200, + 1699174800, 1710064800, 1730624400, 1741514400, 1762074000, 1772964000, 69818400, 89172000, + 101268000, 120621600, 132717600, 152071200, 164167200, 183520800, 196221600, 214970400, 227671200, + 246420000, 259120800, 278474400, 290570400, 309924000, 322020000, 341373600, 354675600, 372819600, + 386125200, 404269200, 417574800, 435718800, 449024400, 467773200, 481078800, 499222800, 512528400, + 530672400, 543978000, 562122000, 575427600, 593571600, 606877200, 625626000, 638326800, 657075600, + 670381200, 688525200, 701830800, 719974800, 733280400, 751424400, 764730000, 782874000, 796179600, + 814323600, 828234000, 846378000, 859683600, 877827600, 891133200, 909277200, 922582800, 941331600, + 954032400, 972781200, 985482000, 1004230800, 1017536400, 1035680400, 1048986000, 1067130000, + 1080435600, 1099184400, 1111885200, 1130634000, 1143334800, 1162083600, 1174784400, 1193533200, + 1206838800, 1224982800, 1238288400, 1256432400, 1269738000, 1288486800, 1301187600, 1319936400, + 1332637200, 1351386000, 1364691600, 1382835600, 1396141200, 1414285200, 1427590800, 1445734800, + 1459040400, 1477789200, 1490490000, 1509238800, 1521939600, 1540688400, 1553994000, 1572138000, + 1585443600, 1603587600, 1616893200, 1635642000, 1648342800, 1667091600, 1679792400, 1698541200, + 1711846800, 1729990800, 1743296400, 1761440400, 1774746000, 1792890000, 1806195600, 1824944400, + 1837645200, 1856394000, 1869094800, 1887843600, 1901149200, 1919293200, 1932598800, 1950742800, + 1964048400, 1982797200, 1995498000, 2014246800, 2026947600, 2045696400, 2058397200, 2077146000, + 2090451600, 2108595600, 2121901200, 2140045200, +}; +static const ScrTzNames scr_tz_names[] = { + { 0, 0, 0, 0, 0 }, + { 15, 0, 0, 0, 0 }, + { 28, 20, 20, 0, 0 }, + { 47, 37, 68, 0, 0 }, + { 62, 20, 20, 0, 0 }, + { 76, 20, 20, 0, 0 }, + { 90, 0, 0, 0, 0 }, + { 104, 97, 97, 0, 0 }, + { 118, 0, 0, 0, 0 }, + { 132, 0, 0, 0, 0 }, + { 146, 114, 114, 0, 0 }, + { 162, 97, 97, 0, 0 }, + { 181, 114, 114, 0, 0 }, + { 198, 134, 165, 0, 0 }, + { 211, 194, 225, 0, 60 }, + { 229, 37, 68, 0, 0 }, + { 242, 0, 0, 0, 0 }, + { 257, 0, 0, 0, 0 }, + { 270, 20, 20, 0, 0 }, + { 291, 20, 20, 0, 0 }, + { 307, 97, 97, 0, 0 }, + { 321, 194, 225, 60, 58 }, + { 337, 0, 0, 0, 0 }, + { 353, 114, 114, 0, 0 }, + { 369, 114, 114, 0, 0 }, + { 383, 254, 254, 0, 0 }, + { 403, 114, 281, 0, 0 }, + { 415, 20, 20, 0, 0 }, + { 430, 114, 281, 0, 0 }, + { 446, 114, 114, 0, 0 }, + { 460, 97, 97, 0, 0 }, + { 476, 97, 97, 0, 0 }, + { 489, 97, 97, 0, 0 }, + { 507, 0, 0, 0, 0 }, + { 519, 97, 97, 0, 0 }, + { 533, 114, 114, 0, 0 }, + { 551, 114, 114, 0, 0 }, + { 565, 97, 97, 0, 0 }, + { 579, 114, 114, 0, 0 }, + { 593, 254, 254, 0, 0 }, + { 607, 254, 254, 0, 0 }, + { 622, 20, 20, 0, 0 }, + { 639, 0, 0, 0, 0 }, + { 655, 20, 20, 0, 0 }, + { 670, 97, 291, 0, 0 }, + { 686, 97, 97, 0, 0 }, + { 700, 0, 0, 0, 0 }, + { 718, 0, 0, 0, 0 }, + { 737, 97, 97, 0, 0 }, + { 755, 0, 0, 0, 0 }, + { 771, 0, 0, 0, 0 }, + { 787, 134, 165, 0, 0 }, + { 802, 37, 68, 0, 0 }, + { 815, 114, 281, 118, 48 }, + { 831, 301, 331, 0, 0 }, + { 844, 361, 382, 0, 0 }, + { 862, 403, 403, 0, 0 }, + { 879, 403, 403, 0, 0 }, + { 895, 426, 449, 0, 0 }, + { 913, 470, 494, 0, 0 }, + { 944, 470, 494, 0, 0 }, + { 972, 470, 494, 0, 0 }, + { 1005, 470, 494, 0, 0 }, + { 1031, 470, 494, 0, 0 }, + { 1055, 470, 494, 0, 0 }, + { 1082, 470, 494, 0, 0 }, + { 1108, 470, 494, 0, 0 }, + { 1139, 470, 494, 0, 0 }, + { 1163, 470, 494, 0, 0 }, + { 1190, 470, 494, 0, 0 }, + { 1217, 470, 494, 0, 0 }, + { 1243, 470, 494, 0, 0 }, + { 1269, 403, 403, 0, 0 }, + { 1283, 516, 539, 0, 0 }, + { 1300, 560, 560, 0, 0 }, + { 1317, 301, 331, 0, 0 }, + { 1330, 426, 449, 0, 0 }, + { 1344, 582, 604, 0, 0 }, + { 1367, 403, 626, 0, 0 }, + { 1384, 426, 449, 0, 0 }, + { 1398, 582, 604, 0, 0 }, + { 1413, 403, 403, 0, 0 }, + { 1434, 649, 670, 0, 0 }, + { 1452, 689, 712, 0, 0 }, + { 1467, 733, 756, 0, 0 }, + { 1481, 470, 494, 0, 0 }, + { 1502, 733, 756, 0, 0 }, + { 1524, 649, 670, 0, 0 }, + { 1545, 560, 779, 0, 0 }, + { 1560, 801, 801, 0, 0 }, + { 1576, 470, 494, 0, 0 }, + { 1594, 816, 816, 0, 0 }, + { 1610, 560, 560, 0, 0 }, + { 1625, 582, 604, 0, 0 }, + { 1641, 582, 604, 0, 0 }, + { 1659, 733, 756, 0, 0 }, + { 1681, 560, 560, 0, 0 }, + { 1703, 470, 494, 0, 0 }, + { 1719, 582, 604, 0, 0 }, + { 1738, 835, 835, 0, 0 }, + { 1756, 733, 733, 0, 0 }, + { 1772, 649, 670, 0, 0 }, + { 1787, 403, 403, 0, 0 }, + { 1803, 0, 845, 0, 0 }, + { 1824, 855, 866, 0, 0 }, + { 1839, 733, 756, 0, 0 }, + { 1860, 733, 756, 0, 0 }, + { 1875, 560, 779, 0, 0 }, + { 1891, 403, 403, 0, 0 }, + { 1908, 733, 756, 166, 109 }, + { 1925, 876, 895, 0, 0 }, + { 1942, 582, 604, 0, 0 }, + { 1962, 912, 934, 0, 0 }, + { 1979, 733, 756, 0, 0 }, + { 1999, 560, 779, 0, 0 }, + { 2018, 426, 449, 0, 0 }, + { 2036, 403, 626, 0, 0 }, + { 2054, 956, 980, 0, 0 }, + { 2070, 403, 626, 0, 0 }, + { 2088, 560, 779, 0, 0 }, + { 2107, 403, 403, 0, 0 }, + { 2123, 403, 403, 0, 0 }, + { 2142, 582, 604, 0, 0 }, + { 2160, 1002, 1015, 0, 0 }, + { 2178, 1025, 1025, 0, 0 }, + { 2193, 403, 626, 0, 0 }, + { 2209, 1037, 1056, 0, 0 }, + { 2224, 1075, 1105, 0, 0 }, + { 2243, 560, 779, 0, 0 }, + { 2272, 582, 604, 0, 0 }, + { 2293, 560, 779, 0, 0 }, + { 2317, 560, 779, 0, 0 }, + { 2344, 582, 604, 0, 0 }, + { 2370, 560, 779, 0, 0 }, + { 2392, 560, 779, 0, 0 }, + { 2418, 560, 779, 0, 0 }, + { 2442, 560, 779, 0, 0 }, + { 2463, 733, 756, 0, 0 }, + { 2478, 560, 779, 0, 0 }, + { 2494, 560, 779, 0, 0 }, + { 2510, 470, 494, 0, 0 }, + { 2524, 361, 382, 0, 0 }, + { 2539, 560, 779, 0, 0 }, + { 2567, 560, 779, 0, 0 }, + { 2595, 582, 604, 0, 0 }, + { 2611, 403, 403, 0, 0 }, + { 2630, 1135, 1135, 0, 0 }, + { 2645, 1148, 1167, 0, 0 }, + { 2658, 912, 934, 0, 0 }, + { 2678, 560, 779, 0, 0 }, + { 2697, 403, 403, 0, 0 }, + { 2719, 426, 449, 0, 0 }, + { 2734, 582, 604, 0, 0 }, + { 2750, 649, 670, 0, 0 }, + { 2765, 403, 403, 0, 0 }, + { 2781, 403, 626, 0, 0 }, + { 2800, 582, 604, 0, 0 }, + { 2818, 1075, 1105, 0, 0 }, + { 2835, 470, 494, 0, 0 }, + { 2851, 582, 604, 0, 0 }, + { 2869, 582, 604, 0, 0 }, + { 2884, 361, 382, 0, 0 }, + { 2903, 582, 604, 0, 0 }, + { 2923, 1184, 1220, 0, 0 }, + { 2940, 403, 626, 0, 0 }, + { 2956, 582, 604, 0, 0 }, + { 2974, 1256, 1278, 0, 0 }, + { 2993, 560, 779, 0, 0 }, + { 3010, 403, 403, 0, 0 }, + { 3029, 560, 779, 0, 0 }, + { 3044, 560, 779, 0, 0 }, + { 3061, 560, 779, 0, 0 }, + { 3077, 361, 382, 0, 0 }, + { 3090, 1298, 1332, 0, 0 }, + { 3106, 582, 604, 0, 0 }, + { 3134, 582, 604, 0, 0 }, + { 3162, 582, 604, 0, 0 }, + { 3193, 956, 980, 0, 0 }, + { 3206, 582, 604, 0, 0 }, + { 3222, 560, 560, 0, 0 }, + { 3237, 560, 779, 0, 0 }, + { 3257, 1364, 1364, 0, 0 }, + { 3276, 733, 733, 0, 0 }, + { 3292, 560, 779, 0, 0 }, + { 3315, 403, 403, 0, 0 }, + { 3337, 876, 895, 0, 0 }, + { 3356, 649, 670, 0, 0 }, + { 3376, 403, 403, 0, 0 }, + { 3396, 835, 835, 0, 0 }, + { 3417, 582, 604, 0, 0 }, + { 3437, 582, 604, 0, 0 }, + { 3458, 426, 449, 0, 0 }, + { 3473, 582, 582, 0, 0 }, + { 3488, 582, 604, 0, 0 }, + { 3505, 876, 895, 0, 0 }, + { 3524, 470, 494, 0, 0 }, + { 3540, 912, 934, 0, 0 }, + { 3561, 426, 449, 0, 0 }, + { 3578, 1378, 1398, 0, 0 }, + { 3595, 403, 626, 0, 0 }, + { 3617, 426, 449, 0, 0 }, + { 3635, 956, 980, 0, 0 }, + { 3656, 733, 756, 0, 0 }, + { 3673, 361, 382, 0, 0 }, + { 3687, 403, 403, 0, 0 }, + { 3709, 1416, 1443, 0, 0 }, + { 3726, 403, 403, 0, 0 }, + { 3743, 403, 403, 0, 0 }, + { 3760, 403, 403, 0, 0 }, + { 3778, 403, 403, 0, 0 }, + { 3797, 582, 582, 0, 0 }, + { 3819, 582, 604, 0, 0 }, + { 3839, 403, 626, 0, 0 }, + { 3853, 560, 779, 0, 0 }, + { 3873, 912, 934, 0, 0 }, + { 3889, 560, 779, 0, 0 }, + { 3905, 403, 403, 0, 0 }, + { 3921, 912, 934, 275, 113 }, + { 3939, 403, 403, 0, 0 }, + { 3954, 855, 866, 0, 0 }, + { 3973, 582, 604, 0, 0 }, + { 3990, 361, 382, 0, 0 }, + { 4006, 733, 756, 388, 109 }, + { 4026, 1470, 1470, 0, 0 }, + { 4043, 1503, 1503, 0, 0 }, + { 4060, 1514, 1514, 0, 0 }, + { 4086, 1538, 1571, 0, 0 }, + { 4107, 1604, 1604, 0, 0 }, + { 4125, 1616, 1642, 0, 0 }, + { 4144, 835, 835, 0, 0 }, + { 4162, 1668, 1668, 0, 0 }, + { 4181, 1616, 1642, 0, 0 }, + { 4203, 1681, 1681, 0, 0 }, + { 4220, 0, 281, 0, 0 }, + { 4237, 1692, 1692, 0, 0 }, + { 4255, 37, 68, 0, 0 }, + { 4275, 1704, 1704, 0, 0 }, + { 4285, 1726, 1742, 0, 0 }, + { 4297, 1752, 1752, 0, 0 }, + { 4308, 1762, 1786, 0, 0 }, + { 4320, 1726, 1742, 0, 0 }, + { 4331, 1726, 1742, 0, 0 }, + { 4343, 1808, 1835, 0, 0 }, + { 4357, 1808, 1835, 0, 0 }, + { 4372, 1726, 1742, 0, 0 }, + { 4384, 1704, 1860, 0, 0 }, + { 4397, 1704, 1704, 0, 0 }, + { 4410, 1882, 1907, 0, 0 }, + { 4420, 1930, 1930, 0, 0 }, + { 4433, 1945, 1971, 0, 0 }, + { 4446, 134, 165, 0, 0 }, + { 4458, 1995, 2011, 0, 0 }, + { 4471, 2021, 2021, 0, 0 }, + { 4483, 2033, 2033, 0, 0 }, + { 4497, 2053, 2075, 0, 0 }, + { 4508, 2095, 2121, 0, 0 }, + { 4524, 2145, 2165, 0, 0 }, + { 4539, 2145, 2165, 0, 0 }, + { 4554, 2033, 2033, 0, 0 }, + { 4567, 2185, 2210, 0, 0 }, + { 4578, 1752, 1752, 0, 0 }, + { 4592, 2185, 2210, 0, 0 }, + { 4603, 2233, 2233, 0, 0 }, + { 4613, 2250, 2250, 0, 0 }, + { 4624, 2269, 1742, 0, 0 }, + { 4638, 134, 165, 0, 0 }, + { 4653, 134, 165, 0, 0 }, + { 4663, 2145, 2165, 0, 0 }, + { 4675, 134, 165, 0, 0 }, + { 4687, 1930, 1930, 0, 0 }, + { 4704, 2285, 2309, 0, 0 }, + { 4719, 2331, 2351, 0, 0 }, + { 4729, 2369, 2391, 0, 0 }, + { 4742, 2411, 2434, 0, 0 }, + { 4756, 2455, 2455, 0, 0 }, + { 4769, 2478, 2478, 0, 0 }, + { 4783, 2501, 2522, 0, 0 }, + { 4798, 2543, 2543, 0, 0 }, + { 4809, 1762, 1786, 0, 0 }, + { 4824, 2560, 2583, 0, 0 }, + { 4837, 2011, 2011, 0, 0 }, + { 4850, 2604, 2604, 0, 0 }, + { 4865, 2604, 2604, 0, 0 }, + { 4879, 2053, 2075, 0, 0 }, + { 4893, 2033, 2033, 0, 0 }, + { 4906, 1945, 1971, 0, 0 }, + { 4923, 2615, 2615, 0, 0 }, + { 4941, 2615, 2615, 0, 0 }, + { 4954, 1704, 1704, 0, 0 }, + { 4966, 2145, 2165, 0, 0 }, + { 4977, 2145, 2165, 0, 0 }, + { 4988, 2629, 2651, 0, 0 }, + { 5001, 2671, 2671, 0, 0 }, + { 5015, 2694, 2719, 0, 0 }, + { 5027, 2250, 2250, 0, 0 }, + { 5039, 134, 165, 0, 0 }, + { 5052, 1945, 1971, 0, 0 }, + { 5070, 1945, 1971, 0, 0 }, + { 5087, 2742, 2761, 0, 0 }, + { 5097, 1726, 1742, 0, 0 }, + { 5107, 1930, 1930, 0, 0 }, + { 5123, 2455, 2455, 0, 0 }, + { 5138, 2778, 2778, 0, 0 }, + { 5153, 1704, 1704, 0, 0 }, + { 5164, 1726, 1742, 0, 0 }, + { 5178, 1726, 1742, 0, 0 }, + { 5193, 2799, 2799, 0, 0 }, + { 5206, 1704, 1704, 0, 0 }, + { 5218, 1930, 1930, 0, 0 }, + { 5230, 2629, 2651, 0, 0 }, + { 5244, 2812, 2837, 0, 0 }, + { 5259, 2778, 2860, 0, 0 }, + { 5270, 2145, 2165, 0, 0 }, + { 5284, 2881, 2881, 0, 0 }, + { 5299, 2629, 2651, 0, 0 }, + { 5318, 2905, 2926, 0, 0 }, + { 5330, 2812, 2837, 0, 0 }, + { 5344, 2947, 2969, 0, 0 }, + { 5357, 2989, 3008, 0, 0 }, + { 5369, 2501, 2522, 0, 0 }, + { 5383, 3027, 3027, 0, 0 }, + { 5395, 3027, 3027, 0, 0 }, + { 5408, 3039, 3039, 0, 0 }, + { 5419, 1945, 1971, 0, 0 }, + { 5430, 2671, 2671, 0, 0 }, + { 5449, 2095, 2121, 0, 0 }, + { 5466, 2095, 2121, 0, 0 }, + { 5482, 2011, 2011, 0, 0 }, + { 5494, 3059, 3085, 0, 0 }, + { 5508, 1930, 1930, 0, 0 }, + { 5523, 3059, 3085, 0, 0 }, + { 5540, 2053, 2075, 0, 0 }, + { 5553, 2799, 2799, 0, 0 }, + { 5565, 3109, 3137, 0, 0 }, + { 5584, 3163, 3185, 0, 0 }, + { 5597, 3205, 3226, 0, 0 }, + { 5613, 403, 626, 0, 0 }, + { 5630, 194, 225, 0, 0 }, + { 5646, 3245, 3245, 0, 0 }, + { 5666, 194, 225, 0, 0 }, + { 5682, 194, 225, 0, 0 }, + { 5697, 37, 68, 0, 0 }, + { 5716, 194, 225, 0, 0 }, + { 5733, 0, 0, 0, 0 }, + { 5752, 3270, 3270, 0, 0 }, + { 5775, 0, 0, 0, 0 }, + { 5794, 3289, 3320, 0, 0 }, + { 5811, 1538, 1571, 0, 0 }, + { 5825, 3349, 3382, 0, 0 }, + { 5844, 1538, 1571, 0, 0 }, + { 5863, 3349, 3382, 0, 0 }, + { 5885, 1538, 1571, 0, 0 }, + { 5904, 1538, 1571, 0, 0 }, + { 5921, 3349, 3349, 0, 0 }, + { 5938, 3415, 3456, 0, 0 }, + { 5954, 1538, 1571, 0, 0 }, + { 5971, 3497, 3521, 0, 0 }, + { 5985, 1538, 1571, 0, 0 }, + { 6004, 3497, 3521, 0, 0 }, + { 6024, 1538, 1571, 0, 0 }, + { 6044, 1538, 1571, 0, 0 }, + { 6058, 3349, 3349, 0, 0 }, + { 6074, 1470, 3545, 0, 0 }, + { 6090, 1538, 1571, 0, 0 }, + { 6111, 3349, 3382, 0, 0 }, + { 6127, 1538, 1571, 0, 0 }, + { 6144, 1538, 1571, 0, 0 }, + { 6163, 1538, 1571, 0, 0 }, + { 6182, 1470, 3545, 0, 0 }, + { 6197, 3349, 3382, 0, 0 }, + { 6218, 876, 895, 0, 0 }, + { 6230, 1298, 1332, 0, 0 }, + { 6247, 426, 449, 0, 0 }, + { 6259, 649, 670, 0, 0 }, + { 6271, 37, 68, 0, 0 }, + { 6275, 582, 604, 0, 0 }, + { 6283, 403, 626, 0, 0 }, + { 6299, 582, 604, 0, 0 }, + { 6314, 560, 779, 0, 0 }, + { 6329, 733, 756, 497, 109 }, + { 6345, 1416, 1443, 0, 0 }, + { 6365, 912, 934, 606, 113 }, + { 6380, 582, 582, 0, 0 }, + { 6400, 855, 866, 0, 0 }, + { 6413, 1378, 1398, 0, 0 }, + { 6431, 3578, 3606, 0, 0 }, + { 6450, 1037, 1056, 0, 0 }, + { 6455, 134, 165, 0, 0 }, + { 6459, 560, 560, 0, 0 }, + { 6463, 560, 779, 0, 0 }, + { 6471, 134, 165, 0, 0 }, + { 6477, 0, 0, 0, 0 }, + { 6482, 0, 0, 0, 0 }, + { 6490, 0, 0, 0, 0 }, + { 6500, 3632, 3632, 0, 0 }, + { 6510, 3642, 3642, 0, 0 }, + { 6521, 3652, 3652, 0, 0 }, + { 6532, 3662, 3662, 0, 0 }, + { 6543, 3672, 3672, 0, 0 }, + { 6553, 835, 835, 0, 0 }, + { 6563, 3682, 3682, 0, 0 }, + { 6573, 1015, 1015, 0, 0 }, + { 6583, 3692, 3692, 0, 0 }, + { 6593, 866, 866, 0, 0 }, + { 6603, 3702, 3702, 0, 0 }, + { 6613, 3712, 3712, 0, 0 }, + { 6623, 0, 0, 0, 0 }, + { 6633, 291, 291, 0, 0 }, + { 6643, 3722, 3722, 0, 0 }, + { 6654, 3732, 3732, 0, 0 }, + { 6665, 3742, 3742, 0, 0 }, + { 6676, 3752, 3752, 0, 0 }, + { 6687, 3762, 3762, 0, 0 }, + { 6698, 281, 281, 0, 0 }, + { 6708, 1752, 1752, 0, 0 }, + { 6718, 3772, 3772, 0, 0 }, + { 6728, 1742, 1742, 0, 0 }, + { 6738, 2011, 2011, 0, 0 }, + { 6748, 3782, 3782, 0, 0 }, + { 6758, 3792, 3792, 0, 0 }, + { 6768, 3802, 3802, 0, 0 }, + { 6778, 0, 0, 0, 0 }, + { 6787, 0, 0, 0, 0 }, + { 6801, 3812, 3812, 0, 0 }, + { 6809, 3812, 3812, 0, 0 }, + { 6817, 3812, 3812, 0, 0 }, + { 6831, 3812, 3812, 0, 0 }, + { 6840, 37, 68, 0, 0 }, + { 6857, 37, 68, 0, 0 }, + { 6872, 3839, 3860, 0, 0 }, + { 6889, 134, 165, 0, 0 }, + { 6903, 0, 3879, 0, 0 }, + { 6918, 37, 68, 0, 0 }, + { 6934, 37, 68, 0, 0 }, + { 6948, 37, 68, 0, 0 }, + { 6966, 37, 68, 0, 0 }, + { 6982, 134, 165, 0, 0 }, + { 6999, 37, 68, 0, 0 }, + { 7015, 37, 68, 0, 0 }, + { 7031, 134, 165, 0, 0 }, + { 7047, 37, 68, 0, 0 }, + { 7065, 0, 3899, 719, 132 }, + { 7079, 37, 68, 0, 0 }, + { 7096, 0, 291, 0, 0 }, + { 7112, 134, 165, 0, 0 }, + { 7128, 0, 291, 0, 0 }, + { 7147, 2411, 2434, 0, 0 }, + { 7163, 0, 291, 0, 0 }, + { 7177, 134, 165, 0, 0 }, + { 7196, 134, 165, 0, 0 }, + { 7208, 3919, 3940, 0, 0 }, + { 7221, 134, 165, 0, 0 }, + { 7233, 194, 225, 0, 0 }, + { 7247, 37, 68, 0, 0 }, + { 7264, 0, 3879, 0, 0 }, + { 7278, 37, 68, 0, 0 }, + { 7296, 37, 68, 0, 0 }, + { 7310, 37, 68, 0, 0 }, + { 7323, 134, 165, 0, 0 }, + { 7340, 3919, 3940, 0, 0 }, + { 7353, 37, 68, 0, 0 }, + { 7367, 3919, 3940, 0, 0 }, + { 7381, 134, 165, 0, 0 }, + { 7396, 37, 68, 0, 0 }, + { 7408, 37, 68, 0, 0 }, + { 7421, 37, 68, 0, 0 }, + { 7438, 37, 68, 0, 0 }, + { 7452, 134, 165, 0, 0 }, + { 7464, 37, 68, 0, 0 }, + { 7476, 3839, 3860, 0, 0 }, + { 7490, 37, 68, 0, 0 }, + { 7508, 37, 68, 0, 0 }, + { 7524, 3839, 3860, 0, 0 }, + { 7539, 3919, 3940, 0, 0 }, + { 7557, 37, 68, 0, 0 }, + { 7571, 134, 165, 0, 0 }, + { 7584, 37, 68, 0, 0 }, + { 7601, 134, 165, 0, 0 }, + { 7616, 37, 68, 0, 0 }, + { 7630, 134, 165, 0, 0 }, + { 7646, 3839, 3860, 0, 0 }, + { 7663, 134, 165, 0, 0 }, + { 7679, 37, 68, 0, 0 }, + { 7692, 37, 68, 0, 0 }, + { 7707, 37, 68, 0, 0 }, + { 7721, 134, 165, 0, 0 }, + { 7736, 3919, 3940, 0, 0 }, + { 7753, 37, 68, 0, 0 }, + { 7767, 37, 68, 0, 0 }, + { 7781, 134, 165, 0, 0 }, + { 7799, 37, 68, 0, 0 }, + { 7813, 0, 3879, 0, 0 }, + { 7816, 0, 3879, 0, 0 }, + { 7824, 0, 0, 0, 0 }, + { 7828, 0, 0, 0, 0 }, + { 7834, 0, 0, 0, 0 }, + { 7840, 0, 0, 0, 0 }, + { 7845, 0, 0, 0, 0 }, + { 7855, 301, 301, 0, 0 }, + { 7859, 2285, 2309, 0, 0 }, + { 7868, 0, 0, 0, 0 }, + { 7876, 20, 20, 0, 0 }, + { 7896, 3959, 3959, 0, 0 }, + { 7910, 3977, 3977, 0, 0 }, + { 7927, 3999, 3999, 0, 0 }, + { 7940, 20, 20, 0, 0 }, + { 7954, 4018, 4018, 0, 0 }, + { 7971, 4051, 4051, 0, 0 }, + { 7983, 4067, 4067, 0, 0 }, + { 7999, 4081, 4105, 0, 0 }, + { 8016, 20, 20, 0, 0 }, + { 8031, 4127, 4127, 0, 0 }, + { 8046, 2989, 3008, 0, 0 }, + { 8051, 2501, 2522, 0, 0 }, + { 8058, 560, 779, 0, 0 }, + { 8066, 3039, 3039, 0, 0 }, + { 8072, 4141, 4141, 0, 0 }, + { 8082, 134, 165, 0, 0 }, + { 8088, 37, 68, 0, 0 }, + { 8092, 733, 733, 0, 0 }, + { 8096, 733, 756, 0, 0 }, + { 8104, 912, 934, 0, 0 }, + { 8121, 1075, 1105, 0, 0 }, + { 8136, 582, 604, 0, 0 }, + { 8151, 1616, 1642, 0, 0 }, + { 8154, 4163, 4185, 0, 0 }, + { 8162, 733, 756, 0, 0 }, + { 8169, 2145, 2165, 0, 0 }, + { 8173, 912, 934, 0, 0 }, + { 8181, 4207, 4227, 0, 0 }, + { 8194, 1616, 1642, 0, 0 }, + { 8211, 3732, 3732, 0, 0 }, + { 8232, 4163, 4185, 0, 0 }, + { 8248, 4247, 4247, 0, 0 }, + { 8262, 3578, 3606, 0, 0 }, + { 8277, 4258, 4280, 0, 0 }, + { 8291, 4300, 4300, 0, 0 }, + { 8309, 4321, 4321, 0, 0 }, + { 8325, 4334, 4353, 0, 0 }, + { 8338, 4370, 4370, 0, 0 }, + { 8355, 4382, 3692, 0, 0 }, + { 8373, 4397, 4397, 0, 0 }, + { 8389, 4410, 4410, 0, 0 }, + { 8409, 4431, 3722, 0, 0 }, + { 8422, 301, 301, 0, 0 }, + { 8439, 301, 301, 0, 0 }, + { 8456, 4300, 4300, 0, 0 }, + { 8471, 4454, 4454, 0, 0 }, + { 8490, 4472, 4472, 0, 0 }, + { 8505, 4141, 4141, 0, 0 }, + { 8523, 4141, 4141, 0, 0 }, + { 8538, 4484, 4484, 0, 0 }, + { 8556, 4499, 4499, 0, 0 }, + { 8571, 4528, 4528, 0, 0 }, + { 8585, 4539, 4539, 0, 0 }, + { 8598, 4549, 4578, 0, 0 }, + { 8614, 4607, 4635, 0, 0 }, + { 8629, 4499, 4499, 0, 0 }, + { 8647, 4661, 4661, 0, 0 }, + { 8661, 4672, 4672, 0, 0 }, + { 8678, 4686, 4686, 0, 0 }, + { 8694, 4686, 4686, 0, 0 }, + { 8709, 4699, 4699, 0, 0 }, + { 8730, 4721, 4748, 0, 0 }, + { 8748, 4431, 3722, 0, 0 }, + { 8763, 4499, 4499, 0, 0 }, + { 8777, 4773, 4773, 0, 0 }, + { 8792, 4785, 4785, 0, 0 }, + { 8807, 4806, 4826, 0, 0 }, + { 8825, 4247, 4247, 0, 0 }, + { 8838, 4844, 4844, 0, 0 }, + { 8851, 4861, 4861, 0, 0 }, + { 8866, 4247, 4247, 0, 0 }, + { 8878, 37, 68, 0, 0 }, + { 8885, 194, 225, 0, 0 }, + { 8894, 2905, 2926, 0, 0 }, + { 8898, 2778, 2860, 0, 0 }, + { 8902, 2881, 2881, 0, 0 }, + { 8912, 2411, 2434, 0, 0 }, + { 8919, 3812, 3812, 0, 0 }, + { 8923, 361, 382, 0, 0 }, + { 8933, 301, 331, 0, 0 }, + { 8945, 733, 733, 0, 0 }, + { 8956, 582, 604, 0, 0 }, + { 8967, 560, 779, 0, 0 }, + { 8983, 560, 779, 0, 0 }, + { 8994, 301, 301, 0, 0 }, + { 9004, 582, 604, 0, 0 }, + { 9022, 560, 779, 0, 0 }, + { 9034, 733, 756, 0, 0 }, + { 9046, 912, 934, 0, 0 }, + { 9057, 4499, 4499, 0, 0 }, + { 9066, 3812, 3812, 0, 0 }, + { 9070, 3812, 3812, 0, 0 }, + { 9080, 3919, 3940, 0, 0 }, + { 9085, 194, 225, 0, 0 }, + { 9089, 3812, 3812, 0, 0 }, +}; +#endif diff --git a/scripts/gen-timezone-names.mjs b/scripts/gen-timezone-names.mjs new file mode 100644 index 000000000..69aab583e --- /dev/null +++ b/scripts/gen-timezone-names.mjs @@ -0,0 +1,292 @@ +#!/usr/bin/env node +// Long time zone names for Date.prototype.toString, toTimeString and +// String(date), derived from the pinned Node release's ICU. +// +// node scripts/gen-timezone-names.mjs [--tzdata ] regenerate the table +// node scripts/gen-timezone-names.mjs [--tzdata ] --check verify only +// +// Node prints the zone's long English name for standard or daylight time +// after the GMT offset ("GMT-0500 (Central Daylight Time)"). The name +// depends only on the zone and on whether the instant is in daylight time +// (V8 maps instants outside 1970 to 2038-01-19 into that range), so each +// zone ID in the IANA tzdata (zones and links, read from tzdata.zi) is +// sampled once a day over that range with TZ set to it, and the two names +// Node prints are recorded. Where the name changes together with the UTC +// offset, the name at the larger offset is the daylight name. Zones +// without a CLDR name print Node's "GMT+hh:mm" fallback, which is recorded +// as is. +// +// The runtime takes daylight time from tm_isdst, except for two kinds of +// zones, which get the exact instants at which Node's name changes: +// zones whose tzdata rules use a negative daylight saving (Europe/Dublin's +// winter time, Morocco's Ramadan time), where ICU's data calls the larger +// offset daylight time, and zones that Node shows in daylight time through +// 2038 (British Columbia's permanent UTC-7, which tzdata names standard +// time). +import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { availableParallelism } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const self = fileURLToPath(import.meta.url); +const root = fileURLToPath(new URL("..", import.meta.url)); +const out = join(root, "packages/runtime/src/scr_timezone_data.h"); + +// V8 names the zone of instants in [0, 2^31 - 1] seconds directly. +const FIRST_MS = 0; +const LAST_MS = 2147483647000; +const DAY_MS = 86400000; + +const zoneName = (ms) => { + const text = new Date(ms).toString(); + return text.slice(text.indexOf("(") + 1, -1); +}; + +if (process.argv.includes("--sample")) { + // One zone, selected by TZ: the first name printed, then every change of + // name with the exact instant (ms) and the UTC offsets (minutes) before + // and after it. + const changes = []; + let previous = null; + const visit = (ms) => { + const name = zoneName(ms); + const offset = -new Date(ms).getTimezoneOffset(); + if (previous === null) { + changes.push({ at: ms, name, offset }); + } else if (previous.name !== name) { + // The first second with the new name. + let lo = previous.ms / 1000; + let hi = ms / 1000; + while (hi - lo > 1) { + const mid = Math.floor((lo + hi) / 2); + if (zoneName(mid * 1000) === name) hi = mid; + else lo = mid; + } + changes.push({ at: hi * 1000, from: previous, name, offset }); + } + previous = { ms, name, offset }; + }; + for (let ms = FIRST_MS; ms < LAST_MS; ms += DAY_MS) visit(ms); + visit(LAST_MS); + process.stdout.write(JSON.stringify(changes)); + process.exit(0); +} + +const tzdataIndex = process.argv.indexOf("--tzdata"); +const tzdata = tzdataIndex >= 0 ? process.argv[tzdataIndex + 1] : "/usr/share/zoneinfo/tzdata.zi"; +const checkOnly = process.argv.includes("--check"); + +/** Zone and link names in tzdata.zi, and the zones that save a negative + * amount at some time from 1970 on. Links name their target. */ +function readTzdata(path) { + const text = readFileSync(path, "utf8"); + const zones = new Map(); + const negativeRules = new Set(); + const negativeZones = new Set(); + const year = (field) => (field === undefined ? Infinity : Number.parseInt(field, 10)); + let zone = null; + for (const line of text.split("\n")) { + const fields = line.trim().split(/\s+/); + if (line.startsWith("R ")) { + // R NAME FROM TO - IN ON AT SAVE LETTER, TO "o" (only) or "ma" (max). + zone = null; + const to = + fields[3] === "o" ? year(fields[2]) : fields[3] === "ma" ? Infinity : year(fields[3]); + if (fields[8]?.startsWith("-") && to >= 1970) negativeRules.add(fields[1]); + continue; + } + if (line.startsWith("L ")) { + zone = null; + zones.set(fields[2], fields[1]); + continue; + } + // Z NAME STDOFF RULES FORMAT [UNTIL], continued by STDOFF RULES FORMAT [UNTIL]. + let rest; + if (line.startsWith("Z ")) { + zone = fields[1]; + zones.set(zone, zone); + rest = fields.slice(2); + } else if (zone && fields.length >= 3 && !line.startsWith("#")) { + rest = fields; + } else { + zone = null; + continue; + } + const [, rule, , until] = rest; + if (year(until) >= 1970 && (negativeRules.has(rule) || /^-\d/.test(rule))) + negativeZones.add(zone); + } + const version = /^# version (\S+)/.exec(text)?.[1] ?? "unknown"; + return { zones, negativeZones, version }; +} + +/** The zone's [standard, daylight] names as Node prints them: at a change + * of name that also changes the UTC offset, the name printed at the larger + * offset is the daylight name; the name chosen at most such changes wins. */ +function classify(id, changes) { + const names = new Set(changes.map((c) => c.name)); + if (names.size > 2) throw new Error(`${id}: more than two names`); + if (names.size === 1) { + const [name] = names; + return [name, name]; + } + // Zones that moved between offsets keep one daylight name, so the vote + // can include a transition that only changed the standard offset. + const votes = new Map(); + for (const c of changes) { + if (!c.from || c.from.offset === c.offset) continue; + const name = c.offset > c.from.offset ? c.name : c.from.name; + votes.set(name, (votes.get(name) ?? 0) + 1); + } + const ranked = [...votes].sort((a, b) => b[1] - a[1]); + if (ranked.length === 0 || (ranked.length === 2 && ranked[0][1] === ranked[1][1])) + throw new Error(`${id}: no daylight name`); + const daylight = ranked[0][0]; + const standard = [...names].find((n) => n !== daylight); + return [standard, daylight]; +} + +function sample(id) { + return new Promise((resolve, reject) => { + const env = { ...process.env, TZ: id, LANG: "en_US.UTF-8" }; + delete env.LC_ALL; + delete env.LC_MESSAGES; + const child = spawn(process.execPath, [self, "--sample"], { env }); + let text = ""; + child.stdout.on("data", (chunk) => (text += chunk)); + child.on("error", reject); + child.on("close", (code) => + code === 0 ? resolve(JSON.parse(text)) : reject(new Error(`${id}: sampler exited ${code}`)), + ); + }); +} + +const { zones, negativeZones, version } = readTzdata(tzdata); +const ids = [...zones.keys()] + .filter((id) => { + try { + new Intl.DateTimeFormat("en-US", { timeZone: id }); + return true; + } catch { + return false; + } + }) + .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + +const samples = new Map(); +let next = 0; +await Promise.all( + Array.from({ length: availableParallelism() }, async () => { + while (next < ids.length) { + const id = ids[next++]; + samples.set(id, await sample(id)); + } + }), +); + +// Pools of NUL-terminated strings, addressed by byte offset. +function pool() { + const offsets = new Map(); + let text = ""; + return { + add(s) { + if (!offsets.has(s)) { + offsets.set(s, Buffer.byteLength(text)); + text += s + "\0"; + } + return offsets.get(s); + }, + get text() { + return text; + }, + }; +} +const idPool = pool(); +const namePool = pool(); +const changeSeconds = []; +const rows = ids.map((id) => { + const changes = samples.get(id); + const [standard, daylight] = classify(id, changes); + // Exact daylight-time changes, as epoch seconds at which daylight time + // starts or ends; INT32_MIN first when 1970 starts in daylight time. + let first = 0; + let count = 0; + // Daylight time with no change in the last year sampled. + const last = changes.at(-1); + const permanent = last.name === daylight && LAST_MS - last.at > 366 * DAY_MS; + if (standard !== daylight && (negativeZones.has(zones.get(id)) || permanent)) { + first = changeSeconds.length; + if (changes[0].name === daylight) changeSeconds.push("INT32_MIN"); + for (const c of changes.slice(1)) changeSeconds.push(String(c.at / 1000)); + count = changeSeconds.length - first; + } + return ` { ${idPool.add(id)}, ${namePool.add(standard)}, ${namePool.add(daylight)}, ${first}, ${count} },`; +}); +// The runtime formats into a fixed buffer. +for (const [standard, daylight] of ids.map((id) => classify(id, samples.get(id)))) { + if (Buffer.byteLength(standard) > 64 || Buffer.byteLength(daylight) > 64) + throw new Error(`zone name longer than 64 bytes: ${standard} / ${daylight}`); +} +for (const p of [idPool, namePool]) { + if (Buffer.byteLength(p.text) > 0xffff) throw new Error("string pool exceeds 16-bit offsets"); +} + +const cString = (text) => + text + .split("\0") + .slice(0, -1) + .map((s) => ` "${s.replace(/[\\"]/g, "\\$&")}\\0"`) + .join("\n"); + +const wrap = (items) => { + const lines = []; + let line = " "; + for (const item of items) { + if (line.length + item.length + 2 > 100) { + lines.push(line); + line = " "; + } + line += ` ${item},`; + } + if (line.trim()) lines.push(line); + return lines.join("\n"); +}; + +const header = `/* Generated by scripts/gen-timezone-names.mjs from Node ${process.version} + * (ICU ${process.versions.icu}, CLDR ${process.versions.cldr}) and tzdata ${version}. Do not edit. + * Each IANA zone or link ID maps to the long English names Node prints for + * its standard and daylight time. A row with change_count > 0 takes + * daylight time from scr_tz_daylight_changes[first_change ...]: the epoch + * seconds at which daylight time starts or ends between 1970 and + * 2038-01-19 (INT32_MIN first when 1970 starts in daylight time). Rows are + * sorted by ID. */ +#ifndef SCR_TIMEZONE_DATA_H +#define SCR_TIMEZONE_DATA_H +#include +typedef struct { uint16_t id, standard, daylight, first_change, change_count; } ScrTzNames; +static const char scr_tz_ids[] = +${cString(idPool.text)}; +static const char scr_tz_name_text[] = +${cString(namePool.text)}; +static const int32_t scr_tz_daylight_changes[] = { +${wrap(changeSeconds)} +}; +static const ScrTzNames scr_tz_names[] = { +${rows.join("\n")} +}; +#endif +`; + +if (checkOnly) { + const current = readFileSync(out, "utf8"); + if (current !== header) { + process.stderr.write(`${out} is out of date; run node scripts/gen-timezone-names.mjs\n`); + process.exit(1); + } + process.stdout.write(`${out} is up to date (${ids.length} zones)\n`); +} else { + await writeFile(out, header); + process.stdout.write(`wrote ${out} (${ids.length} zones)\n`); +} diff --git a/tests/corpus/5963-date-to-string.ts b/tests/corpus/5963-date-to-string.ts new file mode 100644 index 000000000..3b3024f3c --- /dev/null +++ b/tests/corpus/5963-date-to-string.ts @@ -0,0 +1,93 @@ +// Date.prototype.toString, toDateString and toTimeString, String(date), +// template literals and string concatenation print local time with the +// GMT offset and the zone's long name, as Node does. Each write to +// process.env.TZ changes the zone for later conversions and getters. +function at(ms: number): Date { + return new Date(ms); +} +const winter = at(Date.UTC(2026, 0, 15, 12, 30, 45, 678)); +const summer = at(Date.UTC(2026, 6, 15, 3, 4, 5)); +const epoch = at(0); +const beforeEpoch = at(Date.UTC(1950, 5, 1, 18)); +const farFuture = at(Date.UTC(2100, 6, 4, 9)); +const yearZero = at(-62167219200000); +const maxDate = at(8.64e15); +const minDate = at(-8.64e15); +const invalid = new Date(NaN); + +function show(zone: string): void { + process.env.TZ = zone; + console.log(`== ${zone}`); + console.log(winter.toString()); + console.log(String(summer)); + console.log(`${epoch} | ${beforeEpoch}`); + console.log("far: " + farFuture); + console.log(winter.toDateString(), "/", winter.toTimeString()); + console.log(summer.toDateString(), "/", summer.toTimeString()); + console.log(summer.getHours(), summer.getTimezoneOffset()); +} + +for (const zone of [ + "America/Chicago", + "America/New_York", + "America/St_Johns", + "Europe/Dublin", + "Europe/London", + "Europe/Berlin", + "Asia/Kolkata", + "Asia/Kathmandu", + "Asia/Tokyo", + "Australia/Lord_Howe", + "Australia/Sydney", + "Pacific/Chatham", + "America/Sao_Paulo", + "UTC", + "Etc/GMT+5", + "Etc/GMT-14", + "US/Pacific", + ":Asia/Seoul", +]) { + show(zone); +} + +// Daylight-time edges: the instants before and at a change. +process.env.TZ = "America/Chicago"; +const spring = Date.UTC(2026, 2, 8, 8); +const autumn = Date.UTC(2026, 10, 1, 7); +console.log(new Date(spring - 1000).toString(), "|", new Date(spring).toString()); +console.log(new Date(autumn - 1000).toTimeString(), "|", new Date(autumn).toTimeString()); +process.env.TZ = "Europe/Dublin"; +console.log(new Date(Date.UTC(2026, 2, 29, 0, 59, 59)).toString()); +console.log(new Date(Date.UTC(2026, 2, 29, 1)).toString()); +process.env.TZ = "Australia/Sydney"; +console.log(new Date(Date.UTC(2026, 3, 4, 15, 59, 59)).toString()); +console.log(new Date(Date.UTC(2026, 3, 4, 16)).toString()); + +// Extreme and invalid time values. +process.env.TZ = "UTC"; +console.log(maxDate.toString(), "|", minDate.toDateString()); +console.log(yearZero.toString(), "|", at(Date.UTC(-1, 11, 31, 23)).toDateString()); +console.log(at(Date.UTC(-12345, 0, 1)).toDateString(), "|", at(Date.UTC(12345, 0, 1)).toDateString()); +console.log(invalid.toString(), invalid.toDateString(), invalid.toTimeString(), `${invalid}`, String(invalid)); + +// A Date held as unknown converts the same way. +process.env.TZ = "Asia/Kolkata"; +const held: unknown = winter; +console.log(String(held), `${held}`); + +// Values that are not zone IDs and unset TZ. +for (const zone of ["Not/A_Zone", ""]) { + process.env.TZ = zone; + console.log(JSON.stringify(zone), winter.toString()); +} +delete process.env.TZ; +process.env.TZ = "UTC"; +console.log(winter.toString()); + +// Formatting functions and fields. +function stamp(label: string, d: Date): string { + return `${label}: ${d.toDateString()} at ${d.toTimeString().slice(0, 8)}`; +} +const event = { name: "launch", when: summer }; +process.env.TZ = "America/New_York"; +console.log(stamp(event.name, event.when), "|", event.when.toString().length); diff --git a/tests/corpus/5964-date-dynamic-conversions.js b/tests/corpus/5964-date-dynamic-conversions.js new file mode 100644 index 000000000..c2e5aff0d --- /dev/null +++ b/tests/corpus/5964-date-dynamic-conversions.js @@ -0,0 +1,29 @@ +// Dates held in untyped JavaScript values convert like Node's: string +// conversions use toString() (String, templates, +, ==, join), number +// conversions use the time value (Number, BigInt, unary +, -, *, <, >=), +// and toString, toDateString and toTimeString are callable as methods and +// as function values. +process.env.TZ = "America/Chicago"; +const date = new Date(Date.UTC(2026, 9, 18, 14, 12, 30)); +const held = [date, new Date(NaN), new Date(0)]; +const x = /** @type {any} */ (held[0]); +const bad = /** @type {any} */ (held[1]); +const zero = /** @type {any} */ (held[2]); + +console.log(String(x)); +console.log(`${x} / ${bad}`); +console.log(x.toString(), "|", x.toDateString(), "|", x.toTimeString()); +console.log(held.join(" ; ")); +console.log(x + 1, "|", 1 + x, "|", x + ""); +console.log(+x, x - 0, x * 1, Number(x), Number(bad), BigInt(zero)); +console.log(x > 5, x < zero, x >= x, x <= 1792332720000, bad < x, bad >= x); +console.log(x == x.toString(), x == 1792332750000, zero == "0"); + +const toTime = x.toTimeString; +const toDate = x.toDateString; +console.log(toTime.call(x), "|", toDate.call(zero)); + +process.env.TZ = "Asia/Kathmandu"; +console.log(String(x), "|", [x, zero].toString()); +process.env.TZ = "Europe/Dublin"; +console.log(`${zero}`, "|", zero.toTimeString()); diff --git a/tests/diagnostics/date-formatters.ts b/tests/diagnostics/date-formatters.ts index 6a2cb57a7..1ec1cacc6 100644 --- a/tests/diagnostics/date-formatters.ts +++ b/tests/diagnostics/date-formatters.ts @@ -1,5 +1,5 @@ // Unsupported formatters must reach the Date method diagnostic, including // names inherited from Object.prototype by the compiler's dispatch table. const date = new Date(0); -date.toString(); +date.toUTCString(); date.toLocaleString(); diff --git a/tests/harness/__snapshots__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 66d98d861..6fbe82dd6 100644 --- a/tests/harness/__snapshots__/date-formatters.ts.txt +++ b/tests/harness/__snapshots__/date-formatters.ts.txt @@ -1,17 +1,17 @@ -date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the standard library types but has no scriptc lowering yet +date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toUTCString' is part of the standard library types but has no scriptc lowering yet 3 | const date = new Date(0); - 4 | date.toString(); - | ^~~~~~~~~~~~~~~ + 4 | date.toUTCString(); + | ^~~~~~~~~~~~~~~~~~ 5 | date.toLocaleString(); - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet - 4 | date.toString(); + 4 | date.toUTCString(); 5 | date.toLocaleString(); | ^~~~~~~~~~~~~~~~~~~~~ 6 | - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file + hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 337354eec..34e6d5a3c 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 8131c1cd2..9fd80a3fd 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2190,6 +2190,8 @@ "5960-generic-mapped-types.ts": 2540, "5961-frame-spread-updates.ts": 3260, "5962-frame-tuples.ts": 3208, + "5963-date-to-string.ts": 2355, + "5964-date-dynamic-conversions.js": 2307, "5970-net-connect-host-name.ts": 2400, "5971-buffer-chunks-to-string.ts": 2700, "5972-net-set-encoding-listeners.ts": 2550, @@ -5249,6 +5251,8 @@ "5960-generic-mapped-types.ts": 3300, "5961-frame-spread-updates.ts": 3732, "5962-frame-tuples.ts": 3372, + "5963-date-to-string.ts": 3199, + "5964-date-dynamic-conversions.js": 3084, "5970-net-connect-host-name.ts": 2800, "5971-buffer-chunks-to-string.ts": 3100, "5972-net-set-encoding-listeners.ts": 3000, From c48983889c5f509b7c35bda0b656e2b251fceaf0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:35:23 -0500 Subject: [PATCH 089/120] Clear timers through handles that may be undefined or null clearTimeout, clearInterval and clearImmediate refused a handle typed `Timeout | undefined` or `Timeout | null`, the type of a timer variable that starts empty (`let timer: ReturnType | undefined`) and of `handles[i]!` reads. Such a handle now passes id 0 when it is absent, which matches no timer, so the clear does nothing, as in Node. --- .../src/frontend/lowering/lower-timers.ts | 28 ++++-- .../5684-clear-optional-timer-handles.ts.json | 6 ++ .../5684-clear-optional-timer-handles.ts | 89 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json create mode 100644 tests/corpus/5684-clear-optional-timer-handles.ts diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 44b2b6a1d..c2a54dfc0 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -6,6 +6,7 @@ import { canBoxFuncIntoDyn, DYN, F64, + isUnitType, VOID, type IrExpr, type IrType, @@ -299,6 +300,19 @@ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLo return null; } +/** A clear's handle as the f64 id it passes to the runtime. A handle that + * may be undefined or null (`let timer: Timeout | undefined`) passes 0 + * when absent: no timer has id 0, so the clear does nothing, as Node's + * clearTimeout(undefined) does. Other handle types are returned as they + * are. */ +function optionalTimerHandle(lowerer: Lowerer, handle: IrExpr, loc: SrcLoc): IrExpr { + if (handle.type.kind !== "union") return handle; + const arms = lowerer.unions.get(handle.type.unionId)?.arms ?? []; + const present = arms.filter((arm) => !isUnitType(arm)); + if (present.length !== 1 || present[0]!.kind !== "f64") return handle; + return { kind: "nullish", left: handle, right: numLit(0, loc), type: F64, loc }; +} + const MOCKABLE_TIMER_FNS: ReadonlySet = new Set([ "timers.setTimeout", "timers.setTimeoutHandle", @@ -392,8 +406,7 @@ function lowerTimerCall( return { kind: "libCall", fn: "timers.setTimeout", args: [cb, ms], type: VOID, loc }; } // clearTimeout(handle): shares the interval clear (the handle ids - // share one space). A `Timeout | null` handle narrows first, like - // clearInterval. + // share one space). An optional handle clears nothing when absent. if (member === "clearTimeout") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; @@ -419,11 +432,12 @@ function lowerTimerCall( loc, }; } + handle = optionalTimerHandle(lowerer, handle, loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearTimeout of '${lowerer.fmt(handle.type)}' handles`, expr.arguments[0]!, - "the handle is the Timeout setTimeout returned (narrow 'Timeout | null' first)", + "the handle is the Timeout setTimeout returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearTimeout", args: [handle], type: VOID, loc }; @@ -434,12 +448,12 @@ function lowerTimerCall( if (expr.arguments.length !== 1) { lowerer.noLowering(`clearInterval with ${expr.arguments.length} arguments`, expr); } - const handle = lowerer.lowerExpr(expr.arguments[0]!); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearInterval of '${lowerer.fmt(handle.type)}' handles`, expr.arguments[0]!, - "the handle is the number setInterval returned (narrow `number | null` first)", + "the handle is the Timeout setInterval returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearInterval", args: [handle], type: VOID, loc }; @@ -478,12 +492,12 @@ function lowerTimerCall( if (expr.arguments.length !== 1) { lowerer.noLowering(`clearImmediate with ${expr.arguments.length} arguments`, expr); } - const handle = lowerer.lowerExpr(expr.arguments[0]!); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearImmediate of '${lowerer.fmt(handle.type)}' handles`, expr.arguments[0]!, - "the handle is the Immediate setImmediate returned (narrow 'Immediate | undefined' first)", + "the handle is the Immediate setImmediate returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearImmediate", args: [handle], type: VOID, loc }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json new file mode 100644 index 000000000..307a74fa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5684-clear-optional-timer-handles.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5684-clear-optional-timer-handles.ts b/tests/corpus/5684-clear-optional-timer-handles.ts new file mode 100644 index 000000000..45a2f48aa --- /dev/null +++ b/tests/corpus/5684-clear-optional-timer-handles.ts @@ -0,0 +1,89 @@ +// clearTimeout, clearInterval and clearImmediate on handles that may be +// undefined or null: an absent handle clears nothing, a present one clears +// its timer, as in Node. Covers a debounce that re-arms through +// `Timeout | undefined`, array elements read with `!`, intervals that +// clear themselves through an optional handle, and immediates. + +const log: string[] = []; + +// A debounce: each call clears the pending timer, which is undefined at first. +let pending: ReturnType | undefined; +function debounce(label: string, ms: number): void { + clearTimeout(pending); + pending = setTimeout(() => { + log.push(`debounced ${label}`); + pending = undefined; + }, ms); +} +debounce("a", 20); +debounce("b", 20); +debounce("c", 5); + +// Null-initialized handles, cleared before and after they are set. +let retry: ReturnType | null = null; +clearTimeout(retry); +retry = setTimeout(() => log.push("retry fired"), 1); +let cancelled: ReturnType | null = setTimeout(() => log.push("cancelled fired"), 1); +clearTimeout(cancelled); +cancelled = null; +clearTimeout(cancelled); + +// Handles kept in an array, cleared through `!` element reads. +const handles = [setTimeout(() => log.push("kept 0"), 2)]; +for (let i = 1; i < 6; i++) handles.push(setTimeout(() => log.push(`kept ${i}`), 2 + i)); +for (let i = 1; i < handles.length; i += 2) clearTimeout(handles[i]!); + +// An interval that clears itself through its optional handle. +let ticks = 0; +let ticker: ReturnType | undefined; +clearInterval(ticker); +ticker = setInterval(() => { + ticks++; + if (ticks === 3) { + clearInterval(ticker); + ticker = undefined; + log.push(`ticker stopped at ${ticks}`); + } +}, 1); +let idle: ReturnType | null = null; +clearInterval(idle); + +// Immediates: an optional handle, cleared while pending and when absent. +let soon: ReturnType | undefined = setImmediate(() => log.push("soon fired")); +clearImmediate(soon); +soon = undefined; +clearImmediate(soon); +let later: ReturnType | null = setImmediate(() => log.push("later fired")); +const keep = later; +later = null; +clearImmediate(later); +console.log("kept immediate", keep !== null); + +// A class that owns an optional timer. +class Poller { + timer: ReturnType | undefined = undefined; + runs = 0; + start(): void { + this.stop(); + this.timer = setTimeout(() => { + this.runs++; + log.push(`poller run ${this.runs}`); + this.timer = undefined; + }, 3); + } + stop(): void { + clearTimeout(this.timer); + this.timer = undefined; + } +} +const poller = new Poller(); +poller.stop(); +poller.start(); +poller.start(); + +setTimeout(() => { + clearTimeout(pending); + clearInterval(ticker); + console.log(log.sort().join("\n")); + console.log("pending", pending === undefined, "ticker", ticker === undefined, "runs", poller.runs); +}, 60); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index a3ee81f9b..979c6ace1 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2004,6 +2004,7 @@ "5681-array-pop-and-length.ts": 3754, "5682-publish-wide-objects.ts": 3326, "5683-date-local-fields-tz.ts": 3848, + "5684-clear-optional-timer-handles.ts": 3989, "5700-cycle-release-retained.ts": 2156, "5701-cycle-release-old-edges.ts": 2601, "5702-publish-candidate-buffers.ts": 3469, @@ -5068,6 +5069,7 @@ "5681-array-pop-and-length.ts": 3216, "5682-publish-wide-objects.ts": 3376, "5683-date-local-fields-tz.ts": 3911, + "5684-clear-optional-timer-handles.ts": 1757, "5700-cycle-release-retained.ts": 3414, "5701-cycle-release-old-edges.ts": 4210, "5702-publish-candidate-buffers.ts": 3848, From 076d3e01fbf7bc0d9a74aa15710a50c95f4c98f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:46:53 -0500 Subject: [PATCH 090/120] List the Date string formatters in the library profile's Date family The determinism fence over stdlib.date. now covers toString, toDateString and toTimeString, which read the host time zone. --- tests/harness/library-profile.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index fc243ccb5..18abeecf8 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -551,7 +551,10 @@ describe("library profile fences", () => { "stdlib.date.getUTCSeconds", "stdlib.date.now", "stdlib.date.parse", + "stdlib.date.toDateString", "stdlib.date.toISOString", + "stdlib.date.toString", + "stdlib.date.toTimeString", "stdlib.date.valueOf", ]); expect(r.profile.fences[0]!.surfaces.every((s) => s.detector !== undefined)).toBe(true); From 2ed5136923f5b79899298112a9e3d7b79220cc67 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:55:25 -0500 Subject: [PATCH 091/120] Convert class instances to strings and numbers through their own methods String(x), template literals, `+` with a string and Number(x) or unary plus on a program class instance failed with SC2001 ("values of type 'Money' cannot be compiled yet"), even when the class defines toString. They now run Node's ToPrimitive protocol through the class's methods: - `[Symbol.toPrimitive](hint)` when the class defines it, with the hint "string" for String(x) and template literals, "default" for `+`, and "number" for Number(x) and unary plus. - Otherwise toString (string hint) or valueOf and then toString (default and number hints), and "[object Object]" (NaN as a number) when the class defines neither. - The method dispatches like a call of it. Subclasses that convert through a different method are tested first with instanceof, so an array of base-class values converts each element by its own class. - Unions of class instances and primitives (`Box | undefined`) convert each arm as it would alone. Conversions the program can change at run time keep a named refusal: a toString or valueOf instance property or prototype write, a Symbol.toStringTag, and conversion methods that can return an object. Builtin classes and their subclasses keep their existing conversions. --- .../frontend/lowering/class-to-primitive.ts | 325 ++++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 4 + .../src/frontend/lowering/lower-exprs.ts | 34 +- .../corpus/6053-class-to-primitive.ts.json | 6 + .../corpus/6054-js-class-to-primitive.js.json | 6 + tests/corpus/6053-class-to-primitive.ts | 125 +++++++ tests/corpus/6054-js-class-to-primitive.js | 15 + tests/harness/corpus-costs.json | 4 + 8 files changed, 513 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/class-to-primitive.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json create mode 100644 tests/corpus/6053-class-to-primitive.ts create mode 100644 tests/corpus/6054-js-class-to-primitive.js diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts new file mode 100644 index 000000000..516a62f3c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -0,0 +1,325 @@ +import { varRef } from "../../ir/build.js"; +import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; +import { isClassCallback } from "./class-callbacks.js"; +import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; + +/** The ToPrimitive hint of a conversion: `String(x)` and template literals + * take "string", `+` "default", `Number(x)` "number". */ +export type ConversionHint = "string" | "default" | "number"; +export type StringConversionHint = "string" | "default"; + +type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString"; + +/** How an instance of one class converts: through one of its methods, or + * through Object.prototype.toString ("[object Object]"). */ +type Plan = { method: ConversionMethod; ret: IrType } | { method: null }; + +const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]); + +/** ToString(ToPrimitive(value, hint)) of a program class instance, as Node + * computes it: `[Symbol.toPrimitive](hint)` when the class defines it, then + * `toString` (hint "string") or `valueOf` and then `toString` (hint + * "default"), and "[object Object]" when the class defines neither. The + * methods dispatch like calls of them; subclasses that convert through a + * different method are tested first. Null for values whose class is not a + * plain program class (builtin classes and their subclasses keep their own + * conversions). */ +export function classToStringConversion( + lowerer: Lowerer, + value: IrExpr, + hint: StringConversionHint, + node: ts.Node, +): IrExpr | null { + return classConversion(lowerer, value, hint, node, STRING, (primitive) => + lowerer.ensureString(primitive, node), + ); +} + +/** ToNumber(ToPrimitive(value, "number")) of a program class instance + * (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then + * `toString`, and NaN for "[object Object]". Null as for + * classToStringConversion. */ +export function classToNumberConversion( + lowerer: Lowerer, + value: IrExpr, + node: ts.Node, + operator: "Number" | "unary plus" = "Number", +): IrExpr | null { + return classConversion(lowerer, value, "number", node, F64, (primitive) => + numberOfPrimitive(lowerer, primitive, node, operator), + ); +} + +/** ToNumber of a primitive value: `Number(primitive)`, or unary plus, + * which throws for a BigInt instead of converting it. */ +function numberOfPrimitive( + lowerer: Lowerer, + value: IrExpr, + node: ts.Node, + operator: "Number" | "unary plus", +): IrExpr { + const loc = value.loc; + if (operator === "unary plus" && mayBeBigint(lowerer, value.type)) + lowerer.unsupported( + "SC1090", + node, + "unary plus of class instances whose conversion can return a bigint", + ); + switch (value.type.kind) { + case "f64": + return value; + case "string": + return { kind: "libCall", fn: "num.fromString", args: [value], type: F64, loc }; + case "bool": + return { + kind: "ternary", + cond: value, + then: { kind: "numLit", value: 1, type: F64, loc }, + else_: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }; + case "bigint": + return { kind: "libCall", fn: "bigint.toF64", args: [value], type: F64, loc }; + default: + if (!lowerer.dynConvertible(value.type)) + lowerer.unsupported("SC1090", node, "number conversions of this class's primitive value"); + return { + kind: "libCall", + fn: "dyn.numberConstructor", + args: [lowerer.coerceToExpected(value, DYN)], + type: F64, + loc, + }; + } +} + +function mayBeBigint(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "bigint") return true; + return ( + type.kind === "union" && + lowerer.unions.get(type.unionId)?.arms.some((arm) => arm.kind === "bigint") === true + ); +} + +function classConversion( + lowerer: Lowerer, + value: IrExpr, + hint: ConversionHint, + node: ts.Node, + resultType: IrType, + finish: (primitive: IrExpr) => IrExpr, +): IrExpr | null { + if (value.type.kind !== "object") return null; + const info = lowerer.classes.get(value.type.className); + if (!info || !plainProgramClass(info)) return null; + const classes = subtree(lowerer, info); + for (const cls of classes) refuseReplacedConversions(lowerer, cls, node); + const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)])); + // A class whose plan differs from its base's starts a group; the deepest + // groups are tested first, so each instance takes its own class's plan. + const tops = classes + .filter((cls) => { + if (cls === info) return false; + const base = cls.base; + return base !== null && plans.has(base) && !samePlan(plans.get(base)!, plans.get(cls)!); + }) + .sort((a, b) => depth(b) - depth(a)); + const loc = value.loc; + const prefix: IrStmt[] = []; + let receiver = value; + if (tops.length > 0 && !isSafeToRepeat(value)) { + const local = lowerer.declareHiddenLocal("%converted", value.type); + prefix.push({ kind: "varDecl", localId: local.id, init: value, loc }); + receiver = varRef(local.id, value.type, loc); + } + const convert = (cls: ClassInfo, object: IrExpr): IrExpr => { + const plan = plans.get(cls)!; + if (plan.method === null) + return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc }); + const args: IrExpr[] = + plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : []; + return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc)); + }; + let result = convert(info, receiver); + for (let i = tops.length - 1; i >= 0; i--) { + const top = tops[i]!; + const narrowed: IrExpr = { + kind: "downcast", + value: receiver, + type: { kind: "object", className: top.def.name }, + loc, + }; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: receiver, className: top.def.name, type: BOOL, loc }, + then: convert(top, narrowed), + else_: result, + type: resultType, + loc, + }; + } + return prefix.length > 0 + ? { kind: "seqExpr", stmts: prefix, result, type: resultType, loc } + : result; +} + +/** A class of the program whose chain reaches no builtin base: its + * instances inherit Object.prototype's conversions. */ +function plainProgramClass(info: ClassInfo): boolean { + for (let cls: ClassInfo | null = info; cls; cls = cls.base) { + if ( + cls.def.runtime || + cls.builtinError || + cls.builtinEmitter || + cls.builtinStream || + cls.callableBase !== undefined + ) + return false; + } + return true; +} + +function subtree(lowerer: Lowerer, info: ClassInfo): ClassInfo[] { + const out: ClassInfo[] = []; + const visit = (cls: ClassInfo): void => { + out.push(cls); + for (const child of cls.subclasses) visit(child); + }; + visit(info); + return out.filter( + (cls) => cls === info || lowerer.classCanBeConstructed(cls) || cls.subclasses.length > 0, + ); +} + +function depth(info: ClassInfo): number { + let n = 0; + for (let cls = info.base; cls; cls = cls.base) n++; + return n; +} + +function samePlan(a: Plan, b: Plan): boolean { + return a.method === b.method; +} + +/** A conversion method the class declares or inherits (a field of that + * name is an instance property the program sets, refused separately). */ +function declared( + lowerer: Lowerer, + info: ClassInfo, + method: ConversionMethod, +): { ret: IrType } | null { + if (info.fields.has(method)) return null; + const found = findMethodOn(lowerer, info, method); + return found ? { ret: found.sig.ret } : null; +} + +function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan { + const exotic = declared(lowerer, info, "sym:toPrimitive"); + if (exotic) { + if (!primitive(lowerer, exotic.ret)) + refuse(lowerer, node, info, "a [Symbol.toPrimitive] method that can return an object"); + return { method: "sym:toPrimitive", ret: exotic.ret }; + } + // OrdinaryToPrimitive: "string" tries toString first, which every object + // has (Object.prototype.toString answers a string); "default" and + // "number" try valueOf first, whose Object.prototype version answers the + // object itself. + const order: ConversionMethod[] = hint === "string" ? ["toString"] : ["valueOf", "toString"]; + for (const method of order) { + const found = declared(lowerer, info, method); + if (!found) continue; + if (!primitive(lowerer, found.ret)) + refuse(lowerer, node, info, `a '${method}' method that can return an object`); + return { method, ret: found.ret }; + } + return { method: null }; +} + +function primitive(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "union") + return ( + lowerer.unions.get(type.unionId)?.arms.every((arm) => PRIMITIVE_KINDS.has(arm.kind)) === true + ); + return PRIMITIVE_KINDS.has(type.kind); +} + +/** Conversions the program can change at run time (an instance property + * of a method's name, a prototype write, a Symbol.toStringTag) would not be + * the class's declared methods. */ +function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.Node): void { + for (const method of ["toString", "valueOf"] as const) { + if (info.fields.has(method) || isClassCallback(lowerer, info, method)) + refuse(lowerer, node, info, `a '${method}' instance property`); + if (lowerer.prototypeMethodAccesses.has(method)) + refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`); + } + if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag")) + refuse(lowerer, node, info, "a Symbol.toStringTag"); +} + +function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never { + lowerer.unsupported( + "SC1090", + node, + `conversions of '${info.def.jsName ?? info.def.name}' instances to primitives (the class has ${what})`, + ); +} + +/** ToString(ToPrimitive(value, hint)) of a union whose arms are program + * class instances and primitives (`Box | undefined`): each arm converts as + * it would alone. Null for other unions. */ +export function classUnionToString( + lowerer: Lowerer, + value: IrExpr, + hint: StringConversionHint, + node: ts.Node, +): IrExpr | null { + if (value.type.kind !== "union") return null; + const unionId = value.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (!arms || !arms.some((arm) => arm.kind === "object")) return null; + const convertible = (arm: IrType): boolean => { + if (PRIMITIVE_KINDS.has(arm.kind)) return true; + if (arm.kind !== "object") return false; + const info = lowerer.classes.get(arm.className); + return info !== undefined && plainProgramClass(info); + }; + if (!arms.every(convertible)) return null; + const loc = value.loc; + const prefix: IrStmt[] = []; + let stable = value; + if (!isSafeToRepeat(value)) { + const local = lowerer.declareHiddenLocal("%converted", value.type); + prefix.push({ kind: "varDecl", localId: local.id, init: value, loc }); + stable = varRef(local.id, value.type, loc); + } + const convertArm = (tag: number): IrExpr => { + const arm = arms[tag]!; + if (arm.kind === "undefinedT" || arm.kind === "nullT") + return { + kind: "strLit", + value: arm.kind === "nullT" ? "null" : "undefined", + type: STRING, + loc, + }; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: stable, type: arm, loc }; + if (arm.kind === "object") return classToStringConversion(lowerer, narrowed, hint, node)!; + return lowerer.ensureString(narrowed, node); + }; + let result = convertArm(arms.length - 1); + for (let tag = arms.length - 2; tag >= 0; tag--) { + result = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: stable, type: BOOL, loc }, + then: convertArm(tag), + else_: result, + type: STRING, + loc, + }; + } + return prefix.length > 0 ? { kind: "seqExpr", stmts: prefix, result, type: STRING, loc } : result; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 303bdf517..f431d4e54 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -157,6 +157,7 @@ import { } from "./lower-exprs.js"; import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; +import { classToNumberConversion } from "./class-to-primitive.js"; import { hasRuntimeStatics } from "./class-runtime-statics.js"; import { httpClientFnBindingOf, @@ -2134,6 +2135,9 @@ function lowerNumberConstructorValue( }; const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc); if (optionalNumber) return optionalNumber; + // A program class instance converts through its own methods. + const classNumber = classToNumberConversion(lowerer, arg, argNode); + if (classNumber) return classNumber; const scalarUnionNumber = lowerScalarUnionNumber(lowerer, arg, argNode, loc); if (scalarUnionNumber) return scalarUnionNumber; return lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 558838c1f..fc820b4d1 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -221,6 +221,11 @@ import { } from "./expressions/optional-chains.js"; import { foldedStringKeyOf } from "./expressions/object-literals.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; +import { + classToNumberConversion, + classToStringConversion, + classUnionToString, +} from "./class-to-primitive.js"; import { fenceNodeModuleMutation, isNodeModuleValue, @@ -8107,7 +8112,7 @@ function lowerElementCompoundParts( const appended: IrExpr = { kind: "strConcat", left: oldValue, - right: lowerer.ensureString(rhs, expr.right), + right: concatOperand(lowerer, rhs, expr.right), type: STRING, loc, }; @@ -8125,7 +8130,7 @@ function lowerElementCompoundParts( computed = { kind: "strConcat", left: lowerer.ensureString(oldRef(), target), - right: lowerer.ensureString(rhs, expr.right), + right: concatOperand(lowerer, rhs, expr.right), type: STRING, loc, }; @@ -8876,6 +8881,16 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx return lowerRecordPropertyKey(lowerer, key, node); } +/** An operand of `+` with a string: ToString of ToPrimitive with the + * default hint, which tries a class instance's valueOf before toString. */ +function concatOperand(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr { + return ( + classToStringConversion(lowerer, e, "default", node) ?? + classUnionToString(lowerer, e, "default", node) ?? + lowerer.ensureString(e, node) + ); +} + export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr { if (e.type.kind === "string") return e; if (e.type.kind === "url") @@ -8919,6 +8934,9 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr return errorToStringCall(lowerer, e); } } + // A program class instance converts through its own methods. + const converted = classToStringConversion(lowerer, e, "string", node); + if (converted) return converted; } if (e.type.kind === "union") { // `${u}` — the arm's ToString via a per-union interned helper @@ -8977,6 +8995,8 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr node, ); } + const viaClasses = classUnionToString(lowerer, e, "string", node); + if (viaClasses) return viaClasses; lowerer.unsupported( "SC1090", node, @@ -9450,6 +9470,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "string") { return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc }; } + const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); + if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "+", [raw.type]); return operand; @@ -9918,7 +9940,7 @@ export function lowerCompoundValueToTarget( { kind: "strConcat", left: lowerer.ensureString(read, expr.left), - right: lowerer.ensureString(rhs, expr.right), + right: concatOperand(lowerer, rhs, expr.right), type: STRING, loc, }, @@ -10981,8 +11003,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr if (left.type.kind === "string" || right.type.kind === "string") { return { kind: "strConcat", - left: lowerer.ensureString(left, expr.left), - right: lowerer.ensureString(right, expr.right), + left: concatOperand(lowerer, left, expr.left), + right: concatOperand(lowerer, right, expr.right), type: STRING, loc, }; @@ -16310,7 +16332,7 @@ function lowerFieldCompoundValue( const numericRead = lowerOptionalNumber(lowerer, read, loc, access); const bigintValue = rhsNode ? bigintCompoundValue(op, read, rhs, loc) : null; if (op === "+" && read.type.kind === "string") { - appended = lowerer.ensureString(rhs, rhsNode ?? access); + appended = concatOperand(lowerer, rhs, rhsNode ?? access); value = { kind: "strConcat", left: read, right: appended, type: STRING, loc }; } else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") { value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json new file mode 100644 index 000000000..6c4f1aaea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6053-class-to-primitive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json new file mode 100644 index 000000000..50591191a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6054-js-class-to-primitive.js" + ], + "diags": [] +} diff --git a/tests/corpus/6053-class-to-primitive.ts b/tests/corpus/6053-class-to-primitive.ts new file mode 100644 index 000000000..c79a5ddcb --- /dev/null +++ b/tests/corpus/6053-class-to-primitive.ts @@ -0,0 +1,125 @@ +// String and number conversions of class instances run Node's ToPrimitive +// protocol through the class's own methods: String(x) and template +// literals use the string hint, `+` with a string the default hint, and +// Number(x) and unary plus the number hint. Classes without conversion +// methods convert to "[object Object]" and NaN. +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + toString(): string { + return `$${(this.cents / 100).toFixed(2)}`; + } + valueOf(): number { + return this.cents; + } +} +const m = new Money(1250); +console.log(String(m), `${m}`, "total " + m, m + "!"); +let s = "sum:"; +s += m; +console.log(s); +class Plain { + x = 1; +} +console.log(String(new Plain()), `${new Plain()}`, "p" + new Plain()); +class Base { + name(): string { + return "base"; + } +} +class Named extends Base { + toString(): string { + return "named " + this.name(); + } +} +class Deeper extends Named { + valueOf(): number { + return 7; + } +} +const items: Base[] = [new Base(), new Named(), new Deeper()]; +for (const item of items) console.log(String(item), `${item}`, "+" + item); +class Hinted { + [Symbol.toPrimitive](hint: string): string | number { + return hint === "number" ? 42 : `hint:${hint}`; + } +} +const h = new Hinted(); +console.log(String(h), `${h}`, "x" + h); +let calls = 0; +class Counted { + toString(): string { + calls++; + return "c" + calls; + } +} +const c = new Counted(); +console.log(`${c}-${c}`, calls); + +class Amount { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + valueOf(): number { + return this.cents; + } +} +const amount = new Amount(1250); +console.log(Number(amount), +amount); +class Label { + toString(): string { + return "12.5"; + } +} +class Flag { + [Symbol.toPrimitive](hint: string): number | string { + return hint === "number" ? 3 : "flag"; + } +} +class Empty {} +console.log(Number(new Label()), +new Label(), Number(new Flag()), +new Flag(), `${new Flag()}`, Number(new Empty())); + +class Box { + value: T; + constructor(value: T) { + this.value = value; + } + toString(): string { + return `Box(${String(this.value)})`; + } + describe(): string { + return `I am ${this}`; + } +} +console.log(`${new Box(1)}`, String(new Box("s")), new Box(true).describe()); +abstract class Shape { + abstract area(): number; + toString(): string { + return `shape:${this.area()}`; + } +} +class Square extends Shape { + side: number; + constructor(side: number) { + super(); + this.side = side; + } + area(): number { + return this.side * this.side; + } +} +const shapes: Shape[] = [new Square(3)]; +console.log(shapes.map((shape) => `${shape}`).join(",")); +class Code { + valueOf(): string { + return "code-value"; + } +} +console.log(String(new Code()), "" + new Code()); +const maybe: Box | undefined = process.argv.length > 99 ? undefined : new Box(5); +console.log(`${maybe}`); +const pair: Code | number | null = process.argv.length > 99 ? null : new Code(); +console.log(`${pair}`, "p:" + pair); diff --git a/tests/corpus/6054-js-class-to-primitive.js b/tests/corpus/6054-js-class-to-primitive.js new file mode 100644 index 000000000..e5f47b233 --- /dev/null +++ b/tests/corpus/6054-js-class-to-primitive.js @@ -0,0 +1,15 @@ +// JavaScript class instances convert to strings through their own toString, +// and to "[object Object]" without one, as in Node. +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + toString() { + return `(${this.x}, ${this.y})`; + } +} +const p = new Point(1, 2); +console.log(`${p}`, String(p), "p=" + p); +class Bare {} +console.log(`${new Bare()}`); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index b1e77984a..8c6307926 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2230,6 +2230,8 @@ "6043-string-index-narrowed-union.ts": 2983, "605-closures-forward-capture-tdz.ts": 3277, "6051-generic-class-factory-results.ts": 4363, + "6053-class-to-primitive.ts": 4385, + "6054-js-class-to-primitive.js": 2181, "6070-iterator-helpers-untyped.js": 4050, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, @@ -5297,6 +5299,8 @@ "6043-string-index-narrowed-union.ts": 3627, "605-closures-forward-capture-tdz.ts": 4638, "6051-generic-class-factory-results.ts": 46581, + "6053-class-to-primitive.ts": 36270, + "6054-js-class-to-primitive.js": 2958, "6070-iterator-helpers-untyped.js": 6900, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, From e8e432b6ddc0791f56c2ff84d92c8afb028e0c7e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:58:20 -0500 Subject: [PATCH 092/120] Clean up subtests cancelled by their parent's timeout node:test changes, matching Node 24.22: - A subtest cancelled because its parent timed out has its mocks restored and its mock timers reset when it is cancelled, as Node's postRun resets the test's mock tracker. Before, a mocked method stayed mocked for the tests that ran after it. - Its afterEach chain and t.after hooks run at the next turn, once the runner has moved on to the next test, as Node runs them when the cancelled test's own run resumes. Before, they never ran. The chain is copied when the test is cancelled, since its ancestors drop their hooks when they finish. - Its hooks are then dropped, so closures that capture its context do not keep it alive. The registration parent is still not restored when the cancelled body settles: it belongs to the test that runs then. The timeout-cancel-cleanup fixture records the mocked method and the hook order against the next test's body and compares them with Node, plain and with SCRIPTC_SAN=1. It fails without this change. --- packages/runtime/src/scr_test.c | 62 ++++++++++++++++++- .../node-test/cases/timeout-cancel-cleanup.ts | 44 +++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 4217169b8..00e609129 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -599,9 +599,59 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) { scr_test_report_result(c); } +/* A cancelled subtest's afterEach chain and t.after hooks, on their own + * fiber: they start at the next turn, once the runner has moved on, as + * Node runs them when the cancelled test's own run resumes. The afterEach + * chain is copied when the test is cancelled, because its ancestors drop + * their hooks when they finish. Then the test's hooks are dropped, + * releasing closures that capture its context. */ +typedef struct { + ScrTestCtx *test; /* owned ref */ + ScrTestHooks each; /* the afterEach chain, innermost first; owned closures */ +} ScrTestCancelled; + +static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { + (void)self; + ScrTestCancelled *job = arg; + ScrTestCtx *c = job->test; + scr_await_hop(); /* the spawn runs the fiber at once; start at the next turn */ + for (size_t i = 0; i < job->each.n && !scr_exc_pending(); i++) + scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, NULL); + if (scr_exc_pending()) scr_exc_clear(); + scr_test_run_hooks_of(c, SCR_HOOK_AFTER, c); + if (scr_exc_pending()) scr_exc_clear(); + for (int k = 0; k < 4; k++) scr_test_hooks_free(&c->hooks[k]); + scr_test_hooks_free(&job->each); + scr_testctx_release(c); + free(job); +} + +static void scr_test_cancelled_hooks(ScrTestCtx *c) { + ScrTestCancelled *job = calloc(1, sizeof *job); + if (!job) scr_test_oom(); + job->test = scr_testctx_retain(c); + for (ScrTestCtx *p = c->parent; p; p = p->parent) { + ScrTestHooks *h = &p->hooks[SCR_HOOK_AFTER_EACH]; + for (size_t i = 0; i < h->n; i++) { + if (job->each.n == job->each.cap) { + job->each.cap = job->each.cap ? job->each.cap * 2 : 4; + job->each.v = realloc(job->each.v, job->each.cap * sizeof *job->each.v); + if (!job->each.v) scr_test_oom(); + } + job->each.v[job->each.n].cb = scr_closure_retain(h->v[i].cb); + job->each.v[job->each.n].flags = h->v[i].flags; + job->each.v[job->each.n].at = NULL; + job->each.n++; + } + } + scr_promise_release(scr_async_spawn(scr_test_cancelled_hooks_entry, job)); +} + /* A test that timed out (Node): its subtests that are still running are - * cancelled, innermost first, with their durations so far. Their bodies - * keep running unobserved and report nothing when they settle. */ + * cancelled, innermost first, with their durations so far. Each one's + * mocks are restored and its mock timers reset at once, and its afterEach + * chain and t.after hooks run once the runner yields. Their bodies keep + * running unobserved and report nothing when they settle. */ static void scr_test_cancel_running(ScrTestCtx *t) { for (size_t i = 0; i < t->nchildren; i++) { ScrTestCtx *c = t->children[i]; @@ -613,6 +663,9 @@ static void scr_test_cancel_running(ScrTestCtx *t) { c->dur_ms = scr_now_ms() - c->start_ms; c->state = SCR_TS_CANCELLED; scr_test_report_result(c); + scr_test_mocks_restore(&c->mocks, true); + scr_mt_reset(c->mock_timers); + scr_test_cancelled_hooks(c); } } @@ -725,7 +778,10 @@ static void scr_test_run_node(ScrTestCtx *t) { scr_test_call(t->cb, t->flags, t, t); if (t->state != SCR_TS_PENDING) { /* Its parent timed out and cancelled it while the body ran: the - * result is reported, and the runner has moved on. */ + * result is reported, its cleanup ran at the cancellation + * (scr_test_cancel_running), and the runner has moved on, so the + * registration parent is not restored either: it belongs to the + * test that runs now. */ if (scr_exc_pending()) scr_exc_clear(); if (t->concurrency > 1) scr_test_defer--; return; diff --git a/tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts b/tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts new file mode 100644 index 000000000..f9c0e726c --- /dev/null +++ b/tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts @@ -0,0 +1,44 @@ +// @exit: 1 +// A subtest cancelled by its parent's timeout is cleaned up as Node does: +// its mocks are restored at once, and its afterEach chain and t.after +// hooks run once the runner has moved on to the next test. Its body keeps +// running and sees the restored method when it settles. +import { afterEach, test } from "node:test"; +import * as assert from "node:assert"; + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); +const api = { greet: (): string => "real" }; +const seen: string[] = []; + +afterEach((c) => { + seen.push(`root afterEach for ${c.name}`); +}); + +test("parent", { timeout: 30 }, async (t) => { + t.afterEach((c) => { + seen.push(`parent afterEach for ${c.name}`); + }); + await t.test("sub", async (t) => { + t.mock.method(api, "greet", () => "mocked"); + t.after(() => { + seen.push(`after hook of ${t.name}`); + }); + seen.push(`sub runs: ${api.greet()}`); + await sleep(200); + seen.push(`sub settles: ${api.greet()}`); + }); +}); + +test("next", async () => { + seen.push(`next starts: ${api.greet()}`); + await sleep(300); + assert.deepStrictEqual(seen, [ + "sub runs: mocked", + "root afterEach for parent", + "next starts: real", + "parent afterEach for sub", + "root afterEach for sub", + "after hook of sub", + "sub settles: real", + ]); +}); From 96f59b0f3f782d7f14aaf334648034dc23514017 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 21:53:14 -0500 Subject: [PATCH 093/120] Free errors that crossed into checked-dynamic code and attach Node's error properties The error identity cache (scr_dyn_from_error) keeps one checked-dynamic node per error so a crossing error compares reference-equal and keeps the properties set on it. It retained both sides for the whole process, so every error that crossed a checked-dynamic boundary (a caught error read through a cast, an error delivered to an untyped callback, a dns failure, a warning) stayed alive until exit: 100,000 caught AssertionErrors read through a cast grew the resident set by about 210 MiB. The cache now sweeps itself. An entry whose error and node are referenced only by the cache is unreachable (a node holds no reference to its error; a registered subclass box holds one, and a spawnSync error's `error` property refers to its own node), so a sweep releases both sides. Sweeps run when the cache doubles past the entries the last sweep kept, so they cost amortized constant time per crossing. The cache is per thread, and objects shared across threads are published (immortal), so a sweep never races another thread's reference. Live errors and live nodes keep their entries: identity, rethrowing the same instance, isNativeError and kind checks are unchanged. scr_error_set_prop sets an own property on an error's node; dns, assert, cipher, crypto, util.callbackify and warning errors use it for the properties they attach. util.inspect of a runtime error renders through its node: Node's own properties follow the bracket form in key order (a system error's errno, code, syscall and path, and the extra properties a throw site attached), and an AssertionError renders at depth 0 with long string actual/expected values shortened, as its custom inspect does. A node that contains itself renders as Node's /[Circular *1]. child_process errors carry Node's own properties: - execSync and execFileSync: status, signal, output, pid, stdout and stderr (Buffers, or strings with an encoding option); a spawn failure or timeout adds errno, syscall, path, spawnargs and error first. - execFile's callback and the promisified execFile/exec: code (the exit status, or null after a signal), killed, signal and cmd, with stdout and stderr when promisified; a spawn failure has errno, code, syscall, path, spawnargs and cmd. The sync forms echo the captured stderr before a timeout throw too, as Node's inheritStderr does. The cp.execSync libCall takes a flag for Buffer outputs. An error's own `stack` stays non-enumerable when assigned through its node. A worker's record-source table is freed when the worker exits. --- .../process-arguments-and-errors.mdx | 2 +- .../lowering/builtins/child-process-sync.ts | 6 + .../compiler/src/ir/builtin-signatures.ts | 9 +- .../5634-error-extra-props-inspect.ts.json | 6 + .../5635-error-extra-props-retry-loop.ts.json | 6 + .../corpus/5636-child-error-props.ts.json | 6 + packages/runtime/src/scr_assert.c | 17 +- packages/runtime/src/scr_async_dyn.c | 4 +- packages/runtime/src/scr_child.c | 377 +++++++++++++++--- packages/runtime/src/scr_child.h | 12 +- packages/runtime/src/scr_cipher.c | 10 +- packages/runtime/src/scr_dgram.c | 2 +- packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_inspect.c | 77 +++- packages/runtime/src/scr_inspect.h | 11 +- packages/runtime/src/scr_json.c | 137 ++++++- packages/runtime/src/scr_lib.c | 8 +- packages/runtime/src/scr_util_callable.c | 2 +- .../corpus/5634-error-extra-props-inspect.ts | 64 +++ .../5635-error-extra-props-retry-loop.ts | 31 ++ tests/corpus/5636-child-error-props.ts | 95 +++++ .../fixtures/error-cache-sweep/retry-loop.ts | 34 ++ tests/harness/corpus-costs.json | 6 + tests/harness/error-cache-sweep.test.ts | 38 ++ 24 files changed, 856 insertions(+), 108 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json create mode 100644 tests/corpus/5634-error-extra-props-inspect.ts create mode 100644 tests/corpus/5635-error-extra-props-retry-loop.ts create mode 100644 tests/corpus/5636-child-error-props.ts create mode 100644 tests/fixtures/error-cache-sweep/retry-loop.ts create mode 100644 tests/harness/error-cache-sweep.test.ts diff --git a/docs/content/limitations/process-arguments-and-errors.mdx b/docs/content/limitations/process-arguments-and-errors.mdx index d048b4154..69d7d9dc1 100644 --- a/docs/content/limitations/process-arguments-and-errors.mdx +++ b/docs/content/limitations/process-arguments-and-errors.mdx @@ -1,5 +1,5 @@ `process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against. -Uncaught exceptions print `Uncaught ` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`. +Uncaught exceptions print `Uncaught ` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code` as typed members. The other own properties Node gives them, such as a system error's `errno`, `syscall`, and `path`, a child process error's `status`, `stdout`, and `stderr`, or an assertion error's `actual` and `expected`, are readable through an untyped view of the error (for example a cast of a caught value) and appear in `util.inspect` output. Errors carry no stack frames. `console.log` and `util.inspect` print an Error in Node's stackless form, `[TypeError: message]`, where Node prints `TypeError: message` followed by stack frames; the two agree when `Error.stackTraceLimit` is `0`. diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index a8dea8001..bfa2bc99a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -102,6 +102,8 @@ export function lowerExecSyncCall( let stdoutMode = 1; let stderrMode = 0; let stdinInherit = false; + // Without an encoding option, a failure's error carries Buffer outputs. + let encodingGiven = false; // A conditional env spread (`...(c ? { env: ... } : {})`): the call // itself splits into a ternary of the two env-nesses at the tail. let condEnvSpread: { cond: IrExpr; pairs: IrExpr; whenTrue: boolean } | null = null; @@ -134,6 +136,8 @@ export function lowerExecSyncCall( runtime.timeout, runtime.stdoutMode, runtime.stderrMode, + // The runtime options carry a utf8 encoding (others throw). + boolLit(false, loc), ], type: STRING, loc, @@ -251,6 +255,7 @@ export function lowerExecSyncCall( 'outputs are captured as utf8 — pass { encoding: "utf8" }', ); } + encodingGiven = true; break; } case "cwd": @@ -395,6 +400,7 @@ export function lowerExecSyncCall( timeout, numLit(stdoutMode + (stdinInherit ? 4 : 0), loc), numLit(stderrMode, loc), + boolLit(!encodingGiven, loc), ], type: STRING, loc, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index d2c503214..9467a2458 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2285,11 +2285,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ result: VOID, }, /** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input, - * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode — see - * scr_runtime.h). Throws Node's exact errors: "Command failed: " + * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers — + * see scr_child.h). Throws Node's exact errors: "Command failed: " * (+ captured stderr) on non-zero exit or signal death, "spawnSync * ENOENT" on spawn failure, "spawnSync ETIMEDOUT" after - * the SIGTERM timeout. Result is the captured utf8 stdout (+1). */ + * the SIGTERM timeout, with Node's status, signal, output, pid, stdout + * and stderr properties (Buffer outputs when buffers is set: no + * encoding option). Result is the captured utf8 stdout (+1). */ "cp.execSync": { argTypes: [ STRING, @@ -2303,6 +2305,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ F64, F64, F64, + BOOL, ], result: STRING, }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json new file mode 100644 index 000000000..418b206b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5634-error-extra-props-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json new file mode 100644 index 000000000..9fbf4c473 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5635-error-extra-props-retry-loop.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json new file mode 100644 index 000000000..bf6db4e51 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5636-child-error-props.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 5a4769038..4917b1291 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -167,19 +167,16 @@ static ScrDyn *scr_assert_dyn_text(const char *text) { return d; } -/* Sets the properties on the error's view in Node's order. The values - * move in. */ +/* Sets the error's own properties in Node's order. The values move in. */ static void scr_assert_attach_values(ScrError *e, bool generated, ScrDyn *actual, ScrDyn *expected, ScrDyn *op, ScrDyn *diff) { - ScrDyn *view = scr_dyn_from_error(e); - scr_dyn_obj_set(view, "generatedMessage", 16, scr_dyn_new_bool(generated)); + scr_error_set_prop(e, "generatedMessage", 16, scr_dyn_new_bool(generated)); scr_error_set_code(e, "ERR_ASSERTION"); - scr_dyn_obj_set(view, "code", 4, scr_assert_dyn_text("ERR_ASSERTION")); - scr_dyn_obj_set(view, "actual", 6, actual); - scr_dyn_obj_set(view, "expected", 8, expected); - scr_dyn_obj_set(view, "operator", 8, op); - scr_dyn_obj_set(view, "diff", 4, diff); - scr_dyn_release(view); + scr_error_set_prop(e, "code", 4, scr_assert_dyn_text("ERR_ASSERTION")); + scr_error_set_prop(e, "actual", 6, actual); + scr_error_set_prop(e, "expected", 8, expected); + scr_error_set_prop(e, "operator", 8, op); + scr_error_set_prop(e, "diff", 4, diff); } static ScrError *scr_assert_error(ScrStr *message, bool generated, const char *diff) { diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 95771d457..ffcb85050 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -1036,11 +1036,9 @@ void scr_process_emit_warning(ScrDyn *args) { e->name = type ? scr_str_retain((ScrStr *)type) : scr_str_new("Warning", 7); e->name_present = true; if (code) e->code = scr_str_retain((ScrStr *)code); + if (detail) scr_error_set_prop(e, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */ ScrDyn *w = scr_dyn_from_error(e); scr_error_release(e); - if (detail) { - scr_dyn_obj_set(w, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */ - } scr_warning_emit_deferred(w); scr_dyn_release(w); } diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 1235001ba..87ead5755 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -62,6 +62,118 @@ enum { SCR_CHILD_DATA_STRING = 2, }; +/* ── error properties (lib/child_process.js) ────────────────────────── + * The own properties Node attaches to child_process errors, set on the + * error's checked-dynamic node (scr_error_set_prop) in Node's key order. */ + +static ScrDyn *scr_child_dyn_cstr(const char *text) { + ScrStr *s = scr_str_new(text, strlen(text)); + ScrDyn *d = scr_dyn_new_str(s); + scr_str_release(s); + return d; +} + +static ScrDyn *scr_child_dyn_text_or_null(const char *text) { + return text != NULL ? scr_child_dyn_cstr(text) : scr_dyn_new_null(); +} + +/* A spawn failure's errnoException fields: errno, code (already stamped), + * syscall " " and path come from the native system fields, + * then spawnargs (the arguments after the file). Set before any other + * property, so the node orders them first. */ +static void scr_child_error_spawn_fields(ScrError *e, double uv_errno, const char *verb, + ScrStr *file, ScrArr *args) { + size_t cap = strlen(verb) + 1 + file->len + 1; + char *call = malloc(cap); + if (!call) scr_trap("scriptc: out of memory\n"); + int len = snprintf(call, cap, "%s %s", verb, file->data); + e->system_errno = uv_errno; + e->system_call = scr_str_new(call, (size_t)len); + free(call); + e->system_path = scr_str_retain(file); + ScrDyn *spawnargs = scr_dyn_new_arr(); + size_t n = args != NULL ? (size_t)scr_arr_len(args) : 0; + for (size_t i = 0; i < n; i++) { + ScrStr *arg = (ScrStr *)scr_arr_get_ref(args, (double)i); + scr_dyn_arr_push(spawnargs, scr_dyn_new_str(arg)); + scr_str_release(arg); + } + scr_error_set_prop(e, "spawnargs", 9, spawnargs); +} + +/* exec/execFile's command failure: code (the exit status, or null after a + * signal), killed and signal, before cmd. */ +static void scr_child_error_exit_fields(ScrError *e, bool has_code, double code, + const char *signal, bool killed) { + scr_error_set_prop(e, "code", 4, has_code ? scr_dyn_new_num(code) : scr_dyn_new_null()); + scr_error_set_prop(e, "killed", 6, scr_dyn_new_bool(killed)); + scr_error_set_prop(e, "signal", 6, scr_child_dyn_text_or_null(signal)); +} + +/* The sync forms' spawn failure and timeout carry the error itself as + * `error` (Node assigns the spawnSync result, whose error it is). */ +static void scr_child_error_self(ScrError *e) { + scr_error_set_prop(e, "error", 5, scr_dyn_from_error(e)); +} + +static void scr_child_error_cmd(ScrError *e, const char *cmd) { + scr_error_set_prop(e, "cmd", 3, scr_child_dyn_cstr(cmd)); +} + +/* The promisified forms add the captured outputs. Borrows. */ +static void scr_child_error_outputs(ScrError *e, ScrStr *out, ScrStr *err) { + scr_error_set_prop(e, "stdout", 6, scr_dyn_new_str(out)); + scr_error_set_prop(e, "stderr", 6, scr_dyn_new_str(err)); +} + +/* A captured sync output as Node's result holds it: a Buffer, the utf8 + * string with an encoding option, or null for a stream that was not + * piped. +1. */ +static ScrDyn *scr_child_sync_output(const char *data, size_t len, bool piped, bool buffers) { + if (!piped) return scr_dyn_new_null(); + if (!buffers) { + ScrStr *s = scr_str_new(data, len); + ScrDyn *d = scr_dyn_new_str(s); + scr_str_release(s); + return d; + } + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len); + b->is_buffer = true; + if (len) memcpy(b->data, data, len); + ScrDyn *d = scr_dyn_new_buffer(b); + scr_bytes_release(b); + return d; +} + +/* execSync/execFileSync: the spawnSync result's status, signal, output, + * pid, stdout and stderr. out/err move in (null for a stream that was not + * piped); output is [null, stdout, stderr]. A child that never started + * has a null output and undefined outputs. */ +static void scr_child_error_sync_fields(ScrError *e, bool has_status, double status, + const char *signal, double pid, bool started, + ScrDyn *out, ScrDyn *err) { + if (!started) { + scr_dyn_release(out); + scr_dyn_release(err); + out = scr_dyn_retain(scr_dyn_undefined()); + err = scr_dyn_retain(scr_dyn_undefined()); + } + scr_error_set_prop(e, "status", 6, has_status ? scr_dyn_new_num(status) : scr_dyn_new_null()); + scr_error_set_prop(e, "signal", 6, scr_child_dyn_text_or_null(signal)); + if (started) { + ScrDyn *output = scr_dyn_new_arr(); + scr_dyn_arr_push(output, scr_dyn_new_null()); + scr_dyn_arr_push(output, scr_dyn_retain(out)); + scr_dyn_arr_push(output, scr_dyn_retain(err)); + scr_error_set_prop(e, "output", 6, output); + } else { + scr_error_set_prop(e, "output", 6, scr_dyn_new_null()); + } + scr_error_set_prop(e, "pid", 3, scr_dyn_new_num(pid)); + scr_error_set_prop(e, "stdout", 6, out); + scr_error_set_prop(e, "stderr", 6, err); +} + #ifdef _WIN32 /* ── Windows arm: real children via CreateProcessW ──────────────────── * The POSIX arm below owns the corresponding implementation; this arm @@ -766,6 +878,7 @@ typedef struct { const char *spawn_errname; /* NULL = the child ran (and was reaped) */ bool timed_out; /* the deadline TerminateProcess fired */ DWORD exit_code; + DWORD pid; /* the child's process id (0 when it never ran) */ ScrCapBuf out, err; /* always initialized; caller frees */ } ScrWinSyncRes; @@ -780,6 +893,7 @@ static void scr_win_run_sync(ScrStr *cmd, ScrArr *args, const ScrStr *input, res->spawn_errname = NULL; res->timed_out = false; res->exit_code = 0; + res->pid = 0; scr_cap_init(&res->out); scr_cap_init(&res->err); @@ -842,6 +956,7 @@ static void scr_win_run_sync(ScrStr *cmd, ScrArr *args, const ScrStr *input, res->spawn_errname = sp.errname; return; } + res->pid = sp.pid; /* Pump: drain both captures, feed stdin (PIPE_NOWAIT — a full pipe * writes short, never blocks), watch the deadline. The 1ms wait rides @@ -1082,8 +1197,8 @@ static char *scr_exec_display(ScrStr *cmd, ScrArr *args, bool shell) { return out; } -static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, - bool with_stderr, bool always_nl) { +static ScrError *scr_exec_failed_error(const char *display, const ScrCapBuf *err_cap, + bool with_stderr, bool always_nl) { size_t dlen = strlen(display); size_t elen = with_stderr ? err_cap->len : 0; size_t cap = 16 + dlen + 1 + elen + 1; @@ -1095,18 +1210,33 @@ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, memcpy(msg + at, err_cap->data, elen); at += elen; } - scr_throw_error_msg(SCR_ERR_ERROR, msg, at); + ScrStr *message = scr_str_new(msg, at); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + return e; } -static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) { +/* Node's spawn-failure/timeout error with its errnoException fields + * (errno is libuv's code for the name). +1. */ +static ScrError *scr_exec_spawn_error(ScrStr *cmd, ScrArr *args, const char *errname, + bool async_shape) { + const char *verb = async_shape ? "spawn" : "spawnSync"; size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1; char *msg = malloc(cap); if (!msg) scr_child_oom(); - int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync", - cmd->data, errname); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname); + int len = snprintf(msg, cap, "%s %s %s", verb, cmd->data, errname); + ScrStr *message = scr_str_new(msg, (size_t)len); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(e, errname); + scr_child_error_spawn_fields(e, (double)scr_win_uv_errno(errname), verb, cmd, args); + return e; +} + +static void scr_exec_throw(ScrError *e /* moves */) { + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } /* The shared exec body (sync + promisified shapes — the POSIX arm's @@ -1115,7 +1245,7 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell, const ScrStr *input, const ScrStr *cwd, ScrArr *env_pairs, double timeout_ms, int stdout_mode, int stderr_mode, bool async_shape, - ScrStr **stderr_out) { + bool buffers, ScrStr **stderr_out) { /* stdio "inherit"'s stdin rides bit 4 of stdout_mode (scr_runtime.h). */ bool stdin_inherit = (stdout_mode & 4) != 0; stdout_mode &= 3; @@ -1129,31 +1259,67 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell, if (w.spawn_errname != NULL) { free(w.out.data); free(w.err.data); - scr_exec_throw_spawn(cmd, w.spawn_errname, async_shape); + ScrError *e = scr_exec_spawn_error(cmd, args, w.spawn_errname, async_shape); + if (async_shape) { + char *display = scr_exec_display(cmd, args, shell); + scr_child_error_cmd(e, display); + free(display); + ScrStr *empty = scr_str_new("", 0); + scr_child_error_outputs(e, empty, empty); + scr_str_release(empty); + } else { + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, NULL, 0, false, NULL, NULL); + } + scr_exec_throw(e); return NULL; } - if (w.timed_out && !async_shape) { - /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape - * reports the kill's exit-1 death as an ordinary command failure - * below, exactly Node's promisified execFile. */ - free(w.out.data); - free(w.err.data); - scr_exec_throw_spawn(cmd, "ETIMEDOUT", false); - return NULL; - } - bool failed = w.exit_code != 0; /* Node's inheritStderr: no stdio option given → the captured stderr - * echoes to the parent's stderr AFTER completion, success or failure. */ + * echoes to the parent's stderr AFTER completion, success, failure or + * timeout. */ if (stderr_mode == 0 && w.err.len > 0) { fflush(stdout); fwrite(w.err.data, 1, w.err.len, stderr); } + if (w.timed_out && !async_shape) { + /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape + * reports the kill's death as an ordinary command failure below, + * exactly Node's promisified execFile. */ + ScrError *e = scr_exec_spawn_error(cmd, args, "ETIMEDOUT", false); + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, "SIGTERM", (double)w.pid, true, + scr_child_sync_output(w.out.data, w.out.len, cap_out, buffers), + scr_child_sync_output(w.err.data, w.err.len, cap_err, buffers)); + free(w.out.data); + free(w.err.data); + scr_exec_throw(e); + return NULL; + } + bool failed = w.exit_code != 0 || w.timed_out; if (failed) { char *display = scr_exec_display(cmd, args, shell); - scr_exec_throw_failed(display, &w.err, cap_err, async_shape); + ScrError *e = scr_exec_failed_error(display, &w.err, cap_err, async_shape); + /* libuv's kill emulation: a timed-out child died to the kill signal. */ + bool exited = !w.timed_out; + if (async_shape) { + scr_child_error_exit_fields(e, exited, (double)w.exit_code, exited ? NULL : "SIGTERM", + w.timed_out); + scr_child_error_cmd(e, display); + ScrStr *out_s = scr_str_new(w.out.data, w.out.len); + ScrStr *err_s = scr_str_new(w.err.data, w.err.len); + scr_child_error_outputs(e, out_s, err_s); + scr_str_release(out_s); + scr_str_release(err_s); + } else { + scr_child_error_sync_fields(e, exited, (double)w.exit_code, exited ? NULL : "SIGTERM", + (double)w.pid, true, + scr_child_sync_output(w.out.data, w.out.len, cap_out, buffers), + scr_child_sync_output(w.err.data, w.err.len, cap_err, buffers)); + } free(display); free(w.out.data); free(w.err.data); + scr_exec_throw(e); return NULL; } if (async_shape && stderr_out != NULL) { @@ -1167,10 +1333,11 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs, - double timeout_ms, double stdout_mode, double stderr_mode) { + double timeout_ms, double stdout_mode, double stderr_mode, + bool buffers) { return scr_exec_run(cmd, args, shell, has_input ? input : NULL, cwd->len > 0 ? cwd : NULL, has_env ? env_pairs : NULL, - timeout_ms, (int)stdout_mode, (int)stderr_mode, false, NULL); + timeout_ms, (int)stdout_mode, (int)stderr_mode, false, buffers, NULL); } ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd, @@ -1178,7 +1345,7 @@ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd ScrStr *err_s = NULL; ScrStr *out_s = scr_exec_run(cmd, args, shell, NULL, cwd->len > 0 ? cwd : NULL, has_env ? env_pairs : NULL, timeout_ms, 1, 1, - true, &err_s); + true, false, &err_s); if (out_s == NULL) return NULL; /* exception pending; dummy result */ return scr_spawn_res_new(true, 0, out_s, err_s); } @@ -1302,6 +1469,9 @@ struct ScrChild { ScrClosure *exec_cb; ScrExecFileFn exec_fn; ScrStr *exec_display; + ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */ + ScrArr *exec_args; /* a spawn failure's path and spawnargs */ + void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */ ScrChildStream *out_stream; ScrChildStream *err_stream; ScrChildWriter *in_stream; @@ -1356,6 +1526,8 @@ void scr_child_release(ScrChild *c) { scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */ scr_str_release(c->err_msg); scr_str_release(c->exec_display); + scr_str_release(c->exec_file); + if (c->exec_args) scr_arr_release(c->exec_args); scr_dyn_release(c->out_dyn); scr_dyn_release(c->err_dyn); scr_dyn_release(c->in_dyn); @@ -2236,10 +2408,24 @@ ScrChild *scr_fork(double target, ScrArr *args, double in_mode, return child; } +/* Node's properties on the callback form's error. Reached only through + * the hook scr_exec_file installs, so programs without the callback form + * link none of the property code (the settle path is shared by every + * child). */ +static void scr_exec_file_error_props(ScrChild *c, ScrError *error) { + if (c->state == SCR_CHILD_SPAWN_FAILED) { + scr_child_error_spawn_fields(error, (double)c->spawn_uv_errno, "spawn", c->exec_file, c->exec_args); + } else { + scr_child_error_exit_fields(error, c->has_code, c->code, c->exit_signal, c->killed); + } + scr_child_error_cmd(error, c->exec_display->data); +} + static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { if (c->state == SCR_CHILD_SPAWN_FAILED) { ScrError *error = scr_error_new(0, c->err_msg); scr_error_set_code(error, c->spawn_errname); + if (c->exec_props) c->exec_props(c, error); return error; } if (c->has_code && c->code == 0) return NULL; @@ -2254,6 +2440,7 @@ static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { free(msg); ScrError *error = scr_error_new(0, message); scr_str_release(message); + if (c->exec_props) c->exec_props(c, error); return error; } @@ -2273,6 +2460,9 @@ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb, scr_str_release(empty); c->exec_cb = cb; c->exec_fn = fn; + c->exec_file = scr_str_retain(cmd); + c->exec_args = scr_arr_retain(args); + c->exec_props = &scr_exec_file_error_props; char *display = scr_exec_display(cmd, args, shell); c->exec_display = scr_str_new(display, strlen(display)); free(display); @@ -3099,9 +3289,10 @@ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ * the child completes (Node's inheritStderr flag — the capture is * unconditional); an explicit stdio option turns that off, and an * "ignore" stderr slot discards it entirely (nothing in the message). - * - The thrown error carries the MESSAGE only: Node's status/stdout/ - * stderr properties don't exist here (SEMANTICS.md — nothing the - * supported catch forms can read anyway). + * - The thrown error carries Node's own properties: status, signal, + * output, pid, stdout and stderr (Buffers, or strings with an encoding + * option), after the errnoException fields on a spawn failure or a + * timeout. * The result is the captured utf8 stdout (+1). */ typedef struct { @@ -3121,6 +3312,8 @@ typedef struct { * lands and the death reports as an ordinary command failure, exactly * Node's promisified behavior. */ bool async_shape; + /* The sync forms' error outputs are Buffers (no encoding option). */ + bool buffers; /* async_shape success: the captured stderr string (+1) lands here. */ ScrStr **stderr_out; } ScrExecOpts; @@ -3187,12 +3380,12 @@ static void scr_exec_envp_free(char **envp) { free(envp); } -/* The Command-failed thrower (message only — see the block comment). +/* The Command-failed error (+1; the caller adds its properties). * always_nl is the async shape: Node's promisified execFile appends * "\n" unconditionally (a trailing newline on empty stderr), * where the sync form appends only when stderr is non-empty. */ -static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, - bool with_stderr, bool always_nl) { +static ScrError *scr_exec_failed_error(const char *display, const ScrCapBuf *err_cap, + bool with_stderr, bool always_nl) { size_t dlen = strlen(display); size_t elen = with_stderr ? err_cap->len : 0; size_t cap = 16 + dlen + 1 + elen + 1; @@ -3204,22 +3397,34 @@ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, memcpy(msg + at, err_cap->data, elen); at += elen; } - scr_throw_error_msg(SCR_ERR_ERROR, msg, at); + ScrStr *message = scr_str_new(msg, at); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + return e; } -/* "spawnSync " — Node's uv-error message for spawn - * failures and timeouts, with `code` = the errno name (Node's - * errnoException shape; the Command-failed error stays code-less — Node - * puts the exit STATUS there, in properties this surface doesn't carry). */ -static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) { +/* "spawnSync " — Node's uv-error for spawn failures and + * timeouts (+1): `code` is the errno name, and the errnoException fields + * (errno, syscall, path, spawnargs) follow. */ +static ScrError *scr_exec_spawn_error(ScrStr *cmd, ScrArr *args, int errnum, bool async_shape) { + const char *errname = scr_errname(errnum); + const char *verb = async_shape ? "spawn" : "spawnSync"; size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1; char *msg = malloc(cap); if (!msg) scr_child_oom(); - int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync", - cmd->data, errname); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname); + int len = snprintf(msg, cap, "%s %s %s", verb, cmd->data, errname); + ScrStr *message = scr_str_new(msg, (size_t)len); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(e, errname); + scr_child_error_spawn_fields(e, -(double)errnum, verb, cmd, args); + return e; +} + +static void scr_exec_throw(ScrError *e /* moves */) { + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { @@ -3286,7 +3491,19 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { if (has_input) close(infd[1]); /* execSync's ENOENT is the SHELL missing — can't happen; the file * form reports Node's spawnSync error. */ - scr_exec_throw_spawn(cmd, scr_errname(spawn_err), o->async_shape); + ScrError *e = scr_exec_spawn_error(cmd, args, spawn_err, o->async_shape); + if (o->async_shape) { + char *display = scr_exec_display(cmd, args, o->shell); + scr_child_error_cmd(e, display); + free(display); + ScrStr *empty = scr_str_new("", 0); + scr_child_error_outputs(e, empty, empty); + scr_str_release(empty); + } else { + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, NULL, 0, false, NULL, NULL); + } + scr_exec_throw(e); return NULL; } @@ -3379,29 +3596,53 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { } while (reaped < 0 && errno == EINTR); } - char *display = NULL; - if (timed_out && !o->async_shape) { - /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape - * reports the SIGTERM death as an ordinary command failure below, - * exactly Node's promisified execFile. */ - free(out.data); - free(err.data); - scr_exec_throw_spawn(cmd, "ETIMEDOUT", false); - return NULL; - } - bool failed = !WIFEXITED(st) || WEXITSTATUS(st) != 0; /* Node's inheritStderr: no stdio option given → the captured stderr - * echoes to the parent's stderr AFTER completion, success or failure. */ + * echoes to the parent's stderr AFTER completion, success, failure or + * timeout. */ if (o->stderr_mode == 0 && err.len > 0) { fflush(stdout); fwrite(err.data, 1, err.len, stderr); } + bool exited = WIFEXITED(st) && !(timed_out && !o->async_shape); + const char *signal = WIFSIGNALED(st) ? scr_signal_name(WTERMSIG(st)) : NULL; + if (timed_out && !o->async_shape) { + /* The SYNC forms throw Node's spawnSync ETIMEDOUT (signal = the kill + * signal even when the child raced it); the async shape reports the + * SIGTERM death as an ordinary command failure below, exactly Node's + * promisified execFile. */ + ScrError *e = scr_exec_spawn_error(cmd, args, ETIMEDOUT, false); + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, signal != NULL ? signal : "SIGTERM", (double)pid, + true, scr_child_sync_output(out.data, out.len, cap_out, o->buffers), + scr_child_sync_output(err.data, err.len, cap_err, o->buffers)); + free(out.data); + free(err.data); + scr_exec_throw(e); + return NULL; + } + bool failed = !exited || WEXITSTATUS(st) != 0; if (failed) { - display = scr_exec_display(cmd, args, o->shell); - scr_exec_throw_failed(display, &err, cap_err, o->async_shape); + char *display = scr_exec_display(cmd, args, o->shell); + ScrError *e = scr_exec_failed_error(display, &err, cap_err, o->async_shape); + if (o->async_shape) { + scr_child_error_exit_fields(e, exited, exited ? (double)WEXITSTATUS(st) : 0, signal, + timed_out); + scr_child_error_cmd(e, display); + ScrStr *out_s = scr_str_new(out.data, out.len); + ScrStr *err_s = scr_str_new(err.data, err.len); + scr_child_error_outputs(e, out_s, err_s); + scr_str_release(out_s); + scr_str_release(err_s); + } else { + scr_child_error_sync_fields(e, exited, exited ? (double)WEXITSTATUS(st) : 0, signal, + (double)pid, true, + scr_child_sync_output(out.data, out.len, cap_out, o->buffers), + scr_child_sync_output(err.data, err.len, cap_err, o->buffers)); + } free(display); free(out.data); free(err.data); + scr_exec_throw(e); return NULL; } if (o->async_shape && o->stderr_out != NULL) { @@ -3421,7 +3662,8 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { * it means an empty environment). */ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs, - double timeout_ms, double stdout_mode, double stderr_mode) { + double timeout_ms, double stdout_mode, double stderr_mode, + bool buffers) { ScrExecOpts o = { .shell = shell, .input = has_input ? input : NULL, @@ -3433,6 +3675,7 @@ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, .stdout_mode = (int)stdout_mode & 3, .stderr_mode = (int)stderr_mode, .stdin_inherit = ((int)stdout_mode & 4) != 0, + .buffers = buffers, }; return scr_exec_sync_core(cmd, args, &o); } @@ -3634,6 +3877,9 @@ struct ScrChild { ScrClosure *exec_cb; ScrExecFileFn exec_fn; ScrStr *exec_display; + ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */ + ScrArr *exec_args; /* a spawn failure's path and spawnargs */ + void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */ /* Piped stdio (stdio mode 3): the stream handles child.stdout / * child.stderr answer — owned (+1 each), NULL when not piped. */ ScrChildStream *out_stream; @@ -3828,6 +4074,8 @@ void scr_child_release(ScrChild *c) { scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */ scr_str_release(c->err_msg); scr_str_release(c->exec_display); + scr_str_release(c->exec_file); + if (c->exec_args) scr_arr_release(c->exec_args); scr_dyn_release(c->out_dyn); scr_dyn_release(c->err_dyn); scr_dyn_release(c->in_dyn); @@ -3858,6 +4106,7 @@ static const char *scr_errname(int err) { case EAGAIN: return "EAGAIN"; case EIO: return "EIO"; case EINVAL: return "EINVAL"; + case ETIMEDOUT: return "ETIMEDOUT"; default: return "EUNKNOWN"; } } @@ -4881,10 +5130,24 @@ ScrChild *scr_fork(double target, ScrArr *args, double in_mode, return child; } +/* Node's properties on the callback form's error. Reached only through + * the hook scr_exec_file installs, so programs without the callback form + * link none of the property code (the settle path is shared by every + * child). */ +static void scr_exec_file_error_props(ScrChild *c, ScrError *error) { + if (c->state == SCR_CHILD_SPAWN_FAILED) { + scr_child_error_spawn_fields(error, -(double)c->spawn_errno, "spawn", c->exec_file, c->exec_args); + } else { + scr_child_error_exit_fields(error, c->has_code, c->code, c->exit_signal, c->killed); + } + scr_child_error_cmd(error, c->exec_display->data); +} + static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { if (c->state == SCR_CHILD_SPAWN_FAILED) { ScrError *error = scr_error_new(0, c->err_msg); scr_error_set_code(error, scr_errname(c->spawn_errno)); + if (c->exec_props) c->exec_props(c, error); return error; } if (c->has_code && c->code == 0) return NULL; @@ -4899,6 +5162,7 @@ static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { free(msg); ScrError *error = scr_error_new(0, message); scr_str_release(message); + if (c->exec_props) c->exec_props(c, error); return error; } @@ -4918,6 +5182,9 @@ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb, scr_str_release(empty); c->exec_cb = cb; c->exec_fn = fn; + c->exec_file = scr_str_retain(cmd); + c->exec_args = scr_arr_retain(args); + c->exec_props = &scr_exec_file_error_props; char *display = scr_exec_display(cmd, args, shell); c->exec_display = scr_str_new(display, strlen(display)); free(display); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index 1819b9c53..1187985c8 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -44,18 +44,22 @@ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ * 0 captures AND echoes to the parent stderr after completion (Node's * no-stdio-option default), 1 captures only, 2 ignores, 3 inherits. * Result is the captured utf8 stdout (+1) — "" when nothing captures - * (Node answers null there; discarded results cannot tell). All args + * (Node answers null there; discarded results cannot tell). A failure's + * error carries Node's status, signal, output, pid, stdout and stderr; + * `buffers` (no encoding option) makes those outputs Buffers. All args * borrowed. */ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs, - double timeout_ms, double stdout_mode, double stderr_mode); + double timeout_ms, double stdout_mode, double stderr_mode, + bool buffers); /* The promisified-execFile/exec capture (cp.execCapture; shell as for * scr_exec_sync — the /bin/sh -c argv): the same core in the * async shape — both streams captured (no echo), Node's ASYNC error * messages on the throw paths ("Command failed: \n" with the * unconditional newline, "spawn " with .code, timeouts as - * ordinary SIGTERM command failures). +1 ScrSpawnRes on success (stdout + - * stderr; status unused). All args borrowed. */ + * ordinary SIGTERM command failures), each error with Node's code, killed, + * signal, cmd, stdout and stderr properties. +1 ScrSpawnRes on success + * (stdout + stderr; status unused). All args borrowed. */ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd, bool has_env, ScrArr *env_pairs, double timeout_ms); ScrSpawnRes *scr_spawn_res_retain(ScrSpawnRes *r); diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index 97b7a3e97..df62a79a3 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -159,22 +159,20 @@ static void sc_throw_openssl(const char *packed, const char *reason, const char snprintf(message, sizeof message, "error:%s:Provider routines::%s", packed, reason); ScrStr *text = scr_str_new(message, strlen(message)); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); - ScrDyn *view = scr_dyn_from_error(e); ScrDyn *stack = scr_dyn_new_arr(); scr_dyn_arr_push(stack, scr_dyn_new_str(text)); scr_str_release(text); - scr_dyn_obj_set(view, "opensslErrorStack", 17, stack); + scr_error_set_prop(e, "opensslErrorStack", 17, stack); ScrStr *library = scr_str_new("Provider routines", 17); - scr_dyn_obj_set(view, "library", 7, scr_dyn_new_str(library)); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); scr_str_release(library); ScrStr *reason_text = scr_str_new(reason, strlen(reason)); - scr_dyn_obj_set(view, "reason", 6, scr_dyn_new_str(reason_text)); + scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); scr_str_release(reason_text); scr_error_set_code(e, code); ScrStr *code_text = scr_str_new(code, strlen(code)); - scr_dyn_obj_set(view, "code", 4, scr_dyn_new_str(code_text)); + scr_error_set_prop(e, "code", 4, scr_dyn_new_str(code_text)); scr_str_release(code_text); - scr_dyn_release(view); scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index b02c41c03..c34923607 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -1297,9 +1297,9 @@ static ScrDyn *scr_dns_exception(int rc, const char *syscall, const ScrStr *host scr_error_set_code(e, code); e->system_call = scr_str_new(syscall, strlen(syscall)); e->system_errno = errnum; + scr_error_set_prop(e, "hostname", 8, scr_dyn_new_str((ScrStr *)host)); ScrDyn *d = scr_dyn_from_error(e); scr_error_release(e); - scr_dyn_obj_set(d, "hostname", 8, scr_dyn_new_str((ScrStr *)host)); return d; } diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bccd8d55c..86fcc7709 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -998,6 +998,10 @@ ScrError *scr_error_from_dyn(const ScrDyn *d); /* scr_async_dyn.c (gated) */ * the gated extraction reads/writes through these). */ ScrError *scr_errdyn_err_of(const ScrDyn *d); /* +1 or NULL */ void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */ +/* Sets an own property on an error: its checked-dynamic node, which casts + * and util.inspect read. Moves value. */ +void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *value); +ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */ /* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */ bool scr_dyn_truthy(const ScrDyn *d); /* JS String() over a dyn value (arrays join, [object Object], the error diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 34438353f..4aa703711 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -801,14 +801,32 @@ static ScrStr *scr_insp_array_buffer(const ScrBytes *h, const char *cls, double return out; } +static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth); + +/* A runtime error through its node: the cached one, or one built from its + * fields. */ +ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth) { + ScrDyn *view = scr_errdyn_view_of(e); + if (view != NULL && view->kind == SCR_DYN_OBJ) return scr_insp_dyn_error(view, recurse, depth); + ScrDyn *fields = scr_error_dyn_fields(e); + ScrStr *out = scr_insp_dyn_error(fields, recurse, depth); + scr_dyn_release(fields); + return out; +} + /* Errors: the STACKLESS rendering — compiled binaries carry no stack, so * the base is formatError's bracket form `[Name: message]` / `[Name]` * (Node prints these exact forms for an error whose stack is empty; - * stack-carrying Node output is a documented divergence). A stamped - * `code` slot renders as the one extra own property Node would show: - * `[Error: m] { code: 'X' }`, `[Error]` beyond depth. Message newlines - * indent like Node's formatError tail. */ + * stack-carrying Node output is a documented divergence). A runtime error + * renders through its checked-dynamic node, so the own properties Node + * shows follow in key order: the system-error fields, code, and the extra + * properties a throw site attached (`[Error: m] { errno: -2, code: + * 'ENOENT', ... }`). Other errors render a stamped `code` slot as their + * one extra property: `[Error: m] { code: 'X' }`, `[Error]` beyond depth. + * Message newlines indent like Node's formatError tail. */ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) { + if (e->vt >= &scr_error_vts[0] && e->vt < &scr_error_vts[SCR_ERR_COUNT]) + return scr_insp_error_node(e, recurse, depth); bool has_code = e->code != NULL; /* the stamped slot, read directly (borrowed) */ if (has_code && recurse > depth) { /* `[${getCtxStyle(...)}]` with the trailing space sliced: [Name] */ @@ -897,13 +915,53 @@ static const ScrDyn *insp_error_slot(const ScrDyn *d, const char *key, size_t le return v != NULL && v->kind == SCR_DYN_STR ? v : NULL; } +/* AssertionError's custom inspect (lib/internal/assert/assertion_error.js) + * renders the error at depth 0 and shortens string actual/expected values + * with addEllipsis: past 10 lines, the first 10 and "\n..."; otherwise past + * 512 characters, the text from offset 512 on and "...". +1. */ +static ScrDyn *insp_assertion_ellipsis(ScrDyn *value) { + if (value->kind != SCR_DYN_STR) return scr_dyn_retain(value); + ScrStr *text = value->v.str; + size_t lines = 1, cut = 0; + for (size_t i = 0; i < text->len; i++) { + if (text->data[i] != '\n') continue; + if (++lines == 11) { + cut = i; + break; + } + } + InspBuf b = {0}; + if (lines > 10) { + ib_bytes(&b, text->data, cut); + ib_cstr(&b, "\n..."); + } else if (scr_str_utf16_len(text) > 512) { + ScrStr *tail = scr_str_slice(text, 512, scr_str_utf16_len(text)); + ib_str(&b, tail); + ib_cstr(&b, "..."); + scr_str_release(tail); + } else { + return scr_dyn_retain(value); + } + ScrStr *shortened = ib_take(&b); + ScrDyn *out = scr_dyn_new_str(shortened); + scr_str_release(shortened); + return out; +} + /* The checked-dynamic Error encoding (scr_error_dyn_fields) rendered like * scr_insp_error: the stackless bracket form `[Name: message]`, then the * remaining enumerable own properties (code, errno, syscall, ...) in * Node's `{ ... }` tail. */ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { + /* An error already on the traversal stack (a spawnSync error's own + * `error`): Node's [Circular *N], and on the outer rendering. */ + double circular = scr_insp_circ_check(d); + if (circular != 0) return scr_insp_circular(circular); const ScrDyn *name = insp_error_slot(d, "name", 4); const ScrDyn *message = insp_error_slot(d, "message", 7); + bool assertion = name != NULL && name->v.str->len == 14 && + memcmp(name->v.str->data, "AssertionError", 14) == 0; + double value_depth = assertion ? recurse : depth; size_t extra = 0; for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *ent = &d->v.obj.entries[i]; @@ -934,6 +992,7 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { ScrStr *base_str = ib_take(&base); if (extra == 0) return base_str; scr_insp_begin(recurse + 1); + scr_insp_seen_push(d); for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *ent = &d->v.obj.entries[i]; if (!ent->enumerable) continue; @@ -941,7 +1000,11 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)) continue; - ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + bool shortened = assertion && ((ent->key_len == 6 && memcmp(ent->key, "actual", 6) == 0) || + (ent->key_len == 8 && memcmp(ent->key, "expected", 8) == 0)); + ScrDyn *shown = shortened ? insp_assertion_ellipsis(ent->value) : scr_dyn_retain(ent->value); + ScrStr *val = scr_insp_dyn(shown, recurse + 1, value_depth); + scr_dyn_release(shown); InspBuf eb = {0}; insp_key_into(&eb, ent->key, ent->key_len); ib_cstr(&eb, ": "); @@ -953,10 +1016,12 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { } ScrStr *b0 = scr_str_new("{", 1); ScrStr *b1 = scr_str_new("}", 1); - ScrStr *out = scr_insp_end(base_str, b0, b1, recurse + 1, false, false); + ScrStr *body = scr_insp_end(base_str, b0, b1, recurse + 1, false, false); scr_str_release(b0); scr_str_release(b1); scr_str_release(base_str); + ScrStr *out = scr_insp_ref_wrap(d, body); + scr_str_release(body); return out; } diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index f8d28b4f0..4414330b5 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -26,10 +26,15 @@ ScrStr *scr_insp_regex(ScrRegex *re); /* /source/flags */ /* A Date (its time value): toISOString(), or "Invalid Date" for NaN (+1). */ ScrStr *scr_insp_date(double ms); ScrStr *scr_insp_buffer(ScrBytes *b); /* , 50-byte cap */ -/* The stackless error form: [Name: message] / [Name], plus the stamped - * code slot as its one extra property ({ code: 'X' }); [Name] beyond - * depth when a code makes it composite. */ +/* The stackless error form: [Name: message] / [Name], then a runtime + * error's own properties through its checked-dynamic node, or another + * error's stamped code slot as its one extra property ({ code: 'X' }); + * [Name] beyond depth when a code makes it composite. */ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth); +/* A runtime error rendered through its checked-dynamic node (the cached + * one, or one built from its fields), so Node's own properties follow the + * bracket form. */ +ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth); /* The checked-dynamic tree: shape lives in the value, so the whole * traversal is here. dyn-boxed bytes render in the checked-dynamic tree's documented * Uint8Array identity. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 401933904..0376ece2c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2568,10 +2568,26 @@ static bool scr_record_source_declared(const ScrRecordSource *source, const char return scr_record_source_index(source, key, key_len) < source->count; } +/* Frees the table at thread exit (a worker's table dies with its thread): + * the storage is detached before any remaining view is released. */ +static void scr_record_source_teardown(void) { + ScrRecordSource *table = scr_record_sources; + size_t capacity = scr_record_source_capacity; + scr_record_sources = NULL; + scr_record_source_capacity = scr_record_source_count = scr_record_source_deleted = 0; + free(scr_record_source_filter); + scr_record_source_filter = NULL; + for (size_t i = 0; i < capacity; i++) { + if (table[i].rec && table[i].rec != SCR_RECORD_SOURCE_DELETED) scr_dyn_release(table[i].view); + } + free(table); +} + /* Inserts the entry of a record the caller just built; the table takes * the entry's view. A fresh record has no entry: the previous owner of its * address left the table when it was freed (the dispose hook). */ static void scr_record_source_put(const ScrRecordSource *entry) { + if (!scr_record_sources) scr_atexit(scr_record_source_teardown); scr_record_source_reserve(); if (!scr_record_source_filter) { scr_record_source_filter = calloc(((size_t)1 << SCR_RECORD_SOURCE_FILTER_BITS) / 64, @@ -6392,10 +6408,13 @@ ScrStr *scr_dyn_format_j(const ScrDyn *d) { * IDENTITY-CACHED: one error instance boxes to ONE dyn node, however many * times it crosses (Node passes the error OBJECT through, so `found.error * === thrown` and re-crossings compare reference-equal — the tracing - * suite's shape). The cache retains both sides for the process (like the - * dc registry; released atexit before the RC audit) and the node - * SNAPSHOTS name/message/code at first crossing — a later mutation of the - * error is invisible through it (SEMANTICS.md). Two pointer-keyed hash + * suite's shape). The cache retains both sides; an entry lives while + * either side is referenced elsewhere, and a sweep releases the entries + * whose error and node only the cache still references (the rest are + * released atexit before the RC audit). The node also holds the error's + * extra own properties (scr_error_set_prop) and SNAPSHOTS name/message/ + * code at first crossing — a later mutation of the error is invisible + * through it (SEMANTICS.md). Two pointer-keyed hash * indexes find an entry by either side: Promise.allSettled and * Promise.any cross every rejection reason. Both sides are unique per * entry (an error boxes once; an alien view enters once). */ @@ -6410,6 +6429,9 @@ static SCR_TL bool scr_errdyn_teardown_registered = false; /* Open-addressed indexes holding entry index + 1 (0 = empty slot). */ static SCR_TL size_t *scr_errdyn_by_err = NULL, *scr_errdyn_by_dyn = NULL; static SCR_TL size_t scr_errdyn_slots = 0; /* a power of two */ +/* The entry count that triggers the next sweep: twice the entries the + * previous sweep kept, so sweeping costs amortized O(1) per entry. */ +static SCR_TL size_t scr_errdyn_sweep_at = 64; static size_t scr_errdyn_hash(const void *p) { size_t x = (size_t)(uintptr_t)p >> 4; @@ -6445,8 +6467,70 @@ static size_t scr_errdyn_find(const void *key, bool by_err) { static void scr_errdyn_teardown(void); +/* The entries of a node that hold the node itself (a spawnSync error's + * `error`): references the node makes to itself, not reachability. */ +static size_t scr_errdyn_self_refs(const ScrDyn *d) { + if (d->kind != SCR_DYN_OBJ) return 0; + size_t n = 0; + for (size_t i = 0; i < d->v.obj.len; i++) n += d->v.obj.entries[i].value == d; + return n; +} + +/* Releases a node the cache owned, dropping its references to itself + * first so the release frees it. */ +static void scr_errdyn_release_node(ScrDyn *d) { + for (size_t i = 0; d->kind == SCR_DYN_OBJ && i < d->v.obj.len; i++) { + ScrDynEntry *entry = &d->v.obj.entries[i]; + if (entry->value != d) continue; + entry->value = scr_dyn_retain(scr_dyn_undefined()); + scr_dyn_release(d); + } + scr_dyn_release(d); +} + +/* Releases the entries nothing but the cache references. A field node + * holds no reference to its error and a registered subclass box holds + * one, so an entry whose node has only the cache's reference and whose + * error has no reference beyond the cache's and its box's is + * unreachable. The indexes are rebuilt over the + * kept entries before anything is released (a teardown never sees a + * stale entry); an entry a released node kept alive goes in a later + * sweep. */ +static void scr_errdyn_sweep(void) { + ScrErrDynEnt *dead = NULL; + size_t n_dead = 0, kept = 0; + for (size_t i = 0; i < scr_errdyn_n; i++) { + ScrErrDynEnt ent = scr_errdyn_cache[i]; + size_t boxed = ent.dyn->kind == SCR_DYN_TYPED_REF && ent.dyn->v.typed_ref.ptr == (void *)ent.err; + if (ent.dyn->rc == 1 + scr_errdyn_self_refs(ent.dyn) && ent.err->rc == 1 + boxed) { + if (dead == NULL) { + dead = malloc((scr_errdyn_n - i) * sizeof *dead); + if (!dead) scr_trap("scriptc: out of memory\n"); + } + dead[n_dead++] = ent; + } else { + scr_errdyn_cache[kept++] = ent; + } + } + scr_errdyn_n = kept; + scr_errdyn_sweep_at = kept * 2 > 64 ? kept * 2 : 64; + if (n_dead == 0) return; + memset(scr_errdyn_by_err, 0, scr_errdyn_slots * sizeof *scr_errdyn_by_err); + memset(scr_errdyn_by_dyn, 0, scr_errdyn_slots * sizeof *scr_errdyn_by_dyn); + for (size_t i = 0; i < scr_errdyn_n; i++) { + scr_errdyn_index_put(scr_errdyn_by_err, scr_errdyn_cache[i].err, i, true); + scr_errdyn_index_put(scr_errdyn_by_dyn, scr_errdyn_cache[i].dyn, i, false); + } + for (size_t i = 0; i < n_dead; i++) { + scr_error_release(dead[i].err); + scr_errdyn_release_node(dead[i].dyn); + } + free(dead); +} + /* Appends an entry (both sides retained) and indexes it. */ static void scr_errdyn_append(ScrError *e, ScrDyn *d) { + if (scr_errdyn_n >= scr_errdyn_sweep_at) scr_errdyn_sweep(); if (scr_errdyn_n == scr_errdyn_cap) { scr_errdyn_cap = scr_errdyn_cap ? scr_errdyn_cap * 2 : 8; scr_errdyn_cache = realloc(scr_errdyn_cache, scr_errdyn_cap * sizeof *scr_errdyn_cache); @@ -6502,17 +6586,21 @@ void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)) { } static void scr_errdyn_teardown(void) { - for (size_t i = 0; i < scr_errdyn_n; i++) { - scr_error_release(scr_errdyn_cache[i].err); - scr_dyn_release(scr_errdyn_cache[i].dyn); - } - free(scr_errdyn_cache); + /* Detach the storage first: no release below sees a half-torn cache. */ + ScrErrDynEnt *cache = scr_errdyn_cache; + size_t n = scr_errdyn_n; scr_errdyn_cache = NULL; scr_errdyn_n = scr_errdyn_cap = 0; free(scr_errdyn_by_err); free(scr_errdyn_by_dyn); scr_errdyn_by_err = scr_errdyn_by_dyn = NULL; scr_errdyn_slots = 0; + scr_errdyn_sweep_at = 64; + for (size_t i = 0; i < n; i++) { + scr_error_release(cache[i].err); + scr_errdyn_release_node(cache[i].dyn); + } + free(cache); } ScrDyn *scr_error_dyn_fields(const ScrError *e) { @@ -6540,7 +6628,11 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message)); d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; } - if (e->stack) scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); + if (e->stack) { + /* Node's own `stack` is non-enumerable. */ + scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); + d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + } if (e->system_message) { /* SystemError: code, the read-only `info` context ({ message, path, * syscall, errno, code }), then errno, syscall, path and dest. */ @@ -6617,6 +6709,23 @@ ScrDyn *scr_dyn_from_error(const ScrError *e) { return d; } +/* An own property on an error, as a throw site attaches Node's extra + * fields (a dns error's hostname, an exec error's status): set on the + * error's checked-dynamic node, which casts, the checked-dynamic surface + * and util.inspect read. Moves value. */ +void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *value) { + ScrDyn *view = scr_dyn_from_error(e); + scr_dyn_obj_set(view, key, key_len, value); + scr_dyn_release(view); +} + +/* The error's checked-dynamic node when it has one (borrowed), else NULL: + * util.inspect reads its extra own properties. */ +ScrDyn *scr_errdyn_view_of(const ScrError *e) { + size_t hit = scr_errdyn_find(e, true); + return hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].dyn; +} + /* The %Error EXTRACTION (dynCheck of `u as Error` / an instanceof-Error * narrow, and the dyn-boxed thunk's Error-typed parameters): the REVERSE * of scr_dyn_from_error, riding the same identity cache — a dyn error @@ -7741,7 +7850,15 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return; } + bool created = scr_dyn_entry(recv, key) == NULL; scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value)); + if (created && key->len == 5 && memcmp(key->data, "stack", 5) == 0 && + scr_errdyn_find(recv, false) != SIZE_MAX) { + /* An error's own `stack` exists from construction as a non-enumerable + * data property, so assigning it adds no enumerable key. */ + ScrDynEntry *entry = scr_dyn_entry(recv, key); + if (entry) entry->enumerable = false; + } scr_error_sync_cause(recv, key); return; } diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index af57ab057..6493494f6 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -8738,12 +8738,10 @@ static void scr_crypto_not_xof(void) { ScrStr *text = scr_str_new(message, sizeof message - 1); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); scr_str_release(text); - ScrDyn *view = scr_dyn_from_error(e); - scr_dyn_obj_set(view, "library", 7, scr_crypto_dyn_str(scr_str_new("digital envelope routines", 25))); - scr_dyn_obj_set(view, "reason", 6, scr_crypto_dyn_str(scr_str_new("not XOF or invalid length", 25))); + scr_error_set_prop(e, "library", 7, scr_crypto_dyn_str(scr_str_new("digital envelope routines", 25))); + scr_error_set_prop(e, "reason", 6, scr_crypto_dyn_str(scr_str_new("not XOF or invalid length", 25))); scr_error_set_code(e, code); - scr_dyn_obj_set(view, "code", 4, scr_crypto_dyn_str(scr_str_new(code, sizeof code - 1))); - scr_dyn_release(view); + scr_error_set_prop(e, "code", 4, scr_crypto_dyn_str(scr_str_new(code, sizeof code - 1))); scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index f5ef488dc..c905ea8ae 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -395,9 +395,9 @@ static ScrDyn *uc_callbackify_rejected(ScrClosure *closure, ScrDyn *const *args, ScrError *e = scr_error_new(SCR_ERR_ERROR, text); scr_str_release(text); scr_error_set_code(e, "ERR_FALSY_VALUE_REJECTION"); + scr_error_set_prop(e, "reason", 6, scr_dyn_retain(reason)); err = scr_dyn_from_error(e); scr_error_release(e); - scr_dyn_obj_set(err, "reason", 6, scr_dyn_retain(reason)); } uc_schedule_callback(cb, err, scr_dyn_retain(scr_dyn_undefined()), 1); scr_dyn_release(cb); diff --git a/tests/corpus/5634-error-extra-props-inspect.ts b/tests/corpus/5634-error-extra-props-inspect.ts new file mode 100644 index 000000000..b5d16ef97 --- /dev/null +++ b/tests/corpus/5634-error-extra-props-inspect.ts @@ -0,0 +1,64 @@ +// util.inspect of errors that carry Node's extra own properties: the +// AssertionError fields (rendered at depth 0, with long string values +// shortened), the falsy-rejection reason util.callbackify attaches, an +// OpenSSL-style crypto error's library and reason, and a system error's +// errno, syscall and path. Each error's stack is cleared first, so Node +// prints the same stackless form. +import * as assert from "node:assert"; +import * as crypto from "node:crypto"; +import * as fs from "node:fs"; +import { callbackify, inspect } from "node:util"; + +function show(e: unknown): void { + if (e instanceof Error) { + e.stack = ""; + console.log(inspect(e)); + } +} + +try { + assert.strictEqual(1, 2); +} catch (e) { + show(e); +} +type Doc = { a: number; nested?: { b: number[] } }; +const left: Doc = { a: 1, nested: { b: [1, 2] } }; +const right: Doc = { a: 2 }; +try { + assert.deepStrictEqual(left, right, "custom message"); +} catch (e) { + show(e); +} +try { + assert.notStrictEqual("x".repeat(600), "x".repeat(600)); +} catch (e) { + show(e); +} +try { + assert.ok(false); +} catch (e) { + show(e); +} +try { + throw new RangeError("no extra properties"); +} catch (e) { + show(e); +} +try { + crypto.createHash("sha256", { outputLength: 5 }); +} catch (e) { + show(e); +} +try { + fs.readFileSync("/no/such/dir/file.txt"); +} catch (e) { + show(e); + console.log(inspect({ error: e }, { depth: 0 })); +} + +async function rejectsNull(): Promise { + return Promise.reject(null); +} +callbackify(rejectsNull)((err: NodeJS.ErrnoException | null) => { + show(err); +}); diff --git a/tests/corpus/5635-error-extra-props-retry-loop.ts b/tests/corpus/5635-error-extra-props-retry-loop.ts new file mode 100644 index 000000000..8cba3b984 --- /dev/null +++ b/tests/corpus/5635-error-extra-props-retry-loop.ts @@ -0,0 +1,31 @@ +// Errors that cross into checked-dynamic code (a cast of a caught error) +// in a retry loop, on the main thread and in a worker at the same time: +// every crossing reads the properties of its own error. +import * as assert from "node:assert"; +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +function round(n: number, offset: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + try { + assert.strictEqual(i + offset, -1); + } catch (e) { + const err = e as { code?: string; operator?: string; actual?: number }; + total += (err.code ?? "").length + (err.operator ?? "").length + (err.actual === i + offset ? 1 : 0); + } + } + return total; +} + +if (isMainThread) { + const worker = new Worker(new URL(import.meta.url)); + const fromWorker = new Promise((resolve) => worker.once("message", resolve)); + let sum = 0; + for (let r = 0; r < 5; r++) sum += round(5000, r); + console.log("main", sum); + console.log("worker", await fromWorker); +} else { + let sum = 0; + for (let r = 0; r < 5; r++) sum += round(5000, 100 + r); + parentPort?.postMessage(sum); +} diff --git a/tests/corpus/5636-child-error-props.ts b/tests/corpus/5636-child-error-props.ts new file mode 100644 index 000000000..b62f4385c --- /dev/null +++ b/tests/corpus/5636-child-error-props.ts @@ -0,0 +1,95 @@ +// child_process errors carry Node's own properties: the sync forms' +// status, signal, output, pid, stdout and stderr (Buffers, or strings with +// an encoding option), the async forms' code, killed, signal and cmd (with +// stdout and stderr when promisified), and a spawn failure's errno, +// syscall, path and spawnargs. Pids vary, so only their type prints. +import { exec, execFile, execFileSync, execSync } from "node:child_process"; +import { inspect, promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +function keys(e: unknown): string { + return Object.keys(e as object).join(","); +} + +function show(label: string, e: unknown): void { + if (!(e instanceof Error)) return; + const value: unknown = e; + const pid = typeof (value as { pid?: number }).pid; + e.stack = ""; + const text = inspect(e).replace(/pid: \d+/, "pid: "); + console.log(label, pid, keys(e)); + console.log(text); +} + +try { + execFileSync("/bin/sh", ["-c", "echo out; echo err 1>&2; exit 3"], { stdio: "pipe" }); +} catch (e) { + const err = e as { status?: number | null; signal?: string | null; stdout?: Buffer; stderr?: Buffer }; + console.log("status", err.status, err.signal, err.stdout?.toString(), err.stderr?.toString()); + show("execFileSync", e); +} +try { + execSync("echo text; exit 4", { encoding: "utf8", stdio: "pipe" }); +} catch (e) { + const err = e as { status?: number | null; stdout?: string; output?: (string | null)[] }; + console.log("encoding", err.status, JSON.stringify(err.stdout), inspect(err.output)); +} +try { + execSync("exit 5", { stdio: "ignore" }); +} catch (e) { + const err = e as { output?: unknown[]; stdout?: unknown }; + console.log("ignored", inspect(err.output), err.stdout); +} +try { + execFileSync("/bin/sh", ["-c", "kill -TERM $$"], { stdio: "pipe" }); +} catch (e) { + const err = e as { status?: number | null; signal?: string | null }; + console.log("signal", err.status, err.signal); +} +try { + execFileSync("/no/such/binary", ["a", "b"]); +} catch (e) { + show("spawn failure", e); + const err = e as { errno?: number; syscall?: string; path?: string; spawnargs?: string[] }; + console.log(err.errno, err.syscall, err.path, inspect(err.spawnargs)); +} +try { + execFileSync("/bin/sh", ["-c", "echo partial; exec sleep 5"], { timeout: 100, stdio: "pipe" }); +} catch (e) { + const err = e as { code?: string; status?: number | null; signal?: string | null; stdout?: Buffer }; + console.log("timeout", err.code, err.status, err.signal, err.stdout?.toString()); +} + +execFile("/bin/sh", ["-c", "echo o; exit 6"], (err, stdout) => { + const fields: unknown = err; + const e = fields as { code?: number | null; killed?: boolean; signal?: string | null; cmd?: string }; + console.log("callback", e.code, e.killed, e.signal, e.cmd, JSON.stringify(stdout)); + show("callback error", err); + execFile("/no/such/binary", ["x"], (missing) => { + show("callback spawn failure", missing); + exec("echo shell; exit 4", (shellError) => { + show("exec callback", shellError); + void promised(); + }); + }); +}); + +async function promised(): Promise { + try { + await execFileAsync("/bin/sh", ["-c", "echo po; echo pe 1>&2; exit 7"]); + } catch (e) { + show("promisified", e); + } + try { + await execFileAsync("/bin/sh", ["-c", "exec sleep 5"], { timeout: 100 }); + } catch (e) { + const err = e as { code?: number | null; killed?: boolean; signal?: string | null }; + console.log("promisified timeout", err.code, err.killed, err.signal); + } + try { + await execFileAsync("/no/such/binary"); + } catch (e) { + show("promisified spawn failure", e); + } +} diff --git a/tests/fixtures/error-cache-sweep/retry-loop.ts b/tests/fixtures/error-cache-sweep/retry-loop.ts new file mode 100644 index 000000000..3c58b0ba6 --- /dev/null +++ b/tests/fixtures/error-cache-sweep/retry-loop.ts @@ -0,0 +1,34 @@ +// 100,000 caught AssertionErrors read through a cast, on the main thread +// or (with the "worker" argument) in a worker; prints the resident-set +// growth in MiB after a warm-up round. +import * as assert from "node:assert"; +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +function round(n: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + try { + assert.strictEqual(i, -1); + } catch (e) { + const err = e as { code?: string; operator?: string }; + total += (err.code ?? "").length + (err.operator ?? "").length; + } + } + return total; +} + +function measure(): number { + round(20000); + const before = process.memoryUsage().rss; + for (let r = 0; r < 5; r++) round(20000); + return Math.round((process.memoryUsage().rss - before) / 1048576); +} + +if (!isMainThread) { + parentPort?.postMessage(measure()); +} else if (process.argv[2] === "worker") { + const worker = new Worker(new URL(import.meta.url)); + worker.once("message", (growth: number) => console.log(growth)); +} else { + console.log(measure()); +} diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 000efcdfc..1a4ad1180 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1982,6 +1982,9 @@ "5631-promisify-crypto.ts": 2200, "5632-promisify-dns.ts": 1700, "5633-zlib-typed-options.ts": 1800, + "5634-error-extra-props-inspect.ts": 2300, + "5635-error-extra-props-retry-loop.ts": 3300, + "5636-child-error-props.ts": 7600, "5644-net-socket-addresses.ts": 2220, "5645-net-socket-callbacks.ts": 2442, "5646-net-server-connections.ts": 2389, @@ -5052,6 +5055,9 @@ "5631-promisify-crypto.ts": 2600, "5632-promisify-dns.ts": 2000, "5633-zlib-typed-options.ts": 2200, + "5634-error-extra-props-inspect.ts": 2000, + "5635-error-extra-props-retry-loop.ts": 6100, + "5636-child-error-props.ts": 9800, "5644-net-socket-addresses.ts": 2393, "5645-net-socket-callbacks.ts": 2060, "5646-net-server-connections.ts": 2755, diff --git a/tests/harness/error-cache-sweep.test.ts b/tests/harness/error-cache-sweep.test.ts new file mode 100644 index 000000000..821a0aae2 --- /dev/null +++ b/tests/harness/error-cache-sweep.test.ts @@ -0,0 +1,38 @@ +/* Errors that cross into checked-dynamic code stay in the runtime's error + * identity cache only while something references them: a retry loop that + * reads 100,000 caught errors through a cast keeps the resident set flat, + * on the main thread and in a worker (each thread sweeps its own cache). + * Plain lane only: the sanitizer's allocator quarantine holds freed + * memory, so resident size says nothing there. */ +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/error-cache-sweep"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +describe.skipIf(sanitize)("error identity cache", () => { + test("a retry loop of crossing errors keeps memory flat", { timeout: 300_000 }, async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(join(repoRoot, "tests/fixtures/error-cache-sweep/retry-loop.ts"), { + outPath: join(outDir, "retry-loop"), + outDir, + backend: "llvm", + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + for (const args of [[], ["worker"]]) { + const run = await execFileAsync(result.binaryPath, args); + expect(run.stderr).toBe(""); + expect(Number(run.stdout.trim()), `growth in MiB (${args.join(" ") || "main"})`).toBeLessThan( + 16, + ); + } + }); +}); From b6e9e42308d9c2565ee02c0cd4072838389e0fe2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 21:56:43 -0500 Subject: [PATCH 094/120] Derive the docs compatibility metadata at build time node-v24-compatibility-meta.json held two values derived from the public compatibility artifact: a content hash for the data URL and its row count. Every compatibility change rewrote the hash line, so concurrent changes conflicted on it. next.config.mjs now computes both from the committed artifact and passes them to the page through Next's build-time env, with the same values. The generator no longer writes the file, and its local check compares the public artifact itself. --- AGENTS.md | 1 - docs/next.config.mjs | 11 +++++++++ .../components/node-compatibility-matrix.tsx | 13 +++++++---- .../node-v24-compatibility-meta.json | 7 ------ docs/test/routes.test.mjs | 7 ++++-- internal/compatibility/src/generate.mjs | 23 ++----------------- 6 files changed, 26 insertions(+), 36 deletions(-) delete mode 100644 docs/src/generated/node-v24-compatibility-meta.json diff --git a/AGENTS.md b/AGENTS.md index b94c90067..c2bdbea2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,7 +140,6 @@ Never hand-edit these files: - `internal/compatibility/generated/node-v24-internal.json` - `internal/compatibility/generated/node-v24-backlog.json` - `docs/src/generated/node-v24-compatibility.json` -- `docs/src/generated/node-v24-compatibility-meta.json` Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes. diff --git a/docs/next.config.mjs b/docs/next.config.mjs index 36c0ea2cb..7079a6844 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -1,10 +1,21 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; import { createGeistdocs } from "@vercel/geistdocs/next"; const withGeistdocs = createGeistdocs(); const legacySlugs = ["quickstart", "coverage", "dependencies", "ffi", "native-objects", "wasm", "platforms", "cli", "how-it-works", "limitations"]; +// The compatibility page fetches its data under a content hash of the +// committed artifact, so a deploy never serves a cached older copy, and +// checks the row count it expects. Both are derived here at build time. +const compatibilityArtifact = readFileSync(new URL("./src/generated/node-v24-compatibility.json", import.meta.url), "utf8"); + /** @type {import('next').NextConfig} */ const nextConfig = { + env: { + COMPATIBILITY_ARTIFACT_VERSION: createHash("sha256").update(compatibilityArtifact).digest("hex").slice(0, 20), + COMPATIBILITY_ROW_COUNT: String(JSON.parse(compatibilityArtifact).rows.length), + }, agentRules: false, allowedDevOrigins: ["127.0.0.1"], distDir: process.env.NEXT_DIST_DIR || ".next", diff --git a/docs/src/components/node-compatibility-matrix.tsx b/docs/src/components/node-compatibility-matrix.tsx index f0a817a2f..095bcdaf2 100644 --- a/docs/src/components/node-compatibility-matrix.tsx +++ b/docs/src/components/node-compatibility-matrix.tsx @@ -5,7 +5,10 @@ // @refresh reset import { useEffect, useMemo, useState } from "react"; -import compatibilityMeta from "@/generated/node-v24-compatibility-meta.json"; + +// Set by next.config.mjs from the committed artifact. +const artifactVersion = process.env.COMPATIBILITY_ARTIFACT_VERSION; +const rowCount = Number(process.env.COMPATIBILITY_ROW_COUNT); type Status = | "supported" @@ -211,14 +214,14 @@ export function NodeCompatibilityMatrix() { useEffect(() => { const controller = new AbortController(); - fetch(`/compatibility/data?v=${compatibilityMeta.artifactVersion}`, { cache: "no-store", signal: controller.signal }) + fetch(`/compatibility/data?v=${artifactVersion}`, { cache: "no-store", signal: controller.signal }) .then((response) => { if (!response.ok) throw new Error(`compatibility data: ${response.status}`); return response.json() as Promise; }) .then((data) => { - if (!Array.isArray(data.rows) || data.rows.length !== compatibilityMeta.rowCount) { - throw new Error(`compatibility data is stale or malformed (expected ${compatibilityMeta.rowCount} rows)`); + if (!Array.isArray(data.rows) || data.rows.length !== rowCount) { + throw new Error(`compatibility data is stale or malformed (expected ${rowCount} rows)`); } setSnapshot(data); }) @@ -281,7 +284,7 @@ export function NodeCompatibilityMatrix() { return
The compatibility dataset could not be loaded.
; } if (!snapshot) { - return
Loading {compatibilityMeta.rowCount.toLocaleString()} Node.js API rows…
; + return
Loading {rowCount.toLocaleString()} Node.js API rows…
; } return ( diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json deleted file mode 100644 index 1cf76d3e4..000000000 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schemaVersion": 3, - "nodeVersion": "24.22.0", - "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "efc18bcd8a7a03fd8622", - "rowCount": 3880 -} diff --git a/docs/test/routes.test.mjs b/docs/test/routes.test.mjs index bd4b27bde..1520a5aa3 100644 --- a/docs/test/routes.test.mjs +++ b/docs/test/routes.test.mjs @@ -151,8 +151,11 @@ test("compatibility page and data endpoints keep their existing contracts", asyn const response = await get("/compatibility/data"); assert.equal(response.status, 200); const dataset = await response.json(); - const metadata = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility-meta.json", import.meta.url), "utf8")); - assert.equal(dataset.rows.length, metadata.rowCount); + const artifact = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility.json", import.meta.url), "utf8")); + assert.equal(dataset.rows.length, artifact.rows.length); + // The row count next.config.mjs derives reaches the page (React separates + // adjacent text nodes with empty comments). + assert.ok(html.replaceAll("", "").includes(`Loading ${artifact.rows.length.toLocaleString()} Node.js API rows`)); assert.equal((await get("/node-compatibility")).status, 308); assert.equal((await get("/node-compatibility/data")).status, 308); }); diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 3433e14f9..8c7f136cf 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -24,7 +24,6 @@ const root = fileURLToPath(new URL("../../../", import.meta.url)); const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`; const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`; const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`; -const publicMetaOutputPath = `${root}docs/src/generated/node-v24-compatibility-meta.json`; const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`; const compilerCompatPath = `${packageRoot}static-support.json`; const pinPath = `${packageRoot}node-v24.json`; @@ -1009,17 +1008,6 @@ function publicSnapshot(snapshot) { }; } -function publicMetadata(publicRendered, snapshot) { - const matrix = projectPublicMatrix(snapshot); - return { - schemaVersion: 3, - nodeVersion: snapshot.nodeVersion, - nodeCommit: snapshot.nodeCommit, - artifactVersion: sha(publicRendered).slice(0, 20), - rowCount: matrix.rows.length, - }; -} - function checkLocal() { const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8")); if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) { @@ -1071,11 +1059,8 @@ function checkLocal() { if (readFileSync(backlogOutputPath, "utf8") !== render(buildBacklog(snapshot))) { throw new Error("the Node compatibility backlog is stale; run 'pnpm node-compat'"); } - const publicRendered = render(publicSnapshot(snapshot)); - if ( - readFileSync(publicMetaOutputPath, "utf8") !== render(publicMetadata(publicRendered, snapshot)) - ) { - throw new Error("the docs compatibility metadata is stale; run 'pnpm node-compat'"); + if (readFileSync(publicOutputPath, "utf8") !== render(publicSnapshot(snapshot))) { + throw new Error("the docs compatibility artifact is stale; run 'pnpm node-compat'"); } for (const row of snapshot.rows) { for (const key of ["static"]) { @@ -1141,7 +1126,6 @@ async function main() { const internalRendered = render(snapshot); const backlogRendered = render(buildBacklog(snapshot)); const publicRendered = render(publicSnapshot(snapshot)); - const publicMetaRendered = render(publicMetadata(publicRendered, snapshot)); if (args.has("--check")) { if (readFileSync(internalOutputPath, "utf8") !== internalRendered) @@ -1150,8 +1134,6 @@ async function main() { throw new Error("Node compatibility backlog is stale; run 'pnpm node-compat'"); if (readFileSync(publicOutputPath, "utf8") !== publicRendered) throw new Error("public Node compatibility snapshot is stale; run 'pnpm node-compat'"); - if (readFileSync(publicMetaOutputPath, "utf8") !== publicMetaRendered) - throw new Error("public Node compatibility metadata is stale; run 'pnpm node-compat'"); console.log( `Node compatibility snapshot matches ${pin.tag} (${snapshot.rows.length} API rows, commit ${pin.commit.slice(0, 12)})`, ); @@ -1160,7 +1142,6 @@ async function main() { writeFileSync(internalOutputPath, internalRendered); writeFileSync(backlogOutputPath, backlogRendered); writeFileSync(publicOutputPath, publicRendered); - writeFileSync(publicMetaOutputPath, publicMetaRendered); console.log( `wrote compatibility ledger, backlog, and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`, ); From ec2f2a341f45a9278a955999f41f8f76dee5f4ca Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:04:17 -0500 Subject: [PATCH 095/120] Record new corpus program costs in per-program files Every change that adds a corpus program also added its CI costs to tests/harness/corpus-costs.json. The table is sorted by program name and programs are numbered in sequence, so concurrent changes inserted lines at the same place and conflicted. A change now writes tests/harness/corpus-costs/.json ({ "plain": ms, "san": ms }). The shard loader applies these files over the table, and scripts/ci-corpus-costs.mjs folds them into the table and deletes them when it refreshes the costs from CI logs. With no per-program files the table and the shard assignment are unchanged. --- AGENTS.md | 2 + scripts/ci-corpus-costs.mjs | 19 +++++++++- tests/harness/corpus-costs/README.md | 9 +++++ tests/harness/differential.test.ts | 2 +- tests/harness/shard.test.ts | 33 ++++++++++++++-- tests/harness/shard.ts | 56 ++++++++++++++++++++++++---- 6 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 tests/harness/corpus-costs/README.md diff --git a/AGENTS.md b/AGENTS.md index b94c90067..45dc2bd57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways. +Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/.json`, not in `tests/harness/corpus-costs.json`; see the README there. + Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep. A change that must not cost good code anything, such as a compatibility fix that replaces a refusal, proves it with the zero-cost gate. The gate compiles the benchmark workloads, the self-hosted compiler, and the TypeScript corpus with two built checkouts. It requires byte-identical IR, LLVM, and release executables, and compile time within tolerance. `--allow=` names the change's pattern signature: programs whose sources match may differ, and the PR lists them. Any other difference fails the gate, which names the first differing function. The gate also enforces the two-tier runtime separation: no executable may gain symbols from `DYNAMIC_TIER_PREFIXES` in `scripts/zero-cost-compare.mjs`, and fully typed benchmarks may link none; extend that list as dynamic-tier units land. `--quick` checks only the benchmarks; `--cachegrind` adds per-function instruction counts for executables that differ. Run it in a Sandbox with both checkouts prepared: diff --git a/scripts/ci-corpus-costs.mjs b/scripts/ci-corpus-costs.mjs index e934647c8..9f0e8b085 100644 --- a/scripts/ci-corpus-costs.mjs +++ b/scripts/ci-corpus-costs.mjs @@ -1,16 +1,31 @@ #!/usr/bin/env node // Refreshes tests/harness/corpus-costs.json from the logs of the Linux test shards of green CI runs on main. // +// It first folds the per-program cost files (tests/harness/corpus-costs/.json, which changes that add or +// remeasure a program write instead of editing the table) into the table and deletes them. +// // Usage: node scripts/ci-corpus-costs.mjs ... // // Each log is a downloaded "test (Linux, plain|san, i/10)" job log. vitest prints every case slower than its // threshold with its duration; for each program this takes the median of its differential and of its // llvm-differential case over the logs of its flavor, and stores their sum. Programs without a measurement keep // their current value. -import { readFileSync, writeFileSync } from "node:fs"; +import { globSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; const path = new URL("../tests/harness/corpus-costs.json", import.meta.url); const table = JSON.parse(readFileSync(path, "utf8")); +const filesDir = fileURLToPath(new URL("../tests/harness/corpus-costs", import.meta.url)); +let folded = 0; +for (const rel of globSync("**/*.json", { cwd: filesDir })) { + const key = rel.replaceAll("\\", "/").slice(0, -".json".length); + const entry = JSON.parse(readFileSync(join(filesDir, rel), "utf8")); + for (const flavor of ["plain", "san"]) + if (entry[flavor] !== undefined) table[flavor][key] = entry[flavor]; + rmSync(join(filesDir, rel)); + folded++; +} const samples = { plain: {}, san: {} }; for (const log of process.argv.slice(2)) { // Logs keep the terminal color codes (ESC [ ... m); drop them before matching. @@ -40,4 +55,4 @@ for (const flavor of ["plain", "san"]) Object.entries(table[flavor]).sort(([a], [b]) => a.localeCompare(b)), ); writeFileSync(path, JSON.stringify(table, null, 2) + "\n"); -console.log(`updated ${updated} program costs`); +console.log(`updated ${updated} program costs, folded ${folded} per-program cost files`); diff --git a/tests/harness/corpus-costs/README.md b/tests/harness/corpus-costs/README.md new file mode 100644 index 000000000..e005fa793 --- /dev/null +++ b/tests/harness/corpus-costs/README.md @@ -0,0 +1,9 @@ +# Per-program corpus costs + +A change that adds a corpus program, or changes how long one takes, records the program's CI cost here instead of editing `../corpus-costs.json`: one file per program, named after it, with the milliseconds of its differential plus llvm-differential case per flavor. + +```json +{ "plain": 2700, "san": 3200 } +``` + +`5637-child-max-buffer-sync.ts` goes in `5637-child-max-buffer-sync.ts.json`, and a directory program such as `2946-child-fork-ipc/main.ts` in `2946-child-fork-ipc/main.ts.json`. Either flavor may be left out. These files override the table for shard balancing (`tests/harness/shard.ts`), and `scripts/ci-corpus-costs.mjs` folds them into the table and deletes them when it refreshes the costs from CI logs. Separate files keep changes that add programs from editing the same lines of the table. diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index a2206fd97..d8139299d 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -36,7 +36,7 @@ const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); // SCRIPTC_TEST_SHARD="i/n" (CI's matrix) keeps only this shard's slice of // the corpus — the file is this suite's wall-time monster, so vitest's // file-granular --shard alone cannot split it. Slices are balanced by each -// program's measured cost (corpus-costs.json). Unset = everything. +// program's measured cost (corpus-costs.json and corpus-costs/). Unset = everything. const ENTRY_EXTS = ["ts", "js", "mjs", "cjs"]; const files = corpusSelect( ENTRY_EXTS.flatMap((ext) => [ diff --git a/tests/harness/shard.test.ts b/tests/harness/shard.test.ts index e645f2cac..61eb84083 100644 --- a/tests/harness/shard.test.ts +++ b/tests/harness/shard.test.ts @@ -3,19 +3,20 @@ * depends only on the item's key (list growth never moves an existing item), * and no spec means everything. differential/llvm-differential/npm/server * lean on these properties for CI sharding — see shard.ts. */ -import { globSync } from "node:fs"; +import { globSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { balancedShardSelect, + corpusCostFiles, corpusSelect, + loadCorpusCosts, parseShardSpec, shardOf, shardSelect, shardSuffix, skipUnownedTest, } from "./shard.js"; -import corpusCosts from "./corpus-costs.json"; import fixtureCosts from "./effect4-costs.json"; import effectCosts from "./effect-costs.json"; @@ -268,7 +269,7 @@ describe("corpusSelect", () => { ); test("measured costs cover the corpus and balance the shards", () => { - const costs = corpusCosts.plain as Record; + const costs = loadCorpusCosts().plain; const measured = programs.filter((k) => costs[k] !== undefined).length; expect(measured / programs.length).toBeGreaterThan(0.9); const loads = Array.from({ length: 10 }, (_, i) => @@ -279,4 +280,30 @@ describe("corpusSelect", () => { ); expect(Math.max(...loads) / Math.min(...loads)).toBeLessThan(1.1); }); + + test("every per-program cost file names a corpus program", () => { + const known = new Set(programs); + for (const key of corpusCostFiles().keys()) + expect(known, `corpus-costs/${key}.json`).toContain(key); + }); + + test("per-program cost files override the table, and invalid ones throw", () => { + const dir = join( + import.meta.dirname, + "../../node_modules/.cache/scriptc-tests/corpus-costs-files", + ); + rmSync(dir, { recursive: true, force: true }); + mkdirSync(join(dir, "200-dir"), { recursive: true }); + writeFileSync(join(dir, "100-flat.ts.json"), '{ "plain": 2500, "san": 4000 }\n'); + writeFileSync(join(dir, "200-dir/main.ts.json"), '{ "san": 900 }\n'); + expect([...corpusCostFiles(dir)]).toEqual([ + ["100-flat.ts", { plain: 2500, san: 4000 }], + ["200-dir/main.ts", { san: 900 }], + ]); + for (const bad of ['{ "plian": 1 }', '{ "plain": 0 }', '{ "san": "9" }']) { + writeFileSync(join(dir, "100-flat.ts.json"), bad); + expect(() => corpusCostFiles(dir)).toThrow(/plain|san/); + } + rmSync(dir, { recursive: true, force: true }); + }); }); diff --git a/tests/harness/shard.ts b/tests/harness/shard.ts index 4c3b9c244..a2797acf9 100644 --- a/tests/harness/shard.ts +++ b/tests/harness/shard.ts @@ -16,7 +16,9 @@ * everything: local runs are unchanged. */ import { createHash } from "node:crypto"; -import { readFileSync } from "node:fs"; +import { globSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import type { Suite, Test, TestContext } from "vitest"; export interface ShardSpec { @@ -94,11 +96,53 @@ export function balancedShardSelect( return items.filter((_, index) => owners[index] === spec.index); } -/** Measured milliseconds per corpus program and flavor (corpus-costs.json): its differential plus - * llvm-differential case on a CI runner. */ -type CorpusCosts = { plain: Record; san: Record }; +/** Measured milliseconds per corpus program and flavor: its differential plus llvm-differential case on a CI + * runner. corpus-costs.json is the table scripts/ci-corpus-costs.mjs refreshes from CI logs. A change that adds + * or remeasures a program records its costs in corpus-costs/.json instead (`{ "plain": ms, "san": ms }`, + * a directory program as corpus-costs//main.ts.json), which overrides the table until the next refresh + * folds it in. One file per program keeps changes that add programs from editing the same lines. */ +export type CorpusCosts = { plain: Record; san: Record }; let corpusCostTable: CorpusCosts | undefined; +/** The per-program cost files: program key → its file's costs. Throws on a field other than plain/san or a + * cost that is not a positive finite number. */ +export function corpusCostFiles( + dir: string = fileURLToPath(new URL("./corpus-costs", import.meta.url)), +): Map>> { + const files = new Map>>(); + for (const rel of globSync("**/*.json", { cwd: dir }) + .map((path) => path.replaceAll("\\", "/")) + .sort()) { + const key = rel.slice(0, -".json".length); + const entry = JSON.parse(readFileSync(join(dir, rel), "utf8")) as Record; + for (const [flavor, cost] of Object.entries(entry)) { + if ( + (flavor !== "plain" && flavor !== "san") || + typeof cost !== "number" || + !Number.isFinite(cost) || + cost <= 0 + ) + throw new Error( + `corpus-costs/${rel}: ${flavor} must be "plain" or "san" with a positive cost`, + ); + } + files.set(key, entry as Partial>); + } + return files; +} + +/** corpus-costs.json with the per-program files applied. */ +export function loadCorpusCosts(): CorpusCosts { + const table = JSON.parse( + readFileSync(new URL("./corpus-costs.json", import.meta.url), "utf8"), + ) as CorpusCosts; + for (const [key, entry] of corpusCostFiles()) { + if (entry.plain !== undefined) table.plain[key] = entry.plain; + if (entry.san !== undefined) table.san[key] = entry.san; + } + return table; +} + /** The corpus programs this shard owns, balanced by measured cost. differential.test.ts and * llvm-differential.test.ts both select with this, so a shard owns the same programs in both lanes and their * compile cache serves both. A program without a measurement weighs as much as the median measured one, so @@ -109,9 +153,7 @@ export function corpusSelect( sanitize: boolean, spec: ShardSpec | undefined = parseShardSpec(), ): T[] { - corpusCostTable ??= JSON.parse( - readFileSync(new URL("./corpus-costs.json", import.meta.url), "utf8"), - ) as CorpusCosts; + corpusCostTable ??= loadCorpusCosts(); const costs = sanitize ? corpusCostTable.san : corpusCostTable.plain; const known = Object.values(costs).sort((a, b) => a - b); const median = known.length === 0 ? 1 : known[Math.floor(known.length / 2)]!; From b9cc0ed82027d27f1b00b8af26635e54b26960f0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:06:48 -0500 Subject: [PATCH 096/120] Sort the handle tags by name with explicit values Every change that added a checked-dynamic handle kind appended its tag just before SCR_DYNH_COUNT in scr_dyn.h, so any two such changes in flight conflicted there. The tags are now sorted by name, each with its current value written out, so a new tag goes in its sorted place with the next free value, and changes adding different tags edit different lines. SCR_DYNH_COUNT, used only by the capacity check, is gone. The values are unchanged and the runtime artifacts are identical. A new test checks what the enum's order used to guarantee: values are unique and below SCR_DYNH_CAPACITY, and the LLVM backend's hardcoded tag numbers (DYN_HANDLE_TAG_NUM) match the header. --- .../src/backend/llvm/dyn-handle-tags.test.ts | 39 +++++ packages/compiler/src/backend/llvm/dyn.ts | 4 +- packages/runtime/src/scr_dyn.h | 135 +++++++++--------- packages/runtime/src/scr_message.test.c | 2 +- 4 files changed, 112 insertions(+), 68 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts diff --git a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts new file mode 100644 index 000000000..8aa69b386 --- /dev/null +++ b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from "node:fs"; +import { expect, test } from "vitest"; +import { DYN_HANDLE_KINDS } from "../../ir/ir.js"; +import { DYN_HANDLE_TAG_NUM } from "./dyn.js"; + +// ScrDynHandleTag in scr_dyn.h lists its tags by name, each with an explicit +// value, so changes that add tags edit different lines. These checks stand +// in for what the enum's order used to guarantee. +const header = readFileSync(new URL("../../../../runtime/src/scr_dyn.h", import.meta.url), "utf8"); +const body = /typedef enum \{\n([\s\S]*?)\} ScrDynHandleTag;/.exec(header)![1]!; +const tags = [...body.matchAll(/^\s*(SCR_DYNH_[A-Z0-9_]+) = (\d+),/gm)].map(([, name, value]) => ({ + name: name!, + value: Number(value), +})); +const capacity = Number(/#define SCR_DYNH_CAPACITY (\d+)/.exec(header)![1]); + +test("every ScrDynHandleTag has an explicit value, in name order", () => { + const lines = body.split("\n").filter((line) => line.trim().startsWith("SCR_DYNH_")); + expect(tags.length).toBe(lines.length); + expect(tags.map((tag) => tag.name)).toEqual(tags.map((tag) => tag.name).toSorted()); +}); + +test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => { + const owners = new Map(); + for (const { name, value } of tags) { + expect(owners.get(value), `${name} = ${value}`).toBeUndefined(); + owners.set(value, name); + expect(value, name).toBeLessThan(capacity); + } +}); + +test("the backend's handle tag numbers match scr_dyn.h", () => { + const values = new Map(tags.map((tag) => [tag.name, tag.value])); + for (const [kind, num] of Object.entries(DYN_HANDLE_TAG_NUM)) { + const tag = DYN_HANDLE_KINDS.get(kind)?.tag; + expect(tag, kind).toBeDefined(); + expect(values.get(tag!), `${kind} (${tag})`).toBe(num); + } +}); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 9b11d6240..154b80c02 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -72,8 +72,8 @@ export function recordDynKeepsIdentity(shape: IrRecordShape): boolean { } /** ScrDynKind values (scr_runtime.h). */ -/** ScrDynHandleTag numeric values (scr_runtime.h's enum order). */ -const DYN_HANDLE_TAG_NUM: Record = { +/** ScrDynHandleTag values (scr_dyn.h's explicit values; dyn-handle-tags.test.ts checks them). */ +export const DYN_HANDLE_TAG_NUM: Record = { httpReq: 0, httpRes: 1, netSocket: 2, diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bccd8d55c..b28dfb906 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -117,76 +117,81 @@ typedef enum { /* The handle-type tags the checked-dynamic tree can carry. The set is deliberately the * HANDLE kinds whose member surfaces already have complete static * lowerings (the http/net receiver surface); new kinds join by adding a - * tag + an ops registration in their owning unit. */ + * tag + an ops registration in their owning unit. + * + * Tags are sorted by name and every value is explicit and permanent: the + * LLVM backend hardcodes some of them (DYN_HANDLE_TAG_NUM in + * packages/compiler/src/backend/llvm/dyn.ts). A new tag goes in its sorted + * place with the next value after the largest one in use, below + * SCR_DYNH_CAPACITY. packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts + * checks that values are unique and in range and that the backend's + * numbers match. */ typedef enum { - SCR_DYNH_HTTP_REQ, /* ScrHttpReq — IncomingMessage (server req & client res) */ - SCR_DYNH_HTTP_RES, /* ScrHttpRes — ServerResponse */ - SCR_DYNH_NET_SOCKET, /* ScrNetSocket — net.Socket */ - SCR_DYNH_NET_SERVER, /* ScrNetServer — net.Server (http.Server rides the same handle) */ - SCR_DYNH_H2_SESSION, /* ScrH2Session — Http2Session (client & server) */ - SCR_DYNH_H2_STREAM, /* ScrH2Stream — Http2Stream (client & server) */ - SCR_DYNH_HTTP_CLIENT, /* ScrHttpClientReq — http.ClientRequest */ - SCR_DYNH_HTTP_AGENT, /* ScrHttpAgent — http.Agent / https.Agent */ - SCR_DYNH_ABORT_SIGNAL, /* native static-fetch AbortSignal */ - SCR_DYNH_WEB_STREAM, /* native WHATWG ReadableStream */ - SCR_DYNH_WEB_READER, /* native ReadableStreamDefaultReader */ - SCR_DYNH_WEB_CONTROLLER, /* native ReadableStreamDefaultController */ - SCR_DYNH_FETCH_RESPONSE, /* native static-fetch Response */ - SCR_DYNH_FETCH_HEADERS, /* native static-fetch response Headers */ - SCR_DYNH_EVENT, /* ScrEvent — new Event() and AbortSignal's abort events */ - SCR_DYNH_ABORT_CONTROLLER, /* native static-fetch AbortController */ - SCR_DYNH_CHILD, /* child_process.ChildProcess */ - SCR_DYNH_FILE_HANDLE, /* fs/promises.FileHandle */ - SCR_DYNH_ARRAY_BUFFER, /* fixed-length ArrayBuffer storage */ - SCR_DYNH_SEGMENTER, /* default Unicode grapheme segmentation */ - SCR_DYNH_SEGMENTS, /* iterable immutable segmentation result */ - SCR_DYNH_BUILTIN_MODULE, /* native builtin export object */ - SCR_DYNH_GLOBAL, /* native global object identity */ - SCR_DYNH_WEAK_MAP, /* native weak-key metadata */ - SCR_DYNH_WEAK_SET, /* native weak membership */ - SCR_DYNH_SET, /* native Set, shared backing map */ - SCR_DYNH_REGEXP, /* native RegExp, shared compiled pattern */ - SCR_DYNH_STDIO, /* stable process stdin/stdout/stderr values */ - SCR_DYNH_URL, /* native WHATWG URL */ - SCR_DYNH_MAP, /* native Map, shared backing */ - SCR_DYNH_DATE, /* JavaScript Date with stable native identity */ - SCR_DYNH_ITERATOR, /* live native array/string/byte iterator */ - SCR_DYNH_CONSOLE, /* stored global console value */ - SCR_DYNH_SEARCH_PARAMS, /* live WHATWG URLSearchParams */ - SCR_DYNH_FETCH_REQUEST, - SCR_DYNH_WORKER, - SCR_DYNH_MESSAGE_PORT, - SCR_DYNH_SHARED_ARRAY_BUFFER, - SCR_DYNH_CONSOLE_INSTANCE, /* node:console Console instance */ - SCR_DYNH_EVENT_TARGET, /* ScrEventTarget — new EventTarget() */ - SCR_DYNH_CUSTOM_EVENT, /* ScrEvent with detail — new CustomEvent() */ - SCR_DYNH_BLOB, /* ScrBlob — new Blob() and Blob#slice() */ - SCR_DYNH_FILE, /* ScrBlob with a name — new File() */ - SCR_DYNH_KEY_OBJECT, /* node:crypto secret KeyObject (scr_lib.c) */ - SCR_DYNH_DIR, /* fs.Dir (scr_lib.c) */ - SCR_DYNH_CIPHER, /* node:crypto Cipheriv (scr_cipher.c) */ - SCR_DYNH_DECIPHER, /* node:crypto Decipheriv (scr_cipher.c) */ - SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */ - SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */ - SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */ - SCR_DYNH_MESSAGE_EVENT, /* ScrEvent with data — BroadcastChannel's 'message' */ - SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */ - SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */ - SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */ - SCR_DYNH_ITERATOR_HELPER, /* an iterator helper object over a checked iterator (scr_json.c) */ - SCR_DYNH_COUNT, + SCR_DYNH_ABORT_CONTROLLER = 15, /* native static-fetch AbortController */ + SCR_DYNH_ABORT_SIGNAL = 8, /* native static-fetch AbortSignal */ + SCR_DYNH_ARRAY_BUFFER = 18, /* fixed-length ArrayBuffer storage */ + SCR_DYNH_BLOB = 41, /* ScrBlob — new Blob() and Blob#slice() */ + SCR_DYNH_BROADCAST_CHANNEL = 51, /* worker_threads BroadcastChannel (scr_worker_events.c) */ + SCR_DYNH_BUILTIN_MODULE = 21, /* native builtin export object */ + SCR_DYNH_CHILD = 16, /* child_process.ChildProcess */ + SCR_DYNH_CIPHER = 45, /* node:crypto Cipheriv (scr_cipher.c) */ + SCR_DYNH_CONSOLE = 32, /* stored global console value */ + SCR_DYNH_CONSOLE_INSTANCE = 38, /* node:console Console instance */ + SCR_DYNH_CUSTOM_EVENT = 40, /* ScrEvent with detail — new CustomEvent() */ + SCR_DYNH_DATE = 30, /* JavaScript Date with stable native identity */ + SCR_DYNH_DECIPHER = 46, /* node:crypto Decipheriv (scr_cipher.c) */ + SCR_DYNH_DIR = 44, /* fs.Dir (scr_lib.c) */ + SCR_DYNH_DISPOSABLE_STACK = 49, /* DisposableStack (scr_symbol.c) */ + SCR_DYNH_EVENT = 14, /* ScrEvent — new Event() and AbortSignal's abort events */ + SCR_DYNH_EVENT_TARGET = 39, /* ScrEventTarget — new EventTarget() */ + SCR_DYNH_FETCH_HEADERS = 13, /* native static-fetch response Headers */ + SCR_DYNH_FETCH_REQUEST = 34, + SCR_DYNH_FETCH_RESPONSE = 12, /* native static-fetch Response */ + SCR_DYNH_FILE = 42, /* ScrBlob with a name — new File() */ + SCR_DYNH_FILE_HANDLE = 17, /* fs/promises.FileHandle */ + SCR_DYNH_GLOBAL = 22, /* native global object identity */ + SCR_DYNH_H2_SESSION = 4, /* ScrH2Session — Http2Session (client & server) */ + SCR_DYNH_H2_STREAM = 5, /* ScrH2Stream — Http2Stream (client & server) */ + SCR_DYNH_HASH = 52, /* node:crypto Hash (scr_lib.c) */ + SCR_DYNH_HMAC = 53, /* node:crypto Hmac (scr_lib.c) */ + SCR_DYNH_HTTP_AGENT = 7, /* ScrHttpAgent — http.Agent / https.Agent */ + SCR_DYNH_HTTP_CLIENT = 6, /* ScrHttpClientReq — http.ClientRequest */ + SCR_DYNH_HTTP_REQ = 0, /* ScrHttpReq — IncomingMessage (server req & client res) */ + SCR_DYNH_HTTP_RES = 1, /* ScrHttpRes — ServerResponse */ + SCR_DYNH_ITERATOR = 31, /* live native array/string/byte iterator */ + SCR_DYNH_ITERATOR_HELPER = 54, /* an iterator helper object over a checked iterator (scr_json.c) */ + SCR_DYNH_KEY_OBJECT = 43, /* node:crypto secret KeyObject (scr_lib.c) */ + SCR_DYNH_MAP = 29, /* native Map, shared backing */ + SCR_DYNH_MESSAGE_EVENT = 50, /* ScrEvent with data — BroadcastChannel's 'message' */ + SCR_DYNH_MESSAGE_PORT = 36, + SCR_DYNH_NET_BLOCK_LIST = 48, /* net.BlockList (scr_net.c) */ + SCR_DYNH_NET_SERVER = 3, /* ScrNetServer — net.Server (http.Server rides the same handle) */ + SCR_DYNH_NET_SOCKET = 2, /* ScrNetSocket — net.Socket */ + SCR_DYNH_NET_SOCKET_ADDRESS = 47, /* net.SocketAddress (scr_net.c) */ + SCR_DYNH_REGEXP = 26, /* native RegExp, shared compiled pattern */ + SCR_DYNH_SEARCH_PARAMS = 33, /* live WHATWG URLSearchParams */ + SCR_DYNH_SEGMENTER = 19, /* default Unicode grapheme segmentation */ + SCR_DYNH_SEGMENTS = 20, /* iterable immutable segmentation result */ + SCR_DYNH_SET = 25, /* native Set, shared backing map */ + SCR_DYNH_SHARED_ARRAY_BUFFER = 37, + SCR_DYNH_STDIO = 27, /* stable process stdin/stdout/stderr values */ + SCR_DYNH_URL = 28, /* native WHATWG URL */ + SCR_DYNH_WEAK_MAP = 23, /* native weak-key metadata */ + SCR_DYNH_WEAK_SET = 24, /* native weak membership */ + SCR_DYNH_WEB_CONTROLLER = 11, /* native ReadableStreamDefaultController */ + SCR_DYNH_WEB_READER = 10, /* native ReadableStreamDefaultReader */ + SCR_DYNH_WEB_STREAM = 9, /* native WHATWG ReadableStream */ + SCR_DYNH_WORKER = 35, } ScrDynHandleTag; /* Every per-tag table (scr_dynh_ops in scr_json.c, the message clone hooks - * in scr_message.c) is sized by this fixed capacity, never by - * SCR_DYNH_COUNT. Appending a tag for a link-gated unit therefore changes - * no always-linked storage or code: a program that never boxes the new - * handle links byte-identical objects. Raising the capacity is a - * program-wide layout change; do it in a change of its own, measured as - * neutral, and only when the enum outgrows it. */ + * in scr_message.c) is sized by this fixed capacity, never by the number of + * tags. Adding a tag for a link-gated unit therefore changes no + * always-linked storage or code: a program that never boxes the new handle + * links byte-identical objects. Raising the capacity is a program-wide + * layout change; do it in a change of its own, measured as neutral, and + * only when the tags outgrow it. */ #define SCR_DYNH_CAPACITY 128 -_Static_assert(SCR_DYNH_COUNT <= SCR_DYNH_CAPACITY, - "ScrDynHandleTag outgrew SCR_DYNH_CAPACITY: raise it in its own change"); typedef struct ScrDyn ScrDyn; typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */ diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index 0c6bd7ae3..a6c940783 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -175,7 +175,7 @@ static void check_documents(void) { /* A link-gated unit's handle crosses threads through the transfer hooks in * its ops: share on the sender, adopt on the receiver, drop with the * message. The test borrows the last capacity slot, which no unit uses, so - * it also proves that slots past SCR_DYNH_COUNT work. */ + * it also proves that slots past the tags in use work. */ #define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1)) typedef struct { _Atomic size_t rc; From dc6a638e63cf1663776e9d72e679d0965fa25015 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:10:17 -0500 Subject: [PATCH 097/120] Lower string and typed array iterators, Iterator.from and helper return() String iterators (s[Symbol.iterator]()) and typed array values(), keys() and entries() compile as iterator objects: generators over the code points and over the indices. The iterator helpers apply to them, and for...of, spreads and Array.from consume them. Iterator.from over an array, string, typed array, Set, Map or iterator object answers the value's own iterator, as Node does for iterators that already inherit from Iterator.prototype. Objects with their own next method keep the Iterator fence. As the root of a helper chain, Iterator.from(x) walks x itself: an array by index, a Set or Map live, a generator directly. h.return!() and h.return?.() compile when h is a helper call or a const initialized with one: helper objects always have return(), so both spellings are the call. Other iterator objects keep the fence, since the array, string, Map and Set iterators have no return(). flatMap callbacks may return Sets (their values) and Maps (their entries), walked live; a string result throws Node's TypeError and closes the source. --- .../builtin-apis/iterator-helpers.mdx | 2 +- .../src/frontend/lowering/containers/bytes.ts | 16 ++ .../lowering/containers/iterator-helpers.ts | 222 +++++++++++++++++- .../frontend/lowering/iterator-adapters.ts | 216 +++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 81 +++++-- .../src/frontend/lowering/lower-exprs.ts | 5 + packages/compiler/src/frontend/type-mapper.ts | 5 +- .../corpus/6071-iterator-sources.ts.json | 6 + .../corpus/6072-iterator-sources-js.js.json | 6 + .../string-iterator-helpers.ts.json | 6 - tests/corpus/6071-iterator-sources.ts | 70 ++++++ tests/corpus/6072-iterator-sources-js.js | 70 ++++++ .../collection-iterator-helpers.ts | 18 +- tests/diagnostics/es2025-fences.ts | 6 +- tests/diagnostics/string-iterator-helpers.ts | 3 - .../collection-iterator-helpers.ts.txt | 22 +- .../__snapshots__/es2025-fences.ts.txt | 8 +- .../string-iterator-helpers.ts.txt | 8 - tests/harness/corpus-costs.json | 4 + 19 files changed, 700 insertions(+), 74 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json create mode 100644 tests/corpus/6071-iterator-sources.ts create mode 100644 tests/corpus/6072-iterator-sources-js.js delete mode 100644 tests/diagnostics/string-iterator-helpers.ts delete mode 100644 tests/harness/__snapshots__/string-iterator-helpers.ts.txt diff --git a/docs/content/limitations/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx index 01f779cb6..cb7cab1f4 100644 --- a/docs/content/limitations/builtin-apis/iterator-helpers.mdx +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -1 +1 @@ -- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported. +- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects are unsupported. diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index c41bf5163..91cf0b263 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -35,6 +35,7 @@ import { } from "../optional-arguments.js"; import { lowerCheckedArrayFrom } from "./array-construction.js"; import { lowerToNumberArgument } from "../lower-exprs.js"; +import { lowerBytesIteratorValue } from "../iterator-adapters.js"; /** TypedArray.prototype.sort and toSorted. The receiver and comparator * expressions are evaluated before entering the helper (buildBytesSortFn). @@ -462,6 +463,21 @@ export function lowerBytesMethodCall( if (name === "sort" || name === "toSorted") { return lowerBytesSortCall(lowerer, call, access, receiverIr); } + // values()/keys()/entries() as iterator objects (for...of heads and + // stored numeric iterators lower before this). + if ((name === "values" || name === "keys" || name === "entries") && nArgs === 0) { + const type = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (type?.kind === "generator") { + const value = lowerBytesIteratorValue( + lowerer, + lowerer.lowerExpr(access.expression), + name, + type, + loc, + ); + if (value) return value; + } + } if (name === "toReversed") { if (nArgs !== 0) { lowerer.noLowering(`.toReversed with ${nArgs} arguments on typed arrays`, call); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 7cb3753ac..70e0d85e0 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -5,6 +5,7 @@ import type { Lowerer } from "../lowerer.js"; import { BOOL, CAUGHT, + DYN, F64, type IrMapIntrinsicMethod, type IrSetIntrinsicMethod, @@ -28,12 +29,18 @@ import { concatStrings, throwTypeError } from "./dynamic-receivers.js"; import { type GenType, generatorLoopBinding, + resultRecordOf, genStep, genValue, stepDrivable, } from "../lower-generators.js"; import { exitBeforeReturns } from "./for-of.js"; -import { iteratorSlotAdapter } from "../iterator-adapters.js"; +import { + iteratorSlotAdapter, + lowerArrayIteratorValue, + lowerBytesIteratorValue, + lowerStringIteratorValue, +} from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; /* ── iterator helpers (ES2025) ─────────────────────────────────────────── */ @@ -189,6 +196,24 @@ export function lowerGeneratorHelperCall( collectionSeed(lowerer, source), ); } + // Iterator.from(arr) walks the array by index. + const arrayRoot = arrayIteratorCall(lowerer, cur); + if (arrayRoot !== null) { + const items = lowerer.lowerExpr(arrayRoot.receiver); + if (items.type.kind !== "array") + lowerer.badType(arrayRoot.receiver, lowerer.typeOf(arrayRoot.receiver)); + const elem = + arrayRoot.proj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem; + return lowerFusedChain( + lowerer, + call, + name, + stages, + { kind: "array", type: items.type, proj: arrayRoot.proj }, + items, + elem, + ); + } const src = lowerHelperSource(lowerer, cur); return lowerFusedChain( lowerer, @@ -201,6 +226,128 @@ export function lowerGeneratorHelperCall( ); } +/** `h.return!()` or `h.return?.()` on a helper object. The lib declares + * return() as optional on iterator objects, and a helper object always + * has one, so both spellings are the call: the helper completes and closes + * its source, and the result is `{ value: undefined, done: true }`. The + * receiver must be provably a helper object — a helper stage call, or a + * const binding initialized with one — since other iterator objects (the + * array, string, Map and Set iterators) have no return(). Null otherwise, + * and for the plain `h.return()` spelling (lowerGenMethodCall's). */ +export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + let callee: ts.Expression = call.expression; + const nonNull = ts.isNonNullExpression(callee); + if (nonNull) callee = (callee as ts.NonNullExpression).expression; + if (!nonNull && call.questionDotToken === undefined) return null; + if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null; + if (callee.name.text !== "return" || call.arguments.length !== 0) return null; + if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null; + const callT = lowerer.mapTypeOf(lowerer.typeOf(call)); + const gen = lowerer.lowerExpr(callee.expression); + if (gen.type.kind !== "generator" || gen.type.async) return null; + const loc = locOf(call); + const recT = resultRecordOf(lowerer, gen.type); + const resume: IrExpr = { kind: "genResume", mode: "return", gen, arg: null, type: recT, loc }; + // `?.` types the call `IteratorResult | undefined`, which has no mapping + // as a whole; the result is never undefined, so it is the record itself. + if (callT === null || typeEquals(callT, recT)) return resume; + const result = lowerer.coerceToExpected(resume, callT); + if (!typeEquals(result.type, callT)) lowerer.badType(call, lowerer.typeOf(call)); + return result; +} + +/** Whether `expr` is a helper stage call, or a const binding initialized + * with one. */ +function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (isStageCall(lowerer, expr)) return true; + if (!ts.isIdentifier(expr)) return false; + const symbol = lowerer.resolveValueSymbol(expr); + const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + if ( + !decl || + !ts.isVariableDeclaration(decl) || + decl.initializer === undefined || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & ts.NodeFlags.Const) === 0 + ) + return false; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + return isStageCall(lowerer, init); +} + +/** `Iterator.from(value)` over a typed built-in iterable: the value's own + * iterator, which already inherits from Iterator.prototype, so Node returns + * it rather than a wrapper. A generator (including a helper object or an + * array iterator) is itself; an array, string or typed array gives its + * values iterator; a Set its values and a Map its entries, adapted into a + * generator over the live collection. The result converts into the call's + * IteratorObject type. Null for other arguments (objects with their own + * next method, untyped values), which keep the Iterator fence. */ +export function lowerIteratorFromCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "from" || call.arguments.length !== 1) return null; + const argNode = call.arguments[0]!; + if (ts.isSpreadElement(argNode)) return null; + const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); + if (resultT?.kind !== "generator" || resultT.async || argT === null) return null; + const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"]; + if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null; + const loc = locOf(call); + const value = lowerer.lowerExpr(argNode); + const own = (yieldT: IrType): GenType => ({ + kind: "generator", + yieldT, + retT: VOID, + nextT: UNDEFINED_T, + }); + let iterator: IrExpr | null = null; + const t = value.type; + if (t.kind === "generator") iterator = value; + else if (t.kind === "array") + iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc); + else if (t.kind === "string") + iterator = lowerStringIteratorValue(lowerer, value, own(STRING), loc); + else if (t.kind === "bytes") + iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc); + else if (t.kind === "set" || t.kind === "map") { + const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value); + const native: IrExpr = { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(value, DYN), + method: t.kind === "set" ? "values" : "entries", + calleeName: t.kind === "set" ? "set.values" : "map.entries", + args: [], + type: DYN, + loc, + }; + iterator = iteratorSlotAdapter(lowerer, native, own(elem)); + } + if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode)); + const result = lowerer.coerceToExpected(iterator, resultT); + if (!typeEquals(result.type, resultT)) lowerer.badType(call, lowerer.typeOf(call)); + return result; +} + +/** The interned [K, V] pair a Map's entries yield. */ +function entryPairTypeOf(lowerer: Lowerer, keyT: IrType, valueT: IrType): IrType { + return { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: keyT }, + { name: "1", type: valueT }, + ], + true, + ), + }; +} + const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]); const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]); /** Native iterators that ride checked runtime values and adapt into @@ -353,12 +500,30 @@ export function lowerForOfHelperChain( } } -/** `arr.values()` or `arr.entries()` on an array value, as the root of a - * helper chain. */ +/** The argument of an `Iterator.from(x)` call, or null. As a chain's root, + * the call is the argument's own iterator, so the chain can walk the + * argument directly. */ +function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; + if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1) + return null; + const arg = expr.arguments[0]!; + if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null; + if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null; + return lowerer.isStdlibMember(expr.expression) ? arg : null; +} + +/** `arr.values()`, `arr.entries()` or `Iterator.from(arr)` on an array value, + * as the root of a helper chain. */ function arrayIteratorCall( lowerer: Lowerer, expr: ts.Expression, ): { receiver: ts.Expression; proj: "values" | "entries" } | null { + const fromArg = iteratorFromArgument(lowerer, expr); + if (fromArg !== null) + return lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "array" + ? { receiver: fromArg, proj: "values" } + : null; if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) return null; @@ -401,6 +566,17 @@ function collectionIteratorCall( lowerer: Lowerer, expr: ts.Expression, ): { receiver: ts.Expression; proj: "keys" | "values" | "entries" } | null { + // Iterator.from(set) is the Set's values iterator, Iterator.from(map) the + // Map's entries iterator. + const fromArg = iteratorFromArgument(lowerer, expr); + if (fromArg !== null) { + const kind = lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind; + return kind === "set" + ? { receiver: fromArg, proj: "values" } + : kind === "map" + ? { receiver: fromArg, proj: "entries" } + : null; + } if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) return null; @@ -480,6 +656,11 @@ function lowerHelperSource( lowerer: Lowerer, expr: ts.Expression, ): { gen: IrExpr & { type: GenType }; elem: IrType } { + // Iterator.from(gen) is the generator itself; the call's IteratorObject + // type would add an adapter generator over it. + const fromArg = iteratorFromArgument(lowerer, expr); + if (fromArg !== null && lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "generator") + return lowerHelperSource(lowerer, fromArg); const tsType = lowerer.typeOf(expr); const mapped = lowerer.mapTypeOf(tsType); const arg = lowerer.checker.getTypeArguments(tsType as ts.TypeReference)[0]; @@ -577,10 +758,17 @@ function lowerStage( if (ret.kind === "array") return { stage: { kind: "flatMap", fn }, out: ret.elem }; if (ret.kind === "generator" && ret.async !== true && stepDrivable(ret)) return { stage: { kind: "flatMap", fn }, out: ret.yieldT }; + // A Set flattens its values and a Map its entries, walked live. A string + // is rejected at run time with Node's TypeError (strings are not + // iterator objects). + if (ret.kind === "set") return { stage: { kind: "flatMap", fn }, out: ret.elem }; + if (ret.kind === "map") + return { stage: { kind: "flatMap", fn }, out: entryPairTypeOf(lowerer, ret.key, ret.value) }; + if (ret.kind === "string") return { stage: { kind: "flatMap", fn }, out: STRING }; lowerer.noLowering( `.flatMap over a callback returning '${lowerer.fmt(ret)}'`, s.argNode, - "callbacks returning an array or a generator are the supported forms", + "callbacks returning an array, a generator, a Set or a Map are the supported forms", ); } @@ -1245,8 +1433,9 @@ function walkCollection( } /** A flatMap stage's inner walk over the callback's result in local - * `innerId`: an array by index, or a generator driven (and closed on - * early exit) like the source. `stop` ends the walk early. */ + * `innerId`: an array by index, a Set's values or a Map's entries walked + * live, or a generator driven (and closed on early exit) like the source. + * A string result throws Node's TypeError. `stop` ends the walk early. */ function walkInner( lowerer: Lowerer, frame: ChainFrame, @@ -1257,6 +1446,27 @@ function walkInner( loc: SrcLoc, ): IrStmt[] { if (innerT.kind === "array") return [walkArray(frame, innerId, innerT, stop, body, loc)]; + if (innerT.kind === "set" || innerT.kind === "map") { + const source: ChainSource = { + kind: "collection", + type: innerT, + proj: innerT.kind === "set" ? "values" : "entries", + }; + return walkCollection(lowerer, frame, innerId, source, stop ?? boolLit(false, loc), body, loc); + } + if (innerT.kind === "string") { + return [ + throwTypeError( + { + kind: "strLit", + value: "Iterator.prototype.flatMap called on non-object", + type: STRING, + loc, + }, + loc, + ), + ]; + } if (innerT.kind !== "generator") throw new InternalCompilerError("lowerer bug: flatMap over a non-iterable result"); const pre: IrStmt[] = diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index fb59c4cee..812f29242 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -265,6 +265,222 @@ function dynIteratorAdapter( return { kind: "call", callee: name, args: [expr], type: expected, loc }; } +/** `ta.values()`, `ta.keys()` or `ta.entries()` on a typed array as a + * value: an interned generator over the indices, reading the length and + * the element at each step, as the typed array iterator does. Null when + * `type` is not the matching number (or [number, number]) generator. */ +export function lowerBytesIteratorValue( + lowerer: Lowerer, + receiver: IrExpr, + method: "values" | "keys" | "entries", + type: IrType, + loc: SrcLoc, +): IrExpr | null { + const bytesT = receiver.type; + if (bytesT.kind !== "bytes" || type.kind !== "generator") return null; + if (type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null; + const pair = type.yieldT.kind === "record" ? lowerer.shapes.get(type.yieldT.shapeId) : undefined; + const pairOk = + pair?.tuple === true && + pair.fields.length === 2 && + pair.fields.every((f) => (f.name === "0" || f.name === "1") && f.type.kind === "f64"); + if (method === "entries" ? !pairOk : type.yieldT.kind !== "f64") return null; + const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + name = `%bytes.iter.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, name); + const list = varRef("list.0", bytesT, loc); + const index = varRef("index.0", F64, loc); + const element: IrExpr = { + kind: "bytesIntrinsic", + method: "get", + receiver: list, + args: [index], + type: F64, + loc, + }; + const yielded: IrExpr = + method === "keys" + ? index + : method === "values" + ? element + : { + kind: "recordLit", + fields: [ + { name: "0", value: index }, + { name: "1", value: element }, + ], + type: type.yieldT, + loc, + }; + lowerer.liftedFns.push({ + name, + params: [{ localId: "list.0", name: "list", type: bytesT }], + returnType: VOID, + locals: [ + { id: "list.0", name: "list", type: bytesT, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "index.0", + init: { kind: "numLit", value: 0, type: F64, loc }, + loc, + }, + { + kind: "while", + cond: { + kind: "bin", + op: "<", + left: index, + right: { + kind: "bytesIntrinsic", + method: "length", + receiver: list, + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + body: [ + { kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc }, + { + kind: "assign", + localId: "index.0", + value: { + kind: "bin", + op: "+", + left: index, + right: { kind: "numLit", value: 1, type: F64, loc }, + type: F64, + loc, + }, + loc, + }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ], + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type, loc }; +} + +/** `s[Symbol.iterator]()` on a string value: an interned generator over + * its code points, the string iterator's steps (a lone surrogate is one + * step). Null when `type` is not the plain string-yielding generator. */ +export function lowerStringIteratorValue( + lowerer: Lowerer, + receiver: IrExpr, + type: IrType, + loc: SrcLoc, +): IrExpr | null { + if (receiver.type.kind !== "string" || type.kind !== "generator") return null; + if ( + type.yieldT.kind !== "string" || + type.retT.kind !== "void" || + type.nextT.kind !== "undefinedT" + ) + return null; + const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `strIter:${typeKey(type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + name = `%str.iter.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, name); + const text = varRef("text.0", STRING, loc); + const index = varRef("index.0", F64, loc); + const point = varRef("point.0", STRING, loc); + lowerer.liftedFns.push({ + name, + params: [{ localId: "text.0", name: "text", type: STRING }], + returnType: VOID, + locals: [ + { id: "text.0", name: "text", type: STRING, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + { id: "point.0", name: "point", type: STRING, mutable: false }, + ], + body: [ + { + kind: "varDecl", + localId: "index.0", + init: { kind: "numLit", value: 0, type: F64, loc }, + loc, + }, + { + kind: "while", + cond: { + kind: "bin", + op: "<", + left: index, + right: { + kind: "strIntrinsic", + method: "length", + receiver: text, + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + body: [ + { + kind: "varDecl", + localId: "point.0", + init: { + kind: "strIntrinsic", + method: "cpAt", + receiver: text, + args: [index], + type: STRING, + loc, + }, + loc, + }, + { + kind: "assign", + localId: "index.0", + value: { + kind: "bin", + op: "+", + left: index, + right: { + kind: "strIntrinsic", + method: "length", + receiver: point, + args: [], + type: F64, + loc, + }, + type: F64, + loc, + }, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value: point, type: VOID, loc }, loc }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ], + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type, loc }; +} + /** A generator flowing into a slot with other channel types (a * `Generator` returned through an * `IterableIterator` annotation). The interned adapter generator diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 23b8b69d2..58cbb9543 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -94,6 +94,8 @@ import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/ import { lowerGroupByStaticCall } from "./containers/collection-grouping.js"; import { lowerGeneratorHelperCall, + lowerHelperReturnCall, + lowerIteratorFromCall, lowerIteratorHelperCall, } from "./containers/iterator-helpers.js"; import { @@ -223,6 +225,7 @@ import { completeArgs, } from "./call-arguments.js"; import { isThisParameter, type ParamShape } from "./call-signatures.js"; +import { lowerStringIteratorValue } from "./iterator-adapters.js"; import { checkedArmExtraction } from "./coercions/unions.js"; import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js"; import { @@ -365,6 +368,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // machinery (the member always exists on a POSIX target, so the // optional call IS the call; `process.getuid` itself has no value // lowering for the chain to guard). + // `h.return!()` / `h.return?.()` on a helper object: the method exists. + const helperReturn = lowerHelperReturnCall(lowerer, expr); + if (helperReturn) return helperReturn; const processOptional = lowerer.lowerProcessOptionalMethodCall(expr); if (processOptional) return processOptional; const reqStreamOptional = lowerCompatReqStreamOptionalCall(lowerer, expr); @@ -1616,6 +1622,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`); } + // `s[Symbol.iterator]()` on a string: the string iterator as a value. + if ( + ts.isElementAccessExpression(expr.expression) && + !expr.expression.questionDotToken && + !expr.questionDotToken && + expr.arguments.length === 0 + ) { + const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression); + if (iterator) return iterator; + } + // The element spelling of an unsupported native Web method must fence // before lowering the callee into a checked-dynamic keyed read. if (ts.isElementAccessExpression(expr.expression)) { @@ -3157,10 +3174,32 @@ function lowerOptionalStringNumber( return { kind: "call", callee: helper, args: [arg], type: F64, loc }; } -/** Iterator types with checked runtime receivers: Map/Set iterators' - * helper calls lower in lowerGeneratorHelperCall; the string iterator's - * have no lowering. */ -const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]); +/** `s[Symbol.iterator]()` on a string-typed receiver: the generator over + * its code points. Null for other element calls. */ +function lowerStringIteratorCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.ElementAccessExpression, +): IrExpr | null { + let key = access.argumentExpression; + while (ts.isParenthesizedExpression(key)) key = key.expression; + if ( + !ts.isPropertyAccessExpression(key) || + lowerer.stdlibGlobalMember(key, "Symbol") !== "iterator" + ) + return null; + if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null; + const type = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (type?.kind !== "generator") return null; + const receiver = lowerer.lowerExpr(access.expression); + const value = lowerStringIteratorValue(lowerer, receiver, type, locOf(call)); + if (!value) lowerer.unsupported("SC1090", call, "this string iterator type"); + return value; +} + +/** Iterator types with checked runtime receivers, whose helper calls lower + * in lowerGeneratorHelperCall. */ +const CHECKED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator"]); const ITERATOR_HELPER_METHODS = new Set([ "map", "filter", @@ -3220,25 +3259,17 @@ function lowerDynReceiverMethodCall( const recvSymbol = recvTs.getSymbol(); if ( recvSymbol && - UNLOWERED_ITERATOR_TYPES.has(recvSymbol.name) && + CHECKED_ITERATOR_TYPES.has(recvSymbol.name) && lowerer.isStdlibSymbol(recvSymbol) && ITERATOR_HELPER_METHODS.has(access.name.text) && lowerer.isStdlibMember(access) ) { - if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") { - const helper = lowerGeneratorHelperCall(lowerer, call, access); - if (helper) return helper; - lowerer.noLowering( - `collection iterator .${access.name.text}()`, - call, - "stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering", - ); - } - // The checked runtime's string iterator has no helper methods. + const helper = lowerGeneratorHelperCall(lowerer, call, access); + if (helper) return helper; lowerer.noLowering( - `${recvSymbol.name}.${access.name.text}`, + `collection iterator .${access.name.text}()`, call, - "iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()", + "iterator helpers on Map/Set iterators are called directly on the iterator, without optional chaining", ); } const arrayReceiver = lowerer.checker.isArrayType(recvTs); @@ -6644,10 +6675,10 @@ function recordAssignHelper( return helper; } -/** The `Iterator` global's statics (ES2025 — Iterator.from, and the - * abstract constructor as a value) have no lowering: every member fences - * with the working spelling named instead of the generic-method fence's - * monomorphization wording. */ +/** The `Iterator` global's statics (ES2025): `Iterator.from` over a typed + * built-in iterable lowers (lowerIteratorFromCall); every other member, + * and other arguments, fence with the working spelling named instead of + * the generic-method fence's monomorphization wording. */ function lowerIteratorStaticFence( lowerer: Lowerer, call: ts.CallExpression, @@ -6655,11 +6686,15 @@ function lowerIteratorStaticFence( ): IrExpr | null { if (!lowerer.isStdlibGlobal(access.expression, "Iterator")) return null; if (!lowerer.isStdlibMember(access)) return null; + const from = lowerIteratorFromCall(lowerer, call, access); + if (from) return from; lowerer.noLowering( `Iterator.${access.name.text}`, call, - "iterator helpers apply directly to generators and to array, Map and Set iterators: " + - "arr.values().map(f).take(n).toArray()", + access.name.text === "from" + ? "Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators" + : "iterator helpers apply directly to generators and to array, Map and Set iterators: " + + "arr.values().map(f).take(n).toArray()", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index a613a2af0..32f80ef5b 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -147,6 +147,7 @@ import { } from "../../diagnostics/diagnostic.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; +import { lowerHelperReturnCall } from "./containers/iterator-helpers.js"; import { arrayNumberRead, arrayValueRead, @@ -1904,6 +1905,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false); if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr); if (ts.isCallExpression(expr)) { + // `h.return?.()` on a helper object: the method exists, so the + // optional call is the call. + const helperReturn = lowerHelperReturnCall(lowerer, expr); + if (helperReturn) return helperReturn; // The tail of an optional chain whose `?.` sits deeper // (`x?.trim().toLowerCase()`): the whole tail short-circuits together. if (isOptionalChainTail(lowerer, expr)) return lowerer.lowerOptionalChain(expr); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index d61464b07..41d18a6a2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3267,8 +3267,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Native collection iterators use checked runtime iterator objects. if (isStdlibInterface("MapIterator") || isStdlibInterface("SetIterator")) return DYN; // Array iterators (`list.values()` and friends) are generators over the - // live array: each step reads the current length and element. - if (isStdlibInterface("ArrayIterator")) { + // live array: each step reads the current length and element. String + // iterators (`s[Symbol.iterator]()`) are generators over the code points. + if (isStdlibInterface("ArrayIterator") || isStdlibInterface("StringIterator")) { const arg = checker.getTypeArguments(widened as ts.TypeReference)[0]; const yieldT = arg ? mapType(arg, ctx) : null; if (!yieldT || yieldT.kind === "void" || isUnitType(yieldT)) return null; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json new file mode 100644 index 000000000..2745f7978 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6071-iterator-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json new file mode 100644 index 000000000..e89c895e4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6072-iterator-sources-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json deleted file mode 100644 index 0a716bec5..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/string-iterator-helpers.ts" - ], - "diags": [] -} diff --git a/tests/corpus/6071-iterator-sources.ts b/tests/corpus/6071-iterator-sources.ts new file mode 100644 index 000000000..c63469022 --- /dev/null +++ b/tests/corpus/6071-iterator-sources.ts @@ -0,0 +1,70 @@ +// Iterator objects from strings and typed arrays, Iterator.from over the +// built-in iterables, return() on helper objects, and flatMap over Set, +// Map and string results. +const log: string[] = []; +function* count(label: string, n: number): Generator { + try { + for (let i = 1; i <= n; i++) { + log.push(`${label}${i}`); + yield i; + } + } finally { + log.push(`${label}:closed`); + } +} +function flush(what: unknown): void { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +// String iterators step by code point; a lone surrogate is one step. +const text = "a\u{1F600}b\uD800"; +const chars = text[Symbol.iterator](); +console.log(chars.next(), chars.next().value?.length, [...chars].length); +console.log(text[Symbol.iterator]().map((c) => c.codePointAt(0)!.toString(16)).toArray()); +console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join("")); +for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c); +console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h")); + +// Typed array iterators. +const bytes = new Uint8Array([9, 8, 7]); +console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray()); +console.log(bytes.entries().map(([i, v]) => i * v).toArray()); +const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1); +console.log(floats.next(), [...floats]); +for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v); + +// Iterator.from returns each built-in iterable's own iterator. +const list = [1, 2]; +const fromList = Iterator.from(list); +list.push(3); +console.log([...fromList], Iterator.from("a\u{1F600}").toArray()); +console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray()); +console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0)); +const fromGen = Iterator.from(count("a", 3)); +flush([fromGen.next(), fromGen.take(1).toArray()]); +flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray()); + +// return() on helper objects. +const started = count("c", 3); +started.next(); +const doubled = started.map((x) => x * 2); +flush([doubled.return!(), started.next()]); +const fresh = count("d", 3); +const kept = fresh.filter(() => true); +flush([kept.return?.(), fresh.next(), kept.next()]); +const stepping = count("e", 5).drop(1); +flush([stepping.next(), stepping.return!(), stepping.next()]); +flush(count("f", 4).map((x) => x).take(2).return!()); + +// flatMap over Set and Map results, and a string result's TypeError. +console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray()); +console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray()); +const pairs = new Map([["m", 1], ["n", 2]]); +for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v); +try { + count("g", 3).flatMap((x) => `s${x}`).toArray(); +} catch (e) { + log.push(`${(e as Error).name}: ${(e as Error).message}`); +} +flush("string result"); diff --git a/tests/corpus/6072-iterator-sources-js.js b/tests/corpus/6072-iterator-sources-js.js new file mode 100644 index 000000000..84dd00067 --- /dev/null +++ b/tests/corpus/6072-iterator-sources-js.js @@ -0,0 +1,70 @@ +// The JavaScript form: iterator objects from strings and typed arrays, +// Iterator.from over the built-in iterables, return() on helper objects, +// and flatMap over Set, Map and string results. +const log = []; +function* count(label , n ) { + try { + for (let i = 1; i <= n; i++) { + log.push(`${label}${i}`); + yield i; + } + } finally { + log.push(`${label}:closed`); + } +} +function flush(what ) { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +// String iterators step by code point; a lone surrogate is one step. +const text = "a\u{1F600}b\uD800"; +const chars = text[Symbol.iterator](); +console.log(chars.next(), chars.next().value.length, [...chars].length); +console.log(text[Symbol.iterator]().map((c) => c.length).toArray()); +console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join("")); +for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c); +console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h")); + +// Typed array iterators. +const bytes = new Uint8Array([9, 8, 7]); +console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray()); +console.log(bytes.entries().map(([i, v]) => i * v).toArray()); +const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1); +console.log(floats.next(), [...floats]); +for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v); + +// Iterator.from returns each built-in iterable's own iterator. +const list = [1, 2]; +const fromList = Iterator.from(list); +list.push(3); +console.log([...fromList], Iterator.from("a\u{1F600}").toArray()); +console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray()); +console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0)); +const fromGen = Iterator.from(count("a", 3)); +flush([fromGen.next(), fromGen.take(1).toArray()]); +flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray()); + +// return() on helper objects. +const started = count("c", 3); +started.next(); +const doubled = started.map((x) => x * 2); +flush([doubled.return(), started.next()]); +const fresh = count("d", 3); +const kept = fresh.filter(() => true); +flush([kept.return?.(), fresh.next(), kept.next()]); +const stepping = count("e", 5).drop(1); +flush([stepping.next(), stepping.return(), stepping.next()]); +flush(count("f", 4).map((x) => x).take(2).return()); + +// flatMap over Set and Map results, and a string result's TypeError. +console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray()); +console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray()); +const pairs = new Map ([["m", 1], ["n", 2]]); +for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v); +try { + count("g", 3).flatMap((x) => `s${x}`).toArray(); +} catch (e) { + log.push(`${e.name}: ${e.message}`); +} +flush("string result"); diff --git a/tests/diagnostics/collection-iterator-helpers.ts b/tests/diagnostics/collection-iterator-helpers.ts index 127b5c64b..d204e05c3 100644 --- a/tests/diagnostics/collection-iterator-helpers.ts +++ b/tests/diagnostics/collection-iterator-helpers.ts @@ -1,9 +1,13 @@ -// Iterator helpers lower over Map/Set iterators, generators and array -// iterators. These helper forms are refused explicitly. +// Iterator helpers lower over Map/Set iterators, generators and array, +// string and typed array iterators. These helper forms are refused +// explicitly. const set = new Set([1, 2]); -// flatMap callbacks return arrays or generators. -set.values().flatMap((value) => new Set([value, value + 1])); -// return() on a helper object. -const mapped = set.values().map((value) => value * 2); -mapped.return!(); +// return() on an iterator object that is not provably a helper object +// (array, string, Map and Set iterators have no return method). +function close(it: IteratorObject): void { + it.return!(); +} +close(set.values().map((value) => value * 2)); +// throw() on a helper object (helpers have no throw method). +set.values().map((value) => value + 1).throw!(new Error("stop")); // Each helper call must produce a diagnostic. diff --git a/tests/diagnostics/es2025-fences.ts b/tests/diagnostics/es2025-fences.ts index 067da192e..1d141eb84 100644 --- a/tests/diagnostics/es2025-fences.ts +++ b/tests/diagnostics/es2025-fences.ts @@ -1,6 +1,6 @@ // The es2025-lib surface WITHOUT a lowering fences by name: resizable // ArrayBuffers and transfer, SharedArrayBuffer.grow, Atomics.waitAsync, -// first-class iterator objects, and the unlowered forms of the +// Iterator.from over objects with their own next method, and the unlowered forms of the // implemented statics (Promise.try with fewer arguments than parameters, // groupBy's literal-union keys, a set-like non-Set argument). @@ -21,8 +21,8 @@ sab.grow(16); const ia = new Int32Array(4); const waited = Atomics.waitAsync(ia, 0, 0, 1); -// First-class iterator objects. -const it = Iterator.from([1, 2, 3].values()); +// Iterator.from over an object with its own next method. +const it = Iterator.from({ next: (): IteratorResult => ({ value: 1, done: false }) }); // Promise.try with fewer arguments than the callback's parameters. const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1); diff --git a/tests/diagnostics/string-iterator-helpers.ts b/tests/diagnostics/string-iterator-helpers.ts deleted file mode 100644 index ed0e2315f..000000000 --- a/tests/diagnostics/string-iterator-helpers.ts +++ /dev/null @@ -1,3 +0,0 @@ -// The string iterator has no iterator-helper lowering; the call is -// refused instead of failing at run time. -const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase()); diff --git a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt index 87470b621..9c56aa958 100644 --- a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt +++ b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt @@ -1,17 +1,17 @@ -collection-iterator-helpers.ts:5:22 - error SC2020: '.flatMap over a callback returning 'Set'' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:8:3 - error SC2020: 'IteratorObject.return' is part of the standard library types but has no scriptc lowering yet - 4 | // flatMap callbacks return arrays or generators. - 5 | set.values().flatMap((value) => new Set([value, value + 1])); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | // return() on a helper object. + 7 | function close(it: IteratorObject): void { + 8 | it.return!(); + | ^~~~~~~~~ + 9 | } - hint: callbacks returning an array or a generator are the supported forms + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -collection-iterator-helpers.ts:8:1 - error SC2020: 'IteratorObject.return' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:12:1 - error SC2020: 'IteratorObject.throw' is part of the standard library types but has no scriptc lowering yet - 7 | const mapped = set.values().map((value) => value * 2); - 8 | mapped.return!(); - | ^~~~~~~~~~~~~ - 9 | // Each helper call must produce a diagnostic. + 11 | // throw() on a helper object (helpers have no throw method). + 12 | set.values().map((value) => value + 1).throw!(new Error("stop")); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 13 | // Each helper call must produce a diagnostic. hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt index d7eda27b6..b1ad939fe 100644 --- a/tests/harness/__snapshots__/es2025-fences.ts.txt +++ b/tests/harness/__snapshots__/es2025-fences.ts.txt @@ -45,12 +45,12 @@ es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standa es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet - 24 | // First-class iterator objects. - 25 | const it = Iterator.from([1, 2, 3].values()); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 24 | // Iterator.from over an object with its own next method. + 25 | const it = Iterator.from({ next: (): IteratorResult => ({ value: 1, done: false }) }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | - hint: iterator helpers apply directly to generators and to array, Map and Set iterators: arr.values().map(f).take(n).toArray() + hint: Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators es2025-fences.ts:28:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt b/tests/harness/__snapshots__/string-iterator-helpers.ts.txt deleted file mode 100644 index ef768934e..000000000 --- a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt +++ /dev/null @@ -1,8 +0,0 @@ -string-iterator-helpers.ts:3:15 - error SC2020: 'StringIterator.map' is part of the standard library types but has no scriptc lowering yet - - 2 | // refused instead of failing at run time. - 3 | const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase()); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 4 | - - hint: iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values() \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 000efcdfc..0439e2afb 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2234,6 +2234,8 @@ "6053-class-to-primitive.ts": 4385, "6054-js-class-to-primitive.js": 2181, "6070-iterator-helpers-untyped.js": 4050, + "6071-iterator-sources.ts": 5150, + "6072-iterator-sources-js.js": 5350, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5304,6 +5306,8 @@ "6053-class-to-primitive.ts": 36270, "6054-js-class-to-primitive.js": 2958, "6070-iterator-helpers-untyped.js": 6900, + "6071-iterator-sources.ts": 5150, + "6072-iterator-sources-js.js": 5350, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From 15636ecf90565dc78c8a42ce199f5a4c66a0d526 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:18:10 -0500 Subject: [PATCH 098/120] List bench/thread-slots among the zero-cost gate's dynamic workloads The workload's release binary links dynamic-tier symbols through its worker messages: node:worker_threads values (Worker, parentPort, workerData) are dyn objects, and worker.on and postMessage lower to dynamic method calls. ThreadSlots and InternTable link none on their own. intern-table and typed-array-scan-workers are listed for the same reason. --- scripts/zero-cost-gate.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index f4272ac6a..297e741e5 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -316,7 +316,9 @@ const definedFunctions = (binary) => * b14377864 (DYNAMIC_TIER_PREFIXES in zero-cost-compare.mjs): JSON.parse * trees, worker messages, regex exec metadata (two-tier §3.3 L5), and a * shared scr_dyn_call/scr_dyn_define_property core not yet traced to its - * typed root. Every other bench workload is fully typed and must link none. + * typed root. bench/thread-slots, added later, links them through worker + * messages: node:worker_threads values are dyn objects. Every other bench + * workload is fully typed and must link none. * Remove a workload here when a leak fix makes its binary clean. */ const DYNAMIC_WORKLOADS = new Set([ "bench/json-records", @@ -326,6 +328,7 @@ const DYNAMIC_WORKLOADS = new Set([ "bench/regex-logs", "bench/atomics-contended", "bench/intern-table", + "bench/thread-slots", "bench/typed-array-scan-workers", "bench/log-summary", "bench/inventory-report", From 1f7f029a05f2d753fffb20a89386cd7645c92c8c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:26:09 -0500 Subject: [PATCH 099/120] Carry AsyncLocalStorage stores into timer, immediate, tick and microtask callbacks AsyncLocalStorage changes, matching Node 24.22: - setTimeout, setInterval, setImmediate, process.nextTick and queueMicrotask callbacks run with the stores that were active when they were scheduled. Before, getStore() returned undefined in them. - An interval keeps that context on every tick, and a refreshed timer keeps it too. - enterWith inside one of these callbacks lasts until the callback returns. An enterWith in top-level code ends when the main module's body returns, and one inside a callback scheduled with no store active ends when that callback returns, so the store does not reach callbacks that were scheduled without it. Programs that enter stores (store.run, exit, enterWith, or a diagnostics_channel store binding) schedule through new *_als runtime entry points, which wrap a callback scheduled with a store active in a closure that installs the stores around the call. The emitter picks them from the new asyncContext runtime feature; other programs keep the plain entry points, so their scheduling is unchanged. The limitations page notes that completion callbacks of callback-style I/O APIs still run with no store. 5815-als-callback-context compares the stores each kind of callback sees with Node, plain and with SCRIPTC_SAN=1. --- docs/content/docs/limitations.mdx | 2 + .../builtin-apis/asynclocalstorage.mdx | 1 + .../generated/node-v24-backlog.json | 21 ++-- .../generated/node-v24-internal.json | 25 +++-- internal/compatibility/static-support.json | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 8 ++ .../compiler/src/backend/llvm/lib-dispatch.ts | 8 +- .../compiler/src/backend/llvm/lib-process.ts | 20 ++-- packages/compiler/src/ir/runtime-features.ts | 17 +++ .../corpus/5815-als-callback-context.ts.json | 6 ++ packages/runtime/src/scr_async.c | 100 +++++++++++++++++- packages/runtime/src/scr_async.h | 12 +++ packages/runtime/src/scr_async_dyn.c | 1 + packages/runtime/src/scr_test.c | 20 ++++ packages/runtime/src/scr_test.h | 6 ++ tests/corpus/5815-als-callback-context.ts | 76 +++++++++++++ tests/harness/corpus-costs.json | 2 + 17 files changed, 299 insertions(+), 30 deletions(-) create mode 100644 docs/content/limitations/builtin-apis/asynclocalstorage.mdx create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json create mode 100644 tests/corpus/5815-als-callback-context.ts diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index ae166836f..d261b6385 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -64,6 +64,8 @@ title: "Limitations" ../limitations/builtin-apis/eventemitter.mdx +../limitations/builtin-apis/asynclocalstorage.mdx + ../limitations/builtin-apis/streams-and-readline.mdx ../limitations/builtin-apis/commonjs-metadata.mdx diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx new file mode 100644 index 000000000..020382915 --- /dev/null +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -0,0 +1 @@ +- **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 7d0fcf75c..a0d7bec4b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -880,7 +880,8 @@ "confidence": "high", "source": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -932,7 +933,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.disable", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -959,7 +961,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.getStore", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -986,7 +989,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.enterWith", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -1013,7 +1017,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.run", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -1040,7 +1045,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.exit", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -1771,7 +1777,8 @@ "confidence": "high", "source": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c87f98fec..cfbde8940 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1953,7 +1953,8 @@ "status": "partial", "evidence": "derived:descendants", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "chapter" @@ -2002,7 +2003,8 @@ "status": "partial", "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2074,7 +2076,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.disable", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2100,7 +2103,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.getStore", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2126,7 +2130,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.enterWith", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2152,7 +2157,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.run", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2178,7 +2184,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.exit", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2481,6 +2488,7 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts", "tests/harness/node-test.test.ts" ] }, @@ -3042,7 +3050,8 @@ "status": "partial", "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 584492bf1..2e5eb1c4c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -36,7 +36,7 @@ "syntax.for-await-readable-streams": ["tests/corpus/2861-readable-iterator-close.ts", "tests/corpus/2862-web-readable-async-iteration.ts"], "syntax.for-await-class-async-iterators": ["tests/corpus/2863-class-async-iterator.ts"], "node-builtin.assert": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1609-assert-async.ts"], - "node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs"], + "node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"], "node-builtin.buffer": ["tests/corpus/1402-buffer-encodings.ts", "tests/corpus/1660-buffer-read-write-num.ts", "tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"], "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"], "node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"], @@ -395,7 +395,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 588e8c26a..872153f40 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -968,6 +968,14 @@ export class LlEmitter { /** The program calls mock.timers members (moduleUsesMockTimers): timer * calls emit scr_test.c's mock-aware entry points. */ readonly mockTimers: boolean; + private schedulesInContextValue: boolean | undefined = undefined; + /** Whether timer and tick scheduling goes through the *_als entry + * points (RuntimeFeatures asyncContext). */ + get schedulesInContext(): boolean { + if (this.schedulesInContextValue === undefined) + this.schedulesInContextValue = moduleRuntimeFeatures(this.mod).asyncContext; + return this.schedulesInContextValue; + } /** Emitted ref-kind resolve thunks for new Promise, interned per inner * typeKey → thunk symbol. */ readonly resolveThunks = new Map(); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index f21ded106..034f0b6f1 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -526,7 +526,13 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) { throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); } - const sym = mockTimerSym(host, e) ?? LIB_FN_SYMS[e.fn]; + // queueMicrotask's checked-dynamic form carries the AsyncLocalStorage + // context like the static one (lib-process.ts). + const sym = + mockTimerSym(host, e) ?? + (e.fn === "timers.queueMicrotaskDyn" && host.schedulesInContext + ? "scr_queue_microtask_dyn_als" + : LIB_FN_SYMS[e.fn]); if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); const args = READ_ONLY_STRING_LIB_FNS.has(e.fn) ? emitBorrowedInputs(host, e.args) diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 546c9a209..3c35ae6dc 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -374,16 +374,18 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr host.usesTimers = true; const cb = host.emitExpr(e.args[0]!); host.moveTemp(cb); + // Programs that enter AsyncLocalStorage stores schedule through the + // *_als entry points, which carry the active stores into the callback. const sym = - mockTimerSym(host, e) ?? - { - "timers.setTimeout": "scr_set_timeout", - "timers.setInterval": "scr_set_interval", - "timers.setTimeoutHandle": "scr_set_timeout_handle", - "timers.setImmediate": "scr_set_immediate", - "process.nextTick": "scr_next_tick", - "timers.queueMicrotask": "scr_queue_microtask", - }[e.fn]; + (mockTimerSym(host, e) ?? + { + "timers.setTimeout": "scr_set_timeout", + "timers.setInterval": "scr_set_interval", + "timers.setTimeoutHandle": "scr_set_timeout_handle", + "timers.setImmediate": "scr_set_immediate", + "process.nextTick": "scr_next_tick", + "timers.queueMicrotask": "scr_queue_microtask", + }[e.fn]!) + (host.schedulesInContext ? "_als" : ""); const rest = e.args.slice(1).map((a) => host.emitExpr(a)); const argList = [`ptr ${cb.name}`, ...rest.map((a) => `${host.llType(a.type)} ${a.name}`)].join( ", ", diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 04443ee18..d50d173b8 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -24,6 +24,11 @@ export interface RuntimeFeatures { assert: boolean; dynInvoke: boolean; dynAsync: boolean; + /** The program enters AsyncLocalStorage stores (store.run, enterWith, + * a diagnostics_channel store binding): its timer, immediate, tick and + * queueMicrotask scheduling goes through the runtime's *_als entry + * points, which carry the active stores into each callback. */ + asyncContext: boolean; inspect: boolean; childProcess: boolean; net: boolean; @@ -108,6 +113,16 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "dc.chanRunStores", ]); +/** The libCalls that enter AsyncLocalStorage stores (RuntimeFeatures + * asyncContext). */ +const ASYNC_CONTEXT_LIB_FNS: ReadonlySet = new Set([ + "als.run", + "als.exitRun", + "als.enterWith", + "dc.chanBindStore", + "dc.chanRunStores", +]); + const LEGACY_URL_LIB_FNS: ReadonlySet = new Set([ "url.legacyParse", "url.legacyFormat", @@ -203,6 +218,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run dc: false, assert: false, dynInvoke: mod.workers === true, + asyncContext: false, dynAsync: mod.workers === true || mod.functions.some( @@ -360,6 +376,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run ) features.dynInvoke = true; if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; + if (ASYNC_CONTEXT_LIB_FNS.has(fn)) features.asyncContext = true; if ( fn.startsWith("insp.") || fn.startsWith("console.") || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json new file mode 100644 index 000000000..f192a6b22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5815-als-callback-context.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index f630ee5f9..b1ebfc6e9 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -694,6 +694,26 @@ static inline void *scr_fiber_region_capture(void) { return regions != NULL ? regions->capture() : NULL; } +/* enterWith on the main stack lasts until that code returns to the loop, + * so the store does not reach callbacks scheduled without it: the loop + * clears the main slot when the main module's body returns, and an + * enterWith inside a callback the loop runs puts a tick that clears it at + * the front of the tick queue, so it runs as soon as that callback + * returns. */ +static SCR_TL bool scr_loop_started = false; + +static void scr_als_main_clear(void) { + scr_als_ctx_release(scr_als_main_slot); + scr_als_main_slot = NULL; +} + +static void scr_next_tick_raw_first(void (*fn)(void)); + +void scr_als_entered_with(void) { + if (scr_loop_started && SCR_ALS_SLOT() == &scr_als_main_slot) + scr_next_tick_raw_first(scr_als_main_clear); +} + static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */ static SCR_TL ScrFiber *scr_current = NULL; /* A finished fiber whose last switch went to the loop (scr_resume_fiber). */ @@ -1168,6 +1188,17 @@ void scr_next_tick_raw(void (*fn)(void)) { scr_nt_tail = t; } +/* A raw tick at the front of the queue: it runs before the ticks already + * queued. */ +static void scr_next_tick_raw_first(void (*fn)(void)) { + ScrNtick *t = calloc(1, sizeof *t); + if (!t) scr_oom(); + t->raw = fn; + t->next = scr_nt_head; + scr_nt_head = t; + if (scr_nt_tail == NULL) scr_nt_tail = t; +} + void scr_nticks_teardown(void) { while (scr_nt_head != NULL) { ScrNtick *t = scr_nt_head; @@ -3731,6 +3762,54 @@ void scr_queue_microtask(ScrClosure *cb) { scr_ready_push(f); } +/* ── scheduling in an AsyncLocalStorage context ─────────────────────── + * Programs that enter AsyncLocalStorage stores schedule their timer, + * interval, setImmediate, process.nextTick and queueMicrotask callbacks + * through these entry points (the emitter picks them when the program + * uses the store API), so each callback runs with the stores that were + * active when it was scheduled, as Node's async resources capture them. + * With a snapshot active, the callback is wrapped in a closure that + * installs it around the call; other programs keep the plain entry + * points. */ +static void *scr_als_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_als_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static void scr_als_bound_fire(ScrClosure *self) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[1]); /* the slot owns this reference */ + ((void (*)(ScrClosure *))cb->fn)(cb); + /* An enterWith inside the callback replaced the slot's snapshot; either + * way the slot's reference is released here. */ + slot = SCR_ALS_SLOT(); + scr_als_ctx_release(*slot); + *slot = prev; + scr_closure_release(cb); +} + +static ScrClosure *scr_als_bind(ScrClosure *cb /*moves*/) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (active == NULL) return cb; + ScrClosure *bound = scr_closure_new((void *)scr_als_bound_fire, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + return bound; +} + +void scr_set_timeout_als(ScrClosure *cb, double ms) { scr_set_timeout(scr_als_bind(cb), ms); } +double scr_set_interval_als(ScrClosure *cb, double ms) { + return scr_set_interval(scr_als_bind(cb), ms); +} +double scr_set_timeout_handle_als(ScrClosure *cb, double ms) { + return scr_set_timeout_handle(scr_als_bind(cb), ms); +} +double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_bind(cb)); } +void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); } +void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); } + /* The checked-dynamic argument form (JS files — common.mustCall wrappers * and the suite's invalid-input probes): a non-function throws Node's * ERR_INVALID_ARG_TYPE synchronously; a function value is called with @@ -3743,16 +3822,28 @@ static void scr_micro_dyn_fire(ScrClosure *c) { scr_dyn_release(fn); } -void scr_queue_microtask_dyn(const ScrDyn *cb) { +/* The closure that calls a dyn function value, or NULL with Node's + * ERR_INVALID_ARG_TYPE pending for a non-function. */ +static ScrClosure *scr_micro_dyn_closure(const ScrDyn *cb) { if (cb->kind != SCR_DYN_FUNC) { static const char msg[] = "The \"callback\" argument must be of type function."; scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE"); - return; + return NULL; } ScrClosure *c = scr_closure_new((void *)scr_micro_dyn_fire, 1); c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); scr_box_set_ref(c->caps[0], scr_dyn_retain((ScrDyn *)cb)); - scr_queue_microtask(c); + return c; +} + +void scr_queue_microtask_dyn(const ScrDyn *cb) { + ScrClosure *c = scr_micro_dyn_closure(cb); + if (c) scr_queue_microtask(c); +} + +void scr_queue_microtask_dyn_als(const ScrDyn *cb) { + ScrClosure *c = scr_micro_dyn_closure(cb); + if (c) scr_queue_microtask_als(c); } /* ── the event hooks (scr_events.c) ────────────────────────────────── @@ -4520,6 +4611,9 @@ static SCR_TL bool scr_loop_cjs_entry = false; void scr_loop_commonjs_entry(void) { scr_loop_cjs_entry = true; } bool scr_loop_run(ScrPromise *top_level) { + /* The main module's body has returned: an enterWith it made ends here. */ + scr_loop_started = true; + if (scr_als_main_slot != NULL) scr_als_main_clear(); bool first_checkpoint = !scr_loop_cjs_entry; bool rejection_failed; for (;;) { diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index efa98e942..96421ca1c 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -158,6 +158,8 @@ ScrAlsCtx **scr_als_slot(void); #endif ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c); void scr_als_ctx_release(ScrAlsCtx *c); +/* After an enterWith: on the main stack, queues the tick that ends it. */ +void scr_als_entered_with(void); double scr_als_new(void); ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */ @@ -409,6 +411,7 @@ double scr_set_immediate(ScrClosure *cb); /* cb ownership moves in */ * synchronously on a non-function; borrowed. */ void scr_queue_microtask(ScrClosure *cb); void scr_queue_microtask_dyn(const ScrDyn *cb); +void scr_queue_microtask_dyn_als(const ScrDyn *cb); /* setImmediate AS A dyn VALUE (the traceCallback shape): a minted dyn * callable that schedules args[0](args[1..]) on the immediate queue and * answers undefined; a non-function first argument throws Node's @@ -428,6 +431,15 @@ bool scr_immediate_has_ref(double handle); * teardown (they must never run yet must not leak). cb ownership moves * in. */ void scr_next_tick(ScrClosure *cb); +/* The scheduling entry points of programs that enter AsyncLocalStorage + * stores: each callback runs with the stores active when it was scheduled + * (scr_async.c). */ +void scr_set_timeout_als(ScrClosure *cb, double ms); +double scr_set_interval_als(ScrClosure *cb, double ms); +double scr_set_timeout_handle_als(ScrClosure *cb, double ms); +double scr_set_immediate_als(ScrClosure *cb); +void scr_next_tick_als(ScrClosure *cb); +void scr_queue_microtask_als(ScrClosure *cb); /* A successful process.stdout/stderr.write completion on that same queue. * The callback and its program-shaped Error | null adapter both MOVE into * the entry; the adapter is invoked with NULL (Node's success argument). */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index ffcb85050..7611378fb 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -120,6 +120,7 @@ void scr_als_restore(ScrAlsCtx *prev) { void scr_als_enter_with(double id, ScrDyn *value) { ScrAlsCtx *prev = scr_als_enter(id, value); scr_als_ctx_release(prev); /* no restore point — Node's enterWith */ + scr_als_entered_with(); } void scr_als_disable(double id) { diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 00e609129..43affdd8a 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -2279,6 +2279,26 @@ double scr_test_mt_set_immediate(ScrClosure *cb /*moves*/) { ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; return m ? scr_mt_create(m, false, cb, -1)->handle : scr_set_immediate(cb); } +/* The same calls in a program that enters AsyncLocalStorage stores: a + * mocked timer runs in the context its tick() runs in, as in Node; a real + * one keeps the stores active when it was scheduled. */ +void scr_test_mt_set_timeout_als(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_TIMEOUT]; + if (m) (void)scr_mt_create(m, false, cb, ms); + else scr_set_timeout_als(cb, ms); +} +double scr_test_mt_set_timeout_handle_als(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_TIMEOUT]; + return m ? scr_mt_create(m, false, cb, ms)->handle : scr_set_timeout_handle_als(cb, ms); +} +double scr_test_mt_set_interval_als(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_INTERVAL]; + return m ? scr_mt_create(m, true, cb, ms)->handle : scr_set_interval_als(cb, ms); +} +double scr_test_mt_set_immediate_als(ScrClosure *cb /*moves*/) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; + return m ? scr_mt_create(m, false, cb, -1)->handle : scr_set_immediate_als(cb); +} void scr_test_mt_clear_immediate(double handle) { ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; if (m) scr_mt_clear(m, handle); diff --git a/packages/runtime/src/scr_test.h b/packages/runtime/src/scr_test.h index 64a280bf7..b29fca362 100644 --- a/packages/runtime/src/scr_test.h +++ b/packages/runtime/src/scr_test.h @@ -110,6 +110,12 @@ void scr_test_mt_clear_immediate(double handle); void scr_test_mt_immediate_unref(double handle); void scr_test_mt_immediate_ref(double handle); bool scr_test_mt_immediate_has_ref(double handle); +/* The setters of programs that also enter AsyncLocalStorage stores: an + * unmocked call uses the loop's *_als function. */ +void scr_test_mt_set_timeout_als(ScrClosure *cb, double ms); +double scr_test_mt_set_timeout_handle_als(ScrClosure *cb, double ms); +double scr_test_mt_set_interval_als(ScrClosure *cb, double ms); +double scr_test_mt_set_immediate_als(ScrClosure *cb); double scr_test_mt_date_now(void); /* timers/promises scheduler.wait(delay) (+1 promise; NULL after a throw). */ ScrPromise *scr_test_mt_scheduler_wait(double ms); diff --git a/tests/corpus/5815-als-callback-context.ts b/tests/corpus/5815-als-callback-context.ts new file mode 100644 index 000000000..461a2aa95 --- /dev/null +++ b/tests/corpus/5815-als-callback-context.ts @@ -0,0 +1,76 @@ +// AsyncLocalStorage context reaches timer, interval, setImmediate, +// process.nextTick and queueMicrotask callbacks: each runs in the store +// that was active when it was scheduled, including callbacks they schedule +// in turn, an interval on every tick, and a refreshed timer. A cleared +// timer or immediate never runs. enterWith inside a callback lasts until +// the callback returns, also in a callback scheduled outside any store, +// and callbacks scheduled outside any store, or inside exit(), see +// undefined. A store entered at the top level reaches only the callbacks +// scheduled after it. +import { AsyncLocalStorage } from "node:async_hooks"; + +const requests = new AsyncLocalStorage(); +const users = new AsyncLocalStorage(); +const seen: string[] = []; + +function record(where: string): void { + seen.push(`${where}: ${requests.getStore() ?? "-"} ${users.getStore() ?? "-"}`); +} + +requests.run("req-1", () => { + setTimeout(() => { + record("timeout"); + process.nextTick(() => record("tick from timeout")); + queueMicrotask(() => record("microtask from timeout")); + }, 1); + setImmediate(() => { + record("immediate"); + setTimeout(() => record("timeout from immediate"), 1); + }); + process.nextTick(() => record("tick")); + queueMicrotask(() => record("microtask")); + users.run("ada", () => { + setTimeout(() => record("nested timeout"), 2); + setImmediate(() => record("nested immediate")); + }); + let ticks = 0; + const interval = setInterval(() => { + ticks++; + record(`interval ${ticks}`); + if (ticks === 3) clearInterval(interval); + }, 1); + const refreshed = setTimeout(() => record("refreshed timeout"), 3); + refreshed.refresh(); + const cleared = setTimeout(() => record("cleared timeout"), 1); + clearTimeout(cleared); + const clearedImmediate = setImmediate(() => record("cleared immediate")); + clearImmediate(clearedImmediate); + requests.exit(() => { + setTimeout(() => record("timeout in exit"), 1); + queueMicrotask(() => record("microtask in exit")); + }); +}); + +requests.run("req-2", () => { + setTimeout(() => { + users.enterWith("grace"); + record("timeout after enterWith"); + setTimeout(() => record("timeout from enterWith"), 1); + }, 4); + setTimeout(() => record("timeout after the enterWith timeout"), 5); +}); + +setTimeout(() => { + record("timeout outside any store"); + requests.enterWith("entered outside"); +}, 1); +setTimeout(() => record("timeout after one that entered outside any store"), 8); +process.nextTick(() => record("tick outside any store")); + +setTimeout(() => { + record("final"); + for (const line of seen.sort()) console.log(line); +}, 30); + +users.enterWith("top level"); +setTimeout(() => record("timeout after a top-level enterWith"), 6); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 1a4ad1180..a03ce551f 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2092,6 +2092,7 @@ "5812-dyn-box-async-fields.ts": 3522, "5813-mock-timers-module.ts": 3766, "5814-mock-timers-date.ts": 3760, + "5815-als-callback-context.ts": 3110, "5830-timers-promises-signal.ts": 3875, "5831-util-types-brands.ts": 3347, "5832-util-types-brands-dynamic.js": 3250, @@ -5165,6 +5166,7 @@ "5812-dyn-box-async-fields.ts": 5278, "5813-mock-timers-module.ts": 4227, "5814-mock-timers-date.ts": 17708, + "5815-als-callback-context.ts": 4542, "5830-timers-promises-signal.ts": 4170, "5831-util-types-brands.ts": 4380, "5832-util-types-brands-dynamic.js": 3658, From a1a475c76f21f5338793ada72d2afe5f36fedb15 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:30:45 -0500 Subject: [PATCH 100/120] Count each statement once in coverage however often it lowers Speculative lowering lowers some function bodies several times: the receiver of `(function () { ... }).call(this)` goes through four call lowerings that each probe it with tryLowerExpression, and each probe lowered and counted the body. A five-statement program with such a wrapper reported 14 statements, and a fenced statement inside it counted as four failures. The Lowerer now records which statements and class field initializers are in the totals and which are counted as failed, and counts each node once. Corpus 6110's @deferred-fences directive stays 1, its one fenced statement in a class base lowered once, and its header no longer ties the count to how many times that body lowers. --- .../src/frontend/lowering/lower-classes.ts | 12 +++------ .../src/frontend/lowering/lower-stmts.ts | 10 ++----- .../compiler/src/frontend/lowering/lowerer.ts | 26 +++++++++++++++++++ .../6110-cjs-speculative-wrapper-fences.cjs | 3 +-- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a4b771ded..d44748084 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -4598,8 +4598,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] continue; } const f = item.f; - lowerer.stats.statementsTotal++; - lowerer.bumpFileStat(locOf(f.initializer).file, "total"); + lowerer.countStatement(f.initializer, locOf(f.initializer).file); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -4635,8 +4634,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] out.push({ kind: "assign", localId: f.globalId, value, loc: locOf(f.initializer) }); } catch (e) { if (!(e instanceof PoisonError)) throw e; - lowerer.stats.statementsFailed++; - lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); + lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); if (isJsSourceFile(f.initializer.getSourceFile())) { const fence = lowerer.diagSink === null @@ -7764,8 +7762,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL }); } if (!f.initializer) continue; - lowerer.stats.statementsTotal++; - lowerer.bumpFileStat(locOf(f.initializer).file, "total"); + lowerer.countStatement(f.initializer, locOf(f.initializer).file); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -7780,8 +7777,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL }); } catch (e) { if (!(e instanceof PoisonError)) throw e; - lowerer.stats.statementsFailed++; - lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); + lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); if (isJsSourceFile(f.initializer.getSourceFile())) { const fence = lowerer.diagSink === null diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 43ad3819b..8ded9106b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -689,10 +689,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir entry.index = i; if (unreachable && skipsWhenUnreachable(stmt)) continue; if (inFunction && isAbruptStatement(stmt)) unreachable = true; - if (!lowerer.suppressStats) { - lowerer.stats.statementsTotal++; - if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total"); - } + if (!lowerer.suppressStats) lowerer.countStatement(stmt, sf?.fileName); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -757,10 +754,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir } else if (!(e instanceof PoisonError)) { throw e; } - if (!lowerer.suppressStats) { - lowerer.stats.statementsFailed++; - if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "failed"); - } + if (!lowerer.suppressStats) lowerer.countStatementFailed(stmt, sf?.fileName); lowerer.noteBlockedBindings(stmt); // The runtime-fence conversion (JS sources, direct diagnostics // only — a capture wrapper in force means collection owns the diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6493bc5f3..a77142cb0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2925,6 +2925,32 @@ export class Lowerer { * pass skips it (attribution describes the build). */ readonly statsByFile = new Map(); + /** Statements and field initializers already in the coverage totals, and + * those already counted as failed. Speculative lowering can lower one + * function body several times: each receiver probe of + * `(function () { ... }).call(this)` lowers it. A source statement counts + * once however many times it lowers. */ + private readonly countedStatements = new Set(); + private readonly failedStatements = new Set(); + + /** Adds `node` to the coverage totals unless it is already counted (the + * caller checks suppressStats). */ + countStatement(node: ts.Node, file: string | undefined): void { + if (this.countedStatements.has(node)) return; + this.countedStatements.add(node); + this.stats.statementsTotal++; + if (file !== undefined) this.bumpFileStat(file, "total"); + } + + /** Counts `node` as failed unless it already is (the caller checks + * suppressStats). */ + countStatementFailed(node: ts.Node, file: string | undefined): void { + if (this.failedStatements.has(node)) return; + this.failedStatements.add(node); + this.stats.statementsFailed++; + if (file !== undefined) this.bumpFileStat(file, "failed"); + } + /** Bumps the per-file attribution counter (no-op unless provenance is * active and this is the emit/discovery lane — mirror the CALLER's * suppressStats guard, this method only gates remainder). */ diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index fbb1a65fc..3ae9df3dc 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -5,8 +5,7 @@ // and `typeof module.exports` compile; a statement with no static lowering // (here `module.exports` passed as a value in the class base) compiles to // its runtime fence instead of vanishing. It never runs, so the binary -// matches Node. Coverage counts that one fenced statement once: the class -// base's body is lowered once. +// matches Node. const verbose = process.argv.length > 99; (function () { var root = this; From 05f2dd585987519c4bc728d941535bf7df6f12ab Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:03:25 -0500 Subject: [PATCH 101/120] Walk Map and Set iterators inside iterator helper objects A helper object over a Map or Set iterator that is stored and consumed later (const it = set.values().map(f); for (const x of it) ...) stepped the runtime's checked iterator through an adapter generator, boxing each value and resuming two generators per element: 5.1x Node's time on a 100k-element Set. A stage on m.keys(), m.values(), m.entries() (or a Set's), or on Iterator.from(set) or Iterator.from(map), now walks the collection inside the helper's own generator. The walk steps the runtime's native iterator by entry index through the new scr_dyn_collection_iterator_index and reads each key or value with the typed intrinsics. The native iterator keeps the walk live and ends the collection's iteration when it is exhausted or released, so an abandoned or returned helper leaves the collection free to compact. Other helper sources over a Map or Set, and Iterator.from over a Set or Map, use a typed generator over the collection built the same way. --- .../backend/llvm/checked-value-lifetimes.ts | 7 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/containers/iterator-helpers.ts | 233 +++++++++++++----- .../frontend/lowering/iterator-adapters.ts | 116 +++++++++ .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/int32-slots.ts | 1 + .../6073-stored-collection-helpers.ts.json | 6 + .../6074-stored-collection-helpers-js.js.json | 6 + packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_json.c | 15 ++ .../corpus/6073-stored-collection-helpers.ts | 60 +++++ .../6074-stored-collection-helpers-js.js | 57 +++++ tests/harness/corpus-costs.json | 4 + 13 files changed, 453 insertions(+), 58 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json create mode 100644 tests/corpus/6073-stored-collection-helpers.ts create mode 100644 tests/corpus/6074-stored-collection-helpers-js.js diff --git a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts index c359f6175..5f8119079 100644 --- a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts @@ -42,5 +42,10 @@ export function borrowsJsonInputs(fn: string): boolean { * loop. Runtime steps borrow that state but may invoke indexed getters, so * this permits stable-local borrowing without preserving reference edges. */ export function borrowsIteratorInputs(fn: string): boolean { - return fn === "dyn.iteratorCanStep" || fn === "dyn.iteratorStep" || fn === "dyn.iteratorStepDone"; + return ( + fn === "dyn.iteratorCanStep" || + fn === "dyn.iteratorStep" || + fn === "dyn.iteratorStepDone" || + fn === "dyn.collectionIteratorIndex" + ); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index ef83bf07e..8d758a0db 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -889,6 +889,7 @@ export const LIB_FN_SYMS: Record = { "dyn.iteratorCanStep": "scr_dyn_iterator_can_step", "dyn.iteratorStep": "scr_dyn_iterator_step", "dyn.iteratorStepDone": "scr_dyn_iterator_step_done", + "dyn.collectionIteratorIndex": "scr_dyn_collection_iterator_index", "dyn.mapSeedEntries": "scr_dyn_map_seed_entries", "dyn.mapSeedEntry": "scr_dyn_map_seed_entry", "dyn.arrLen": "scr_dyn_arr_len", diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 70e0d85e0..229999627 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -39,6 +39,7 @@ import { iteratorSlotAdapter, lowerArrayIteratorValue, lowerBytesIteratorValue, + lowerCollectionIteratorValue, lowerStringIteratorValue, } from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; @@ -157,15 +158,45 @@ export function lowerGeneratorHelperCall( if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { lowerer.noLowering(`.${name} with ${call.arguments.length} arguments`, call); } + // A stage on m.keys()/values()/entries() (or a Set's) walks the + // collection inside the helper's own generator. + const collection = collectionIteratorCall(lowerer, access.expression); + if (collection !== null) { + const coll = lowerer.lowerExpr(collection.receiver); + if (coll.type.kind !== "map" && coll.type.kind !== "set") + lowerer.badType(collection.receiver, lowerer.typeOf(collection.receiver)); + const source: ChainSource = { kind: "collection", type: coll.type, proj: collection.proj }; + const elem = collectionSeed(lowerer, source); + const { stage, out } = lowerStage(lowerer, { name, argNode, access }, elem, loc); + return lowerHelperObject(lowerer, call, coll, source, elem, stage, out, loc); + } const src = lowerHelperSource(lowerer, access.expression); if (name !== "take" && name !== "drop") { const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc); - return lowerHelperObject(lowerer, call, src.gen, src.elem, stage, out, loc); + return lowerHelperObject( + lowerer, + call, + src.gen, + { kind: "generator", type: src.gen.type, elem: src.elem }, + src.elem, + stage, + out, + loc, + ); } // The budget check closes the source, so the source is bound first. const bound = bindSource(lowerer, src.gen, loc); const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc, bound.ref); - const helper = lowerHelperObject(lowerer, call, bound.ref, src.elem, stage, out, loc); + const helper = lowerHelperObject( + lowerer, + call, + bound.ref, + { kind: "generator", type: src.gen.type, elem: src.elem }, + src.elem, + stage, + out, + loc, + ); return { kind: "seqExpr", stmts: [bound.decl], result: helper, type: helper.type, loc }; } // The stages before the terminal, back to the first receiver that is @@ -281,8 +312,8 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { * iterator, which already inherits from Iterator.prototype, so Node returns * it rather than a wrapper. A generator (including a helper object or an * array iterator) is itself; an array, string or typed array gives its - * values iterator; a Set its values and a Map its entries, adapted into a - * generator over the live collection. The result converts into the call's + * values iterator; a Set its values and a Map its entries, as a generator + * over the live collection. The result converts into the call's * IteratorObject type. Null for other arguments (objects with their own * next method, untyped values), which keep the Iterator fence. */ export function lowerIteratorFromCall( @@ -317,16 +348,13 @@ export function lowerIteratorFromCall( iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc); else if (t.kind === "set" || t.kind === "map") { const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value); - const native: IrExpr = { - kind: "dynInvoke", - recv: lowerer.coerceToExpected(value, DYN), - method: t.kind === "set" ? "values" : "entries", - calleeName: t.kind === "set" ? "set.values" : "map.entries", - args: [], - type: DYN, + iterator = lowerCollectionIteratorValue( + lowerer, + value, + t.kind === "set" ? "values" : "entries", + own(elem), loc, - }; - iterator = iteratorSlotAdapter(lowerer, native, own(elem)); + ); } if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode)); const result = lowerer.coerceToExpected(iterator, resultT); @@ -686,6 +714,14 @@ function lowerHelperSource( ) lowerer.badType(expr, tsType); const target: GenType = { kind: "generator", yieldT, retT: VOID, nextT: UNDEFINED_T }; + // m.keys()/values()/entries() (or a Set's) on a typed collection: a + // generator over the collection with typed reads. + const collection = collectionIteratorCall(lowerer, expr); + if (collection !== null) { + const coll = lowerer.lowerExpr(collection.receiver); + const walk = lowerCollectionIteratorValue(lowerer, coll, collection.proj, target, locOf(expr)); + if (walk !== null) return { gen: walk as IrExpr & { type: GenType }, elem: yieldT }; + } const adapted = iteratorSlotAdapter(lowerer, lowerer.lowerExpr(expr), target); if (adapted === null || adapted.type.kind !== "generator") lowerer.badType(expr, tsType); return { gen: adapted as IrExpr & { type: GenType }, elem: yieldT }; @@ -937,7 +973,8 @@ function lowerFusedChain( function lowerHelperObject( lowerer: Lowerer, call: ts.CallExpression, - src: IrExpr & { type: GenType }, + src: IrExpr, + source: (ChainSource & { kind: "generator" }) | (ChainSource & { kind: "collection" }), srcElem: IrType, stage: StageIr, out: IrType, @@ -961,21 +998,23 @@ function lowerHelperObject( "the helper's element type must be representable — annotate the callback's types", ); } - if (!stepDrivable(src.type)) { + if (source.kind === "generator" && !stepDrivable(source.type)) { lowerer.noLowering( - `.${stage.kind} over an iterator yielding '${lowerer.fmt(src.type.yieldT)}'`, + `.${stage.kind} over an iterator yielding '${lowerer.fmt(source.type.yieldT)}'`, call, "helpers over iterators of numbers, booleans, strings, arrays, records and class instances are the supported forms", ); } const arg = "budget" in stage ? stage.budget : stage.fn; - const key = `iterHelper:${stage.kind}:${typeKey(src.type)}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; + const srcKey = + source.kind === "collection" ? `${source.proj}:${typeKey(source.type)}` : typeKey(source.type); + const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( - buildHelperGenFn(lowerer, name, src.type, srcElem, stage, arg.type, outT, resultType, loc), + buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), ); } return { kind: "call", callee: name, args: [src, arg], type: outT, loc }; @@ -1267,6 +1306,107 @@ function driveGenerator( ]; } +/** Statements that walk the Map or Set in local `collId` from inside a + * helper's generator: each pass runs `pre` (which may `break`), takes the + * next live entry index from the runtime's native iterator, which keeps + * the walk live and ends it when exhausted or released, and runs `body` + * over the projected element read with the typed intrinsics. */ +function driveCollectionNative( + lowerer: Lowerer, + frame: ChainFrame, + collId: string, + source: ChainSource & { kind: "collection" }, + pre: IrStmt[], + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, +): IrStmt[] { + const t = source.type; + const coll = (): IrExpr => varRef(collId, t, loc); + const nativeId = frame.local("native", DYN, false); + const iId = frame.local("i", F64, false); + const read = (method: "iterKey" | "iterValue", type: IrType): IrExpr => + t.kind === "map" + ? { kind: "mapIntrinsic", method, receiver: coll(), args: [varRef(iId, F64, loc)], type, loc } + : { + kind: "setIntrinsic", + method: "iterKey", + receiver: coll(), + args: [varRef(iId, F64, loc)], + type, + loc, + }; + const keyT = t.kind === "map" ? t.key : t.elem; + const valueT = t.kind === "map" ? t.value : t.elem; + const seedT = collectionSeed(lowerer, source); + const seed: IrExpr = + source.proj === "entries" + ? { + kind: "recordLit", + fields: [ + { name: "0", value: read("iterKey", keyT) }, + { name: "1", value: read("iterValue", valueT) }, + ], + type: seedT, + loc, + } + : source.proj === "values" && t.kind === "map" + ? read("iterValue", valueT) + : read("iterKey", keyT); + const vId = frame.local("v", seedT, false); + return [ + { + kind: "varDecl", + localId: nativeId, + init: { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(coll(), DYN), + method: "values", + calleeName: t.kind === "map" ? "map.values" : "set.values", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "while", + cond: boolLit(true, loc), + body: [ + ...pre, + { + kind: "varDecl", + localId: iId, + init: { + kind: "libCall", + fn: "dyn.collectionIteratorIndex", + args: [varRef(nativeId, DYN, loc)], + type: F64, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: varRef(iId, F64, loc), + right: numLit(0, loc), + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + { kind: "varDecl", localId: vId, init: seed, loc }, + ...body(varRef(vId, seedT, loc)), + ], + loc, + }, + ]; +} + /** `for (j = 0; j < arr.length && !stop; j++) body(arr[j])` over a live * array local (the array iterator's per-step length read). */ function walkArray( @@ -1978,7 +2118,7 @@ function buildIterChainFn( function buildHelperGenFn( lowerer: Lowerer, name: string, - srcT: GenType, + source: (ChainSource & { kind: "generator" }) | (ChainSource & { kind: "collection" }), srcElem: IrType, stage: StageIr, argT: IrType, @@ -1987,8 +2127,12 @@ function buildHelperGenFn( loc: SrcLoc, ): IrFunction { const frame = new LiftedFrame(loc); - const srcId = frame.param("src", srcT); + const srcId = frame.param("src", source.type); const argId = frame.param(stage.kind === "take" || stage.kind === "drop" ? "n" : "f", argT); + const drive = (pre: IrStmt[], each: (value: IrExpr) => IrStmt[], at: SrcLoc): IrStmt[] => + source.kind === "generator" + ? driveGenerator(lowerer, frame, srcId, source.type, srcElem, pre, each, at) + : driveCollectionNative(lowerer, frame, srcId, source, pre, each, at); const yieldOf = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: { @@ -2014,12 +2158,7 @@ function buildHelperGenFn( const n = (): IrExpr => varRef(argId, F64, loc); if (stage.kind === "map") { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const mId = frame.local("m", fnT!.ret, false); @@ -2034,12 +2173,7 @@ function buildHelperGenFn( ); } else if (stage.kind === "filter") { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const okId = frame.local("ok", BOOL, false); @@ -2070,28 +2204,12 @@ function buildHelperGenFn( loc, }, ]; - body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, - pre, - (value) => [yieldOf(value)], - loc, - ), - ); + body.push(...drive(pre, (value) => [yieldOf(value)], loc)); } else if (stage.kind === "drop") { const droppedId = frame.local("dropped", F64, true); body.push({ kind: "varDecl", localId: droppedId, init: numLit(0, loc), loc }); body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => [ { @@ -2115,12 +2233,7 @@ function buildHelperGenFn( ); } else { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const innerT = fnT!.ret; @@ -2142,7 +2255,13 @@ function buildHelperGenFn( returnType: VOID, locals: frame.locals, body, - generator: { yieldT: outT.yieldT, nextT: outT.nextT, resultType, closesSource: true }, + generator: { + yieldT: outT.yieldT, + nextT: outT.nextT, + resultType, + // A native Map or Set iterator has no return() to call. + ...(source.kind === "generator" ? { closesSource: true as const } : {}), + }, loc, }; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 812f29242..f086417de 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -265,6 +265,122 @@ function dynIteratorAdapter( return { kind: "call", callee: name, args: [expr], type: expected, loc }; } +/** `m.keys()`, `m.values()` or `m.entries()` (or a Set's) as a source + * generator for an iterator helper object: an interned generator over the + * typed collection that steps the runtime's native iterator by entry + * index and reads each key or value with the typed intrinsics, so values + * keep their types without a checked conversion per step. The native + * iterator owns the live-walk bracket: it ends the iteration when the + * collection is exhausted or when the generator, done or abandoned, + * releases it. `type` yields the key, the value, or the interned [key, + * value] pair. Null when `type` does not match. */ +export function lowerCollectionIteratorValue( + lowerer: Lowerer, + receiver: IrExpr, + method: "keys" | "values" | "entries", + type: IrType, + loc: SrcLoc, +): IrExpr | null { + const collT = receiver.type; + if ((collT.kind !== "map" && collT.kind !== "set") || type.kind !== "generator") return null; + if (type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null; + const keyT = collT.kind === "map" ? collT.key : collT.elem; + const valueT = collT.kind === "map" ? collT.value : collT.elem; + const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `collIter:${method}:${typeKey(collT)}:${typeKey(type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const coll = varRef("coll.0", collT, loc); + const index = varRef("index.0", F64, loc); + const native = varRef("native.0", DYN, loc); + const read = (m: "iterKey" | "iterValue", t: IrType): IrExpr => + collT.kind === "map" + ? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc } + : { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc }; + let yielded: IrExpr; + if (method === "keys" || (collT.kind === "set" && method === "values")) + yielded = read("iterKey", keyT); + else if (method === "values") yielded = read("iterValue", valueT); + else + yielded = { + kind: "recordLit", + fields: [ + { name: "0", value: read("iterKey", keyT) }, + { name: "1", value: read("iterValue", valueT) }, + ], + type: type.yieldT, + loc, + }; + if (!typeEquals(yielded.type, type.yieldT)) return null; + name = `%coll.iter.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "coll.0", name: "coll", type: collT }], + returnType: VOID, + locals: [ + { id: "coll.0", name: "coll", type: collT, mutable: false }, + { id: "native.0", name: "native", type: DYN, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "native.0", + init: { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(coll, DYN), + method: "values", + calleeName: collT.kind === "map" ? "map.values" : "set.values", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "varDecl", + localId: "index.0", + init: { + kind: "libCall", + fn: "dyn.collectionIteratorIndex", + args: [native], + type: F64, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: index, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc }, + ], + loc, + }, + ], + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type, loc }; +} + /** `ta.values()`, `ta.keys()` or `ta.entries()` on a typed array as a * value: an interned generator over the indices, reading the length and * the element at each step, as the typed array iterator does. Null when diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 51683990b..51765286b 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -182,6 +182,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, + "dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 }, "dyn.mapSeedEntries": { argTypes: [DYN], result: DYN }, "dyn.mapSeedEntry": { argTypes: [DYN], result: DYN }, /** The for-of-over-dyn pack accessors — the emitted index loop drives diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index d7d06db7d..903823358 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -371,6 +371,7 @@ const DYN_READ_ONLY = new Set([ "dyn.iteratorResult", "dyn.iteratorStep", "dyn.iteratorStepDone", + "dyn.collectionIteratorIndex", "dyn.mapSeedEntries", "dyn.mapSeedEntry", "dyn.numberConstructor", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json new file mode 100644 index 000000000..6d9eeff70 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6073-stored-collection-helpers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json new file mode 100644 index 000000000..fbbaf0df4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6074-stored-collection-helpers-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 2606e1473..e76563ab7 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -702,6 +702,10 @@ ScrDyn *scr_dyn_iterator_result(ScrDyn *value); bool scr_dyn_iterator_can_step(const ScrDyn *iterator, const ScrDyn *next); ScrDyn *scr_dyn_iterator_step(const ScrDyn *iterator); bool scr_dyn_iterator_step_done(const ScrDyn *iterator); +/* A native Map or Set iterator stepped by the compiler's typed walk: the + * next live entry index of its collection, or -1 once the collection is + * exhausted (the iterator then ends its iteration). */ +double scr_dyn_collection_iterator_index(const ScrDyn *iterator); /* The ordinary object snapshot plus a retained typed source. source_access * releases that source when materialize=false and returns a fresh snapshot * when true. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 0376ece2c..d18b3d910 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4673,6 +4673,21 @@ bool scr_dyn_iterator_step_done(const ScrDyn *value) { return ((ScrNativeIterator *)value->v.handle.ptr)->step_done; } +double scr_dyn_collection_iterator_index(const ScrDyn *value) { + ScrNativeIterator *iterator = value->v.handle.ptr; + ScrDyn *source = iterator->source; + if (!source) return -1; + ScrMap *map = source->v.handle.ptr; + iterator->index = scr_map_iter_skip(map, iterator->index); + if (iterator->index >= scr_map_iter_count(map)) { + iterator->source = NULL; + scr_map_iter_exit(map); + scr_dyn_release(source); + return -1; + } + return iterator->index++; +} + static int scr_ih_method_index(const char *name, size_t len); static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc); static ScrDyn *scr_ih_method_function(int index); diff --git a/tests/corpus/6073-stored-collection-helpers.ts b/tests/corpus/6073-stored-collection-helpers.ts new file mode 100644 index 000000000..ed739eb85 --- /dev/null +++ b/tests/corpus/6073-stored-collection-helpers.ts @@ -0,0 +1,60 @@ +// Iterator helper objects over Map and Set iterators that are stored and +// consumed later: the helpers observe the collection live, and abandoned +// or returned helpers leave it usable. +const set = new Set([1, 2, 3]); +const doubled = set.values().map((x) => x * 2); +set.add(4); +set.delete(1); +console.log(doubled.next(), [...doubled]); + +const map = new Map([ + ["a", 1], + ["b", 2], + ["c", 3], +]); +const large = map.entries().filter(([, v]) => v > 1); +map.set("d", 4); +map.delete("b"); +console.log([...large].map(([k, v]) => `${k}=${v}`).join(" ")); +const keys = map.keys().map((k, i) => k.toUpperCase() + i); +console.log(keys.next(), keys.return!(), keys.next()); +const rest = map.values().drop(2); +for (const v of rest) console.log("value", v); +const firstTwo = map.keys().take(2); +console.log([...firstTwo], [...firstTwo]); + +// A stage on a stage, and Iterator.from over a Set and a Map. +const chained = set.values().filter((x) => x % 2 === 0).map((x) => `n${x}`); +console.log(chained.toArray()); +const fromSet = Iterator.from(set).map((x) => x + 0.5); +set.add(9); +console.log([...fromSet]); +console.log(Iterator.from(map).map(([k, v]) => k + v).toArray()); + +// A helper left mid-walk does not stop later changes or walks. +const abandoned = set.values().map((x) => x); +abandoned.next(); +for (let i = 0; i < 100; i++) { + set.add(100 + i); + set.delete(100 + i); +} +console.log(set.size, set.values().take(3).toArray(), [...set].length); +const returned = map.values().map((v) => v * 10); +returned.next(); +returned.return!(); +for (let i = 0; i < 50; i++) map.set(`t${i}`, i); +for (let i = 0; i < 50; i++) map.delete(`t${i}`); +console.log(map.size, [...map.keys()].join(",")); + +// Records and strings as elements. +interface Row { + id: number; + tag: string; +} +const rows = new Map([ + [1, { id: 1, tag: "x" }], + [2, { id: 2, tag: "y" }], +]); +const tags = rows.values().map((r) => r.tag.repeat(r.id)); +rows.set(3, { id: 3, tag: "z" }); +console.log([...tags], rows.entries().find(([, r]) => r.tag === "y")?.[0]); diff --git a/tests/corpus/6074-stored-collection-helpers-js.js b/tests/corpus/6074-stored-collection-helpers-js.js new file mode 100644 index 000000000..3a314a3c1 --- /dev/null +++ b/tests/corpus/6074-stored-collection-helpers-js.js @@ -0,0 +1,57 @@ +// The JavaScript form: iterator helper objects over Map and Set iterators +// that are stored and consumed later: the helpers observe the collection live, and abandoned +// or returned helpers leave it usable. +const set = new Set([1, 2, 3]); +const doubled = set.values().map((x) => x * 2); +set.add(4); +set.delete(1); +console.log(doubled.next(), [...doubled]); + +const map = new Map ([ + ["a", 1], + ["b", 2], + ["c", 3], +]); +const large = map.entries().filter(([, v]) => v > 1); +map.set("d", 4); +map.delete("b"); +console.log([...large].map(([k, v]) => `${k}=${v}`).join(" ")); +const keys = map.keys().map((k, i) => k.toUpperCase() + i); +console.log(keys.next(), keys.return(), keys.next()); +const rest = map.values().drop(2); +for (const v of rest) console.log("value", v); +const firstTwo = map.keys().take(2); +console.log([...firstTwo], [...firstTwo]); + +// A stage on a stage, and Iterator.from over a Set and a Map. +const chained = set.values().filter((x) => x % 2 === 0).map((x) => `n${x}`); +console.log(chained.toArray()); +const fromSet = Iterator.from(set).map((x) => x + 0.5); +set.add(9); +console.log([...fromSet]); +console.log(Iterator.from(map).map(([k, v]) => k + v).toArray()); + +// A helper left mid-walk does not stop later changes or walks. +const abandoned = set.values().map((x) => x); +abandoned.next(); +for (let i = 0; i < 100; i++) { + set.add(100 + i); + set.delete(100 + i); +} +console.log(set.size, set.values().take(3).toArray(), [...set].length); +const returned = map.values().map((v) => v * 10); +returned.next(); +returned.return(); +for (let i = 0; i < 50; i++) map.set(`t${i}`, i); +for (let i = 0; i < 50; i++) map.delete(`t${i}`); +console.log(map.size, [...map.keys()].join(",")); + +// Records and strings as elements. + +const rows = new Map ([ + [1, { id: 1, tag: "x" }], + [2, { id: 2, tag: "y" }], +]); +const tags = rows.values().map((r) => r.tag.repeat(r.id)); +rows.set(3, { id: 3, tag: "z" }); +console.log([...tags], rows.entries().find(([, r]) => r.tag === "y")?.[0]); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 98055a9f1..1793158d5 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2239,6 +2239,8 @@ "6070-iterator-helpers-untyped.js": 4050, "6071-iterator-sources.ts": 5150, "6072-iterator-sources-js.js": 5350, + "6073-stored-collection-helpers.ts": 4000, + "6074-stored-collection-helpers-js.js": 4100, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5314,6 +5316,8 @@ "6070-iterator-helpers-untyped.js": 6900, "6071-iterator-sources.ts": 5150, "6072-iterator-sources-js.js": 5350, + "6073-stored-collection-helpers.ts": 4150, + "6074-stored-collection-helpers-js.js": 3600, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From 9e9d467c69c9bffc2eaab20c3d4027fbe4b81554 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:08:12 -0500 Subject: [PATCH 102/120] Look up runtime features in sorted sets instead of comparison chains scanRuntimeFeatures matched libCall names with long `fn === "a" || fn === "b"` chains. Adding a case at the end edited the previous last line to add `||`, so changes that added cases to the same chain always conflicted. Each chain is now a sorted module-level set (typed as IrLibFn, so a misspelled name fails to compile), with prefix lists where a chain also matched by prefix. The intrinsic method chains become sets too. The features are unchanged: for every libCall in LIB_FN_SIGS and each string, array and typed-array method the scan tests, the old and new moduleRuntimeFeatures agree. --- packages/compiler/src/ir/runtime-features.ts | 321 ++++++++++--------- 1 file changed, 175 insertions(+), 146 deletions(-) diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 04443ee18..74bb08b46 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -123,6 +123,166 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([ "util.debuglog", ]); +/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */ +const REGEX_LIB_FNS: ReadonlySet = new Set([ + "dyn.nativeRegexIs", + "fs.callbackGlob", + "fs.globSync", + "fs.globTypesSync", + "fsp.globError", + "fsp.globStrings", + "fsp.globTypes", + "path.matchesGlob", + "path.win32MatchesGlob", + "regexp.escape", + "rl.cursorPos", + "rl.keypressStdin", + "rl.keypressStream", + "stdin.onKeypress", + "util.stripVTControlCharacters", +]); + +/** TextDecoder entry points over the legacy encodings. */ +const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet = new Set([ + "text.decodeLegacy", + "text.decodeLegacyOptions", + "text.decodeStream", +]); + +/** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */ +const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([ + "http.clientSignal", + "rl.createSignal", + "rl.questionAsyncSignal", + "sp.finishedOptsSignal", + "sp.pipelineOptsSignal", + "stream.addAbortSignal", + "stream.addAbortSignalFn", + "stream.finishedOptsSignal", + "tp.schedulerWaitSignal", + "tp.setImmediateSignal", + "tp.setTimeoutSignal", +]); + +/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */ +const STREAM_LIB_FNS: ReadonlySet = new Set([ + "console.bind", + "crypto.hashStream", + "crypto.hashStreamPiped", + "fileHandle.createReadStream", + "fileHandle.createWriteStream", + "fs.createReadStream", + "fs.createWriteStream", + "sp.finished", + "sp.pipeline", + "sp.pipelineStages", + "stream.addAbortSignal", + "stream.addAbortSignalFn", + "stream.destroy", + "stream.destroyErr", + "stream.errored", + "stream.prop", + "stream.workerStdio", +]); + +/** libCall name prefixes that link the stream unit. */ +const STREAM_LIB_PREFIXES: readonly string[] = [ + "duplex.", + "fs.fsStream", + "passthrough.", + "readable.", + "sc.", + "sp.finishedOpts", + "sp.pipelineOpts", + "stream.finishedOpts", + "stream.pipelineStages", + "stream.set", + "transform.", + "writable.", + "zlib.stream", +]; + +/** The Readable operators and Readable.from over a compiled generator or a checked-dynamic iterator (scr_stream_ops.c), which await through the dyn-async unit (with READABLE_DYN_ASYNC_PREFIXES). */ +const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ + "readable.fromAsyncGen", + "readable.fromAsyncIter", + "readable.fromDone", + "readable.fromDyn", + "readable.fromIter", +]); + +/** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */ +const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "readable.op"]; + +/** Entry points that link the dyn-invoke unit. */ +const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ + "dyn.arrayProtoCall", + "dyn.arrayPrototype", + "dyn.builtinMethod", + "dyn.defineProperty", + "dyn.defineProps", + "dyn.definePrototypeProps", + "dyn.functionApply", + "dyn.objCreateWithProperties", +]); + +/** Entry points that link the child-process unit (with CHILD_PROCESS_LIB_PREFIXES): the fork IPC channel on the process object. */ +const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([ + "process.connected", + "process.disconnect", + "process.forkTarget", + "process.onDisconnect", + "process.onMessage", + "process.send", + "process.sendCb", +]); + +/** libCall name prefixes that link the child-process unit. */ +const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."]; + +/** Entry points that link the symbol unit, besides the sym. and disposable. families. */ +const SYMBOL_LIB_FNS: ReadonlySet = new Set([ + "dyn.usingDispose", + "dyn.usingMethod", + "util.isDeepStrictEqual", +]); + +/** Entry points that link the querystring unit. */ +const QS_LIB_FNS: ReadonlySet = new Set(["qs.parse", "qs.stringify", "qs.unescape"]); + +/** Entry points that link the parseArgs unit. */ +const PARSE_ARGS_LIB_FNS: ReadonlySet = new Set([ + "util.isDeepStrictEqual", + "util.parseArgs", + "util.styleText", +]); + +/** String methods whose Unicode tables live in the regex unit's libunicode. */ +const CASE_MAPPING_STR_METHODS: ReadonlySet = new Set([ + "normalize", + "toLowerCase", + "toUpperCase", +]); + +/** Array methods that link the copying unit. */ +const COPYING_ARR_METHODS: ReadonlySet = new Set([ + "toReversed", + "toSpliced", + "with", + "withUndefined", +]); + +/** Typed-array methods that link the copying unit. */ +const COPYING_BYTES_METHODS: ReadonlySet = new Set([ + "join", + "toArray", + "toReversed", + "with", +]); + +const startsWithAny = (fn: string, prefixes: readonly string[]): boolean => + prefixes.some((prefix) => fn.startsWith(prefix)); + /** Inspect types and expressions together in one traversal. This is a fresh * snapshot: callers may mutate an IR module between compilation passes. */ /** The checked-dynamic undefined value (dynUndefinedExpr). */ @@ -239,126 +399,31 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run const expr = (node: IrExpr): boolean => { if (node.kind === "libCall") { const fn = node.fn; - if ( - fn === "regexp.escape" || - fn === "dyn.nativeRegexIs" || - fn === "util.stripVTControlCharacters" || - fn === "path.matchesGlob" || - fn === "path.win32MatchesGlob" || - // fs.glob's walk compiles its patterns with the glob matcher. - fn === "fs.globSync" || - fn === "fs.globTypesSync" || - fn === "fs.callbackGlob" || - fn === "fsp.globStrings" || - fn === "fsp.globTypes" || - fn === "fsp.globError" - ) - features.regex = true; - if ( - fn === "text.decodeLegacy" || - fn === "text.decodeLegacyOptions" || - fn === "text.decodeStream" - ) - features.legacyTextDecoder = true; + if (REGEX_LIB_FNS.has(fn)) features.regex = true; + if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; - // Signal-taking readline, stream and timers/promises entry points - // register abort listeners. - if ( - fn === "rl.createSignal" || - fn === "rl.questionAsyncSignal" || - fn === "http.clientSignal" || - fn === "tp.setTimeoutSignal" || - fn === "tp.schedulerWaitSignal" || - fn === "tp.setImmediateSignal" || - fn === "stream.addAbortSignal" || - fn === "stream.addAbortSignalFn" || - fn === "sp.finishedOptsSignal" || - fn === "sp.pipelineOptsSignal" || - fn === "stream.finishedOptsSignal" - ) { - features.eventTarget = true; - } + if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true; if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; - // readline's terminal unit (scr_readline_tty.c) measures with the - // regex unit's stripVTControlCharacters; with a completer it prints - // completion errors through the inspect unit. - if ( - fn === "rl.cursorPos" || - fn === "rl.keypressStdin" || - fn === "rl.keypressStream" || - fn === "stdin.onKeypress" - ) - features.regex = true; + // With a completer, readline's terminal unit prints completion errors + // through the inspect unit. if (fn === "rl.terminal") { features.regex = true; if (!isDynUndefined(node.args[3]!)) features.inspect = true; } if (fn.startsWith("emitter.")) features.emitter = true; - if ( - fn.startsWith("readable.") || - fn.startsWith("writable.") || - fn.startsWith("duplex.") || - fn.startsWith("transform.") || - fn.startsWith("passthrough.") || - fn.startsWith("sc.") || - fn.startsWith("stream.set") || - fn === "stream.destroy" || - fn === "stream.destroyErr" || - fn === "stream.prop" || - fn === "stream.workerStdio" || - fn === "stream.errored" || - fn === "sp.finished" || - fn === "sp.pipeline" || - fn === "sp.pipelineStages" || - fn.startsWith("stream.pipelineStages") || - fn === "console.bind" || - fn === "crypto.hashStream" || - fn === "crypto.hashStreamPiped" || - fn === "stream.addAbortSignal" || - fn === "stream.addAbortSignalFn" || - fn.startsWith("sp.finishedOpts") || - fn.startsWith("sp.pipelineOpts") || - fn.startsWith("stream.finishedOpts") || - fn.startsWith("zlib.stream") || - fn === "fs.createReadStream" || - fn === "fs.createWriteStream" || - fn === "fileHandle.createReadStream" || - fn === "fileHandle.createWriteStream" || - fn.startsWith("fs.fsStream") - ) { + if (STREAM_LIB_FNS.has(fn) || startsWithAny(fn, STREAM_LIB_PREFIXES)) { // the stream unit emits through the emitter unit, also when the // program constructs no stream (a pipeline of functions) features.stream = true; features.emitter = true; } if (fn.startsWith("zlib.")) features.zlib = true; - // The Readable operators and Readable.from over a compiled generator - // or a checked-dynamic iterator (scr_stream_ops.c) await through the - // dyn-async unit. - if ( - fn.startsWith("readable.op") || - fn.startsWith("readable.fromGen") || - fn === "readable.fromDyn" || - fn === "readable.fromIter" || - fn === "readable.fromAsyncIter" || - fn === "readable.fromAsyncGen" || - fn === "readable.fromDone" - ) + if (READABLE_DYN_ASYNC_LIB_FNS.has(fn) || startsWithAny(fn, READABLE_DYN_ASYNC_PREFIXES)) features.dynAsync = true; if (fn.startsWith("dc.")) features.dc = true; if (fn.startsWith("assert.")) features.assert = true; - if ( - fn === "dyn.defineProps" || - fn === "dyn.definePrototypeProps" || - fn === "dyn.defineProperty" || - fn === "dyn.objCreateWithProperties" || - fn === "dyn.arrayProtoCall" || - fn === "dyn.arrayPrototype" || - fn === "dyn.functionApply" || - fn === "dyn.builtinMethod" - ) - features.dynInvoke = true; + if (DYN_INVOKE_LIB_FNS.has(fn)) features.dynInvoke = true; if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; if ( fn.startsWith("insp.") || @@ -367,19 +432,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run fn === "util.styleText" ) features.inspect = true; - if ( - fn.startsWith("cp.") || - fn.startsWith("child.") || - fn.startsWith("writer.") || - fn.startsWith("spawnRes.") || - fn === "process.forkTarget" || - fn === "process.connected" || - fn === "process.send" || - fn === "process.sendCb" || - fn === "process.disconnect" || - fn === "process.onMessage" || - fn === "process.onDisconnect" - ) + if (CHILD_PROCESS_LIB_FNS.has(fn) || startsWithAny(fn, CHILD_PROCESS_LIB_PREFIXES)) features.childProcess = true; if (fn.startsWith("net.")) features.net = true; if (fn.startsWith("http.")) { @@ -392,18 +445,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.tls = true; } if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true; - if ( - fn.startsWith("sym.") || - fn === "util.isDeepStrictEqual" || - fn === "dyn.usingMethod" || - fn === "dyn.usingDispose" || - fn.startsWith("disposable.") - ) + if (fn.startsWith("sym.") || fn.startsWith("disposable.") || SYMBOL_LIB_FNS.has(fn)) features.symbol = true; if (fn === "process.hrtimeValue") features.bigint = true; if (fn.startsWith("bigint.")) features.bigint = true; if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true; - if (fn === "qs.parse" || fn === "qs.stringify" || fn === "qs.unescape") features.qs = true; + if (QS_LIB_FNS.has(fn)) features.qs = true; // scr_url_legacy.c lowercases and normalizes hostnames (the regex // unit's libunicode), parses and stringifies queries, and emits its // deprecation warnings through the dyn-async unit. @@ -412,8 +459,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.qs = true; features.dynAsync = true; } - if (fn === "util.parseArgs" || fn === "util.isDeepStrictEqual" || fn === "util.styleText") - features.parseArgs = true; + if (PARSE_ARGS_LIB_FNS.has(fn)) features.parseArgs = true; if (fn === "util.styleText") features.processEvents = true; // scr_util_callable.c: dyn wrappers over the descriptor, inspect, // regex (NODE_DEBUG matching) and terminal-color units. @@ -450,30 +496,13 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.regex = true; break; case "strIntrinsic": - if ( - node.method === "toLowerCase" || - node.method === "toUpperCase" || - node.method === "normalize" - ) - features.regex = true; + if (CASE_MAPPING_STR_METHODS.has(node.method)) features.regex = true; break; case "arrIntrinsic": - if ( - node.method === "toReversed" || - node.method === "toSpliced" || - node.method === "with" || - node.method === "withUndefined" - ) - features.copying = true; + if (COPYING_ARR_METHODS.has(node.method)) features.copying = true; break; case "bytesIntrinsic": - if ( - node.method === "toReversed" || - node.method === "with" || - node.method === "join" || - node.method === "toArray" - ) - features.copying = true; + if (COPYING_BYTES_METHODS.has(node.method)) features.copying = true; break; case "dynInvoke": features.dynInvoke = true; From 86e8a09d86dda51640ca8952fcb3c57696f70dd5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:14:59 -0500 Subject: [PATCH 103/120] Sort ShapeHost's members and the lowerer's mode options by name Changes that add a backend host hook or a lowering option appended it at the end of ShapeHost, LowererMode and the Lowerer constructor's option assignments, so concurrent additions edited the same lines. ShapeHost's and LowererMode's members are now sorted by name with their comments, and the constructor assigns the plain mode options in name order, followed by the values derived from them in their previous order. Type-only and reordering changes: the emitted LLVM is unchanged. --- packages/compiler/src/backend/llvm/shapes.ts | 70 +++++++++---------- .../compiler/src/frontend/lowering/lowerer.ts | 65 ++++++++--------- 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 951ed61c9..d49df20b2 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -61,64 +61,64 @@ import { LlvmUnsupportedError } from "./unsupported.js"; /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { - declare(decl: string): void; /** The hierarchy root of a class (a program class or a runtime base), * or undefined for classes outside the class graph. */ classRootOf?(className: string): string | undefined; - /** Request the shared OOM abort helper (@sc_oom) — emitted once. */ - needOom(): void; + /** Byte offset from an object pointer back to ScrCycHdr.buf. */ + readonly cycleBufOffset: number; + declare(decl: string): void; + /** Inline the small-object allocator's fast paths (llvm/alloc.ts). */ + readonly inlineAlloc?: boolean; + /** Record and class fields that store a scalar union's tag and bits + * inline (inline-unions.ts): they own nothing. */ + readonly inlineUnions: InlineUnionFields; /** The immutable ScrMapOps descriptor (scr_map.h) of a Map or Set type: * a module constant holding the LLVM operands { key retain, key release, * key trace, value retain, value release, value trace, union key kinds }, * or `null` when every operand is null. */ mapOps(operands: readonly string[]): string; - /** The target C ABI's `size_t` / refcount integer type. */ - readonly sizeType: "i32" | "i64"; - /** Byte offset from an object pointer back to ScrCycHdr.buf. */ - readonly cycleBufOffset: number; - readonly tracedShapes: Set; - readonly tracedUnions: Set; + /** Request the shared OOM abort helper (@sc_oom) — emitted once. */ + needOom(): void; + /** The next collector statistics index for an emitted ScrCycType + * descriptor (cycTypeDescriptor), counting from FIRST_EMITTED_CYC_KIND + * in emission order. + * Absent: every descriptor gets 0, which keeps no statistics. */ + nextCycKind?(): number; /** Unions represented as a nullable arm pointer (nullable-unions.ts): * their RC and trace entry points are the arm's NULL-tolerant ones. */ readonly nullableUnions: NullableUnions; - /** Record and class fields that store a scalar union's tag and bits - * inline (inline-unions.ts): they own nothing. */ - readonly inlineUnions: InlineUnionFields; - /** The zeroed `%ScrUnion` an inline store reads for a NULL dummy. */ - unionNoneRef(): string; - readonly recordsById: Map; - readonly recordCloneShapes: ReadonlySet; - /** The program constructs lifetime regions: the ProgramLifetime classes - * allocate in regions and count references in their region chunk instead - * of being empty (llvm/regions.ts). */ - regions?: RegionHooks | null; - /** The alias tag of a runtime word that lifetime-region code reads and - * writes outside every field: the chunk count, the chunk owner, the class - * cursors and scr_region_counting (", !tbaa !N", or "" in debug builds). */ - regionAliasTag?(word: "count" | "owner" | "cursor" | "counting"): string; /** Emit the live-object audit notes (`scr_obj_alloc_note` / * `scr_obj_free_note`) in emitted new/free helpers. Only runtimes built * with SCR_RC_AUDIT (the sanitized lane) count them; release and dev * runtime packs define them empty, so plain builds omit the calls. * Absent means emit. */ readonly objectAudit?: boolean; - /** Inline the small-object allocator's fast paths (llvm/alloc.ts). */ - readonly inlineAlloc?: boolean; - /** Worker executables: the inline allocator state is thread-local. */ - readonly threadLocalAlloc?: boolean; /** Inline RC fast-path helpers requested so far (":"); the * emitter defines each once through {@link emitInlineRcHelpers}. Null * keeps every retain/release a runtime call. */ readonly rcHelpers: Set | null; - /** Which fast paths are inlined (see {@link InlineRcMode}). */ - readonly rcMode?: InlineRcMode; /** Define the fast-path helpers `noinline` (see {@link inlineRcOption}). */ readonly rcHelpersNoinline?: boolean; - /** The next collector statistics index for an emitted ScrCycType - * descriptor (cycTypeDescriptor), counting from FIRST_EMITTED_CYC_KIND - * in emission order. - * Absent: every descriptor gets 0, which keeps no statistics. */ - nextCycKind?(): number; + /** Which fast paths are inlined (see {@link InlineRcMode}). */ + readonly rcMode?: InlineRcMode; + readonly recordCloneShapes: ReadonlySet; + readonly recordsById: Map; + /** The alias tag of a runtime word that lifetime-region code reads and + * writes outside every field: the chunk count, the chunk owner, the class + * cursors and scr_region_counting (", !tbaa !N", or "" in debug builds). */ + regionAliasTag?(word: "count" | "owner" | "cursor" | "counting"): string; + /** The program constructs lifetime regions: the ProgramLifetime classes + * allocate in regions and count references in their region chunk instead + * of being empty (llvm/regions.ts). */ + regions?: RegionHooks | null; + /** The target C ABI's `size_t` / refcount integer type. */ + readonly sizeType: "i32" | "i64"; + /** Worker executables: the inline allocator state is thread-local. */ + readonly threadLocalAlloc?: boolean; + readonly tracedShapes: Set; + readonly tracedUnions: Set; + /** The zeroed `%ScrUnion` an inline store reads for a NULL dummy. */ + unionNoneRef(): string; } /** What class emission asks of lifetime regions (LifetimeRegions in diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a77142cb0..2fb266be0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -845,7 +845,25 @@ type RuntimeFenceTarget = /** The Lowerer's pass configuration (see lowerToIr). */ export interface LowererMode { + /** Symbols whose deferred diagnostics reachable emit already flushed — + * the remainder must not report them a second time. */ + alreadyFlushed?: ReadonlySet; + /** LowerOptions.externalTypes, threaded through every lowering pass. */ + externalTypes?: ReadonlyMap; + /** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */ + externalTypeSpecifiersByFile?: ReadonlyMap; + /** Program-validated ambient declaration symbols for each FFI name. + * Undefined in discovery's legacy call-local validation path. */ + ffiBindingSymbols?: ReadonlyMap>; + /** The build's outbound native FFI declarations. */ + ffiImports?: readonly IrFfiImport[]; + /** Statically resolved child_process.fork program roots, in stable target-id order. */ + forkTargets?: readonly ts.SourceFile[]; frontendServices?: FrontendServices | undefined; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; + /** LowerOptions.libraryCallbacks (see there). */ + libraryCallbacks?: boolean; projectCheckJs?: boolean; /** Names of bodies a prior reachability pass reached; null lowers everything. */ reachable?: ReadonlySet | null; @@ -853,35 +871,17 @@ export interface LowererMode { * always-reachable init bodies and module building, and report deferred * collection diagnostics nothing flushed. */ remainder?: boolean; - /** Symbols whose deferred diagnostics reachable emit already flushed — - * the remainder must not report them a second time. */ - alreadyFlushed?: ReadonlySet; - /** The build's target platform (LowerOptions.targetPlatform — lowerToIr - * passes it to every pass). Defaults to the host. */ - targetPlatform?: string; /** Node's startup refusal (preflight's resolution walk / named-import * link checks): %main opens with exactly this throw, before any * module init — Node refuses the whole graph before anything evaluates, * so nothing runs. */ startupCrash?: StartupCrash | null; - /** LoadResult.inlineRequireCycleCalls. */ - inlineRequireCycleCalls?: ReadonlySet | undefined; - /** The build's outbound native FFI declarations. */ - ffiImports?: readonly IrFfiImport[]; - /** LowerOptions.libraryCallbacks (see there). */ - libraryCallbacks?: boolean; - /** Statically resolved child_process.fork program roots, in stable target-id order. */ - forkTargets?: readonly ts.SourceFile[]; - workerTargets?: readonly ts.SourceFile[]; - /** Program-validated ambient declaration symbols for each FFI name. - * Undefined in discovery's legacy call-local validation path. */ - ffiBindingSymbols?: ReadonlyMap>; - /** LowerOptions.externalTypes, threaded through every lowering pass. */ - externalTypes?: ReadonlyMap; - /** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */ - externalTypeSpecifiersByFile?: ReadonlyMap; + /** The build's target platform (LowerOptions.targetPlatform — lowerToIr + * passes it to every pass). Defaults to the host. */ + targetPlatform?: string; /** LowerOptions.tierMode, shared by every pass. */ tierMode?: TierMode; + workerTargets?: readonly ts.SourceFile[]; } function directExternalTypeSpecifiersByFile( @@ -2968,28 +2968,29 @@ export class Lowerer { readonly moduleOrder: ts.SourceFile[], mode: LowererMode = {}, ) { + // The mode's options, sorted by name; the values derived from them follow. + this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); + this.externalTypes = mode.externalTypes ?? new Map(); + this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null; + this.ffiImports = mode.ffiImports ?? []; + this.forkTargets = mode.forkTargets ?? []; this.frontendServices = mode.frontendServices; + this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); + this.libraryCallbacks = mode.libraryCallbacks ?? false; + this.projectCheckJs = mode.projectCheckJs ?? false; this.reachable = mode.reachable ?? null; this.remainder = mode.remainder ?? false; - this.tierMode = mode.tierMode ?? "auto"; - this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); - this.targetPlatform = mode.targetPlatform ?? process.platform; this.startupCrash = mode.startupCrash ?? null; - this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); - this.projectCheckJs = mode.projectCheckJs ?? false; - this.ffiImports = mode.ffiImports ?? []; - this.libraryCallbacks = mode.libraryCallbacks ?? false; + this.targetPlatform = mode.targetPlatform ?? process.platform; + this.tierMode = mode.tierMode ?? "auto"; this.workerTargets = mode.workerTargets ?? []; this.workerTargets.forEach((sf, id) => { this.workerTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id); }); - this.forkTargets = mode.forkTargets ?? []; this.forkTargets.forEach((sf, id) => { this.forkTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id); }); this.ffiImportsByName = new Map(this.ffiImports.map((entry) => [entry.name, entry])); - this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null; - this.externalTypes = mode.externalTypes ?? new Map(); this.externalTypeSpecifiersByFile = mode.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(this.externalTypes); this.checker = program.getTypeChecker(); From b1395256d29f4c74143a5b6aa12c5e4922a71b6f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:17:31 -0500 Subject: [PATCH 104/120] Initialize the asyncContext runtime feature in declaration order The runtime feature scanner's result is serialized as JSON, and the self-hosted scanner lays its record out in the interface's field order. asyncContext was initialized before dynAsync, while the interface declares it after, so the self-hosted scanner's JSON put the key elsewhere than the TypeScript one. The initializer now follows the interface order. --- packages/compiler/src/ir/runtime-features.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index d50d173b8..e3a234260 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -218,12 +218,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run dc: false, assert: false, dynInvoke: mod.workers === true, - asyncContext: false, dynAsync: mod.workers === true || mod.functions.some( (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn", ), + asyncContext: false, inspect: false, childProcess: false, net: false, From 60535fd09ceb77ce992e9cf78362b8d5afaf52f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:21:03 -0500 Subject: [PATCH 105/120] Accept test and describe calls without a name, and settle skip and todo as Node does node:test changes, matching Node 24.22: - test, it, describe, suite, their skip/todo/only twins and t.test take Node's (name?, options?, fn?) forms: test(fn), test(options, fn), test(options) and test() compile, as does describe(fn) and describe(options, fn). The fallback declarations add the overloads. - A missing or empty name is the function's name, or "", as Node's Test constructor completes it. The name is the function expression's or declaration's own name, or the binding name of a const arrow or anonymous function; a name that is empty at run time takes the same default. - With both skip and todo, the test is skipped, and the directive message is the skip string, else the todo string. The skip and todo method twins override the option of the same name, so test.skip("x", { skip: "msg" }) reports a bare SKIP and it.skip("x", { todo: "msg" }) reports "msg". Before, the last option set the mode. The limitations page notes that a thrown object or array that is not an Error reports [object Object] in the failing section. The test-name-forms fixture compares the names, directives and counts with Node, plain and with SCRIPTC_SAN=1. --- .../limitations/builtin-apis/test-runner.mdx | 1 + .../ambient/scriptc-node-fallback.d.ts | 53 +++-- .../src/frontend/lowering/lower-test.ts | 218 ++++++++++++++---- .../node-test/cases/test-name-forms.ts | 46 ++++ 4 files changed, 256 insertions(+), 62 deletions(-) create mode 100644 tests/fixtures/node-test/cases/test-name-forms.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 77d3d9a84..5e368cc03 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -4,3 +4,4 @@ - **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`. - **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named ``. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged. - **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. +- **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 9655b7580..3e3f66a2a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3405,8 +3405,10 @@ declare module "node:test" { readonly tags: readonly string[]; readonly assert: TestContextAssert; readonly mock: MockTracker; - test(name: string, fn?: TestFn): Promise; - test(name: string, options: TestOptions, fn?: TestFn): Promise; + test(name?: string, fn?: TestFn): Promise; + test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + test(options?: TestOptions, fn?: TestFn): Promise; + test(fn?: TestFn): Promise; skip(message?: string): void; todo(message?: string): void; diagnostic(message: string): void; @@ -3436,23 +3438,44 @@ declare module "node:test" { match(value: string, regExp: RegExp, message?: string): void; doesNotMatch(value: string, regExp: RegExp, message?: string): void; } - function test(name: string, fn?: TestFn): Promise; - function test(name: string, options: TestOptions, fn?: TestFn): Promise; + /* The name, options and function are each optional, as in Node: a test + * without a name takes the function's name, or "". */ + function test(name?: string, fn?: TestFn): Promise; + function test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function test(options?: TestOptions, fn?: TestFn): Promise; + function test(fn?: TestFn): Promise; namespace test { - function skip(name: string, fn?: TestFn): Promise; - function skip(name: string, options: TestOptions, fn?: TestFn): Promise; - function todo(name: string, fn?: TestFn): Promise; - function todo(name: string, options: TestOptions, fn?: TestFn): Promise; - function only(name: string, fn?: TestFn): Promise; - function only(name: string, options: TestOptions, fn?: TestFn): Promise; + function skip(name?: string, fn?: TestFn): Promise; + function skip(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function skip(options?: TestOptions, fn?: TestFn): Promise; + function skip(fn?: TestFn): Promise; + function todo(name?: string, fn?: TestFn): Promise; + function todo(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function todo(options?: TestOptions, fn?: TestFn): Promise; + function todo(fn?: TestFn): Promise; + function only(name?: string, fn?: TestFn): Promise; + function only(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function only(options?: TestOptions, fn?: TestFn): Promise; + function only(fn?: TestFn): Promise; } const it: typeof test; - function describe(name: string, fn?: SuiteFn): void; - function describe(name: string, options: TestOptions, fn?: SuiteFn): void; + function describe(name?: string, fn?: SuiteFn): void; + function describe(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function describe(options?: TestOptions, fn?: SuiteFn): void; + function describe(fn?: SuiteFn): void; namespace describe { - function skip(name: string, fn?: SuiteFn): void; - function todo(name: string, fn?: SuiteFn): void; - function only(name: string, fn?: SuiteFn): void; + function skip(name?: string, fn?: SuiteFn): void; + function skip(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function skip(options?: TestOptions, fn?: SuiteFn): void; + function skip(fn?: SuiteFn): void; + function todo(name?: string, fn?: SuiteFn): void; + function todo(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function todo(options?: TestOptions, fn?: SuiteFn): void; + function todo(fn?: SuiteFn): void; + function only(name?: string, fn?: SuiteFn): void; + function only(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function only(options?: TestOptions, fn?: SuiteFn): void; + function only(fn?: SuiteFn): void; } const suite: typeof describe; function before(fn: HookFn): void; diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index e36518559..bb525e02d 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -90,12 +90,14 @@ function atStringOf(expr: ts.CallExpression): string { return `${sf.fileName}:${pos.line + 1}:${pos.character + 1}`; } -/** One parsed { skip?, todo?, only? } options literal. `mode` is the - * runtime literal (0 run / 1 skip / 2 todo), `msg` the directive message - * expression ("" = none). */ +/** One parsed { skip?, todo?, only? } options literal. `skip` and `todo` + * are whether each is set (a string or true), and `skipMsg` and `todoMsg` + * the string messages (directiveOf settles them). */ interface TestOptions { - mode: number; - msg: IrExpr | null; + skip: boolean; + skipMsg: IrExpr | null; + todo: boolean; + todoMsg: IrExpr | null; only: boolean; /** The { plan } count, -1 without one. */ plan: number; @@ -109,8 +111,10 @@ interface TestOptions { } const noOptions = (): TestOptions => ({ - mode: 0, - msg: null, + skip: false, + skipMsg: null, + todo: false, + todoMsg: null, only: false, plan: -1, timeout: null, @@ -118,6 +122,23 @@ const noOptions = (): TestOptions => ({ pre: [], }); +/** The runtime mode (0 run / 1 skip / 2 todo) and directive message of a + * test or suite, as Node's Test constructor settles them: the skip and todo + * method twins override the options of the same name, skip wins over + * todo, and the message is the skip string, else the todo string. */ +function directiveOf(opts: TestOptions, methodMode: number, loc: SrcLoc): [IrExpr, IrExpr] { + if (methodMode === 1) { + opts.skip = true; + opts.skipMsg = null; + } + if (methodMode === 2) { + opts.todo = true; + opts.todoMsg = null; + } + const mode = opts.skip ? 1 : opts.todo ? 2 : 0; + return [numLit(mode, loc), opts.skipMsg ?? opts.todoMsg ?? strLit("", loc)]; +} + /** Flags 8 and 16: a { timeout } and a { concurrency } are given. */ const optionFlags = (opts: TestOptions): number => (opts.only ? 4 : 0) | (opts.timeout ? 8 : 0) | (opts.concurrency ? 16 : 0); @@ -168,15 +189,33 @@ function lowerTestOptions( // A shorthand entry's value is the binding of the same name. const init: ts.Expression = ts.isPropertyAssignment(prop) ? prop.initializer : key; if (name === "skip" || name === "todo") { - const mode = name === "skip" ? 1 : 2; const t = lowerer.typeOf(init); if (t.isStringLiteralType() || lowerer.mapTypeOf(t)?.kind === "string") { - out.mode = mode; - out.msg = optionValue(lowerer, out, lowerer.lowerExprExpecting(init, STRING), locOf(init)); - } else if (init.kind === ts.SyntaxKind.TrueKeyword) { - out.mode = mode; - } else if (init.kind === ts.SyntaxKind.FalseKeyword) { - // explicit false: run normally + const msg = optionValue( + lowerer, + out, + lowerer.lowerExprExpecting(init, STRING), + locOf(init), + ); + if (name === "skip") { + out.skip = true; + out.skipMsg = msg; + } else { + out.todo = true; + out.todoMsg = msg; + } + } else if ( + init.kind === ts.SyntaxKind.TrueKeyword || + init.kind === ts.SyntaxKind.FalseKeyword + ) { + const on = init.kind === ts.SyntaxKind.TrueKeyword; + if (name === "skip") { + out.skip = on; + out.skipMsg = null; + } else { + out.todo = on; + out.todoMsg = null; + } } else { lowerer.noLowering( `${what} with a non-literal ${name} value`, @@ -284,6 +323,110 @@ function lowerBodyArg( /** The shared registration lowering behind test/it and their skip/todo/ * only method twins — and, with `sub`, behind t.test. Argument shapes: * (name), (name, fn), (name, options, fn). */ +/** The `name` a function value has in JavaScript, when the compiler can + * tell: "" for an anonymous function expression, the declared or binding + * name otherwise; null for other expressions. */ +function staticFunctionName(lowerer: Lowerer, node: ts.Expression): string | null { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (ts.isArrowFunction(node)) return ""; + if (ts.isFunctionExpression(node)) return node.name?.text ?? ""; + if (!ts.isIdentifier(node)) return null; + const symbol = lowerer.resolveValueSymbol(node); + if (!symbol) return null; + for (const decl of lowerer.checker.declarationsOf(symbol)) { + if (ts.isFunctionDeclaration(decl)) return decl.name?.text ?? "default"; + if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.initializer) { + let init = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isFunctionExpression(init) && init.name) return init.name.text; + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) return decl.name.text; + } + } + return null; +} + +/** Node's (name?, options?, fn?) argument forms (`Test#createSubtest`): a + * leading function is the body, a leading object the options, otherwise + * the first argument is the name and a following object the options. */ +function splitTestArgs( + lowerer: Lowerer, + expr: ts.CallExpression, + what: string, +): { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } { + const args = expr.arguments; + const isFn = (a: ts.Expression): boolean => + lowerer.checker.getCallSignatures(lowerer.typeOf(a)).length > 0; + const isOptions = (a: ts.Expression): boolean => ts.isObjectLiteralExpression(a); + const unsupported = (): never => + lowerer.noLowering( + `${what} with these arguments`, + expr, + "the supported forms are (name?, options?, fn?) with a string name, an options object literal, and a function", + ); + if (args.length > 3 || args.some(ts.isSpreadElement)) unsupported(); + const out: { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } = {}; + let i = 0; + const next = args[i]; + if (next !== undefined && !isFn(next) && !isOptions(next)) { + out.nameNode = next; + i++; + } + const options = args[i]; + if (options !== undefined && isOptions(options)) { + out.optionsNode = options; + i++; + } + const fn = args[i]; + if (fn !== undefined && isFn(fn)) { + out.fnNode = fn; + i++; + } + if (i < args.length) unsupported(); + return out; +} + +/** The test's name, as Node's Test constructor completes it: a missing or + * empty name takes the function's name, or "". */ +function lowerTestName( + lowerer: Lowerer, + expr: ts.CallExpression, + what: string, + nameNode: ts.Expression | undefined, + fnNode: ts.Expression | undefined, + loc: SrcLoc, +): IrExpr { + const fallback = (): string => { + const fnName = fnNode ? staticFunctionName(lowerer, fnNode) : ""; + if (fnName === null) { + lowerer.noLowering( + `${what} without a name, with a function whose name is not known at compile time`, + fnNode ?? expr, + "give the test a name, or pass a function expression or a declared function", + ); + } + return fnName || ""; + }; + if (!nameNode) return strLit(fallback(), loc); + const name = lowerer.lowerExprExpecting(nameNode, STRING); + if (name.kind === "strLit") return name.value === "" ? strLit(fallback(), loc) : name; + const slot = lowerer.declareHiddenLocal("%testName", STRING); + const value = varRef(slot.id, STRING, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: slot.id, init: name, loc }], + result: { + kind: "ternary", + cond: { kind: "strEq", negated: false, left: value, right: strLit("", loc), type: BOOL, loc }, + then: strLit(fallback(), loc), + else_: value, + type: STRING, + loc, + }, + type: STRING, + loc, + }; +} + function lowerRegistration( lowerer: Lowerer, expr: ts.CallExpression, @@ -293,21 +436,11 @@ function lowerRegistration( sub: IrExpr | null, // the lowered TestContext receiver (t.test) ): IrExpr { if (sub === null) requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; - if (args.length < 1 || args.length > 3) { - lowerer.noLowering( - `${what} with ${args.length} arguments`, - expr, - "the supported forms are (name), (name, fn), and (name, options, fn)", - ); - } - let name = lowerer.lowerExprExpecting(args[0]!, STRING); + const { nameNode, optionsNode, fnNode } = splitTestArgs(lowerer, expr, what); + let name = lowerTestName(lowerer, expr, what, nameNode, fnNode, loc); let opts = noOptions(); - let fnNode: ts.Expression | undefined; - if (args.length === 2) fnNode = args[1]; - if (args.length === 3) { - opts = lowerTestOptions(lowerer, args[1]!, what, false); - fnNode = args[2]; + if (optionsNode) { + opts = lowerTestOptions(lowerer, optionsNode, what, false); // The name runs before the option values. if (opts.pre.length > 0 && name.kind !== "strLit") { const slot = lowerer.declareHiddenLocal("%testName", STRING); @@ -315,11 +448,9 @@ function lowerRegistration( name = { kind: "varRef", localId: slot.id, type: STRING, loc }; } } - if (methodMode === 1 || methodMode === 2) opts.mode = methodMode; if (methodMode === 3) opts.only = true; const at = strLit(atStringOf(expr), loc); - const mode = numLit(opts.mode, loc); - const msg = opts.msg ?? strLit("", loc); + const [mode, msg] = directiveOf(opts, methodMode, loc); const flagBits = optionFlags(opts); const planTimeout = [ numLit(opts.plan, loc), @@ -387,27 +518,21 @@ function lowerSuite( methodMode: number, ): IrExpr { requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; - if (args.length < 2 || args.length > 3) { - lowerer.noLowering( - `${what} with ${args.length} arguments`, - expr, - "the supported forms are (name, fn) and (name, options, fn)", - ); - } - let name = lowerer.lowerExprExpecting(args[0]!, STRING); + const split = splitTestArgs(lowerer, expr, what); + let name = lowerTestName(lowerer, expr, what, split.nameNode, split.fnNode, loc); let opts = noOptions(); - let fnNode = args[1]!; - if (args.length === 3) { - opts = lowerTestOptions(lowerer, args[1]!, what, true); - fnNode = args[2]!; + if (split.optionsNode) { + opts = lowerTestOptions(lowerer, split.optionsNode, what, true); if (opts.pre.length > 0 && name.kind !== "strLit") { const slot = lowerer.declareHiddenLocal("%testName", STRING); opts.pre.unshift({ kind: "varDecl", localId: slot.id, init: name, loc }); name = { kind: "varRef", localId: slot.id, type: STRING, loc }; } } - if (methodMode === 1 || methodMode === 2) opts.mode = methodMode; + const fnNode = split.fnNode; + if (!fnNode) { + lowerer.noLowering(`${what} without a body`, expr, "give the suite a body function"); + } const cb = lowerer.lowerExpr(fnNode); // The body may take the SuiteContext (flag 2, as test bodies take theirs). if ( @@ -429,8 +554,7 @@ function lowerSuite( ); } const at = strLit(atStringOf(expr), loc); - const mode = numLit(opts.mode, loc); - const msg = opts.msg ?? strLit("", loc); + const [mode, msg] = directiveOf(opts, methodMode, loc); // describe.only and { only: true } mark the suite (flag 4, as for tests). const flags = numLit( optionFlags(opts) | (methodMode === 3 ? 4 : 0) | (cb.type.params.length === 1 ? 2 : 0), diff --git a/tests/fixtures/node-test/cases/test-name-forms.ts b/tests/fixtures/node-test/cases/test-name-forms.ts new file mode 100644 index 000000000..915d68a79 --- /dev/null +++ b/tests/fixtures/node-test/cases/test-name-forms.ts @@ -0,0 +1,46 @@ +// The (name?, options?, fn?) forms of test, it, describe and t.test, and +// Node's default names: a missing or empty name takes the function's name, +// or "". With both skip and todo, skip wins, in either order and +// against the todo method twins. +import { describe, it, test } from "node:test"; + +function declaredTest(): void {} +const boundArrow = (): void => {}; +const emptyName = process.argv.length > 99 ? "never" : ""; + +test(function namedExpression() {}); +test(() => {}); +test(declaredTest); +test(boundArrow); +test("", boundArrow); +test(emptyName, declaredTest); +test({ skip: "options first" }, () => {}); +test({ todo: true }); +test(); +test("name and options", { skip: true }); + +describe(function namedSuite() { + it(() => {}); + it({ todo: "later" }, function insideTodo() {}); +}); +describe({ skip: "whole suite" }, () => { + it("not run", () => {}); +}); + +test("subtests", async (t) => { + await t.test(function namedSub() {}); + await t.test(() => {}); + await t.test({ skip: true }, () => {}); +}); + +test("skip then todo", { skip: "skip message", todo: "todo message" }, () => {}); +test("todo then skip", { todo: "todo message", skip: true }, () => {}); +test.todo("todo twin with skip", { skip: true }, () => {}); +it.skip("skip twin with todo", { todo: true }, () => {}); +// The twins override the option of the same name, and the message is the +// skip string, else the todo string. +test.skip("skip twin with a skip message", { skip: "option message" }, () => {}); +it.todo("todo twin with a todo message", { todo: "option message" }, () => {}); +it.skip("skip twin with a todo message", { todo: "todo message" }, () => {}); +test.todo("todo twin with a skip message", { skip: "skip message" }, () => {}); +test("skip false with a todo message", { skip: false, todo: "todo message" }, () => {}); From 496cf577c73793a0ea9815e051067ae463d4401d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 01:10:39 -0500 Subject: [PATCH 106/120] Print iterators and iterator helper objects as Node does console.log and util.inspect of a generator object, an iterator helper, or an array or string iterator were refused, and printing a Map or Set iterator threw "util.inspect of a dynamic Array Iterator is not supported yet". Compiled generators now carry the tag their object prints with: "Generator" or "AsyncGenerator" by default, "Iterator Helper" for the helpers, "Array Iterator" and "String Iterator" for the iterators the compiler builds as generators. The spawn wrapper sets it through the new scr_gen_set_tag; an adapter over another generator copies that generator's tag (scr_gen_copy_tag), and generators that stand in for an object of another class, such as a Set iterator adapted into an iterator-typed slot, keep refusing to print. They print as Node's Object [] {} at any depth, and Object.prototype.toString answers [object ]. The runtime's native iterators print as Node does: Object [Array Iterator] {} and Object [String Iterator] {}, and for Map and Set iterators the entries the next steps would produce, without advancing, under [Set Iterator], [Set Entries], [Map Iterator] or [Map Entries], as [Object [Set Iterator]] past the depth limit and { } when none are left. Object.prototype.toString answers [object Set Iterator] and the other tags. --- .../builtin-apis/iterator-helpers.mdx | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 9 ++ .../lowering/containers/iterator-helpers.ts | 1 + .../frontend/lowering/iterator-adapters.ts | 22 +++-- .../src/frontend/lowering/lower-calls.ts | 11 ++- .../src/frontend/lowering/lower-inspect.ts | 12 +++ .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 9 ++ .../tests/corpus/6075-print-iterators.ts.json | 6 ++ .../corpus/6076-print-iterators-js.js.json | 6 ++ packages/runtime/src/scr_async.c | 9 ++ packages/runtime/src/scr_dyn.h | 6 ++ packages/runtime/src/scr_generator.h | 10 ++ packages/runtime/src/scr_inspect.c | 98 +++++++++++++++++++ packages/runtime/src/scr_json.c | 31 ++++++ tests/corpus/6075-print-iterators.ts | 19 ++++ tests/corpus/6076-print-iterators-js.js | 17 ++++ tests/harness/corpus-costs.json | 4 + 18 files changed, 264 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json create mode 100644 tests/corpus/6075-print-iterators.ts create mode 100644 tests/corpus/6076-print-iterators-js.js diff --git a/docs/content/limitations/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx index cb7cab1f4..b4c4d8200 100644 --- a/docs/content/limitations/builtin-apis/iterator-helpers.mdx +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -1 +1 @@ -- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects are unsupported. +- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported. diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 872153f40..48012704b 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -4644,6 +4644,9 @@ export class LlEmitter { // An iterator helper's return() before its first next() closes the // source generator in its first argument slot. const closesSource = fn.generator.closesSource === true; + if (fn.generator.tag !== undefined) this.declare(`declare void @scr_gen_set_tag(ptr, ptr)`); + if (fn.generator.inheritsTag === true) + this.declare(`declare void @scr_gen_copy_tag(ptr, ptr)`); if (closesSource) { const field = lifted ? 1 : 0; this.declare(`declare void @scr_gen_close(ptr)`); @@ -4676,6 +4679,12 @@ export class LlEmitter { settleAsync === null ? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, + ...(fn.generator.tag !== undefined + ? [` call void @scr_gen_set_tag(ptr %gg, ptr ${this.cstr(fn.generator.tag)})`] + : []), + ...(fn.generator.inheritsTag === true + ? [` call void @scr_gen_copy_tag(ptr %gg, ptr %a${lifted ? 1 : 0})`] + : []), ...(closesSource ? [ ` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`, diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 229999627..79b91af64 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -2259,6 +2259,7 @@ function buildHelperGenFn( yieldT: outT.yieldT, nextT: outT.nextT, resultType, + tag: "Iterator Helper", // A native Map or Set iterator has no return() to call. ...(source.kind === "generator" ? { closesSource: true as const } : {}), }, diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index f086417de..898c46f20 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -258,7 +258,7 @@ function dynIteratorAdapter( returnType: expected.retT, locals, body, - generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType }, + generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType, tag: "!" }, loc, }); } @@ -374,7 +374,12 @@ export function lowerCollectionIteratorValue( loc, }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { + yieldT: type.yieldT, + nextT: UNDEFINED_T, + resultType, + tag: "!", + }, loc, }); } @@ -484,7 +489,7 @@ export function lowerBytesIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "Array Iterator" }, loc, }); } @@ -590,7 +595,7 @@ export function lowerStringIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "String Iterator" }, loc, }); } @@ -751,7 +756,12 @@ function generatorRetypeAdapter( loc, }, ], - generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType: toResult }, + generator: { + yieldT: expected.yieldT, + nextT: expected.nextT, + resultType: toResult, + inheritsTag: true, + }, loc, }); } @@ -1022,7 +1032,7 @@ export function lowerArrayIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "Array Iterator" }, loc, }); } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 58cbb9543..b3cd7e39b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2800,7 +2800,8 @@ function lowerObjectPrototypeCall( const immediate = immediateObjectTagReceiver(lowerer, receiverNode); const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode); const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type); - if (tag === null && receiver.type.kind !== "dyn") { + // A generator object answers its tag through its checked capsule. + if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") { lowerer.noLowering( "Object.prototype.toString.call over a " + lowerer.fmt(receiver.type) + " receiver", receiverNode, @@ -2820,7 +2821,13 @@ function lowerObjectPrototypeCall( } const result: IrExpr = tag === null - ? { kind: "libCall", fn: "dyn.objectTag", args: [ref], type: STRING, loc } + ? { + kind: "libCall", + fn: "dyn.objectTag", + args: [ref.type.kind === "generator" ? lowerer.coerceToExpected(ref, DYN) : ref], + type: STRING, + loc, + } : { kind: "strLit", value: "[object " + tag + "]", type: STRING, loc }; return { kind: "seqExpr", stmts, result, type: STRING, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 1d67cef92..2551031eb 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -253,6 +253,10 @@ function inspectSupport( visiting.delete(t.className); return null; } + // Generator objects print through their checked capsule: Node's + // `Object [Generator] {}`, or the tag of the iterator they implement. + case "generator": + return null; default: return `'${lowerer.fmt(t)}' values have no inspect lowering`; } @@ -343,6 +347,14 @@ export function inspectExpr( return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc }; case "dyn": return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc }; + case "generator": + return { + kind: "libCall", + fn: "insp.dyn", + args: [lowerer.coerceToExpected(value, DYN), recurse, depth], + type: STRING, + loc, + }; case "object": if (eventTargetRootOf(lowerer.classes.get(t.className)) !== null) { return { diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index ab4b05c8b..117bba24a 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -143,6 +143,7 @@ const FUNCTION_FIELDS: Record>, Role> = { "captures[].localId": "other", "classCaptures[].name": "other", "classCaptures[].localId": "other", + "generator.tag": "other", }; const MODULE_FIELDS: Record>, Role> = { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 0aa510b8d..751c6c021 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1712,6 +1712,15 @@ export interface IrFunction { * iterator helper's source): `.return()` before the first `.next()` * closes it. */ closesSource?: true; + /** The @@toStringTag the object prints with (util.inspect, + * Object.prototype.toString) when it is not a generator object: + * "Iterator Helper", "Array Iterator", "String Iterator". A tag that + * starts with '!' marks a generator standing in for an object of + * another class, which those refuse to print. */ + tag?: string; + /** The first parameter is a generator this one adapts: it prints with + * that generator's tag. */ + inheritsTag?: true; }; /** Source function name for native error stack frames. */ sourceName?: string; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json new file mode 100644 index 000000000..04adc830d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6075-print-iterators.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json new file mode 100644 index 000000000..255e32494 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6076-print-iterators-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index b1ebfc6e9..124304970 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4986,6 +4986,7 @@ struct ScrGen { void (*drop_args)(void *); /* scr_gen_set_unstarted_return's hook; NULL for most generators. */ void (*unstarted_return)(void *); + const char *tag; /* scr_gen_set_tag's; NULL is the default */ bool is_async; void (*settle_async)(ScrGen *, ScrPromise *); ScrAsyncGenRequest *requests_head; @@ -5547,6 +5548,14 @@ void scr_gen_resume(ScrGen *g) { void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)) { g->unstarted_return = hook; } +void scr_gen_set_tag(ScrGen *g, const char *tag) { g->tag = tag; } + +void scr_gen_copy_tag(ScrGen *g, const ScrGen *from) { g->tag = from->tag; } + +const char *scr_gen_tag(const ScrGen *g) { + return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator"; +} + void scr_gen_close(ScrGen *g) { scr_gen_slot_reset(&g->ret); scr_gen_resume_return(g); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index e76563ab7..4938ee01d 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -706,6 +706,12 @@ bool scr_dyn_iterator_step_done(const ScrDyn *iterator); * next live entry index of its collection, or -1 once the collection is * exhausted (the iterator then ends its iteration). */ double scr_dyn_collection_iterator_index(const ScrDyn *iterator); +/* For util.inspect: a native iterator's kind (0 array or typed array, + * 1 string, 2 Map, 3 Set, 4 a handle's), its selection (0 values, 1 keys, + * 2 entries), its collection (NULL once exhausted, and for kinds other + * than 2 and 3) and the index its next step reads from. */ +void scr_dyn_native_iterator_state(const ScrDyn *iterator, unsigned *kind, unsigned *selection, + ScrMap **collection, double *index); /* The ordinary object snapshot plus a retained typed source. source_access * releases that source when materialize=false and returns a fresh snapshot * when true. */ diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index a92cd3222..76d152f8a 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -63,6 +63,16 @@ ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, /* Runs when return() reaches the generator before its first resume, with * the packed arguments still alive: an iterator helper closes its source. */ void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)); +/* The @@toStringTag that util.inspect and Object.prototype.toString print + * for the generator object: "Generator" (or "AsyncGenerator") unless set, + * as the compiler does for iterator helpers ("Iterator Helper") and for + * the array and string iterators it builds as generators. A tag that + * starts with '!' marks a generator standing in for an object of another + * class, which these refuse to print. */ +void scr_gen_set_tag(ScrGen *g, const char *tag); +/* An adapter generator prints with the tag of the generator it adapts. */ +void scr_gen_copy_tag(ScrGen *g, const ScrGen *from); +const char *scr_gen_tag(const ScrGen *g); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ void *scr_gen_retain_v(void *g); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 4aa703711..019f36536 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1058,6 +1058,99 @@ static ScrStr *scr_insp_weak_collection(const char *label, double recurse, doubl return ib_take(&out); } +/* A native iterator: the array and string iterators print Node's empty + * `Object [Array Iterator] {}`; the Map and Set iterators print the + * entries their next steps would produce, under Node's labels + * (`[Set Iterator] { 1, 2 }`, `[Map Entries] { [ 'a', 1 ] }`), without + * advancing. NULL for a handle's iterator (kind 4), which keeps the + * unsupported answer. */ +static ScrStr *scr_insp_native_iterator(ScrDyn *d, double recurse, double depth) { + unsigned kind, selection; + ScrMap *map; + double index; + scr_dyn_native_iterator_state(d, &kind, &selection, &map, &index); + if (kind == 0) return scr_str_new("Object [Array Iterator] {}", 26); + if (kind == 1) return scr_str_new("Object [String Iterator] {}", 27); + if (kind != 2 && kind != 3) return NULL; + bool pairs = selection == 2; + const char *label = kind == 3 ? (pairs ? "[Set Entries]" : "[Set Iterator]") + : (pairs ? "[Map Entries]" : "[Map Iterator]"); + if (recurse > depth) { + InspBuf out = {0}; + ib_cstr(&out, "[Object "); + ib_cstr(&out, label); + ib_char(&out, ']'); + return ib_take(&out); + } + size_t remaining = 0; + if (map) { + for (double i = scr_map_iter_skip(map, index); i < scr_map_iter_count(map); i++) + if (scr_map_iter_live(map, i)) remaining++; + } + if (remaining == 0) { + InspBuf out = {0}; + ib_cstr(&out, label); + ib_cstr(&out, " { }"); + return ib_take(&out); + } + ScrStr *base = scr_str_new(label, strlen(label)); + scr_insp_begin(recurse + 1); + scr_map_iter_enter(map); + size_t shown = 0; + for (double i = scr_map_iter_skip(map, index); i < scr_map_iter_count(map) && shown < 100; i++) { + if (!scr_map_iter_live(map, i)) continue; + shown++; + ScrDyn *item; + if (pairs) { + item = scr_dyn_new_arr(); + scr_dyn_arr_push(item, scr_map_dyn_key(map, i)); + scr_dyn_arr_push(item, kind == 3 ? scr_map_dyn_key(map, i) : scr_map_dyn_value(map, i)); + } else { + item = kind == 2 && selection == 0 ? scr_map_dyn_value(map, i) : scr_map_dyn_key(map, i); + } + ScrStr *text = scr_insp_dyn(item, recurse + 1, depth); + scr_dyn_release(item); + scr_insp_entry(text, false); + scr_str_release(text); + } + scr_map_iter_exit(map); + if (remaining > shown) { + ScrStr *more = scr_insp_more_items((double)(remaining - shown)); + scr_insp_entry(more, false); + scr_str_release(more); + } + ScrStr *b0 = scr_str_new("{", 1); + ScrStr *b1 = scr_str_new("}", 1); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, false, false); + scr_str_release(base); + scr_str_release(b0); + scr_str_release(b1); + return out; +} + +/* A compiled generator object: Node's empty `Object [] {}`, with the + * tag the compiler set (scr_gen_set_tag). A generator standing in for an + * object of another class (a tag starting with '!') throws the + * "not supported yet" Error instead. */ +static bool scr_insp_is_generator(const ScrDyn *d) { + const char *key = d->v.typed_ref.type_key; + return !strncmp(key, "generator<", 10) || !strncmp(key, "async-generator<", 16); +} + +static ScrStr *scr_insp_generator(const ScrDyn *d) { + const char *tag = scr_gen_tag(d->v.typed_ref.ptr); + if (tag[0] == '!') { + static const char message[] = "util.inspect of this iterator object is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return scr_str_new("", 0); + } + InspBuf out = {0}; + ib_cstr(&out, "Object ["); + ib_cstr(&out, tag); + ib_cstr(&out, "] {}"); + return ib_take(&out); +} + static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, bool is_map) { ScrMap *map = d->v.handle.ptr; const char *label = is_map ? "Map" : "Set"; @@ -1309,6 +1402,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { return scr_insp_weak_collection(d->v.handle.tag == SCR_DYNH_WEAK_MAP ? "WeakMap" : "WeakSet", recurse, depth); if (d->v.handle.tag == SCR_DYNH_DATE) return scr_insp_date(scr_dyn_native_date_value(d)); + if (d->v.handle.tag == SCR_DYNH_ITERATOR) { + ScrStr *iterator = scr_insp_native_iterator(d, recurse, depth); + if (iterator) return iterator; + } const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(d); if (ops->inspect_view) { /* Node's custom inspectors answer the bare class name beyond the @@ -1349,6 +1446,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_TYPED_REF: { + if (scr_insp_is_generator(d)) return scr_insp_generator(d); const char *cls = NULL; ScrDyn *view = scr_event_ref_hooks ? scr_event_ref_hooks->inspect_view(d, &cls) : NULL; if (view) { diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index d18b3d910..206c01839 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4673,6 +4673,16 @@ bool scr_dyn_iterator_step_done(const ScrDyn *value) { return ((ScrNativeIterator *)value->v.handle.ptr)->step_done; } +void scr_dyn_native_iterator_state(const ScrDyn *value, unsigned *kind, unsigned *selection, + ScrMap **collection, double *index) { + const ScrNativeIterator *iterator = value->v.handle.ptr; + *kind = iterator->kind; + *selection = iterator->selection; + *collection = (iterator->kind == 2 || iterator->kind == 3) && iterator->source + ? iterator->source->v.handle.ptr : NULL; + *index = iterator->index; +} + double scr_dyn_collection_iterator_index(const ScrDyn *value) { ScrNativeIterator *iterator = value->v.handle.ptr; ScrDyn *source = iterator->source; @@ -6217,6 +6227,20 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { } if (d->kind == SCR_DYN_TYPED_REF) { if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14); + if (scr_dyn_generator(d)) { + const char *gen_tag = scr_gen_tag(d->v.typed_ref.ptr); + if (gen_tag[0] == '!') { + static const char message[] = "Object.prototype.toString of this iterator is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return NULL; + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "[object "); + scr_jb_puts(&b, gen_tag); + scr_jb_putc(&b, ']'); + return scr_jb_finish(&b); + } ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); ScrStr *out = scr_dyn_object_tag(materialized); scr_dyn_release(materialized); @@ -6249,6 +6273,13 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { scr_jb_putc(&b, ']'); return scr_jb_finish(&b); } + if (d->v.handle.tag == SCR_DYNH_ITERATOR && ((const ScrNativeIterator *)d->v.handle.ptr)->kind < 4) { + static const char *const tags[] = { + "[object Array Iterator]", "[object String Iterator]", "[object Map Iterator]", "[object Set Iterator]", + }; + const char *it_tag = tags[((const ScrNativeIterator *)d->v.handle.ptr)->kind]; + return scr_str_new(it_tag, strlen(it_tag)); + } if (scr_dyn_handle_ops_of(d)->to_string_tag) { ScrJsonBuf b; scr_jb_init(&b); diff --git a/tests/corpus/6075-print-iterators.ts b/tests/corpus/6075-print-iterators.ts new file mode 100644 index 000000000..8b4833264 --- /dev/null +++ b/tests/corpus/6075-print-iterators.ts @@ -0,0 +1,19 @@ +// util.inspect and Object.prototype.toString of iterator objects: generators, +// array, typed array and string iterators and iterator helpers print as +// Object [] {}, and Map and Set iterators print the entries they have +// left. +import { inspect } from "node:util"; +function* g(): Generator { yield 1; } +const s = new Set([1, 2, 3]); +const m = new Map([["a", 1], ["b", 2]]); +const si = s.values(); si.next(); +console.log(g(), [1, 2].values(), "ab"[Symbol.iterator](), new Uint8Array([1]).values()); +console.log(s.values(), s.entries(), m.keys(), m.values(), m.entries(), si); +console.log([1].values().map((x) => x), Iterator.from([1]), g().filter(() => true)); +console.log(inspect({ a: { b: { c: s.values(), d: g() } } })); +console.log(Object.prototype.toString.call(g()), Object.prototype.toString.call([1].values().map((x) => x)), Object.prototype.toString.call(m.keys())); +const done = s.values(); Array.from(done); +console.log(done, new Map().entries(), g()); +const keyed = new Map<{ x: number }, number[]>([[{ x: 1 }, [1, 2]]]); +console.log(keyed.entries(), keyed.keys()); +console.log(new Set(Array.from({ length: 120 }, (_, i) => i)).values()); diff --git a/tests/corpus/6076-print-iterators-js.js b/tests/corpus/6076-print-iterators-js.js new file mode 100644 index 000000000..ad4022b6b --- /dev/null +++ b/tests/corpus/6076-print-iterators-js.js @@ -0,0 +1,17 @@ +// The JavaScript form: util.inspect and Object.prototype.toString of +// iterator objects. +import { inspect } from "node:util"; +function* g() { yield 1; } +const s = new Set([1, 2, 3]); +const m = new Map ([["a", 1], ["b", 2]]); +const si = s.values(); si.next(); +console.log(g(), [1, 2].values(), "ab"[Symbol.iterator](), new Uint8Array([1]).values()); +console.log(s.values(), s.entries(), m.keys(), m.values(), m.entries(), si); +console.log([1].values().map((x) => x), Iterator.from([1]), g().filter(() => true)); +console.log(inspect({ a: { b: { c: s.values(), d: g() } } })); +console.log(Object.prototype.toString.call(g()), Object.prototype.toString.call([1].values().map((x) => x)), Object.prototype.toString.call(m.keys())); +const done = s.values(); Array.from(done); +console.log(done, new Map ().entries(), g()); +const keyed = new Map ([[{ x: 1 }, [1, 2]]]); +console.log(keyed.entries(), keyed.keys()); +console.log(new Set(Array.from({ length: 120 }, (_, i) => i)).values()); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index c37b780ea..10d9c3aca 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2242,6 +2242,8 @@ "6072-iterator-sources-js.js": 5350, "6073-stored-collection-helpers.ts": 4000, "6074-stored-collection-helpers-js.js": 4100, + "6075-print-iterators.ts": 3800, + "6076-print-iterators-js.js": 3500, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5320,6 +5322,8 @@ "6072-iterator-sources-js.js": 5350, "6073-stored-collection-helpers.ts": 4150, "6074-stored-collection-helpers-js.js": 3600, + "6075-print-iterators.ts": 3450, + "6076-print-iterators-js.js": 3300, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From e63d0e51c906b1d491d72dc8677dace5ed4c3a95 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 01:14:20 -0500 Subject: [PATCH 107/120] Wait before returning parked workers' memory, and discard one waiting arena to claim one When the last running worker parks, the trim helper now waits SCR_SA_DISCARD_DELAY_MS (100 by default) before it returns the parked workers' pages, and stops if a worker starts meanwhile. A program that exits within the delay, such as a one-shot type check whose checkers finish just before it reports and exits, no longer returns pages during its last milliseconds. A thread that finds every arena claimed discards one waiting arena and claims it, instead of running every waiting discard. --- packages/runtime/src/scr_alloc.c | 42 ++++++++++++++++++---- packages/runtime/src/scr_alloc.test.c | 52 ++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 2212a1a36..246d27f0a 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -638,6 +638,7 @@ static _Thread_local ScrSaKeep *scr_sa_keep; /* while the worker's memory is kep static void scr_sa_keep_begin(unsigned i); static void scr_sa_keep_drained(ScrSaKeep *k, ScrSaBlock *b, unsigned c); static bool scr_sa_drain(void); +static bool scr_sa_drain_some(bool one); #endif /* Moves every block other threads freed into this arena's free lists. */ @@ -672,8 +673,8 @@ static bool scr_sa_reserve(void) { if (claimed == UINT32_MAX) { #ifdef SCR_SA_TRIM /* Arenas held by discards that wait (see scr_sa_thread_park): this - * thread discards them and claims one. */ - if (scr_sa_drain()) { + * thread discards one of them and claims it. */ + if (scr_sa_drain_some(true)) { scr_sa_refused = false; claimed = atomic_load_explicit(&scr_sa_claimed, memory_order_relaxed); continue; @@ -934,6 +935,12 @@ static void scr_sa_trim(void) { * interrupts every running thread of the process to flush its TLB, and * glibc's trim locks the arenas other threads allocate from, so discards * wait for the last worker of a group, which hands them to one helper; + * - the helper waits SCR_SA_DISCARD_DELAY_MS (100 by default) and stops if + * a worker started meanwhile: a program that starts its next workers or + * exits within that time does not return pages it would fault back in or + * never touch again, and the last worker of the next group hands the + * discards to a new helper; a thread that finds every arena claimed + * discards one waiting arena and claims it; * - the helper then returns the pages * it carved (MADV_DONTNEED; the start of the first page, which it may * share with older blocks, is cleared), frees the recorded system blocks, @@ -1276,11 +1283,13 @@ static void scr_sa_defer_discard(unsigned i, ScrSaKeep *k) { pthread_mutex_unlock(&scr_sa_pending_lock); } -/* Runs the waiting discards on the calling thread, which ends with no arena; - * returns whether there were any. */ -static bool scr_sa_drain(void) { +/* Runs the waiting discards on the calling thread, which ends with no arena: + * all of them, or only the first one when `one` is set; returns whether + * there were any. */ +static bool scr_sa_drain_some(bool one) { bool any = false; for (;;) { + if (one && any) return true; pthread_mutex_lock(&scr_sa_pending_lock); uint32_t pending = scr_sa_pending; unsigned i = pending ? (unsigned)__builtin_ctz(pending) : 0; @@ -1298,9 +1307,30 @@ static bool scr_sa_drain(void) { } } +static bool scr_sa_drain(void) { return scr_sa_drain_some(false); } + +/* SCR_SA_DISCARD_DELAY_MS: how long the trim helper waits after the last + * worker parks before it returns their memory (default 100). */ +static long scr_sa_discard_delay_ms(void) { + const char *delay = getenv("SCR_SA_DISCARD_DELAY_MS"); + if (!delay) return 100; + long ms = strtol(delay, NULL, 10); + return ms < 0 ? 0 : ms; +} + +/* Waits out the delay first, and stops if a worker starts meanwhile: the + * discards then wait for the last worker of that group. A program that exits + * within the delay does not spend its last milliseconds returning pages, and + * one that starts its next workers within it does not return pages while + * they start. */ static void *scr_sa_drain_main(void *arg) { (void)arg; - scr_sa_drain(); + for (long left = scr_sa_discard_delay_ms(); left > 0; left -= 5) { + struct timespec pause = {0, (left < 5 ? left : 5) * 1000000L}; + nanosleep(&pause, NULL); + if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) != 0) return NULL; + } + if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) == 0) scr_sa_drain(); return NULL; } diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index f0efdc935..aa6b0b60b 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -627,7 +627,7 @@ static void *small_worker(void *arg) { /* Discards wait while another worker runs, and the last worker to park * starts them. When every arena is held by a discard that waits, a worker - * that needs one runs them itself. */ + * that needs one discards one of them and claims it. */ static void check_deferred_discard(void) { size_t page = (size_t)sysconf(_SC_PAGESIZE); atomic_store(&discard_step, 0); @@ -656,6 +656,55 @@ static void check_deferred_discard(void) { discard_in_old_arena = true; } +/* A plain thread that holds an arena until discard_step reaches 3. */ +static void *holding_thread(void *arg) { + (void)arg; + void *p = scr_mem_alloc(DISCARD_SIZE); + assert(p && owned(p)); + atomic_store(&discard_step, 1); + wait_step(3); + scr_mem_free(p); + scr_sa_thread_park(); + return NULL; +} + +/* The helper waits SCR_SA_DISCARD_DELAY_MS before it returns the pages, and + * stops if a worker starts meanwhile; the last worker of that group starts + * the discards again. A plain thread holds the arena whose slice the earlier + * checks filled, so the worker carves a slice of its own. */ +static void check_discard_delay(void) { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + setenv("SCR_SA_DISCARD_DELAY_MS", "300", 1); + atomic_store(&discard_step, 0); + pthread_t plain; + assert(pthread_create(&plain, NULL, holding_thread, NULL) == 0); + wait_step(1); + discard_shares = false; + discard_in_old_arena = false; + pthread_t t; + assert(pthread_create(&t, NULL, discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + atomic_store(&discard_step, 0); + pthread_t holder; + assert(pthread_create(&holder, NULL, holding_worker, NULL) == 0); + wait_step(1); + /* Past the delay: the helper stopped when the holder started. */ + struct timespec pause = {0, 600000000L}; + nanosleep(&pause, NULL); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + size_t total = (discard_hi - first) / page; + assert(resident_pages(first, discard_hi) > total / 2); + for (size_t i = 0; i < DISCARD_BLOCKS; i += 4099) verify(discard_blocks[i], block_size, (unsigned char)i); + setenv("SCR_SA_DISCARD_DELAY_MS", "0", 1); + atomic_store(&discard_step, 3); + assert(pthread_join(holder, NULL) == 0); + assert(pthread_join(plain, NULL) == 0); + scr_sa_wait_trims(); + unsetenv("SCR_SA_DISCARD_DELAY_MS"); + assert(resident_pages(first, discard_hi) < total / 2); + discard_in_old_arena = true; +} + static void check_discard(void) { discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); assert(discard_blocks); @@ -750,6 +799,7 @@ static void check_discard(void) { free(trim_blocks); check_deferred_discard(); + check_discard_delay(); for (unsigned i = 0; i < DISCARD_OLD; i++) scr_mem_free(old_blocks[i]); free(discard_blocks); } From 192ea32474e299bff72057b30a7306489ab08f88 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 02:40:22 -0500 Subject: [PATCH 108/120] Read generator tags through a pointer the generator runtime installs scr_json.c and scr_inspect.c called scr_gen_tag, which lives in scr_async.c. Libraries and the runtime's unit tests link those units without scr_async.c, so their links failed on the undefined symbol. Both now read the tag through scr_gen_tag_reader, defined in scr_json.c and set at startup by scr_async.c. A generator capsule only exists when the generator runtime is linked, so the pointer is always set when read. --- packages/runtime/src/scr_async.c | 4 ++++ packages/runtime/src/scr_generator.h | 4 ++++ packages/runtime/src/scr_inspect.c | 2 +- packages/runtime/src/scr_json.c | 4 +++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 124304970..583b1f6d0 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -5556,6 +5556,10 @@ const char *scr_gen_tag(const ScrGen *g) { return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator"; } +__attribute__((constructor)) static void scr_gen_tag_reader_init(void) { + scr_gen_tag_reader = scr_gen_tag; +} + void scr_gen_close(ScrGen *g) { scr_gen_slot_reset(&g->ret); scr_gen_resume_return(g); diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index 76d152f8a..c4397e021 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -73,6 +73,10 @@ void scr_gen_set_tag(ScrGen *g, const char *tag); /* An adapter generator prints with the tag of the generator it adapts. */ void scr_gen_copy_tag(ScrGen *g, const ScrGen *from); const char *scr_gen_tag(const ScrGen *g); +/* scr_gen_tag for units that also link without the generator runtime (a + * library links none): the generator runtime sets it at startup, and only + * then can a generator capsule exist. */ +extern const char *(*scr_gen_tag_reader)(const ScrGen *g); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ void *scr_gen_retain_v(void *g); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 019f36536..f72d7aca8 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1138,7 +1138,7 @@ static bool scr_insp_is_generator(const ScrDyn *d) { } static ScrStr *scr_insp_generator(const ScrDyn *d) { - const char *tag = scr_gen_tag(d->v.typed_ref.ptr); + const char *tag = scr_gen_tag_reader(d->v.typed_ref.ptr); if (tag[0] == '!') { static const char message[] = "util.inspect of this iterator object is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 206c01839..6d931d621 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1678,6 +1678,8 @@ ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key) { * later, and it neither grows nor owns heap references. */ static ScrDyn scr_dyn_small_ints[384]; +const char *(*scr_gen_tag_reader)(const ScrGen *g) = NULL; + __attribute__((constructor)) static void scr_dyn_small_ints_init(void) { for (int i = 0; i < 384; i++) { scr_dyn_small_ints[i].rc = SIZE_MAX; @@ -6228,7 +6230,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { if (d->kind == SCR_DYN_TYPED_REF) { if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14); if (scr_dyn_generator(d)) { - const char *gen_tag = scr_gen_tag(d->v.typed_ref.ptr); + const char *gen_tag = scr_gen_tag_reader(d->v.typed_ref.ptr); if (gen_tag[0] == '!') { static const char message[] = "Object.prototype.toString of this iterator is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); From 8802765a1c2bc457517e5fed22cf8855874be3be Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:19:31 -0500 Subject: [PATCH 109/120] Destroy an idle pooled http socket that receives data Node 24's http.Agent installs a read guard on each socket it puts in freeSockets: bytes that arrive while the socket is idle destroy it, so they are never read as the response to the next request that reuses the socket. scriptc discarded the bytes and kept the socket in the pool, so the next request reused it (reusedSocket true, one connection) where Node dials a new one. The client connection's data hook now destroys the socket when it is in an agent's free list. The socket leaves freeSockets and totalSocketCount, and the next request through the agent opens a new connection, as in Node. Corpus 6200 covers an idle socket that receives a stray response. --- .../6200-http-agent-free-socket-data.ts.json | 6 ++ packages/runtime/src/scr_http.c | 7 +++ .../6200-http-agent-free-socket-data.ts | 57 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 72 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json create mode 100644 tests/corpus/6200-http-agent-free-socket-data.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json new file mode 100644 index 000000000..e8df3e384 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6200-http-agent-free-socket-data.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index f34cc18bb..83a1b3583 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -3323,6 +3323,13 @@ static void scr_http_conn_pump(ScrHttpConn *conn) { static void scr_http_conn_data(void *ctx, const char *buf, size_t n) { ScrHttpConn *conn = (ScrHttpConn *)ctx; + if (conn->pool_agent != NULL) { + /* Data on an idle pooled socket: the agent destroys the socket, as + * Node's free-socket read guard does, so the bytes are never read as + * the next request's response. */ + scr_net_sock_destroy(conn->sock); + return; + } if (conn->len + n > conn->cap) { size_t cap = conn->cap ? conn->cap : 8192; while (cap < conn->len + n) cap *= 2; diff --git a/tests/corpus/6200-http-agent-free-socket-data.ts b/tests/corpus/6200-http-agent-free-socket-data.ts new file mode 100644 index 000000000..69e89de49 --- /dev/null +++ b/tests/corpus/6200-http-agent-free-socket-data.ts @@ -0,0 +1,57 @@ +// Bytes that arrive on an idle pooled socket are not read as the next +// response: the agent destroys the socket, and the next request through +// the agent dials a new connection. +import * as http from "node:http"; +import * as net from "node:net"; + +let conns = 0; +const server = net.createServer((sock) => { + const id = ++conns; + let buf = ""; + sock.on("data", (d: Buffer) => { + buf += d.toString(); + while (buf.includes("\r\n\r\n")) { + const head = buf.slice(0, buf.indexOf("\r\n\r\n")); + buf = buf.slice(buf.indexOf("\r\n\r\n") + 4); + const path = head.split(" ")[1]!; + const body = `real ${path} conn${id}`; + sock.write(`HTTP/1.1 200 OK\r\nContent-Length: ${body.length}\r\nKeep-Alive: timeout=5\r\n\r\n${body}`); + if (path === "/poisoned") { + setTimeout(() => sock.write("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nPOISON"), 50); + } + } + }); + sock.on("close", () => console.log(`server conn${id} closed`)); + sock.on("error", () => {}); +}); + +const agent = new http.Agent({ keepAlive: true }); +function get(port: number, path: string): Promise { + return new Promise((resolve) => { + const req = http.get({ host: "127.0.0.1", port, path, agent }, (res) => { + let body = ""; + res.on("data", (c: Buffer) => (body += c.toString())); + res.on("end", () => resolve(`${body} reused=${req.reusedSocket}`)); + }); + req.on("error", (e: Error) => resolve(`error ${e.message}`)); + }); +} + +function free(): number { + return Object.keys(agent.freeSockets).length; +} + +server.listen(0, "127.0.0.1", async () => { + const port = (server.address() as net.AddressInfo).port; + console.log(await get(port, "/quiet")); + await new Promise((r) => setTimeout(r, 20)); + console.log("free after /quiet", free()); + console.log(await get(port, "/again")); + console.log(await get(port, "/poisoned")); + await new Promise((r) => setTimeout(r, 200)); + console.log("free after stray bytes", free(), "total", agent.totalSocketCount); + console.log(await get(port, "/next")); + console.log("connections", conns); + agent.destroy(); + server.close(() => console.log("server closed")); +}); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 10d9c3aca..8e3cbeaf2 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2264,6 +2264,7 @@ "6154-cjs-require-nested-destructure/main.js": 2800, "6155-cjs-export-table-methods/main.js": 2600, "6156-cjs-function-members-from-ts/main.ts": 3000, + "6200-http-agent-free-socket-data.ts": 3846, "700-classes-basic.ts": 2530, "701-classes-composition.ts": 2357, "702-classes-this-capture.ts": 2140, @@ -5344,6 +5345,7 @@ "6154-cjs-require-nested-destructure/main.js": 6400, "6155-cjs-export-table-methods/main.js": 6000, "6156-cjs-function-members-from-ts/main.ts": 7000, + "6200-http-agent-free-socket-data.ts": 3452, "700-classes-basic.ts": 2674, "701-classes-composition.ts": 2606, "702-classes-this-capture.ts": 2403, From f75b7d8aabc07daa8095ffb96542959557d02569 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:22:05 -0500 Subject: [PATCH 110/120] Use a computed test name as given when the function's name is unknown A test name computed at run time took the function's name when it was empty, which needs that name at compile time. For a function the program received as a parameter, such as a helper that registers `test(\`case ${label}\`, body)` for its callers, the call was refused, where it compiled before. The name is now used as given in that case, and the limitations page notes that an empty one stays empty, where Node uses the function's name. The test-name-forms fixture registers tests through such a helper and compares them with Node, plain and with SCRIPTC_SAN=1. Without this change it fails to compile. --- docs/content/limitations/builtin-apis/test-runner.mdx | 1 + packages/compiler/src/frontend/lowering/lower-test.ts | 6 ++++++ tests/fixtures/node-test/cases/test-name-forms.ts | 11 ++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 5e368cc03..70143a0c3 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -5,3 +5,4 @@ - **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named ``. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged. - **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. - **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it. +- **Test names:** a test without a name, or with an empty one, takes its function's name or ``, as in Node, when the function's name is known at compile time: a function expression, a function declaration, or a `const` bound to an arrow or anonymous function. A name computed at run time, with a function the program received as a parameter, is used as given, so an empty one stays empty where Node uses the function's name. `test(fn)` and `test("", fn)` with such a function throw `SC2020`. diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index bb525e02d..c31e674af 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -409,6 +409,12 @@ function lowerTestName( if (!nameNode) return strLit(fallback(), loc); const name = lowerer.lowerExprExpecting(nameNode, STRING); if (name.kind === "strLit") return name.value === "" ? strLit(fallback(), loc) : name; + // A name computed at run time: when the function's own name is not known + // at compile time (a function passed in as a parameter, say), the name + // is used as given, and an empty one stays empty (the limitations page + // notes it). Refusing here would refuse every helper that registers + // tests for its callers. + if (fnNode && staticFunctionName(lowerer, fnNode) === null) return name; const slot = lowerer.declareHiddenLocal("%testName", STRING); const value = varRef(slot.id, STRING, loc); return { diff --git a/tests/fixtures/node-test/cases/test-name-forms.ts b/tests/fixtures/node-test/cases/test-name-forms.ts index 915d68a79..b2f61dd89 100644 --- a/tests/fixtures/node-test/cases/test-name-forms.ts +++ b/tests/fixtures/node-test/cases/test-name-forms.ts @@ -1,6 +1,7 @@ // The (name?, options?, fn?) forms of test, it, describe and t.test, and // Node's default names: a missing or empty name takes the function's name, -// or "". With both skip and todo, skip wins, in either order and +// or "". A helper may register tests with computed names and +// functions it receives as parameters. With both skip and todo, skip wins, in either order and // against the todo method twins. import { describe, it, test } from "node:test"; @@ -8,6 +9,14 @@ function declaredTest(): void {} const boundArrow = (): void => {}; const emptyName = process.argv.length > 99 ? "never" : ""; +// A helper that registers tests for its callers: the name is computed and +// the function arrives as a parameter. +function registerCase(label: string, body: () => void): void { + test(`case ${label}`, body); +} +registerCase("one", () => {}); +registerCase("two", declaredTest); + test(function namedExpression() {}); test(() => {}); test(declaredTest); From d8d48e1637c1161d1f87ffc4939ae3de765565aa Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:31:48 -0500 Subject: [PATCH 111/120] Commit default-region memory as ProgramLifetime instances use it A thread's default region asked for huge pages from its first chunk and gave every class a 16 KiB segment, so in a program that only references LifetimeRegion each thread committed whole 2 MiB pages and per-class segment space for a few instances of each class. Default regions now use small pages, and a class's segments in a default region start at 1 KiB and double up to 16 KiB. Freeable regions keep their page and segment policy. --- packages/runtime/src/scr_region.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index 31d01296e..cc094ef0b 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -65,7 +65,14 @@ void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((we #endif #define SCR_RGN_HDR ((size_t)64) +/* A class's segments in a default region start at SCR_RGN_SEG_FIRST bytes + * and double up to SCR_RGN_SEG: a class with a few instances in a thread's + * default region, as most classes have in most threads, then leaves little + * unused memory behind its last instance. Other regions start at + * SCR_RGN_SEG: their classes are usually many and short-lived, and more + * segments cost the release walk. */ #define SCR_RGN_SEG ((size_t)16384) +#define SCR_RGN_SEG_FIRST ((size_t)1024) #define SCR_RGN_AHEAD ((size_t)256 << 10) /* see scr_rgn_ahead */ #define SCR_RGN_AHEAD_START ((size_t)1 << 20) /* see scr_rgn_ahead */ _Static_assert(sizeof(ScrRgnChunk) <= SCR_RGN_HDR, "the chunk header fits before the first segment"); @@ -82,9 +89,11 @@ _Static_assert(sizeof(ScrRgnChunk) <= SCR_RGN_HDR, "the chunk header fits before #define SCR_RGN_CACHE_BYTES ((size_t)8 << 20) #define SCR_RGN_CACHE_WARM ((size_t)256 << 10) -/* Regions start with small pages and switch to huge pages once they span - * this many chunks (a default region at once): small regions then do not - * pay a 2 MiB fault, large ones do not pay 512 small ones. */ +/* Freeable regions start with small pages and switch to huge pages once + * they span this many chunks: small regions then do not pay a 2 MiB fault, + * large ones do not pay 512 small ones. Default regions keep small pages: + * every thread has one, most hold few instances, and a huge page commits + * 2 MiB per thread for them. */ #define SCR_RGN_HUGE_AFTER 4 /* Retries. A condemned region is attempted at the first retry after its @@ -340,7 +349,7 @@ static void scr_rgn_chunk_unmap(ScrRgnChunk *c) { } /* Whether the region's next chunk uses huge pages. */ -static bool scr_rgn_huge(const ScrRegion *r) { return r->id == 0 || r->nchunks + 1 >= SCR_RGN_HUGE_AFTER; } +static bool scr_rgn_huge(const ScrRegion *r) { return r->id != 0 && r->nchunks + 1 >= SCR_RGN_HUGE_AFTER; } /* A chunk for `r`'s next one, with its page policy. */ static ScrRgnChunk *scr_rgn_chunk_reserve(const ScrRegion *r) { @@ -479,9 +488,11 @@ static void scr_rgn_chunk_retire(ScrRgnThread *t, ScrRgnChunk *c) { /* The instance stride: the emitted fast path rounds the size the same way. */ static inline size_t scr_rgn_stride(const ScrRgnClass *cls) { return (cls->size + 7) & ~(size_t)7; } -static ScrRgnSeg *scr_rgn_seg_new(ScrRegion *r, const ScrRgnClass *cls) { +static ScrRgnSeg *scr_rgn_seg_new(ScrRegion *r, const ScrRgnClass *cls, const ScrRgnSeg *prev) { size_t stride = scr_rgn_stride(cls); - size_t need = stride > SCR_RGN_SEG ? stride : SCR_RGN_SEG; + size_t size = prev ? (size_t)(prev->limit - prev->start) * 2 : r->id == 0 ? SCR_RGN_SEG_FIRST : SCR_RGN_SEG; + if (size > SCR_RGN_SEG) size = SCR_RGN_SEG; + size_t need = stride > size ? stride : size; if (need > SCR_RGN_CHUNK - SCR_RGN_HDR) scr_trap("scriptc: ProgramLifetime instance too large for a region\n"); ScrRgnChunk *c = r->chunks; if (!c || (size_t)((char *)c + SCR_RGN_CHUNK - c->bump) < need) c = scr_rgn_chunk_new(r); @@ -589,7 +600,7 @@ void *scr_region_alloc(ScrRgnCursor *cursor, ScrRgnClass *cls) { if (s && cursor->bump) s->end = cursor->bump; size_t stride = scr_rgn_stride(cls); if (!s || (size_t)(s->limit - s->end) < stride) { - s = scr_rgn_seg_new(r, cls); + s = scr_rgn_seg_new(r, cls, s); r->cur[slot] = s; } char *o = s->end; From 0523806107f5c980971e12a5a678b5f81050d9fd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:37:40 -0500 Subject: [PATCH 112/120] Inspect class instances with added properties as Node does console.log and util.inspect of a class instance list the properties added to it after construction, after its declared fields. JavaScript classes print their own properties in creation order: a field first assigned in a method or under a condition appears only once it exists. Such instances render through their checked-dynamic view when the class has a property bag or is a JavaScript class; other classes keep the generated inspect helper. The runtime inspector detects cycles through the class instances it reaches through views, so a cycle prints [Circular *N] instead of expanding to the depth limit. A circular target's prefix counts toward the 80-column line width, as in Node. Capsules of a recursive class type carry the class's view operations, so a nested instance prints with its class name. --- .../src/backend/llvm/expr-stream-bridges.ts | 9 +- .../src/frontend/lowering/lower-inspect.ts | 70 +++++++++-- .../compiler/src/ir/builtin-signatures.ts | 2 +- ...002-inspect-class-added-properties.ts.json | 6 + .../6003-inspect-js-class-properties.js.json | 6 + packages/runtime/src/scr_inspect.c | 50 ++++++-- packages/runtime/src/scr_inspect.h | 4 +- .../6002-inspect-class-added-properties.ts | 117 ++++++++++++++++++ .../6003-inspect-js-class-properties.js | 40 ++++++ tests/harness/corpus-costs.json | 4 + 10 files changed, 285 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json create mode 100644 tests/corpus/6002-inspect-class-added-properties.ts create mode 100644 tests/corpus/6003-inspect-js-class-properties.js diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 76ef35358..8a53b5fa8 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -708,6 +708,10 @@ export function streamTypedRefMaterializeAdapter( } // Publish both symbols before their bodies: class-array and callback // converters can request this same adapter while emitting the commit. + // A recursive field type binds capsules of this class there too, so the + // class view ops are published first. + const opsMeta = isDynTypedRefType(t) ? host.classMeta.get(t.className) : undefined; + if (opsMeta && liveClassOpsEligible(opsMeta)) adapter.classOps = `@${snapshot}_cops`; adapter.commit = `@${snapshot}_commit`; adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot); const B = new BlockBuilder(); @@ -719,10 +723,7 @@ export function streamTypedRefMaterializeAdapter( `llvm emitter bug: typed-ref materialize of unknown class ${t.className}`, ); } - if (liveClassOpsEligible(meta)) { - adapter.classOps = `@${snapshot}_cops`; - liveClassOps(host, snapshot, meta); - } + if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta); if (meta.hierarchy && meta.children.length) { const vtSlot = B.tmp(), vt = B.tmp(), diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 2551031eb..37233bcdc 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -20,6 +20,9 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + classDynViewSupported, + DYN_CLASS_PROPERTIES, + isClassOwnEnumerableFieldName, isDirentShape, isDynTypedRefType, recordTextCodecClass, @@ -220,9 +223,10 @@ function inspectSupport( ? null : "error subclasses with declared fields have no inspect lowering yet (the extra own properties are runtime-dynamic)"; } - // A class with subclasses renders through its checked-dynamic view, - // which names the instance's own class and holds its own fields. - if (info.subclasses.length > 0) { + // A class with subclasses, or a JavaScript class, renders through its + // checked-dynamic view, which names the instance's own class and holds + // its own properties. + if (classInspectsThroughView(lowerer, info)) { if (!isDynTypedRefType(t) || visiting.has(t.className)) return null; visiting.add(t.className); const hierarchy = (current: ClassInfo): ClassInfo[] => [ @@ -374,7 +378,7 @@ export function inspectExpr( loc, }; } - if (lowerer.classes.get(t.className)?.subclasses.length) { + if (classInspectsThroughView(lowerer, lowerer.classes.get(t.className))) { return { kind: "libCall", fn: "insp.dyn", @@ -475,6 +479,40 @@ function typeConstituents(lowerer: Lowerer, u: IrType): IrType[] { } } +/** Whether instances of a class render through its checked-dynamic view + * instead of a helper over the declared layout. The view names the + * instance's own class, which a base-typed value may not know statically, + * and lists own properties in creation order. A JavaScript class needs that + * order: its fields can be created conditionally, outside the constructor, + * or by writes to the instance after construction. */ +function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined): boolean { + if (info === undefined) return false; + if (info.subclasses.length > 0) return true; + return ( + info.decl != null && + isJsSourceFile(info.decl.getSourceFile()) && + classDynViewSupported( + info.def.fields, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ); +} + +/** Whether a class holds properties added outside its declared layout, which + * only its checked-dynamic view can list. Decided after lowering, when the + * class has its final fields. */ +function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolean { + return ( + info.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) && + classDynViewSupported( + info.def.fields, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ); +} + /* ── the interned per-type helper ────────────────────────────────────── */ /** `%util.insp.(v, r, d): string` — formatValue over the static type: @@ -506,8 +544,8 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // CYCLE-CAPABLE composites (typeReachesItself) run Node's circular // machinery: circCheck first (a value already on the traversal stack // renders [Circular *N] — before the empty/depth answers, Node's - // order), seen-push after begin, and refWrap around end's result (the - // prefix on values the walk found circular). Acyclic types + // order), seen-push after begin, and refWrap around end's result (end + // adds the prefix on values the walk found circular). Acyclic types // keep the zero-cost path. const onCycle = (t.kind === "record" || t.kind === "array" || t.kind === "map" || t.kind === "object") && @@ -1196,7 +1234,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // #private fields never render: they are not properties in any // observable way — Node's inspect omits them entirely (verified), // so a class whose only fields are private prints as `C {}`. - const visible = info.def.fields.filter((f) => !f.name.startsWith("#")); + const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name)); if (visible.length === 0) { body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))]; break; @@ -1316,6 +1354,24 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { body, loc, }; + const inspected = t.kind === "object" ? lowerer.classes.get(t.className) : undefined; + if (inspected !== undefined) { + // Dynamic stores add a property bag to the class after this helper was + // built. The checked-dynamic view lists the bag's properties after the + // declared fields, so a class that has one renders through the view. + lowerer.shapeOrderHelperFinalizers.push(() => { + if (!classInspectsAddedProperties(lowerer, inspected)) return; + fn.body = [ + ret({ + kind: "libCall", + fn: "insp.dyn", + args: [{ kind: "dynFrom", value: v(), liveRef: true, type: DYN, loc }, r(), d()], + type: STRING, + loc, + }), + ]; + }); + } if (rebuildShapeOrderBody !== null) { lowerer.shapeOrderHelperFinalizers.push(() => { rebuildShapeOrderBody!(); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 51765286b..038f591c4 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3888,7 +3888,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /* Circular references over cycle-capable composites (recursive record/ * class types): circCheck answers a value's circular id when it is * already on the traversal stack (0 otherwise), seenPush/refWrap - * bracket the frame (refWrap adds Node's " " prefix to values + * bracket the frame (insp.end adds Node's " " prefix to values * the walk found circular), circular renders "[Circular *N]". */ "insp.circCheck": { argTypes: [null], result: F64 }, "insp.seenPush": { argTypes: [null], result: VOID }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json new file mode 100644 index 000000000..2665f2a27 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6002-inspect-class-added-properties.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json new file mode 100644 index 000000000..d0b03b570 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6003-inspect-js-class-properties.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index f72d7aca8..3194528d3 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -236,6 +236,7 @@ typedef struct { size_t len; size_t cap; bool all_num; /* every entry so far flagged numeric (grouping order) */ + const void *seen; /* the value this frame pushed on the seen stack, or NULL */ } InspFrame; static SCR_TL InspFrame *g_frames; @@ -264,6 +265,10 @@ static void insp_circ_reset(void); /* circular-reference state, below */ /* Begin a non-empty composite: formatRaw's `recurseTimes += 1; * ctx.currentDepth = recurseTimes` plus the uniform +2 the children * format under. `recurse` is the CHILDREN's recursion depth. */ +/* A value whose frame the next scr_insp_begin opens: that frame pushes it + * on the seen stack (the runtime inspector's class instances). */ +static SCR_TL const void *g_pending_seen; + void scr_insp_begin(double recurse) { if (recurse == 1) insp_circ_reset(); /* a fresh top-level value: Node's per-inspect ctx */ if (g_nframes == g_frames_cap) { @@ -276,8 +281,14 @@ void scr_insp_begin(double recurse) { f->len = 0; f->cap = 0; f->all_num = true; + f->seen = NULL; g_cur_depth = recurse; g_indent += 2; + if (g_pending_seen) { + const void *v = g_pending_seen; + g_pending_seen = NULL; + scr_insp_seen_push(v); + } } /* Append one already-rendered entry (borrowed; retained here). is_num @@ -352,6 +363,7 @@ void scr_insp_seen_push(const void *v) { g_seen[g_nseen].ptr = v; g_seen[g_nseen].id = 0; g_nseen++; + if (g_nframes > 0) insp_top()->seen = v; } ScrStr *scr_insp_circular(double id) { @@ -360,17 +372,12 @@ ScrStr *scr_insp_circular(double id) { return scr_str_new(buf, (size_t)n); } +/* Pops the value's seen entry after its frame closed. scr_insp_end already + * wrote the `` prefix of a circular target. */ ScrStr *scr_insp_ref_wrap(const void *v, ScrStr *s) { + (void)v; if (g_nseen > 0) g_nseen--; - int id = insp_circ_id(v); - if (id == 0) return scr_str_retain(s); - char buf[32]; - int n = snprintf(buf, sizeof buf, " ", id); - ScrStr *out = scr_str_alloc_raw(s->len + (size_t)n, s->len + (size_t)n); - memcpy(out->data, buf, (size_t)n); - memcpy(out->data + n, s->data, s->len); - out->data[out->len] = '\0'; - return out; + return scr_str_retain(s); } /* formatRaw's Date base: toISOString(), or toString()'s "Invalid Date" @@ -507,6 +514,20 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, bool * and Map and Set entries) by their rendered text, in UTF-16 code-unit * order; arrays keep their element order. */ if (g_sorted && !array_extras && f->len > 1) qsort(f->items, f->len, sizeof *f->items, insp_item_cmp); + /* Node adds a circular target's `` to the base before it chooses + * the layout, so the reference counts toward the line width. */ + ScrStr *ref_base = NULL; + int ref_id = f->seen ? insp_circ_id(f->seen) : 0; + if (ref_id) { + char buf[32]; + int n = snprintf(buf, sizeof buf, base->len ? " " : "", ref_id); + size_t len = (size_t)n + base->len; + ref_base = scr_str_alloc_raw(len, len); + memcpy(ref_base->data, buf, (size_t)n); + memcpy(ref_base->data + n, base->data, base->len); + ref_base->data[len] = '\0'; + base = ref_base; + } size_t entries = f->len; bool compact = g_compact >= 1; if (compact && array_extras && entries > 6) insp_group(f, trailing_more); @@ -553,6 +574,7 @@ done:; for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); free(f->items); g_nframes--; + if (ref_base) scr_str_release(ref_base); return ib_take(&out); } @@ -1464,6 +1486,12 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(body); return ib_take(&out); } + /* A class instance already on the traversal stack renders as + * [Circular *N]. The key is the native instance, the same key the + * compiled inspect helpers push, so a cycle through both resolves. */ + const void *instance = d->v.typed_ref.ptr; + double circular = scr_insp_circ_check(instance); + if (circular > 0) return scr_insp_circular(circular); ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); /* A class instance prints with its class's name, which the checked * view of its fields does not carry. */ @@ -1475,7 +1503,11 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (!cls) scr_trap("scriptc: out of memory\n"); memcpy(cls, name->data, name->len); cls[name->len] = '\0'; + size_t nseen = g_nseen; + g_pending_seen = instance; out = scr_insp_obj(materialized, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; free(cls); } else { out = scr_insp_dyn(materialized, recurse, depth); diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 4414330b5..4eb607a5c 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -55,8 +55,8 @@ void scr_insp_entry(ScrStr *s, bool is_num); * FIRST (a value already on the traversal stack answers its circular id, * assigned in discovery order — the caller renders scr_insp_circular and * descends no further), seen_push after begin, and ref_wrap around end's - * result (pops the stack; values in the circular map gain the " " - * prefix). State resets at each top-level value's first frame + * result (pops the stack; scr_insp_end gives values in the circular map + * the " " prefix). State resets at each top-level value's first frame * (scr_insp_begin(1)). ref_wrap BORROWS s and returns +1. */ double scr_insp_circ_check(const void *v); void scr_insp_seen_push(const void *v); diff --git a/tests/corpus/6002-inspect-class-added-properties.ts b/tests/corpus/6002-inspect-class-added-properties.ts new file mode 100644 index 000000000..a143aa437 --- /dev/null +++ b/tests/corpus/6002-inspect-class-added-properties.ts @@ -0,0 +1,117 @@ +import { inspect } from "node:util"; + +class Point { + x = 1; + y = 2; +} +class Empty {} +class WithCtor { + a: number; + b: string; + constructor(a: number, b: string) { + this.a = a; + this.b = b; + } +} +class Derived extends Point { + w = 4; +} +class Methods { + n = 1; + get double(): number { + return this.n * 2; + } + inc(): void { + this.n++; + } +} + +const p = new Point(); +console.log(p); +console.log(new Empty()); +console.log(new WithCtor(1, "s")); +console.log(new Derived()); +console.log(new Methods()); +console.log([p, p]); +console.log({ p }); +console.log(inspect(p)); +console.log(`${inspect(new Derived())}`); + +class Pair { + x = 1; + y = 2; +} + +class Node2 { + name: string; + next: Node2 | null = null; + constructor(name: string) { + this.name = name; + } +} + +class Holder { + #secret = 1; + label = "h"; + items: number[] = [1, 2, 3]; + inner = new Pair(); +} + +const Anonymous = class { + v = 1; +}; + +const pair = new Pair(); +console.log(pair); +const loose: any = pair; +loose["z"] = 3; +(pair as any).w = "added"; +console.log(pair); +console.log([pair], { pair }); +console.log(inspect(pair, { depth: 0 })); +console.log(inspect({ deep: { deeper: pair } }, { depth: 1 })); + +const a = new Node2("a"); +const b = new Node2("b"); +a.next = b; +b.next = a; +console.log(a); + +const h = new Holder(); +(h as any).extra = { nested: true }; +console.log(h); +console.log(new Anonymous()); +console.log(`${inspect(new Pair())}`); + +const self = new Node2("s"); +self.next = self; +console.log(self); +console.log([a]); +console.log({ ring: a, other: self }); + +class Shape { + label = "shape"; + peer: Shape | null = null; +} +class Circle extends Shape { + r = 1; +} +const c1 = new Circle(); +const c2 = new Circle(); +c1.peer = c2; +c2.peer = c1; +const shapes: Shape[] = [c1]; +console.log(shapes); +console.log(c2); + +class Link { + label = "link"; + peer: Link | null = null; +} +class Tagged extends Link { + tag = 1; +} +const l1 = new Tagged(); +l1.peer = new Tagged(); +const links: Link[] = [l1]; +console.log(links); diff --git a/tests/corpus/6003-inspect-js-class-properties.js b/tests/corpus/6003-inspect-js-class-properties.js new file mode 100644 index 000000000..6a5541841 --- /dev/null +++ b/tests/corpus/6003-inspect-js-class-properties.js @@ -0,0 +1,40 @@ +class Box { + constructor(v) { + this.v = v; + } +} +const b = new Box(1); +console.log(b); +b.extra = "e"; +console.log(b); + +class Plain {} +const q = new Plain(); +console.log(q); +q.a = 1; +q.b = [1, 2]; +console.log(q); +q.nested = new Box(2); +console.log(q); +console.log([q]); + +class Late { + constructor() { + this.a = 1; + } + setB() { + this.b = 2; + } +} +const late = new Late(); +console.log(late); +late.setB(); +console.log(late); + +class Cond { + constructor(flag) { + if (flag) this.x = 1; + this.y = 2; + } +} +console.log(new Cond(true), new Cond(false)); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 10d9c3aca..4f22ed466 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2224,6 +2224,8 @@ "600-closures-basic.ts": 2349, "6000-json-stringify-class-instances.ts": 7567, "6001-json-stringify-js-class-instances.js": 4668, + "6002-inspect-class-added-properties.ts": 6044, + "6003-inspect-js-class-properties.js": 2752, "601-closures-loops.ts": 1788, "602-closures-identity-recursion.ts": 2372, "6020-class-extends-function-constructor.js": 4300, @@ -5304,6 +5306,8 @@ "600-closures-basic.ts": 2534, "6000-json-stringify-class-instances.ts": 12107, "6001-json-stringify-js-class-instances.js": 7469, + "6002-inspect-class-added-properties.ts": 9670, + "6003-inspect-js-class-properties.js": 4403, "601-closures-loops.ts": 2297, "602-closures-identity-recursion.ts": 2427, "6020-class-extends-function-constructor.js": 7500, From ef394d0de9c9e7ee821ef05c512d09b68695078b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 04:09:13 -0500 Subject: [PATCH 113/120] Name the destination of every ownership transfer in the LLVM emitter moveTemp, retainValue and releaseValue are replaced by forms that state where a reference goes or comes from: toHeap and toFrame, retainHeap and retainFrame, releaseHeap and releaseFrame. A heap reference is one that heap memory holds: a field, an array or map element, a capture box, a union box, a module global, a promise or generator slot, the exception cell or another runtime structure. A frame reference is one that a local, a parameter, a return value, a stack object or a temporary holds. Sites whose destination depends on the binding (a local or a module global) or on the function (a coroutine's return fulfills its promise) choose at emission time. adoptHeap marks the runtime results whose +1 the runtime made from heap memory and that become frame values: capture box reads, generator in and out slots, and await results. The statement-frame release is renamed releaseTemps. Both forms of each operation emit what the old call emitted, so the generated IR is unchanged. --- .../src/backend/llvm/dense-array-access.ts | 8 +- packages/compiler/src/backend/llvm/emitter.ts | 172 ++++++++++++------ .../compiler/src/backend/llvm/expr-async.ts | 24 +-- .../compiler/src/backend/llvm/expr-calls.ts | 60 +++--- .../src/backend/llvm/expr-containers.ts | 10 +- .../compiler/src/backend/llvm/expr-control.ts | 28 +-- .../compiler/src/backend/llvm/expr-dynamic.ts | 14 +- .../src/backend/llvm/expr-primitives.ts | 31 ++-- .../compiler/src/backend/llvm/expr-records.ts | 12 +- packages/compiler/src/backend/llvm/lib-abi.ts | 2 +- .../compiler/src/backend/llvm/lib-connect.ts | 2 +- .../compiler/src/backend/llvm/lib-datagram.ts | 12 +- .../compiler/src/backend/llvm/lib-dispatch.ts | 16 +- .../src/backend/llvm/lib-filesystem.ts | 20 +- .../compiler/src/backend/llvm/lib-http2.ts | 6 +- .../compiler/src/backend/llvm/lib-lookup.ts | 2 +- .../compiler/src/backend/llvm/lib-network.ts | 42 ++--- .../compiler/src/backend/llvm/lib-process.ts | 34 ++-- .../compiler/src/backend/llvm/lib-stream.ts | 26 +-- .../compiler/src/backend/llvm/lib-test.ts | 14 +- packages/compiler/src/backend/llvm/lib-tls.ts | 4 +- .../src/backend/llvm/local-array-reads.ts | 4 +- .../src/backend/llvm/local-union-storage.ts | 14 +- .../src/backend/llvm/map-number-lookup.ts | 4 +- .../compiler/src/backend/llvm/split-loops.ts | 4 +- .../src/backend/llvm/stack-objects.ts | 2 +- .../src/backend/llvm/string-slices.ts | 2 +- .../compiler/src/backend/llvm/thread-slots.ts | 8 +- 28 files changed, 318 insertions(+), 259 deletions(-) diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index f19073b75..d02d7f7c3 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -257,7 +257,7 @@ export function emitDenseSlotUnlink( B.condBr(same, swap, done); B.startBlock(swap); B.line(`store ptr ${placeholder}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); - host.releaseValue(current, element); + host.releaseHeap(current, element); B.br(done); B.startBlock(done); } @@ -315,7 +315,7 @@ export function emitDenseArrayGet( fastLabel = B.newLabel("arr.get.ref"); B.condBr(nonnull, fastLabel, slow); B.startBlock(fastLabel); - fast = retain ? host.retainValue(raw, element) : raw; + fast = retain ? host.retainFrame(raw, element) : raw; } B.br(join); B.startBlock(slow); @@ -493,7 +493,7 @@ export function emitDenseArraySet( const releaseLabel = B.newLabel("arr.set.release"); B.condBr(hadValue, releaseLabel, join); B.startBlock(releaseLabel); - host.releaseValue(old, element); + host.releaseHeap(old, element); } B.br(join); // Below the bound the slot holds a present value: replace it, leaving @@ -508,7 +508,7 @@ export function emitDenseArraySet( ); } storeValue(filledValuePtr); - if (filledOld) host.releaseValue(filledOld, element); + if (filledOld) host.releaseHeap(filledOld, element); B.br(join); B.startBlock(slow); call(" cold"); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 48012704b..7e7e70df6 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -4962,8 +4962,8 @@ export class LlEmitter { } } - /** Strike a refcounted temp from its frame: ownership is being moved. */ - moveTemp(v: LlValue): void { + /** Strike a refcounted temp from its frame. */ + private disownTemp(v: LlValue): void { if (!isRefCounted(v.type) || this.isImmortal(v.name)) return; for (let i = this.frames.length - 1; i >= 0; i--) { const idx = this.frames[i]!.findIndex((e) => e.name === v.name); @@ -4977,23 +4977,69 @@ export class LlEmitter { ); } + /* Ownership transfers name their destination. A heap reference is one + * that heap memory holds: a field, an array or map element, a capture + * box, a union box, a module global, a promise or generator slot, the + * exception cell, or any other runtime structure. A frame reference is + * one that a local, a parameter, a return value, a stack object or a + * temporary holds. Both forms of each operation emit the same code. */ + + /** Ends a temp's frame ownership: its reference moves into heap memory. */ + toHeap(v: LlValue): void { + this.disownTemp(v); + } + + /** Ends a temp's frame ownership: its reference moves to another frame + * owner (a local or parameter slot, a callee's parameter, a return + * value, a stack object, another temp). */ + toFrame(v: LlValue): void { + this.disownTemp(v); + } + /** The retained (+1) read of a refcounted value — type-directed through * the `_v` table (immortals skip, exactly the C retain calls). */ - retainValue(name: string, type: IrType): string { + private retainCall(name: string, type: IrType): string { if (this.isImmortal(name)) return name; const t = this.B.tmp(); this.B.line(`${t} = call ptr ${retainSym(this.shapeHost, type)}(ptr ${name})`); return t; } + /** A +1 of `name` that heap memory keeps. */ + retainHeap(name: string, type: IrType): string { + return this.retainCall(name, type); + } + + /** A +1 of `name` that a frame owns. */ + retainFrame(name: string, type: IrType): string { + return this.retainCall(name, type); + } + /** The release call for one owned refcounted value — type-directed like * releaseCallC (all runtime releases are NULL-tolerant). */ - releaseValue(name: string, type: IrType): void { + private releaseCall(name: string, type: IrType): void { if (this.isImmortal(name)) return; this.B.line(`call void ${releaseSym(this.shapeHost, type)}(ptr ${name})`); } - releaseFrame(frame: LlValue[]): void { + /** Releases a reference that heap memory held. */ + releaseHeap(name: string, type: IrType): void { + this.releaseCall(name, type); + } + + /** Releases a reference that a frame owned. */ + releaseFrame(name: string, type: IrType): void { + this.releaseCall(name, type); + } + + /** A +1 that the runtime made with the heap retain (a box, generator or + * await read) becomes a frame value. Emits nothing; answers `name`. */ + adoptHeap(name: string, _type: IrType): string { + return name; + } + + /** Releases a statement frame's temps and owned slots. */ + releaseTemps(frame: LlValue[]): void { for (const v of frame) { if (v.slot) { const t = this.B.tmp(); @@ -5001,10 +5047,10 @@ export class LlEmitter { if (v.boxed) { this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`); } else { - this.releaseValue(t, v.type); + this.releaseFrame(t, v.type); } } else { - this.releaseValue(v.name, v.type); + this.releaseFrame(v.name, v.type); } } } @@ -5029,7 +5075,7 @@ export class LlEmitter { } else if (e.boxed) { this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`); } else { - this.releaseValue(t, e.type); // runtime releases are NULL-tolerant + this.releaseFrame(t, e.type); // runtime releases are NULL-tolerant } } } @@ -5039,7 +5085,7 @@ export class LlEmitter { * everything whose normal fall-through releases the jump bypasses. * Release every scope crossed by the control-flow edge. */ private releaseForJump(frameDepth: number, scopeDepth: number): void { - for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!); + for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseTemps(this.frames[i]!); for (let i = this.scopes.length - 1; i >= scopeDepth; i--) this.releaseScope(this.scopes[i]!); } @@ -5112,7 +5158,7 @@ export class LlEmitter { const target = this.tryStack[this.tryStack.length - 1]; const frameDepth = target?.frameDepth ?? 0; const scopeDepth = target?.scopeDepth ?? 0; - for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!); + for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseTemps(this.frames[i]!); let terminator: string; if (target) { target.used = true; @@ -5642,7 +5688,7 @@ export class LlEmitter { if (target.value !== null) { this.frames.push([]); for (const read of speculativeReads(target.value)) reads.set(read, this.emitExpr(read)); - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); } this.frames.push([]); let result: string; @@ -5966,7 +6012,7 @@ export class LlEmitter { unionExtract(uName: string, unionId: string, arm: IrType): string { if (arm.kind === "f64" || arm.kind === "procStream") return this.unionGetF64(uName); if (arm.kind === "bool") return this.unionGetBool(uName); - return this.retainValue(this.unionPeek(uName, unionId), arm); + return this.retainFrame(this.unionPeek(uName, unionId), arm); } /** A scalar f64 arm's payload (scr_union_get_f64 inlined). Inline loads @@ -6208,11 +6254,11 @@ export class LlEmitter { * in, and the old value is released AFTER the field is overwritten. */ replaceField(ptr: string, type: IrType, inline: boolean, v: LlValue): void { if (!inline && isRefCounted(type)) { - this.moveTemp(v); + this.toHeap(v); const old = this.B.tmp(); this.B.line(`${old} = load ptr, ptr ${ptr}`); this.storeField(ptr, type, v.name, false); - this.releaseValue(old, type); + this.releaseHeap(old, type); return; } this.storeField(ptr, type, v.name, inline); @@ -6220,7 +6266,7 @@ export class LlEmitter { /** Stores `v` (from emitFieldValue) into a fresh field slot. */ initField(ptr: string, type: IrType, inline: boolean, v: LlValue): void { - if (!inline && isRefCounted(type)) this.moveTemp(v); + if (!inline && isRefCounted(type)) this.toHeap(v); this.storeField(ptr, type, v.name, inline); } @@ -6386,7 +6432,7 @@ export class LlEmitter { if (!stack) B.line(`${box} = ${boxNewCall(this.shapeHost, type)}`); const value = B.tmp(); B.line(`${value} = load ${this.llType(type)}, ptr %${mangleLocal(localId)}`); - this.boxSet(box, type, isRefCounted(type) ? this.retainValue(value, type) : value); + this.boxSet(box, type, isRefCounted(type) ? this.retainHeap(value, type) : value); B.line(`store ptr ${box}, ptr ${cache}`); B.br(join); B.startBlock(join); @@ -6532,7 +6578,7 @@ export class LlEmitter { this.boxSet( box, local.type, - borrowed && isRefCounted(local.type) ? this.retainValue(value, local.type) : value, + borrowed && isRefCounted(local.type) ? this.retainHeap(value, local.type) : value, ); } @@ -7557,14 +7603,14 @@ export class LlEmitter { B.line(`${present} = icmp ne ptr %result, null`); B.condBr(present, own, done); B.startBlock(own); - B.line(`store ptr ${this.retainValue("%result", fn.returnType)}, ptr ${slot}`); + B.line(`store ptr ${this.retainFrame("%result", fn.returnType)}, ptr ${slot}`); B.br(done); B.startBlock(done); result = B.tmp(); B.line(`${result} = load ptr, ptr ${slot}`); } for (const index of borrowed) - if (!kept.has(index)) this.releaseValue(`%p${index}`, fn.params[index]!.type); + if (!kept.has(index)) this.releaseFrame(`%p${index}`, fn.params[index]!.type); B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`); const symbol = kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name); @@ -7725,7 +7771,7 @@ export class LlEmitter { private emitAdoptingReturn(s: IrStmt & { kind: "return" }, adopted: ExprOf<"awaitExpr">): void { const B = this.B; const promise = this.emitExpr(adopted.value); - this.moveTemp(promise); + this.toFrame(promise); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ptr ; adopted return promise`); B.line(`store ptr ${promise.name}, ptr ${slot}`); @@ -7759,7 +7805,8 @@ export class LlEmitter { const v = this.emitExpr(s.value!); if (this.adoptedOperand !== null) throw new InternalCompilerError("llvm emitter bug: adopted return promise was not awaited"); - this.moveTemp(v); + if (this.currentCoro !== null) this.toHeap(v); + else this.toFrame(v); this.releaseForJump(0, 0); if (!B.isTerminated()) { if (this.currentCoro !== null) { @@ -8104,7 +8151,7 @@ export class LlEmitter { const union = emitStackUnion(this, localUnion); B.line(`store ptr ${union.value.name}, ptr ${b.slot}`); if (union.payload) { - this.moveTemp(union.payload); + this.toFrame(union.payload); this.scopes[this.scopes.length - 1]!.push({ slot: union.payloadSlot, type: union.payload.type, @@ -8151,7 +8198,7 @@ export class LlEmitter { if (stack.owner) this.scopes[this.scopes.length - 1]!.push(stack.owner); if (s.init) { const value = this.emitExpr(s.init); - if (isRefCounted(value.type)) this.moveTemp(value); + if (isRefCounted(value.type)) this.toFrame(value); this.boxSet(stack.box, b.type, value.name); } break; @@ -8178,7 +8225,7 @@ export class LlEmitter { break; } const v = this.emitExpr(s.init); - if (isRefCounted(v.type)) this.moveTemp(v); // the box takes ownership + if (isRefCounted(v.type)) this.toHeap(v); // the box takes ownership if (b.local!.tdz === true && boxAccess(b.type) !== "ref") { this.tdzScalarInit(box, b.type, v.name); } else { @@ -8236,7 +8283,8 @@ export class LlEmitter { break; } const v = this.emitExpr(s.init); - this.moveTemp(v); + if (b.kind === "global") this.toHeap(v); + else this.toFrame(v); B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`); this.storeIntegerView(s.localId, v, s.init); if (isRefCounted(b.type)) { @@ -8259,10 +8307,10 @@ export class LlEmitter { const param = this.binding(s.localId); const slot = param.slot; const v = this.emitExpr(s.value); - this.moveTemp(v); + this.toFrame(v); const old = B.tmp(); B.line(`${old} = load ptr, ptr ${owner}`); - this.releaseValue(old, param.type); + this.releaseFrame(old, param.type); B.line(`store ptr ${v.name}, ptr ${slot}`); B.line(`store ptr ${v.name}, ptr ${owner}`); break; @@ -8289,7 +8337,7 @@ export class LlEmitter { B.line(`${bits} = load ${struct}, ptr ${value.name}`); B.line(`store ${struct} ${bits}, ptr ${target}`); this.disownSlots(owned); - for (const field of old) this.releaseValue(field.name, field.type); + for (const field of old) this.releaseFrame(field.name, field.type); break; } const localStorage = this.localUnionStorage.get(s.localId); @@ -8313,14 +8361,16 @@ export class LlEmitter { if (b.kind === "boxed") { this.writeBindingBox(this.loadBox(b.slot), b.local!, v.name, s.initializes); // The RHS remains frame-owned until a possible TDZ throw passes. - if (isRefCounted(v.type)) this.moveTemp(v); + if (isRefCounted(v.type)) this.toHeap(v); break; } - this.moveTemp(v); + if (b.kind === "global") this.toHeap(v); + else this.toFrame(v); if (isRefCounted(b.type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${b.slot}`); - this.releaseValue(old, b.type); + if (b.kind === "global") this.releaseHeap(old, b.type); + else this.releaseFrame(old, b.type); } B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`); this.storeIntegerView(s.localId, v, s.value); @@ -8371,7 +8421,7 @@ export class LlEmitter { throw new InternalCompilerError("llvm emitter bug: arraySet on non-array"); const acc = elemAccess(s.arr.type.elem); this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); - if (acc === "ref") this.moveTemp(v); + if (acc === "ref") this.toHeap(v); emitDenseArraySet( this, arr.name, @@ -8436,7 +8486,7 @@ export class LlEmitter { if (acc === "ref" && isRefCounted(elem)) { const old = B.tmp(); B.line(`${old} = inttoptr i64 ${raw} to ptr`); - this.releaseValue(old, elem); + this.releaseHeap(old, elem); } B.br(done); B.startBlock(call); @@ -8586,7 +8636,7 @@ export class LlEmitter { const v = this.emitExpr(s.value); if (this.publishedTypes?.recordGuarded(s.shapeId) === true) this.emitPublishedFieldGuard(obj.name, null, "[key]"); - if (isRefCounted(v.type)) this.moveTemp(v); + if (isRefCounted(v.type)) this.toHeap(v); const shape = this.recordShape(s.shapeId); // Signature-free shapes dispatch over their (one-typed) declared // fields and TRAP on a miss (scr_record_key_miss — JS would add @@ -8652,12 +8702,12 @@ export class LlEmitter { if (inline) { // The slot copies the checked value's bits; nv is released below. this.storeField(ptr, type, nv, true); - this.releaseValue(nv, type); + this.releaseFrame(nv, type); } else if (isRefCounted(type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${ptr}`); this.storeField(ptr, type, nv, false); - this.releaseValue(old, type); + this.releaseHeap(old, type); } else { this.storeField(ptr, type, nv, false); } @@ -8687,12 +8737,12 @@ export class LlEmitter { if (inline) { // The write owns the moved-in value; the slot copies its bits. this.storeField(ptr, type, v.name, true); - this.releaseValue(v.name, type); + this.releaseHeap(v.name, type); } else if (isRefCounted(type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${ptr}`); this.storeField(ptr, type, v.name, false); - this.releaseValue(old, type); + this.releaseHeap(old, type); } else { this.storeField(ptr, type, v.name, false); } @@ -8703,7 +8753,7 @@ export class LlEmitter { // The MISS on a fixed shape: release the moved-in value, throw // the catchable TypeError naming the key (scr_record_key_miss — // JS would add the property, the documented divergence). - if (isRefCounted(iv)) this.releaseValue(v.name, iv); + if (isRefCounted(iv)) this.releaseHeap(v.name, iv); this.declare(`declare void @scr_record_key_miss(ptr)`); B.line(`call void @scr_record_key_miss(ptr ${key.name})`); B.br(join); @@ -9346,7 +9396,8 @@ export class LlEmitter { } else v = this.emitReadReceiver(s.value); } else if (s.value !== null) { v = this.emitExpr(s.value); - this.moveTemp(v); + if (this.currentCoro !== null) this.toHeap(v); + else this.toFrame(v); } // An async generator's `return v` awaits v before any crossed // finally runs. Past finally regions that turn is taken here and @@ -9415,7 +9466,7 @@ export class LlEmitter { // unwind unconditionally (the innermost try handler, or out of the // function) — the same release path as return/break/continue. const v = this.emitExpr(s.value); - if (isRefCounted(s.value.type)) this.moveTemp(v); // the cell takes ownership + if (isRefCounted(s.value.type)) this.toHeap(v); // the cell takes ownership this.emitThrowValue({ name: v.name, type: s.value.type }); this.emitUnwind(); break; @@ -9461,7 +9512,7 @@ export class LlEmitter { s.kind !== "rethrow" && s.kind !== "runtimeFence" ) { - this.releaseFrame(frame); + this.releaseTemps(frame); } } @@ -9673,7 +9724,7 @@ export class LlEmitter { } else { B.line(`${hit} = fcmp oeq double ${disc.name}, ${t.name}`); } - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); const next = B.newLabel("sw.t"); B.condBr(hit, caseLabels[i]!, next); B.startBlock(next); @@ -9714,7 +9765,7 @@ export class LlEmitter { private emitCondition(cond: IrExpr): string { const v = this.emitExpr(cond); const frame = this.currentFrame(); - this.releaseFrame(frame); + this.releaseTemps(frame); frame.length = 0; return v.name; } @@ -9726,11 +9777,11 @@ export class LlEmitter { emitBranchInto(slot: string, expr: IrExpr, integerSlot?: string): boolean { this.frames.push([]); const v = this.emitExpr(expr); - this.moveTemp(v); + this.toFrame(v); this.B.line(`store ${this.llType(expr.type)} ${v.name}, ptr ${slot}`); if (integerSlot !== undefined && v.uint32 !== undefined) this.B.line(`store i32 ${v.uint32}, ptr ${integerSlot}`); - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); return v.uint32 !== undefined; } @@ -9884,12 +9935,13 @@ export class LlEmitter { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${b.slot}`); B.line(`store ptr null, ptr ${b.slot}`); - this.releaseValue(old, b.type); + if (b.kind === "global") this.releaseHeap(old, b.type); + else this.releaseFrame(old, b.type); } let raw: string; if (charCode !== null) { // The snapshot moves into the consuming character append. - this.moveTemp(snapshot); + this.toFrame(snapshot); this.declare(`declare ptr @scr_str_concat_move_char_code(ptr, double)`); raw = B.tmp(); B.line( @@ -9904,11 +9956,15 @@ export class LlEmitter { } const result = this.own({ name: raw, type: left.type }); if (retainForYield) { - const stored = this.retainValue(result.name, result.type); + const stored = + b.kind === "local" + ? this.retainFrame(result.name, result.type) + : this.retainHeap(result.name, result.type); if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, stored); else B.line(`store ptr ${stored}, ptr ${b.slot}`); } else { - this.moveTemp(result); + if (b.kind === "local") this.toFrame(result); + else this.toHeap(result); if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, result.name); else B.line(`store ptr ${result.name}, ptr ${b.slot}`); } @@ -9971,7 +10027,7 @@ export class LlEmitter { if (owned) { const old = this.loadField(ptr, fieldType, false); this.storeField(ptr, fieldType, "null", false); - this.releaseValue(old, fieldType); + this.releaseHeap(old, fieldType); } let raw: string; if (right === null) { @@ -10416,7 +10472,7 @@ export class LlEmitter { } // The result IS the operand: ownership moves to the result temp. if (!isRefCounted(value.type)) return { name: value.name, type: e.type }; - this.moveTemp(value); + this.toFrame(value); return this.own({ name: value.name, type: e.type }); } @@ -10669,7 +10725,7 @@ export class LlEmitter { this.frames.push([]); const result = this.emitReadReceiver(value); B.line(`store ptr ${result.name}, ptr ${slot}`); - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); B.br(join); } B.startBlock(join); @@ -10765,7 +10821,7 @@ export class LlEmitter { const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field); const p = this.B.tmp(); this.B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); - const owner = this.own({ name: this.retainValue(p, nullable.arm), type: nullable.arm }); + const owner = this.own({ name: this.retainFrame(p, nullable.arm), type: nullable.arm }); return { box: this.stackNullableBox(owner.name, nullable), owner }; } @@ -10804,7 +10860,7 @@ export class LlEmitter { B.line(`store ptr ${this.unitInstanceRef(nullable.unionId, nullable.unitTag)}, ptr ${slot}`); B.br(join); B.startBlock(ref); - const owned = this.retainValue(p, nullable.arm); + const owned = this.retainHeap(p, nullable.arm); const box = this.unionNewOwned(nullable.unionId, nullable.refTag, { name: owned, type: nullable.arm, @@ -10827,7 +10883,7 @@ export class LlEmitter { B.line(`${isRef} = icmp eq i32 ${tag}, ${nullable.refTag}`); const payload = this.unionPeek(u, nullable.unionId); B.line(`${sel} = select i1 ${isRef}, ptr ${payload}, ptr null`); - return this.retainValue(sel, nullable.arm); + return this.retainHeap(sel, nullable.arm); } /** `obj.f = value` for a nullable-pointer field: the new instance pointer @@ -10852,7 +10908,7 @@ export class LlEmitter { ) { if (value.tag === nullable.refTag) { const inner = this.emitExpr(value.value); - this.moveTemp(inner); + this.toHeap(inner); next = inner.name; } else { // Unit payloads carry nothing; a void payload runs for effects. @@ -10868,7 +10924,7 @@ export class LlEmitter { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${ptr}`); B.line(`store ptr ${next}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); - this.releaseValue(old, nullable.arm); + this.releaseHeap(old, nullable.arm); } /** A union binding whose slot may hold a private stack box: stack locals diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 6731e13c9..6da63977d 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -162,7 +162,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic const p = B.tmp(); B.line(`${p} = call ptr @scr_promise_new()`); const out = host.own({ name: p, type: e.type }); - host.moveTemp(reason); // the cell takes ownership + host.toHeap(reason); // the cell takes ownership host.emitThrowValue({ name: reason.name, type: reasonT }); B.line(`call void @scr_promise_reject_pending(ptr ${p})`); return out; @@ -191,12 +191,12 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic host.declare(`declare void @scr_promise_fulfill_bool(ptr, i1 zeroext)`); B.line(`call void @scr_promise_fulfill_bool(ptr ${p}, i1 ${v.name})`); } else if (t.kind === "string") { - host.moveTemp(v); + host.toHeap(v); host.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`); B.line(`call void @scr_promise_fulfill_str(ptr ${p}, ptr ${v.name})`); } else { const rc = vAdapters(host.shapeHost, t); - host.moveTemp(v); + host.toHeap(v); host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); B.line( `call void @scr_promise_fulfill_ref(ptr ${p}, ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`, @@ -537,7 +537,7 @@ export function emitAsyncExpr( B.line(`call void @scr_gen_yield_bool(i1 ${v.name})`); } } else { - host.moveTemp(v); // the OUT slot takes ownership + host.toHeap(v); // the OUT slot takes ownership if (host.currentCoro !== null) { const coro = host.currentCoro; const release = vAdapters(host.shapeHost, yt).release; @@ -601,7 +601,7 @@ export function emitAsyncExpr( // Refcounted channels: the slot's +1 moves out. host.declare(`declare ptr @scr_gen_take_in_ref()`); B.line(`${t} = call ptr @scr_gen_take_in_ref()`); - return host.own({ name: t, type: e.type }); + return host.own({ name: host.adoptHeap(t, e.type), type: e.type }); } case "genResume": { // One consumer resume: park the sent value (typed per mode), hop @@ -731,7 +731,7 @@ export function emitAsyncExpr( ); host.declare(`declare ptr @scr_gen_take_out_ref(ptr)`); B.line(`${t} = call ptr @scr_gen_take_out_ref(ptr ${gb.name})`); - return host.own({ name: t, type: e.type }); + return host.own({ name: host.adoptHeap(t, e.type), type: e.type }); } // Valueless: an empty IN slot reads as undefined on any channel. host.declare(`declare zeroext i1 @scr_gen_step(ptr)`); @@ -748,7 +748,7 @@ export function emitAsyncExpr( const g = host.emitExpr(e.gen); // borrowed for the calls below const sendArg = (store: (aName: string, t: IrType) => void): void => { const a = host.emitExpr(e.arg!); - if (isRefCounted(e.arg!.type)) host.moveTemp(a); // the slot takes ownership + if (isRefCounted(e.arg!.type)) host.toHeap(a); // the slot takes ownership store(a.name, e.arg!.type); }; const parkIn = (name: string, t: IrType): void => { @@ -786,7 +786,7 @@ export function emitAsyncExpr( } else { const a = host.emitExpr(e.arg); const t = e.arg.type; - if (isRefCounted(t)) host.moveTemp(a); + if (isRefCounted(t)) host.toHeap(a); if (t.kind === "f64" || t.kind === "date") { host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`); call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`; @@ -805,7 +805,7 @@ export function emitAsyncExpr( } else { const a = host.emitExpr(e.arg); const t = e.arg.type; - if (isRefCounted(t)) host.moveTemp(a); + if (isRefCounted(t)) host.toHeap(a); if (t.kind === "f64" || t.kind === "date") { host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`); call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`; @@ -823,7 +823,7 @@ export function emitAsyncExpr( "llvm emitter bug: async genResume throw with no payload", ); const a = host.emitExpr(e.arg); - if (isRefCounted(e.arg.type)) host.moveTemp(a); + if (isRefCounted(e.arg.type)) host.toHeap(a); host.emitThrowValue({ name: a.name, type: e.arg.type }); host.declare(`declare ptr @scr_async_gen_throw(ptr)`); call = `call ptr @scr_async_gen_throw(ptr ${g.name})`; @@ -873,7 +873,7 @@ export function emitAsyncExpr( if (e.arg === null) throw new InternalCompilerError("llvm emitter bug: genResume throw with no payload"); const a = host.emitExpr(e.arg); - if (isRefCounted(e.arg.type)) host.moveTemp(a); // the cell takes ownership + if (isRefCounted(e.arg.type)) host.toHeap(a); // the cell takes ownership host.emitThrowValue({ name: a.name, type: e.arg.type }); host.declare(`declare void @scr_gen_resume_throw(ptr)`); B.line(`call void @scr_gen_resume_throw(ptr ${g.name})`); @@ -955,7 +955,7 @@ export function emitAsyncExpr( host.declare(`declare ptr @${refAwait}(ptr)`); B.line(`${t} = call ptr @${refAwait}(ptr ${pr.name})`); } - const out = host.own({ name: t, type: e.type }); + const out = host.own({ name: host.adoptHeap(t, e.type), type: e.type }); host.emitPendingCheck(); return out; } diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index b1d810a3f..6446a581b 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -165,7 +165,7 @@ export function emitCallExpr( // This also preserves snapshots when a later argument mutates its // source, and releases earlier arguments if evaluation throws. args.forEach((a, index) => { - if (!borrowed?.has(index)) host.moveTemp(a); + if (!borrowed?.has(index)) host.toFrame(a); }); const abi = awaitEntry ? undefined : host.int32Abis.get(e.callee); const argList = args @@ -224,14 +224,14 @@ export function emitCallExpr( if (e.type.kind === "void") { B.line(`call void ${target}(${argList})`); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); - if (borrowed) host.releaseFrame(host.frames.pop()!); + if (borrowed) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); if (abi?.union) { B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); - if (borrowed) host.releaseFrame(host.frames.pop()!); + if (borrowed) host.releaseTemps(host.frames.pop()!); if (stackResult) return host.stackScalarUnion(t, e.type); return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type }); } @@ -239,7 +239,7 @@ export function emitCallExpr( B.line(`${t} = call i32 ${target}(${argList})`); const out = host.int32Result(t, e.type); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); - if (borrowed) host.releaseFrame(host.frames.pop()!); + if (borrowed) host.releaseTemps(host.frames.pop()!); return out; } B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`); @@ -249,14 +249,14 @@ export function emitCallExpr( // test the exception first, then own the result before the // argument snapshots that keep it reachable are released. if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); - out = host.own({ name: host.retainValue(t, e.type), type: e.type }); + out = host.own({ name: host.retainFrame(t, e.type), type: e.type }); } else { out = host.own({ name: t, type: e.type }); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); } if (borrowed) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; @@ -718,7 +718,7 @@ export function emitCallExpr( // every mention yields the same pointer, so `f === f` is true. host.fnValues.add(e.fnName); return host.own({ - name: host.retainValue(`@${mangleFnClosure(e.fnName)}`, e.type), + name: host.retainFrame(`@${mangleFnClosure(e.fnName)}`, e.type), type: e.type, }); } @@ -880,7 +880,7 @@ export function emitCallExpr( lent?.has(i) && lend[i] ? host.emitReadReceiver(a) : host.emitExpr(a), ); args.forEach((a, i) => { - if (!lent?.has(i)) host.moveTemp(a); + if (!lent?.has(i)) host.toFrame(a); }); const argListOf = (values: readonly LlValue[]): string => [ @@ -891,7 +891,7 @@ export function emitCallExpr( const ownedArgs = (): LlValue[] => lent ? args.map((a, i) => - lent.has(i) ? { name: host.retainValue(a.name, a.type), type: a.type } : a, + lent.has(i) ? { name: host.retainFrame(a.name, a.type), type: a.type } : a, ) : args; const invoke = (target: string, needsReceiver: boolean, values = args): string => { @@ -978,7 +978,7 @@ export function emitCallExpr( } if (e.type.kind === "void") { if (host.indirectMayThrow) host.emitPendingCheck(); - if (lent) host.releaseFrame(host.frames.pop()!); + if (lent) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } // The check runs AFTER the result temp joins the frame: an unwind @@ -986,15 +986,15 @@ export function emitCallExpr( const out = host.own({ name: t, type: e.type }); if (host.indirectMayThrow) host.emitPendingCheck(); if (lent) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; } case "selfRef": // The running closure itself (env is borrowed; the result owned). - return host.own({ name: host.retainValue("%sc_env", e.type), type: e.type }); + return host.own({ name: host.retainFrame("%sc_env", e.type), type: e.type }); case "new": { // Allocate (fields zeroed, vt stamped), then run the ctor. The // ctor owns and releases its `this` param like any callee, so it @@ -1034,7 +1034,7 @@ export function emitCallExpr( }); return host.own({ name: value, type: e.type }); } - return host.own({ name: host.retainValue(`@${sym}`, e.type), type: e.type }); + return host.own({ name: host.retainFrame(`@${sym}`, e.type), type: e.type }); } case "newValue": { // Construction through a class VALUE: call the class object's @@ -1050,7 +1050,7 @@ export function emitCallExpr( ); const callee = host.emitExpr(e.callee); const args = e.args.map((a) => host.emitExpr(a)); - for (const a of args) host.moveTemp(a); // the constructor owns its params + for (const a of args) host.toFrame(a); // the constructor owns its params const ctorp = B.tmp(); const thunk = B.tmp(); B.line(`${ctorp} = getelementptr inbounds %ScrClassObj, ptr ${callee.name}, i64 0, i32 3`); @@ -1121,7 +1121,7 @@ export function emitCallExpr( // One ScrPromise* either way — ownership transfers, type-only //. const v = host.emitExpr(e.value); - host.moveTemp(v); + host.toFrame(v); return host.own({ name: v.name, type: e.type }); } case "upcast": @@ -1134,7 +1134,7 @@ export function emitCallExpr( const v = host.emitExpr(e.value); if (e.kind === "downcast" && e.type.kind === "object" && v.type.kind === "object") emitDowncastGuard(host, v.name, v.type.className, e.type.className); - if (isRefCounted(v.type)) host.moveTemp(v); + if (isRefCounted(v.type)) host.toFrame(v); return host.own({ name: v.name, type: e.type }); } case "instanceOf": { @@ -1188,7 +1188,7 @@ export function emitCallExpr( borrowed.has(index) && inputs[index] ? host.emitReadReceiver(a) : host.emitExpr(a), ); args.forEach((a, index) => { - if (!borrowed.has(index)) host.moveTemp(a); + if (!borrowed.has(index)) host.toFrame(a); }); const recv = args[0]!.name; const vtp = B.tmp(); @@ -1239,7 +1239,7 @@ export function emitCallExpr( B.startBlock(done); if (e.type.kind === "void") { if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); - if (borrowed.size > 0) host.releaseFrame(host.frames.pop()!); + if (borrowed.size > 0) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); @@ -1247,8 +1247,8 @@ export function emitCallExpr( const out = host.own({ name: t, type: e.type }); if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); if (borrowed.size > 0) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; @@ -1256,7 +1256,7 @@ export function emitCallExpr( if (e.type.kind === "void") { B.line(`call void ${fn}(${argList})`); if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); - if (borrowed.size > 0) host.releaseFrame(host.frames.pop()!); + if (borrowed.size > 0) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); @@ -1264,8 +1264,8 @@ export function emitCallExpr( const out = host.own({ name: t, type: e.type }); if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); if (borrowed.size > 0) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; @@ -1392,7 +1392,7 @@ export function emitConstruction( return host.emitExpr(a); }); args.forEach((a, i) => { - if (!borrowed.has(i + 1)) host.moveTemp(a); + if (!borrowed.has(i + 1)) host.toFrame(a); }); const self = borrowed.has(0) ? o @@ -1408,11 +1408,11 @@ export function emitConstruction( `call void @${mangleBorrowedFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`, ); if (host.mayThrow.has(ctorName)) host.emitPendingCheck(); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); return; } const args = e.args.map((a) => host.emitExpr(a)); - for (const a of args) host.moveTemp(a); + for (const a of args) host.toFrame(a); const r = B.tmp(); B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`); const argList = [`ptr ${r}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(", "); @@ -1454,8 +1454,8 @@ function emitNumericIndexRead( B.startBlock(slow); host.frames.push([]); const read = emitCallExpr(host, e, false, false, undefined, true); - host.moveTemp(read); - host.releaseFrame(host.frames.pop()!); + host.toFrame(read); + host.releaseTemps(host.frames.pop()!); const slowLabel = B.currentLabel(); B.terminate(`br label %${join}`); B.startBlock(join); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 22a0b4df5..a04f46f7b 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -118,7 +118,7 @@ export function emitArrayValues( sharedBuffer?: string, ): string { const B = host.B; - if (acc === "ref") values.forEach((value) => host.moveTemp(value)); + if (acc === "ref") values.forEach((value) => host.toHeap(value)); if (values.length === 0) { host.declare("declare double @scr_arr_len(ptr)"); const result = B.tmp(); @@ -219,7 +219,7 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void { B.startBlock(label); host.frames.push([]); emitDiscarded(host, arm); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); B.br(lj); } B.startBlock(lj); @@ -235,7 +235,7 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void { const start = host.emitExpr(e.args[0]!); const count = e.args[1] ? host.emitExpr(e.args[1]).name : F64_INF; const values = items.map((item) => host.emitExpr(item)); - if (acc === "ref") values.forEach((value) => host.moveTemp(value)); + if (acc === "ref") values.forEach((value) => host.toHeap(value)); let buffer = "null"; if (values.length > 0) { buffer = B.tmp(); @@ -1069,7 +1069,7 @@ export function emitMapNew(host: LlvmEmitterContext, e: IrExpr & { kind: "mapNew for (const pair of e.seed ?? []) { const k = emitBorrowedInput(host, pair.key); const v = host.emitExpr(pair.value); - if (vAcc === "ref") host.moveTemp(v); // the value MOVES in + if (vAcc === "ref") host.toHeap(v); // the value MOVES in host.mapSet(m, kAcc, vAcc, k.name, v.name); } return out; @@ -1370,7 +1370,7 @@ export function emitMapLikeIntrinsic( const k = emitBorrowedInput(host, e.args[0]!); const v = host.emitExpr(e.args[1]!); const vAcc = elemAccess(receiverType.value); - if (vAcc === "ref") host.moveTemp(v); + if (vAcc === "ref") host.toHeap(v); const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name); if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) { const bits = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index fd4bc8787..fd2061aae 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -58,7 +58,7 @@ export function emitControlExpr( // and the right runs in its own frame. const ty = host.llType(e.type); const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ${ty}`); B.line(`store ${ty} ${l.name}, ptr ${slot}`); @@ -73,7 +73,7 @@ export function emitControlExpr( if (e.op === "&&") B.condBr(truthy, rightLabel, joinLabel); else B.condBr(truthy, joinLabel, rightLabel); B.startBlock(rightLabel); - if (isRefCounted(e.type)) host.releaseValue(l.name, e.type); + if (isRefCounted(e.type)) host.releaseFrame(l.name, e.type); const rightInteger = host.emitBranchInto(slot, e.right, integerSlot); B.br(joinLabel); B.startBlock(joinLabel); @@ -157,7 +157,7 @@ export function emitControlExpr( host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true }); host.frames.push([]); host.emitExpr(e.body); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); host.chainSlots.delete(e.id); B.br(lj); B.startBlock(lj); @@ -203,7 +203,7 @@ export function emitControlExpr( bindPresent(); host.frames.push([]); host.emitExpr(e.body); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); host.chainSlots.delete(e.id); B.br(lj); B.startBlock(lj); @@ -272,7 +272,7 @@ export function emitControlExpr( const v = B.tmp(); B.line(`${v} = load ${host.llType(bound.type)}, ptr ${bound.name}`); if (!isRefCounted(e.type)) return { name: v, type: e.type }; - return host.own({ name: host.retainValue(v, e.type), type: e.type }); + return host.own({ name: host.retainFrame(v, e.type), type: e.type }); } case "orDefault": { // `u || d` narrowed to the single non-unit arm: nullish's dance @@ -309,7 +309,7 @@ export function emitControlExpr( return host.own({ name: t, type: e.type }); } const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); const ty = host.llType(e.type); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ${ty}`); @@ -337,11 +337,11 @@ export function emitControlExpr( B.line( `store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`, ); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); } B.br(lj); B.startBlock(lf); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); host.emitBranchInto(slot, e.right); B.br(lj); B.startBlock(lj); @@ -359,7 +359,7 @@ export function emitControlExpr( // The native checked-value kind decides whether the left is // nullish; the right runs lazily in its branch. const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); host.declare(`declare zeroext i1 @scr_dyn_is_nullish(ptr)`); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ptr`); @@ -370,7 +370,7 @@ export function emitControlExpr( const lj = B.newLabel("nuld.j"); B.condBr(isN, lu, lv); B.startBlock(lu); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); host.emitBranchInto(slot, e.right); B.br(lj); B.startBlock(lv); @@ -426,7 +426,7 @@ export function emitControlExpr( return host.own({ name: t, type: e.type }); } const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); const ty = host.llType(e.type); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ${ty}`); @@ -436,7 +436,7 @@ export function emitControlExpr( const lj = B.newLabel("nul.j"); B.condBr(isUnit, lu, lv); B.startBlock(lu); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); host.emitBranchInto(slot, e.right); B.br(lj); B.startBlock(lv); @@ -445,7 +445,7 @@ export function emitControlExpr( B.line(`store ${ty} ${l.name}, ptr ${slot}`); } else { B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); } B.br(lj); B.startBlock(lj); @@ -533,7 +533,7 @@ export function emitOptionalChainReceiver( const isUnit = host.tagInSet(r.name, def.id, unitTags); const extract = (): string => multiple - ? host.retainValue(r.name, narrowed) + ? host.retainFrame(r.name, narrowed) : borrowBind ? host.unionPeek(r.name, def.id) : host.unionExtract(r.name, def.id, narrowed); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 76df9795d..8a24fcf23 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -161,7 +161,7 @@ export function emitDynamicExpr( ); host.emitPendingCheck(); } else { - host.moveTemp(v); + host.toHeap(v); B.line(`call void @scr_dyn_arr_push(ptr ${pack}, ptr ${v.name})`); } }); @@ -253,7 +253,7 @@ export function emitDynamicExpr( const out = host.own({ name: arr, type: e.type }); for (const el of e.elems) { const v = host.emitExpr(el); - host.moveTemp(v); + host.toHeap(v); B.line(`call void @scr_dyn_arr_push(ptr ${arr}, ptr ${v.name})`); } return out; @@ -296,7 +296,7 @@ export function emitDynamicExpr( return host.own({ name: host.unitInstanceRef(e.unionId, e.tag), type: e.type }); } const v = host.emitExpr(e.value); - if (isRefCounted(arm)) host.moveTemp(v); + if (isRefCounted(arm)) host.toHeap(v); return host.own({ name: host.unionNewOwned(e.unionId, e.tag, v), type: e.type }); } case "unionNarrow": { @@ -353,7 +353,7 @@ export function emitDynamicExpr( const value = nullable ? host.nullableToOwnedUnion(v, nullable) : isRefCounted(e.type) - ? host.retainValue(v, e.type) + ? host.retainFrame(v, e.type) : v; B.line(`store ${ty} ${value}, ptr ${slot}`); B.br(join); @@ -465,7 +465,7 @@ export function emitDynamicExpr( const owned = inline ? host.inlineFieldUnion(ptr, ft) : isRefCounted(ft) - ? host.retainValue(v, ft) + ? host.retainFrame(v, ft) : v; B.line(`store ${ty} ${owned}, ptr ${slot}`); B.br(join); @@ -487,7 +487,7 @@ export function emitDynamicExpr( ft.kind === "f64" || ft.kind === "bool" ? host.unionNewOwned(e.type.unionId, tag, { name: v, type: ft }) : host.unionNewOwned(e.type.unionId, tag, { - name: host.retainValue(v, ft), + name: host.retainHeap(v, ft), type: ft, }); B.line(`store ptr ${wrapped}, ptr ${slot}`); @@ -1087,7 +1087,7 @@ export function emitDynamicExpr( B.line(`${pp} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 4`); B.line(`${payload} = load ptr, ptr ${pp}`); if (e.type.kind === "string") { - return host.own({ name: host.retainValue(payload, e.type), type: e.type }); + return host.own({ name: host.retainFrame(payload, e.type), type: e.type }); } if (e.type.kind === "object") { // Retain through the snapshot's own entry point (the payload's diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index e237f8f13..4e7d418f8 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -47,11 +47,11 @@ export function emitLiteralExpr( return { name: e.value ? "true" : "false", type: e.type }; case "strLit": { const sym = host.internLiteral(e.value); - return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } case "moduleNsRef": { const sym = host.internLiteral(`module:${e.moduleId}`); - return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } case "unitLit": // unitLits are consumed inline by the unionWrap case (a unit arm is @@ -110,10 +110,10 @@ export function emitLiteralExpr( const box = host.loadBox(b.slot); if (b.local!.tdz === true) { const v = host.tdzBoxRead(box, e.type, b.local!.name); - return host.own({ name: v, type: e.type }); + return host.own({ name: host.adoptHeap(v, e.type), type: e.type }); } const v = host.boxGet(box, e.type); - return host.own({ name: v, type: e.type }); + return host.own({ name: host.adoptHeap(v, e.type), type: e.type }); } const t = B.tmp(); B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}`); @@ -131,7 +131,7 @@ export function emitLiteralExpr( return host.markImmortal({ name: t, type: e.type }); // The binding's last read takes over its reference (lastUseRead). if (host.takeLastUseMove(e, b.slot, b.type)) return host.own({ name: t, type: e.type }); - return host.own({ name: host.retainValue(t, e.type), type: e.type }); + return host.own({ name: host.retainFrame(t, e.type), type: e.type }); } return { name: t, type: e.type }; } @@ -488,8 +488,11 @@ export function emitOperatorExpr( if (isRefCounted(b.type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${b.slot}`); - host.releaseValue(old, b.type); - B.line(`store ptr ${host.retainValue(v.name, v.type)}, ptr ${b.slot}`); + if (b.kind === "global") host.releaseHeap(old, b.type); + else host.releaseFrame(old, b.type); + B.line( + `store ptr ${b.kind === "global" ? host.retainHeap(v.name, v.type) : host.retainFrame(v.name, v.type)}, ptr ${b.slot}`, + ); } else { B.line(`store ${host.llType(b.type)} ${v.name}, ptr ${b.slot}`); host.storeIntegerView(e.localId, v, e.value); @@ -632,12 +635,12 @@ export function emitStringExpr( case "undefinedT": case "nullT": { const lit = host.internLiteral(arm.kind === "undefinedT" ? "undefined" : "null"); - B.line(`store ptr ${host.retainValue(lit, e.type)}, ptr ${slot}`); + B.line(`store ptr ${host.retainFrame(lit, e.type)}, ptr ${slot}`); break; } case "string": B.line( - `store ptr ${host.retainValue(host.unionPeek(v.name, def.id), e.type)}, ptr ${slot}`, + `store ptr ${host.retainFrame(host.unionPeek(v.name, def.id), e.type)}, ptr ${slot}`, ); break; case "f64": { @@ -691,7 +694,7 @@ export function emitStringExpr( // String(record) / `${record}`: Object.prototype.toString's // constant — the interned literal, retained like a strLit. const sym = host.internLiteral("[object Object]"); - return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } if (v.type.kind === "caught") { // String(e) / `${e}` on a catch binding: JS's String() over the @@ -748,7 +751,7 @@ export function emitStringExpr( }; host.templateStringsInstances.set(e.key, inst); } - return host.own({ name: host.retainValue(`@${inst.sym}`, e.type), type: e.type }); + return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type }); } case "regexIntrinsic": return host.emitRegexIntrinsic(e); @@ -975,7 +978,7 @@ export function emitRecordExpr( } const v = host.loadField(ptr, type, false); if (isRefCounted(e.type)) - return host.own({ name: host.retainValue(v, e.type), type: e.type }); + return host.own({ name: host.retainFrame(v, e.type), type: e.type }); return { name: v, type: e.type }; } case "recordGet": { @@ -984,7 +987,7 @@ export function emitRecordExpr( if (inline) return host.own({ name: host.inlineFieldUnion(ptr, type), type: e.type }); const v = host.loadRecordField(ptr, type, false); if (isRefCounted(e.type)) - return host.own({ name: host.retainValue(v, e.type), type: e.type }); + return host.own({ name: host.retainFrame(v, e.type), type: e.type }); return { name: v, type: e.type }; } case "recordLit": { @@ -1090,7 +1093,7 @@ export function emitRecordLitFields( if (f.overflow) { const lit = host.internLiteral(f.name); const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref"; - if (acc === "ref") host.moveTemp(v); + if (acc === "ref") host.toHeap(v); const ovf = host.recordOvfPtr(rec, shapeId); const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1 zeroext" : "ptr"; host.declare(`declare void @scr_map_set_str_${acc}(ptr, ptr, ${argTy})`); diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 0cbfe15dd..bca728e9d 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -41,7 +41,7 @@ export function emitUnionWiden( B.br(join); }); B.startBlock(join); - if (owned) host.releaseValue(value, { kind: "union", unionId: fromId }); + if (owned) host.releaseFrame(value, { kind: "union", unionId: fromId }); const result = B.tmp(); B.line(`${result} = load ptr, ptr ${slot}`); return result; @@ -58,7 +58,7 @@ export function emitUnionRetype( e: IrExpr & { kind: "unionRetype" }, ): LlValue { const value = host.emitExpr(e.value); - host.moveTemp(value); + host.toFrame(value); const from = host.nullableUnions.get(e.fromId); const to = host.nullableUnions.get(e.unionId); if (from && to) { @@ -131,7 +131,7 @@ function rebuildRetype( B.br(join); }); B.startBlock(join); - host.releaseValue(value, { kind: "union", unionId: e.fromId }); + host.releaseFrame(value, { kind: "union", unionId: e.fromId }); const result = B.tmp(); B.line(`${result} = load ptr, ptr ${slot}`); return result; @@ -486,10 +486,10 @@ export function keyedRecordReadInto( if (!typeEquals(vt, resultType)) { throw new LlvmUnsupportedError(`recordKeyGet:narrow:${vt.kind}`, loc); } - return owned || !isRefCounted(vt) ? expr : host.retainValue(expr, vt); + return owned || !isRefCounted(vt) ? expr : host.retainFrame(expr, vt); } if (typeEquals(vt, resultType)) { - return owned ? expr : host.retainValue(expr, vt); + return owned ? expr : host.retainFrame(expr, vt); } if (vt.kind === "union" && resultType.kind === "union") { return emitUnionWiden(host, expr, vt.unionId, resultType.unionId, owned); @@ -500,7 +500,7 @@ export function keyedRecordReadInto( if (vt.kind === "f64" || vt.kind === "bool") { return host.unionNewOwned(def.id, tag, { name: expr, type: vt }); } - const payload = owned ? expr : host.retainValue(expr, vt); + const payload = owned ? expr : host.retainHeap(expr, vt); return host.unionNewOwned(def.id, tag, { name: payload, type: vt }); }; for (const f of overflowOnly ? [] : shape.fields) { diff --git a/packages/compiler/src/backend/llvm/lib-abi.ts b/packages/compiler/src/backend/llvm/lib-abi.ts index 52d3201b7..6917d8256 100644 --- a/packages/compiler/src/backend/llvm/lib-abi.ts +++ b/packages/compiler/src/backend/llvm/lib-abi.ts @@ -85,7 +85,7 @@ export function callbackAdapter( symbol: string, params: string[], ): AbiArgument[] { - host.moveTemp(value); + host.toHeap(value); host.declare(`declare void @${symbol}(${params.join(", ")})`); return [ptr(value.name), ptr(`@${symbol}`)]; } diff --git a/packages/compiler/src/backend/llvm/lib-connect.ts b/packages/compiler/src/backend/llvm/lib-connect.ts index ba828fa48..ce9c40e21 100644 --- a/packages/compiler/src/backend/llvm/lib-connect.ts +++ b/packages/compiler/src/backend/llvm/lib-connect.ts @@ -47,7 +47,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L type = callbackType(cb), arity = type.params.length, second = type.params[1]; - host.moveTemp(cb); + host.toHeap(cb); let h1: string, h2 = "null"; if (second?.kind === "union") { diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 3fae845e2..b32446bcb 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -131,7 +131,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L case "dgram.bindCb": case "dgram.connect": case "dgram.connectCb": - if (args[3] !== undefined) host.moveTemp(args[3]); + if (args[3] !== undefined) host.toHeap(args[3]); return finishRuntimeCall( host, e, @@ -140,7 +140,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L ); case "dgram.close": case "dgram.closeCb": - if (args[1] !== undefined) host.moveTemp(args[1]); + if (args[1] !== undefined) host.toHeap(args[1]); return finishRuntimeCall(host, e, "scr_dgram_close", [a[0]!, a[1] ?? ptr()]); case "dgram.address": { const type = e.type; @@ -151,7 +151,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L const record = host.B.tmp(); host.B.line(`${record} = call ptr @${mangleRecordNew(type.shapeId)}()`); const out = host.own({ name: record, type }); - host.moveTemp(ip); + host.toHeap(ip); const shape = host.recordsById.get(type.shapeId); if (shape === undefined) throw new InternalCompilerError("missing datagram address shape"); for (const field of ["address", "family", "port"]) { @@ -185,7 +185,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L ? "scr_dgram_msg_thunk1" : messageAdapter(host, type.params[1]!); // Program-specific adapters are definitions, so do not redeclare them. - host.moveTemp(cb); + host.toHeap(cb); if (type.params.length < 2) host.declare(`declare void @${adapter}(ptr, ptr, ptr, ptr, double, double)`); return finishRuntimeCall(host, e, "scr_dgram_on_message", [ @@ -213,7 +213,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L case "dgram.onListening": case "dgram.onClose": case "dgram.onConnect": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall( host, e, @@ -227,7 +227,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L case "dns.lookup": { const cb = args[2]!, symbol = lookupAdapter(host, cb); - host.moveTemp(cb); + host.toHeap(cb); if (callbackType(cb).params.length === 0) host.declare("declare void @scr_dns_thunk0(ptr, ptr, ptr, double)"); return finishRuntimeCall(host, e, "scr_dns_lookup", [...a, ptr("@" + symbol)]); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 034f0b6f1..daa8e08f3 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -92,7 +92,7 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func"); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = cbT.params.length === 0 ? "scr_rl_answer_thunk0" @@ -165,7 +165,7 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue const ev = host.emitExpr(evArg); const cb = host.emitExpr(e.args[2]!); const once = host.emitExpr(e.args[3]!); - host.moveTemp(cb); + host.toHeap(cb); let adapter = "null"; if (evArg.value === 0 || evArg.value === 4) { const param = cbT.params[0]; @@ -287,7 +287,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[4]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: randomFill callback not a func"); - host.moveTemp(args[4]!); + host.toHeap(args[4]!); const dyn = e.fn === "crypto.randomFillCbDyn"; const adapter = host.cryptoBytesThunkFor(cbT, false, dyn ? "dyn" : "view"); const symbol = dyn ? "scr_crypto_random_fill_async_dyn" : "scr_crypto_random_fill_async"; @@ -305,7 +305,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: generateKey callback not a func"); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = host.cryptoBytesThunkFor(cbT, false, "dyn"); host.declare(`declare void @scr_crypto_generate_key_async(ptr, ptr, ptr, ptr)`); B.line( @@ -320,7 +320,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[3]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: randomInt callback not a func"); - host.moveTemp(args[3]!); + host.toHeap(args[3]!); const adapter = host.cryptoIntThunkFor(cbT); host.declare(`declare void @scr_crypto_random_int_async(double, double, double, ptr, ptr)`); B.line( @@ -429,7 +429,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[cbIndex]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: crypto callback not a func"); - host.moveTemp(args[cbIndex]!); + host.toHeap(args[cbIndex]!); const adapter = host.cryptoBytesThunkFor(cbT, e.fn === "crypto.hkdfCb"); if (e.fn === "crypto.randomBytesCb") { host.declare(`declare void @scr_crypto_random_bytes_async(double, ptr, ptr)`); @@ -486,7 +486,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[cbIdx]!.type; if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); - host.moveTemp(args[cbIdx]!); + host.toHeap(args[cbIdx]!); cb = args[cbIdx]!.name; const sym = cbT.params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res"; host.declare(`declare void @${sym}(ptr, ptr)`); @@ -722,7 +722,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { // Literals (`123n`) and constant BigInt("...") texts are immortal // statics instead of a parse at every evaluation. const sym = host.internBigIntLiteral(e.args[0].value); - if (sym) return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + if (sym) return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } if (e.fn.startsWith("bigint.")) { // Small-value operator trees run in 64-bit registers (bigint-wrap.ts). diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 74278789c..4798b1f09 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -193,7 +193,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const cbT = e.args[1]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: zlib callback not a func"); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = host.zlibBytesThunkFor(cbT); const compressing = e.fn === "zlib.deflateCb" || e.fn === "zlib.deflateRawCb" || e.fn === "zlib.gzipCb"; @@ -246,7 +246,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const cbT = e.args[3]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: zlib callback not a func"); - host.moveTemp(args[3]!); + host.toHeap(args[3]!); const adapter = host.zlibBytesThunkFor(cbT); const sym = e.fn === "zlib.cbOpts" ? "scr_zlib_codec_async_opts" : "scr_zlib_codec_async_opts_dyn"; @@ -293,7 +293,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const operands = args.slice(0, withCb ? -1 : undefined).map((a) => `ptr ${a.name}`); let cb = "null"; if (withCb) { - host.moveTemp(args[args.length - 1]!); + host.toHeap(args[args.length - 1]!); cb = args[args.length - 1]!.name; } const sym = @@ -356,7 +356,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: fs.rename callback not a func"); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = host.fsRenameThunkFor(cbT); host.declare(`declare void @scr_fs_rename_async(ptr, ptr, ptr, ptr)`); B.line( @@ -403,7 +403,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const countField = e.fn === "fileHandle.read" || e.fn === "fileHandle.readv" ? "bytesRead" : "bytesWritten"; host.storeField(host.recordFieldPtr(rec, inner.shapeId, countField).ptr, F64, count, false); - const payload = host.retainValue(args[1]!.name, e.args[1]!.type); + const payload = host.retainHeap(args[1]!.name, e.args[1]!.type); B.line( `store ptr ${payload}, ptr ${host.recordFieldPtr(rec, inner.shapeId, vector ? "buffers" : "buffer").ptr}`, ); @@ -465,7 +465,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): throw new InternalCompilerError("llvm emitter bug: fs.watchCb callback not a func"); const path = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event"; host.declare(`declare void @${adapter}(ptr, ptr)`); host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`); @@ -618,7 +618,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line(`${nm} = call ptr @scr_fs_scandir_name(ptr ${snap}, ${host.sizeType} ${i}) ; +1`); B.line(`store ptr ${nm}, ptr ${host.recordFieldPtr(row, recT.shapeId, "name").ptr}`); B.line( - `store ptr ${host.retainValue(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`, + `store ptr ${host.retainHeap(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`, ); host.arrPush(arr, "ref", row); // push takes ownership of the row const i2 = B.tmp(); @@ -630,7 +630,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): if (!promiseForm) return arrayOut; const rc = vAdapters(host.shapeHost, arrayT); host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`); - host.moveTemp(arrayOut); // promise fulfillment owns the result array + host.toHeap(arrayOut); // promise fulfillment owns the result array const promise = B.tmp(); B.line( `${promise} = call ptr @scr_promise_settled_ref(ptr ${arr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, arrayT)})`, @@ -696,7 +696,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): if (!promiseForm) return value; const rc = vAdapters(host.shapeHost, unionT); host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`); - host.moveTemp(value); + host.toHeap(value); const promise = B.tmp(); B.line( `${promise} = call ptr @scr_promise_settled_ref(ptr ${value.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, unionT)})`, @@ -941,7 +941,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const retained = B.tmp(); B.line(`${retained} = call ptr @scr_arr_retain_v(ptr ${peeked})`); B.line(`store ptr ${retained}, ptr ${rowsSlot}`); - host.releaseValue(cell, iv); + host.releaseFrame(cell, iv); B.br(lj); B.startBlock(lm); const fresh = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index f4c57c63e..0254db555 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -170,7 +170,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm"); type = arms[tag]!; value = host.unwrapNullableClosure(value, tag); - } else host.moveTemp(cb); + } else host.toHeap(cb); if (type.kind !== "func" || type.params[1] === undefined) throw new InternalCompilerError("SNI callback lacks its answer function"); sni = [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))]; @@ -207,7 +207,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa case "http2.streamCloseCb": { const stream = e.fn.startsWith("http2.stream"), cbIndex = stream ? 2 : 1; - if (args[cbIndex] !== undefined) host.moveTemp(args[cbIndex]!); + if (args[cbIndex] !== undefined) host.toHeap(args[cbIndex]!); return finishRuntimeCall( host, e, @@ -310,7 +310,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa default: throw new InternalCompilerError(`unhandled HTTP/2 listener ${e.fn}`); } - if (thunk === null) host.moveTemp(cb); + if (thunk === null) host.toHeap(cb); const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params); return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]); } diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts index a6e76e4e0..c6bc4d97b 100644 --- a/packages/compiler/src/backend/llvm/lib-lookup.ts +++ b/packages/compiler/src/backend/llvm/lib-lookup.ts @@ -86,7 +86,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV ); } host.usesTimers = true; - host.moveTemp(cb); + host.toHeap(cb); return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ ...args.map((arg) => abiValue(host, arg)), ptr("@" + symbol), diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 322479a85..ea37fc2cf 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -42,7 +42,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (e.fn === "http.requestConn" || e.fn === "http.requestConnCb") { const args = e.args.map((arg) => host.emitExpr(arg)); host.usesTimers = true; - host.moveTemp(args[0]!); + host.toHeap(args[0]!); const cb = args[6]; const handler = cb === undefined @@ -72,7 +72,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const cbT = e.args[0]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: net.createServerCb handler not a func"); - host.moveTemp(args[0]!); + host.toHeap(args[0]!); cb = args[0]!.name; adapter = cbT.params.length === 0 ? "@scr_net_conn_thunk0" : "@scr_net_conn_thunk_sock"; host.declare(`declare void ${adapter}(ptr, ptr)`); @@ -86,7 +86,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn === "net.listenCb") { - host.moveTemp(args[2]!); + host.toHeap(args[2]!); cb = args[2]!.name; } host.declare(`declare void @scr_net_listen(ptr, double, ptr)`); @@ -109,7 +109,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (withCallback) { const cbT = e.args[cbIndex]!.type; if (cbT.kind === "func") { - host.moveTemp(args[cbIndex]!); + host.toHeap(args[cbIndex]!); cb = args[cbIndex]!.name; } else { if (cbT.kind !== "union") @@ -192,7 +192,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn === "net.serverCloseCb") { - host.moveTemp(args[1]!); + host.toHeap(args[1]!); cb = args[1]!.name; } host.declare(`declare void @scr_net_server_close(ptr, ptr)`); @@ -233,7 +233,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) throw new InternalCompilerError("llvm emitter bug: close override not a func"); const args = e.args.map((a) => host.emitExpr(a)); const wrapSym = host.closeOverrideWrapFor(cbT.params[0]!, cbT.ret.kind === "netServer"); - host.moveTemp(args[1]!); // ownership moves into the wrapper's env box + host.toHeap(args[1]!); // ownership moves into the wrapper's env box const wrap = host.wrapEmitterListener(args[1]!.name, wrapSym); host.declare(`declare void @scr_net_server_set_close_override(ptr, ptr)`); B.line(`call void @scr_net_server_set_close_override(ptr ${args[0]!.name}, ptr ${wrap})`); @@ -244,7 +244,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; const entry = e.fn === "net.serverOnError" ? "scr_net_server_on_error" : "scr_net_sock_on_error"; @@ -295,7 +295,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "http.clientOnContinue": "scr_http_client_on_continue", }[e.fn]!; const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); host.declare(`declare void @${entry}(ptr, ptr, i1 zeroext)`); B.line(`call void @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, i1 ${args[2]!.name})`); return { name: "", type: e.type }; @@ -312,7 +312,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[cbIndex]!); + host.toHeap(args[cbIndex]!); const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock"; host.declare(`declare void @${adapter}(ptr, ptr)`); if (e.fn === "http.serverSetTimeoutCb") { @@ -340,7 +340,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn === "net.connectCb") { - host.moveTemp(args[2]!); + host.toHeap(args[2]!); cb = args[2]!.name; } host.declare(`declare ptr @scr_net_connect_checked(double, ptr, ptr)`); @@ -357,7 +357,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 0 ? "scr_net_data_thunk0" @@ -533,7 +533,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const cbT = e.args[0]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: http.createServer handler not a func"); - host.moveTemp(args[0]!); + host.toHeap(args[0]!); cb = args[0]!.name; const sym = cbT.params.length === 2 @@ -554,7 +554,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} handler not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 2 ? "scr_http_handler_thunk2" @@ -580,7 +580,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 3 ? "scr_http_upgrade_thunk3" @@ -607,7 +607,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = e.fn === "http.clientOnResponse" ? cbT.params.length === 0 @@ -630,7 +630,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "http.resOnFinish" || e.fn === "http.resOnEndCallback") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const entry = e.fn === "http.resOnFinish" ? "scr_http_res_on_finish" : "scr_http_res_on_end_callback"; host.declare(`declare void @${entry}(ptr, ptr)`); @@ -639,7 +639,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "http.resSetTimeoutCb" || e.fn === "http.reqSetTimeoutCb") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const entry = e.fn === "http.resSetTimeoutCb" ? "scr_http_res_set_timeout" : "scr_http_req_set_timeout"; host.declare(`declare void @${entry}(ptr, double, ptr)`); @@ -650,7 +650,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "http.clientSetTimeoutCb") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare("declare void @scr_http_client_set_timeout_cb(ptr, double, ptr)"); B.line( `call void @scr_http_client_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`, @@ -659,21 +659,21 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "net.sockOnFinish") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); host.declare(`declare void @scr_net_sock_on_finish(ptr, ptr)`); B.line(`call void @scr_net_sock_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); return { name: "", type: e.type }; } if (e.fn === "net.sockOnWriteFlush") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); host.declare(`declare void @scr_net_sock_on_write_flush(ptr, ptr)`); B.line(`call void @scr_net_sock_on_write_flush(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); return { name: "", type: e.type }; } if (e.fn === "net.sockSetTimeoutCb") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare("declare void @scr_net_sock_set_timeout_cb(ptr, double, ptr)"); B.line( `call void @scr_net_sock_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`, diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 3c35ae6dc..e6d8acdbe 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -18,7 +18,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr const cmd = host.emitExpr(e.args[0]!); const argv = host.emitExpr(e.args[1]!); const cb = host.emitExpr(callbackArg); - host.moveTemp(cb); + host.toHeap(cb); const shell = host.emitExpr(e.args[3]!); const adapter = host.execFileThunkFor(cbT); host.usesTimers = true; @@ -72,7 +72,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr if (e.fn === "child.onSpawn") { const child = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); host.declare("declare void @scr_child_on_spawn(ptr, ptr)"); B.line(`call void @scr_child_on_spawn(ptr ${child.name}, ptr ${cb.name})`); return { name: "", type: e.type }; @@ -87,7 +87,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const child = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); let adapter: string; if (cbT.params.length === 0) { adapter = "scr_child_exit_thunk0"; @@ -110,7 +110,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError("llvm emitter bug: child.onError callback not a func"); const child = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; host.declare(`declare void @${adapter}(ptr, ptr)`); host.declare(`declare void @scr_child_on_error(ptr, ptr, ptr)`); @@ -136,7 +136,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: child.send callback not a func"); const cb = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); suffix = `, ptr ${cb.name}, ptr @${host.ipcSendThunkFor(cbT)}`; decl += ", ptr, ptr"; } @@ -160,7 +160,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); if (e.fn === "child.onMessage") { host.declare(`declare void @scr_child_ipc_on_message(ptr, ptr, ptr, i1 zeroext)`); B.line( @@ -337,7 +337,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const once = host.emitExpr(e.args[2]!); if (e.fn === "writer.onError") { const adapter = @@ -373,7 +373,7 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr // main runs the loop. host.usesTimers = true; const cb = host.emitExpr(e.args[0]!); - host.moveTemp(cb); + host.toHeap(cb); // Programs that enter AsyncLocalStorage stores schedule through the // *_als entry points, which carry the active stores into the callback. const sym = @@ -466,7 +466,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: process.send callback not a func"); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); suffix = `, ptr ${cb.name}, ptr @${host.ipcSendThunkFor(cbT)}`; decl += ", ptr, ptr"; } @@ -488,7 +488,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[0]!); const once = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); if (e.fn === "process.onMessage") { host.declare(`declare void @scr_process_ipc_on_message(ptr, ptr, i1 zeroext)`); B.line( @@ -509,7 +509,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: process write callback not a func"); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = host.fsRenameThunkFor(cbT); const write = e.fn === "process.stdoutWriteBytesCb" @@ -540,7 +540,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[0]!); const once = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); if (e.fn === "stdin.onEnd") { host.declare(`declare void @scr_stdin_on_end(ptr, i1 zeroext)`); B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`); @@ -588,7 +588,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll throw new InternalCompilerError("llvm emitter bug: process.onExit callback not a func"); const cb = host.emitExpr(e.args[0]!); const once = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const adapter = cbT.params.length === 0 ? "scr_exit_thunk0" : "scr_exit_thunk_code"; host.declare(`declare void @${adapter}(ptr, double)`); host.declare(`declare void @scr_process_on_exit(ptr, ptr, i1 zeroext)`); @@ -743,7 +743,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr const cbr = B.tmp(); B.line(`${cbr} = call ptr @scr_closure_retain_v(ptr ${args[2]!.name})`); const wrapped = host.wrapEmitterListener(cbr, adapterFn); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare( `declare ptr @scr_emitter_on_via(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, ); @@ -767,7 +767,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError("llvm emitter bug: emitter.onDyn adapter not a func"); const args = e.args.map((a) => host.emitExpr(a)); const { fn: adapterFn, shim } = host.emitterFixedAdapter(adT); - host.moveTemp(args[3]!); // the frame's +1 moves into the wrapper's box + host.toHeap(args[3]!); // the frame's +1 moves into the wrapper's box const wrapped = host.wrapEmitterListener(args[3]!.name, adapterFn); host.declare( `declare ptr @scr_emitter_on_dyn(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, @@ -866,7 +866,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr host.declare(`declare void @scr_ee_inv_fixed2(ptr, ptr)`); const t = B.tmp(); if (isDyn) { - host.moveTemp(args[3]!); + host.toHeap(args[3]!); const wrapped = host.wrapEmitterListener(args[3]!.name, adapterFn); host.declare( `declare ptr @scr_emitter_on_dyn(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, @@ -880,7 +880,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr const cbr = B.tmp(); B.line(`${cbr} = call ptr @scr_closure_retain_v(ptr ${args[2]!.name})`); const wrapped = host.wrapEmitterListener(cbr, adapterFn); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare( `declare ptr @scr_emitter_on_via(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, ); diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index dbb6d0181..b62b237df 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -48,7 +48,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const s0 = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); const param = cbT.params[0]; let adapter: string; if (param === undefined) { @@ -70,7 +70,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const s0 = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); host.declare(`declare void @scr_child_stream_on_end(ptr, ptr, i1 zeroext)`); B.line(`call void @scr_child_stream_on_end(ptr ${s0.name}, ptr ${cb.name}, i1 ${once.name})`); return { name: "", type: e.type }; @@ -86,7 +86,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const s0 = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`); host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`); B.line( @@ -127,7 +127,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV } const cb = args[at]!; const cbT = e.args[at]!.type; - host.moveTemp(cb); // the callback closure MOVES into the stream + host.toHeap(cb); // the callback closure MOVES into the stream cbArgs.push(`ptr ${cb.name}`, `ptr @${host.streamCbThunkFor(canonical[i]!, cbT)}`); at++; } @@ -182,7 +182,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const recv = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const cbT = e.args[1]!.type; - host.moveTemp(cb); // the callback closure MOVES into the stream + host.toHeap(cb); // the callback closure MOVES into the stream const sym = symOf[e.fn]!; host.declare(`declare void @${sym}(ptr, ptr, ptr)`); B.line( @@ -210,7 +210,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV } const cb = args[at]!; const cbT = e.args[at]!.type; - host.moveTemp(cb); + host.toHeap(cb); cbArgs.push(`ptr ${cb.name}`, `ptr @${host.streamCbThunkFor(canonical[i]!, cbT)}`); at++; } @@ -321,7 +321,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV B.line(`${raw} = call ptr @scr_stream_read(ptr ${args[0]!.name}, double ${args[1]!.name})`); const b = host.own({ name: raw, type: def!.arms[bytesTag]! }); host.emitPendingCheck(); - host.moveTemp(b); // moves into the union arm when present + host.toHeap(b); // moves into the union arm when present return host.wrapNullable(raw, raw, def!.arms[bytesTag]!, bytesTag, e.type, nullTag); } if (e.fn === "readable.flowing") { @@ -383,7 +383,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn !== "writable.writeDyn" && e.args.length > 2) { - host.moveTemp(args[2]!); + host.toHeap(args[2]!); cb = args[2]!.name; } host.declare(`declare zeroext i1 @${entry}(ptr, ptr, ptr)`); @@ -412,7 +412,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV else if (flags & 8) chunkD = args[at++]!.name; let cbName = "null"; if (flags & 4) { - host.moveTemp(args[at]!); + host.toHeap(args[at]!); cbName = args[at]!.name; } if (chunkD !== null) { @@ -490,7 +490,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const cbT = e.args[1]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: stream.finished callback not a func"); - host.moveTemp(args[1]!); // the watcher closure MOVES into the stream + host.toHeap(args[1]!); // the watcher closure MOVES into the stream const thunk = host.streamCbThunkFor("e", cbT); host.declare(`declare ptr @scr_stream_finished(ptr, ptr, ptr)`); B.line( @@ -599,7 +599,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV throw new InternalCompilerError( "llvm emitter bug: stream.finishedOpts callback not a func", ); - host.moveTemp(args[cbIdx]!); + host.toHeap(args[cbIdx]!); const thunk = host.streamCbThunkFor("e", cbT); host.declare( `declare ptr @scr_stream_finished_opts(ptr, ptr, ptr, double, double, i1, ptr, ptr)`, @@ -696,7 +696,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV throw new InternalCompilerError( "llvm emitter bug: stream.pipelineStages callback not a func", ); - host.moveTemp(args[args.length - 1]!); + host.toHeap(args[args.length - 1]!); const thunk = host.streamCbThunkFor("e", cbT); host.declare(`declare ptr @scr_stream_pipeline_stages(${params}, ptr, ptr)`); B.line( @@ -732,7 +732,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const cbT = e.args[1 + n]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: stream.pipeline callback not a func"); - host.moveTemp(args[1 + n]!); + host.toHeap(args[1 + n]!); const thunk = host.streamCbThunkFor("e", cbT); host.declare(`declare ptr @scr_stream_pipeline(double, ptr, ptr, ptr)`); B.line( diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts index 37e642d88..4780311f8 100644 --- a/packages/compiler/src/backend/llvm/lib-test.ts +++ b/packages/compiler/src/backend/llvm/lib-test.ts @@ -8,7 +8,7 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal host.usesTimers = true; switch (e.fn) { case "test.register": - host.moveTemp(args[3]!); + host.toHeap(args[3]!); return finishRuntimeCall(host, e, "scr_test_register", a); case "test.registerEmpty": return finishRuntimeCall(host, e, "scr_test_register", [ @@ -17,13 +17,13 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal ...a.slice(3), ]); case "test.suite": - host.moveTemp(args[3]!); + host.toHeap(args[3]!); return finishRuntimeCall(host, e, "scr_test_suite", a); case "test.hook": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_test_hook", a); case "test.sub": - host.moveTemp(args[4]!); + host.toHeap(args[4]!); return finishRuntimeCall(host, e, "scr_test_sub", a); case "test.subEmpty": { const promise = callRuntime(host, "scr_test_sub", "ptr", [ @@ -50,21 +50,21 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal case "test.workerId": return finishRuntimeCall(host, e, "scr_test_worker_id", a); case "test.ctxHook": - host.moveTemp(args[2]!); + host.toHeap(args[2]!); return finishRuntimeCall(host, e, "scr_test_ctx_hook", a); case "test.ctxPlan": return finishRuntimeCall(host, e, "scr_test_ctx_plan", a); case "test.ctxCount": return finishRuntimeCall(host, e, "scr_test_ctx_count", a); case "test.mockTrack": - host.moveTemp(args[0]!); + host.toHeap(args[0]!); return finishRuntimeCall(host, e, "scr_test_mock_track", a); case "test.mockRestoreAll": return finishRuntimeCall(host, e, "scr_test_mock_restore_all", a); case "test.mockReset": return finishRuntimeCall(host, e, "scr_test_mock_reset", a); case "test.ctxMockTrack": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_test_ctx_mock_track", a); case "test.ctxMockRestoreAll": return finishRuntimeCall(host, e, "scr_test_ctx_mock_restore_all", a); diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index a993ec49a..18fc006d4 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -94,10 +94,10 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu } case "tls.connect": case "tls.connectCb": - if (args[3] !== undefined) host.moveTemp(args[3]); + if (args[3] !== undefined) host.toHeap(args[3]); return finishRuntimeCall(host, e, "scr_tls_connect_dyn", [...a.slice(0, 3), a[3] ?? ptr()]); case "tls.sockOnSecureConnect": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a); case "tls.sockOnSession": { const cb = args[1]!, diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index b31c02ad0..6951896d2 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -629,7 +629,7 @@ export function emitLocalArrayRead( // A slot of a nullable-union array may hold NULL even when present. const owned = read.nullableElement ? read.type : read.element; const store = (value: string): void => { - B.line(`store ptr ${read.borrow ? value : host.retainValue(value, owned)}, ptr ${localSlot}`); + B.line(`store ptr ${read.borrow ? value : host.retainFrame(value, owned)}, ptr ${localSlot}`); }; if (inline) emitDenseReferenceArrayRead(host, array, index, integerIndex, store, no, slow, join, "ptr"); @@ -676,7 +676,7 @@ export function emitLocalArrayRead( B.line(`store i64 ${bits}, ptr ${payload}`); } else { B.line( - `store ptr ${read.borrow ? value : host.retainValue(value, read.element)}, ptr ${payload}`, + `store ptr ${read.borrow ? value : host.retainFrame(value, read.element)}, ptr ${payload}`, ); } B.line(`store i32 ${read.presentTag}, ptr ${tag}`); diff --git a/packages/compiler/src/backend/llvm/local-union-storage.ts b/packages/compiler/src/backend/llvm/local-union-storage.ts index af65920dc..445ff45de 100644 --- a/packages/compiler/src/backend/llvm/local-union-storage.ts +++ b/packages/compiler/src/backend/llvm/local-union-storage.ts @@ -202,7 +202,7 @@ export function storeLocalUnion( B.startBlock(label); host.frames.push([]); storeLocalUnion(host, storage, value); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); B.br(join); } B.startBlock(join); @@ -241,7 +241,7 @@ export function storeLocalUnion( const copied = host.emitOwnedNullableStack(expr); source = copied.box; owner = copied.owner.name; - host.moveTemp(copied.owner); + host.toFrame(copied.owner); } else if (expr.kind === "varRef" && !canStackUnion(expr, host.boxedUnionsById)) { // A copy of another binding: snapshot the tag and bits, and retain the // reference payload (scalar and unit arms own nothing). @@ -260,7 +260,7 @@ export function storeLocalUnion( B.line(`${isRef} = icmp eq i32 ${tag}, ${refTag}`); B.condBr(isRef, yes, join); B.startBlock(yes); - const retained = host.retainValue(host.unionPeek(source, unionId), storage.ownerType); + const retained = host.retainFrame(host.unionPeek(source, unionId), storage.ownerType); B.line(`store ptr ${retained}, ptr ${held}`); B.br(join); B.startBlock(join); @@ -272,7 +272,7 @@ export function storeLocalUnion( source = wrapped.value.name; if (wrapped.payload) { owner = wrapped.payload.name; - host.moveTemp(wrapped.payload); + host.toFrame(wrapped.payload); } } else { const read = matchMapRead(expr, host.boxedUnionsById)!; @@ -294,7 +294,7 @@ export function storeLocalUnion( const old = B.tmp(); B.line(`${old} = load ptr, ptr ${storage.owner}`); B.line(`store ptr ${owner}, ptr ${storage.owner}`); - host.releaseValue(old, storage.ownerType); + host.releaseFrame(old, storage.ownerType); } } @@ -326,7 +326,7 @@ export function emitScalarOptional( bindPresent(); host.frames.push([]); storeLocalUnion(host, storage, expr.body); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); host.chainSlots.delete(expr.id); B.br(join); } else { @@ -339,7 +339,7 @@ export function emitScalarOptional( B.startBlock(fallback); host.frames.push([]); storeLocalUnion(host, storage, expr.right); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); B.br(join); } B.startBlock(join); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 57c517f6a..f0aa03497 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -105,7 +105,7 @@ function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrTy join = B.newLabel("mapn.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const retained = host.retainValue(emitNumberMapSlot(host, entry, "ptr"), value); + const retained = host.retainFrame(emitNumberMapSlot(host, entry, "ptr"), value); B.br(join); B.startBlock(miss); B.br(join); @@ -251,7 +251,7 @@ export function emitMapIterSlot( B.line(`${loaded} = icmp ne i64 ${bits}, 0`); } else { B.line(`${loaded} = load ${llTy}, ptr ${slot}`); - if (access === "ref") loaded = host.retainValue(loaded, type); + if (access === "ref") loaded = host.retainFrame(loaded, type); } const hitEnd = B.currentLabel(); B.br(join); diff --git a/packages/compiler/src/backend/llvm/split-loops.ts b/packages/compiler/src/backend/llvm/split-loops.ts index b7f1d0ef9..c29705919 100644 --- a/packages/compiler/src/backend/llvm/split-loops.ts +++ b/packages/compiler/src/backend/llvm/split-loops.ts @@ -102,8 +102,8 @@ export function storeSplitSnapshot( host.B.line(`store ptr ${snapshot.source}, ptr ${sourceSlot}`); host.B.line(`store ptr ${snapshot.separator}, ptr ${separatorSlot}`); host.B.line(`store i32 ${snapshot.limit}, ptr ${limitSlot}`); - host.moveTemp({ name: snapshot.source, type: STRING }); - host.moveTemp({ name: snapshot.separator, type: STRING }); + host.toFrame({ name: snapshot.source, type: STRING }); + host.toFrame({ name: snapshot.separator, type: STRING }); return { sourceSlot, separatorSlot, limitSlot }; } diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index cbbf0cc29..c7a58a7ea 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -236,7 +236,7 @@ function emitRecordCloneFields( const fieldTy = llFieldType(from.type, from.inline); const v = B.tmp(); B.line(`${v} = load ${fieldTy}, ptr ${from.ptr}`); - const stored = isRefCounted(from.type) && !from.inline ? host.retainValue(v, from.type) : v; + const stored = isRefCounted(from.type) && !from.inline ? host.retainFrame(v, from.type) : v; B.line(`store ${fieldTy} ${stored}, ptr ${to.ptr}`); } const initialized = new Set(); diff --git a/packages/compiler/src/backend/llvm/string-slices.ts b/packages/compiler/src/backend/llvm/string-slices.ts index ae73f1ea1..d00c681d6 100644 --- a/packages/compiler/src/backend/llvm/string-slices.ts +++ b/packages/compiler/src/backend/llvm/string-slices.ts @@ -91,7 +91,7 @@ export function emitStringSliceSnapshot( B.line( `call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`, ); - host.moveTemp(source); + host.toFrame(source); return { source: sourceSlot, range }; } diff --git a/packages/compiler/src/backend/llvm/thread-slots.ts b/packages/compiler/src/backend/llvm/thread-slots.ts index da182bde2..85656878a 100644 --- a/packages/compiler/src/backend/llvm/thread-slots.ts +++ b/packages/compiler/src/backend/llvm/thread-slots.ts @@ -49,7 +49,7 @@ export function emitThreadSlotsIntrinsic( case "slots.get": { const read = readSlot(host, block, word); if (host.nullableUnions.of(e.type) !== null) - return host.own({ name: host.retainValue(read, e.type), type: e.type }); + return host.own({ name: host.retainFrame(read, e.type), type: e.type }); return boxedRead(host, read, e.type); } case "slots.has": { @@ -67,13 +67,13 @@ export function emitThreadSlotsIntrinsic( case "slots.set": { if (value === null) throw new InternalCompilerError("llvm emitter bug: slots.set value"); // The value moves into the block. - host.moveTemp(value); + host.toHeap(value); host.declare(`declare void @scr_slots_set(ptr, ptr, ptr, ptr, ptr)`); B.line( `call void @scr_slots_set(ptr ${block}, ptr ${key.name}, ptr ${word}, ptr ${value.name}, ptr ${releaseSym(host.shapeHost, value.type)})`, ); if (e.type.kind === "void") return { name: "", type: e.type }; - return host.own({ name: host.retainValue(slots.name, e.type), type: e.type }); + return host.own({ name: host.retainFrame(slots.name, e.type), type: e.type }); } default: throw new InternalCompilerError(`llvm emitter bug: unknown intrinsic ${e.name}`); @@ -100,7 +100,7 @@ function boxedRead(host: LlvmEmitterContext, read: string, type: IrType): LlValu B.condBr(has, some, none); B.startBlock(some); const arm = def.arms[refTag]!; - const box = host.unionNewOwned(def.id, refTag, { name: host.retainValue(read, arm), type: arm }); + const box = host.unionNewOwned(def.id, refTag, { name: host.retainHeap(read, arm), type: arm }); B.line(`store ptr ${box}, ptr ${result}`); B.br(done); B.startBlock(none); From 309fa897ec6bec258a6a7712cb01e9583a1d2c52 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 04:48:10 -0500 Subject: [PATCH 114/120] Use EventEmitter, the stream classes and the error classes as values `function make(C: typeof EventEmitter) { return new C(); }`, `const kinds: ErrorConstructor[] = [TypeError, RangeError]` and `(typeof Readable)[]` stopped at SC2020 (the type has no mapping) or SC1090 (builtin classes as values). These classes are now class values: the name in a TypeScript file (or a const alias of it) is the class object, `typeof EventEmitter`, `typeof Readable` and ErrorConstructor- style types map to class values, and parameters, arrays, Map values and object fields hold them. Construction through a value calls %C.construct, which builds the instance with the runtime constructor the direct `new` uses (emitter.new, a stream's newDyn without options, error.newOptions for Error and its six standard subclasses). instanceof, typeof, .name and === answer as in Node. A Duplex value widens into a `typeof Readable` slot, and stream values widen into `typeof EventEmitter` slots, as the constructor ABIs match; a union of such class values (`const kinds = [Readable, Writable]`) is a value of their nearest common ancestor, and `new K()` through it answers the checker's union of instances. `instanceof` through a value that may be Writable keeps the Duplex rule at run time. EventEmitter and stream values constructed with options, runtime class values stored as untyped values or printed, statics read through a value, and DOMException, AggregateError, AssertionError and SuppressedError values report SC1090. The existing static lowerings (`EventEmitter.setMaxListeners`, `Error.stackTraceLimit`, the events module) are unchanged. JavaScript files keep the identity token. --- packages/compiler/src/backend/llvm/classes.ts | 26 ++- packages/compiler/src/backend/llvm/emitter.ts | 3 +- .../compiler/src/backend/llvm/expr-calls.ts | 6 +- .../lowering/builtins/class-values.ts | 55 ++++++ .../src/frontend/lowering/lower-calls.ts | 9 + .../src/frontend/lowering/lower-classes.ts | 174 ++++++++++++++++-- .../src/frontend/lowering/lower-exprs.ts | 172 +++++++++++++++++ .../src/frontend/lowering/lower-stmts.ts | 4 +- .../compiler/src/frontend/lowering/lowerer.ts | 32 +++- packages/compiler/src/frontend/type-mapper.ts | 9 + packages/compiler/src/ir/analysis.ts | 3 +- packages/compiler/src/ir/ir.ts | 21 +++ packages/compiler/src/ir/validate.ts | 38 ++-- .../corpus/5981-runtime-class-values.ts.json | 6 + .../main.ts.json | 7 + .../runtime-class-value-fences.ts.json | 6 + tests/corpus/5981-runtime-class-values.ts | 117 ++++++++++++ .../5982-runtime-class-value-modules/kinds.ts | 14 ++ .../5982-runtime-class-value-modules/main.ts | 16 ++ .../diagnostics/builtin-class-alias-escape.ts | 14 +- tests/diagnostics/class-values.ts | 6 +- .../diagnostics/runtime-class-value-fences.ts | 23 +++ .../builtin-class-alias-escape.ts.txt | 26 +-- .../harness/__snapshots__/class-values.ts.txt | 8 +- .../runtime-class-value-fences.ts.txt | 63 +++++++ tests/harness/corpus-costs.json | 4 + 26 files changed, 786 insertions(+), 76 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json create mode 100644 tests/corpus/5981-runtime-class-values.ts create mode 100644 tests/corpus/5982-runtime-class-value-modules/kinds.ts create mode 100644 tests/corpus/5982-runtime-class-value-modules/main.ts create mode 100644 tests/diagnostics/runtime-class-value-fences.ts create mode 100644 tests/harness/__snapshots__/runtime-class-value-fences.ts.txt diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 02036e4d9..717b08d3b 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -11,6 +11,7 @@ import { RUNTIME_EVENT_TARGET_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, + classValueConstructor, } from "../../ir/ir.js"; import { streamRooted, undefinedArmTag } from "../../ir/analysis.js"; import { @@ -926,14 +927,32 @@ export function emitClassObjDefs( `llvm emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`, ); } - const ctor = fnByName.get(`%${className}.constructor`); + const valueCtor = classValueConstructor(meta.def); + const ctor = fnByName.get(valueCtor.fn); if (!ctor) throw new InternalCompilerError( `llvm emitter bug: class object for ${className} without a constructor`, ); - const params = ctor.params.slice(1); + const params = ctor.params.slice(valueCtor.firstArg); const paramDecls = params.map((p, i) => `${llType(p.type)} %a${i}`).join(", "); const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`); + const classObj = + `@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` + + `{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null } ; class ${className}`; + if (meta.def.runtime) { + // A runtime-provided class: %C.construct allocates through the + // runtime and returns the instance. + out.push( + `define internal ptr @${mangleCtorThunk(className)}(ptr %class${paramDecls ? ", " + paramDecls : ""}) ${FN_ATTRS} { ; construct thunk ${className}`, + `entry:`, + ` %o = call ptr @${mangleFunction(valueCtor.fn)}(${ctorArgs.join(", ")})`, + ` ret ptr %o`, + `}`, + classObj, + ``, + ); + continue; + } if (meta.def.localCaptures !== undefined) host.declare(`declare ptr @scr_classobj_retain_v(ptr)`); const localBaseInits: string[] = []; @@ -992,8 +1011,7 @@ export function emitClassObjDefs( ` call void @${mangleFunction(`%${className}.constructor`)}(${[`ptr %r`, ...ctorArgs].join(", ")})`, ` ret ptr %o`, `}`, - `@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` + - `{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? 0}, ptr null, ptr null } ; class ${className}`, + classObj, ``, ); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 7e7e70df6..494b7a6c4 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -56,6 +56,7 @@ import { typeKey, STRING, VOID, + classJsName, } from "../../ir/ir.js"; import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js"; import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; @@ -6123,7 +6124,7 @@ export class LlEmitter { classObjSym(className: string): string { if (!this.classObjs.has(className)) { const meta = this.classMetaOf(className); - this.classObjs.set(className, { nameSym: this.internLiteral(meta.def.jsName ?? "") }); + this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) }); } return mangleClassObj(className); } diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 6446a581b..b49c8895e 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -10,6 +10,7 @@ import { typeKey, type IrExpr, type IrFunction, + classValueConstructor, } from "../../ir/ir.js"; import { liveRefView } from "./expr-stream-bridges.js"; import { typedRefConstructor, vAdapters } from "./shapes.js"; @@ -1043,7 +1044,8 @@ export function emitCallExpr( if (e.callee.type.kind !== "classval") throw new InternalCompilerError("llvm emitter bug: newValue on non-classval callee"); const cls = e.callee.type.className; - const ctor = host.fnByName.get(`%${cls}.constructor`); + const valueCtor = classValueConstructor(host.classMetaOf(cls).def); + const ctor = host.fnByName.get(valueCtor.fn); if (!ctor) throw new InternalCompilerError( `llvm emitter bug: newValue on ${cls} without a constructor`, @@ -1056,7 +1058,7 @@ export function emitCallExpr( B.line(`${ctorp} = getelementptr inbounds %ScrClassObj, ptr ${callee.name}, i64 0, i32 3`); B.line(`${thunk} = load ptr, ptr ${ctorp}`); const argList = args - .map((a, i) => `${host.llType(ctor.params[i + 1]!.type)} ${a.name}`) + .map((a, i) => `${host.llType(ctor.params[i + valueCtor.firstArg]!.type)} ${a.name}`) .join(", "); const t = B.tmp(); B.line(`${t} = call ptr ${thunk}(ptr ${callee.name}${argList ? ", " + argList : ""})`); diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts index f24db09cf..5e4db921c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/class-values.ts +++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts @@ -7,6 +7,7 @@ import { stripTypeCasts } from "./arguments.js"; import { streamClassAliasDecl } from "../lower-stream.js"; import { zlibClassName, zlibStreamModeOf } from "./compression.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; +import { RUNTIME_ERROR_CLASSES } from "../../../ir/ir.js"; /** A builtin class an expression names at compile time: the error * classes, node:events EventEmitter, the node:stream classes (each a @@ -123,6 +124,60 @@ export function registerBuiltinClassAlias( return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol); } +/** The runtime-provided class (EventEmitter, a stream class, an error + * class) `expr` names, when values of it construct (valueCtorParams), or + * null. */ +export function runtimeClassValueOf(lowerer: Lowerer, expr: ts.Expression): ClassInfo | null { + const ref = builtinClassRefOf(lowerer, expr); + if (ref === null || ref.kind === "zlib" || ref.kind === "collection") return null; + return ref.info.valueCtorParams !== undefined ? ref.info : null; +} + +/** The runtime-provided class whose static side `type` is (`typeof + * EventEmitter`, `typeof Readable`, `ErrorConstructor`), when values of + * it construct, or null. Instance types have no construct signatures and + * answer null. A union of such static sides (`[Readable, Writable]`'s + * element type) is their nearest common ancestor's value when every arm + * constructs through the same parameters: a slot of the ancestor holds + * any of them. */ +export function runtimeClassValueOfType(lowerer: Lowerer, type: ts.Type): string | null { + if (type.isUnionType()) { + const arms: ClassInfo[] = []; + for (const part of ts.constituentTypes(type)) { + const name = runtimeClassValueOfType(lowerer, part); + const info = name !== null ? lowerer.classes.get(name) : undefined; + if (!info) return null; + arms.push(info); + } + const ancestor = commonRuntimeAncestor(arms); + if (ancestor === null) return null; + return arms.every((arm) => lowerer.ctorAbiEquals(arm, ancestor)) ? ancestor.def.name : null; + } + if (lowerer.checker.getConstructSignatures(type).length === 0) return null; + const symbol = type.getSymbol(); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + let info = lowerer.builtinEmitterInfoOf(symbol) ?? lowerer.builtinStreamInfoOf(symbol); + if (!info && symbol.name.endsWith("Constructor")) { + for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { + if (`${rec.lib}Constructor` === symbol.name) info = lowerer.classes.get(irName) ?? null; + } + } + return info?.valueCtorParams !== undefined ? info.def.name : null; +} + +/** The nearest class every one of `infos` is or extends, when it is a + * runtime class whose values construct, or null. */ +function commonRuntimeAncestor(infos: ClassInfo[]): ClassInfo | null { + for (let candidate = infos[0] ?? null; candidate; candidate = candidate.base) { + const holdsAll = infos.every((info) => { + for (let c: ClassInfo | null = info; c; c = c.base) if (c === candidate) return true; + return false; + }); + if (holdsAll) return candidate.def.runtime && candidate.valueCtorParams ? candidate : null; + } + return null; +} + /** True when two references name the same builtin class. */ export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean { if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b3cd7e39b..5c1b47e6a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -65,6 +65,7 @@ import { isUnitType, shapeHasAccessorSlots, typeEquals, + RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; @@ -8968,6 +8969,14 @@ export function lowerObjectMethodCall( ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; let mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + // EventEmitter's static side (the class, a value of it, or the events + // module, which is the class) keeps its static surface on the emitter + // dispatch (`EventEmitter.setMaxListeners(n)`), which tells the statics + // apart by the receiver's construct signatures. The stream classes + // inherit the same statics. + if (mappedReceiver?.kind === "classval" && mappedReceiver.className === RUNTIME_EMITTER_CLASS) { + mappedReceiver = { kind: "object", className: RUNTIME_EMITTER_CLASS }; + } // A specialized JS parameter can expose a native field whose checker // type still belongs to the unspecialized body (plane.normal.dot()). // Use the lowered receiver's representation and retain its evaluation. diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d44748084..093e7eeef 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2,6 +2,7 @@ import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { lowerReadlineUiNew } from "./builtins/readline.js"; +import { checkedClassUnionAssertion } from "./class-assertions.js"; import { collectClassMethodMutations } from "./class-method-mutations.js"; import { InternalCompilerError } from "../../errors.js"; /* Class lowering: shape collection over the single-inheritance graph @@ -12,6 +13,7 @@ import { InternalCompilerError } from "../../errors.js"; import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js"; +import { everyStmtList } from "../../ir/traverse.js"; import { type BuiltinClassRef, builtinClassAliasOf, @@ -28,6 +30,7 @@ import { type IrClassDef, type IrExpr, type IrFunction, + type IrLibFn, type IrLocal, type IrParam, type IrStmt, @@ -37,6 +40,7 @@ import { RUNTIME_STREAM_CLASSES, STRING, type SrcLoc, + classValueConstructor, UNDEFINED_T, VOID, arrayOf, @@ -539,6 +543,11 @@ export interface ClassInfo { * inherited signature, and the synthesized constructor forwards to it * (forwarding the completed ABI values; defaults apply in the base). */ ctorParams: ParamShape[]; + /** A runtime-provided class whose values construct (`const kinds = + * [Readable, Writable]`, `(C: typeof EventEmitter) => new C()`): the + * parameters of its `%C.construct` function (classValueConstructor). + * Absent for runtime classes that are not values. */ + valueCtorParams?: ParamShape[]; base: ClassInfo | null; /** Family heritage resolves its type arguments separately for each instance. */ genericBase?: ts.ExpressionWithTypeArguments; @@ -782,6 +791,17 @@ function symbolSlotReturnType( return out; } +/** The error classes whose values construct through error.newOptions. */ +const ERROR_CLASS_VALUES: ReadonlySet = new Set([ + "%Error", + "%TypeError", + "%RangeError", + "%SyntaxError", + "%ReferenceError", + "%EvalError", + "%URIError", +]); + /** The builtin Error hierarchy * as eagerly-registered ClassInfos: mapType names them the moment a lib * Error type appears, so the infos must exist before any lowering. They @@ -860,6 +880,17 @@ export function registerBuiltinErrorClasses(lowerer: Lowerer): void { { type: DYN, mode: "omittable" }, { type: DYN, mode: "omittable" }, ], + // The ECMAScript error constructors take (message?, options?); + // DOMException, AggregateError, AssertionError and SuppressedError + // have their own construction and are not values. + ...(ERROR_CLASS_VALUES.has(irName) + ? { + valueCtorParams: [ + { type: DYN, mode: "omittable" as const }, + { type: DYN, mode: "omittable" as const }, + ], + } + : {}), base, subclasses: [], throwingSetters: [], @@ -892,6 +923,7 @@ export function registerBuiltinEmitterClass(lowerer: Lowerer): void { // `new EventEmitter()` — zero-argument (the options bag fences at // construction sites; the checker may admit it via @types/node). ctorParams: [], + valueCtorParams: [], base: null, subclasses: [], throwingSetters: [], @@ -923,6 +955,9 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void { // `new Readable(opts?)` — the options bag is parsed structurally // by the stream spoke (lowerNew never completes against this). ctorParams: [], + // Construction through a class value builds the stream without + // options (lowerNew reports options passed through a value). + valueCtorParams: [], base, subclasses: [], throwingSetters: [], @@ -5059,11 +5094,20 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) ); } if (info.builtinError || info.builtinEmitter || info.builtinStream !== undefined) { - lowerer.unsupported( - "SC1090", - blame, - `builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`, - ); + if (info.valueCtorParams === undefined) { + lowerer.unsupported( + "SC1090", + blame, + `builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`, + ); + } + defineClassValueConstruct(lowerer, info); + return { + kind: "classRef", + className: info.def.name, + type: { kind: "classval", className: info.def.name }, + loc: locOf(blame), + }; } if ( lowerer.inheritsBuiltinEmitterCtor(info) || @@ -5090,14 +5134,92 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) }; } +/** Notes a runtime-provided class whose value is named, widened to, or + * constructed through (a no-op for program classes, whose constructor is + * their own). runtimeClassConstructs defines `%C.construct` for the + * noted classes the finished module still references. */ +export function defineClassValueConstruct(lowerer: Lowerer, info: ClassInfo): void { + if (!info.def.runtime || info.valueCtorParams === undefined) return; + lowerer.noteEdge(classValueConstructor(info.def).fn); + lowerer.runtimeClassValues.set(info.def.name, info); +} + +/** The `%C.construct` functions of the noted runtime classes that a + * classRef, newValue or classval upcast in `functions` names. Lowering can + * name a class speculatively and discard the expression, so the finished + * functions decide. */ +export function runtimeClassConstructs(lowerer: Lowerer, functions: IrFunction[]): IrFunction[] { + if (lowerer.runtimeClassValues.size === 0) return []; + const used = new Set(); + const note = (type: IrType): void => { + if (type.kind === "classval" && lowerer.runtimeClassValues.has(type.className)) + used.add(type.className); + }; + const visitor = { + expr: (e: IrExpr): boolean => { + if (e.kind === "classRef") note(e.type); + else if (e.kind === "newValue") note(e.callee.type); + else if (e.kind === "upcast") { + note(e.type); + note(e.value.type); + } + return true; + }, + stmt: (): boolean => true, + }; + for (const fn of functions) everyStmtList(fn.body, visitor); + return [...used] + .sort() + .map((name) => runtimeClassConstruct(lowerer.runtimeClassValues.get(name)!)); +} + +/** `%C.construct` for a runtime-provided class value: the thunk's target, + * which builds the instance through the runtime constructor the direct + * `new C(...)` uses (emitter.new, a stream's .newDyn without options, + * error.newOptions). */ +function runtimeClassConstruct(info: ClassInfo): IrFunction { + const params = info.valueCtorParams ?? []; + const name = classValueConstructor(info.def).fn; + const loc = info.def.loc; + const type: IrType = { kind: "object", className: info.def.name }; + const ids = params.map((_, i) => `a${i}.0`); + const args = ids.map((id) => varRef(id, DYN, loc)); + let fn: IrLibFn = "error.newOptions"; + if (info.builtinEmitter) fn = "emitter.new"; + else if (info.builtinStream !== undefined) { + fn = `${streamCtorShape(info.def.name).fn}.newDyn` as IrLibFn; + args.push(dynUndefinedExpr(loc)); + } + return { + name, + params: params.map((shape, i) => ({ localId: ids[i]!, name: `a${i}`, type: shape.type })), + returnType: type, + locals: params.map((shape, i) => ({ + id: ids[i]!, + name: `a${i}`, + type: shape.type, + mutable: false, + })), + body: [{ kind: "return", value: { kind: "libCall", fn, args, type, loc }, loc }], + loc, + }; +} + +/** The constructor parameters construction through a class value completes + * against: a program class's constructor, or a runtime class's + * `%C.construct`. */ +function classValueParams(info: ClassInfo): ParamShape[] { + return info.def.runtime ? (info.valueCtorParams ?? info.ctorParams) : info.ctorParams; +} + /** Constructor-ABI equality — the classval widening rule: a classval(D) * value may flow into a classval(C) slot only when D's completed * constructor signature equals C's (same count, modes, and ABI types), * which is what keeps newValue completion against C's one signature * sound for every value legally in the slot. */ export function ctorAbiEquals(lowerer: Lowerer, sub: ClassInfo, sup: ClassInfo): boolean { - const a = sub.ctorParams; - const b = sup.ctorParams; + const a = classValueParams(sub); + const b = classValueParams(sup); return ( a.length === b.length && a.every((p, i) => p.mode === b[i]!.mode && typeEquals(p.type, b[i]!.type)) @@ -6232,6 +6354,9 @@ export function lowerClassValueProperty( const loc = locOf(expr); const member = expr.name.text; const info = lowerer.classes.get(recvT.className); + // A runtime-provided class value answers `.name` from its class object; + // its other statics (`Error.stackTraceLimit`) keep their own lowerings. + if (info?.def.runtime && member !== "name") return null; if ( info && ((hasRuntimeStatics(info) && !findStaticOn(lowerer, info, member)?.field) || @@ -9777,24 +9902,51 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee); if (callee.type.kind !== "classval") lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); - lowerer.noteEdge(`%${info.def.name}.constructor`); + lowerer.noteEdge(classValueConstructor(info.def).fn); // Every constructor a value in this slot can dispatch to is a // descendant's — mark them reachable like a virtual edge. const below = (c: ClassInfo): void => { for (const s of c.subclasses) { - lowerer.noteEdge(`%${s.def.name}.constructor`); + lowerer.noteEdge(classValueConstructor(s.def).fn); below(s); } }; below(info); - const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); - return { + // EventEmitter and stream class values construct without options + // only: a slot of either may hold any of those classes, and the + // direct `new Readable({...})` lowering reads its options at compile + // time (EventEmitter's options have no lowering at all). + if ( + (info.builtinStream !== undefined || info.builtinEmitter) && + (expr.arguments?.length ?? 0) > 0 + ) { + lowerer.unsupported( + "SC1090", + expr, + `EventEmitter and stream class values constructed with options (construct with 'new ${info.def.name.slice(1)}({ ... })' directly)`, + ); + } + defineClassValueConstruct(lowerer, info); + const args = lowerer.completeArgs(expr.arguments ?? [], classValueParams(info), loc, expr); + const built: IrExpr = { kind: "newValue", callee, args, type: { kind: "object", className: info.def.name }, loc, }; + // A runtime class slot widened from a union (`[Readable, Writable]`'s + // elements hold EventEmitter values): the checker types the result as + // the union of the arms' instances, which the checked class view + // answers from the instance's class. + if (info.def.runtime) { + const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (resultT?.kind === "union") { + const asserted = checkedClassUnionAssertion(lowerer, built, resultT, loc); + if (asserted) return asserted; + } + } + return built; } if (calleeT?.kind === "union") { const def = lowerer.unions.get(calleeT.unionId); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4b8ce648..d46226384 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -39,6 +39,8 @@ import { lowerZlibStreamBytesWritten, } from "./builtins/compression.js"; import { + runtimeClassValueOf, + runtimeClassValueOfType, builtinClassAliasOwnsNoSlot, builtinClassRefOf, sameBuiltinClass, @@ -940,6 +942,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { `a builtin function alias as an escaping value (call '${expr.text}' directly)`, ); } + // `EventEmitter`, `Readable` or `TypeError` (or a const alias of + // one) as a value in a TypeScript file: the runtime class's class + // object. JavaScript files keep the identity token. + if (!isJsSourceFile(expr.getSourceFile())) { + const runtimeClass = runtimeClassValueOf(lowerer, expr); + if (runtimeClass) return lowerer.classValueRef(runtimeClass, expr); + } // `const E = EventEmitter` owns no runtime slot: construction, // instanceof, typeof and .name lower at their use sites. if (sym && builtinClassAliasOwnsNoSlot(lowerer, sym)) { @@ -1962,6 +1971,16 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const ref = builtinClassRefOf(lowerer, expr.expression); if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc }; } + // `C.name` through a runtime-provided class VALUE (a `typeof Readable` + // parameter): the class object's stored name. + if ( + expr.name.text === "name" && + !expr.questionDotToken && + runtimeClassValueOfType(lowerer, lowerer.typeOf(expr.expression)) !== null + ) { + const read = lowerer.lowerFieldRead(expr); + if (read) return read; + } // Expando statics may still be absent even when JS inference sees // the eventual assignment's scalar type. Preserve the checked value. const staticField = lowerStaticFieldRead(lowerer, expr); @@ -13013,9 +13032,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc } const left = lowerer.lowerExpr(expr.left); if (left.type.kind === "dyn") { + if (targetInfo.def.runtime) { + lowerer.unsupported( + "SC1090", + expr, + "'instanceof' through a runtime-provided class value on an untyped value (give the operand a class type first)", + ); + } const classValue = lowerer.lowerExpr(expr.right); return classInstanceOf(lowerer, left, targetInfo, loc, classValue); } + // A union of class instances against a runtime class value (`new K()` + // through a widened `[Readable, Writable]` element). + if (left.type.kind === "union" && targetInfo.def.runtime) { + const tested = unionClassValueInstanceOf(lowerer, left, expr.right, targetInfo, loc); + if (tested) return tested; + } if (left.type.kind !== "object") { lowerer.unsupported( "SC1090", @@ -13033,6 +13065,15 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const classValue = lowerer.lowerExpr(expr.right); if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right)); + const writableTest = writableClassValueTest( + lowerer, + left, + classValue, + targetInfo, + lhsInfo, + loc, + ); + if (writableTest) return writableTest; return { kind: "instanceOfValue", value: left, classValue, type: BOOL, loc }; } // A standalone side: the answer is static (the target slot can @@ -13331,6 +13372,137 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ); } +/** `x instanceof K` for a union-typed operand whose arms are class + * instances or unit values: each instance arm tests against the class + * value (with the Writable rule), unit arms answer false. Null when an arm + * is anything else. */ +function unionClassValueInstanceOf( + lowerer: Lowerer, + left: IrExpr, + right: ts.Expression, + targetInfo: ClassInfo, + loc: SrcLoc, +): IrExpr | null { + if (left.type.kind !== "union") return null; + const unionId = left.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if ( + !arms?.every((arm) => { + if (arm.kind !== "object") return isUnitType(arm); + const info = lowerer.classes.get(arm.className); + return info !== undefined && lowerer.inHierarchy(info); + }) + ) + return null; + const classValue = lowerer.lowerExpr(right); + if (classValue.type.kind !== "classval") lowerer.badType(right, lowerer.typeOf(right)); + const value = lowerer.declareHiddenLocal("%instanceValue", left.type); + const cls = lowerer.declareHiddenLocal("%instanceClass", classValue.type); + const valueRef = varRef(value.id, left.type, loc); + const clsRef = varRef(cls.id, classValue.type, loc); + let result: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + arms.forEach((arm, tag) => { + if (arm.kind !== "object") return; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: valueRef, type: arm, loc }; + const lhsInfo = lowerer.classes.get(arm.className)!; + const test: IrExpr = writableClassValueTest( + lowerer, + narrowed, + clsRef, + targetInfo, + lhsInfo, + loc, + ) ?? { kind: "instanceOfValue", value: narrowed, classValue: clsRef, type: BOOL, loc }; + result = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: valueRef, type: BOOL, loc }, + then: test, + else_: result, + type: BOOL, + loc, + }; + }); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: value.id, init: left, loc }, + { kind: "varDecl", localId: cls.id, init: classValue, loc }, + ], + result, + type: BOOL, + loc, + }; +} + +/** `x instanceof K` where K is a class value that may hold Writable (a + * `typeof Writable` or `typeof EventEmitter` slot): Node's + * Writable[Symbol.hasInstance] also answers true for streams with a + * writable side that do not inherit from Writable (Duplex and its + * subclasses), so when K is Writable the Duplex interval answers too. + * Null when the slot cannot hold Writable or the operand cannot be a + * Duplex. */ +function writableClassValueTest( + lowerer: Lowerer, + left: IrExpr, + classValue: IrExpr, + targetInfo: ClassInfo, + lhsInfo: ClassInfo, + loc: SrcLoc, +): IrExpr | null { + const writable = lowerer.classes.get("%Writable"); + const duplex = lowerer.classes.get("%Duplex"); + if (!writable || !duplex || classValue.type.kind !== "classval" || left.type.kind !== "object") + return null; + const slotHoldsWritable = + targetInfo === writable || lowerer.isSubclassOf("%Writable", targetInfo.def.name); + const operandMayBeDuplex = + lhsInfo === duplex || + lowerer.isSubclassOf(lhsInfo.def.name, "%Duplex") || + lowerer.isSubclassOf("%Duplex", lhsInfo.def.name); + if (!slotHoldsWritable || !operandMayBeDuplex) return null; + lowerer.defineClassValueConstruct(writable); + const value = lowerer.declareHiddenLocal("%instanceValue", left.type); + const cls = lowerer.declareHiddenLocal("%instanceClass", classValue.type); + const valueRef = varRef(value.id, left.type, loc); + const clsRef = varRef(cls.id, classValue.type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: value.id, init: left, loc }, + { kind: "varDecl", localId: cls.id, init: classValue, loc }, + ], + result: { + kind: "logical", + op: "||", + left: { kind: "instanceOfValue", value: valueRef, classValue: clsRef, type: BOOL, loc }, + right: { + kind: "logical", + op: "&&", + left: { + kind: "bin", + op: "===", + left: clsRef, + right: { + kind: "classRef", + className: "%Writable", + type: { kind: "classval", className: "%Writable" }, + loc, + }, + type: BOOL, + loc, + }, + right: { kind: "instanceOf", value: valueRef, className: "%Duplex", type: BOOL, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; +} + /** A statically decided `instanceof` still reads its class after the left * operand: a class with an initialization flag throws there before its * declaration ran. */ diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 700f68a2e..3a0958ae6 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6870,7 +6870,9 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm } } else { const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.left.expression)); - if (recvT?.kind === "classval") { + // A runtime-provided class (`Error.stackTraceLimit = 0`) keeps + // its static lowering below. + if (recvT?.kind === "classval" && !lowerer.classes.get(recvT.className)?.def.runtime) { // A static write through a class VALUE: sound exactly when // the value can only BE the declaring class — a LEAF class // (no strict descendants can flow into the slot, so JS's diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 2fb266be0..1873ac361 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -269,7 +269,11 @@ import { stdlibGlobalMember, nodeTypesOnlySymbol, } from "./surfaces.js"; -import { type BuiltinClassRef, builtinClassAliasOf } from "./builtins/class-values.js"; +import { + type BuiltinClassRef, + builtinClassAliasOf, + runtimeClassValueOfType, +} from "./builtins/class-values.js"; import { type FileParts, splitFiles, @@ -302,6 +306,8 @@ import { type ClassIteratorInfo, type GenericClassInfo, registerBuiltinErrorClasses, + defineClassValueConstruct, + runtimeClassConstructs, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, @@ -2774,6 +2780,9 @@ export class Lowerer { // top level, top = the innermost nested function currently lowering). fnStack: FnCtx[] = []; readonly liftedFns: IrFunction[] = []; + /** Runtime-provided classes used as values (defineClassValueConstruct), + * by IR class name. */ + readonly runtimeClassValues = new Map(); /** One Error-method helper belongs to this lowering session. Its cached * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; @@ -3004,6 +3013,7 @@ export class Lowerer { isErasing: () => this.erasingDepth > 0, genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), + runtimeClassValue: (type) => runtimeClassValueOfType(this, type), mixinClassInstance: (decl) => this.mixinTypeContext && this.mixinTypeContext.classNode === decl ? { kind: "object", className: this.mixinTypeContext.className } @@ -3217,6 +3227,10 @@ export class Lowerer { return builtinClassAliasOf(this, symbol); } + defineClassValueConstruct(info: ClassInfo): void { + defineClassValueConstruct(this, info); + } + /** Program-wide qualified name for a top-level declaration. */ qualify(sf: ts.SourceFile, name: string): string { return `${this.fileTag.get(sf) ?? ""}${name}`; @@ -6744,6 +6758,7 @@ export class Lowerer { // implicit-any instances lowered eagerly at their first call sites. functions.push(...this.liftedFns); functions.push(...this.implicitFns); + functions.push(...runtimeClassConstructs(this, functions)); if (this.remainder) { // Deferred collection diagnostics nothing flushed — declarations no @@ -7419,6 +7434,7 @@ export class Lowerer { ...this.liftedFns, ...this.implicitFns, ]; + functions.push(...runtimeClassConstructs(this, functions)); if (this.regexGroupResults) for (const fn of functions) foldRegexSpanReads(this, fn); // The worklist can leave a virtual site with a single implementation. // Devirtualize after construction/override reachability has settled. @@ -8552,6 +8568,18 @@ export class Lowerer { loc: expr.loc, }; } + // A runtime-provided class value has no checked-dynamic form that + // inspects as Node's does (`[Function: EventEmitter] { ... }`). + if (expr.type.kind === "classval" && this.classes.get(expr.type.className)?.def.runtime) { + this.pushDiag( + unsupportedDiag( + "SC1090", + expr.loc, + `a runtime-provided class value ('${expr.type.className.slice(1)}') as an untyped value`, + ), + ); + throw new PoisonError(); + } if (expr.kind === "unitLit" || this.dynConvertible(expr.type)) { // Records and arrays keep their identity as untyped values, including // when they arrive through a union, so `unknown` views, weak @@ -8771,6 +8799,7 @@ export class Lowerer { // and no completion target is meaningful — the exact-shape fence // downstream names the class-value flow instead. if (sub && sup && !sup.generic && this.ctorAbiEquals(sub, sup)) { + defineClassValueConstruct(this, sup); return { kind: "upcast", value: expr, type: expected, loc: expr.loc }; } } @@ -8891,6 +8920,7 @@ export class Lowerer { }); if (baseTag >= 0) { if (!sub || !this.ctorAbiEquals(sub, c)) break; + defineClassValueConstruct(this, c); const widened: IrExpr = { kind: "upcast", value: expr, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 41d18a6a2..b7fa17b7b 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1055,6 +1055,9 @@ export interface TypeMapperCtx { * mixin context (the type alone cannot name a call site). */ mixinClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null; localClassInstance?: (decl: ts.ClassLikeDeclaration, instanceType?: ts.Type) => IrType | null; + /** The runtime-provided class whose static side a type is (`typeof + * EventEmitter`, `ErrorConstructor`), when values of it construct. */ + runtimeClassValue?: (type: ts.Type) => string | null; /** Live class protocols retain identity and receiver dispatch rather than * copying an instance into an unrelated record layout. */ classImplementedProtocol?: (type: ts.Type) => boolean; @@ -2047,6 +2050,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } return { kind: "object", className: classNamer(classDecl) }; } + // A runtime-provided class's static side (`typeof EventEmitter`, + // `typeof Readable`, `ErrorConstructor`): that class's value. + { + const runtimeClass = ctx.runtimeClassValue?.(widened); + if (runtimeClass) return { kind: "classval", className: runtimeClass }; + } // The class STATIC side (`typeof C` — the class name as a value): the // same symbol WITH its construct signatures. Program classes only; lib // and package statics keep their fences. diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts index 88f6f394f..4fa2d8361 100644 --- a/packages/compiler/src/ir/analysis.ts +++ b/packages/compiler/src/ir/analysis.ts @@ -11,6 +11,7 @@ import { type IrStmt, type IrType, type IrUnionDef, + classValueConstructor, } from "./ir.js"; /** Destination tags for a union widening that preserves every payload's @@ -313,7 +314,7 @@ export function newValueMayThrow( if (!meta) throw new InternalCompilerError(`IR analysis bug: newValue on unknown class ${className}`); const any = (node: IrClassGraphNode): boolean => - mayThrow.has(`%${node.def.name}.constructor`) || node.children.some(any); + mayThrow.has(classValueConstructor(node.def).fn) || node.children.some(any); return any(meta); } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 87a4de606..6c8df7733 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1494,6 +1494,27 @@ export const RUNTIME_ERROR_CLASSES: ReadonlyMap< * owns listener closures, whatever the subclass fields say. */ export const RUNTIME_EMITTER_CLASS = "%EventEmitter"; +/** The function that construction through a class value calls, and the + * index of its first argument. A program class constructs through + * `%C.constructor(this, ...)` after the thunk allocates; a runtime-provided + * class (EventEmitter, the stream and error classes) through the + * frontend's `%C.construct(...)`, which allocates through the runtime and + * returns the new instance. */ +export function classValueConstructor(def: { name: string; runtime?: true }): { + fn: string; + firstArg: number; +} { + return def.runtime + ? { fn: `%${def.name}.construct`, firstArg: 0 } + : { fn: `%${def.name}.constructor`, firstArg: 1 }; +} + +/** The JavaScript `.name` of a class: its jsName, or the runtime-provided + * class's own name (`%TypeError` is TypeError). */ +export function classJsName(def: { name: string; runtime?: true; jsName?: string }): string { + return def.jsName ?? (def.runtime ? def.name.replace(/^%/, "") : ""); +} + /** The runtime-provided node:stream classes (ScrStream / scr_stream_*, * scr_stream.c — link-gated by moduleUsesStream). They root at the * emitter class (base chains below), so the emitter method surface and diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 44b6814db..5cf262e1f 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -56,7 +56,7 @@ import { maybePromiseUnionArmsOk, VOID, } from "./ir.js"; -import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js"; +import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; @@ -2937,15 +2937,21 @@ function validateFunction( err(`upcast: "${sub}" does not extend "${sup}"`, e.loc); break; } - const subCtor = functions.get(`%${sub}.constructor`); - const supCtor = functions.get(`%${sup}.constructor`); - if (!subCtor || !supCtor) { + const subClass = classes.get(sub); + const supClass = classes.get(sup); + const subValueCtor = subClass ? classValueConstructor(subClass) : null; + const supValueCtor = supClass ? classValueConstructor(supClass) : null; + const subCtor = subValueCtor ? functions.get(subValueCtor.fn) : undefined; + const supCtor = supValueCtor ? functions.get(supValueCtor.fn) : undefined; + if (!subCtor || !supCtor || !subValueCtor || !supValueCtor) { err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc); break; } + const subArgs = subCtor.params.slice(subValueCtor.firstArg); + const supArgs = supCtor.params.slice(supValueCtor.firstArg); const abiEqual = - subCtor.params.length === supCtor.params.length && - subCtor.params.every((p, i) => i === 0 || typeEquals(p.type, supCtor.params[i]!.type)); + subArgs.length === supArgs.length && + subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type)); if (!abiEqual) { err(`classval upcast: "${sub}" and "${sup}" constructor ABIs differ`, e.loc); } @@ -2970,16 +2976,13 @@ function validateFunction( err(`classRef to undeclared class "${e.className}"`, e.loc); break; } - if (cls.runtime) { - err(`classRef to runtime-provided class "${e.className}"`, e.loc); - } - if (cls.jsName === undefined) { + if (cls.jsName === undefined && !cls.runtime) { err(`classRef to "${e.className}" without a jsName (the class object's .name)`, e.loc); } // A class value can always be constructed through — the frontend // notes the constructor edge at every classRef, so the emitted // thunk has a body to call. - if (!functions.has(`%${e.className}.constructor`)) { + if (!functions.has(classValueConstructor(cls).fn)) { err(`classRef to "${e.className}" without its constructor function`, e.loc); } if (!typeEquals(e.type, { kind: "classval", className: e.className })) { @@ -3008,21 +3011,24 @@ function validateFunction( break; } const cls = e.callee.type.className; - const ctor = functions.get(`%${cls}.constructor`); - if (!ctor) { + const calleeClass = classes.get(cls); + const valueCtor = calleeClass ? classValueConstructor(calleeClass) : null; + const ctor = valueCtor ? functions.get(valueCtor.fn) : undefined; + if (!ctor || !valueCtor) { err(`newValue on "${cls}": missing constructor function`, e.loc); break; } // Count-exact against the static class's completed ABI (every // value legally in the slot shares it — the upcast rule). - if (ctor.params.length !== e.args.length + 1) { + const first = valueCtor.firstArg; + if (ctor.params.length !== e.args.length + first) { err( - `newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - 1}`, + `newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - first}`, e.loc, ); } e.args.forEach((a, i) => { - const p = ctor.params[i + 1]; + const p = ctor.params[i + first]; if (p) expectType(a, p.type, `newValue on "${cls}" arg ${i}`); }); if (!typeEquals(e.type, { kind: "object", className: cls })) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json new file mode 100644 index 000000000..6703b89ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5981-runtime-class-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json new file mode 100644 index 000000000..a9aeabf71 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/5982-runtime-class-value-modules/kinds.ts", + "/tests/corpus/5982-runtime-class-value-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json new file mode 100644 index 000000000..eded433eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/runtime-class-value-fences.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5981-runtime-class-values.ts b/tests/corpus/5981-runtime-class-values.ts new file mode 100644 index 000000000..b2ebec988 --- /dev/null +++ b/tests/corpus/5981-runtime-class-values.ts @@ -0,0 +1,117 @@ +// EventEmitter, the stream classes and the error classes as values: +// parameters, arrays, Map values and object fields hold the class, and +// construction, instanceof, typeof, .name and === through the value +// answer as in Node. +import { EventEmitter } from "node:events"; +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; + +function makeEmitter(C: typeof EventEmitter): EventEmitter { + return new C(); +} +const emitter = makeEmitter(EventEmitter); +emitter.on("ping", (n: number) => console.log("ping", n)); +emitter.emit("ping", 1); +console.log("emitter", emitter instanceof EventEmitter); + +const E: typeof EventEmitter = EventEmitter; +console.log("emitter value", typeof E, E.name, E === EventEmitter, new E() instanceof E); +const emitterKinds: (typeof EventEmitter)[] = [EventEmitter, PassThrough]; +for (const Kind of emitterKinds) { + const made = new Kind(); + console.log("emitter kind", Kind.name, made instanceof EventEmitter, made instanceof Readable); +} + +const streamKinds: (typeof Readable)[] = [Readable, Duplex, Transform, PassThrough]; +for (const Kind of streamKinds) { + const stream = new Kind(); + console.log( + "stream", + Kind.name, + stream instanceof Kind, + stream instanceof Readable, + stream instanceof Duplex, + stream instanceof Writable, + ); +} + +const W: typeof Writable = Writable; +const pass = new PassThrough(); +console.log("writable value", pass instanceof W, new Readable() instanceof W, new W() instanceof W); + +function writableKind(): typeof Writable { + return Writable; +} +console.log("computed", pass instanceof writableKind(), new Readable() instanceof writableKind()); + +const mixedKinds = [Readable, Writable]; +for (const Kind of mixedKinds) { + const made = new Kind(); + console.log( + "mixed", + Kind.name, + typeof Kind, + made instanceof Kind, + made instanceof EventEmitter, + pass instanceof Kind, + ); +} +const emitterMix = [EventEmitter, Readable]; +const SecondKind = emitterMix[1]; +console.log("emitter mix", emitterMix[0] === EventEmitter, new SecondKind() instanceof Readable); + +const readable = new Readable(); +readable._read = () => {}; +readable.on("data", (chunk: Buffer) => console.log("data", chunk.toString())); +readable.push("chunk"); +readable.push(null); + +const errorKinds: ErrorConstructor[] = [Error, TypeError, RangeError, SyntaxError]; +for (const Kind of errorKinds) { + const err = new Kind("failed", { cause: Kind.name }); + console.log( + "error", + Kind.name, + err.name, + err.message, + err.cause, + err instanceof Kind, + err instanceof Error, + err instanceof TypeError, + ); +} + +function raise(Kind: ErrorConstructor, message: string): never { + throw new Kind(message); +} +try { + raise(RangeError, "out of range"); +} catch (err) { + console.log("caught", err instanceof RangeError, (err as Error).message, String(err)); +} + +const byName = new Map([ + ["type", TypeError], + ["reference", ReferenceError], +]); +const ReferenceKind = byName.get("reference")!; +console.log("map value", ReferenceKind.name, new ReferenceKind() instanceof ReferenceError); + +const registry: { stream: typeof Readable; error: ErrorConstructor } = { + stream: Transform, + error: URIError, +}; +console.log( + "fields", + registry.stream.name, + new registry.stream() instanceof Transform, + new registry.error("bad uri").name, +); + +const EmitterAlias = EventEmitter; +const aliasKinds = [EmitterAlias]; +let mutableKind = EventEmitter; +console.log("aliases", aliasKinds.length, typeof mutableKind, mutableKind === EmitterAlias); + +const sameClass = errorKinds[1] === TypeError; +const firstKind = streamKinds[0]; +console.log("identity", sameClass, firstKind === Readable, firstKind === Duplex); diff --git a/tests/corpus/5982-runtime-class-value-modules/kinds.ts b/tests/corpus/5982-runtime-class-value-modules/kinds.ts new file mode 100644 index 000000000..68a5f6d93 --- /dev/null +++ b/tests/corpus/5982-runtime-class-value-modules/kinds.ts @@ -0,0 +1,14 @@ +import { EventEmitter } from "node:events"; +import { PassThrough, Readable, Transform } from "node:stream"; + +export const errorKinds: ErrorConstructor[] = [TypeError, RangeError]; +export const streamKinds: (typeof Readable)[] = [PassThrough, Transform]; + +export function build(Kind: typeof EventEmitter): EventEmitter { + return new Kind(); +} + +export function describe(Kind: ErrorConstructor, message: string): string { + const err = new Kind(message); + return `${Kind.name}: ${err.message} (${err instanceof Kind})`; +} diff --git a/tests/corpus/5982-runtime-class-value-modules/main.ts b/tests/corpus/5982-runtime-class-value-modules/main.ts new file mode 100644 index 000000000..0b9ea4cd3 --- /dev/null +++ b/tests/corpus/5982-runtime-class-value-modules/main.ts @@ -0,0 +1,16 @@ +// Runtime class values exported from one module and used in another: +// arrays of error and stream classes, and functions taking a class. +import { EventEmitter } from "node:events"; +import { Readable } from "node:stream"; +import { build, describe, errorKinds, streamKinds } from "./kinds.ts"; +import * as kinds from "./kinds.ts"; + +for (const Kind of errorKinds) console.log(describe(Kind, "from main")); +for (const Kind of kinds.streamKinds) { + const stream = new Kind(); + console.log(Kind.name, stream instanceof Kind, stream instanceof Readable); +} +const emitter = build(EventEmitter); +emitter.on("done", () => console.log("done")); +emitter.emit("done"); +console.log(streamKinds.length, errorKinds[0] === TypeError, kinds.errorKinds[1] === RangeError); diff --git a/tests/diagnostics/builtin-class-alias-escape.ts b/tests/diagnostics/builtin-class-alias-escape.ts index b3f9be5d3..29186738c 100644 --- a/tests/diagnostics/builtin-class-alias-escape.ts +++ b/tests/diagnostics/builtin-class-alias-escape.ts @@ -1,15 +1,7 @@ -// A builtin class alias owns no runtime value in a TypeScript file: +// A Map or Set alias owns no runtime value in a TypeScript file: // construction, instanceof, typeof and .name compile, other uses report -// at the use. A let binding of a class is not an alias. -import { EventEmitter } from "node:events"; - -const Emitter = EventEmitter; -const kinds = [Emitter]; -console.log(kinds.length); - +// at the use. Aliases of EventEmitter, the stream classes and the error +// classes are class values. const M = Map; const boxed: unknown = M; console.log(boxed !== undefined); - -let Mutable = EventEmitter; -console.log(typeof Mutable); diff --git a/tests/diagnostics/class-values.ts b/tests/diagnostics/class-values.ts index 3a53e5185..4e5a6ea4c 100644 --- a/tests/diagnostics/class-values.ts +++ b/tests/diagnostics/class-values.ts @@ -18,9 +18,9 @@ class Unrelated { static tag = "unrelated"; } -// Builtin classes as values: a const alias resolves at compile time, and -// an array of builtin classes has no element type. -const E = Error; +// Builtin classes with their own construction as values (Error and its +// standard subclasses are values; AggregateError is not). +const E = AggregateError; const errorClasses = [E]; // Classes extending builtin bases as values (the error-message completion diff --git a/tests/diagnostics/runtime-class-value-fences.ts b/tests/diagnostics/runtime-class-value-fences.ts new file mode 100644 index 000000000..3404eab13 --- /dev/null +++ b/tests/diagnostics/runtime-class-value-fences.ts @@ -0,0 +1,23 @@ +// Values of EventEmitter, the stream classes and the error classes +// construct, test instanceof and answer typeof, .name and ===. These uses +// report at the use. +import { EventEmitter } from "node:events"; +import { Readable } from "node:stream"; + +const R: typeof Readable = Readable; +const configured = new R({ read() {} }); +console.log(configured.readable); + +const C: typeof EventEmitter = EventEmitter; +console.log(C); +const boxed: unknown = Readable; +console.log(boxed !== undefined, C.defaultMaxListeners); + +// TypeScript types this array as RangeErrorConstructor[] (the error +// constructor interfaces are structurally identical); annotate it as +// ErrorConstructor[]. +const errorMix = [TypeError, RangeError]; +console.log(errorMix.length); + +const aggregates = [AggregateError]; +console.log(aggregates.length); diff --git a/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt index 9d1d981e2..bf2c56649 100644 --- a/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt +++ b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt @@ -1,22 +1,6 @@ -builtin-class-alias-escape.ts:7:16 - error SC1090: a builtin class alias as an escaping value (use 'Emitter' with new, instanceof, typeof or .name) is not supported yet +builtin-class-alias-escape.ts:6:24 - error SC1090: a builtin class alias as an escaping value (use 'M' with new, instanceof, typeof or .name) is not supported yet - 6 | const Emitter = EventEmitter; - 7 | const kinds = [Emitter]; - | ^~~~~~~ - 8 | console.log(kinds.length); - -builtin-class-alias-escape.ts:11:24 - error SC1090: a builtin class alias as an escaping value (use 'M' with new, instanceof, typeof or .name) is not supported yet - - 10 | const M = Map; - 11 | const boxed: unknown = M; - | ^ - 12 | console.log(boxed !== undefined); - -builtin-class-alias-escape.ts:14:15 - error SC2020: 'events.EventEmitter' is part of the standard library types but has no scriptc lowering yet - - 13 | - 14 | let Mutable = EventEmitter; - | ^~~~~~~~~~~~ - 15 | console.log(typeof Mutable); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file + 5 | const M = Map; + 6 | const boxed: unknown = M; + | ^ + 7 | console.log(boxed !== undefined); \ No newline at end of file diff --git a/tests/harness/__snapshots__/class-values.ts.txt b/tests/harness/__snapshots__/class-values.ts.txt index 953d1c2d1..af1dd7a13 100644 --- a/tests/harness/__snapshots__/class-values.ts.txt +++ b/tests/harness/__snapshots__/class-values.ts.txt @@ -1,15 +1,15 @@ -class-values.ts:24:7 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile +class-values.ts:24:7 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile - 23 | const E = Error; + 23 | const E = AggregateError; 24 | const errorClasses = [E]; | ^~~~~~~~~~~~ 25 | hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -class-values.ts:24:22 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile +class-values.ts:24:22 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile - 23 | const E = Error; + 23 | const E = AggregateError; 24 | const errorClasses = [E]; | ^~~ 25 | diff --git a/tests/harness/__snapshots__/runtime-class-value-fences.ts.txt b/tests/harness/__snapshots__/runtime-class-value-fences.ts.txt new file mode 100644 index 000000000..26a4f8c25 --- /dev/null +++ b/tests/harness/__snapshots__/runtime-class-value-fences.ts.txt @@ -0,0 +1,63 @@ +runtime-class-value-fences.ts:8:20 - error SC1090: EventEmitter and stream class values constructed with options (construct with 'new Readable({ ... })' directly) are not supported yet + + 7 | const R: typeof Readable = Readable; + 8 | const configured = new R({ read() {} }); + | ^~~~~~~~~~~~~~~~~~~~ + 9 | console.log(configured.readable); + +runtime-class-value-fences.ts:12:13 - error SC1090: console.log of 'typeof EventEmitter' values ('typeof EventEmitter' values have no inspect lowering) are not supported yet + + 11 | const C: typeof EventEmitter = EventEmitter; + 12 | console.log(C); + | ^ + 13 | const boxed: unknown = Readable; + +runtime-class-value-fences.ts:13:24 - error SC1090: a runtime-provided class value ('Readable') as an untyped value is not supported yet + + 12 | console.log(C); + 13 | const boxed: unknown = Readable; + | ^~~~~~~~ + 14 | console.log(boxed !== undefined, C.defaultMaxListeners); + +runtime-class-value-fences.ts:14:34 - error SC2020: 'typeof EventEmitter.defaultMaxListeners' is part of the standard library types but has no scriptc lowering yet + + 13 | const boxed: unknown = Readable; + 14 | console.log(boxed !== undefined, C.defaultMaxListeners); + | ^~~~~~~~~~~~~~~~~~~~~ + 15 | + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +runtime-class-value-fences.ts:19:19 - error SC1090: structurally-typed class-value flows ('typeof TypeError' into a 'typeof RangeError' slot: only a class and its subclasses share a slot — extend the base class) are not supported yet + + 18 | // ErrorConstructor[]. + 19 | const errorMix = [TypeError, RangeError]; + | ^~~~~~~~~ + 20 | console.log(errorMix.length); + +runtime-class-value-fences.ts:22:7 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile + + 21 | + 22 | const aggregates = [AggregateError]; + | ^~~~~~~~~~ + 23 | console.log(aggregates.length); + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +runtime-class-value-fences.ts:22:20 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile + + 21 | + 22 | const aggregates = [AggregateError]; + | ^~~~~~~~~~~~~~~~ + 23 | console.log(aggregates.length); + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +runtime-class-value-fences.ts:23:13 - error SC2020: 'AggregateErrorConstructor[].length' is part of the standard library types but has no scriptc lowering yet + + 22 | const aggregates = [AggregateError]; + 23 | console.log(aggregates.length); + | ^~~~~~~~~~~~~~~~~ + 24 | + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index d185f09e6..62703f6a4 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2216,6 +2216,8 @@ "5977-builtin-class-aliases.cjs": 2950, "5978-builtin-class-alias-modules/main.ts": 2750, "5979-writable-instanceof-duplex.ts": 2700, + "5981-runtime-class-values.ts": 2750, + "5982-runtime-class-value-modules/main.ts": 2500, "5990-worker-uncloneable.mjs": 3283, "5991-worker-async-dispose.mjs": 5699, "5992-worker-thread-name-lifetime.mjs": 2687, @@ -5303,6 +5305,8 @@ "5977-builtin-class-aliases.cjs": 4300, "5978-builtin-class-alias-modules/main.ts": 4450, "5979-writable-instanceof-duplex.ts": 6000, + "5981-runtime-class-values.ts": 6900, + "5982-runtime-class-value-modules/main.ts": 3400, "5990-worker-uncloneable.mjs": 22432, "5991-worker-async-dispose.mjs": 25768, "5992-worker-thread-name-lifetime.mjs": 28932, From 6a8732f57a2d52714fb46c4061d91918f3804a88 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 05:52:22 -0500 Subject: [PATCH 115/120] Run iterator helpers and destructuring on generators passed through untyped values A compiled generator that reached an untyped value, as in `function f(it) { return it.map(g) } f(gen())`, refused its iterator helper calls with SC1071 at run time: the runtime's helpers could not step a generator's capsule. Array destructuring of untyped values only handled arrays, strings and bytes, so `const [a, b] = it` threw "is not iterable" for generators, iterators, Maps and Sets. Programs that box a synchronous generator and call a helper method or destructure an untyped value now register, first thing in %main, the checked getters that read a generator capsule's next and return methods (scr_dyn_set_generator_members). With them: - helper calls on a generator capsule run the runtime's helpers, which step and close the generator through its own next() and return(); - reads of a helper name answer the runtime's helper function, so `typeof it.map` is "function" and `it.reduce.call(it, f, init)` works; - destructuring of any other iterable steps it through the iterator protocol (scr_dyn_iter_take, and rest elements through scr_dyn_iter_pack), closing an iterator that has values left. A return() through an untyped value with no argument, or with undefined, now completes with undefined as the typed .return() does, instead of converting undefined to the generator's return type. This also fixes for-of loops over untyped generators with a typed return value that exit early. --- .../builtin-apis/iterator-helpers.mdx | 2 +- packages/compiler/src/backend/llvm/dyn.ts | 90 ++---- .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../lowering/generator-dynamic-dispatch.ts | 259 +++++++++++++----- .../src/frontend/lowering/lower-modules.ts | 1 + .../src/frontend/lowering/lower-stmts.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 4 + packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 + .../6077-generator-helpers-untyped.js.json | 6 + ...6078-destructure-untyped-iterables.js.json | 6 + .../corpus/6079-generator-helpers-any.ts.json | 6 + packages/runtime/src/scr_dyn.h | 29 ++ packages/runtime/src/scr_dyn_invoke.c | 4 + packages/runtime/src/scr_json.c | 131 ++++++++- .../corpus/6077-generator-helpers-untyped.js | 81 ++++++ .../6078-destructure-untyped-iterables.js | 60 ++++ tests/corpus/6079-generator-helpers-any.ts | 24 ++ tests/harness/corpus-costs.json | 6 + 19 files changed, 569 insertions(+), 149 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6077-generator-helpers-untyped.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6078-destructure-untyped-iterables.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6079-generator-helpers-any.ts.json create mode 100644 tests/corpus/6077-generator-helpers-untyped.js create mode 100644 tests/corpus/6078-destructure-untyped-iterables.js create mode 100644 tests/corpus/6079-generator-helpers-any.ts diff --git a/docs/content/limitations/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx index b4c4d8200..4ec19d581 100644 --- a/docs/content/limitations/builtin-apis/iterator-helpers.mdx +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -1 +1 @@ -- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported. +- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped generators and array, string, Map and Set iterators answer the same methods, and array destructuring of untyped values steps generators, iterators, Maps and Sets the way Node does. Helper methods on untyped async generators are unsupported. `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported. diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 154b80c02..cf7578c02 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -4014,14 +4014,9 @@ export class LlDyn { const name = "sc_dyn_iter_n"; this.dynBuilders.set(memoKey, name); const host = this.host; - host.declare(`declare void @scr_jb_init(ptr)`); - host.declare(`declare ptr @scr_jb_finish(ptr)`); - host.declare(`declare void @scr_jb_puts(ptr, ptr)`); - host.declare(`declare void @scr_throw_error(i32, ptr)`); host.declare(`declare ptr @scr_dyn_new_arr()`); host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`); host.declare(`declare ptr @scr_dyn_new_num(double)`); - host.declare(`declare ptr @scr_f64_to_scrstr(double)`); host.declare(`declare void @scr_str_release(ptr)`); const B = new BlockBuilder(); const kd = this.kindOf(B, "%d"); @@ -4033,6 +4028,21 @@ export class LlDyn { const lNext = B.newLabel("din.nt"); B.condBr(isTyped, lTyped, lNext); B.startBlock(lTyped); + // Compiled generators step through the iterator protocol. + host.declare(`declare i1 @scr_dyn_iterates_by_protocol(ptr)`); + host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`); + const protocol = B.tmp(); + const lProtocol = B.newLabel("din.pr"); + const lView = B.newLabel("din.vw"); + B.line(`${protocol} = call i1 @scr_dyn_iterates_by_protocol(ptr %d)`); + B.condBr(protocol, lProtocol, lView); + B.startBlock(lProtocol); + const stepped = B.tmp(); + B.line( + `${stepped} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`, + ); + B.terminate(`ret ptr ${stepped}`); + B.startBlock(lView); host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); const materialized = B.tmp(); @@ -4057,71 +4067,13 @@ export class LlDyn { const lGo = B.newLabel("din.g"); const lThrow = B.newLabel("din.t"); B.condBr(ok, lGo, lThrow); - // V8's exact not-iterable TypeError. + // Other iterables step through the iterator protocol; non-iterables + // throw V8's exact not-iterable TypeError. B.startBlock(lThrow); - const buf = "%dib"; - B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`); - B.line(`call void @scr_jb_init(ptr ${buf})`); - const hasSpelling = B.tmp(); - B.line(`${hasSpelling} = icmp ne ptr %spelling, null`); - const named = B.newLabel("din.named"), - unnamed = B.newLabel("din.unnamed"); - B.condBr(hasSpelling, named, unnamed); - B.startBlock(named); - B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spelling)`); - const namedMessage = B.tmp(); - B.line(`${namedMessage} = call ptr @scr_jb_finish(ptr ${buf})`); - B.line(`call void @scr_throw_error(i32 1, ptr ${namedMessage})`); - B.terminate(`ret ptr null`); - B.startBlock(unnamed); - const lU = B.newLabel("din.u"); - const lN = B.newLabel("din.n"); - const lB2 = B.newLabel("din.b"); - const lNum = B.newLabel("din.m"); - const lF = B.newLabel("din.f"); - const lDef = B.newLabel("din.d"); - const lTail = B.newLabel("din.e"); - B.terminate( - `switch i32 ${kd}, label %${lDef} [ i32 ${DYN_KIND.UNDEF}, label %${lU} i32 ${DYN_KIND.NULL}, label %${lN} i32 ${DYN_KIND.BOOL}, label %${lB2} i32 ${DYN_KIND.NUM}, label %${lNum} i32 ${DYN_KIND.FUNC}, label %${lF} ]`, - ); - B.startBlock(lU); - this.puts(B, buf, "undefined"); - B.br(lTail); - B.startBlock(lN); - this.puts(B, buf, "object null"); - B.br(lTail); - B.startBlock(lB2); - { - const bv = this.boolOf(B, "%d"); - const s = B.tmp(); - B.line( - `${s} = select i1 ${bv}, ptr ${host.cstr("boolean true")}, ptr ${host.cstr("boolean false")}`, - ); - B.line(`call void @scr_jb_puts(ptr ${buf}, ptr ${s})`); - B.br(lTail); - } - B.startBlock(lNum); - { - this.puts(B, buf, "number "); - const x = this.payloadOf(B, "%d", "double"); - const s = B.tmp(); - B.line(`${s} = call ptr @scr_f64_to_scrstr(double ${x})`); - this.putScrStr(B, buf, s); - B.line(`call void @scr_str_release(ptr ${s})`); - B.br(lTail); - } - B.startBlock(lF); - this.puts(B, buf, "function"); - B.br(lTail); - B.startBlock(lDef); - this.puts(B, buf, "object"); - B.br(lTail); - B.startBlock(lTail); - this.puts(B, buf, " is not iterable (cannot read property Symbol(Symbol.iterator))"); - const msg = B.tmp(); - B.line(`${msg} = call ptr @scr_jb_finish(ptr ${buf})`); - B.line(`call void @scr_throw_error(i32 1, ptr ${msg}) ; SCR_ERR_TYPE`); - B.terminate(`ret ptr null`); + host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`); + const taken = B.tmp(); + B.line(`${taken} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`); + B.terminate(`ret ptr ${taken}`); B.startBlock(lGo); const out = B.tmp(); B.line(`${out} = call ptr @scr_dyn_new_arr()`); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index e4c12058f..be67e1b67 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -886,12 +886,15 @@ export const LIB_FN_SYMS: Record = { "dyn.typedRefIs": "scr_dyn_typed_ref_is_key", "dyn.classIs": "scr_dyn_class_is_key", "dyn.iterPack": "scr_dyn_iter_pack", + "dyn.destructureRest": "scr_dyn_destructure_rest", "dyn.iterator": "scr_dyn_iterator", "dyn.arrayFromIterator": "scr_dyn_array_from_iterator", "dyn.iteratorResult": "scr_dyn_iterator_result", "dyn.iteratorCanStep": "scr_dyn_iterator_can_step", "dyn.iteratorStep": "scr_dyn_iterator_step", "dyn.iteratorStepDone": "scr_dyn_iterator_step_done", + "dyn.setGeneratorMembers": "scr_dyn_set_generator_members", + "dyn.iteratorHelperMethod": "scr_dyn_iterator_helper_method", "dyn.collectionIteratorIndex": "scr_dyn_collection_iterator_index", "dyn.mapSeedEntries": "scr_dyn_map_seed_entries", "dyn.mapSeedEntry": "scr_dyn_map_seed_entry", diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index 10505e063..ca6a5ecdc 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -7,6 +7,7 @@ import { funcOf, typeEquals, typeKey, + VOID, type IrExpr, type IrFunction, type IrStmt, @@ -18,6 +19,22 @@ import type { Lowerer } from "./lowerer.js"; type GeneratorType = Extract; type Invoke = Extract; +type KeyGet = Extract; + +/** The iterator helper methods the runtime answers on checked iterators. */ +const HELPER_METHODS = new Set([ + "map", + "filter", + "take", + "drop", + "flatMap", + "reduce", + "toArray", + "forEach", + "some", + "every", + "find", +]); /** An untyped consumer resumes the original native fiber. The capsule's * exact type key selects the channel conversions and result layout. */ @@ -29,6 +46,54 @@ export class GeneratorDynamicDispatch { >(); private readonly generated = new Set(); private readonly properties = new Map }>(); + /** Whether %main registers the next/return getters with the runtime. */ + private membersRegistered = false; + + /** The getter for a generator capsule's `method` (next, return, throw or + * an iterator helper): a checked function over the capsule's type keys + * that answers the method as a function value. `read` is the dyn read + * whose fallback it keeps for other receivers. */ + private ensureProperty(lowerer: Lowerer, method: string, read: KeyGet): IrFunction { + const existing = this.properties.get(method); + if (existing) return existing.fn; + const loc = read.loc; + const fn: IrFunction = { + name: `%dyn.generator.get.${method}`, + params: [{ localId: "value", name: "value", type: DYN }], + returnType: DYN, + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + body: [{ kind: "return", value: { ...read, value: varRef("value", DYN, loc) }, loc }], + loc, + }; + this.properties.set(method, { fn, types: new Set() }); + this.generated.add(fn); + lowerer.liftedFns.push(fn); + // The runtime's own helper functions answer the helper methods. + if (HELPER_METHODS.has(method)) return fn; + lowerer.liftedFns.push({ + name: `%dyn.generator.method.${method}`, + params: [{ localId: "arg", name: "arg", type: DYN }], + returnType: DYN, + locals: [{ id: "arg", name: "arg", type: DYN, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "dynInvoke", + method, + calleeName: `Generator.${method}`, + recv: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, + args: [varRef("arg", DYN, loc)], + type: DYN, + loc, + }, + loc, + }, + ], + loc, + }); + return fn; + } process( lowerer: Lowerer, @@ -37,6 +102,7 @@ export class GeneratorDynamicDispatch { ): boolean { const seen = new Set(); const rewrite = new Set(); + let runtimeSteps = false; const discover = (type: IrType): void => { const key = typeKey(type); if (seen.has(key)) return; @@ -56,10 +122,22 @@ export class GeneratorDynamicDispatch { if (expr.kind === "dynFrom") discover(expr.value.type); if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method)) rewrite.add(fn); + // The runtime steps the values iterator helpers and destructuring + // receive. + if ( + (expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) || + (expr.kind === "dynKeyGet" && + expr.key.kind === "strLit" && + HELPER_METHODS.has(expr.key.value)) || + expr.kind === "dynIterN" || + (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") + ) + runtimeSteps = true; if ( expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && - ["next", "return", "throw"].includes(expr.key.value) + (["next", "return", "throw"].includes(expr.key.value) || + HELPER_METHODS.has(expr.key.value)) ) rewrite.add(fn); return true; @@ -74,56 +152,20 @@ export class GeneratorDynamicDispatch { fn.body = transformStmtList(fn.body, { stmt: (stmt) => stmt, expr: (expr) => { + // A helper method read reaches the runtime's helpers through the + // same getters once generators are boxed. if ( expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && - ["next", "return", "throw"].includes(expr.key.value) + (["next", "return", "throw"].includes(expr.key.value) || + (HELPER_METHODS.has(expr.key.value) && this.generators.size > 0)) ) { const method = expr.key.value; - let property = this.properties.get(method); - if (!property) { - const loc = expr.loc; - const fn: IrFunction = { - name: `%dyn.generator.get.${method}`, - params: [{ localId: "value", name: "value", type: DYN }], - returnType: DYN, - locals: [{ id: "value", name: "value", type: DYN, mutable: false }], - body: [ - { kind: "return", value: { ...expr, value: varRef("value", DYN, loc) }, loc }, - ], - loc, - }; - property = { fn, types: new Set() }; - this.properties.set(method, property); - this.generated.add(fn); - lowerer.liftedFns.push(fn); - lowerer.liftedFns.push({ - name: `%dyn.generator.method.${method}`, - params: [{ localId: "arg", name: "arg", type: DYN }], - returnType: DYN, - locals: [{ id: "arg", name: "arg", type: DYN, mutable: false }], - body: [ - { - kind: "return", - value: { - kind: "dynInvoke", - method, - calleeName: `Generator.${method}`, - recv: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, - args: [varRef("arg", DYN, loc)], - type: DYN, - loc, - }, - loc, - }, - ], - loc, - }); - changed = true; - } + if (!this.properties.has(method)) changed = true; + const getter = this.ensureProperty(lowerer, method, expr); return { kind: "call", - callee: property.fn.name, + callee: getter.name, args: [expr.value], type: DYN, loc: expr.loc, @@ -179,6 +221,50 @@ export class GeneratorDynamicDispatch { }, }); } + // Iterator helpers called on untyped values may reach a compiled + // generator's capsule; the runtime drives it through these getters, + // which %main registers first. + const firstFn = functions[0]; + if ( + runtimeSteps && + !this.membersRegistered && + firstFn !== undefined && + [...this.generators.values()].some((t) => t.async !== true) + ) { + const loc = firstFn.loc; + const read = (method: string): KeyGet => ({ + kind: "dynKeyGet", + value: varRef("value", DYN, loc), + key: { kind: "strLit", value: method, type: STRING, loc }, + type: DYN, + loc, + }); + const getterValue = (method: string): IrExpr => ({ + kind: "dynFrom", + value: { + kind: "closure", + fnName: this.ensureProperty(lowerer, method, read(method)).name, + captures: [], + type: funcOf([DYN], DYN), + loc, + }, + type: DYN, + loc, + }); + lowerer.mainPrelude.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.setGeneratorMembers", + args: [getterValue("next"), getterValue("return")], + type: VOID, + loc, + }, + loc, + }); + this.membersRegistered = true; + changed = true; + } for (const [method, property] of this.properties) for (const [key] of this.generators) { if (property.types.has(key)) continue; @@ -196,18 +282,26 @@ export class GeneratorDynamicDispatch { then: [ { kind: "return", - value: { - kind: "dynFrom", - value: { - kind: "closure", - fnName: `%dyn.generator.method.${method}`, - captures: [], - type: funcOf([DYN], DYN), - loc, - }, - type: DYN, - loc, - }, + value: HELPER_METHODS.has(method) + ? { + kind: "libCall", + fn: "dyn.iteratorHelperMethod", + args: [{ kind: "strLit", value: method, type: STRING, loc }], + type: DYN, + loc, + } + : { + kind: "dynFrom", + value: { + kind: "closure", + fnName: `%dyn.generator.method.${method}`, + captures: [], + type: funcOf([DYN], DYN), + loc, + }, + type: DYN, + loc, + }, loc, }, ], @@ -228,10 +322,14 @@ export class GeneratorDynamicDispatch { const supplied = dispatch.source.args.length > 0; const incoming = supplied ? varRef("p.1", DYN, loc) : dynUndefinedExpr(loc); const channel = mode === "next" ? type.nextT : type.retT; + // return() with no value completes with undefined, as the typed + // .return() does, whatever the return channel's type. const arg = mode === "throw" ? incoming - : channel.kind === "undefinedT" || channel.kind === "void" + : channel.kind === "undefinedT" || + channel.kind === "void" || + (mode === "return" && !supplied) ? null : lowerer.coerceToExpected(incoming, channel); const convertible = canConvertToDyn( @@ -263,25 +361,36 @@ export class GeneratorDynamicDispatch { else_: null, loc, }); + const resume = (sent: IrExpr | null): IrStmt => ({ + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "genResume", + mode, + gen: { kind: "dynCheck", value: receiver, type, loc }, + arg: sent, + type: resultType, + loc, + }, + type: DYN, + loc, + }, + loc, + }); + const valid = + convertible && (arg === null || mode === "throw" || typeEquals(arg.type, channel)); + if (valid && mode === "return" && arg !== null) + body.push({ + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: incoming, type: BOOL, loc }, + then: [resume(null)], + else_: null, + loc, + }); body.push( - convertible && (arg === null || mode === "throw" || typeEquals(arg.type, channel)) - ? { - kind: "return", - value: { - kind: "dynFrom", - value: { - kind: "genResume", - mode, - gen: { kind: "dynCheck", value: receiver, type, loc }, - arg, - type: resultType, - loc, - }, - type: DYN, - loc, - }, - loc, - } + valid + ? resume(arg) : { kind: "runtimeFence", code: "SC1071", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 128652bd2..3ec1d6c09 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -3132,6 +3132,7 @@ function rememberEvaluationError( export function buildMain(lowerer: Lowerer): IrFunction { const loc: SrcLoc = { file: lowerer.entry.fileName, start: 0, end: 0 }; const body: IrStmt[] = [ + ...lowerer.mainPrelude, ...cjsModuleRegistryPrelude(lowerer, loc), ...handleEventReaderPrelude(lowerer, loc), ]; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 3a0958ae6..43efebd16 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2396,7 +2396,7 @@ export function lowerBindingPattern( ) ? { kind: "libCall", - fn: "dyn.iterPack", + fn: "dyn.destructureRest", args: [srcRef(), { kind: "strLit", value: dynSpell ?? "", type: STRING, loc }], type: DYN, loc, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 1873ac361..be71498da 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10604,6 +10604,10 @@ export class Lowerer { return lowerDefaultExport(this, stmt); } + /** Statements %main runs before any module initializer: runtime + * registrations the lowering passes add. */ + readonly mainPrelude: IrStmt[] = []; + buildMain(): IrFunction { return buildMain(this); } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index fc4c383ff..f20d0945e 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -569,6 +569,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.propertyIsEnumerableComputed", // the destructuring pack throws V8's TypeError on non-iterable dyn kinds "dyn.iterPack", + "dyn.destructureRest", "dyn.arrayFromIterator", "dyn.iterator", "dyn.iteratorResult", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 034d76917..df4c3209f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -176,12 +176,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * not iterable (cannot read property Symbol(Symbol.iterator))"). In the * may-throw seed set. */ "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, + "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, "dyn.iteratorResult": { argTypes: [DYN], result: DYN }, "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, + "dyn.setGeneratorMembers": { argTypes: [DYN, DYN], result: VOID }, + "dyn.iteratorHelperMethod": { argTypes: [STRING], result: DYN }, "dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 }, "dyn.mapSeedEntries": { argTypes: [DYN], result: DYN }, "dyn.mapSeedEntry": { argTypes: [DYN], result: DYN }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6077-generator-helpers-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6077-generator-helpers-untyped.js.json new file mode 100644 index 000000000..3d3c69f19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6077-generator-helpers-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6077-generator-helpers-untyped.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6078-destructure-untyped-iterables.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6078-destructure-untyped-iterables.js.json new file mode 100644 index 000000000..6feb69d8e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6078-destructure-untyped-iterables.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6078-destructure-untyped-iterables.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6079-generator-helpers-any.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6079-generator-helpers-any.ts.json new file mode 100644 index 000000000..3d94dc336 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6079-generator-helpers-any.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6079-generator-helpers-any.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 1ab857e57..17b68712e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -713,6 +713,22 @@ double scr_dyn_collection_iterator_index(const ScrDyn *iterator); * than 2 and 3) and the index its next step reads from. */ void scr_dyn_native_iterator_state(const ScrDyn *iterator, unsigned *kind, unsigned *selection, ScrMap **collection, double *index); +/* A compiled program that boxes generators and calls iterator helpers on + * untyped values registers the checked functions that read a generator + * capsule's next and return methods (each takes the capsule). The + * iterator helpers below then drive compiled generators. */ +void scr_dyn_set_generator_members(ScrDyn *next_getter, ScrDyn *return_getter); +/* An iterator helper method called on a synchronous generator capsule: + * true with *out set (+1, or NULL with the exception pending) when + * `method` is one. NULL until the program registers its generator + * members. */ +extern SCR_TL bool (*scr_dyn_generator_helper_hook)(ScrDyn *recv, const char *method, + ScrDyn *const *args, size_t argc, + ScrDyn **out); +/* The Iterator.prototype helper method `name` as a function value (+1), + * the value a compiled generator's helper property reads; undefined for + * other names. */ +ScrDyn *scr_dyn_iterator_helper_method(const ScrStr *name); /* The ordinary object snapshot plus a retained typed source. source_access * releases that source when materialize=false and returns a fresh snapshot * when true. */ @@ -857,6 +873,19 @@ void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what); * compile-time source spelling), else the runtime kind wording. Borrows * both; NULL with the exception pending on the throw. */ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg); +/* A rest element's pack (`const [a, ...rest] = d`): scr_dyn_iter_pack's + * kinds pack as there, and other iterables step through the iterator + * protocol to the end. +1, or NULL with the error pending. */ +ScrDyn *scr_dyn_destructure_rest(const ScrDyn *src, const ScrStr *msg); +/* Destructuring's first `n` values (`const [a, b] = d`) of a dyn source + * other than an array, string or bytes (the emitted walk packs those), + * through the iterator protocol: an iterator with values left closes. + * `spell` is the compile-time not-iterable wording, or NULL. +1, or NULL + * with the error pending. */ +ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell); +/* True for typed capsules that iterate through the protocol (a compiled + * synchronous generator) rather than through their materialized view. */ +bool scr_dyn_iterates_by_protocol(const ScrDyn *value); /* Map constructor seeds: retain array inputs for live iteration, accept * nullish inputs as empty, and validate each entry before reading 0/1. */ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index a3085399f..752c19805 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -815,6 +815,10 @@ static ScrDyn *scr_dyn_invoke_impl( bool handled = false; ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); if (handled) return fast; + /* Iterator helpers on a compiled generator. */ + if (scr_dyn_generator_helper_hook && + scr_dyn_generator_helper_hook(recv, method, args, argc, &fast)) + return fast; if (scr_event_ref_hooks && scr_event_ref_hooks->invoke(recv, method, args, argc, what, &fast)) return fast; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 525b6a192..6174913c0 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4704,6 +4704,19 @@ static int scr_ih_method_index(const char *name, size_t len); static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc); static ScrDyn *scr_ih_method_function(int index); +static bool scr_dyn_sync_generator(const ScrDyn *value); +extern SCR_TL ScrDyn *(*scr_generator_member_hook)(const ScrDyn *, const char *, size_t); + +/* scr_iterator_read of an iterator's own next or return method: a + * compiled generator answers through the program's registered getters. */ +static ScrDyn *scr_iterator_member(const ScrDyn *iterator, const char *key, size_t len) { + if (scr_generator_member_hook && scr_dyn_sync_generator(iterator)) { + ScrDyn *member = scr_generator_member_hook(iterator, key, len); + if (member) return member; + } + return scr_iterator_read(iterator, key, len); +} + static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) { if (len != 4 || memcmp(key, "next", 4)) { /* The Iterator.prototype helper methods. */ @@ -4893,7 +4906,7 @@ static bool scr_ih_budget(const ScrDyn *limit, double *out) { * Node's TypeError. Answers false when an exception is pending after. */ static bool scr_ih_close(ScrDyn *iterator, bool abrupt) { ScrCaught *first = abrupt ? scr_exc_take() : NULL; - ScrDyn *method = scr_iterator_read(iterator, "return", 6); + ScrDyn *method = scr_iterator_member(iterator, "return", 6); bool ok = method != NULL; if (ok && method->kind != SCR_DYN_UNDEF && method->kind != SCR_DYN_NULL) { ScrDyn *result = scr_iterator_call(method, iterator, "return"); @@ -4960,7 +4973,7 @@ static ScrDyn *scr_ih_direct(const ScrDyn *self, const char *method) { scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); return NULL; } - return scr_iterator_read(self, "next", 4); + return scr_iterator_member(self, "next", 4); } static ScrDyn *scr_ih_result(ScrDyn *value, bool done) { @@ -5017,7 +5030,7 @@ static ScrDyn *scr_ih_flattenable(ScrDyn *value, ScrDyn **next_out) { } scr_dyn_release(method); if (!iterator) return NULL; - ScrDyn *next = scr_iterator_read(iterator, "next", 4); + ScrDyn *next = scr_iterator_member(iterator, "next", 4); if (!next) { scr_dyn_release(iterator); return NULL; @@ -13967,3 +13980,115 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { return scr_dyn_new_obj(); } } + +/* ── compiled generators through untyped values ─────────────────────── */ + +static SCR_TL ScrDyn *scr_generator_next_getter; +static SCR_TL ScrDyn *scr_generator_return_getter; +/* Set with the getters, so only programs that register them link the + * iterator helpers through these paths. External, so the compiler keeps + * the calls through them indirect. */ +SCR_TL ScrDyn *(*scr_generator_member_hook)(const ScrDyn *, const char *, size_t) = NULL; +SCR_TL bool (*scr_dyn_generator_helper_hook)(ScrDyn *, const char *, ScrDyn *const *, size_t, + ScrDyn **) = NULL; +static ScrDyn *scr_generator_member(const ScrDyn *capsule, const char *key, size_t len); +static bool scr_generator_helper_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, + size_t argc, ScrDyn **out); + +static void scr_generator_members_cleanup(void) { + scr_dyn_release(scr_generator_next_getter); + scr_dyn_release(scr_generator_return_getter); + scr_generator_next_getter = NULL; + scr_generator_return_getter = NULL; + scr_generator_member_hook = NULL; + scr_dyn_generator_helper_hook = NULL; +} + +void scr_dyn_set_generator_members(ScrDyn *next_getter, ScrDyn *return_getter) { + if (!scr_generator_next_getter) scr_atexit(scr_generator_members_cleanup); + scr_dyn_release(scr_generator_next_getter); + scr_dyn_release(scr_generator_return_getter); + scr_generator_next_getter = scr_dyn_retain(next_getter); + scr_generator_return_getter = scr_dyn_retain(return_getter); + scr_generator_member_hook = &scr_generator_member; + scr_dyn_generator_helper_hook = &scr_generator_helper_invoke; +} + +static bool scr_dyn_sync_generator(const ScrDyn *value) { + return scr_dyn_generator(value) && value->v.typed_ref.type_key[0] == 'g'; +} + +/* A synchronous generator capsule's next or return method (+1), read + * through the program's registered getters. NULL for other keys. */ +static ScrDyn *scr_generator_member(const ScrDyn *capsule, const char *key, size_t len) { + ScrDyn *getter = len == 4 && !memcmp(key, "next", 4) ? scr_generator_next_getter + : len == 6 && !memcmp(key, "return", 6) ? scr_generator_return_getter : NULL; + if (!getter) return NULL; + ScrDyn *args[1] = { (ScrDyn *)capsule }; + return scr_dyn_call(getter, args, 1, "next"); +} + +static bool scr_generator_helper_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, + size_t argc, ScrDyn **out) { + if (!scr_dyn_sync_generator(recv)) return false; + int index = scr_ih_method_index(method, strlen(method)); + if (index < 0) return false; + *out = scr_iterator_helper_method(recv, index, args, argc); + return true; +} + +ScrDyn *scr_dyn_iterator_helper_method(const ScrStr *name) { + int index = scr_ih_method_index(name->data, name->len); + return index >= 0 ? scr_ih_method_function(index) : scr_dyn_retain(scr_dyn_undefined()); +} + +/* Destructuring over an iterable the pack walk cannot list (a compiled + * generator, an iterator or iterator helper, a Map or Set): GetIterator, + * then the first `n` values through the iterator protocol, or every value + * when `all`, into a fresh array. Positions past the end read undefined, + * and an iterator with values left closes. `spell` is the compile-time + * not-iterable wording, or NULL. +1, or NULL with the error pending. */ +static ScrDyn *scr_dyn_iter_steps(const ScrDyn *src, const ScrStr *spell, size_t n, bool all) { + ScrDyn *iterator = scr_dyn_iterator(src, spell); + if (!iterator) return NULL; + ScrDyn *next = scr_iterator_member(iterator, "next", 4); + if (!next) { + scr_dyn_release(iterator); + return NULL; + } + ScrDyn *out = scr_dyn_new_arr(); + bool done = false; + for (size_t i = 0; all ? !done : i < n; i++) { + ScrDyn *value = done ? scr_dyn_retain(scr_dyn_undefined()) : scr_ih_step(iterator, next, &done); + if (!value) goto fail; + if (all && done) scr_dyn_release(value); + else scr_dyn_arr_push(out, value); + } + if (!done && !scr_ih_close(iterator, false)) goto fail; + scr_dyn_release(next); + scr_dyn_release(iterator); + return out; +fail: + scr_dyn_release(out); + scr_dyn_release(next); + scr_dyn_release(iterator); + return NULL; +} + +ScrDyn *scr_dyn_destructure_rest(const ScrDyn *src, const ScrStr *msg) { + bool packs = src->kind == SCR_DYN_ARR || src->kind == SCR_DYN_STR || src->kind == SCR_DYN_BYTES || + (src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack) || + (src->kind == SCR_DYN_TYPED_REF && !scr_dyn_iterates_by_protocol(src)); + return packs ? scr_dyn_iter_pack(src, msg) : scr_dyn_iter_steps(src, msg, 0, true); +} + +ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell) { + ScrStr *spelling = spell && *spell ? scr_str_new(spell, strlen(spell)) : NULL; + ScrDyn *out = scr_dyn_iter_steps(src, spelling, n, false); + scr_str_release(spelling); + return out; +} + +bool scr_dyn_iterates_by_protocol(const ScrDyn *value) { + return scr_dyn_sync_generator(value); +} diff --git a/tests/corpus/6077-generator-helpers-untyped.js b/tests/corpus/6077-generator-helpers-untyped.js new file mode 100644 index 000000000..381595d9d --- /dev/null +++ b/tests/corpus/6077-generator-helpers-untyped.js @@ -0,0 +1,81 @@ +// Iterator helpers on compiled generators that reach untyped parameters: +// the runtime's helpers step the generator through its own next() and +// close it through return(). +const log = []; +const flush = () => log.splice(0).join(" "); +function* count(n) { + try { + for (let i = 1; i <= n; i++) { + log.push("y" + i); + yield i; + } + return "done"; + } finally { + log.push("closed"); + } +} +function twice(it) { return it.map((x) => x * 2).toArray(); } +console.log(twice(count(3)), flush()); +function firstOver(it, k) { return it.find((x) => x > k); } +console.log(firstOver(count(9), 3), flush()); +function stepper(it) { + const h = it.filter((x) => x % 2 === 1); + return [h.next(), h.next(), h.return(), h.next()]; +} +console.log(stepper(count(9)), flush()); +function sum(it) { return it.reduce((a, b) => a + b, 0); } +console.log(sum(count(4)), flush()); +function product(it) { return it.reduce((a, b) => a * b); } +console.log(product(count(4)), flush()); +function lazy(it) { return it.drop(1).take(2); } +console.log([...lazy(count(9))], flush()); +function each(it) { const out = []; it.forEach((x, i) => out.push(x + ":" + i)); return out; } +console.log(each(count(3)), flush()); +function some(it) { return it.some((x) => x === 2); } +console.log(some(count(5)), flush()); +function every(it) { return it.every((x) => x < 3); } +console.log(every(count(5)), flush()); +function flat(it) { return it.flatMap((x) => [x, -x]).toArray(); } +console.log(flat(count(2)), flush()); +function chain(it) { return it.map((x) => x + 1).filter((x) => x % 2 === 0).map(String).toArray(); } +console.log(chain(count(6)), flush()); +function loop(it) { + const out = []; + for (const x of it.map((x) => x * 10)) { + out.push(x); + if (x >= 20) break; + } + return out; +} +console.log(loop(count(5)), flush()); +function from(it) { return Array.from(it.take(2)); } +console.log(from(count(5)), flush()); +function zeroTake(it) { return it.take(0).toArray(); } +console.log(zeroTake(count(5)), flush()); +function after(it) { const a = it.take(1).toArray(); return [a, it.next()]; } +console.log(after(count(3)), flush()); +function failing(it) { + try { + it.map((x) => { if (x === 2) throw new Error("boom " + x); return x; }).toArray(); + } catch (e) { + return e.message; + } +} +console.log(failing(count(5)), flush()); +function errorOf(run) { try { run(); } catch (e) { return e.name + ": " + e.message; } } +function check(it) { + return [ + errorOf(() => it.map(42)), + errorOf(() => it.take(-1)), + errorOf(() => it.reduce((a, b) => a + b)), + ]; +} +console.log(check(count(0)), flush()); +function kinds(it) { return [typeof it.map, typeof it.toArray, typeof it.next]; } +console.log(kinds(count(1))); +function readNext(it) { const next = it.next; return [next.call(it), next.call(it)]; } +console.log(readNext(count(5)), flush()); +function first(it) { for (const x of it) return x; } +console.log(first(count(3)), flush()); +function closeEarly(it) { it.next(); return it.return(); } +console.log(closeEarly(count(3)), flush()); diff --git a/tests/corpus/6078-destructure-untyped-iterables.js b/tests/corpus/6078-destructure-untyped-iterables.js new file mode 100644 index 000000000..cf8435cba --- /dev/null +++ b/tests/corpus/6078-destructure-untyped-iterables.js @@ -0,0 +1,60 @@ +// Array destructuring of untyped iterables: Map and Set objects, their +// iterators, compiled generators and objects with a Symbol.iterator method +// step through the iterator protocol, and an iterator with values left +// closes. +const m = new Map([["a", 1], ["b", 2], ["c", 3]]); +function two(it) { const [x, y] = it; return [x, y]; } +console.log(two(m), two(new Set([7, 8, 9])), two(m.keys()), two("hey"), two([1])); +function rest(it) { const [x, ...more] = it; return [x, more]; } +console.log(rest(m.values()), rest(new Set([1, 2, 3])), rest(m.entries())); +function* g() { + try { + yield 1; + yield 2; + yield 3; + } finally { + console.log("finally"); + } +} +console.log(two(g()), rest(g())); +function holes(it) { const [, b, , d] = it; return [b, d]; } +console.log(holes(g())); +function none(it) { const [] = it; return "none"; } +console.log(none(g())); +function* short() { yield "only"; } +console.log(two(short())); +const custom = { + [Symbol.iterator]() { + let i = 0; + return { + next: () => ({ value: i++, done: i > 5 }), + return() { + console.log("return called"); + return {}; + }, + }; + }, +}; +console.log(two(custom)); +const helper = [1, 2, 3, 4].values().map((x) => x * 3); +console.log(two(helper), [...helper]); +for (const bad of [1, null, undefined, {}, true]) { + try { + two(bad); + } catch (e) { + console.log(e.name, e.message); + } +} +const throwing = { + [Symbol.iterator]() { + return { + next() { throw new Error("next failed"); }, + return() { console.log("not called"); return {}; }, + }; + }, +}; +try { + two(throwing); +} catch (e) { + console.log(e.message); +} diff --git a/tests/corpus/6079-generator-helpers-any.ts b/tests/corpus/6079-generator-helpers-any.ts new file mode 100644 index 000000000..f72fd55ef --- /dev/null +++ b/tests/corpus/6079-generator-helpers-any.ts @@ -0,0 +1,24 @@ +// Iterator helpers on generators passed through `any` and `unknown`. +function* letters(): Generator { + yield "a"; + yield "b"; + yield "c"; +} +function upper(it: any): string[] { + return it.map((s: string) => s.toUpperCase()).toArray(); +} +console.log(upper(letters())); +const boxed: unknown = letters(); +console.log((boxed as any).filter((s: string) => s !== "b").toArray()); +const held: any[] = [letters(), letters()]; +console.log(held.map((h) => h.drop(1).toArray())); +function* numbered(): Generator { + yield 1; + yield 2; + return "end"; +} +function firstTwo(it: any): number[] { + return it.take(2).toArray(); +} +const later: any = numbered(); +console.log(firstTwo(numbered()), later.some((n: number) => n > 1)); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 62703f6a4..1cb2f1a14 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2252,6 +2252,9 @@ "6074-stored-collection-helpers-js.js": 4100, "6075-print-iterators.ts": 3800, "6076-print-iterators-js.js": 3500, + "6077-generator-helpers-untyped.js": 2550, + "6078-destructure-untyped-iterables.js": 2500, + "6079-generator-helpers-any.ts": 2550, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5341,6 +5344,9 @@ "6074-stored-collection-helpers-js.js": 3600, "6075-print-iterators.ts": 3450, "6076-print-iterators-js.js": 3300, + "6077-generator-helpers-untyped.js": 3050, + "6078-destructure-untyped-iterables.js": 2550, + "6079-generator-helpers-any.ts": 2700, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From 60084fcf4874302af536c723c1aac24f286d7a08 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 06:14:15 -0500 Subject: [PATCH 116/120] Test the common IR node kinds first in child traversals everyExprChild, everyStmtChild, mapExprChildren and mapStmtChildren switch over the node kind: 96 expression and 26 statement string cases, which the engine tests one after another in source order. Every analysis and rewrite walks the IR through them, and some of the most common kinds were near the end: in the three.js nodes module, dynFrom, libCall and dynCheck make up 27% of expression nodes and were the 64th, 94th and 96th cases. The cases for the most common kinds now come first, in the order of how often they occur in the three.js nodes and Effect fixtures. All other cases keep their order, and no case changes. In those fixtures, seven expression kinds make up 89% of expression nodes and four statement kinds make up 88% of statements. A full child walk of the three.js nodes module takes about 108 ns per node instead of 154 ns. Compiling it to LLVM text takes 161 s instead of 180 s (median of five interleaved runs), and the Effect schema fixture 64 s instead of 69 s. The emitted IR is unchanged. --- packages/compiler/src/ir/traverse.ts | 196 ++++++++++++++------------- 1 file changed, 102 insertions(+), 94 deletions(-) diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts index 1ef70bdb2..6375eb39b 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -127,21 +127,54 @@ export function everyExprChild( expr: (expr: IrExpr) => boolean, stmt: (stmt: IrStmt) => boolean, ): boolean { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "numLit": + case "varRef": return true; + case "call": + return node.args.every((child) => expr(child)); + case "dynFrom": + return expr(node.value); case "strLit": return true; - case "moduleNsRef": + case "libCall": + return node.args.every((child) => expr(child)); + case "dynCheck": + return expr(node.value); + case "strEq": + return expr(node.left) && expr(node.right); + case "fieldGet": + return expr(node.obj); + case "ternary": + return expr(node.cond) && expr(node.then) && expr(node.else_); + case "closure": return true; case "boolLit": return true; + case "numLit": + return true; case "unitLit": return true; - case "varRef": - return true; + case "seqExpr": + return node.stmts.every((child) => stmt(child)) && expr(node.result); + case "dynCall": + return ( + expr(node.callee) && + (node.receiver === undefined || expr(node.receiver)) && + (node.calleeNameValue === undefined || expr(node.calleeNameValue)) && + node.args.every((child) => expr(child)) + ); + case "dynTest": + return expr(node.value); + case "dynScalarEq": + return expr(node.left) && expr(node.right); + case "upcast": + return expr(node.value); case "bin": return expr(node.left) && expr(node.right); + case "moduleNsRef": + return true; case "unary": return expr(node.operand); case "incDec": @@ -156,14 +189,10 @@ export function everyExprChild( return expr(node.left) && expr(node.right); case "strConcat": return expr(node.left) && expr(node.right); - case "strEq": - return expr(node.left) && expr(node.right); case "strCmp": return expr(node.left) && expr(node.right); case "toString": return expr(node.operand); - case "ternary": - return expr(node.cond) && expr(node.then) && expr(node.else_); case "nullish": return expr(node.left) && expr(node.right); case "orDefault": @@ -206,12 +235,8 @@ export function everyExprChild( return node.seed === undefined || expr(node.seed); case "setIntrinsic": return expr(node.receiver) && node.args.every((child) => expr(child)); - case "call": - return node.args.every((child) => expr(child)); case "ffiCall": return node.args.every((child) => expr(child)); - case "closure": - return true; case "callValue": return ( expr(node.callee) && @@ -240,8 +265,6 @@ export function everyExprChild( return expr(node.callee) && node.args.every((child) => expr(child)); case "instanceOfValue": return expr(node.value) && expr(node.classValue); - case "upcast": - return expr(node.value); case "promiseVoidWiden": return expr(node.value); case "downcast": @@ -250,8 +273,6 @@ export function everyExprChild( return expr(node.value); case "virtualCall": return node.args.every((child) => expr(child)); - case "fieldGet": - return expr(node.obj); case "recordLit": return node.fields.every((child) => expr(child.value)); case "recordClone": @@ -260,15 +281,6 @@ export function everyExprChild( return expr(node.obj); case "recordKeyGet": return expr(node.obj) && expr(node.key); - case "dynFrom": - return expr(node.value); - case "dynCall": - return ( - expr(node.callee) && - (node.receiver === undefined || expr(node.receiver)) && - (node.calleeNameValue === undefined || expr(node.calleeNameValue)) && - node.args.every((child) => expr(child)) - ); case "dynInvoke": return ( expr(node.recv) && @@ -282,16 +294,10 @@ export function everyExprChild( node.fields === undefined || node.fields.every((child) => expr(child.key) && expr(child.value)) ); - case "dynTest": - return expr(node.value); case "dynKeyGet": return expr(node.key) && expr(node.value); case "dynHasKey": return expr(node.value); - case "dynScalarEq": - return expr(node.left) && expr(node.right); - case "seqExpr": - return node.stmts.every((child) => stmt(child)) && expr(node.result); case "dynDestrCheck": return expr(node.value); case "dynIterN": @@ -332,12 +338,8 @@ export function everyExprChild( return expr(node.left) && expr(node.right); case "intrinsic": return node.args.every((child) => expr(child)); - case "libCall": - return node.args.every((child) => expr(child)); case "jsonStringify": return expr(node.value); - case "dynCheck": - return expr(node.value); } node satisfies never; throw new Error("unhandled IR node"); @@ -349,19 +351,27 @@ export function everyStmtChild( expr: (expr: IrExpr) => boolean, stmt: (stmt: IrStmt) => boolean, ): boolean { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "varDecl": - return node.init === null || expr(node.init); - case "assign": - return expr(node.value); - case "exprStmt": - return expr(node.expr); + case "return": + return node.value === null || expr(node.value); case "if": return ( expr(node.cond) && node.then.every((child) => stmt(child)) && (node.else_ === null || node.else_.every((child) => stmt(child))) ); + case "varDecl": + return node.init === null || expr(node.init); + case "exprStmt": + return expr(node.expr); + case "assign": + return expr(node.value); + case "block": + return node.body.every((child) => stmt(child)); + case "fieldSet": + return expr(node.obj) && expr(node.value); case "while": return expr(node.cond) && node.body.every((child) => stmt(child)); case "doWhile": @@ -393,10 +403,6 @@ export function everyStmtChild( return expr(node.arr) && expr(node.index) && expr(node.value); case "forOf": return expr(node.iterable) && node.body.every((child) => stmt(child)); - case "return": - return node.value === null || expr(node.value); - case "fieldSet": - return expr(node.obj) && expr(node.value); case "recordSet": return expr(node.obj) && expr(node.value); case "recordKeySet": @@ -407,8 +413,6 @@ export function everyStmtChild( return true; case "continue": return true; - case "block": - return node.body.every((child) => stmt(child)); case "throw": return expr(node.value); case "runtimeFence": @@ -432,21 +436,54 @@ export function mapExprChildren( expr: (expr: IrExpr) => IrExpr, stmt: (stmt: IrStmt) => IrStmt, ): IrExpr { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "numLit": + case "varRef": return node; + case "call": + return { ...node, args: node.args.map((child) => expr(child)) }; + case "dynFrom": + return { ...node, value: expr(node.value) }; case "strLit": return node; - case "moduleNsRef": + case "libCall": + return { ...node, args: node.args.map((child) => expr(child)) }; + case "dynCheck": + return { ...node, value: expr(node.value) }; + case "strEq": + return { ...node, left: expr(node.left), right: expr(node.right) }; + case "fieldGet": + return { ...node, obj: expr(node.obj) }; + case "ternary": + return { ...node, cond: expr(node.cond), then: expr(node.then), else_: expr(node.else_) }; + case "closure": return node; case "boolLit": return node; + case "numLit": + return node; case "unitLit": return node; - case "varRef": - return node; + case "seqExpr": + return { ...node, stmts: node.stmts.map((child) => stmt(child)), result: expr(node.result) }; + case "dynCall": { + const result: Extract = { ...node, callee: expr(node.callee) }; + if (node.receiver !== undefined) result.receiver = expr(node.receiver); + if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue); + result.args = node.args.map((child) => expr(child)); + return result; + } + case "dynTest": + return { ...node, value: expr(node.value) }; + case "dynScalarEq": + return { ...node, left: expr(node.left), right: expr(node.right) }; + case "upcast": + return { ...node, value: expr(node.value) }; case "bin": return { ...node, left: expr(node.left), right: expr(node.right) }; + case "moduleNsRef": + return node; case "unary": return { ...node, operand: expr(node.operand) }; case "incDec": @@ -461,14 +498,10 @@ export function mapExprChildren( return { ...node, left: expr(node.left), right: expr(node.right) }; case "strConcat": return { ...node, left: expr(node.left), right: expr(node.right) }; - case "strEq": - return { ...node, left: expr(node.left), right: expr(node.right) }; case "strCmp": return { ...node, left: expr(node.left), right: expr(node.right) }; case "toString": return { ...node, operand: expr(node.operand) }; - case "ternary": - return { ...node, cond: expr(node.cond), then: expr(node.then), else_: expr(node.else_) }; case "nullish": return { ...node, left: expr(node.left), right: expr(node.right) }; case "orDefault": @@ -542,12 +575,8 @@ export function mapExprChildren( receiver: expr(node.receiver), args: node.args.map((child) => expr(child)), }; - case "call": - return { ...node, args: node.args.map((child) => expr(child)) }; case "ffiCall": return { ...node, args: node.args.map((child) => expr(child)) }; - case "closure": - return node; case "callValue": { const callee = expr(node.callee); if (node.receiver === undefined) @@ -581,8 +610,6 @@ export function mapExprChildren( return { ...node, callee: expr(node.callee), args: node.args.map((child) => expr(child)) }; case "instanceOfValue": return { ...node, value: expr(node.value), classValue: expr(node.classValue) }; - case "upcast": - return { ...node, value: expr(node.value) }; case "promiseVoidWiden": return { ...node, value: expr(node.value) }; case "downcast": @@ -591,8 +618,6 @@ export function mapExprChildren( return { ...node, value: expr(node.value) }; case "virtualCall": return { ...node, args: node.args.map((child) => expr(child)) }; - case "fieldGet": - return { ...node, obj: expr(node.obj) }; case "recordLit": return { ...node, @@ -608,15 +633,6 @@ export function mapExprChildren( return { ...node, obj: expr(node.obj) }; case "recordKeyGet": return { ...node, obj: expr(node.obj), key: expr(node.key) }; - case "dynFrom": - return { ...node, value: expr(node.value) }; - case "dynCall": { - const result: Extract = { ...node, callee: expr(node.callee) }; - if (node.receiver !== undefined) result.receiver = expr(node.receiver); - if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue); - result.args = node.args.map((child) => expr(child)); - return result; - } case "dynInvoke": { const result: Extract = { ...node, recv: expr(node.recv) }; if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue); @@ -636,16 +652,10 @@ export function mapExprChildren( value: expr(child.value), })), }; - case "dynTest": - return { ...node, value: expr(node.value) }; case "dynKeyGet": return { ...node, key: expr(node.key), value: expr(node.value) }; case "dynHasKey": return { ...node, value: expr(node.value) }; - case "dynScalarEq": - return { ...node, left: expr(node.left), right: expr(node.right) }; - case "seqExpr": - return { ...node, stmts: node.stmts.map((child) => stmt(child)), result: expr(node.result) }; case "dynDestrCheck": return { ...node, value: expr(node.value) }; case "dynIterN": @@ -686,12 +696,8 @@ export function mapExprChildren( return { ...node, left: expr(node.left), right: expr(node.right) }; case "intrinsic": return { ...node, args: node.args.map((child) => expr(child)) }; - case "libCall": - return { ...node, args: node.args.map((child) => expr(child)) }; case "jsonStringify": return { ...node, value: expr(node.value) }; - case "dynCheck": - return { ...node, value: expr(node.value) }; } node satisfies never; throw new Error("unhandled IR node"); @@ -703,13 +709,11 @@ export function mapStmtChildren( expr: (expr: IrExpr) => IrExpr, stmt: (stmt: IrStmt) => IrStmt, ): IrStmt { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "varDecl": - return { ...node, init: node.init === null ? null : expr(node.init) }; - case "assign": - return { ...node, value: expr(node.value) }; - case "exprStmt": - return { ...node, expr: expr(node.expr) }; + case "return": + return { ...node, value: node.value === null ? null : expr(node.value) }; case "if": return { ...node, @@ -717,6 +721,16 @@ export function mapStmtChildren( then: node.then.map((child) => stmt(child)), else_: node.else_ === null ? null : node.else_.map((child) => stmt(child)), }; + case "varDecl": + return { ...node, init: node.init === null ? null : expr(node.init) }; + case "exprStmt": + return { ...node, expr: expr(node.expr) }; + case "assign": + return { ...node, value: expr(node.value) }; + case "block": + return { ...node, body: node.body.map((child) => stmt(child)) }; + case "fieldSet": + return { ...node, obj: expr(node.obj), value: expr(node.value) }; case "while": return { ...node, cond: expr(node.cond), body: node.body.map((child) => stmt(child)) }; case "doWhile": @@ -755,10 +769,6 @@ export function mapStmtChildren( iterable: expr(node.iterable), body: node.body.map((child) => stmt(child)), }; - case "return": - return { ...node, value: node.value === null ? null : expr(node.value) }; - case "fieldSet": - return { ...node, obj: expr(node.obj), value: expr(node.value) }; case "recordSet": return { ...node, obj: expr(node.obj), value: expr(node.value) }; case "recordKeySet": @@ -769,8 +779,6 @@ export function mapStmtChildren( return node; case "continue": return node; - case "block": - return { ...node, body: node.body.map((child) => stmt(child)) }; case "throw": return { ...node, value: expr(node.value) }; case "runtimeFence": From c836f939986f0cf04854b69020b18461a7454d8f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 06:18:17 -0500 Subject: [PATCH 117/120] Copy repeated childless IR nodes with a spread unshareNodes gives every occurrence of a repeated node its own object. Nodes with children are copied by mapExprChildren and mapStmtChildren, which spread the node and keep its type and location objects. Childless nodes were copied by reparseExpr and reparseStmt, which stringify the node and parse the text, copying the type and location objects as well; on the three.js nodes fixture that took about 6 s. Childless nodes are now spread too. Analyses key their facts by node identity, which each copy still has of its own. The type and location objects are shared as they already are in every copied node with children and in every function without a repeated node. The emitted IR is unchanged for every corpus program. unshareNodes takes about 2 s on the three.js nodes fixture instead of 8 s, and compiling it to LLVM text takes 171 s instead of 178 s (median of five interleaved runs). --- packages/compiler/src/ir/unshare.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/ir/unshare.ts b/packages/compiler/src/ir/unshare.ts index 36dd6176f..df697baa9 100644 --- a/packages/compiler/src/ir/unshare.ts +++ b/packages/compiler/src/ir/unshare.ts @@ -1,5 +1,4 @@ import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js"; -import { reparseExpr, reparseStmt } from "./serialize.js"; import { everyExprChild, everyStmtChild, mapExprChildren, mapStmtChildren } from "./traverse.js"; /** Give every occurrence of an executable node its own object. @@ -42,14 +41,15 @@ export function unshareNodes(mod: IrModule): IrModule { } if (affected.size === 0) return mod; // Rebuilding a function copies every node that has children; a repeated - // childless node reads back as a fresh object at each of its sites there. + // childless node gets its own shallow copy at each of its sites there. + // Both kinds of copy keep sharing the type and location objects. const expr = (node: IrExpr): IrExpr => { const copy = mapExprChildren(node, expr, stmt); - return copy !== node || !repeated.has(node) ? copy : reparseExpr(node); + return copy !== node || !repeated.has(node) ? copy : { ...node }; }; const stmt = (node: IrStmt): IrStmt => { const copy = mapStmtChildren(node, expr, stmt); - return copy !== node || !repeated.has(node) ? copy : reparseStmt(node); + return copy !== node || !repeated.has(node) ? copy : { ...node }; }; return { ...mod, From e82686eab77beddf10e9a880665161bea60017f3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 06:29:28 -0500 Subject: [PATCH 118/120] Add getTestContext to node:test node:test changes, matching Node 24.22: - getTestContext() returns the context of the test, suite or hook whose code is running: undefined at module top level, the root's context in module-level hooks, the suite's in a suite body and in the suite's hooks, and the test's in its body. In the afterEach and t.after hooks of a subtest cancelled by its parent's timeout, it returns each hook's owner. - The context rides the AsyncLocalStorage snapshot under a reserved id, so it follows a test across its awaits, into the async functions, promise callbacks, timers and other callbacks it schedules, and into concurrent subtests, which each see their own. - The root's fullName is "", as its name is. - Optional chains over a test context, such as getTestContext()?.name, lower. AsyncLocalStorage enter and restore move into the always-linked async core so the test runner can use them. The snapshot holds the node's index in a table of live nodes, so a snapshot that outlives its node answers undefined. The get-test-context and get-test-context-cancelled fixtures compare the contexts seen at each point with Node, plain and with SCRIPTC_SAN=1. --- .../limitations/builtin-apis/test-runner.mdx | 1 + .../src/generated/node-v24-compatibility.json | 10 +- .../generated/node-v24-backlog.json | 20 +-- .../generated/node-v24-internal.json | 15 ++- internal/compatibility/static-support.json | 1 + .../ambient/scriptc-node-fallback.d.ts | 4 + .../compiler/src/backend/llvm/lib-test.ts | 4 + .../src/frontend/lowering/lower-test.ts | 46 ++++++- .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/runtime-features.ts | 6 +- packages/runtime/src/scr_async.c | 55 +++++++- packages/runtime/src/scr_async.h | 2 + packages/runtime/src/scr_async_dyn.c | 39 ------ packages/runtime/src/scr_test.c | 84 ++++++++++-- packages/runtime/src/scr_test.h | 4 + .../cases/get-test-context-cancelled.ts | 40 ++++++ .../node-test/cases/get-test-context.ts | 122 ++++++++++++++++++ 17 files changed, 385 insertions(+), 72 deletions(-) create mode 100644 tests/fixtures/node-test/cases/get-test-context-cancelled.ts create mode 100644 tests/fixtures/node-test/cases/get-test-context.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 70143a0c3..25eaebfab 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -6,3 +6,4 @@ - **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. - **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it. - **Test names:** a test without a name, or with an empty one, takes its function's name or ``, as in Node, when the function's name is known at compile time: a function expression, a function declaration, or a `const` bound to an arrow or anonymous function. A name computed at run time, with a function the program received as a parameter, is used as given, so an empty one stays empty where Node uses the function's name. `test(fn)` and `test("", fn)` with such a function throw `SC2020`. +- **Current context:** `getTestContext()` returns the context of the test, suite or hook whose code is running, as in Node: `undefined` at module top level, the root's context in module-level hooks, the suite's in a suite body and in the suite's hooks, and the test's in its body, in the code it awaits, and in the timers, immediates, `process.nextTick` and `queueMicrotask` callbacks it schedules. Node returns, for a suite, a `TestContext` that is a different object from the `SuiteContext` passed to the suite body and its `before` and `after` hooks; in a compiled program they are the same object, so comparing them with `===` gives `true`. Completion callbacks of callback-style I/O APIs, such as `fs.readFile`, get `undefined`, as AsyncLocalStorage stores do. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 0e6387ea9..e2e4cdd19 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -689,9 +689,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 64, + "partial": 65, "refused": 0, - "not-implemented": 53, + "not-implemented": 52, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -67344,9 +67344,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index a0d7bec4b..0ad30b783 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1162, - "verify-gap": 2166, + "audit-partial": 1163, + "verify-gap": 2165, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -68882,12 +68882,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.getTestContext", + "tests": [ + "tests/fixtures/node-test/cases/get-test-context.ts", + "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", + "tests/harness/node-test.test.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index cfbde8940..2f0565f84 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -746,9 +746,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 64, + "partial": 65, "refused": 0, - "not-implemented": 53, + "not-implemented": 52, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -91055,6 +91055,8 @@ "tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-members.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", + "tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", @@ -94803,8 +94805,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.getTestContext", + "tests": [ + "tests/fixtures/node-test/cases/get-test-context.ts", + "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 2e5eb1c4c..5807c1ae9 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -366,6 +366,7 @@ { "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 3e3f66a2a..0a0837389 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3550,6 +3550,9 @@ declare module "node:test" { [Symbol.dispose](): void; } const mock: MockTracker; + /* The context of the test, suite or hook running in the current async + * context; undefined outside one. */ + function getTestContext(): TestContext | SuiteContext | undefined; /* Declared for typechecking only — every use fences (no lowering). * `assert` is the module-level custom-assertion registry (v24's * `const { assert } = require('node:test')`), not TestContext.assert. */ @@ -3567,6 +3570,7 @@ declare module "node:test" { afterEach, mock, assert, + getTestContext, run, snapshot, TestContext, diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts index 4780311f8..0b097a1e4 100644 --- a/packages/compiler/src/backend/llvm/lib-test.ts +++ b/packages/compiler/src/backend/llvm/lib-test.ts @@ -49,6 +49,10 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal return finishRuntimeCall(host, e, "scr_test_ctx_passed", a); case "test.workerId": return finishRuntimeCall(host, e, "scr_test_worker_id", a); + case "test.hasContext": + return finishRuntimeCall(host, e, "scr_test_has_context", a); + case "test.context": + return finishRuntimeCall(host, e, "scr_test_context", a); case "test.ctxHook": host.toHeap(args[2]!); return finishRuntimeCall(host, e, "scr_test_ctx_hook", a); diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index c31e674af..d1cc2fcdd 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -25,6 +25,7 @@ import { DYN, F64, STRING, + TESTCTX_T, VOID, isUnitType, } from "../../ir/ir.js"; @@ -652,6 +653,8 @@ export function lowerNodeTestModuleCall( case "beforeEach": case "afterEach": return lowerHook(lowerer, expr, HOOK_WHICH[bi.member]!, `${bi.member}(...)`, loc); + case "getTestContext": + return lowerGetTestContext(lowerer, expr, loc); case "run": lowerer.noLowering( "test.run", @@ -670,13 +673,47 @@ export function lowerNodeTestModuleCall( lowerer.noLowering( `test.${bi.member}`, expr, - "test/it, describe/suite, before/after/beforeEach/afterEach, and the skip/todo/only twins are the lowered node:test members", + "test/it, describe/suite, before/after/beforeEach/afterEach, getTestContext, and the skip/todo/only twins are the lowered node:test members", ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : undefined, ); } return null; // unreachable — noLowering throws } +/** getTestContext(): the context of the test, suite or hook running in + * the current async context, or undefined outside one (module top level, + * and code reached only through timer, immediate, nextTick and + * queueMicrotask callbacks). */ +function lowerGetTestContext(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { + if (expr.arguments.length > 0) { + lowerer.noLowering( + `getTestContext with ${expr.arguments.length} arguments`, + expr, + "the supported form is getTestContext()", + ); + } + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const missing = declared ? lowerer.wrappedUndefined(declared, loc) : null; + if (!declared || !missing) { + lowerer.noLowering( + "getTestContext() of this type", + expr, + "getTestContext() lowers as TestContext | SuiteContext | undefined", + ); + } + return { + kind: "ternary", + cond: { kind: "libCall", fn: "test.hasContext", args: [], type: BOOL, loc }, + then: lowerer.coerceToExpected( + { kind: "libCall", fn: "test.context", args: [], type: TESTCTX_T, loc }, + declared, + ), + else_: missing, + type: declared, + loc, + }; +} + /** The callable-module form: `test(...)` where the callee identifier IS * the node:test module binding — a default import (`import test from * "node:test"`) or the CJS `const test = require('node:test')` twin @@ -902,7 +939,8 @@ export function lowerTestMethodCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (call.questionDotToken || access.questionDotToken) return null; + if (call.questionDotToken || (access.questionDotToken && !lowerer.chainHandled.has(access))) + return null; const loc = locOf(call); const member = access.name.text; // fn.mock.callCount() and the other mock context members. @@ -1182,7 +1220,9 @@ export function lowerTestCtxProperty( lowerer: Lowerer, expr: ts.PropertyAccessExpression, ): IrExpr | null { - if (expr.questionDotToken) return null; + // `getTestContext()?.name`: the optional-chain step re-enters here with + // the receiver narrowed. + if (expr.questionDotToken && !lowerer.chainHandled.has(expr)) return null; const mockCalls = lowerMockContextProperty(lowerer, expr); if (mockCalls) return mockCalls; if (lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind !== "testCtx") return null; diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 034d76917..df5bf0091 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1479,6 +1479,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // undefined. "test.ctxPassed": { argTypes: [TESTCTX_T], result: BOOL }, "test.workerId": { argTypes: [], result: F64 }, + // getTestContext(): whether a test, suite or hook is running in the + // current async context, and that context. + "test.hasContext": { argTypes: [], result: BOOL }, + "test.context": { argTypes: [], result: TESTCTX_T }, // t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after, // 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test. "test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 955b0edcc..6e71fe91c 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -25,7 +25,8 @@ export interface RuntimeFeatures { dynInvoke: boolean; dynAsync: boolean; /** The program enters AsyncLocalStorage stores (store.run, enterWith, - * a diagnostics_channel store binding): its timer, immediate, tick and + * a diagnostics_channel store binding) or reads the test runner's + * context (getTestContext): its timer, immediate, tick and * queueMicrotask scheduling goes through the runtime's *_als entry * points, which carry the active stores into each callback. */ asyncContext: boolean; @@ -121,6 +122,9 @@ const ASYNC_CONTEXT_LIB_FNS: ReadonlySet = new Set([ "als.enterWith", "dc.chanBindStore", "dc.chanRunStores", + // getTestContext() reads the test runner's entry in the same snapshot. + "test.hasContext", + "test.context", ]); const LEGACY_URL_LIB_FNS: ReadonlySet = new Set([ diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 583b1f6d0..88ad0f7bd 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -665,8 +665,9 @@ struct ScrFiber { /* The ScrAlsCtx layout and the API over it live in scr_runtime.h / * scr_async_dyn.c (gated — the size-class stance); this always-linked - * core keeps only what the fiber machinery itself touches: the active - * slot, the snapshot RC pair, and the switch/spawn/destroy wiring. */ + * core keeps only what the fiber machinery and the test runner touch: the + * active slot, the snapshot RC pair, enter/restore, and the + * switch/spawn/destroy wiring. */ static SCR_TL ScrAlsCtx *scr_als_main_slot = NULL; #ifdef SCR_WORKERS SCR_TL ScrAlsCtx **scr_als_active; @@ -694,6 +695,56 @@ static inline void *scr_fiber_region_capture(void) { return regions != NULL ? regions->capture() : NULL; } +/* Fresh snapshot storage. */ +ScrAlsCtx *scr_als_ctx_alloc(size_t len) { + ScrAlsCtx *c = malloc(sizeof *c + len * sizeof(ScrAlsEntry)); + if (!c) scr_oom(); + c->rc = 1; + c->len = len; + return c; +} + +/* Fresh snapshot with (id → value) replaced-or-appended (value borrowed, + * retained in), installed as the active context; the PREVIOUS snapshot + * returns (ownership moves out) for scr_als_restore. The test runner + * enters its own entry through this too, so it lives in the core. */ +ScrAlsCtx *scr_als_enter(double id, ScrDyn *value) { + ScrAlsCtx *prev = *SCR_ALS_SLOT(); + size_t n = prev ? prev->len : 0; + bool have = false; + for (size_t i = 0; i < n; i++) { + if (prev->entries[i].id == id) { have = true; break; } + } + ScrAlsCtx *next = scr_als_ctx_alloc(have ? n : n + 1); + size_t w = 0; + for (size_t i = 0; i < n; i++) { + if (prev->entries[i].id == id) continue; + next->entries[w].id = prev->entries[i].id; + next->entries[w].value = scr_dyn_retain(prev->entries[i].value); + w++; + } + next->entries[w].id = id; + next->entries[w].value = scr_dyn_retain(value); + *SCR_ALS_SLOT() = next; + return prev; /* ownership moves to the caller */ +} + +void scr_als_restore(ScrAlsCtx *prev) { + scr_als_ctx_release(*SCR_ALS_SLOT()); + *SCR_ALS_SLOT() = prev; /* ownership moves back in */ +} + +/* The value of `id` in the active snapshot (borrowed), or NULL. */ +ScrDyn *scr_als_peek(double id) { + const ScrAlsCtx *c = *SCR_ALS_SLOT(); + if (c) { + for (size_t i = 0; i < c->len; i++) { + if (c->entries[i].id == id) return c->entries[i].value; + } + } + return NULL; +} + /* enterWith on the main stack lasts until that code returns to the loop, * so the store does not reach callbacks scheduled without it: the loop * clears the main slot when the main module's body returns, and an diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 96421ca1c..d16fd8b9c 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -160,6 +160,8 @@ ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c); void scr_als_ctx_release(ScrAlsCtx *c); /* After an enterWith: on the main stack, queues the tick that ends it. */ void scr_als_entered_with(void); +ScrAlsCtx *scr_als_ctx_alloc(size_t len); +ScrDyn *scr_als_peek(double id); /* borrowed; NULL when unset */ double scr_als_new(void); ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 7611378fb..dbddc5b18 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -28,16 +28,6 @@ static void scr_ad_oom(void) { abort(); } -/* Fresh snapshot storage (the layout lives in scr_runtime.h; the RC pair - * in scr_async.c — spawn/destroy touch it). */ -static ScrAlsCtx *scr_als_ctx_alloc(size_t len) { - ScrAlsCtx *c = malloc(sizeof *c + len * sizeof(ScrAlsEntry)); - if (!c) scr_ad_oom(); - c->rc = 1; - c->len = len; - return c; -} - static SCR_TL double scr_als_counter = 0; /* Main-slot teardown (atexit, LIFO after scr_init's audit registration — @@ -68,30 +58,6 @@ ScrDyn *scr_als_get(double id) { return scr_dyn_retain(scr_dyn_undefined()); } -/* Fresh snapshot with (id → value) replaced-or-appended (value borrowed, - * retained in), installed as the active context; the PREVIOUS snapshot - * returns (ownership moves out) for scr_als_restore. */ -ScrAlsCtx *scr_als_enter(double id, ScrDyn *value) { - ScrAlsCtx *prev = *SCR_ALS_SLOT(); - size_t n = prev ? prev->len : 0; - bool have = false; - for (size_t i = 0; i < n; i++) { - if (prev->entries[i].id == id) { have = true; break; } - } - ScrAlsCtx *next = scr_als_ctx_alloc(have ? n : n + 1); - size_t w = 0; - for (size_t i = 0; i < n; i++) { - if (prev->entries[i].id == id) continue; - next->entries[w].id = prev->entries[i].id; - next->entries[w].value = scr_dyn_retain(prev->entries[i].value); - w++; - } - next->entries[w].id = id; - next->entries[w].value = scr_dyn_retain(value); - *SCR_ALS_SLOT() = next; - return prev; /* ownership moves to the caller */ -} - /* The exit() arm: the id REMOVED from the snapshot. */ ScrAlsCtx *scr_als_enter_absent(double id) { ScrAlsCtx *prev = *SCR_ALS_SLOT(); @@ -112,11 +78,6 @@ ScrAlsCtx *scr_als_enter_absent(double id) { return prev; } -void scr_als_restore(ScrAlsCtx *prev) { - scr_als_ctx_release(*SCR_ALS_SLOT()); - *SCR_ALS_SLOT() = prev; /* ownership moves back in */ -} - void scr_als_enter_with(double id, ScrDyn *value) { ScrAlsCtx *prev = scr_als_enter(id, value); scr_als_ctx_release(prev); /* no restore point — Node's enterWith */ diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 43affdd8a..7c5c243d8 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -126,6 +126,7 @@ struct ScrTestCtx { bool passed; /* t.passed: set when the body passes, as Node's pass() does */ bool listed; /* a suite listed in the failing section (a failing root before) */ double start_ms; /* when the test started running; NaN before */ + size_t serial; /* index in scr_test_nodes, getTestContext()'s handle */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -168,6 +169,17 @@ static SCR_TL size_t scr_test_nfibers = 0, scr_test_fibers_cap = 0; /* The module's `mock` tracker: the restore closure of each mock it made. */ static SCR_TL ScrTestHooks scr_test_mocks = {0}; +/* getTestContext(): the test, suite or hook owner whose code is running + * rides the AsyncLocalStorage snapshot under a reserved store id + * (AsyncLocalStorage ids are positive), so it follows a body across its + * awaits and into the async functions it calls, as Node's async context + * does. The entry holds the node's serial, an index in scr_test_nodes + * (borrowed pointers, cleared when a node is freed), so a snapshot that + * outlives its node answers undefined instead of a freed node. */ +#define SCR_TEST_SCOPE_ID (-1.0) +static SCR_TL ScrTestCtx **scr_test_nodes = NULL; +static SCR_TL size_t scr_test_nnodes = 0, scr_test_nodes_cap = 0, scr_test_nodes_live = 0; + #ifdef SCR_RC_AUDIT static SCR_TL long scr_testctx_live = 0; long scr_testctx_live_count(void) { return scr_testctx_live; } @@ -204,6 +216,12 @@ void scr_testctx_release(ScrTestCtx *t) { free(t->children); for (int k = 0; k < 4; k++) scr_test_hooks_free(&t->hooks[k]); scr_test_hooks_free(&t->mocks); + scr_test_nodes[t->serial] = NULL; + if (--scr_test_nodes_live == 0) { + free(scr_test_nodes); + scr_test_nodes = NULL; + scr_test_nnodes = scr_test_nodes_cap = 0; + } #ifdef SCR_RC_AUDIT scr_testctx_live--; #endif @@ -232,12 +250,43 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode, t->timeout = INFINITY; t->concurrency = 1; t->start_ms = NAN; + if (scr_test_nnodes == scr_test_nodes_cap) { + scr_test_nodes_cap = scr_test_nodes_cap ? scr_test_nodes_cap * 2 : 16; + scr_test_nodes = realloc(scr_test_nodes, scr_test_nodes_cap * sizeof *scr_test_nodes); + if (!scr_test_nodes) scr_test_oom(); + } + t->serial = scr_test_nnodes; + scr_test_nodes[scr_test_nnodes++] = t; + scr_test_nodes_live++; #ifdef SCR_RC_AUDIT scr_testctx_live++; #endif return t; } +/* Makes `t` the node getTestContext() answers, until the matching + * scr_test_scope_leave. Returns the previous snapshot. */ +static ScrAlsCtx *scr_test_scope_enter(ScrTestCtx *t) { + ScrDyn *serial = scr_dyn_new_num((double)t->serial); + ScrAlsCtx *prev = scr_als_enter(SCR_TEST_SCOPE_ID, serial); + scr_dyn_release(serial); + return prev; +} + +static void scr_test_scope_leave(ScrAlsCtx *prev /*moves*/) { scr_als_restore(prev); } + +/* The node getTestContext() answers (borrowed), or NULL. */ +static ScrTestCtx *scr_test_scope_node(void) { + ScrDyn *serial = scr_als_peek(SCR_TEST_SCOPE_ID); + if (!serial || serial->kind != SCR_DYN_NUM) return NULL; + size_t i = (size_t)serial->v.num; + return i < scr_test_nnodes ? scr_test_nodes[i] : NULL; +} + +bool scr_test_has_context(void) { return scr_test_scope_node() != NULL; } + +ScrTestCtx *scr_test_context(void) { return scr_testctx_retain(scr_test_scope_node()); } + static void scr_test_child_add(ScrTestCtx *parent, ScrTestCtx *child /*moves*/) { if (parent->nchildren == parent->ccap) { parent->ccap = parent->ccap ? parent->ccap * 2 : 4; @@ -393,9 +442,12 @@ static void scr_test_await_timeout(ScrPromise *p, ScrTestCtx *t) { /* Call one stored closure by its lowering-pinned shape; async shapes are * awaited (the runner is a fiber). `ctx` is passed +1 when the body takes - * the TestContext. Returns with the fiber's exception cell holding any - * failure. `timed` is the test whose { timeout } bounds an async body. */ -static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx *timed) { + * the TestContext, and `scope` is what getTestContext() answers while it + * runs. Returns with the fiber's exception cell holding any failure. + * `timed` is the test whose { timeout } bounds an async body. */ +static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx *scope, + ScrTestCtx *timed) { + ScrAlsCtx *outer = scr_test_scope_enter(scope); bool takes_ctx = (flags & SCR_TEST_F_CTX) != 0; if (flags & SCR_TEST_F_ASYNC) { ScrPromise *p = @@ -412,6 +464,7 @@ static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx } else { ((void (*)(ScrClosure *))cb->fn)(cb); } + scr_test_scope_leave(outer); } /* A mock tracker's restoreAll: each mock's restore, in creation order. @@ -434,7 +487,7 @@ static ScrTestHook *scr_test_run_hooks_of(ScrTestCtx *owner, int which, ScrTestC if (scr_exc_pending()) return NULL; /* an earlier hook in the chain threw */ for (size_t i = 0; i < owner->hooks[which].n; i++) { ScrTestHook *h = &owner->hooks[which].v[i]; - scr_test_call(h->cb, h->flags, ctx, NULL); + scr_test_call(h->cb, h->flags, ctx, owner, NULL); if (scr_exc_pending()) return h; /* the hook that threw */ } return NULL; @@ -608,6 +661,7 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) { typedef struct { ScrTestCtx *test; /* owned ref */ ScrTestHooks each; /* the afterEach chain, innermost first; owned closures */ + ScrTestCtx **owners; /* owned refs: each afterEach hook's suite or test */ } ScrTestCancelled; static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { @@ -616,11 +670,13 @@ static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { ScrTestCtx *c = job->test; scr_await_hop(); /* the spawn runs the fiber at once; start at the next turn */ for (size_t i = 0; i < job->each.n && !scr_exc_pending(); i++) - scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, NULL); + scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, job->owners[i], NULL); if (scr_exc_pending()) scr_exc_clear(); scr_test_run_hooks_of(c, SCR_HOOK_AFTER, c); if (scr_exc_pending()) scr_exc_clear(); for (int k = 0; k < 4; k++) scr_test_hooks_free(&c->hooks[k]); + for (size_t i = 0; i < job->each.n; i++) scr_testctx_release(job->owners[i]); + free(job->owners); scr_test_hooks_free(&job->each); scr_testctx_release(c); free(job); @@ -636,8 +692,10 @@ static void scr_test_cancelled_hooks(ScrTestCtx *c) { if (job->each.n == job->each.cap) { job->each.cap = job->each.cap ? job->each.cap * 2 : 4; job->each.v = realloc(job->each.v, job->each.cap * sizeof *job->each.v); - if (!job->each.v) scr_test_oom(); + job->owners = realloc(job->owners, job->each.cap * sizeof *job->owners); + if (!job->each.v || !job->owners) scr_test_oom(); } + job->owners[job->each.n] = scr_testctx_retain(p); job->each.v[job->each.n].cb = scr_closure_retain(h->v[i].cb); job->each.v[job->each.n].flags = h->v[i].flags; job->each.v[job->each.n].at = NULL; @@ -775,7 +833,7 @@ static void scr_test_run_node(ScrTestCtx *t) { t->err = scr_exc_take(); failed = true; } else if (t->cb) { - scr_test_call(t->cb, t->flags, t, t); + scr_test_call(t->cb, t->flags, t, t, t); if (t->state != SCR_TS_PENDING) { /* Its parent timed out and cancelled it while the body ran: the * result is reported, its cleanup ran at the cancellation @@ -1703,11 +1761,13 @@ void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrow if (cb && (int)mode != SCR_TEST_SKIP) { ScrTestCtx *prev = scr_test_parent_get(); scr_test_parent_set(t); + ScrAlsCtx *outer = scr_test_scope_enter(t); /* flag 2: the body takes its SuiteContext (+1, as test bodies do). */ if ((int)flags & SCR_TEST_F_CTX) ((void (*)(ScrClosure *, ScrTestCtx *))cb->fn)(cb, scr_testctx_retain(t)); else ((void (*)(ScrClosure *))cb->fn)(cb); + scr_test_scope_leave(outer); scr_test_parent_set(prev); } scr_closure_release(cb); @@ -1720,6 +1780,7 @@ void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrow * top-level tests and suites (scr_test_fail_unrun). */ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags) { bool takes_ctx = (flags & SCR_TEST_F_CTX) != 0; + ScrAlsCtx *outer = scr_test_scope_enter(root); if (flags & SCR_TEST_F_ASYNC) { ScrPromise *p = takes_ctx @@ -1736,6 +1797,7 @@ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags * unhandled rejection. */ scr_promise_mark_handled(p); scr_test_root_pending[scr_test_root_npending++] = p; + scr_test_scope_leave(outer); return; } scr_promise_release(p); @@ -1744,6 +1806,7 @@ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags } else { ((void (*)(ScrClosure *))cb->fn)(cb); } + scr_test_scope_leave(outer); scr_test_root_before_failed(root); } @@ -1822,7 +1885,7 @@ void scr_test_ctx_todo(ScrTestCtx *t, ScrStr *msg /*borrowed*/) { * each descendant subtest, and after runs once the test finishes. */ void scr_test_ctx_hook(ScrTestCtx *t, double which, ScrClosure *cb /*moves*/, double flags) { if ((int)which == SCR_HOOK_BEFORE) { - scr_test_call(cb, (int)flags, t, NULL); + scr_test_call(cb, (int)flags, t, t, NULL); scr_closure_release(cb); if (scr_exc_pending() && !t->before_err) t->before_err = scr_exc_take(); else if (scr_exc_pending()) scr_exc_clear(); @@ -2414,8 +2477,6 @@ ScrStr *scr_test_ctx_name(ScrTestCtx *t) { return t->name ? scr_str_retain(t->name) : scr_str_new("", 0); } -/* t.fullName: the names from the outermost suite or test down to this one, - * joined with " > ". */ bool scr_test_ctx_passed(ScrTestCtx *t) { return t->passed; } /* t.workerId: Number(process.env.NODE_TEST_WORKER_ID) || undefined, with 0 @@ -2429,7 +2490,10 @@ double scr_test_worker_id(void) { return isnan(n) ? 0 : n; } +/* t.fullName: the names from the outermost suite or test down to this one, + * joined with " > "; the root's own name for the root. */ ScrStr *scr_test_ctx_full_name(ScrTestCtx *t) { + if (!t->parent) return scr_test_ctx_name(t); const ScrTestCtx *chain[64]; int n = 0; for (const ScrTestCtx *p = t; p && p->parent && n < 64; p = p->parent) chain[n++] = p; diff --git a/packages/runtime/src/scr_test.h b/packages/runtime/src/scr_test.h index b29fca362..faa46406f 100644 --- a/packages/runtime/src/scr_test.h +++ b/packages/runtime/src/scr_test.h @@ -52,6 +52,10 @@ ScrStr *scr_test_ctx_full_name(ScrTestCtx *t); bool scr_test_ctx_passed(ScrTestCtx *t); double scr_test_worker_id(void); ScrStr *scr_test_ctx_name(ScrTestCtx *t); /* +1 */ +/* getTestContext(): whether a test, suite or hook is running in the + * current async context, and its context (+1; call when has_context). */ +bool scr_test_has_context(void); +ScrTestCtx *scr_test_context(void); /* Mock trackers: the module's `mock` and each test's t.mock. track keeps * the closure that restores one mock; restore_all calls them in creation * order; reset calls them and forgets them. A test's t.mock is reset when diff --git a/tests/fixtures/node-test/cases/get-test-context-cancelled.ts b/tests/fixtures/node-test/cases/get-test-context-cancelled.ts new file mode 100644 index 000000000..3317a2914 --- /dev/null +++ b/tests/fixtures/node-test/cases/get-test-context-cancelled.ts @@ -0,0 +1,40 @@ +// @exit: 1 +// getTestContext() in the cleanup hooks of a subtest cancelled by its +// parent's timeout answers each hook's owner, as Node does: the suite for +// its afterEach, the parent test for its t.afterEach, and the cancelled +// subtest for its own t.after. +import { afterEach, describe, getTestContext, it, test } from "node:test"; +import * as assert from "node:assert"; + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); +const seen: string[] = []; + +function record(where: string): void { + seen.push(`${where}: ${getTestContext()?.fullName ?? "undefined"}`); +} + +describe("suite", () => { + afterEach((c) => record(`suite afterEach for ${c.name}`)); + + it("parent", { timeout: 30 }, async (t) => { + t.afterEach((c) => record(`parent afterEach for ${c.name}`)); + await t.test("sub", async (u) => { + u.after(() => record("sub after")); + record("sub body"); + await sleep(200); + record("sub settles"); + }); + }); +}); + +test("next", async () => { + await sleep(300); + assert.deepStrictEqual(seen, [ + "sub body: suite > parent > sub", + "suite afterEach for parent: suite", + "parent afterEach for sub: suite > parent", + "suite afterEach for sub: suite", + "sub after: suite > parent > sub", + "sub settles: suite > parent > sub", + ]); +}); diff --git a/tests/fixtures/node-test/cases/get-test-context.ts b/tests/fixtures/node-test/cases/get-test-context.ts new file mode 100644 index 000000000..2b61841cc --- /dev/null +++ b/tests/fixtures/node-test/cases/get-test-context.ts @@ -0,0 +1,122 @@ +// getTestContext() answers the context of the test, suite or hook whose +// code is running: undefined at module top level, "" in module-level +// hooks, the suite in its body, the hook's owner in a hook (a suite's +// beforeEach answers the suite), and the test in its body, across awaits, +// in async functions, promise callbacks and timers it starts, and in +// subtests. A timer keeps its test's context after the test ends. +// Concurrent subtests each answer their own context across their awaits. +import { after, afterEach, before, beforeEach, describe, getTestContext, it, test } from "node:test"; +import * as assert from "node:assert"; + +const seen: string[] = []; +let late = ""; + +function record(where: string): void { + const c = getTestContext(); + seen.push(`${where}: ${c === undefined ? "undefined" : c.fullName}`); +} + +async function helper(where: string): Promise { + await Promise.resolve(); + record(where); +} + +const tick = (): Promise => new Promise((resolve) => setTimeout(resolve, 2)); + +record("top level"); + +before(() => record("root before")); +beforeEach(() => record("root beforeEach")); + +describe("suite", () => { + record("suite body"); + before(() => record("suite before")); + beforeEach(() => record("suite beforeEach")); + afterEach(() => record("suite afterEach")); + after(() => record("suite after")); + + it("first", async () => { + record("first body"); + await tick(); + record("first after await"); + await helper("first helper"); + await Promise.resolve().then(() => record("first then")); + await new Promise((resolve) => + setTimeout(() => { + record("first timer"); + resolve(); + }, 1), + ); + }); + + describe("inner", () => { + record("inner body"); + it("second", () => record("second body")); + }); +}); + +test("subtests", async (t) => { + t.beforeEach(() => record("subtests beforeEach")); + await t.test("sub", async () => { + record("sub body"); + await tick(); + record("sub after await"); + }); + record("subtests after sub"); + const c = getTestContext(); + assert.strictEqual(c === t, true); + setTimeout(() => { + late = getTestContext()?.name ?? "undefined"; + }, 0); +}); + +test("concurrent", { concurrency: true }, async (t) => { + const order: string[] = []; + const body = async (): Promise => { + order.push(getTestContext()?.name ?? "undefined"); + await tick(); + order.push(getTestContext()?.name ?? "undefined"); + await tick(); + order.push(getTestContext()?.name ?? "undefined"); + }; + t.after(() => { + seen.push(`concurrent: ${order.join(" ")}`); + }); + t.test("c1", body); + t.test("c2", body); +}); + +test("check", () => { + assert.strictEqual(late, "subtests"); + assert.deepStrictEqual(seen, [ + "top level: undefined", + "root before: ", + "suite body: suite", + "inner body: suite > inner", + "suite before: suite", + "root beforeEach: ", + "suite beforeEach: suite", + "first body: suite > first", + "first after await: suite > first", + "first helper: suite > first", + "first then: suite > first", + "first timer: suite > first", + "suite afterEach: suite", + "root beforeEach: ", + "suite beforeEach: suite", + "second body: suite > inner > second", + "suite afterEach: suite", + "suite after: suite", + "root beforeEach: ", + "root beforeEach: ", + "subtests beforeEach: subtests", + "sub body: subtests > sub", + "sub after await: subtests > sub", + "subtests after sub: subtests", + "root beforeEach: ", + "root beforeEach: ", + "root beforeEach: ", + "concurrent: c1 c2 c1 c2 c1 c2", + "root beforeEach: ", + ]); +}); From 3c749a2895f1d86d8e4215a7a5a50e39a589aa29 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 06:33:33 -0500 Subject: [PATCH 119/120] Read AST lists through their nodes The frontend's node lists become node lists, { pos, end, nodes }, the shape of tsc-ts's NodeList, so that the frontend can later read tsc-ts's own nodes. AstNode's list getters return an AstNodeList, ts.NodeArray is that class, and the frontend reads a list's elements through nodes. The lists already carried their positions on the wire. The class is the type, not a structural interface, because native code represents a class and a record differently. A decoded file keeps each list's nodes array, as before, and each read of a list field returns a new AstNodeList over it, so the file holds no more objects than before. nodes keeps its identity; a list object is not the same object on each read. The change is mechanical: a type-aware rewrite of each list field read, plus hand edits where lists are read through casts, generic accessors or test mocks. The generated getters and types follow from generate-ts7-ast-schema.mjs. AstFile and Ts7Session drop their unused listMetadata hook. --- packages/compiler/src/frontend/cjs-syntax.ts | 62 +- .../compiler/src/frontend/contract-types.ts | 8 +- packages/compiler/src/frontend/fork-target.ts | 20 +- .../src/frontend/function-completion.ts | 12 +- .../src/frontend/inferred-js-diagnostics.ts | 11 +- .../compiler/src/frontend/lib-contract.ts | 48 +- packages/compiler/src/frontend/lib-exports.ts | 4 +- .../src/frontend/lowering/assert-instances.ts | 4 +- .../src/frontend/lowering/builtin-calls.ts | 2 +- .../frontend/lowering/builtins/arguments.ts | 2 +- .../lowering/builtins/async-context.ts | 103 +-- .../src/frontend/lowering/builtins/atomics.ts | 12 +- .../src/frontend/lowering/builtins/blob.ts | 6 +- .../lowering/builtins/callable-aliases.ts | 2 +- .../lowering/builtins/child-process-async.ts | 14 +- .../builtins/child-process-methods.ts | 96 +-- .../lowering/builtins/child-process-sync.ts | 20 +- .../lowering/builtins/child-process.ts | 96 +-- .../frontend/lowering/builtins/compression.ts | 73 +- .../frontend/lowering/builtins/constants.ts | 2 +- .../src/frontend/lowering/builtins/crypto.ts | 68 +- .../src/frontend/lowering/builtins/dates.ts | 28 +- .../lowering/builtins/disposable-stack.ts | 6 +- .../frontend/lowering/builtins/environment.ts | 6 +- .../src/frontend/lowering/builtins/errors.ts | 4 +- .../lowering/builtins/filesystem-calls.ts | 233 +++--- .../frontend/lowering/builtins/filesystem.ts | 154 ++-- .../src/frontend/lowering/builtins/fs-glob.ts | 6 +- .../frontend/lowering/builtins/fs-streams.ts | 4 +- .../src/frontend/lowering/builtins/ipc.ts | 14 +- .../src/frontend/lowering/builtins/json.ts | 32 +- .../frontend/lowering/builtins/key-object.ts | 4 +- .../lowering/builtins/module-bindings.ts | 12 +- .../lowering/builtins/module-calls.ts | 92 +-- .../lowering/builtins/module-resolution.ts | 32 +- .../frontend/lowering/builtins/net-address.ts | 8 +- .../src/frontend/lowering/builtins/numbers.ts | 18 +- .../lowering/builtins/operating-system.ts | 24 +- .../frontend/lowering/builtins/performance.ts | 15 +- .../src/frontend/lowering/builtins/process.ts | 186 ++--- .../frontend/lowering/builtins/promises.ts | 50 +- .../frontend/lowering/builtins/querystring.ts | 28 +- .../frontend/lowering/builtins/readline.ts | 39 +- .../frontend/lowering/builtins/reflection.ts | 30 +- .../src/frontend/lowering/builtins/strings.ts | 25 +- .../frontend/lowering/builtins/text-codecs.ts | 63 +- .../src/frontend/lowering/builtins/timers.ts | 26 +- .../src/frontend/lowering/builtins/url.ts | 28 +- .../frontend/lowering/builtins/util-types.ts | 12 +- .../src/frontend/lowering/builtins/workers.ts | 20 +- .../src/frontend/lowering/call-arguments.ts | 8 +- .../src/frontend/lowering/call-signatures.ts | 33 +- .../frontend/lowering/cjs-exports-object.ts | 8 +- .../frontend/lowering/cjs-inline-require.ts | 6 +- .../src/frontend/lowering/cjs-tsc-interop.ts | 12 +- .../frontend/lowering/class-descriptors.ts | 14 +- .../lowering/class-instance-constructor.ts | 2 +- .../lowering/class-method-mutations.ts | 34 +- .../frontend/lowering/class-method-values.ts | 2 +- .../lowering/class-protocol-dispatch.ts | 13 +- .../src/frontend/lowering/class-protocols.ts | 20 +- .../src/frontend/lowering/class-prototypes.ts | 26 +- .../lowering/class-runtime-statics.ts | 2 +- .../frontend/lowering/class-static-data.ts | 2 +- .../lowering/coercions/structural-plans.ts | 2 +- .../frontend/lowering/collection-ingestion.ts | 2 +- .../lowering/construction-escapes.test.ts | 6 +- .../frontend/lowering/construction-escapes.ts | 9 +- .../containers/array-callback-call.ts | 2 +- .../lowering/containers/array-callbacks.ts | 36 +- .../lowering/containers/array-concat.ts | 2 +- .../lowering/containers/array-construction.ts | 20 +- .../lowering/containers/array-from-async.ts | 2 +- .../lowering/containers/array-methods.ts | 180 +++-- .../src/frontend/lowering/containers/bytes.ts | 147 ++-- .../lowering/containers/callback-arguments.ts | 8 +- .../containers/collection-grouping.ts | 17 +- .../lowering/containers/collection-methods.ts | 42 +- .../lowering/containers/exec-result-uses.ts | 2 +- .../frontend/lowering/containers/for-of.ts | 36 +- .../lowering/containers/indexed-objects.ts | 29 +- .../lowering/containers/iterator-helpers.ts | 81 +- .../lowering/containers/match-row-uses.ts | 29 +- .../lowering/containers/string-and-regexp.ts | 51 +- .../lowering/containers/string-replacement.ts | 2 +- .../lowering/deferred-module-initializers.ts | 33 +- .../src/frontend/lowering/devirtualize.ts | 4 +- .../src/frontend/lowering/divergences.ts | 52 +- .../lowering/expressions/native-proxy.ts | 6 +- .../lowering/expressions/object-literals.ts | 114 +-- .../lowering/expressions/optional-chains.ts | 4 +- .../src/frontend/lowering/function-returns.ts | 10 +- .../frontend/lowering/generic-functions.ts | 95 +-- .../src/frontend/lowering/import-meta.ts | 2 +- .../src/frontend/lowering/intern-tables.ts | 19 +- .../src/frontend/lowering/js-class-fields.ts | 14 +- .../frontend/lowering/js-dynamic-objects.ts | 34 +- .../src/frontend/lowering/lifetime-regions.ts | 17 +- .../src/frontend/lowering/lower-assert.ts | 114 +-- .../lowering/lower-async-iteration.ts | 10 +- .../src/frontend/lowering/lower-calls.ts | 689 +++++++++--------- .../src/frontend/lowering/lower-classes.ts | 445 +++++------ .../src/frontend/lowering/lower-comptime.ts | 6 +- .../src/frontend/lowering/lower-console.ts | 24 +- .../src/frontend/lowering/lower-dgram.ts | 10 +- .../src/frontend/lowering/lower-enums.ts | 6 +- .../frontend/lowering/lower-event-emitter.ts | 56 +- .../frontend/lowering/lower-event-target.ts | 4 +- .../src/frontend/lowering/lower-expando.ts | 2 +- .../src/frontend/lowering/lower-exprs.ts | 136 ++-- .../src/frontend/lowering/lower-functions.ts | 20 +- .../src/frontend/lowering/lower-generators.ts | 6 +- .../src/frontend/lowering/lower-inspect.ts | 58 +- .../src/frontend/lowering/lower-mixins.ts | 30 +- .../src/frontend/lowering/lower-mock.ts | 22 +- .../src/frontend/lowering/lower-modules.ts | 81 +- .../src/frontend/lowering/lower-namespaces.ts | 6 +- .../frontend/lowering/lower-node-module.ts | 4 +- .../src/frontend/lowering/lower-server.ts | 116 +-- .../src/frontend/lowering/lower-stdlib.ts | 139 ++-- .../src/frontend/lowering/lower-stmts.ts | 170 ++--- .../src/frontend/lowering/lower-stream.ts | 56 +- .../src/frontend/lowering/lower-test.ts | 24 +- .../src/frontend/lowering/lower-timers.ts | 66 +- .../src/frontend/lowering/lower-url.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 150 ++-- .../src/frontend/lowering/nan-coded-reads.ts | 36 +- .../frontend/lowering/narrowing-stability.ts | 34 +- .../src/frontend/lowering/native-ffi.ts | 10 +- .../frontend/lowering/object-factory-new.ts | 8 +- .../lowering/require-initialization.ts | 6 +- .../lowering/runtime-optional-bounds.ts | 8 +- .../lowering/runtime-optional-elements.ts | 22 +- .../lowering/runtime-optional-guards.test.ts | 2 +- .../lowering/runtime-optional-guards.ts | 18 +- .../lowering/runtime-optional-ownership.ts | 14 +- .../lowering/runtime-optional-records.ts | 42 +- .../src/frontend/lowering/split-consumers.ts | 4 +- .../frontend/lowering/string-index-bounds.ts | 4 +- .../src/frontend/lowering/surfaces.ts | 4 +- .../src/frontend/lowering/symbol-fields.ts | 27 +- .../src/frontend/lowering/symbol-methods.ts | 2 +- .../src/frontend/lowering/thread-slots.ts | 13 +- .../src/frontend/lowering/var-hoisting.ts | 8 +- .../src/frontend/module-source-candidates.ts | 2 +- .../compiler/src/frontend/module-syntax.ts | 18 +- .../src/frontend/npm-static-bundled-cjs.ts | 32 +- .../frontend/npm-static-declaration-syntax.ts | 154 ++-- .../compiler/src/frontend/npm-static-prune.ts | 18 +- .../src/frontend/npm-static-rewrite-syntax.ts | 82 +-- .../src/frontend/npm-static-subtyping.ts | 6 +- .../compiler/src/frontend/pipeline.test.ts | 2 +- packages/compiler/src/frontend/pipeline.ts | 4 +- .../compiler/src/frontend/program.test.ts | 4 +- packages/compiler/src/frontend/program.ts | 231 +++--- .../compiler/src/frontend/services.test.ts | 2 +- .../compiler/src/frontend/tier-analysis.ts | 4 +- .../src/frontend/ts7/ast-node.test.ts | 30 +- .../compiler/src/frontend/ts7/ast-node.ts | 81 +- .../src/frontend/ts7/ast-types.generated.ts | 2 +- .../compiler/src/frontend/ts7/ast-types.ts | 6 +- packages/compiler/src/frontend/ts7/ast.ts | 4 +- .../compiler/src/frontend/ts7/census-check.ts | 2 +- .../compiler/src/frontend/ts7/checker.test.ts | 4 +- packages/compiler/src/frontend/ts7/checker.ts | 2 +- .../src/frontend/ts7/semantic-checker.test.ts | 8 +- .../compiler/src/frontend/ts7/session.test.ts | 2 +- packages/compiler/src/frontend/ts7/session.ts | 3 +- .../src/frontend/ts7/source-parser.test.ts | 32 +- packages/compiler/src/frontend/type-mapper.ts | 27 +- .../src/frontend/type-only-imports.ts | 8 +- .../compiler/src/frontend/worker-target.ts | 10 +- packages/compiler/src/ir/traverse.test.ts | 38 +- packages/compiler/src/wasm/discover.ts | 10 +- packages/compiler/test/ts7/facade.test.ts | 46 +- .../compiler/test/ts7/program-adapter.test.ts | 2 +- packages/compiler/test/ts7/rpc-api.test.ts | 42 +- scripts/generate-ts7-ast-schema.mjs | 6 +- scripts/generate-ts7-ast-views.mjs | 2 + .../self-hosting/frontend-services-cases.ts | 4 +- tests/fixtures/self-hosting/ts7-ast-views.ts | 22 +- .../self-hosting/ts7-checker-cases.ts | 12 +- .../self-hosting/ts7-checker-snapshots.ts | 2 +- .../fixtures/self-hosting/ts7-client-cases.ts | 24 +- .../self-hosting/ts7-program-host-cases.ts | 4 +- .../self-hosting/ts7-semantic-cases.ts | 16 +- .../self-hosting/ts7-source-parser-cases.ts | 4 +- tests/harness/stdlib-surface.test.ts | 4 +- 188 files changed, 3624 insertions(+), 3263 deletions(-) diff --git a/packages/compiler/src/frontend/cjs-syntax.ts b/packages/compiler/src/frontend/cjs-syntax.ts index 8edd52b09..41191432a 100644 --- a/packages/compiler/src/frontend/cjs-syntax.ts +++ b/packages/compiler/src/frontend/cjs-syntax.ts @@ -56,8 +56,8 @@ function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string { export function bareRequireSpecOf(e: ts.Node): string | null { if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return null; if (!ts.isIdentifier(e.expression) || e.expression.text !== "require") return null; - if (e.arguments.length !== 1) return null; - const arg = e.arguments[0]!; + if (e.arguments.nodes.length !== 1) return null; + const arg = e.arguments.nodes[0]!; return ts.isStringLiteral(arg) ? arg.text : null; } @@ -135,7 +135,7 @@ function scanTableLiteral( reexports: string[], ): void { const text = sf.text; - for (const prop of obj.properties) { + for (const prop of obj.properties.nodes) { if (ts.isGetAccessor(prop)) { // `get name() {...}` — merve's explicit early termination... unless // the accessor's NAME is not a plain identifier, where the lexer @@ -253,8 +253,8 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set ) { return; } - if (call.arguments.length !== 3) return; - const [recv, nameArg, desc] = call.arguments as unknown as [ + if (call.arguments.nodes.length !== 3) return; + const [recv, nameArg, desc] = call.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -263,7 +263,7 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set if (!ts.isStringLiteral(nameArg)) return; if (!ts.isObjectLiteralExpression(desc)) return; let i = 0; - const props = desc.properties; + const props = desc.properties.nodes; const first = props[0]; if ( first !== undefined && @@ -287,8 +287,8 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set // and the getter must be the descriptor's LAST property. const body = getterBodyOf(p); if (body === undefined || i !== props.length - 1) return; - if (body.statements.length !== 1) return; - const ret = body.statements[0]!; + if (body.statements.nodes.length !== 1) return; + const ret = body.statements.nodes[0]!; if (!ts.isReturnStatement(ret) || ret.expression === undefined) return; const r = ret.expression; const returnsIdentish = @@ -313,7 +313,7 @@ function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined { p.name.text === "get" && p.asteriskToken === undefined && (ts.getModifiers(p) ?? []).length === 0 && - p.parameters.length === 0 + p.parameters.nodes.length === 0 ) { return p.body; } @@ -322,7 +322,7 @@ function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined { ts.isIdentifier(p.name) && p.name.text === "get" && ts.isFunctionExpression(p.initializer) && - p.initializer.parameters.length === 0 && + p.initializer.parameters.nodes.length === 0 && p.initializer.asteriskToken === undefined && (ts.getModifiers(p.initializer) ?? []).length === 0 ) { @@ -349,7 +349,7 @@ function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string | if (nameNode === undefined) return null; if (nameNode.text !== "__export" && nameNode.text !== "__exportStar") return null; if (!sourceSpellsIdentifier(nameNode, sf)) return null; - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (arg0 === undefined) return null; if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null; return leadingRequireOf(arg0, sf)?.spec ?? null; @@ -369,7 +369,7 @@ function byteAdjacentParen(sf: ts.SourceFile, nameEnd: number, argStart: number) * probed: a member-qualified helper or one space after its paren misses). * Answers [ID, spec]. */ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, string] | null { - const decl = stmt.declarationList.declarations[0]; + const decl = stmt.declarationList.declarations.nodes[0]; if (decl === undefined || !ts.isIdentifier(decl.name) || !sourceSpellsIdentifier(decl.name, sf)) return null; const init = decl.initializer; @@ -381,9 +381,9 @@ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, s init.questionDotToken === undefined && ts.isIdentifier(init.expression) && init.expression.text === "_interopRequireWildcard" && - init.arguments.length >= 1 + init.arguments.nodes.length >= 1 ) { - const arg0 = init.arguments[0]!; + const arg0 = init.arguments.nodes[0]!; if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null; const spec = bareRequireSpecOf(arg0); if (spec !== null) return [decl.name.text, spec]; @@ -434,8 +434,8 @@ function isHasOwnCall(e: ts.Expression, key: string): boolean { ) cur = cur.expression; if (!ts.isIdentifier(cur) || cur.text !== "Object") return false; - if (e.arguments.length !== 2) return false; - const [recv, k] = e.arguments as unknown as [ts.Expression, ts.Expression]; + if (e.arguments.nodes.length !== 2) return false; + const [recv, k] = e.arguments.nodes as unknown as [ts.Expression, ts.Expression]; return ts.isIdentifier(recv) && ts.isIdentifier(k) && k.text === key; } @@ -448,9 +448,9 @@ function isDirectHasOwnCall(e: ts.Expression, key: string): boolean { ts.isIdentifier(e.expression.name) && e.expression.name.text === "hasOwnProperty" && ts.isIdentifier(e.expression.expression) && - e.arguments.length === 1 && - ts.isIdentifier(e.arguments[0]!) && - (e.arguments[0] as ts.Identifier).text === key + e.arguments.nodes.length === 1 && + ts.isIdentifier(e.arguments.nodes[0]!) && + (e.arguments.nodes[0] as ts.Identifier).text === key ); } @@ -477,16 +477,16 @@ function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean ) { return false; } - if (e.arguments.length !== 3) return false; - const [recv, nameArg, desc] = e.arguments as unknown as [ + if (e.arguments.nodes.length !== 3) return false; + const [recv, nameArg, desc] = e.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, ]; if (!isExportsTarget(recv)) return false; if (!ts.isIdentifier(nameArg) || nameArg.text !== key) return false; - if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 2) return false; - const [en, get] = desc.properties as unknown as [ + if (!ts.isObjectLiteralExpression(desc) || desc.properties.nodes.length !== 2) return false; + const [en, get] = desc.properties.nodes as unknown as [ ts.ObjectLiteralElementLike, ts.ObjectLiteralElementLike, ]; @@ -494,8 +494,8 @@ function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean return false; if (en.initializer.kind !== ts.SyntaxKind.TrueKeyword) return false; const body = getterBodyOf(get); - if (body === undefined || body.statements.length !== 1) return false; - const ret = body.statements[0]!; + if (body === undefined || body.statements.nodes.length !== 1) return false; + const ret = body.statements.nodes[0]!; if (!ts.isReturnStatement(ret) || ret.expression === undefined) return false; return isKeyedAccess(ret.expression, (x) => ts.isIdentifier(x) && x.text === id1, key); } @@ -541,16 +541,16 @@ function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null ) { return null; } - if (keysCall.arguments.length !== 1) return null; - const id1Node = keysCall.arguments[0]!; + if (keysCall.arguments.nodes.length !== 1) return null; + const id1Node = keysCall.arguments.nodes[0]!; if (!ts.isIdentifier(id1Node) || !sourceSpellsIdentifier(id1Node, sf)) return null; const id1 = id1Node.text; - const fn = call.arguments[0]; + const fn = call.arguments.nodes[0]; if (fn === undefined || !ts.isFunctionExpression(fn)) return null; // arrows never match (probed) if (fn.name !== undefined) return null; // a NAMED callback misses (probed) if (fn.asteriskToken !== undefined || (ts.getModifiers(fn) ?? []).length > 0) return null; - if (fn.parameters.length !== 1) return null; - const keyParam = fn.parameters[0]!; + if (fn.parameters.nodes.length !== 1) return null; + const keyParam = fn.parameters.nodes[0]!; if ( !ts.isIdentifier(keyParam.name) || keyParam.initializer !== undefined || @@ -558,7 +558,7 @@ function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null ) return null; const key = keyParam.name.text; - const stmts = fn.body.statements; + const stmts = fn.body.statements.nodes; // Form B: the single `if (KEY !== 'default' [&& !hasOwn]) COPY` statement. if (stmts.length === 1 && ts.isIfStatement(stmts[0]!) && stmts[0]!.elseStatement === undefined) { diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index 24462a2ce..9ba041d2c 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -262,7 +262,7 @@ function placeHeritage( return unsupported(out, node, marker); // `implements Acyclic` would make the class implement C's own type, // which the lowering treats as a protocol of C. - if ((node.typeArguments?.length ?? 0) > 0) return unsupported(out, node, marker); + if ((node.typeArguments?.nodes.length ?? 0) > 0) return unsupported(out, node, marker); const note = (name: string): ContractNote => ({ marker, loc: locOf(node), name }); if ( (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && @@ -324,8 +324,8 @@ function place( ts.isTypeReferenceNode(parent) && ts.isIdentifier(parent.typeName) && (parent.typeName.text === "Array" || parent.typeName.text === "ReadonlyArray") && - parent.typeArguments?.length === 1 && - parent.typeArguments[0] === ref + parent.typeArguments?.nodes.length === 1 && + parent.typeArguments.nodes[0] === ref ) { node = parent; parent = parent.parent; @@ -336,7 +336,7 @@ function place( // contract of the function the NoThrow annotates. if (marker !== "NoThrow" && parent !== undefined && ts.isTypeReferenceNode(parent)) { const wrapper = markerOf(checker, parent); - if (wrapper === "NoThrow" && parent.typeArguments?.[0] === node) { + if (wrapper === "NoThrow" && parent.typeArguments?.nodes[0] === node) { node = parent; parent = parent.parent; } diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 0562467be..a7248c674 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -158,13 +158,13 @@ function staticBoolean( if ( ts.isCallExpression(current) && !current.questionDotToken && - current.arguments.length === 1 && + current.arguments.nodes.length === 1 && ts.isPropertyAccessExpression(current.expression) && !current.expression.questionDotToken && (current.expression.name.text === "endsWith" || current.expression.name.text === "startsWith") ) { const receiver = staticString(program, current.expression.expression, seen); - const needle = staticString(program, current.arguments[0]!, seen); + const needle = staticString(program, current.arguments.nodes[0]!, seen); if (receiver === null || needle === null) return null; return current.expression.name.text === "endsWith" ? receiver.endsWith(needle) @@ -194,7 +194,7 @@ function staticString( if (importMetaUrl(current)) return pathToFileURL(current.getSourceFile().fileName).href; if (ts.isTemplateExpression(current)) { let value = current.head.text; - for (const span of current.templateSpans) { + for (const span of current.templateSpans.nodes) { const part = staticString(program, span.expression, seen); if (part === null) return null; value += part + span.literal.text; @@ -219,11 +219,11 @@ function staticString( if ( ts.isCallExpression(current) && !current.questionDotToken && - current.arguments.length === 1 && + current.arguments.nodes.length === 1 && ts.isIdentifier(current.expression) && isBuiltinMemberImport(program, current.expression, "url", "fileURLToPath") ) { - const href = staticString(program, current.arguments[0]!, seen); + const href = staticString(program, current.arguments.nodes[0]!, seen); if (href === null) return null; try { return fileURLToPath(href); @@ -237,7 +237,7 @@ function staticString( (isBuiltinMemberImport(program, current.expression, "url", "URL") || (current.expression.text === "URL" && isNodeGlobal(program, current.expression))) ) { - const args = current.arguments ?? []; + const args = current.arguments?.nodes ?? []; if (args.length < 1 || args.length > 2) return null; const input = staticString(program, args[0]!, seen); const base = args[1] === undefined ? null : staticString(program, args[1], seen); @@ -261,7 +261,7 @@ export function staticForkModulePath(program: ts.Program, expr: ts.Expression): const supported = (ts.isCallExpression(current) && !current.questionDotToken && - current.arguments.length === 1 && + current.arguments.nodes.length === 1 && ts.isIdentifier(current.expression) && isBuiltinMemberImport(program, current.expression, "url", "fileURLToPath")) || (ts.isNewExpression(current) && @@ -279,7 +279,7 @@ export function staticForkModulePath(program: ts.Program, expr: ts.Expression): } export function forkCallModulePath(program: ts.Program, call: ts.CallExpression): string | null { - if (call.questionDotToken || call.arguments[0] === undefined) return null; + if (call.questionDotToken || call.arguments.nodes[0] === undefined) return null; const callee = strip(call.expression); const isFork = (ts.isIdentifier(callee) && isBuiltinMemberImport(program, callee, "child_process", "fork")) || @@ -291,7 +291,7 @@ export function forkCallModulePath(program: ts.Program, call: ts.CallExpression) if (!isFork) { return null; } - return staticForkModulePath(program, call.arguments[0]); + return staticForkModulePath(program, call.arguments.nodes[0]); } /** Source-order static fork targets in the supplied files, deduplicated by @@ -304,7 +304,7 @@ export function forkTargetPaths(program: ts.Program, files: readonly ts.SourceFi const calls = moduleSourceCandidates(program, sourceFile).calls; const bindings: ts.Node[] = []; for (const call of calls) { - if (call.questionDotToken || call.arguments[0] === undefined) continue; + if (call.questionDotToken || call.arguments.nodes[0] === undefined) continue; const callee = strip(call.expression); if (ts.isIdentifier(callee)) bindings.push(callee); else if ( diff --git a/packages/compiler/src/frontend/function-completion.ts b/packages/compiler/src/frontend/function-completion.ts index 7c8d8a9fc..02c85b729 100644 --- a/packages/compiler/src/frontend/function-completion.ts +++ b/packages/compiler/src/frontend/function-completion.ts @@ -6,14 +6,14 @@ export function functionCanFallThrough(declaration: ts.Node): boolean { if ( !ts.isFunctionLike(declaration) || declaration.asteriskToken || - declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + declaration.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ) return false; const body = declaration.body as ts.Block | ts.Expression | undefined; if (!body || !ts.isBlock(body)) return false; const completes = (statement: ts.Statement): boolean => { if (ts.isReturnStatement(statement) || ts.isThrowStatement(statement)) return false; - if (ts.isBlock(statement)) return sequence(statement.statements); + if (ts.isBlock(statement)) return sequence(statement.statements.nodes); if (ts.isIfStatement(statement)) return ( completes(statement.thenStatement) || @@ -29,13 +29,13 @@ export function functionCanFallThrough(declaration: ts.Node): boolean { } if (ts.isSwitchStatement(statement)) return ( - !statement.caseBlock.clauses.some(ts.isDefaultClause) || - statement.caseBlock.clauses.some((clause) => sequence(clause.statements)) + !statement.caseBlock.clauses.nodes.some(ts.isDefaultClause) || + statement.caseBlock.clauses.nodes.some((clause) => sequence(clause.statements.nodes)) ); return true; }; const sequence = (statements: readonly ts.Statement[]): boolean => statements.every(completes); - return sequence(body.statements); + return sequence(body.statements.nodes); } /** A documented JavaScript result must also represent explicit empty returns. */ @@ -44,7 +44,7 @@ export function functionCanReturnUndefined(declaration: ts.Node): boolean { if ( !ts.isFunctionLike(declaration) || declaration.asteriskToken || - declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + declaration.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ) return false; const body = declaration.body; diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.ts index 0fe38c258..a4ad48892 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts @@ -50,9 +50,10 @@ export function inferredJsDiagnosticSuppressed( (ts.isFunctionDeclaration(declaration) || ts.isFunctionExpression(declaration) || ts.isMethodDeclaration(declaration)) && - node.arguments.length > declaration.parameters.length && - node.arguments[declaration.parameters.length]?.getStart(file) === diagnostic.pos && - node.arguments[node.arguments.length - 1]?.getEnd() === diagnostic.end && + node.arguments.nodes.length > declaration.parameters.nodes.length && + node.arguments.nodes[declaration.parameters.nodes.length]?.getStart(file) === + diagnostic.pos && + node.arguments.nodes[node.arguments.nodes.length - 1]?.getEnd() === diagnostic.end && bodyReadsArguments(declaration) ) { suppressed = true; @@ -92,8 +93,8 @@ export function inferredJsDiagnosticSuppressed( ts.isMethodDeclaration(declaration) && declaration.type === undefined && isJsSourceFileName(declaration.getSourceFile().fileName) && - declaration.body?.statements.length === 1 && - ts.isThrowStatement(declaration.body.statements[0]!) + declaration.body?.statements?.nodes.length === 1 && + ts.isThrowStatement(declaration.body.statements.nodes[0]!) ) { suppressed = true; return "stop"; diff --git a/packages/compiler/src/frontend/lib-contract.ts b/packages/compiler/src/frontend/lib-contract.ts index 71217ecda..0432d06e2 100644 --- a/packages/compiler/src/frontend/lib-contract.ts +++ b/packages/compiler/src/frontend/lib-contract.ts @@ -141,7 +141,7 @@ function localTypeBindings(file: ts.SourceFile, bindings: TypeBindings): Set { if (name !== undefined && CONTRACT_GLOBAL_TYPES.has(name.text)) names.add(name.text); }; - for (const stmt of file.statements) { + for (const stmt of file.statements.nodes) { if ( ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || @@ -160,7 +160,7 @@ function localTypeBindings(file: ts.SourceFile, bindings: TypeBindings): Set ts.isTypeParameterDeclaration(p) && p.name.text === name) === true) return false; } @@ -254,7 +254,7 @@ function typeShape( return { k: "array", elem: typeShape(file, node.elementType, bindings) }; if (ts.isTupleTypeNode(node)) { const elems: ContractTypeShape[] = []; - for (const e of node.elements) { + for (const e of node.elements.nodes) { // Named tuple members carry the type under `.type`. const t = ts.isNamedTupleMember(e) ? e.type : (e as ts.TypeNode); elems.push(typeShape(file, t, bindings)); @@ -262,13 +262,13 @@ function typeShape( return { k: "tuple", elems }; } if (ts.isUnionTypeNode(node)) { - return { k: "union", parts: node.types.map((t) => typeShape(file, t, bindings)) }; + return { k: "union", parts: node.types.nodes.map((t) => typeShape(file, t, bindings)) }; } if (ts.isTypeLiteralNode(node)) { let bad: string | null = null; const fields = shapeOfMembers( file, - node.members, + node.members.nodes, (text) => { bad = text; }, @@ -292,16 +292,16 @@ function typeShape( const name = node.typeName.text; const global = CONTRACT_GLOBAL_TYPES.has(name) && isUnshadowedGlobalType(file, node, name, bindings); - if (global && name === "Uint8Array" && (node.typeArguments?.length ?? 0) === 0) + if (global && name === "Uint8Array" && (node.typeArguments?.nodes.length ?? 0) === 0) return { k: "bytes" }; if ( global && (name === "Array" || name === "ReadonlyArray") && - node.typeArguments?.length === 1 + node.typeArguments?.nodes.length === 1 ) { - return { k: "array", elem: typeShape(file, node.typeArguments[0]!, bindings) }; + return { k: "array", elem: typeShape(file, node.typeArguments.nodes[0]!, bindings) }; } - if ((node.typeArguments?.length ?? 0) > 0) + if ((node.typeArguments?.nodes.length ?? 0) > 0) return { k: "unsupported", text: node.getText(file) }; return { k: "ref", name }; } @@ -328,7 +328,7 @@ function stringArray(expr: ts.Expression): string[] | null { const e = unwrapConst(expr); if (!ts.isArrayLiteralExpression(e)) return null; const out: string[] = []; - for (const el of e.elements) { + for (const el of e.elements.nodes) { const v = unwrapConst(el as ts.Expression); if (!ts.isStringLiteral(v)) return null; out.push(v.text); @@ -340,12 +340,12 @@ function envMsgArray(expr: ts.Expression): { env: string; msg: string }[] | null const e = unwrapConst(expr); if (!ts.isArrayLiteralExpression(e)) return null; const out: { env: string; msg: string }[] = []; - for (const el of e.elements) { + for (const el of e.elements.nodes) { const v = unwrapConst(el as ts.Expression); if (!ts.isObjectLiteralExpression(v)) return null; let env: string | null = null; let msg: string | null = null; - for (const prop of v.properties) { + for (const prop of v.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; const name = propName(prop.name); const val = unwrapConst(prop.initializer); @@ -410,7 +410,7 @@ export function entryContractFacts( * and the refusal names every site. */ const collectTypes = (file: ts.SourceFile): void => { const exportedHere = new Set(); - for (const stmt of file.statements) { + for (const stmt of file.statements.nodes) { if ( (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) && isExported(stmt) @@ -418,7 +418,7 @@ export function entryContractFacts( exportedHere.add(stmt.name.text); } } - for (const stmt of file.statements) { + for (const stmt of file.statements.nodes) { if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) { const name = stmt.name.text; const exported = isExported(stmt); @@ -433,14 +433,14 @@ export function entryContractFacts( let bad: string | null = null; const fields = shapeOfMembers( file, - stmt.members, + stmt.members.nodes, (text) => { bad = text; }, bindings, ); - const generic = (stmt.typeParameters?.length ?? 0) > 0; - const heritage = (stmt.heritageClauses?.length ?? 0) > 0; + const generic = (stmt.typeParameters?.nodes.length ?? 0) > 0; + const heritage = (stmt.heritageClauses?.nodes.length ?? 0) > 0; facts.types.push({ name, form: "interface", @@ -455,7 +455,7 @@ export function entryContractFacts( loc: locOf(file, stmt), }); } else { - const generic = (stmt.typeParameters?.length ?? 0) > 0; + const generic = (stmt.typeParameters?.nodes.length ?? 0) > 0; facts.types.push({ name, form: "alias", @@ -473,7 +473,7 @@ export function entryContractFacts( stmt.body !== undefined && ts.isModuleBlock(stmt.body) ) { - for (const inner of stmt.body.statements) { + for (const inner of stmt.body.statements.nodes) { if (ts.isInterfaceDeclaration(inner) || ts.isTypeAliasDeclaration(inner)) { addSite(inner.name.text, siteOf(file, inner)); } @@ -494,25 +494,25 @@ export function entryContractFacts( if (list.length > 1) facts.multiSiteTypes.push({ name, sites: list }); } - for (const stmt of entry.statements) { + for (const stmt of entry.statements.nodes) { if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) { if (!isExported(stmt)) continue; if (stmt.body === undefined && facts.functions.some((f) => f.name === stmt.name!.text)) continue; // overload signature facts.functions.push({ name: stmt.name.text, - params: stmt.parameters.map((p) => ({ + params: stmt.parameters.nodes.map((p) => ({ name: ts.isIdentifier(p.name) ? p.name.text : "", shape: p.type !== undefined ? typeShape(entry, p.type, bindings) : null, })), returns: stmt.type !== undefined ? typeShape(entry, stmt.type, bindings) : null, - generic: (stmt.typeParameters?.length ?? 0) > 0, + generic: (stmt.typeParameters?.nodes.length ?? 0) > 0, loc: locOf(entry, stmt), }); continue; } if (ts.isVariableStatement(stmt) && isExported(stmt)) { - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(decl.name)) continue; const name = decl.name.text; if (!CONVENTION_CONSTS.has(name)) continue; diff --git a/packages/compiler/src/frontend/lib-exports.ts b/packages/compiler/src/frontend/lib-exports.ts index 4839ff97f..738dd2f19 100644 --- a/packages/compiler/src/frontend/lib-exports.ts +++ b/packages/compiler/src/frontend/lib-exports.ts @@ -26,7 +26,7 @@ export interface EntryExportInfo { * Overload signatures collapse onto one entry (same name, one body). */ export function entryFunctionExports(entry: ts.SourceFile): Map { const out = new Map(); - for (const stmt of entry.statements) { + for (const stmt of entry.statements.nodes) { if (!ts.isFunctionDeclaration(stmt) || stmt.name === undefined) continue; const mods = ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined; const exported = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true; @@ -36,7 +36,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map 0, + generic: (stmt.typeParameters?.nodes.length ?? 0) > 0, async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true, generator: stmt.asteriskToken !== undefined, }); diff --git a/packages/compiler/src/frontend/lowering/assert-instances.ts b/packages/compiler/src/frontend/lowering/assert-instances.ts index 3c7f96baa..feeb23019 100644 --- a/packages/compiler/src/frontend/lowering/assert-instances.ts +++ b/packages/compiler/src/frontend/lowering/assert-instances.ts @@ -110,14 +110,14 @@ export function isAssertClassNew(lowerer: Lowerer, expr: ts.Expression): boolean /** The options of `new Assert()`, or null when an option is * not a literal or the argument list has another shape. */ export function literalAssertOptions(expr: ts.NewExpression): AssertInstanceOptions | null { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const options: AssertInstanceOptions = { strict: true, diff: "simple", skipPrototype: false }; if (args.length === 0) return options; if (args.length > 1) return null; const arg = stripTypeCasts(args[0]!); if (ts.isIdentifier(arg) && arg.text === "undefined") return options; if (!ts.isObjectLiteralExpression(arg)) return null; - for (const prop of arg.properties) { + for (const prop of arg.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; const name = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index 7bb5f3e62..7e1852600 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -24,7 +24,7 @@ export function lowerAddAbortListener( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`addAbortListener with this argument list`, expr); } diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index 00aac2794..5e21f6cb3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -192,7 +192,7 @@ export function literalBoolOptions( if (!ts.isObjectLiteralExpression(node)) return null; const bools: Record = {}; const exprs: Record = {}; - for (const p of node.properties) { + for (const p of node.properties.nodes) { if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name)) return null; if (exprKeys.includes(p.name.text)) { exprs[p.name.text] = p.initializer; diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index 27eb3cb87..708284be1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -106,14 +106,14 @@ export function lowerDiagnosticsChannelMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "publish" && call.arguments.length === 1) { + if (name === "publish" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const msg = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); + const msg = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.publish", args: [receiver, msg], type: VOID, loc }; } - if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) { + if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); return name === "subscribe" ? { kind: "libCall", fn: "dc.chanSubscribe", args: [receiver, cb], type: VOID, loc } : { kind: "libCall", fn: "dc.chanUnsubscribe", args: [receiver, cb], type: BOOL, loc }; @@ -126,18 +126,18 @@ export function lowerDiagnosticsChannelMethodCall( // this/arguments to fn exactly like the trace calls. if ( (name === "bindStore" || name === "unbindStore") && - call.arguments.length >= 1 && - call.arguments.length <= (name === "bindStore" ? 2 : 1) + call.arguments.nodes.length >= 1 && + call.arguments.nodes.length <= (name === "bindStore" ? 2 : 1) ) { - if (!isBuiltinClassInstance(lowerer, call.arguments[0]!, "AsyncLocalStorage")) { + if (!isBuiltinClassInstance(lowerer, call.arguments.nodes[0]!, "AsyncLocalStorage")) { lowerer.noLowering( `Channel.${name} with this store argument`, - call.arguments[0]!, + call.arguments.nodes[0]!, "an AsyncLocalStorage instance (node:async_hooks) is the supported store", ); } const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const store = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const store = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); if (name === "unbindStore") { return { kind: "libCall", @@ -148,8 +148,8 @@ export function lowerDiagnosticsChannelMethodCall( }; } const transform: IrExpr = - call.arguments[1] !== undefined - ? lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!) + call.arguments.nodes[1] !== undefined + ? lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!) : dynUndefinedExpr(loc); return { kind: "libCall", @@ -159,15 +159,15 @@ export function lowerDiagnosticsChannelMethodCall( loc, }; } - if (name === "runStores" && call.arguments.length >= 2) { + if (name === "runStores" && call.arguments.nodes.length >= 2) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const data = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const data = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); const thisArg: IrExpr = - call.arguments[2] !== undefined - ? lowerDiagnosticsMessage(lowerer, call.arguments[2]!) + call.arguments.nodes[2] !== undefined + ? lowerDiagnosticsMessage(lowerer, call.arguments.nodes[2]!) : dynUndefinedExpr(loc); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(3), loc); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3), loc); return { kind: "libCall", fn: "dc.chanRunStores", @@ -198,7 +198,7 @@ export function lowerAsyncLocalStorageMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "getStore" && call.arguments.length === 0) { + if (name === "getStore" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", @@ -208,11 +208,11 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "run" && call.arguments.length >= 2) { + if (name === "run" && call.arguments.nodes.length >= 2) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(2), loc); + const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(2), loc); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", fn: "als.run", @@ -221,10 +221,10 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "exit" && call.arguments.length >= 1) { + if (name === "exit" && call.arguments.nodes.length >= 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(1), loc); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(1), loc); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", fn: "als.exitRun", @@ -233,12 +233,12 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "enterWith" && call.arguments.length === 1) { + if (name === "enterWith" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); + const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn: "als.enterWith", args: [receiver, value], type: VOID, loc }; } - if (name === "disable" && call.arguments.length === 0) { + if (name === "disable" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); return { kind: "libCall", fn: "als.disable", args: [receiver], type: VOID, loc }; } @@ -301,13 +301,13 @@ function lowerTracingHandlers(lowerer: Lowerer, node: ts.Expression): IrExpr { if ( ts.isObjectLiteralExpression(e) && lowerer.mapTypeOf(lowerer.typeOf(e))?.kind === "record" && - e.properties.length > 0 && - e.properties.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) + e.properties.nodes.length > 0 && + e.properties.nodes.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) ) { const loc = locOf(e); return { kind: "dynObjLit", - fields: e.properties.map((p) => { + fields: e.properties.nodes.map((p) => { const pa = p as ts.PropertyAssignment; return { key: { @@ -366,32 +366,32 @@ export function lowerTracingChannelMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) { + if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const handlers = lowerTracingHandlers(lowerer, call.arguments[0]!); + const handlers = lowerTracingHandlers(lowerer, call.arguments.nodes[0]!); return name === "subscribe" ? { kind: "libCall", fn: "dc.tcSubscribe", args: [receiver, handlers], type: VOID, loc } : { kind: "libCall", fn: "dc.tcUnsubscribe", args: [receiver, handlers], type: BOOL, loc }; } if ( (name === "traceSync" || name === "traceCallback" || name === "tracePromise") && - call.arguments.length >= 1 + call.arguments.nodes.length >= 1 ) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); // traceSync(fn, context?, thisArg?, ...args) / // tracePromise(fn, context?, thisArg?, ...args) / // traceCallback(fn, position?, context?, thisArg?, ...args) const shift = name === "traceCallback" ? 1 : 0; - const ctxNode = call.arguments[1 + shift]; - const thisNode = call.arguments[2 + shift]; + const ctxNode = call.arguments.nodes[1 + shift]; + const thisNode = call.arguments.nodes[2 + shift]; const ctx: IrExpr = ctxNode !== undefined ? lowerDiagnosticsMessage(lowerer, ctxNode) : { kind: "dynObjLit", fields: [], type: DYN, loc }; const thisArg: IrExpr = thisNode !== undefined ? lowerDiagnosticsMessage(lowerer, thisNode) : dynUndefinedExpr(loc); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(3 + shift), loc); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3 + shift), loc); if (name === "traceSync") { return { kind: "libCall", @@ -416,8 +416,8 @@ export function lowerTracingChannelMethodCall( }; } const pos: IrExpr = - call.arguments[1] !== undefined - ? lowerer.lowerExprExpecting(call.arguments[1]!, F64) + call.arguments.nodes[1] !== undefined + ? lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64) : { kind: "numLit", value: -1, type: F64, loc }; return { kind: "libCall", @@ -473,29 +473,32 @@ export function lowerDiagnosticsChannelCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr | null { - if (bi.member === "channel" && expr.arguments.length === 1) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); + if (bi.member === "channel" && expr.arguments.nodes.length === 1) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.channel", args: [name], type: F64, loc }; } - if ((bi.member === "subscribe" || bi.member === "unsubscribe") && expr.arguments.length === 2) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); - const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments[1]!); + if ( + (bi.member === "subscribe" || bi.member === "unsubscribe") && + expr.arguments.nodes.length === 2 + ) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); + const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments.nodes[1]!); return bi.member === "subscribe" ? { kind: "libCall", fn: "dc.subscribe", args: [name, cb], type: VOID, loc } : { kind: "libCall", fn: "dc.unsubscribe", args: [name, cb], type: BOOL, loc }; } - if (bi.member === "hasSubscribers" && expr.arguments.length === 1) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); + if (bi.member === "hasSubscribers" && expr.arguments.nodes.length === 1) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.hasSubscribers", args: [name], type: BOOL, loc }; } // tracingChannel: the string form interns the five tracing::* // channels; the collection form takes an object literal whose five // event members are Channel-typed values (Node's TracingChannelCollection). - if (bi.member === "tracingChannel" && expr.arguments.length === 1) { - const a = expr.arguments[0]!; + if (bi.member === "tracingChannel" && expr.arguments.nodes.length === 1) { + const a = expr.arguments.nodes[0]!; if (ts.isObjectLiteralExpression(a)) { const byEvent = new Map(); - for (const p of a.properties) { + for (const p of a.properties.nodes) { if ( !ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name) || diff --git a/packages/compiler/src/frontend/lowering/builtins/atomics.ts b/packages/compiler/src/frontend/lowering/builtins/atomics.ts index b4a394dce..41e35a511 100644 --- a/packages/compiler/src/frontend/lowering/builtins/atomics.ts +++ b/packages/compiler/src/frontend/lowering/builtins/atomics.ts @@ -45,9 +45,9 @@ export function lowerAtomicsCall( : 3; const maximum = member === "wait" ? 4 : member === "notify" ? 3 : minimum; if ( - call.arguments.length < minimum || - call.arguments.length > maximum || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < minimum || + call.arguments.nodes.length > maximum || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `Atomics.${member} arguments`, @@ -55,7 +55,7 @@ export function lowerAtomicsCall( `use ${minimum}${maximum > minimum ? `–${maximum}` : ""} arguments`, ); } - const source = call.arguments[0]!; + const source = call.arguments.nodes[0]!; // Generic constraints can remain unions in the checker while this body // has a concrete typed-array binding. Validate the actual lowered storage. const array = lowerer.lowerExpr(source); @@ -75,8 +75,8 @@ export function lowerAtomicsCall( } const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); const arg = (index: number, fallback: number): IrExpr => - call.arguments[index] - ? lowerOptionalArgument(lowerer, call.arguments[index]!, F64, number(fallback)) + call.arguments.nodes[index] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[index]!, F64, number(fallback)) : number(fallback); const index = arg(1, 0); if (member === "wait") diff --git a/packages/compiler/src/frontend/lowering/builtins/blob.ts b/packages/compiler/src/frontend/lowering/builtins/blob.ts index 14d3c6cce..3fc4ec173 100644 --- a/packages/compiler/src/frontend/lowering/builtins/blob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/blob.ts @@ -70,7 +70,7 @@ function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpr export function lowerBlobNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const cls = blobClassOf(lowerer, expr.expression); if (cls === null) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); const values = args.map((arg) => @@ -94,10 +94,10 @@ export function lowerStaticBlobCall(lowerer: Lowerer, call: ts.CallExpression): const member = access.name.text; if (access.questionDotToken || !BLOB_CALLS.has(member)) return null; if (blobReceiverClass(lowerer, access.expression) === null) return null; - if (call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; const loc = locOf(call); const receiver = lowerer.lowerExprExpecting(access.expression, DYN); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); if (member === "slice") { return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index cf19b4003..1669978af 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -126,7 +126,7 @@ export function registerPromisifiedBuiltinDecl( if (registerHttpClientFnBinding(lowerer, nameNode, init)) return true; const e = isPromisifyCall(lowerer, init); if (!e) return false; - const argNode = e.arguments.length === 1 ? e.arguments[0]! : null; + const argNode = e.arguments.nodes.length === 1 ? e.arguments.nodes[0]! : null; // `promisify(exec)` over a named import, or `promisify(cp.exec)` over a // namespace import. const target = !argNode diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index 04b3caba8..5590fbe9f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -42,9 +42,9 @@ export function lowerExecFileAsyncCall( ): IrExpr { const what = shell ? "the promisified exec" : "the promisified execFile"; if ( - expr.arguments.length < 1 || - expr.arguments.length > (shell ? 2 : 3) || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > (shell ? 2 : 3) || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `${what} with this argument shape`, @@ -54,7 +54,7 @@ export function lowerExecFileAsyncCall( : "the supported form is execFileAsync(file, args?, options?)", ); } - const command = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const command = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); // exec: /bin/sh -c , execSync's lowering (the command string // rides as the argv[1] the failure display reads); execFile: file + its // args list. @@ -66,8 +66,8 @@ export function lowerExecFileAsyncCall( type: arrayOf(STRING), loc, } - : lowerChildArgsArg(lowerer, expr.arguments[1], loc); - const optsNode = expr.arguments[shell ? 1 : 2]; + : lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + const optsNode = expr.arguments.nodes[shell ? 1 : 2]; const emptyStr: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; const helper = execFileAsyncHelper(lowerer, loc, shell); @@ -93,7 +93,7 @@ export function lowerExecFileAsyncCall( "pass the options inline so each member can be checked", ); } - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom carrying `env` — the portless // opensslAsync shape: `...(c ? { env: { ...process.env, ...extra // } } : {})` (either orientation). The condition evaluates ONCE diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index 1fc015892..a435bf740 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -35,22 +35,22 @@ export function lowerChildMethodCall( const loc = locOf(call); if (name === "send") { if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `child.send with ${call.arguments.length} arguments`, + `child.send with ${call.arguments.nodes.length} arguments`, call, "send(message[, callback]) is supported; handle transfer and options are not", ); } const receiver = lowerer.lowerExpr(access.expression); - const json = ipcJsonMessage(lowerer, call.arguments[0]!, loc); - if (call.arguments[1] === undefined) { + const json = ipcJsonMessage(lowerer, call.arguments.nodes[0]!, loc); + if (call.arguments.nodes[1] === undefined) { return { kind: "libCall", fn: "child.send", args: [receiver, json], type: BOOL, loc }; } - const callback = ipcSendCallback(lowerer, call.arguments[1]); + const callback = ipcSendCallback(lowerer, call.arguments.nodes[1]); return { kind: "libCall", fn: "child.sendCb", @@ -59,7 +59,7 @@ export function lowerChildMethodCall( loc, }; } - if (name === "disconnect" && call.arguments.length === 0) { + if (name === "disconnect" && call.arguments.nodes.length === 0) { if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( "SC1090", @@ -75,8 +75,8 @@ export function lowerChildMethodCall( loc, }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const evT = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if ( event !== "spawn" && @@ -88,7 +88,7 @@ export function lowerChildMethodCall( ) { lowerer.noLowering( `child.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)', ); } @@ -105,7 +105,7 @@ export function lowerChildMethodCall( return { kind: "libCall", fn: "child.onMessage", - args: [receiver, ipcMessageListener(lowerer, call.arguments[1]!), once], + args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, }; @@ -114,24 +114,24 @@ export function lowerChildMethodCall( return { kind: "libCall", fn: "child.onDisconnect", - args: [receiver, ipcDisconnectListener(lowerer, call.arguments[1]!), once], + args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, }; } - const listenerNode = stripTypeCasts(call.arguments[1]!); + const listenerNode = stripTypeCasts(call.arguments.nodes[1]!); if ( isJsSourceFile(listenerNode.getSourceFile()) && (ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) && - listenerNode.parameters.some((param) => param.dotDotDotToken) && - listenerNode.parameters.every((param) => !param.type) && + listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) && + listenerNode.parameters.nodes.every((param) => !param.type) && !/@(?:param|type)\b/.test( listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()), ) ) { - for (const param of listenerNode.parameters) lowerer.checkedCallbackParams.add(param); + for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param); } - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); if ( (isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") || (cb.type.kind === "func" && @@ -149,7 +149,7 @@ export function lowerChildMethodCall( ) { lowerer.noLowering( "child event listeners with non-representable parameters", - call.arguments[1]!, + call.arguments.nodes[1]!, ); } return { @@ -168,7 +168,7 @@ export function lowerChildMethodCall( if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { lowerer.noLowering( "spawn event listeners with parameters or a returned value", - call.arguments[1]!, + call.arguments.nodes[1]!, ); } return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc }; @@ -176,7 +176,7 @@ export function lowerChildMethodCall( if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event !== "error" ? `${event} listeners with more than two parameters (use (code, signal), (code), or ())` : "error listeners with more than one parameter (use (err) or ())", @@ -188,7 +188,7 @@ export function lowerChildMethodCall( // fenced instead of being called through the wrong ABI. lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "listeners returning a value (make the callback body a block, or return nothing)", ); } @@ -206,7 +206,7 @@ export function lowerChildMethodCall( if (!armsOk) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `${event} listeners whose parameter is not 'number | null' (got '${lowerer.fmt(param!)}')`, ); } @@ -228,7 +228,7 @@ export function lowerChildMethodCall( if (!sigOk) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`, ); } @@ -243,7 +243,7 @@ export function lowerChildMethodCall( if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`, ); } @@ -256,11 +256,11 @@ export function lowerChildMethodCall( // reaped or never spawned (Node's null-handle answer), and a successful // send sets `killed`. if (name === "kill") { - if (call.arguments.length > 1) { - lowerer.noLowering(`child.kill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`child.kill with ${call.arguments.nodes.length} arguments`, call); } const receiver = lowerer.lowerExpr(access.expression); - const sigNode = call.arguments[0]; + const sigNode = call.arguments.nodes[0]; if (!sigNode) { const dflt: IrExpr = { kind: "strLit", value: "SIGTERM", type: STRING, loc }; return { kind: "libCall", fn: "child.kill", args: [receiver, dflt], type: BOOL, loc }; @@ -281,7 +281,7 @@ export function lowerChildMethodCall( // child.unref(): drops the child from the event loop's keep-alive set // (the process may exit while the child runs — Node's semantics; the // child is still reaped while the loop runs for other reasons). - if ((name === "unref" || name === "ref") && call.arguments.length === 0) { + if ((name === "unref" || name === "ref") && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", @@ -320,9 +320,9 @@ export function lowerChildStreamMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "setEncoding" && call.arguments.length === 1) { + if (name === "setEncoding" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, CHILDSTREAM_T); - const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments[0]!); + const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments.nodes[0]!); return { kind: "libCall", fn: "stream.childSetEncoding", @@ -331,13 +331,13 @@ export function lowerChildStreamMethodCall( loc, }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const evT = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if (event !== "data" && event !== "end") { lowerer.noLowering( `stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"data" and "end" are the supported child-stream events (as literals)', ); } @@ -349,7 +349,7 @@ export function lowerChildStreamMethodCall( ); } const receiver = lowerer.lowerExpr(access.expression); - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc }; if ( cb.type.kind !== "func" || @@ -358,7 +358,7 @@ export function lowerChildStreamMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event === "data" ? "data listeners with more than one parameter or a return value (use (chunk) or ())" : "end listeners with parameters or a return value (use ())", @@ -381,7 +381,7 @@ export function lowerChildStreamMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`, ); } @@ -417,15 +417,15 @@ export function lowerChildWriterMethodCall( const loc = locOf(call); if (name === "write") { - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `child stdin write with ${call.arguments.length} arguments`, + `child stdin write with ${call.arguments.nodes.length} arguments`, call, "write(string | Uint8Array) with no encoding or callback is supported", ); } const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T); - const data = lowerer.lowerExpr(call.arguments[0]!); + const data = lowerer.lowerExpr(call.arguments.nodes[0]!); if (data.type.kind === "string") { return { kind: "libCall", fn: "writer.writeString", args: [receiver, data], type: BOOL, loc }; } @@ -434,12 +434,12 @@ export function lowerChildWriterMethodCall( } lowerer.noLowering( `child stdin write of '${lowerer.fmt(data.type)}'`, - call.arguments[0]!, + call.arguments.nodes[0]!, "write one string, Buffer, or Uint8Array value (narrow unions first)", ); } - if ((name === "end" || name === "destroy") && call.arguments.length === 0) { + if ((name === "end" || name === "destroy") && call.arguments.nodes.length === 0) { if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( "SC1090", @@ -457,13 +457,13 @@ export function lowerChildWriterMethodCall( }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const eventType = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const eventType = lowerer.typeOf(call.arguments.nodes[0]!); const event = eventType.isStringLiteralType() ? eventType.value : null; if (event !== "drain" && event !== "finish" && event !== "error") { lowerer.noLowering( `child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"drain", "finish", and "error" are the supported child stdin events', ); } @@ -471,7 +471,7 @@ export function lowerChildWriterMethodCall( lowerer.unsupported("SC1090", call, "chaining child stdin listener registration"); } const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T); - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); const maxParams = event === "error" ? 1 : 0; if ( cb.type.kind !== "func" || @@ -480,7 +480,7 @@ export function lowerChildWriterMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event === "error" ? "error listeners with at most one Error parameter and no return value" : `${event} listeners with no parameters or return value`, @@ -493,7 +493,7 @@ export function lowerChildWriterMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "child stdin error listeners whose parameter is Error", ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index bfa2bc99a..24adcc1ba 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -48,15 +48,15 @@ export function lowerExecSyncCall( shell: boolean, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${shell ? "execSync" : "execFileSync"} with a spread call`, expr); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); // execSync: /bin/sh -c ; execFileSync: file + its args list. let argvExpr: IrExpr; let optsNode: ts.Expression | undefined; if (shell) { - if (expr.arguments.length > 2) { + if (expr.arguments.nodes.length > 2) { lowerer.noLowering( "execSync with this argument shape", expr, @@ -69,17 +69,17 @@ export function lowerExecSyncCall( type: arrayOf(STRING), loc, }; - optsNode = expr.arguments[1]; + optsNode = expr.arguments.nodes[1]; } else { - if (expr.arguments.length > 3) { + if (expr.arguments.nodes.length > 3) { lowerer.noLowering( "execFileSync with this argument shape", expr, "the supported form is execFileSync(file, args?, options?)", ); } - argvExpr = lowerChildArgsArg(lowerer, expr.arguments[1], loc); - optsNode = expr.arguments[2]; + argvExpr = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + optsNode = expr.arguments.nodes[2]; } // The shell command itself is /bin/sh; the "command" string rides as // the argv[1] the display formatter reads. @@ -186,8 +186,8 @@ export function lowerExecSyncCall( modeOf(t.value, 2); return; } - if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) { - node.elements.forEach((el, i) => { + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) { + node.elements.nodes.forEach((el, i) => { const et = lowerer.typeOf(el); if (!et.isStringLiteralType()) { lowerer.noLowering( @@ -206,7 +206,7 @@ export function lowerExecSyncCall( ); }; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom carrying `env` — the openssl-runner // shape: `...(c ? { env: { ...process.env, ...extra } } : {})` // (either orientation). The condition evaluates ONCE and picks diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ecec14fc5..ea2dd379d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -31,8 +31,8 @@ export function lowerChildArgsArg( loc: SrcLoc, ): IrExpr { if (!node) return { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }; - if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) { - const elems = node.elements.map((el) => lowerer.lowerExprExpecting(el, STRING)); + if (ts.isArrayLiteralExpression(node) && !node.elements.nodes.some(ts.isSpreadElement)) { + const elems = node.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, STRING)); return { kind: "arrayLit", elems, type: arrayOf(STRING), loc: locOf(node) }; } return lowerer.lowerExprExpecting(node, arrayOf(STRING)); @@ -92,16 +92,16 @@ const NODE_SIGNAL_NAMES = new Set([ * fences by name. The bare `{ encoding: "utf8" }` shape keeps its * historical cp.spawnSync lowering. */ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "spawnSync with this argument shape", expr, "the supported form is spawnSync(command, args?, options?)", ); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const argv = lowerChildArgsArg(lowerer, expr.arguments[1], loc); - const optsNode = expr.arguments[2]; + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const argv = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + const optsNode = expr.arguments.nodes[2]; let timeout: IrExpr = numLit(0, loc); let killSignal: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; @@ -152,8 +152,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo modeOf(t.value, 2); return; } - if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) { - node.elements.forEach((el, i) => { + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) { + node.elements.nodes.forEach((el, i) => { const et = lowerer.typeOf(el); if (!et.isStringLiteralType()) { lowerer.noLowering("spawnSync stdio tuple entries beyond string literals", el); @@ -182,7 +182,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo 'stdio takes a "pipe"/"ignore"/"inherit" literal (or a value typed as a union of those), or a 3-tuple of literals', ); }; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -280,21 +280,21 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo * `windowsHide` (a POSIX no-op). The bare `{ stdio: "ignore" }` shape * keeps its historical cp.spawn lowering. */ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "spawn with this argument shape", expr, 'the supported form is spawn(command, args?, { stdio: "pipe" | "ignore" | "inherit", detached?, env?, cwd? })', ); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); let argsNode: ts.Expression | undefined; let optsNode: ts.Expression | undefined; - if (expr.arguments.length === 3) { - argsNode = expr.arguments[1]; - optsNode = expr.arguments[2]; - } else if (expr.arguments.length === 2) { - const second = expr.arguments[1]!; + if (expr.arguments.nodes.length === 3) { + argsNode = expr.arguments.nodes[1]; + optsNode = expr.arguments.nodes[2]; + } else if (expr.arguments.nodes.length === 2) { + const second = expr.arguments.nodes[1]!; const secondType = lowerer.typeOf(second); const mapped = lowerer.mapTypeOf(secondType); if ( @@ -338,7 +338,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S let plain = true; // exactly { stdio: "ignore" }: the historical libCall if (optsNode && ts.isObjectLiteralExpression(optsNode)) { - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom `...(isWindows ? {} : { detached: // true })` (either orientation): the one carried member supported // is `detached` with a boolean literal — the platform-conditional @@ -393,7 +393,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S // The 3-tuple form: every slot accepts a literal, including // "pipe" (child.stdin/stdout/stderr); stdout/stderr also accept // a number-typed fd (an openSync result — dup2'd into the child). - if (ts.isArrayLiteralExpression(m.value) && m.value.elements.length === 3) { + if (ts.isArrayLiteralExpression(m.value) && m.value.elements.nodes.length === 3) { const slot = (el: ts.Expression, which: 0 | 1 | 2): void => { const t = lowerer.typeOf(el); if (t.isStringLiteralType()) { @@ -434,7 +434,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S errFd = fd; } }; - m.value.elements.forEach((el, i) => slot(el, i as 0 | 1 | 2)); + m.value.elements.nodes.forEach((el, i) => slot(el, i as 0 | 1 | 2)); sawStdio = true; plain = false; break; @@ -512,7 +512,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S } if (shellEnabled && argsNode !== undefined) { const shellArgs = stripTypeCasts(argsNode); - if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.length !== 0) { + if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.nodes.length !== 0) { lowerer.noLowering( "spawn with shell enabled and separate arguments", argsNode, @@ -551,9 +551,9 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S * Node's default JSON IPC channel is always present. */ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "fork with this argument shape", @@ -561,24 +561,24 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr "use fork(staticModulePath[, stringArgs][, inlineOptions])", ); } - const targetPath = staticForkModulePath(lowerer.program, expr.arguments[0]!); + const targetPath = staticForkModulePath(lowerer.program, expr.arguments.nodes[0]!); const targetId = targetPath === null ? undefined : lowerer.forkTargetIdByPath.get(tsgoPath(resolve(targetPath))); if (targetId === undefined) { lowerer.noLowering( "fork with a runtime-valued module path", - expr.arguments[0]!, + expr.arguments.nodes[0]!, 'resolve a relative worker with new URL("./worker.ts", import.meta.url) or fileURLToPath(...) and bind it to a const if needed', ); } let argsNode: ts.Expression | undefined; let optsNode: ts.Expression | undefined; - if (expr.arguments.length === 3) { - argsNode = expr.arguments[1]; - optsNode = expr.arguments[2]; - } else if (expr.arguments.length === 2) { - const second = expr.arguments[1]!; + if (expr.arguments.nodes.length === 3) { + argsNode = expr.arguments.nodes[1]; + optsNode = expr.arguments.nodes[2]; + } else if (expr.arguments.nodes.length === 2) { + const second = expr.arguments.nodes[1]!; const secondType = lowerer.typeOf(second); const mapped = lowerer.mapTypeOf(secondType); if ( @@ -622,7 +622,7 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr }; if (optsNode && ts.isObjectLiteralExpression(optsNode)) { - for (const property of optsNode.properties) { + for (const property of optsNode.properties.nodes) { const member = optionMember(property); if (!member) { lowerer.noLowering( @@ -651,14 +651,14 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr break; case "stdio": { if (ts.isArrayLiteralExpression(member.value)) { - if (member.value.elements.length !== 4) { + if (member.value.elements.nodes.length !== 4) { lowerer.noLowering( "fork with this stdio tuple", member.value, 'use [stdin, stdout, stderr, "ipc"] with exactly four literal entries', ); } - const ipc = member.value.elements[3]!; + const ipc = member.value.elements.nodes[3]!; const ipcType = lowerer.typeOf(ipc); if (!ipcType.isStringLiteralType() || ipcType.value !== "ipc") { lowerer.noLowering( @@ -667,9 +667,9 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr 'the fourth stdio entry must be "ipc"', ); } - inMode = stdioMode(member.value.elements[0]!, "stdin"); - outMode = stdioMode(member.value.elements[1]!, "stdout"); - errMode = stdioMode(member.value.elements[2]!, "stderr"); + inMode = stdioMode(member.value.elements.nodes[0]!, "stdin"); + outMode = stdioMode(member.value.elements.nodes[1]!, "stdout"); + errMode = stdioMode(member.value.elements.nodes[2]!, "stderr"); } else { const mode = stdioMode(member.value, "scalar"); inMode = outMode = errMode = mode; @@ -743,20 +743,20 @@ export function lowerExecFileCall( ): IrExpr { const what = shell ? "exec" : "execFile"; if ( - expr.arguments.some(ts.isSpreadElement) || - expr.arguments.length < 2 || - expr.arguments.length > (shell ? 3 : 4) + expr.arguments.nodes.some(ts.isSpreadElement) || + expr.arguments.nodes.length < 2 || + expr.arguments.nodes.length > (shell ? 3 : 4) ) { lowerer.noLowering( - `${what} with ${expr.arguments.length} arguments`, + `${what} with ${expr.arguments.nodes.length} arguments`, expr, shell ? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)" : "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)", ); } - const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const callbackNode = expr.arguments[expr.arguments.length - 1]!; + const first = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const callbackNode = expr.arguments.nodes[expr.arguments.nodes.length - 1]!; // exec: /bin/sh -c , execSync's lowering (the runtime reads the // command line back from argv[1] for failure messages); execFile: the // file and its args list. @@ -768,9 +768,15 @@ export function lowerExecFileCall( type: arrayOf(STRING), loc, } - : lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc); + : lowerChildArgsArg( + lowerer, + expr.arguments.nodes.length >= 3 ? expr.arguments.nodes[1] : undefined, + loc, + ); const optionsNode = - expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined; + expr.arguments.nodes.length === (shell ? 3 : 4) + ? expr.arguments.nodes[shell ? 1 : 2] + : undefined; if (optionsNode) { const options = optionsNode; if (!ts.isObjectLiteralExpression(options)) { @@ -792,7 +798,7 @@ export function lowerExecFileCall( ].includes(node.operatorToken.kind) && pureNumber(node.left) && pureNumber(node.right)); - for (const property of options.properties) { + for (const property of options.properties.nodes) { const member = optionMember(property); if (!member) lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 6de345dfe..0cd25cde6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -197,7 +197,7 @@ export function lowerZlibStreamMethodCall( ): IrExpr { const loc = locOf(call); const member = access.name.text; - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement)) lowerer.noLowering(`${member} with spread arguments`, call); const isFunc = (node: ts.Expression): boolean => lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "func"; @@ -304,25 +304,25 @@ export function lowerPromisifiedZlibCall( member: string, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`the promisified zlib.${member} with spread arguments`, expr); } - if (expr.arguments.length !== 1 && expr.arguments.length !== 2) { + if (expr.arguments.nodes.length !== 1 && expr.arguments.nodes.length !== 2) { lowerer.noLowering( - `the promisified zlib.${member} with ${expr.arguments.length} arguments`, + `the promisified zlib.${member} with ${expr.arguments.nodes.length} arguments`, expr, `the supported forms are ${member}Async(data) and ${member}Async(data, options)`, ); } - const input = zlibInput(lowerer, expr.arguments[0]!, loc); + const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc); return { kind: "libCall", fn: input.dyn ? "zlib.promiseDyn" : "zlib.promise", args: [ numLit(ZLIB_ONE_SHOT_MODES[member]!, loc), input.value, - expr.arguments[1] !== undefined - ? zlibOptionsValue(lowerer, expr.arguments[1], `zlib.${member}`, loc) + expr.arguments.nodes[1] !== undefined + ? zlibOptionsValue(lowerer, expr.arguments.nodes[1], `zlib.${member}`, loc) : dynUndefinedExpr(loc), ], type: { kind: "promise", inner: BYTES_U8 }, @@ -346,19 +346,26 @@ export function lowerZlibModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`zlib.${bi.member} with spread arguments`, expr); } const streamMode = ZLIB_STREAM_FACTORIES[bi.member]; if (streamMode !== undefined) { - return lowerZlibStreamNew(lowerer, expr.arguments, streamMode, `zlib.${bi.member}`, expr, loc); + return lowerZlibStreamNew( + lowerer, + expr.arguments.nodes, + streamMode, + `zlib.${bi.member}`, + expr, + loc, + ); } const syncFn = ZLIB_SYNC_FNS[bi.member]; if (syncFn !== undefined) { - if (expr.arguments.length === 0 || expr.arguments.length > 2) { - lowerer.noLowering(`${bi.member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length === 0 || expr.arguments.nodes.length > 2) { + lowerer.noLowering(`${bi.member} with ${expr.arguments.nodes.length} arguments`, expr); } - const input = zlibInput(lowerer, expr.arguments[0]!, loc); + const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc); if (input.dyn) { return { kind: "libCall", @@ -366,8 +373,8 @@ export function lowerZlibModuleCall( args: [ numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc), input.value, - expr.arguments[1] !== undefined - ? zlibOptionsValue(lowerer, expr.arguments[1], `zlib.${bi.member}`, loc) + expr.arguments.nodes[1] !== undefined + ? zlibOptionsValue(lowerer, expr.arguments.nodes[1], `zlib.${bi.member}`, loc) : dynUndefinedExpr(loc), ], type: BYTES_U8, @@ -375,15 +382,15 @@ export function lowerZlibModuleCall( }; } if ( - expr.arguments.length === 2 && + expr.arguments.nodes.length === 2 && (bi.member === "deflateSync" || bi.member === "deflateRawSync" || bi.member === "gzipSync") ) { - const options = expr.arguments[1]!; + const options = expr.arguments.nodes[1]!; // A lone literal level keeps the direct compression entry (build-time // compression in the native emitters uses it); other options take the // runtime option ladder below. - if (ts.isObjectLiteralExpression(options) && options.properties.length === 1) { - const prop = options.properties[0]!; + if (ts.isObjectLiteralExpression(options) && options.properties.nodes.length === 1) { + const prop = options.properties.nodes[0]!; if ( ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) && @@ -416,14 +423,14 @@ export function lowerZlibModuleCall( } } } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return { kind: "libCall", fn: "zlib.syncOpts", args: [ numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc), input.value, - zlibOptionsValue(lowerer, expr.arguments[1]!, `zlib.${bi.member}`, loc), + zlibOptionsValue(lowerer, expr.arguments.nodes[1]!, `zlib.${bi.member}`, loc), ], type: BYTES_U8, loc, @@ -439,19 +446,19 @@ export function lowerZlibModuleCall( } const callbackFn = ZLIB_CALLBACK_FNS[bi.member]; if (callbackFn !== undefined) { - if (expr.arguments.length !== 2 && expr.arguments.length !== 3) { + if (expr.arguments.nodes.length !== 2 && expr.arguments.nodes.length !== 3) { lowerer.noLowering( - `${bi.member} with ${expr.arguments.length} arguments`, + `${bi.member} with ${expr.arguments.nodes.length} arguments`, expr, `${bi.member}(data, callback) and ${bi.member}(data, options, callback) are supported`, ); } - const input = zlibInput(lowerer, expr.arguments[0]!, loc); + const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc); const dynData = input.dyn; - if (expr.arguments.length === 3 || dynData) { + if (expr.arguments.nodes.length === 3 || dynData) { // (data, options, callback): Node validates the options before the // callback and the input, so the runtime ladder runs first. - const withOptions = expr.arguments.length === 3; + const withOptions = expr.arguments.nodes.length === 3; return { kind: "libCall", fn: dynData ? "zlib.cbOptsDyn" : "zlib.cbOpts", @@ -459,9 +466,9 @@ export function lowerZlibModuleCall( numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc), input.value, withOptions - ? zlibOptionsValue(lowerer, expr.arguments[1]!, `zlib.${bi.member}`, loc) + ? zlibOptionsValue(lowerer, expr.arguments.nodes[1]!, `zlib.${bi.member}`, loc) : dynUndefinedExpr(loc), - errorFirstBytesCallback(lowerer, expr.arguments[withOptions ? 2 : 1]!, "zlib"), + errorFirstBytesCallback(lowerer, expr.arguments.nodes[withOptions ? 2 : 1]!, "zlib"), ], type: VOID, loc, @@ -470,15 +477,15 @@ export function lowerZlibModuleCall( return { kind: "libCall", fn: callbackFn, - args: [input.value, errorFirstBytesCallback(lowerer, expr.arguments[1]!, "zlib")], + args: [input.value, errorFirstBytesCallback(lowerer, expr.arguments.nodes[1]!, "zlib")], type: VOID, loc, }; } if (bi.member === "crc32") { - if (expr.arguments.length < 1 || expr.arguments.length > 2) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `zlib.crc32 with ${expr.arguments.length} arguments`, + `zlib.crc32 with ${expr.arguments.nodes.length} arguments`, expr, "crc32(data[, initialValue]) is supported", ); @@ -487,9 +494,9 @@ export function lowerZlibModuleCall( kind: "libCall", fn: "zlib.crc32", args: [ - lowerBuiltinByteInput(lowerer, expr.arguments[0]!, loc, "zlib"), - expr.arguments[1] - ? lowerer.lowerExprExpecting(expr.arguments[1]!, F64) + lowerBuiltinByteInput(lowerer, expr.arguments.nodes[0]!, loc, "zlib"), + expr.arguments.nodes[1] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, F64) : { kind: "numLit", value: 0, type: F64, loc }, ], type: F64, diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index 0325a0ef7..dd9fcf987 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -88,7 +88,7 @@ export function builtinConstantsDestructureDecl( const module = builtinConstantsModuleOf(lowerer, init); if (module === null) return false; const table = BUILTIN_CONSTANTS_TABLES[module]!.table; - return nameNode.elements.every( + return nameNode.elements.nodes.every( (el) => !el.dotDotDotToken && el.initializer === undefined && diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index bfac6128f..575748ef6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -47,17 +47,17 @@ export function lowerCryptoComposedCall( // "hex"/"base64" encoding. Anything else falls through (null): bare // randomBytes lowers to a real Buffer through the crypto table, and // the .toString rides the ordinary Buffer method lowering. - if (recv.arguments.length !== 1) return null; - const encNode = call.arguments[0]; + if (recv.arguments.nodes.length !== 1) return null; + const encNode = call.arguments.nodes[0]; const encT = encNode ? lowerer.typeOf(encNode) : undefined; if ( - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || !encT?.isStringLiteralType() || (encT.value !== "hex" && encT.value !== "base64") ) { return null; } - const size = lowerer.lowerExprExpecting(recv.arguments[0]!, F64); + const size = lowerer.lowerExprExpecting(recv.arguments.nodes[0]!, F64); const enc = lowerer.lowerExprExpecting(encNode!, STRING); return { kind: "libCall", @@ -96,28 +96,30 @@ function lowerHashDigestChain( : null; if (!bi || bi.module !== "crypto" || bi.member !== "createHash") return null; const loc = locOf(call); - const algT = chCall.arguments.length === 1 ? lowerer.typeOf(chCall.arguments[0]!) : undefined; + const algT = + chCall.arguments.nodes.length === 1 ? lowerer.typeOf(chCall.arguments.nodes[0]!) : undefined; if (!algT?.isStringLiteralType() || (algT.value !== "sha256" && algT.value !== "sha1")) return null; - if (updateCall.arguments.length !== 1) { + if (updateCall.arguments.nodes.length !== 1) { return null; } - const encT = call.arguments.length === 1 ? lowerer.typeOf(call.arguments[0]!) : undefined; + const encT = + call.arguments.nodes.length === 1 ? lowerer.typeOf(call.arguments.nodes[0]!) : undefined; if (!encT?.isStringLiteralType() || (encT.value !== "hex" && encT.value !== "base64")) { return null; } // alg and enc are proven literals (fenced above), so lowering them // out of source position observes nothing; the data lowers between // them in its own source order. - const alg = lowerer.lowerExprExpecting(chCall.arguments[0]!, STRING); + const alg = lowerer.lowerExprExpecting(chCall.arguments.nodes[0]!, STRING); // The data picks the runtime entry by its static type, the // fileURLToPath convention: strings hash their UTF-8 bytes (Node's // default input encoding), Buffers/typed arrays hash their bytes. - const dataNode = updateCall.arguments[0]!; + const dataNode = updateCall.arguments.nodes[0]!; const dataIr = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); if (dataIr?.kind === "bytes") { const data = cryptoByteView(lowerer, dataNode, lowerer.lowerExpr(dataNode)); - const enc = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const enc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "crypto.hashDigestBytes", @@ -128,7 +130,7 @@ function lowerHashDigestChain( } if (dataIr?.kind === "string") { const data = lowerer.lowerExprExpecting(dataNode, STRING); - const enc = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const enc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "crypto.hashDigestStr", @@ -257,7 +259,7 @@ export function lowerCryptoModuleCall( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "crypto") return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.${bi.member} with spread arguments`, expr); } @@ -465,8 +467,8 @@ export function lowerCryptoModuleCall( let options = args[2]; // An options literal holding only a literal string-digest // outputEncoding is that encoding; evaluating it observes nothing. - if (options && ts.isObjectLiteralExpression(options) && options.properties.length === 1) { - const property = options.properties[0]!; + if (options && ts.isObjectLiteralExpression(options) && options.properties.nodes.length === 1) { + const property = options.properties.nodes[0]!; if ( ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && @@ -820,8 +822,8 @@ export function lowerCryptoModuleCall( }; const list = own(LISTS, bi.member); if (bi.member !== "getFips" && list === undefined) return null; - if (expr.arguments.length !== 0) { - lowerer.noLowering(`crypto.${bi.member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 0) { + lowerer.noLowering(`crypto.${bi.member} with ${expr.arguments.nodes.length} arguments`, expr); } if (bi.member === "getFips") { return { kind: "numLit", value: 0, type: F64, loc }; @@ -916,7 +918,7 @@ export function lowerCipherMethodCall( "update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not", ); } - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`Cipheriv/Decipheriv.${name} with spread arguments`, call); const loc = locOf(call); return { @@ -924,7 +926,7 @@ export function lowerCipherMethodCall( recv: lowerer.lowerExprExpecting(access.expression, DYN), method: name, calleeName: v8CalleeText(access), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -946,9 +948,9 @@ export function lowerCryptoHashMethodCall( // Hmac has no copy(): Node reads the member as undefined and throws a // TypeError once the arguments are evaluated. if (receiverType.kind === "cryptoHmac" && name === "copy") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("Hmac.copy with spread arguments", call); - const evaluated = [access.expression, ...call.arguments].map((node): IrStmt => ({ + const evaluated = [access.expression, ...call.arguments.nodes].map((node): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc, @@ -965,19 +967,19 @@ export function lowerCryptoHashMethodCall( const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, receiverType); const hash = receiverType.kind === "cryptoHash"; const label = hash ? "Hash" : "Hmac"; - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${label}.${name} with spread arguments`, call); if (name === "update") { - if (call.arguments.length < 1 || call.arguments.length > 2) { + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { lowerer.noLowering( - `${label}.update with ${call.arguments.length} arguments`, + `${label}.update with ${call.arguments.nodes.length} arguments`, call, "update(data[, inputEncoding]) is supported", ); } const target = receiver(); - const dataNode = call.arguments[0]!; - const encodingNode = call.arguments[1]; + const dataNode = call.arguments.nodes[0]!; + const encodingNode = call.arguments.nodes[1]; const dataType = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); // Indexed reads can retain optional storage after a nullish fallback // or narrowing. Use the proven argument type through a checked coercion. @@ -1032,14 +1034,14 @@ export function lowerCryptoHashMethodCall( }; } if (name === "digest") { - if (call.arguments.length > 1) { + if (call.arguments.nodes.length > 1) { lowerer.noLowering( - `${label}.digest with ${call.arguments.length} arguments`, + `${label}.digest with ${call.arguments.nodes.length} arguments`, call, "digest([outputEncoding]) is supported", ); } - const encodingNode = call.arguments[0]; + const encodingNode = call.arguments.nodes[0]; if (encodingNode === undefined) { return { kind: "libCall", @@ -1078,13 +1080,13 @@ export function lowerCryptoHashMethodCall( }; } if (name === "copy" && hash) { - if (call.arguments.length > 1) + if (call.arguments.nodes.length > 1) lowerer.noLowering( - `Hash.copy with ${call.arguments.length} arguments`, + `Hash.copy with ${call.arguments.nodes.length} arguments`, call, "copy([options]) is supported", ); - if (call.arguments.length === 0) + if (call.arguments.nodes.length === 0) return { kind: "libCall", fn: "crypto.hashCopy", @@ -1095,7 +1097,7 @@ export function lowerCryptoHashMethodCall( return { kind: "libCall", fn: "crypto.hashCopyOpts", - args: [receiver(), lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [receiver(), lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], type: CRYPTOHASH_T, loc, }; @@ -1138,7 +1140,7 @@ export function lowerPromisifiedCryptoCall( member: string, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`the promisified crypto.${member} with spread arguments`, expr); } diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 9f0ab7a29..32fe09ab3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -53,8 +53,8 @@ export function lowerDateCall( if (call.questionDotToken || access.questionDotToken) return null; const loc = locOf(call); if (lowerer.stdlibGlobalMember(access, "Date") === "now") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`Date.now with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call); } return { kind: "libCall", fn: "date.now", args: [], type: F64, loc }; } @@ -62,10 +62,10 @@ export function lowerDateCall( // new Date(dateString).getTime(); keep their NaN and TimeClip behavior // identical for the ISO timestamps used by CLI applications. if (lowerer.stdlibGlobalMember(access, "Date") === "parse") { - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { - lowerer.noLowering(`Date.parse with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { + lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call); } - const input = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const input = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "date.parse", args: [input], type: F64, loc }; } // Date.UTC(year[, month[, date[, hours[, minutes[, seconds[, ms]]]]]]): @@ -75,16 +75,16 @@ export function lowerDateCall( // argument to number, so the seven-f64 ABI is exact. if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") { if ( - call.arguments.length < 1 || - call.arguments.length > 7 || - call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 7 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { - lowerer.noLowering(`Date.UTC with ${call.arguments.length} arguments`, call); + lowerer.noLowering(`Date.UTC with ${call.arguments.nodes.length} arguments`, call); } const defaults = [0, 0, 1, 0, 0, 0, 0]; // year is always present (arity ≥ 1) const args: IrExpr[] = []; for (let i = 0; i < 7; i++) { - const a = call.arguments[i]; + const a = call.arguments.nodes[i]; args.push( a !== undefined ? lowerer.lowerExprExpecting(a, F64) @@ -97,7 +97,7 @@ export function lowerDateCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; if (name === "toJSON") return lowerDateToJson(lowerer, call, access); - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); } const raw = lowerer.lowerExpr(access.expression); @@ -147,8 +147,8 @@ function lowerDateToJson( access: ts.PropertyAccessExpression, ): IrExpr { const loc = locOf(call); - if (call.arguments.length > 1 || call.arguments.some((a) => ts.isSpreadElement(a))) - lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some((a) => ts.isSpreadElement(a))) + lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.nodes.length} arguments`, call); const raw = lowerer.lowerExpr(access.expression); const receiver: IrExpr = raw.type.kind === "dyn" @@ -158,7 +158,7 @@ function lowerDateToJson( lowerer.badType(access.expression, lowerer.typeOf(access.expression)); const date = lowerer.declareHiddenLocal("%date", receiver.type); const stmts: IrStmt[] = [{ kind: "varDecl", localId: date.id, init: receiver, loc }]; - const key = call.arguments[0]; + const key = call.arguments.nodes[0]; if (key) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(key), loc }); const value: IrExpr = { kind: "varRef", localId: date.id, type: receiver.type, loc }; const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 469a66ace..fb1c92257 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -22,7 +22,7 @@ export function lowerDisposableStackNew(lowerer: Lowerer, expr: ts.NewExpression if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "DisposableStack") return null; const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - if ((expr.arguments ?? []).length !== 0) return null; + if ((expr.arguments?.nodes ?? []).length !== 0) return null; return { kind: "libCall", fn: "disposable.stackNew", args: [], type: DYN, loc: locOf(expr) }; } @@ -49,8 +49,8 @@ export function lowerDisposableStackUseCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "use" || call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (access.name.text !== "use" || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; if (ts.isSpreadElement(argNode)) return null; if (!isStdlibDisposableStack(lowerer, access.expression)) return null; const valueT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); diff --git a/packages/compiler/src/frontend/lowering/builtins/environment.ts b/packages/compiler/src/frontend/lowering/builtins/environment.ts index 26c589dad..7d3e7ff91 100644 --- a/packages/compiler/src/frontend/lowering/builtins/environment.ts +++ b/packages/compiler/src/frontend/lowering/builtins/environment.ts @@ -78,12 +78,12 @@ export function lowerProcessEnvGet( } export function lowerProcessLoadEnvFile(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("process.loadEnvFile with spread or extra arguments", call); } const loc = locOf(call); - const path: IrExpr = call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + const path: IrExpr = call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); const load: IrExpr = { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 50a97cead..1572aa6ab 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -209,8 +209,8 @@ export function lowerErrorIsErrorCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Error") !== "isError") return null; - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const caughtLocal = lowerer.caughtLocalOf(argNode); if (caughtLocal) { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index d6864a2d9..05028e1df 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -38,12 +38,12 @@ export function lowerFsCallbackCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("filesystem callback call with spread arguments", expr); const timestamp = ["utimes", "futimes", "lutimes"].includes(bi.member); const args: IrExpr = { kind: "dynArrLit", - elems: expr.arguments.map((arg, index) => + elems: expr.arguments.nodes.map((arg, index) => timestamp && (index === 1 || index === 2) ? lowerFsTimestampValue(lowerer, arg, loc) : lowerer.lowerExprExpecting(arg, DYN), @@ -71,11 +71,11 @@ export function lowerFsLinkOrStatfsCall( const symbolic = bi.member === "symlinkSync" || bi.member === "symlink"; const reading = bi.member === "readlinkSync" || bi.member === "readlink"; const arity = symbolic ? 3 : 2; - if (expr.arguments.length > arity || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > arity || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); const args = Array.from({ length: arity }, (_, index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); if (reading) { @@ -117,11 +117,11 @@ export function lowerFsCpCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); const args = [0, 1, 2].map((index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; @@ -134,18 +134,18 @@ export function lowerFsTimestampCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); - const path = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const path = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); return { kind: "libCall", fn: fn.fn, args: [ path, - lowerFsTimestampValue(lowerer, expr.arguments[1], loc), - lowerFsTimestampValue(lowerer, expr.arguments[2], loc), + lowerFsTimestampValue(lowerer, expr.arguments.nodes[1], loc), + lowerFsTimestampValue(lowerer, expr.arguments.nodes[2], loc), ], type: fn.result, loc, @@ -163,14 +163,14 @@ export function lowerFsVectorOrTruncateCall( const vector = bi.member !== "ftruncateSync"; const required = vector ? 2 : 1; if ( - expr.arguments.length < required || - expr.arguments.length > required + 1 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < required || + expr.arguments.nodes.length > required + 1 || + expr.arguments.nodes.some(ts.isSpreadElement) ) lowerer.noLowering(`${name} with this argument shape`, expr); - const args = [lowerer.lowerExprExpecting(expr.arguments[0]!, F64)]; - if (vector) args.push(lowerer.lowerExprExpecting(expr.arguments[1]!, arrayOf(BYTES_U8))); - const optional = expr.arguments[required]; + const args = [lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64)]; + if (vector) args.push(lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, arrayOf(BYTES_U8))); + const optional = expr.arguments.nodes[required]; args.push( optional ? lowerBuiltinOptionalDefault(lowerer, optional, F64, numLit(vector ? -1 : 0, loc), vector) @@ -187,7 +187,7 @@ export function lowerFsNumericOpenCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("openSync with numeric flags and this argument shape", expr); } const bits: Record = { @@ -223,17 +223,17 @@ export function lowerFsNumericOpenCall( } return null; }; - const mask = flagsOf(expr.arguments[1]!); + const mask = flagsOf(expr.arguments.nodes[1]!); if (mask === null) { lowerer.noLowering( "openSync with computed numeric flags", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "use an inline bitwise OR of fs.constants.O_RDONLY/O_WRONLY/O_RDWR/O_CREAT/O_EXCL/O_NOFOLLOW/O_NONBLOCK/O_TRUNC/O_APPEND", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const mode = expr.arguments[2] - ? lowerer.lowerExprExpecting(expr.arguments[2]!, F64) + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const mode = expr.arguments.nodes[2] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64) : ({ kind: "numLit", value: 0o666, type: F64, loc } satisfies IrExpr); return { kind: "libCall", @@ -254,24 +254,24 @@ export function lowerFsPromisesOpenCall( loc: SrcLoc, ): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `fs.promises.open with ${expr.arguments.length} arguments`, + `fs.promises.open with ${expr.arguments.nodes.length} arguments`, expr, "use open(path[, stringFlags[, numericMode]])", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const defaultFlags: IrExpr = { kind: "strLit", value: "r", type: STRING, loc }; const defaultMode: IrExpr = { kind: "numLit", value: 0o666, type: F64, loc }; - const flags = expr.arguments[1] - ? lowerBuiltinOptionalDefault(lowerer, expr.arguments[1]!, STRING, defaultFlags) + const flags = expr.arguments.nodes[1] + ? lowerBuiltinOptionalDefault(lowerer, expr.arguments.nodes[1]!, STRING, defaultFlags) : defaultFlags; - const mode = expr.arguments[2] - ? lowerBuiltinOptionalDefault(lowerer, expr.arguments[2]!, F64, defaultMode) + const mode = expr.arguments.nodes[2] + ? lowerBuiltinOptionalDefault(lowerer, expr.arguments.nodes[2]!, F64, defaultMode) : defaultMode; return { kind: "libCall", @@ -292,16 +292,16 @@ export function lowerFsRenameCallbackCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (expr.arguments.length !== 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `rename with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `rename with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, "the supported form is rename(oldPath, newPath, callback)", ); } - const oldPath = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const newPath = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); - let callback = lowerer.lowerExpr(expr.arguments[2]!); + const oldPath = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const newPath = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); + let callback = lowerer.lowerExpr(expr.arguments.nodes[2]!); // JS/checkJs callback values may arrive as checked-dynamic callables. // Adapt them to the one error-first slot; the runtime passes a dyn // Error on failure and null on success through the emitted thunk. @@ -310,7 +310,7 @@ export function lowerFsRenameCallbackCall( kind: "dynCheck", value: callback, type: funcOf([DYN], VOID), - loc: locOf(expr.arguments[2]!), + loc: locOf(expr.arguments.nodes[2]!), }; } let callbackOk = callback.type.kind === "func" && callback.type.params.length <= 1; @@ -337,7 +337,7 @@ export function lowerFsRenameCallbackCall( if (!callbackOk) { lowerer.unsupported( "SC1090", - expr.arguments[2]!, + expr.arguments.nodes[2]!, "fs.rename callbacks must accept at most one Error | null parameter", ); } @@ -345,7 +345,7 @@ export function lowerFsRenameCallbackCall( // void callback is expected; Node ignores that value. Normalize the // closure to the runtime's void callback ABI after validating its // error-first parameter shape. - callback = voidizedCallback(lowerer, callback, locOf(expr.arguments[2]!)); + callback = voidizedCallback(lowerer, callback, locOf(expr.arguments.nodes[2]!)); return { kind: "libCall", fn: "fs.renameCb", @@ -366,17 +366,17 @@ export function lowerFsReadSyncCall( const supported = "use readSync(fd, buffer), readSync(fd, buffer, { offset?, length?, position? }), or readSync(fd, buffer, offset, length[, position]); bigint positions and non-literal options objects have no lowering"; if ( - expr.arguments.length < 2 || - expr.arguments.length > 5 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 2 || + expr.arguments.nodes.length > 5 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `readSync with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `readSync with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, supported, ); } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return lowerFsSyncBufferWindow( lowerer, expr, @@ -386,8 +386,8 @@ export function lowerFsReadSyncCall( supported, ); } - if (expr.arguments.length === 3) { - const optionsNode = expr.arguments[2]!; + if (expr.arguments.nodes.length === 3) { + const optionsNode = expr.arguments.nodes[2]!; let valueNode = optionsNode; while (ts.isParenthesizedExpression(valueNode)) valueNode = valueNode.expression; if (ts.isObjectLiteralExpression(valueNode) || valueNode.kind === ts.SyntaxKind.NullKeyword) { @@ -403,12 +403,12 @@ export function lowerFsReadSyncCall( lowerer.noLowering("readSync with a non-literal options argument", optionsNode, supported); } const args: IrExpr[] = [ - lowerer.lowerExprExpecting(expr.arguments[0]!, F64), - lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8), - lowerer.lowerExprExpecting(expr.arguments[2]!, F64), - lowerer.lowerExprExpecting(expr.arguments[3]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8), + lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[3]!, F64), ]; - const positionNode = expr.arguments[4]; + const positionNode = expr.arguments.nodes[4]; let positionValueNode = positionNode; while (positionValueNode && ts.isParenthesizedExpression(positionValueNode)) { positionValueNode = positionValueNode.expression; @@ -441,14 +441,14 @@ export function lowerFsWriteSyncCall( ): IrExpr { const supported = 'use writeSync(fd, buffer[, offset[, length[, position]]]), writeSync(fd, buffer, { offset?, length?, position? }), or writeSync(fd, string[, position[, "utf8"]]); non-literal options objects, bigint positions, and other encodings have no lowering'; - if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length < 2) { + if (expr.arguments.nodes.some(ts.isSpreadElement) || expr.arguments.nodes.length < 2) { lowerer.noLowering( - `writeSync with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `writeSync with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, supported, ); } - const dataNode = expr.arguments[1]!; + const dataNode = expr.arguments.nodes[1]!; const dataType = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); const position = (node: ts.Expression | undefined): IrExpr => { let valueNode = node; @@ -474,7 +474,7 @@ export function lowerFsWriteSyncCall( "byte writes take Uint8Array/Buffer data", ); } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return lowerFsSyncBufferWindow( lowerer, expr, @@ -484,8 +484,8 @@ export function lowerFsWriteSyncCall( supported, ); } - if (expr.arguments.length === 3) { - const thirdNode = expr.arguments[2]!; + if (expr.arguments.nodes.length === 3) { + const thirdNode = expr.arguments.nodes[2]!; let valueNode = thirdNode; while (ts.isParenthesizedExpression(valueNode)) valueNode = valueNode.expression; if (ts.isObjectLiteralExpression(valueNode) || valueNode.kind === ts.SyntaxKind.NullKeyword) { @@ -514,37 +514,37 @@ export function lowerFsWriteSyncCall( supported, ); } - if (expr.arguments.length !== 4 && expr.arguments.length !== 5) { + if (expr.arguments.nodes.length !== 4 && expr.arguments.nodes.length !== 5) { lowerer.noLowering( - `writeSync of Buffer data with ${expr.arguments.length} arguments`, + `writeSync of Buffer data with ${expr.arguments.nodes.length} arguments`, expr, supported, ); } - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); return { kind: "libCall", fn: "fs.writeSync", args: [ fd, lowerer.lowerExprExpecting(dataNode, BYTES_U8), - lowerer.lowerExprExpecting(expr.arguments[2]!, F64), - lowerer.lowerExprExpecting(expr.arguments[3]!, F64), - position(expr.arguments[4]), + lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[3]!, F64), + position(expr.arguments.nodes[4]), ], type: F64, loc, }; } if (dataType?.kind === "string") { - if (expr.arguments.length > 4) { + if (expr.arguments.nodes.length > 4) { lowerer.noLowering( - `writeSync of string data with ${expr.arguments.length} arguments`, + `writeSync of string data with ${expr.arguments.nodes.length} arguments`, expr, supported, ); } - const encNode = expr.arguments[3]; + const encNode = expr.arguments.nodes[3]; let enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; if (encNode !== undefined) { const encType = lowerer.typeOf(encNode); @@ -559,11 +559,16 @@ export function lowerFsWriteSyncCall( // a call/getter whose type is the accepted literal cannot vanish. enc = lowerer.lowerExprExpecting(encNode, STRING); } - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); return { kind: "libCall", fn: "fs.writeStrSync", - args: [fd, lowerer.lowerExprExpecting(dataNode, STRING), position(expr.arguments[2]), enc], + args: [ + fd, + lowerer.lowerExprExpecting(dataNode, STRING), + position(expr.arguments.nodes[2]), + enc, + ], type: F64, loc, }; @@ -585,22 +590,22 @@ export function lowerFsReadDescriptorCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); - if (expr.arguments.length === 1) { + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); + if (expr.arguments.nodes.length === 1) { return { kind: "libCall", fn: "fs.readFdSyncBytes", args: [fd], type: BYTES_U8, loc }; } - const encT = lowerer.typeOf(expr.arguments[1]!); + const encT = lowerer.typeOf(expr.arguments.nodes[1]!); if ( - expr.arguments.length !== 2 || + expr.arguments.nodes.length !== 2 || !(encT.isStringLiteralType() && (encT.value === "utf8" || encT.value === "utf-8")) ) { lowerer.noLowering( `readFileSync(fd) with a non-"utf8" encoding`, - expr.arguments[1] ?? expr, + expr.arguments.nodes[1] ?? expr, 'only utf8 reads are supported: readFileSync(fd, "utf8")', ); } - const enc = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); + const enc = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); return { kind: "libCall", fn: "fs.readFdSync", args: [fd, enc], type: STRING, loc }; } @@ -620,8 +625,9 @@ export function lowerFsEncodedRead( descriptor: boolean, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) return null; - const options = expr.arguments[1]!; + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) + return null; + const options = expr.arguments.nodes[1]!; const optionsType = lowerer.mapTypeOf(lowerer.typeOf(options)); if (optionsType?.kind !== "string" && optionsType?.kind !== "record") return null; if (optionsType.kind === "record") { @@ -634,7 +640,7 @@ export function lowerFsEncodedRead( return null; } const sourceType = descriptor ? F64 : STRING; - const source = lowerer.lowerExprExpecting(expr.arguments[0]!, sourceType); + const source = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, sourceType); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -674,10 +680,11 @@ export function lowerFsPromisesEncodedRead( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) return null; + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) + return null; const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (result?.kind !== "promise" || result.inner.kind !== "string") return null; - const options = expr.arguments[1]!; + const options = expr.arguments.nodes[1]!; const optionsTsType = lowerer.typeOf(options); if ( optionsTsType.isStringLiteralType() && @@ -695,7 +702,7 @@ export function lowerFsPromisesEncodedRead( ) return null; } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -729,15 +736,15 @@ export function lowerFsOpendirCall( loc: SrcLoc, ): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 2 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 2 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { - lowerer.noLowering(`${member} with ${expr.arguments.length} arguments`, expr); + lowerer.noLowering(`${member} with ${expr.arguments.nodes.length} arguments`, expr); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); let bufferSize: IrExpr = { kind: "numLit", value: 32, type: F64, loc }; - const optsNode = expr.arguments[1]; + const optsNode = expr.arguments.nodes[1]; if (optsNode && !(ts.isIdentifier(optsNode) && optsNode.text === "undefined")) { const fence = (): never => lowerer.noLowering( @@ -746,7 +753,7 @@ export function lowerFsOpendirCall( "pass an object literal with a numeric bufferSize and a utf8 encoding; recursive walks and other encodings have no lowering", ); if (!ts.isObjectLiteralExpression(optsNode)) fence(); - for (const property of (optsNode as ts.ObjectLiteralExpression).properties) { + for (const property of (optsNode as ts.ObjectLiteralExpression).properties.nodes) { const option = optionMember(property); if (!option) fence(); else if (option.name === "bufferSize") @@ -775,7 +782,7 @@ export function lowerFsStreamCreate( ): IrExpr { return fsStreamCreateExpr( lowerer, - expr.arguments, + expr.arguments.nodes, member === "createReadStream", `fs.${member}`, expr, @@ -795,7 +802,7 @@ export function lowerFsMkdirSyncCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const optsNode = expr.arguments[1]!; + const optsNode = expr.arguments.nodes[1]!; const options = parseMkdirOptions(optsNode); if (!options) { lowerer.noLowering( @@ -805,7 +812,7 @@ export function lowerFsMkdirSyncCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; if (!recursive) { return mode @@ -833,7 +840,7 @@ export function lowerFsPromisesMkdirCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const optsNode = expr.arguments[1]!; + const optsNode = expr.arguments.nodes[1]!; const options = parseMkdirOptions(optsNode); if (!options) { lowerer.noLowering( @@ -843,7 +850,7 @@ export function lowerFsPromisesMkdirCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; const type: IrType = { kind: "promise", inner: VOID }; if (!recursive) { @@ -866,18 +873,18 @@ export function lowerFsPromisesMkdirCall( // historical rmOptsSync lowering unchanged. export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const opts = literalBoolOptions( - expr.arguments[1]!, + expr.arguments.nodes[1]!, ["recursive", "force"], ["maxRetries", "retryDelay"], ); if (opts === null) { lowerer.noLowering( "rmSync with an options argument beyond { recursive, force, maxRetries, retryDelay }", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "the recursive/force flags must be boolean literals; maxRetries/retryDelay are numbers; other options have no lowering", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const recursive = boolLit(opts.bools["recursive"] === true, loc); const force = boolLit(opts.bools["force"] === true, loc); @@ -916,7 +923,7 @@ export function lowerFsWriteBytesCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr | null { - const dataIr = lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!)); + const dataIr = lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!)); if (dataIr?.kind === "union") { const arms = lowerer.unions.get(dataIr.unionId)?.arms; if ( @@ -930,12 +937,12 @@ export function lowerFsWriteBytesCall( if (dataIr.elem !== "u8") { lowerer.noLowering( `${bi.member} of '${lowerer.fmt(dataIr)}' data`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, "byte writes take Uint8Array/Buffer data", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const data = lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8); if (bi.module === "fs/promises") { return { kind: "libCall", @@ -963,7 +970,7 @@ export function lowerFsWriteOptionsCall( loc: SrcLoc, promiseWriteOptions: boolean, ): IrExpr { - const optsNode = expr.arguments[2]!; + const optsNode = expr.arguments.nodes[2]!; const operation = promiseWriteOptions ? "fs.promises.writeFile" : "writeFileSync"; const plainFn: IrLibFn = promiseWriteOptions ? "fsp.writeFile" : "fs.writeFileSync"; const modeFn: IrLibFn = promiseWriteOptions ? "fsp.writeFileMode" : "fs.writeFileModeSync"; @@ -974,8 +981,8 @@ export function lowerFsWriteOptionsCall( // values in object-literal order before issuing the write; otherwise a // call/getter statically typed as the accepted utf8 literal can vanish. const finishWrite = (optionValues: WriteOptionValue[]): IrExpr => { - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const data = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); const pathLocal = lowerer.declareHiddenLocal("%writePath", STRING); const dataLocal = lowerer.declareHiddenLocal("%writeData", STRING); const stmts: IrStmt[] = [ @@ -1021,7 +1028,7 @@ export function lowerFsWriteOptionsCall( if ( t.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8") && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "string" + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" ) { return finishWrite([{ kind: "effect", value: lowerer.lowerExprExpecting(optsNode, STRING) }]); } @@ -1029,7 +1036,7 @@ export function lowerFsWriteOptionsCall( const optionValues: WriteOptionValue[] = []; let ok = ts.isObjectLiteralExpression(optsNode); if (ok) { - for (const p of (optsNode as ts.ObjectLiteralExpression).properties) { + for (const p of (optsNode as ts.ObjectLiteralExpression).properties.nodes) { const m = optionMember(p); if (!m) { ok = false; @@ -1066,7 +1073,7 @@ export function lowerFsWriteOptionsCall( } } } - if (!ok || lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind !== "string") { + if (!ok || lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind !== "string") { lowerer.noLowering( `${operation} with 3 arguments`, optsNode, @@ -1089,20 +1096,20 @@ export function lowerFsReadPathCall( // non-string paths throw ERR_INVALID_ARG_TYPE where the dynCheck // throws its path-annotated TypeError. The fd forms (a number // argument) and literal-utf8 reads keep their existing lowerings. - const pathV = lowerer.lowerExpr(expr.arguments[0]!); + const pathV = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (pathV.type.kind === "string" || pathV.type.kind === "dyn") { const pathArg: IrExpr = pathV.type.kind === "dyn" ? { kind: "dynCheck", value: pathV, type: STRING, loc } : pathV; - if (expr.arguments.length === 1) { + if (expr.arguments.nodes.length === 1) { return { kind: "libCall", fn: "fs.readFileSyncBuf", args: [pathArg], type: BYTES_U8, loc }; } // Runtime nullish and empty encodings read Buffers, supported names // read strings, and invalid names throw ERR_INVALID_ARG_VALUE. A // non-dyn encoding falls through to the literal-utf8 lowering // below (the discarded probe IR never emits). - const encT = lowerer.typeOf(expr.arguments[1]!); + const encT = lowerer.typeOf(expr.arguments.nodes[1]!); if (!(encT.isStringLiteralType() && (encT.value === "utf8" || encT.value === "utf-8"))) { - const enc = lowerer.lowerExpr(expr.arguments[1]!); + const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); if (enc.type.kind === "dyn") { return { kind: "libCall", @@ -1125,7 +1132,7 @@ function parseMkdirOptions( if (!ts.isObjectLiteralExpression(node)) return null; let recursive = false; let modeNode: ts.Expression | null = null; - for (const property of node.properties) { + for (const property of node.properties.nodes) { const member = optionMember(property); if (!member) return null; if (member.name === "recursive") { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index a091094de..3fbea04aa 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -48,8 +48,8 @@ export function lowerFsSyncBufferWindow( source: FsSyncBufferWindowSource, supported: string, ): IrExpr { - const fdValue = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); - const bufferValue = lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8); + const fdValue = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); + const bufferValue = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8); const fdLocal = lowerer.declareHiddenLocal("%fsSyncFd", F64); const bufferLocal = lowerer.declareHiddenLocal("%fsSyncBuffer", BYTES_U8); const stmts: IrStmt[] = [ @@ -88,7 +88,7 @@ export function lowerFsSyncBufferWindow( if (!ts.isObjectLiteralExpression(source.node)) { lowerer.noLowering(`${fn} with a non-literal options argument`, source.node, supported); } - for (const property of (source.node as ts.ObjectLiteralExpression).properties) { + for (const property of (source.node as ts.ObjectLiteralExpression).properties.nodes) { const member = optionMember(property); if (member === null) { lowerer.noLowering(`${fn} with a non-plain options member`, property, supported); @@ -189,11 +189,12 @@ export function lowerFsToUnixTimestampCall( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "fs" || bi.member !== "_toUnixTimestamp") return null; - if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) return null; + if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!)) + return null; return { kind: "libCall", fn: "fs.toUnixTimestamp", - args: [lowerFsTimestampValue(lowerer, expr.arguments[0], loc)], + args: [lowerFsTimestampValue(lowerer, expr.arguments.nodes[0], loc)], type: F64, loc, }; @@ -208,7 +209,7 @@ function opendirLowers(lowerer: Lowerer, args: readonly ts.Expression[]): boolea const options = args[1]; if (!options || (ts.isIdentifier(options) && options.text === "undefined")) return true; if (!ts.isObjectLiteralExpression(options)) return false; - return options.properties.every((property) => { + return options.properties.nodes.every((property) => { const option = optionMember(property); if (!option) return false; if (option.name === "bufferSize") @@ -250,7 +251,7 @@ export function lowerFsLadderCall( } if (bi.module !== "fs" && bi.module !== "fs/promises") return null; if (!isJsSourceFile(expr.getSourceFile())) return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) return null; // Every ladder argument crosses as a dyn value; an argument that // cannot leaves the historical fence in place. @@ -304,12 +305,12 @@ export function lowerFsLadderCall( ts.isIdentifier(pathNode.expression) && pathNode.expression.text === "URL" && lowerer.mapTypeOf(lowerer.typeOf(pathNode))?.kind === "url" && - pathNode.arguments?.length === 1 && - ts.isStringLiteral(pathNode.arguments[0]!) + pathNode.arguments?.nodes.length === 1 && + ts.isStringLiteral(pathNode.arguments.nodes[0]!) ) { let parsed: URL | null = null; try { - parsed = new URL(pathNode.arguments[0]!.text); + parsed = new URL(pathNode.arguments.nodes[0]!.text); } catch { parsed = null; } @@ -512,8 +513,8 @@ export function lowerStringOrBytesWrite( kind: "call", callee: helper, args: [ - lowerer.lowerExprExpecting(call.arguments[0]!, STRING), - lowerer.lowerExprExpecting(call.arguments[1]!, dataType), + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, dataType), ], type: VOID, loc, @@ -648,7 +649,8 @@ export function lowerFileHandleMethodCall( }; if (name === "sync" || name === "datasync") { - if (call.arguments.length !== 0) lowerer.noLowering(`FileHandle.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`FileHandle.${name} with arguments`, call); return { kind: "libCall", fn: name === "sync" ? "fileHandle.sync" : "fileHandle.datasync", @@ -658,34 +660,34 @@ export function lowerFileHandleMethodCall( }; } if (name === "truncate" || name === "chmod") { - if (call.arguments.length > 1 || (name === "chmod" && call.arguments.length !== 1)) - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || (name === "chmod" && call.arguments.nodes.length !== 1)) + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); return { kind: "libCall", fn: name === "truncate" ? "fileHandle.truncate" : "fileHandle.chmod", - args: [receiver(), num(call.arguments[0], 0).value], + args: [receiver(), num(call.arguments.nodes[0], 0).value], type: promise(VOID), loc, }; } if (name === "utimes") { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("FileHandle.utimes with this argument shape", call); return { kind: "libCall", fn: "fileHandle.utimes", args: [ receiver(), - lowerFsTimestampValue(lowerer, call.arguments[0], loc), - lowerFsTimestampValue(lowerer, call.arguments[1], loc), + lowerFsTimestampValue(lowerer, call.arguments.nodes[0], loc), + lowerFsTimestampValue(lowerer, call.arguments.nodes[1], loc), ], type: promise(VOID), loc, }; } if (name === "readv" || name === "writev") { - if (call.arguments.length < 1 || call.arguments.length > 2) - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") lowerer.badType(call, lowerer.typeOf(call)); @@ -694,15 +696,16 @@ export function lowerFileHandleMethodCall( fn: name === "readv" ? "fileHandle.readv" : "fileHandle.writev", args: [ receiver(), - lowerer.lowerExprExpecting(call.arguments[0]!, arrayOf(BYTES_U8)), - num(call.arguments[1], -1).value, + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, arrayOf(BYTES_U8)), + num(call.arguments.nodes[1], -1).value, ], type, loc, }; } if (name === "close" || name === "stat") { - if (call.arguments.length !== 0) lowerer.noLowering(`FileHandle.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`FileHandle.${name} with arguments`, call); return { kind: "libCall", fn: name === "close" ? "fileHandle.close" : "fileHandle.stat", @@ -713,12 +716,12 @@ export function lowerFileHandleMethodCall( } if (name === "readFile") { - if (call.arguments.length > 1) { - lowerer.noLowering(`FileHandle.readFile with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`FileHandle.readFile with ${call.arguments.nodes.length} arguments`, call); } const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise") lowerer.badType(call, lowerer.typeOf(call)); - const encoding = utf8(call.arguments[0]); + const encoding = utf8(call.arguments.nodes[0]); if (typeEquals(type.inner, BYTES_U8)) { return { kind: "libCall", @@ -739,14 +742,14 @@ export function lowerFileHandleMethodCall( } if (name === "writeFile" || name === "appendFile") { - if (call.arguments.length < 1 || call.arguments.length > 2) { - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); } - const dataNode = call.arguments[0]!; + const dataNode = call.arguments.nodes[0]!; const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); // Evaluate a supplied utf8 encoding even for Buffer data, matching // Node's argument order; it does not affect the bytes. - const encoding = utf8(call.arguments[1]); + const encoding = utf8(call.arguments.nodes[1]); if (dataT?.kind === "string") { return { kind: "libCall", @@ -773,10 +776,10 @@ export function lowerFileHandleMethodCall( } if (name === "read" || name === "write") { - const firstNode = call.arguments[0]; + const firstNode = call.arguments.nodes[0]; const firstT = firstNode ? lowerer.mapTypeOf(lowerer.typeOf(firstNode)) : undefined; - const secondT = call.arguments[1] - ? lowerer.mapTypeOf(lowerer.typeOf(call.arguments[1]!)) + const secondT = call.arguments.nodes[1] + ? lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[1]!)) : undefined; const readOptions = name === "read" && @@ -784,7 +787,7 @@ export function lowerFileHandleMethodCall( const bufferOptions = firstT?.kind === "bytes" && firstT.elem === "u8" && secondT?.kind === "record"; if (readOptions || bufferOptions) { - if (call.arguments.length > (bufferOptions ? 2 : 1)) + if (call.arguments.nodes.length > (bufferOptions ? 2 : 1)) lowerer.noLowering(`FileHandle.${name} options with extra arguments`, call); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") @@ -799,7 +802,7 @@ export function lowerFileHandleMethodCall( let buffer = bufferOptions ? stage(lowerer.lowerExprExpecting(firstNode!, BYTES_U8), "%fhWindowBuffer") : undefined; - const optionsNode = bufferOptions ? call.arguments[1] : firstNode; + const optionsNode = bufferOptions ? call.arguments.nodes[1] : firstNode; const options = optionsNode ? stage(lowerer.lowerExpr(optionsNode), "%fhWindowOptions") : undefined; @@ -858,21 +861,21 @@ export function lowerFileHandleMethodCall( } if (name === "read") { - if (call.arguments.length < 1 || call.arguments.length > 4) { + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) { lowerer.noLowering( - `FileHandle.read with ${call.arguments.length} arguments`, + `FileHandle.read with ${call.arguments.nodes.length} arguments`, call, "use read(buffer[, offset[, length[, position]]])", ); } - const buffer = lowerer.lowerExprExpecting(call.arguments[0]!, BYTES_U8); + const buffer = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, BYTES_U8); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") { lowerer.badType(call, lowerer.typeOf(call)); } - const offset = num(call.arguments[1], 0); - const length = num(call.arguments[2], -1); - const position = num(call.arguments[3], -1); + const offset = num(call.arguments.nodes[1], 0); + const length = num(call.arguments.nodes[2], -1); + const position = num(call.arguments.nodes[3], -1); return { kind: "libCall", fn: "fileHandle.read", @@ -883,19 +886,19 @@ export function lowerFileHandleMethodCall( } if (name === "write") { - if (call.arguments.length < 1 || call.arguments.length > 4) { - lowerer.noLowering(`FileHandle.write with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) { + lowerer.noLowering(`FileHandle.write with ${call.arguments.nodes.length} arguments`, call); } - const dataNode = call.arguments[0]!; + const dataNode = call.arguments.nodes[0]!; const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") { lowerer.badType(call, lowerer.typeOf(call)); } if (dataT?.kind === "bytes" && dataT.elem === "u8") { - const offset = num(call.arguments[1], 0); - const length = num(call.arguments[2], -1); - const position = num(call.arguments[3], -1); + const offset = num(call.arguments.nodes[1], 0); + const length = num(call.arguments.nodes[2], -1); + const position = num(call.arguments.nodes[3], -1); return { kind: "libCall", fn: "fileHandle.writeBytes", @@ -912,14 +915,14 @@ export function lowerFileHandleMethodCall( }; } if (dataT?.kind === "string") { - if (call.arguments.length > 3) { + if (call.arguments.nodes.length > 3) { lowerer.noLowering( - `FileHandle.write(string) with ${call.arguments.length} arguments`, + `FileHandle.write(string) with ${call.arguments.nodes.length} arguments`, call, 'use write(string[, position[, "utf8"]])', ); } - const position = num(call.arguments[1], -1); + const position = num(call.arguments.nodes[1], -1); return { kind: "libCall", fn: "fileHandle.writeStr", @@ -927,7 +930,7 @@ export function lowerFileHandleMethodCall( receiver(), lowerer.lowerExprExpecting(dataNode, STRING), position.value, - utf8(call.arguments[2]), + utf8(call.arguments.nodes[2]), ], type, loc, @@ -960,7 +963,7 @@ function lowerFileHandleStreamCall( receiver: IrExpr, loc: SrcLoc, ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement) || args.length > 1) { lowerer.noLowering(`FileHandle.${name} with ${args.length} arguments`, call); } @@ -1021,7 +1024,7 @@ export function lowerDirMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (DIR_METHODS.has(name) && call.arguments.length === 0) { + if (DIR_METHODS.has(name) && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, DIR_T); if (name === "closeSync") return { kind: "libCall", fn: "dir.closeSync", args: [receiver], type: VOID, loc }; @@ -1093,7 +1096,7 @@ export function lowerStatsMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const fn = Object.hasOwn(STATS_TYPE_TESTS, name) ? STATS_TYPE_TESTS[name] : undefined; - if (fn && call.arguments.length === 0) { + if (fn && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn, args: [receiver], type: BOOL, loc: locOf(call) }; } @@ -1120,9 +1123,9 @@ export function lowerStatsMethodCall( * alive until watcher.close(). */ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "fs.watch with this argument shape", @@ -1130,16 +1133,17 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: "the supported forms are watch(path[, options][, listener])", ); } - const hasOptions = expr.arguments.length >= 2 && ts.isObjectLiteralExpression(expr.arguments[1]!); - if (expr.arguments.length === 3 && !hasOptions) { + const hasOptions = + expr.arguments.nodes.length >= 2 && ts.isObjectLiteralExpression(expr.arguments.nodes[1]!); + if (expr.arguments.nodes.length === 3 && !hasOptions) { lowerer.noLowering( "fs.watch with a non-literal options argument", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "pass the options as an object literal: watch(path, { recursive?, persistent?, encoding? }, listener)", ); } if (hasOptions) { - for (const prop of (expr.arguments[1] as ts.ObjectLiteralExpression).properties) { + for (const prop of (expr.arguments.nodes[1] as ts.ObjectLiteralExpression).properties.nodes) { if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) { lowerer.noLowering( "fs.watch options with computed keys or spreads", @@ -1202,14 +1206,14 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: // An undocumented key, dropped like Node drops it. } } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const args: IrExpr[] = [path]; const listenerArg = hasOptions - ? expr.arguments.length === 3 - ? expr.arguments[2]! + ? expr.arguments.nodes.length === 3 + ? expr.arguments.nodes[2]! : null - : expr.arguments.length === 2 - ? expr.arguments[1]! + : expr.arguments.nodes.length === 2 + ? expr.arguments.nodes[1]! : null; if (listenerArg !== null) { const cb = lowerer.lowerExpr(listenerArg); @@ -1253,7 +1257,7 @@ export function lowerWatcherMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "close" && call.arguments.length === 0) { + if (name === "close" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn: "watcher.close", args: [receiver], type: VOID, loc }; } @@ -1291,7 +1295,7 @@ export function lowerFsReaddirTypesCall( ): IrExpr { const operation = promiseForm ? "fs.promises.readdir" : "readdirSync"; const spelling = promiseForm ? "readdir" : "readdirSync"; - const optsNode = call.arguments[1]!; + const optsNode = call.arguments.nodes[1]!; if (!ts.isObjectLiteralExpression(optsNode)) { lowerer.noLowering( `${operation} with a non-literal options argument`, @@ -1300,7 +1304,7 @@ export function lowerFsReaddirTypesCall( ); } let sawWithFileTypes = false; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -1372,7 +1376,7 @@ export function lowerFsReaddirTypesCall( ) { fence(); } - const path = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: promiseForm ? "fsp.readdirTypes" : "fs.readdirTypesSync", @@ -1400,8 +1404,8 @@ export function lowerFsStatOptionsCall( ): IrExpr { const spelling = noFollow ? "lstatSync" : "statSync"; const plainFn: IrLibFn = noFollow ? "fs.lstatSync" : "fs.statSync"; - const optsNode = call.arguments[1]!; - const path = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const optsNode = call.arguments.nodes[1]!; + const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); // The path binds first, then the options' effects, then the stat runs. const plainAfter = (effect: IrExpr | null): IrExpr => { if (effect === null) return { kind: "libCall", fn: plainFn, args: [path], type: STATS_T, loc }; @@ -1481,7 +1485,7 @@ export function lowerFsStatOptionsCall( lowerer.noLowering(`${spelling} with this options value`, optsNode, "pass an object literal"); } } - for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties : []) { + for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties.nodes : []) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -1578,7 +1582,7 @@ export function lowerDirentMethodCall( lowerer.checker.getSymbolAtLocation(access.name), ); } - if (call.arguments.length !== 0) lowerer.noLowering(`Dirent.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) lowerer.noLowering(`Dirent.${name} with arguments`, call); const dtype: IrExpr = { kind: "recordGet", obj: receiver, diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts index 389da8dc0..f988c204d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts @@ -40,7 +40,7 @@ function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean /** An exclude function receives Node's Dirent objects when withFileTypes is * set, which have no native representation here. */ function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: string): void { - const options = call.arguments[1]; + const options = call.arguments.nodes[1]; if (!options) return; const exclude = lowerer.checker.getPropertyOfType(lowerer.typeOf(options), "exclude"); if (!exclude) return; @@ -54,10 +54,10 @@ function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: str } function globArgs(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] { - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${what} with spread arguments`, call, "write the arguments positionally"); } - return call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + return call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); } /** `globSync(pattern, options?)`: string[] or Dirent[]. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 77ad8fa2e..58ad82358 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -87,7 +87,7 @@ export function lowerFsStreamMethodCall( ): IrExpr | null { if (access.name.text !== "close") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement)) lowerer.noLowering("close with spread arguments", call); if (args.length > 1) lowerer.noLowering(`close with ${args.length} arguments`, call); const receiver = lowerer.lowerExpr(access.expression); @@ -171,7 +171,7 @@ export function lowerFsStreamNew( ): IrExpr { return fsStreamCreateExpr( lowerer, - expr.arguments ?? [], + expr.arguments?.nodes ?? [], cls === "ReadStream", `new fs.${cls}`, expr, diff --git a/packages/compiler/src/frontend/lowering/builtins/ipc.ts b/packages/compiler/src/frontend/lowering/builtins/ipc.ts index 630927a60..cf9d96117 100644 --- a/packages/compiler/src/frontend/lowering/builtins/ipc.ts +++ b/packages/compiler/src/frontend/lowering/builtins/ipc.ts @@ -89,20 +89,20 @@ export function ipcSendCallback(lowerer: Lowerer, node: ts.Expression): IrExpr { export function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr { const loc = locOf(call); if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `process.send with ${call.arguments.length} arguments`, + `process.send with ${call.arguments.nodes.length} arguments`, call, "send(message[, callback]) is supported; handle transfer and options are not", ); } - const json = ipcJsonMessage(lowerer, call.arguments[0]!, loc); - if (call.arguments[1] === undefined) { + const json = ipcJsonMessage(lowerer, call.arguments.nodes[0]!, loc); + if (call.arguments.nodes[1] === undefined) { return { kind: "libCall", fn: "process.send", args: [json], type: BOOL, loc }; } - const callback = ipcSendCallback(lowerer, call.arguments[1]); + const callback = ipcSendCallback(lowerer, call.arguments.nodes[1]); return { kind: "libCall", fn: "process.sendCb", args: [json, callback], type: BOOL, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index a08b108ad..b89e25f39 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -36,9 +36,9 @@ export function lowerJsonMethodCall( if (member === null) return null; const loc = locOf(call); if ( - (member === "parse" && (call.arguments.length < 1 || call.arguments.length > 2)) || - (member === "stringify" && call.arguments.length > 3) || - call.arguments.some(ts.isSpreadElement) + (member === "parse" && (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2)) || + (member === "stringify" && call.arguments.nodes.length > 3) || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `JSON.${member} with extra or spread arguments`, @@ -47,8 +47,8 @@ export function lowerJsonMethodCall( ); } if (member === "parse") { - const text = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); - const reviver = call.arguments[1]; + const text = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); + const reviver = call.arguments.nodes[1]; if (!reviver || jsonNullishArgument(lowerer, reviver)) { return { kind: "libCall", fn: "json.parse", args: [text], type: DYN, loc }; } @@ -56,8 +56,8 @@ export function lowerJsonMethodCall( return { kind: "libCall", fn: "json.parseReviver", args: [text, callback], type: DYN, loc }; } if (member === "stringify") { - const replacerNode = call.arguments[1]; - const spaceNode = call.arguments[2]; + const replacerNode = call.arguments.nodes[1]; + const spaceNode = call.arguments.nodes[2]; const runtimeOptions = (replacerNode && !jsonNullishArgument(lowerer, replacerNode) && @@ -68,19 +68,19 @@ export function lowerJsonMethodCall( !ts.isStringLiteral(spaceNode) && !ts.isNoSubstitutionTemplateLiteral(spaceNode) && !(ts.isPrefixUnaryExpression(spaceNode) && ts.isNumericLiteral(spaceNode.operand))); - if (runtimeOptions && call.arguments[0]) { + if (runtimeOptions && call.arguments.nodes[0]) { const args = [0, 1, 2].map((index) => - call.arguments[index] - ? lowerer.lowerExprExpecting(call.arguments[index]!, DYN) + call.arguments.nodes[index] + ? lowerer.lowerExprExpecting(call.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: "json.stringifyValue", args, type: DYN, loc }; } const indent = stringifySpaceIndent(lowerer, call); - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return lowerer.wrappedUndefined(lowerer.withUndefinedArm(STRING), loc)!; } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let value = lowerer.lowerExpr(argNode); // Optional Map/array reads and partially narrowed unions keep their // storage representation when lowered as ordinary arguments. Select @@ -103,7 +103,7 @@ export function lowerJsonMethodCall( if (helper) value = { kind: "call", callee: helper, args: [value], type: narrowed, loc }; } } - const replacer = call.arguments[1]; + const replacer = call.arguments.nodes[1]; if (replacer && !jsonNullishArgument(lowerer, replacer)) { const callback = lowerJsonCallback(lowerer, replacer, "replacer"); if (value.type.kind === "undefinedT" || value.type.kind === "nullT") @@ -573,12 +573,12 @@ function stringifySpaceIndent(lowerer: Lowerer, call: ts.CallExpression): string call, "the serializer is type-directed — shape the value before stringifying", ); - if (call.arguments.length <= 1) return ""; + if (call.arguments.nodes.length <= 1) return ""; const unwrap = (e: ts.Expression): ts.Expression => ts.isParenthesizedExpression(e) ? unwrap(e.expression) : e; const isUndefined = (e: ts.Expression): boolean => jsonNullishArgument(lowerer, e); - if (call.arguments.length === 2) return ""; - const space = unwrap(call.arguments[2]!); + if (call.arguments.nodes.length === 2) return ""; + const space = unwrap(call.arguments.nodes[2]!); if (space.kind === ts.SyntaxKind.NullKeyword || isUndefined(space)) return ""; if (ts.isNumericLiteral(space)) { const n = Number(space.text.replace(/_/g, "")); diff --git a/packages/compiler/src/frontend/lowering/builtins/key-object.ts b/packages/compiler/src/frontend/lowering/builtins/key-object.ts index 72cde522e..ff6c0b38b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/key-object.ts +++ b/packages/compiler/src/frontend/lowering/builtins/key-object.ts @@ -46,13 +46,13 @@ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): I const access = call.expression; if (access.questionDotToken || !KEY_OBJECT_METHODS.has(access.name.text)) return null; if (!isKeyObjectValue(lowerer, access.expression)) return null; - if (call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; return { kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(call), }; diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 9c3a8c0cf..3a178b126 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -206,7 +206,7 @@ export function builtinNamespaceDestructureModuleOf( module = lowerer.builtinNamespaceModuleOf(init); } if (module === null) return null; - for (const el of decl.name.elements) { + for (const el of decl.name.elements.nodes) { if ( el.dotDotDotToken || el.initializer !== undefined || @@ -231,8 +231,8 @@ function createRequireBaseCallOf(lowerer: Lowerer, expr: ts.Expression): ts.Call if (!ts.isIdentifier(e.expression)) return null; const bi = builtinImportOf(lowerer, e.expression); if (!bi || bi.module !== "module" || bi.member !== "createRequire") return null; - if (e.arguments.length !== 1) return null; - const base = stripTypeCasts(e.arguments[0]!); + if (e.arguments.nodes.length !== 1) return null; + const base = stripTypeCasts(e.arguments.nodes[0]!); if (ts.isIdentifier(base) && lowerer.isStdlibGlobal(base, "__filename")) return e; if ( ts.isPropertyAccessExpression(base) && @@ -310,8 +310,8 @@ export function createRequireSpecOf( if (call.questionDotToken) return null; const baseFile = createRequireCalleeFileOf(lowerer, call.expression); if (baseFile === null) return null; - if (call.arguments.length !== 1) return { spec: null, baseFile }; - const a = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return { spec: null, baseFile }; + const a = call.arguments.nodes[0]!; return { spec: ts.isStringLiteralLike(a) ? a.text : null, baseFile }; } @@ -372,7 +372,7 @@ export function createRequireProgramModuleDecl( if (createRequireProgramModuleOf(lowerer, init) === null) return false; if (ts.isIdentifier(nameNode)) return true; if (!ts.isObjectBindingPattern(nameNode)) return false; - return nameNode.elements.every( + return nameNode.elements.nodes.every( (element) => element.name !== undefined && element.dotDotDotToken === undefined && diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index 58ac85aba..956eddb93 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -101,8 +101,8 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && bi.member === "openSync" && - expr.arguments.length >= 2 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "f64" + expr.arguments.nodes.length >= 2 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "f64" ) { return lowerFsNumericOpenCall(lowerer, expr, loc); } @@ -146,7 +146,7 @@ export function lowerBuiltinModuleCall( // verification stance. const syncReaddirTypes = bi.module === "fs" && bi.member === "readdirSync"; const promiseReaddirTypes = bi.module === "fs/promises" && bi.member === "readdir"; - if ((syncReaddirTypes || promiseReaddirTypes) && expr.arguments.length === 2) { + if ((syncReaddirTypes || promiseReaddirTypes) && expr.arguments.nodes.length === 2) { return lowerFsReaddirTypesCall(lowerer, expr, loc, promiseReaddirTypes); } // fs.statSync/fs.lstatSync(path, options): throwIfNoEntry and bigint: @@ -154,7 +154,7 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && (bi.member === "statSync" || bi.member === "lstatSync") && - expr.arguments.length === 2 + expr.arguments.nodes.length === 2 ) { return lowerFsStatOptionsCall(lowerer, expr, loc, bi.member === "lstatSync"); } @@ -216,11 +216,11 @@ export function lowerBuiltinModuleCall( ) { return lowerReadlineCreateCall(lowerer, expr, loc, bi.module === "readline/promises"); } - if (bi.module === "fs" && bi.member === "readFileSync" && expr.arguments.length === 2) { + if (bi.module === "fs" && bi.member === "readFileSync" && expr.arguments.nodes.length === 2) { const encoded = lowerFsEncodedRead( lowerer, expr, - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "f64", + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "f64", loc, ); if (encoded) return encoded; @@ -233,18 +233,26 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && bi.member === "readFileSync" && - expr.arguments.length === 1 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind !== "f64" + expr.arguments.nodes.length === 1 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "f64" ) { - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "fs.readFileSyncBytes", args: [path], type: BYTES_U8, loc }; } - if (bi.module === "fs/promises" && bi.member === "readFile" && expr.arguments.length === 2) { + if ( + bi.module === "fs/promises" && + bi.member === "readFile" && + expr.arguments.nodes.length === 2 + ) { const encoded = lowerFsPromisesEncodedRead(lowerer, expr, loc); if (encoded) return encoded; } - if (bi.module === "fs/promises" && bi.member === "readFile" && expr.arguments.length === 1) { - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + if ( + bi.module === "fs/promises" && + bi.member === "readFile" && + expr.arguments.nodes.length === 1 + ) { + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "fsp.readFileBytes", @@ -256,8 +264,8 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && bi.member === "readFileSync" && - expr.arguments.length >= 1 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "f64" + expr.arguments.nodes.length >= 1 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "f64" ) { return lowerFsReadDescriptorCall(lowerer, expr, loc); } @@ -273,13 +281,13 @@ export function lowerBuiltinModuleCall( ) { return lowerFsStreamCreate(lowerer, expr, bi.member, loc); } - if (bi.module === "fs" && bi.member === "mkdirSync" && expr.arguments.length === 2) { + if (bi.module === "fs" && bi.member === "mkdirSync" && expr.arguments.nodes.length === 2) { return lowerFsMkdirSyncCall(lowerer, expr, loc); } - if (bi.module === "fs/promises" && bi.member === "mkdir" && expr.arguments.length === 2) { + if (bi.module === "fs/promises" && bi.member === "mkdir" && expr.arguments.nodes.length === 2) { return lowerFsPromisesMkdirCall(lowerer, expr, loc); } - if (bi.module === "fs" && bi.member === "rmSync" && expr.arguments.length === 2) { + if (bi.module === "fs" && bi.member === "rmSync" && expr.arguments.nodes.length === 2) { return lowerFsRemoveCall(lowerer, expr, loc); } // accessSync(p, mode?) and truncateSync(p, len?), with their promise @@ -290,20 +298,20 @@ export function lowerBuiltinModuleCall( ? bi.member === "accessSync" || bi.member === "truncateSync" : bi.module === "fs/promises" && (bi.member === "access" || bi.member === "truncate"); if (zeroDefault) { - if (expr.arguments.length < 1 || expr.arguments.length > 2) { - lowerer.noLowering(`${bi.member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { + lowerer.noLowering(`${bi.member} with ${expr.arguments.nodes.length} arguments`, expr); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const second: IrExpr = expr.arguments[1] - ? lowerer.lowerExprExpecting(expr.arguments[1], F64) + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const second: IrExpr = expr.arguments.nodes[1] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1], F64) : { kind: "numLit", value: 0, type: F64, loc }; return { kind: "libCall", fn: fn.fn, args: [path, second], type: fn.result, loc }; } if ( bi.module === "fs/promises" && bi.member === "appendFile" && - expr.arguments.length === 2 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "bytes" + expr.arguments.nodes.length === 2 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "bytes" ) { const lowered = lowerFsWriteBytesCall(lowerer, expr, bi, loc); if (lowered) return lowered; @@ -311,7 +319,7 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && (bi.member === "writeFileSync" || bi.member === "appendFileSync") && - expr.arguments.length === 2 + expr.arguments.nodes.length === 2 ) { const lowered = lowerFsWriteBytesCall(lowerer, expr, bi, loc); if (lowered) return lowered; @@ -325,15 +333,15 @@ export function lowerBuiltinModuleCall( // remain outside the static surface. const syncWriteOptions = bi.module === "fs" && bi.member === "writeFileSync"; const promiseWriteOptions = bi.module === "fs/promises" && bi.member === "writeFile"; - if ((syncWriteOptions || promiseWriteOptions) && expr.arguments.length === 3) { + if ((syncWriteOptions || promiseWriteOptions) && expr.arguments.nodes.length === 3) { return lowerFsWriteOptionsCall(lowerer, expr, loc, promiseWriteOptions); } if (fn.variadicPack) { // join(...parts) forwards the array itself; mixing spread and plain // arguments (or spreading anything but a string[]) stays out. - const spread = expr.arguments.find(ts.isSpreadElement); + const spread = expr.arguments.nodes.find(ts.isSpreadElement); if (spread) { - if (expr.arguments.length === 1) { + if (expr.arguments.nodes.length === 1) { // The whole-array form forwards the operand directly (no copy). const packed = lowerer.lowerExprExpecting(spread.expression, arrayOf(STRING)); return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; @@ -342,31 +350,31 @@ export function lowerBuiltinModuleCall( // tmpdir.resolve: plain arguments and spread arrays pack into one // fresh string[] (arrayLit's spread positions copy element-wise, // JS-exact; a dyn spread source rides the validated extraction). - const elems = expr.arguments.map((a) => + const elems = expr.arguments.nodes.map((a) => ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(STRING)) : lowerer.lowerExprExpecting(a, STRING), ); - const spreads = expr.arguments.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); + const spreads = expr.arguments.nodes.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); const packed: IrExpr = { kind: "arrayLit", elems, spreads, type: arrayOf(STRING), loc }; return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; } - const elems = expr.arguments.map((a) => lowerer.lowerExprExpecting(a, STRING)); + const elems = expr.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, STRING)); const packed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(STRING), loc }; return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; } if ( bi.module === "fs" && bi.member === "readFileSync" && - expr.arguments.length >= 1 && - expr.arguments.length <= 2 && - !expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length >= 1 && + expr.arguments.nodes.length <= 2 && + !expr.arguments.nodes.some(ts.isSpreadElement) ) { const lowered = lowerFsReadPathCall(lowerer, expr, loc); if (lowered) return lowered; } const required = fn.params.length - (fn.defaults?.length ?? 0); - const hasSpread = expr.arguments.some(ts.isSpreadElement); + const hasSpread = expr.arguments.nodes.some(ts.isSpreadElement); if ( bi.module === "url" && ["fileURLToPath", "fileURLToPathBuffer", "pathToFileURL"].includes(bi.member) @@ -386,10 +394,10 @@ export function lowerBuiltinModuleCall( } if ( !hasSpread && - (expr.arguments.length < required || expr.arguments.length > fn.params.length) + (expr.arguments.nodes.length < required || expr.arguments.nodes.length > fn.params.length) ) { lowerer.noLowering( - `${name} with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `${name} with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, bi.member === "readFileSync" || bi.member === "readFile" ? `pass the encoding: ${bi.member}(path, "utf8") — Buffer reads and options objects have no lowering` @@ -406,11 +414,11 @@ export function lowerBuiltinModuleCall( // "utf8" — or Node's "utf-8" alias, the same decoder (the fallback // declaration enforces the pair at typecheck; @types/node accepts // every BufferEncoding). - const enc = lowerer.typeOf(expr.arguments[1]!); + const enc = lowerer.typeOf(expr.arguments.nodes[1]!); if (!(enc.isStringLiteralType() && (enc.value === "utf8" || enc.value === "utf-8"))) { lowerer.noLowering( `${bi.member} with a non-"utf8" encoding`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, `only utf8 reads are supported: ${bi.member}(path, "utf8")`, ); } @@ -434,7 +442,7 @@ export function lowerBuiltinModuleCall( ); return { type, mode: param.mode === "optional" ? "omittable" : "required" }; }); - const args = lowerer.completeArgs(expr.arguments, shapes, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr); return { kind: "callValue", callee, args, type: callee.type.ret, loc }; } let args: IrExpr[]; @@ -453,9 +461,9 @@ export function lowerBuiltinModuleCall( }, }, ); - args = lowerer.completeArgs(expr.arguments, shapes, loc, expr); + args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr); } else { - args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, fn.params[i])); + args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, fn.params[i])); for (let i = args.length; i < fn.params.length; i++) { const dflt = fn.defaults![i - required]!; args.push({ kind: "strLit", value: dflt, type: STRING, loc }); diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index 5fcf35541..afa9dc1ea 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -54,9 +54,9 @@ export function lowerNodeModuleCall( ): IrExpr | null { if (bi.module !== "module") return null; if (bi.member === "syncBuiltinESMExports") { - if (expr.arguments.length !== 0 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 0 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `module.syncBuiltinESMExports with ${expr.arguments.length} arguments`, + `module.syncBuiltinESMExports with ${expr.arguments.nodes.length} arguments`, expr, "syncBuiltinESMExports() takes no arguments", ); @@ -71,14 +71,14 @@ export function lowerNodeModuleCall( return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; } if (bi.member !== "isBuiltin") return null; - if (expr.arguments.length !== 1 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 1 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `module.isBuiltin with ${expr.arguments.length} arguments`, + `module.isBuiltin with ${expr.arguments.nodes.length} arguments`, expr, "isBuiltin(moduleName) takes one string", ); } - const argumentNode = expr.arguments[0]!; + const argumentNode = expr.arguments.nodes[0]!; const argument = lowerer.lowerExpr(argumentNode); if (argument.type.kind !== "string") { if (argument.type.kind === "dyn" || argument.type.kind === "union") { @@ -276,18 +276,18 @@ export function lowerImportMetaResolveCall( ) { return null; } - if (call.questionDotToken !== undefined || call.arguments.length !== 1) { + if (call.questionDotToken !== undefined || call.arguments.nodes.length !== 1) { lowerer.noLowering( "import.meta.resolve with this argument shape", call, 'the lowered form is import.meta.resolve("") using the containing module as its parent', ); } - const specifier = staticString(call.arguments[0]); + const specifier = staticString(call.arguments.nodes[0]); if (specifier === null) { lowerer.noLowering( "import.meta.resolve with a runtime-computed specifier", - call.arguments[0]!, + call.arguments.nodes[0]!, "a compiled binary has a fixed module graph — pass a string literal", ); } @@ -327,7 +327,7 @@ function staticResolveOptionPaths( if (ts.isIdentifier(value) && value.text === "undefined") return undefined; if (!ts.isObjectLiteralExpression(value)) return null; let paths: readonly string[] | undefined; - for (const prop of value.properties) { + for (const prop of value.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; const name = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : null; @@ -335,7 +335,7 @@ function staticResolveOptionPaths( const init = stripTypeCasts(prop.initializer); if (!ts.isArrayLiteralExpression(init)) return null; const entries: string[] = []; - for (const item of init.elements) { + for (const item of init.elements.nodes) { if (!ts.isStringLiteralLike(item)) return null; entries.push(item.text); } @@ -369,8 +369,8 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio if (baseFile === null) return null; if ( call.questionDotToken !== undefined || - call.arguments.length < 1 || - call.arguments.length > (pathsCall ? 1 : 2) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > (pathsCall ? 1 : 2) ) { lowerer.noLowering( pathsCall @@ -379,11 +379,11 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio call, ); } - const specifier = staticString(call.arguments[0]); + const specifier = staticString(call.arguments.nodes[0]); if (specifier === null) { lowerer.noLowering( `${pathsCall ? "require.resolve.paths" : "require.resolve"} with a runtime-computed request`, - call.arguments[0]!, + call.arguments.nodes[0]!, "a compiled binary has a fixed module graph — pass a string literal", ); } @@ -414,11 +414,11 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio }; return lowerer.coerceInto(call, raw, lowerer.irTypeOf(call)); } - const paths = staticResolveOptionPaths(call.arguments[1]); + const paths = staticResolveOptionPaths(call.arguments.nodes[1]); if (paths === null) { lowerer.noLowering( "require.resolve with runtime-computed options", - call.arguments[1]!, + call.arguments.nodes[1]!, 'omit options or pass { paths: ["", ...] }', ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/net-address.ts b/packages/compiler/src/frontend/lowering/builtins/net-address.ts index b49546d8c..d99d63fd3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/net-address.ts +++ b/packages/compiler/src/frontend/lowering/builtins/net-address.ts @@ -40,7 +40,7 @@ function netAddressValueClass(lowerer: Lowerer, node: ts.Expression): NetAddress export function lowerNetAddressNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const cls = netAddressClassOf(lowerer, expr.expression); if (cls === null) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); const values = args.map((arg) => @@ -93,13 +93,13 @@ const INSTANCE_METHODS: Record> = { export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; const access = call.expression; - if (access.questionDotToken || call.arguments.some(ts.isSpreadElement)) return null; + if (access.questionDotToken || call.arguments.nodes.some(ts.isSpreadElement)) return null; const name = access.name.text; const loc = locOf(call); const statics = Object.hasOwn(STATIC_CALLS, name) ? STATIC_CALLS[name]! : null; if (statics !== null && netAddressClassOf(lowerer, access.expression) === statics.cls) { // The member takes one value; any others still evaluate, in order. - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); const first: IrExpr = args[0] ?? dynUndefinedExpr(loc); const result: IrExpr = { kind: "libCall", @@ -128,7 +128,7 @@ export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression): recv: lowerer.lowerExprExpecting(access.expression, DYN), method: name, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 4f86e6f74..18ca0f1f3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -54,10 +54,10 @@ export function lowerNumberStaticCall( const loc = locOf(call); const fn = own(NUMBER_STATIC_PREDICATES, member); if (fn !== undefined) { - if (call.arguments.length !== 1) { - lowerer.noLowering(`Number.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`Number.${member} with ${call.arguments.nodes.length} arguments`, call); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const arg = lowerBuiltinValuePreservingUndefined(lowerer, argNode); const optional = lowerOptionalNumberPredicate(lowerer, arg, fn, loc); @@ -73,9 +73,9 @@ export function lowerNumberStaticCall( } if (member === "parseFloat" || member === "parseInt") { const want = member === "parseFloat" ? 1 : 2; - if (call.arguments.length !== want) { + if (call.arguments.nodes.length !== want) { lowerer.noLowering( - `Number.${member} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, + `Number.${member} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, call, member === "parseInt" ? "pass an explicit radix: Number.parseInt(s, 10)" : undefined, ); @@ -83,9 +83,9 @@ export function lowerNumberStaticCall( // Number.parseFloat/Number.parseInt ARE the global parsers: over a // string argument they lower to the same static parse (parseInt with // its explicit radix); only the coercing argument shapes are refused. - const probed = tryLowerExpression(lowerer, call.arguments[0]!); + const probed = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if (probed !== null && isStringLike(lowerer, probed.type)) { - const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments, loc); + const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc); if (parsed !== null) return parsed; } // A checked-dynamic argument (`Number.parseInt(env.FORCE_COLOR, 10)` @@ -101,13 +101,13 @@ export function lowerNumberStaticCall( }; if (member === "parseFloat") return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc }; - const radix = lowerer.lowerExprExpecting(call.arguments[1]!, F64); + const radix = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64); return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc }; } const argText = probed !== null ? lowerer.fmt(probed.type) - : lowerer.checker.typeToString(lowerer.checker.getTypeAtLocation(call.arguments[0]!)); + : lowerer.checker.typeToString(lowerer.checker.getTypeAtLocation(call.arguments.nodes[0]!)); lowerer.refuseUnsupportedApi( `'Number.${member}' with a '${argText}' argument`, call, diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index b88b97eb6..c10465039 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -28,7 +28,7 @@ export function lowerOsUserInfoCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( "userInfo with options", call, @@ -95,9 +95,9 @@ export function lowerOsNetworkInterfacesCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( - `networkInterfaces with ${call.arguments.length} arguments`, + `networkInterfaces with ${call.arguments.nodes.length} arguments`, call, "networkInterfaces() takes no arguments", ); @@ -179,9 +179,9 @@ export function lowerOsNetworkInterfacesCall( * record per CPU from the snapshot accessors, in the mapped shapes' own * field order. Other result shapes fence. */ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( - `cpus with ${call.arguments.length} arguments`, + `cpus with ${call.arguments.nodes.length} arguments`, call, "cpus() takes no arguments", ); @@ -322,8 +322,8 @@ export function lowerPathParseCall( win32: boolean, loc: SrcLoc, ): IrExpr { - const arg = call.arguments[0]; - if (call.arguments.length !== 1 || !arg || ts.isSpreadElement(arg)) { + const arg = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || !arg || ts.isSpreadElement(arg)) { lowerer.noLowering("path.parse without exactly one path", call, "parse(path) takes one string"); } const result = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -412,8 +412,8 @@ export function lowerPathFormatCall( win32: boolean, loc: SrcLoc, ): IrExpr { - const arg = call.arguments[0]; - if (call.arguments.length !== 1 || !arg || ts.isSpreadElement(arg)) { + const arg = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || !arg || ts.isSpreadElement(arg)) { lowerer.noLowering( "path.format without exactly one object", call, @@ -520,7 +520,7 @@ export function lowerOsSetPriorityCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( "setPriority with these arguments", @@ -542,8 +542,8 @@ export function lowerOsGetPriorityCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const arg = call.arguments[0]; - if (call.arguments.length > 1 || (arg && ts.isSpreadElement(arg))) { + const arg = call.arguments.nodes[0]; + if (call.arguments.nodes.length > 1 || (arg && ts.isSpreadElement(arg))) { lowerer.noLowering( "getPriority with these arguments", call, diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index f62311f13..2a9962b0f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -83,7 +83,7 @@ export function lowerPerfHooksCall( if (access.questionDotToken) return null; const loc = locOf(expr); if (access.name.text === "now" && isPerfHooksPerformanceExpr(lowerer, access.expression)) { - if (expr.arguments.length !== 0) { + if (expr.arguments.nodes.length !== 0) { lowerer.noLowering( "performance.now with arguments", expr, @@ -99,7 +99,10 @@ export function lowerPerfHooksCall( access.expression.name.text === "now" && isPerfHooksPerformanceExpr(lowerer, access.expression.expression) ) { - if (expr.arguments.length !== 1 || !isPerfHooksPerformanceExpr(lowerer, expr.arguments[0]!)) { + if ( + expr.arguments.nodes.length !== 1 || + !isPerfHooksPerformanceExpr(lowerer, expr.arguments.nodes[0]!) + ) { lowerer.noLowering( "this performance.now.bind form", expr, @@ -118,7 +121,7 @@ export function lowerPerfHooksCall( access.name.text === "toJSON" && isPerfEntryType(lowerer, lowerer.mapTypeOf(lowerer.typeOf(access.expression))) ) { - if (expr.arguments.length !== 0) { + if (expr.arguments.nodes.length !== 0) { lowerer.noLowering("PerformanceEntry toJSON with arguments", expr); } const entry = lowerer.lowerExpr(access.expression); @@ -241,10 +244,10 @@ function perfEntryRecord( * protocol), and a non-iterable spread throws V8's TypeError, so the * runtime always receives an array. */ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - if (!call.arguments.some(ts.isSpreadElement)) { + if (!call.arguments.nodes.some(ts.isSpreadElement)) { return { kind: "dynArrLit", - elems: call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)), + elems: call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)), type: DYN, loc, }; @@ -259,7 +262,7 @@ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL loc, }, ]; - for (const arg of call.arguments) { + for (const arg of call.arguments.nodes) { if (!ts.isSpreadElement(arg)) { const value = lowerer.lowerExprExpecting(arg, DYN); stmts.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 94c2cdba7..e4e5004c1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -58,13 +58,13 @@ export function lowerProcStreamMethodCall( recv: receiver, method: name, calleeName: call.expression.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; } } - if (name === "getWindowSize" && call.arguments.length === 0) { + if (name === "getWindowSize" && call.arguments.nodes.length === 0) { const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN); return lowerer.coerceInto( call, @@ -80,13 +80,13 @@ export function lowerProcStreamMethodCall( lowerer.irTypeOf(call), ); } - if (name === "write" && call.arguments.length === 1) { + if (name === "write" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExpr(access.expression); - const data = lowerer.lowerExpr(call.arguments[0]!); + const data = lowerer.lowerExpr(call.arguments.nodes[0]!); if (data.type.kind !== "string") { lowerer.noLowering( `write of '${lowerer.fmt(data.type)}' data on a stream value`, - call.arguments[0]!, + call.arguments.nodes[0]!, "one string is the supported form here — narrow unions first", ); } @@ -498,14 +498,14 @@ export function lowerProcessMethodCall( if (isJsSourceFile(call.getSourceFile()) && ts.isPropertyAccessExpression(access.expression)) { const stream = lowerer.stdlibGlobalMember(access.expression, "process"); if (stream === "stdin" || stream === "stdout" || stream === "stderr") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("process stream spread arguments", call); return { kind: "dynInvoke", recv: lowerer.lowerExpr(access.expression), method: access.name.text, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(call), }; @@ -514,31 +514,31 @@ export function lowerProcessMethodCall( const directProcessMember = lowerer.stdlibGlobalMember(access, "process"); if (directProcessMember === "loadEnvFile") return lowerProcessLoadEnvFile(lowerer, call); if (directProcessMember === "hrtime") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("process.hrtime with spread arguments", call); const loc = locOf(call); return { kind: "dynCall", callee: { kind: "libCall", fn: "process.hrtimeValue", args: [], type: DYN, loc }, - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), calleeName: "process.hrtime", type: DYN, loc, }; } if (directProcessMember === "getBuiltinModule") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("process.getBuiltinModule with spread arguments", call); const loc = locOf(call); - const id = call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + const id = call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : (dynUndefinedExpr(loc) satisfies IrExpr); const binding = lowerer.declareHiddenLocal("%builtinId", DYN); return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: binding.id, init: id, loc }, - ...call.arguments.slice(1).map((arg): IrStmt => ({ + ...call.arguments.nodes.slice(1).map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg), @@ -557,8 +557,8 @@ export function lowerProcessMethodCall( } if (directProcessMember === "send") return lowerProcessIpcSend(lowerer, call); if (directProcessMember === "disconnect") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.disconnect with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.disconnect with ${call.arguments.nodes.length} arguments`, call); } if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( @@ -584,7 +584,7 @@ export function lowerProcessMethodCall( ts.isPropertyAccessExpression(access.expression) && lowerer.stdlibGlobalMember(access.expression, "process") === "stdin" ) { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering("stdin.destroy with arguments", call); } return { kind: "libCall", fn: "process.stdinDestroy", args: [], type: VOID, loc: locOf(call) }; @@ -610,18 +610,18 @@ export function lowerProcessMethodCall( "using the result of stdin.setRawMode(...) (the ReadStream chain — call it as its own statement)", ); } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( - `stdin.setRawMode with ${call.arguments.length} arguments`, + `stdin.setRawMode with ${call.arguments.nodes.length} arguments`, call, "the supported form is setRawMode(mode) with one boolean", ); } - const mode = lowerer.lowerExpr(call.arguments[0]!); + const mode = lowerer.lowerExpr(call.arguments.nodes[0]!); if (mode.type.kind !== "bool") { lowerer.noLowering( `stdin.setRawMode of '${lowerer.fmt(mode.type)}' modes`, - call.arguments[0]!, + call.arguments.nodes[0]!, "the mode is a boolean here — narrow unions first", ); } @@ -639,15 +639,18 @@ export function lowerProcessMethodCall( ) { const loc = locOf(call); const once = access.name.text === "once"; - if (call.arguments.length !== 2) { - lowerer.noLowering(`stdin.${access.name.text} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2) { + lowerer.noLowering( + `stdin.${access.name.text} with ${call.arguments.nodes.length} arguments`, + call, + ); } - const evT = lowerer.typeOf(call.arguments[0]!); + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if (event !== "data" && event !== "end" && event !== "error" && event !== "keypress") { lowerer.noLowering( `stdin.${access.name.text}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"data", "end", "error", and "keypress" are the supported stdin events (as literals)', ); } @@ -660,15 +663,15 @@ export function lowerProcessMethodCall( } if (event === "keypress") { // readline.emitKeypressEvents' (str, key) events - const listener = stdinKeypressListener(lowerer, call.arguments[1]!); + const listener = stdinKeypressListener(lowerer, call.arguments.nodes[1]!); const onceKp: IrExpr = { kind: "boolLit", value: once, type: BOOL, loc }; return { kind: "libCall", fn: "stdin.onKeypress", args: [listener, onceKp], type: VOID, loc }; } - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "stdin listeners with more than one parameter or a return value", ); } @@ -678,7 +681,7 @@ export function lowerProcessMethodCall( if (param !== undefined && !(param.kind === "bytes" && param.elem === "u8")) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `data listeners whose parameter is not 'Uint8Array' (got '${lowerer.fmt(param)}')`, ); } @@ -686,14 +689,18 @@ export function lowerProcessMethodCall( } if (event === "end") { if (param !== undefined) { - lowerer.unsupported("SC1090", call.arguments[1]!, "end listeners with parameters (use ())"); + lowerer.unsupported( + "SC1090", + call.arguments.nodes[1]!, + "end listeners with parameters (use ())", + ); } return { kind: "libCall", fn: "stdin.onEnd", args: [cb, onceArg], type: VOID, loc }; } if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`, ); } @@ -703,7 +710,7 @@ export function lowerProcessMethodCall( const stream = lowerer.stdlibGlobalMember(access.expression, "process"); if (stream === "stdout" || stream === "stderr") { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `process.${stream}.write with ${args.length} arguments`, @@ -917,8 +924,11 @@ export function lowerProcessMethodCall( ts.isPropertyAccessExpression(access.expression) && lowerer.stdlibGlobalMember(access.expression, "process") === "memoryUsage" ) { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.memoryUsage.rss with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering( + `process.memoryUsage.rss with ${call.arguments.nodes.length} arguments`, + call, + ); } return { kind: "libCall", fn: "process.rss", args: [], type: F64, loc: locOf(call) }; } @@ -936,10 +946,10 @@ export function lowerProcessMethodCall( // `removeListener` IS `off` — Node aliases them. const isOff = member === "off" || member === "removeListener"; if (member === "on" || member === "once" || isOff) { - if (call.arguments.length !== 2) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } - const evT = lowerer.typeOf(call.arguments[0]!); + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if ((event === "message" || event === "disconnect") && !isOff) { if (!ts.isExpressionStatement(call.parent)) { @@ -951,8 +961,8 @@ export function lowerProcessMethodCall( } const callback = event === "message" - ? ipcMessageListener(lowerer, call.arguments[1]!) - : ipcDisconnectListener(lowerer, call.arguments[1]!); + ? ipcMessageListener(lowerer, call.arguments.nodes[1]!) + : ipcDisconnectListener(lowerer, call.arguments.nodes[1]!); return { kind: "libCall", fn: event === "message" ? "process.onMessage" : "process.onDisconnect", @@ -965,7 +975,7 @@ export function lowerProcessMethodCall( if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported("SC1090", call, "chaining process exception listener registration"); } - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); const monitor = boolLit(event === "uncaughtExceptionMonitor", loc); return { kind: "libCall", @@ -991,7 +1001,7 @@ export function lowerProcessMethodCall( "chaining process listener registration (the result is void here — register each listener as its own statement)", ); } - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); const onceArg: IrExpr = { kind: "boolLit", value: member === "once", type: BOOL, loc }; const fn: IrLibFn = event === "unhandledRejection" @@ -1014,7 +1024,7 @@ export function lowerProcessMethodCall( "chaining process listener registration (the result is void here — register each listener as its own statement)", ); } - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); return { kind: "libCall", fn: isOff ? "process.offWarning" : "process.onWarning", @@ -1027,15 +1037,18 @@ export function lowerProcessMethodCall( if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported("SC1090", call, "chaining process signal listener registration"); } - const name = lowerer.coerceToExpected(lowerer.lowerExpr(call.arguments[0]!), STRING); + const name = lowerer.coerceToExpected(lowerer.lowerExpr(call.arguments.nodes[0]!), STRING); if (name.type.kind !== "string" && name.type.kind !== "dyn") { - lowerer.noLowering("process signal event name that is not a string", call.arguments[0]!); + lowerer.noLowering( + "process signal event name that is not a string", + call.arguments.nodes[0]!, + ); } const signal = name.type.kind === "string" ? name : { kind: "dynCheck" as const, value: name, type: STRING, loc }; - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); return { kind: "libCall", fn: isOff ? "process.offSignal" : "process.onSignal", @@ -1045,7 +1058,7 @@ export function lowerProcessMethodCall( }; } if (event !== "exit") { - lowerer.noLowering(`process.${member}("${event}", ...)`, call.arguments[0]!); + lowerer.noLowering(`process.${member}("${event}", ...)`, call.arguments.nodes[0]!); } if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( @@ -1054,7 +1067,7 @@ export function lowerProcessMethodCall( "chaining process listener registration (the result is void here — register each listener as its own statement)", ); } - let cb = lowerer.lowerExpr(call.arguments[1]!); + let cb = lowerer.lowerExpr(call.arguments.nodes[1]!); // The checked-dynamic listener (test/common's `process.on('exit', // runCallChecks)` — an implicit-any JS function, func(dyn)=>dyn, or // a dyn VALUE that rode an untyped binding): adapt through the dyn @@ -1093,7 +1106,7 @@ export function lowerProcessMethodCall( if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "process listeners with more than one parameter or a return value", ); } @@ -1102,7 +1115,7 @@ export function lowerProcessMethodCall( if (param !== undefined && param.kind !== "f64") { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `exit listeners whose parameter is not 'number' (got '${lowerer.fmt(param)}')`, ); } @@ -1119,19 +1132,19 @@ export function lowerProcessMethodCall( // shape: `.forEach((args) => process.emitWarning(...args))`). if (member === "emitWarning") { let argsArr: IrExpr | null = null; - if (call.arguments.length === 1 && ts.isSpreadElement(call.arguments[0]!)) { - const spread = lowerer.lowerExpr(call.arguments[0]!.expression); + if (call.arguments.nodes.length === 1 && ts.isSpreadElement(call.arguments.nodes[0]!)) { + const spread = lowerer.lowerExpr(call.arguments.nodes[0]!.expression); if (spread.type.kind === "dyn") { argsArr = spread; } else { lowerer.noLowering( "process.emitWarning with a typed spread argument", - call.arguments[0]!, + call.arguments.nodes[0]!, "spread an untyped (checked-dynamic) array, or write the arguments positionally", ); } } else { - argsArr = lowerTracingArguments(lowerer, call.arguments, loc); + argsArr = lowerTracingArguments(lowerer, call.arguments.nodes, loc); } return { kind: "libCall", fn: "process.emitWarning", args: [argsArr], type: VOID, loc }; } @@ -1146,21 +1159,21 @@ export function lowerProcessMethodCall( // through, boxable parameterized shapes ride the checked-dynamic // boundary, trailing call arguments ride the interned dyn thunk. if (member === "nextTick") { - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { lowerer.noLowering( "process.nextTick with 0 arguments", call, "the supported form is process.nextTick(callback, ...args)", ); } - const cb = timerStyleCallback(lowerer, call.arguments, "process.nextTick", loc); + const cb = timerStyleCallback(lowerer, call.arguments.nodes, "process.nextTick", loc); return { kind: "libCall", fn: "process.nextTick", args: [cb], type: VOID, loc }; } // The process introspection statics — plain reads of the process's // own clocks and counters, Node's shapes exactly. if (member === "uptime" || member === "availableMemory" || member === "constrainedMemory") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } const fn = member === "uptime" @@ -1192,7 +1205,7 @@ export function lowerProcessMethodCall( type: F64, loc, }); - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "recordLit", fields: shape.fields.map((f) => ({ name: f.name, value: sampleField(f.name) })), @@ -1200,10 +1213,10 @@ export function lowerProcessMethodCall( loc, }; } - if (call.arguments.length !== 1) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } - const prev = lowerer.lowerExpr(call.arguments[0]!); + const prev = lowerer.lowerExpr(call.arguments.nodes[0]!); const prevShape = prev.type.kind === "record" ? lowerer.shapes.get(prev.type.shapeId) : undefined; const prevOk = @@ -1214,7 +1227,7 @@ export function lowerProcessMethodCall( if (prev.type.kind !== "record" || !prevOk) { lowerer.noLowering( `process.${member} of a '${lowerer.fmt(prev.type)}' previous value`, - call.arguments[0]!, + call.arguments.nodes[0]!, "the previous value is the record a prior call answered ({ user, system } numbers) — Node's ERR_INVALID_ARG_TYPE shapes have no lowering", ); } @@ -1282,8 +1295,8 @@ export function lowerProcessMethodCall( // storage lives in the same heap). One sample per call: the interned // helper takes it, then the record reads it field by field. if (member === "memoryUsage") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.memoryUsage with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.memoryUsage with ${call.arguments.nodes.length} arguments`, call); } const t = lowerer.mapTypeOf(lowerer.typeOf(call)); if (t?.kind !== "record") lowerer.badType(call, lowerer.typeOf(call)); @@ -1338,8 +1351,11 @@ export function lowerProcessMethodCall( // process.resourceUsage() — getrusage's 16 fields in Node's names and // units (CPU times in microseconds, maxRSS in kilobytes). if (member === "resourceUsage") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.resourceUsage with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering( + `process.resourceUsage with ${call.arguments.nodes.length} arguments`, + call, + ); } const t = lowerer.mapTypeOf(lowerer.typeOf(call)); if (t?.kind !== "record") lowerer.badType(call, lowerer.typeOf(call)); @@ -1390,9 +1406,9 @@ export function lowerProcessMethodCall( // DIVERGENCE (SEMANTICS.md): resources this runtime does not model // as loop handles (TCP wraps, FS requests) are absent from the answer. if (member === "getActiveResourcesInfo") { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( - `process.getActiveResourcesInfo with ${call.arguments.length} arguments`, + `process.getActiveResourcesInfo with ${call.arguments.nodes.length} arguments`, call, ); } @@ -1402,26 +1418,26 @@ export function lowerProcessMethodCall( // completes it to the -1 read sentinel); umask(mask) sets and answers // the previous mask, Node's shape either way. if (member === "umask") { - if (call.arguments.length > 1) { - lowerer.noLowering(`process.umask with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`process.umask with ${call.arguments.nodes.length} arguments`, call); } const mask: IrExpr = - call.arguments.length === 1 - ? lowerer.lowerExpr(call.arguments[0]!) + call.arguments.nodes.length === 1 + ? lowerer.lowerExpr(call.arguments.nodes[0]!) : { kind: "numLit", value: -1, type: F64, loc }; if (mask.type.kind !== "f64") { - lowerer.noLowering("process.umask of non-number masks", call.arguments[0]!); + lowerer.noLowering("process.umask of non-number masks", call.arguments.nodes[0]!); } return { kind: "libCall", fn: "process.umask", args: [mask], type: F64, loc }; } // chdir(2) — throws Node's fs-shaped error on failure. if (member === "chdir") { - if (call.arguments.length !== 1) { - lowerer.noLowering(`process.chdir with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`process.chdir with ${call.arguments.nodes.length} arguments`, call); } - const dir = lowerer.lowerExpr(call.arguments[0]!); + const dir = lowerer.lowerExpr(call.arguments.nodes[0]!); if (dir.type.kind !== "string") { - lowerer.noLowering("process.chdir of non-string paths", call.arguments[0]!); + lowerer.noLowering("process.chdir of non-string paths", call.arguments.nodes[0]!); } return { kind: "libCall", fn: "process.chdir", args: [dir], type: VOID, loc }; } @@ -1430,8 +1446,8 @@ export function lowerProcessMethodCall( // the member optional (that optionality covers Windows). The `?.()` // spelling routes here through lowerProcessOptionalMethodCall. if (PROCESS_CREDENTIAL_FNS.has(member)) { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } return processCredentialCall(member, loc); } @@ -1443,11 +1459,11 @@ export function lowerProcessMethodCall( // Node's `kill ESRCH`/`kill EPERM` Error. The result is Node's // constant true. if (member === "kill") { - if (call.arguments.length < 1 || call.arguments.length > 2) { - lowerer.noLowering(`process.kill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { + lowerer.noLowering(`process.kill with ${call.arguments.nodes.length} arguments`, call); } - const pid = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const sigNode = call.arguments[1]; + const pid = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); + const sigNode = call.arguments.nodes[1]; if (!sigNode) { const dflt: IrExpr = { kind: "strLit", value: "SIGTERM", type: STRING, loc }; return { kind: "libCall", fn: "process.kill", args: [pid, dflt], type: BOOL, loc }; @@ -1466,7 +1482,7 @@ export function lowerProcessMethodCall( ); } if (member === "exit") { - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; const code: IrExpr = arg !== undefined ? lowerer.lowerExprExpecting(arg, F64) @@ -1491,8 +1507,8 @@ export function lowerProcessOptionalMethodCall( const member = lowerer.stdlibGlobalMember(expr.expression, "process"); if (member === "send") return lowerProcessIpcSend(lowerer, expr); if (member === null || !PROCESS_CREDENTIAL_FNS.has(member)) return null; - if (expr.arguments.length !== 0) { - lowerer.noLowering(`process.${member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.${member} with ${expr.arguments.nodes.length} arguments`, expr); } return processCredentialCall(member, locOf(expr)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index da680a86e..2656da18b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -37,12 +37,12 @@ export function lowerPromiseStaticCall( if (member === null) return null; const loc = locOf(call); if (member === "race") { - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if ( !argNode || !ts.isArrayLiteralExpression(argNode) || - argNode.elements.some(ts.isSpreadElement) || - argNode.elements.length === 0 + argNode.elements.nodes.some(ts.isSpreadElement) || + argNode.elements.nodes.length === 0 ) { lowerer.noLowering( "Promise.race over this argument shape", @@ -71,7 +71,7 @@ export function lowerPromiseStaticCall( } return resultArms.some((a) => typeEquals(a, inner)); }; - const entries = argNode.elements.map((el) => { + const entries = argNode.elements.nodes.map((el) => { const entry = lowerer.lowerExpr(el); if (entry.type.kind !== "promise") { lowerer.noLowering( @@ -106,7 +106,7 @@ export function lowerPromiseStaticCall( // Heterogeneous ARRAY LITERALS land on the tuple overload // (Promise<[A, B]>) and fence here — one promise type is the bound. if (member === "all") { - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if (!argNode) { lowerer.noLowering( "Promise.all with this argument shape", @@ -123,10 +123,10 @@ export function lowerPromiseStaticCall( // like the tuple; the elements are the same T either way). if ( ts.isArrayLiteralExpression(argNode) && - !argNode.elements.some(ts.isSpreadElement) && - argNode.elements.length > 0 + !argNode.elements.nodes.some(ts.isSpreadElement) && + argNode.elements.nodes.length > 0 ) { - const elems = argNode.elements.map((el) => lowerer.lowerExpr(el)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); const first = elems[0]!.type; if ( first.kind === "promise" && @@ -213,7 +213,7 @@ export function lowerPromiseStaticCall( return { kind: "intrinsic", name: "promise.all", args: [entries], type: resultT, loc }; } if (member === "allSettled" || member === "any") { - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if (!argNode || ts.isSpreadElement(argNode)) { lowerer.noLowering( `Promise.${member} with this argument shape`, @@ -224,8 +224,11 @@ export function lowerPromiseStaticCall( if (isJsSourceFile(call.getSourceFile())) { // JavaScript entries are checked-dynamic: any iterable, thenables // and plain values included, exactly like Promise.all. - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { - const elems = argNode.elements.map((el) => lowerer.lowerExpr(el)); + if ( + ts.isArrayLiteralExpression(argNode) && + !argNode.elements.nodes.some(ts.isSpreadElement) + ) { + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); if (elems.every((entry) => lowerer.dynConvertible(entry.type))) { const entries: IrExpr = { kind: "arrayLit", @@ -258,7 +261,7 @@ export function lowerPromiseStaticCall( // (plain-value resolve, Error-pinned reject); the emitter assembles // the record from the newPromise machinery. if (member === "withResolvers") { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Promise.withResolvers with arguments`, call); } // The type mapper owns the record shape (its withResolvers @@ -284,11 +287,11 @@ export function lowerPromiseStaticCall( // types. `Promise.try(f, ...args)` evaluates the arguments at the call // and passes them to f, which still runs synchronously. if (member === "try") { - const fNode = call.arguments[0]; - const argNodes = call.arguments.slice(1); - if (fNode === undefined || call.arguments.some(ts.isSpreadElement)) { + const fNode = call.arguments.nodes[0]; + const argNodes = call.arguments.nodes.slice(1); + if (fNode === undefined || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `Promise.try with ${call.arguments.length === 0 ? "no" : "spread"} arguments`, + `Promise.try with ${call.arguments.nodes.length === 0 ? "no" : "spread"} arguments`, call, "pass the callback and its arguments directly: Promise.try(f, a, b)", ); @@ -391,10 +394,13 @@ export function lowerPromiseStaticCall( // object would be ADOPTED in JS, not wrapped) and promise-armed // unions (wrap-or-identity depends on the runtime arm) fence. if (member === "resolve") { - if (call.arguments.length > 1 || call.arguments.some((a) => ts.isSpreadElement(a))) { - lowerer.noLowering(`Promise.resolve with ${call.arguments.length} arguments`, call); + if ( + call.arguments.nodes.length > 1 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + lowerer.noLowering(`Promise.resolve with ${call.arguments.nodes.length} arguments`, call); } - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (argNode !== undefined) { const argT = lowerer.typeOf(argNode); const argIr = lowerer.mapTypeOf(argT); @@ -502,10 +508,10 @@ function typedPromiseEntries( entries = lowered; } else if ( ts.isArrayLiteralExpression(argNode) && - !argNode.elements.some(ts.isSpreadElement) && - argNode.elements.length > 0 + !argNode.elements.nodes.some(ts.isSpreadElement) && + argNode.elements.nodes.length > 0 ) { - const elems = argNode.elements.map((el) => lowerer.lowerExpr(el)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); const first = elems[0]!.type; if ( first.kind !== "promise" || diff --git a/packages/compiler/src/frontend/lowering/builtins/querystring.ts b/packages/compiler/src/frontend/lowering/builtins/querystring.ts index 34ef9c55b..0000fd44e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/querystring.ts +++ b/packages/compiler/src/frontend/lowering/builtins/querystring.ts @@ -51,8 +51,8 @@ export function lowerQuerystringParseCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length > 4 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`querystring.parse with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 4 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`querystring.parse with ${call.arguments.nodes.length} arguments`, call); } const fence: () => never = () => lowerer.noLowering( @@ -82,17 +82,17 @@ export function lowerQuerystringParseCall( else if (arm.kind !== "undefinedT" && arm.kind !== "f64") fence(); } if (!sawStr || !sawArr) fence(); - const argument = call.arguments[0]; + const argument = call.arguments.nodes[0]; if (!argument) lowerer.noLowering("querystring.parse without a query string", call); const str = lowerer.lowerExprExpecting(argument, STRING); - const sep = qsSepEqArg(lowerer, call.arguments[1], "&", "querystring.parse", loc); - const eq = qsSepEqArg(lowerer, call.arguments[2], "=", "querystring.parse", loc); + const sep = qsSepEqArg(lowerer, call.arguments.nodes[1], "&", "querystring.parse", loc); + const eq = qsSepEqArg(lowerer, call.arguments.nodes[2], "=", "querystring.parse", loc); // The options walk: maxKeys lowers (Node's rule — > 0 caps the pair // count, 0 and negatives mean unlimited — lives in the runtime, so // any number expression works); a custom decodeURIComponent changes // every decoded byte and fences by name. let maxKeys: IrExpr = { kind: "numLit", value: 1000, type: F64, loc }; - const optsNode = call.arguments[3]; + const optsNode = call.arguments.nodes[3]; if (optsNode) { if (!ts.isObjectLiteralExpression(optsNode)) { lowerer.noLowering( @@ -101,7 +101,7 @@ export function lowerQuerystringParseCall( "the supported form spells the options inline: parse(s, sep, eq, { maxKeys: n })", ); } - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -144,10 +144,10 @@ export function lowerQuerystringStringifyCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length > 4 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`querystring.stringify with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 4 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`querystring.stringify with ${call.arguments.nodes.length} arguments`, call); } - const objNode = call.arguments[0]; + const objNode = call.arguments.nodes[0]; // stringify() / stringify(undefined) / stringify(null): Node answers // '' for every non-object — the constant folds. if ( @@ -170,9 +170,9 @@ export function lowerQuerystringStringifyCall( "pass a record of string/number/boolean values (arrays of those expand to repeated keys; null/undefined values serialize empty)", ); } - const sep = qsSepEqArg(lowerer, call.arguments[1], "&", "querystring.stringify", loc); - const eq = qsSepEqArg(lowerer, call.arguments[2], "=", "querystring.stringify", loc); - const optsNode = call.arguments[3]; + const sep = qsSepEqArg(lowerer, call.arguments.nodes[1], "&", "querystring.stringify", loc); + const eq = qsSepEqArg(lowerer, call.arguments.nodes[2], "=", "querystring.stringify", loc); + const optsNode = call.arguments.nodes[3]; if (optsNode) { if (!ts.isObjectLiteralExpression(optsNode)) { lowerer.noLowering( @@ -181,7 +181,7 @@ export function lowerQuerystringStringifyCall( "the supported form spells the options inline (and the only documented option, encodeURIComponent, has no lowering)", ); } - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index a14f15f9e..3a0e7ccf0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -88,7 +88,7 @@ function questionSignal(lowerer: Lowerer, node: ts.Expression | undefined): ts.E ); } let signal: ts.Expression | null = null; - for (const p of node.properties) { + for (const p of node.properties.nodes) { if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "signal") { signal = p.initializer; continue; @@ -129,7 +129,7 @@ function processOutputOf(lowerer: Lowerer, node: ts.Expression): 0 | 1 | 2 { export function lowerReadlineUiNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!isReadlineUiTyped(lowerer, expr)) return null; const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new Readline with ${args.length} arguments`, expr); } @@ -158,7 +158,7 @@ export function lowerReadlineUiNew(lowerer: Lowerer, expr: ts.NewExpression): Ir "write the options inline: new Readline(stream, { autoCommit: true })", ); } - for (const p of opts.properties) { + for (const p of opts.properties.nodes) { if ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -199,7 +199,7 @@ function lowerReadlineUiCall( ): IrExpr { const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const plan: Record = { cursorTo: ["rl.uiCursorTo", 1, 2], moveCursor: ["rl.uiMoveCursor", 2, 2], @@ -294,7 +294,7 @@ export function lowerReadlineMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64); if ( name === "question" && @@ -521,7 +521,7 @@ function lowerWriteKey( ); } hasKey = true; - for (const p of keyNode.properties) { + for (const p of keyNode.properties.nodes) { const member = ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) ? p.name.text : null; const value = member !== null ? (p as ts.PropertyAssignment).initializer : null; if (member === "name" && value !== null && !isNullish(value)) { @@ -568,7 +568,7 @@ function lowerWriteKey( * take the interface handle (0 for none) and the timeout (NaN: the * interface's, or 500). */ function lowerEmitKeypressEvents(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`readline.emitKeypressEvents with ${args.length} arguments`, expr); } @@ -601,7 +601,7 @@ function lowerEmitKeypressEvents(lowerer: Lowerer, expr: ts.CallExpression, loc: if (isReadlineTyped(lowerer, ifaceNode)) { iface = lowerer.lowerExprExpecting(ifaceNode, F64); } else if (ts.isObjectLiteralExpression(ifaceNode)) { - for (const p of ifaceNode.properties) { + for (const p of ifaceNode.properties.nodes) { if ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -708,7 +708,7 @@ export function lowerReadlineCursorCall( if (member === "emitKeypressEvents") return lowerEmitKeypressEvents(lowerer, expr, loc); const kind = CURSOR_HELPERS[member]; if (kind === undefined) return null; - const args = [...expr.arguments]; + const args = [...expr.arguments.nodes]; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`readline.${member} with a spread argument`, expr); } @@ -983,7 +983,7 @@ export function lowerReadlineCreateCall( }); terminal = { kind: "varRef", localId: local.id, type: BOOL, loc }; }; - const optsNode = expr.arguments[0]; + const optsNode = expr.arguments.nodes[0]; if (optsNode === undefined) { lowerer.noLowering( "createInterface without arguments", @@ -993,33 +993,34 @@ export function lowerReadlineCreateCall( } if (!ts.isObjectLiteralExpression(optsNode)) { // createInterface(input, output?, completer?, terminal?) - if (expr.arguments.length > 4) { + if (expr.arguments.nodes.length > 4) { lowerer.noLowering( - `createInterface with ${expr.arguments.length} arguments`, - expr.arguments[4]!, + `createInterface with ${expr.arguments.nodes.length} arguments`, + expr.arguments.nodes[4]!, "pass createInterface(input, output, completer, terminal) or one options object", ); } input = lowerInput(lowerer, optsNode); - if (expr.arguments[1] !== undefined) output = lowerOutput(lowerer, expr.arguments[1]); - const completerNode = expr.arguments[2]; + if (expr.arguments.nodes[1] !== undefined) + output = lowerOutput(lowerer, expr.arguments.nodes[1]); + const completerNode = expr.arguments.nodes[2]; if ( completerNode !== undefined && !(ts.isIdentifier(completerNode) && completerNode.text === "undefined") ) { setCompleter(completerNode); } - if (expr.arguments[3] !== undefined) setTerminal(expr.arguments[3]); + if (expr.arguments.nodes[3] !== undefined) setTerminal(expr.arguments.nodes[3]); } else { - if (expr.arguments.length > 1) { + if (expr.arguments.nodes.length > 1) { lowerer.noLowering( "createInterface with more than one argument", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "pass every option in the one options object", ); } let sawInput = false; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const shorthand = ts.isShorthandPropertyAssignment(p); if (!(shorthand || (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)))) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index 8b951e96c..1cf547be9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -23,13 +23,13 @@ export function lowerReflectCall( !call.questionDotToken && !access.questionDotToken && lowerer.stdlibGlobalMember(access, "Reflect") === "ownKeys" && - call.arguments.length === 1 && - !ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + !ts.isSpreadElement(call.arguments.nodes[0]!) ) { return { kind: "libCall", fn: "dyn.ownKeys", - args: [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], type: DYN, loc: locOf(call), }; @@ -39,9 +39,9 @@ export function lowerReflectCall( if (reflectMember === "get" || reflectMember === "set") { const required = reflectMember === "get" ? 2 : 3; if ( - call.arguments.length < required || - call.arguments.length > required + 1 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < required || + call.arguments.nodes.length > required + 1 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `Reflect.${reflectMember} with this argument shape`, @@ -50,9 +50,11 @@ export function lowerReflectCall( ); } const loc = locOf(call); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); const target = - call.arguments.length === required ? lowerer.declareHiddenLocal("%reflectTarget", DYN) : null; + call.arguments.nodes.length === required + ? lowerer.declareHiddenLocal("%reflectTarget", DYN) + : null; const operation: IrExpr = { kind: "libCall", fn: reflectMember === "get" ? "dyn.reflectGet" : "dyn.reflectSet", @@ -76,16 +78,16 @@ export function lowerReflectCall( const loc = locOf(call); const fenceHint = "the lowered form is Reflect.apply(path.join | path.resolve, , args) — call other functions directly (f(...args))"; - if (call.arguments.length !== 3 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("Reflect.apply with this argument shape", call, fenceHint); } - const targetNode = call.arguments[0]!; + const targetNode = call.arguments.nodes[0]!; const bi = ts.isPropertyAccessExpression(targetNode) ? lowerer.builtinMemberOf(targetNode) : null; const fn = bi ? builtinModuleFnOf(lowerer, bi.module, bi.member) : null; if (!fn || fn.variadicPack !== true) { const target = lowerer.lowerExprExpecting(targetNode, DYN); - const receiver = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); - const argumentsList = lowerer.lowerExprExpecting(call.arguments[2]!, DYN); + const receiver = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); + const argumentsList = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN); return { kind: "libCall", fn: "dyn.reflectApply", @@ -94,7 +96,7 @@ export function lowerReflectCall( loc, }; } - const thisNode = call.arguments[1]!; + const thisNode = call.arguments.nodes[1]!; const effectFree = thisNode.kind === ts.SyntaxKind.ThisKeyword || ts.isIdentifier(thisNode) || @@ -107,6 +109,6 @@ export function lowerReflectCall( "the target ignores its receiver, so only effect-free spellings drop honestly (`this`, a binding, null, undefined)", ); } - const packed = lowerer.lowerExprExpecting(call.arguments[2]!, arrayOf(STRING)); + const packed = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, arrayOf(STRING)); return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/strings.ts b/packages/compiler/src/frontend/lowering/builtins/strings.ts index b117c24ff..013cd3fda 100644 --- a/packages/compiler/src/frontend/lowering/builtins/strings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/strings.ts @@ -29,13 +29,16 @@ export function lowerStringStaticCall( // literal, the fromCharCode convention. The runtime interleaves per // the spec's loop. if (member === "raw") { - if (call.arguments.length < 1 || call.arguments.some((a) => ts.isSpreadElement(a))) { + if ( + call.arguments.nodes.length < 1 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { lowerer.noLowering( - `String.raw with ${call.arguments.length === 0 ? "no template" : "spread substitutions"}`, + `String.raw with ${call.arguments.nodes.length === 0 ? "no template" : "spread substitutions"}`, call, ); } - const tmplNode = call.arguments[0]!; + const tmplNode = call.arguments.nodes[0]!; const tmpl = lowerer.lowerExpr(tmplNode); const rawT = arrayOf(STRING); let raw: IrExpr | null = null; @@ -60,7 +63,7 @@ export function lowerStringStaticCall( "the template must carry a string[] `raw` member: String.raw({ raw: [...] }, ...subs)", ); } - const subs = call.arguments.slice(1).map((a): IrExpr => { + const subs = call.arguments.nodes.slice(1).map((a): IrExpr => { const v = lowerer.lowerExpr(a); if (isUnitType(v.type)) return lowerer.ensureString(v, a); if (v.type.kind === "string") return v; @@ -76,9 +79,9 @@ export function lowerStringStaticCall( const packedSubs: IrExpr = { kind: "arrayLit", elems: subs, type: rawT, loc }; return { kind: "libCall", fn: "string.raw", args: [raw, packedSubs], type: STRING, loc }; } - const spread = call.arguments.find(ts.isSpreadElement); + const spread = call.arguments.nodes.find(ts.isSpreadElement); if (spread) { - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( `String.${member} with a mixed spread call`, call, @@ -100,7 +103,7 @@ export function lowerStringStaticCall( } // Each code converts with ToNumber: a missing value is NaN (code unit 0 // for fromCharCode, a RangeError for fromCodePoint), exactly Node. - const elems = call.arguments.map((a) => lowerToNumberArgument(lowerer, a)); + const elems = call.arguments.nodes.map((a) => lowerToNumberArgument(lowerer, a)); const packed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc }; } @@ -119,7 +122,7 @@ export function lowerStringLastIndexOfCall( if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "lastIndexOf with this argument shape", call, @@ -127,12 +130,12 @@ export function lowerStringLastIndexOfCall( ); } const receiver = lowerer.lowerExprExpecting(access.expression, STRING); - const positionNode = call.arguments[1]; + const positionNode = call.arguments.nodes[1]; if (!positionNode) { - const needle = lowerStringSearchArgument(lowerer, call.arguments[0], loc); + const needle = lowerStringSearchArgument(lowerer, call.arguments.nodes[0], loc); return { kind: "libCall", fn: "string.lastIndexOf", args: [receiver, needle], type: F64, loc }; } - const needle = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const needle = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); const position = lowerOptionalArgument( lowerer, positionNode, diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 3adf1a3b6..a5f34f44e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -37,9 +37,9 @@ export function lowerStringDecoderMethodCall( const loc = locOf(call); if ( (name !== "write" && name !== "end") || - call.arguments.length > 1 || - (name === "write" && call.arguments.length !== 1) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 1 || + (name === "write" && call.arguments.nodes.length !== 1) || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `StringDecoder.${name}`, @@ -51,7 +51,7 @@ export function lowerStringDecoderMethodCall( const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "record") lowerer.badType(access.expression, lowerer.typeOf(access.expression)); - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "call", callee: stringDecoderHelper(lowerer, "end", receiver.type.shapeId, loc), @@ -60,7 +60,7 @@ export function lowerStringDecoderMethodCall( loc, }; } - const node = call.arguments[0]!; + const node = call.arguments.nodes[0]!; const undefinedArg = name === "end" ? lowerStaticallyUndefinedArgument(lowerer, node) : null; if (undefinedArg) { const saved = lowerer.declareHiddenLocal("%strdecRecv", receiver.type); @@ -287,7 +287,7 @@ export function lowerTextCodecNew( ctor: ts.NewExpression, cls: TextCodecCtor["cls"], ): IrExpr { - const args = ctor.arguments ?? []; + const args = ctor.arguments?.nodes ?? []; const loc = locOf(ctor); let encoding: IrExpr = { kind: "numLit", value: -1, type: F64, loc }; let label: IrExpr | null = null; @@ -317,7 +317,7 @@ export function lowerTextCodecNew( if (options) { if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering("TextDecoder options outside an object literal", options); - for (const property of options.properties) { + for (const property of options.properties.nodes) { if ( (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) || !ts.isIdentifier(property.name) || @@ -380,16 +380,16 @@ function lowerStoredTextCodecCall( const cls = storedTextCodecClassOf(lowerer, access.expression); if (cls === null || !lowerer.isStdlibMember(access)) return null; if (access.name.text !== (cls === "TextEncoder" ? "encode" : "decode")) return null; - if (call.arguments.length > (cls === "TextDecoder" ? 2 : 1)) { + if (call.arguments.nodes.length > (cls === "TextDecoder" ? 2 : 1)) { lowerer.noLowering(`${cls}.${access.name.text} with extra arguments`, call); } const loc = locOf(call); let stream: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; - if (cls === "TextDecoder" && call.arguments[1]) { - const options = stripTypeCasts(call.arguments[1]); + if (cls === "TextDecoder" && call.arguments.nodes[1]) { + const options = stripTypeCasts(call.arguments.nodes[1]); if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering("TextDecoder.decode options outside an object literal", options); - for (const property of options.properties) { + for (const property of options.properties.nodes) { if ( (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) || !ts.isIdentifier(property.name) || @@ -406,13 +406,13 @@ function lowerStoredTextCodecCall( if (receiver.type.kind !== "record") lowerer.badType(access.expression, lowerer.typeOf(access.expression)); let arg: IrExpr = - call.arguments.length === 0 + call.arguments.nodes.length === 0 ? cls === "TextEncoder" ? strLit("", loc) : { kind: "bytesNew", source: null, type: BYTES_U8, loc } : cls === "TextEncoder" - ? lowerer.lowerExprExpecting(call.arguments[0]!, STRING) - : lowerer.lowerExpr(call.arguments[0]!); + ? lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING) + : lowerer.lowerExpr(call.arguments.nodes[0]!); if ( cls === "TextDecoder" && (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) @@ -420,7 +420,7 @@ function lowerStoredTextCodecCall( arg = { kind: "libCall", fn: "bytes.bufferSource", - args: [lowerer.coerceInto(call.arguments[0]!, arg, DYN)], + args: [lowerer.coerceInto(call.arguments.nodes[0]!, arg, DYN)], type: BYTES_U8, loc, }; @@ -542,14 +542,14 @@ export function lowerTextCodecCall( storedTextCodecClassOf(lowerer, access.expression) === "TextEncoder" && lowerer.isStdlibMember(access) ) { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("TextEncoder.encodeInto with this argument shape", call); const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const result: IrExpr = { kind: "libCall", fn: "text.encodeInto", - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(stripTypeCasts(arg), DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(stripTypeCasts(arg), DYN)), type: DYN, loc, }; @@ -569,7 +569,10 @@ export function lowerTextCodecCall( if (info === null) return lowerStoredTextCodecCall(lowerer, call, access); if (!lowerer.isStdlibMember(access)) return null; const { cls, ctor: recv } = info; - if (cls === "TextDecoder" && ((recv.arguments?.length ?? 0) > 1 || call.arguments.length > 1)) + if ( + cls === "TextDecoder" && + ((recv.arguments?.nodes.length ?? 0) > 1 || call.arguments.nodes.length > 1) + ) return lowerStoredTextCodecCall(lowerer, call, access); if ( !(cls === "TextDecoder" && member === "decode") && @@ -578,7 +581,7 @@ export function lowerTextCodecCall( return null; } const loc = locOf(call); - const ctorArgs = recv.arguments ?? []; + const ctorArgs = recv.arguments?.nodes ?? []; if (cls === "TextDecoder") { // The label must be statically known. A literal-typed effectful inline // expression is accepted, but its evaluation is sequenced before the @@ -605,18 +608,18 @@ export function lowerTextCodecCall( type: result.type, loc, }; - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { // decode() with no input is "" per spec — nothing to evaluate. return afterLabel({ kind: "strLit", value: "", type: STRING, loc }); } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( "decode with a stream option", call, "streaming decode has no lowering — decode whole buffers", ); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let arg = lowerer.lowerExpr(argNode); if (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) { arg = { @@ -649,13 +652,13 @@ export function lowerTextCodecCall( if (ctorArgs.length > 0) { lowerer.noLowering("new TextEncoder with arguments", recv); } - if (call.arguments.length > 1) { - lowerer.noLowering(`TextEncoder.encode with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`TextEncoder.encode with ${call.arguments.nodes.length} arguments`, call); } const s = - call.arguments.length === 0 + call.arguments.nodes.length === 0 ? strLit("", loc) - : lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + : lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); const enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; return { kind: "libCall", fn: "buffer.fromStr", args: [s, enc], type: BYTES_U8, loc }; } @@ -667,15 +670,15 @@ export function lowerBufferEncodingCall( fn: BuiltinModuleFn, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("buffer encoding call with spread arguments", expr); const arity = bi.member === "transcode" ? 3 : 1; const args = Array.from({ length: arity }, (_, index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(stripTypeCasts(expr.arguments[index]!), DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(stripTypeCasts(expr.arguments.nodes[index]!), DYN) : dynUndefinedExpr(loc), ); - if (expr.arguments.length > arity) + if (expr.arguments.nodes.length > arity) lowerer.noLowering("buffer encoding call with extra arguments", expr); return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 98624dc4a..dabfb1d2a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -64,18 +64,18 @@ export function lowerTimersPromisesSetInterval( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "timers/promises" || bi.member !== "setInterval") return null; - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", expr, "spread arguments"); } - if (expr.arguments.length !== 2) { + if (expr.arguments.nodes.length !== 2) { lowerer.noLowering( - `timers/promises.setInterval with ${expr.arguments.length} arguments`, + `timers/promises.setInterval with ${expr.arguments.nodes.length} arguments`, expr, "the lowered form is setInterval(delay, value) with an explicit yielded value; AbortSignal options are not supported yet", ); } - const delayNode = expr.arguments[0]!; - const valueNode = expr.arguments[1]!; + const delayNode = expr.arguments.nodes[0]!; + const valueNode = expr.arguments.nodes[1]!; const delay = lowerer.lowerExpr(delayNode); const ms = delay.kind === "unitLit" @@ -184,7 +184,7 @@ export function lowerTimeoutMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if ((name === "unref" || name === "ref") && call.arguments.length === 0) { + if ((name === "unref" || name === "ref") && call.arguments.nodes.length === 0) { const handle = lowerer.lowerExprExpecting(access.expression, F64); const fn = isImmediate ? name === "unref" @@ -197,12 +197,12 @@ export function lowerTimeoutMethodCall( // returns the Timeout/Immediate); the libCall yields the f64 back. return { kind: "libCall", fn, args: [handle], type: F64, loc }; } - if (name === "hasRef" && call.arguments.length === 0) { + if (name === "hasRef" && call.arguments.nodes.length === 0) { const handle = lowerer.lowerExprExpecting(access.expression, F64); const fn = isImmediate ? "timers.immediateHasRef" : "timers.hasRef"; return { kind: "libCall", fn, args: [handle], type: BOOL, loc }; } - if (name === "refresh" && call.arguments.length === 0 && !isImmediate) { + if (name === "refresh" && call.arguments.nodes.length === 0 && !isImmediate) { // Re-arms to now + the original delay; yields the handle back for // chaining, like unref/ref. const handle = lowerer.lowerExprExpecting(access.expression, F64); @@ -237,11 +237,11 @@ export function lowerTimersPromisesCall( return lowerSchedulerCall(lowerer, expr, bi.member, loc); if (bi.member !== "setTimeout" && bi.member !== "setImmediate") return null; const timeout = bi.member === "setTimeout"; - const [valueNode, optionsNode] = timeout ? expr.arguments.slice(1) : expr.arguments; + const [valueNode, optionsNode] = timeout ? expr.arguments.nodes.slice(1) : expr.arguments.nodes; const ms: IrExpr | null = !timeout ? null - : expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], F64) + : expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], F64) : { kind: "numLit", value: 1, type: F64, loc }; if (valueNode === undefined) { return timeout @@ -311,7 +311,7 @@ function lowerSchedulerCall( // The scheduler.wait libCalls emit timers/promises setTimeout's runtime // calls; mock.timers mocks scheduler.wait and leaves setTimeout's named // and namespace imports real. An omitted delay is NaN, Node's undefined. - const [delayNode, optionsNode] = expr.arguments; + const [delayNode, optionsNode] = expr.arguments.nodes; const ms: IrExpr = delayNode ? lowerer.lowerExprExpecting(delayNode, F64) : { kind: "numLit", value: NaN, type: F64, loc }; @@ -355,7 +355,7 @@ function timersPromisesOptions( } let ref: IrExpr = absent; let signal: IrExpr | null = null; - for (const prop of optionsNode.properties) { + for (const prop of optionsNode.properties.nodes) { const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null; if ( name === "signal" && diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index daf857650..3de973335 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -159,7 +159,7 @@ export function lowerUrlMethodCall( if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "url") return null; if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; - if ((name === "toString" || name === "toJSON") && call.arguments.length === 0) { + if ((name === "toString" || name === "toJSON") && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn: "url.href", args: [receiver], type: STRING, loc: locOf(call) }; } @@ -185,7 +185,7 @@ export function lowerSearchParamsNew( expr: ts.NewExpression, loc: SrcLoc, ): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering( `new URLSearchParams with ${args.length} arguments`, @@ -203,7 +203,7 @@ export function lowerSearchParamsNew( // type already rejects spreads' surprises, but keep the fence tight). if (ts.isObjectLiteralExpression(arg)) { let acc: IrExpr = { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc }; - for (const prop of arg.properties) { + for (const prop of arg.properties.nodes) { if (!ts.isPropertyAssignment(prop) || prop.name === undefined) { lowerer.unsupported( "SC1090", @@ -450,7 +450,7 @@ export function lowerSearchParamsMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; // The WHATWG arity ladder: too few arguments throw Node's // ERR_MISSING_ARGS before any conversion (the invalid-input probes' // `params.get()`). Claimed for effect-free receivers only — the @@ -661,7 +661,7 @@ function lowerSpForEachCall( access: ts.PropertyAccessExpression, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const fnArg = lowerer.lowerExpr(argNode); if ( fnArg.type.kind !== "func" || @@ -758,7 +758,7 @@ export function lowerFileUrlCall( loc: SrcLoc, hasSpread: boolean, ): IrExpr { - if (hasSpread || expr.arguments.length > 2) { + if (hasSpread || expr.arguments.nodes.length > 2) { lowerer.noLowering( `${bi.member} with these arguments`, expr, @@ -767,8 +767,8 @@ export function lowerFileUrlCall( } // Keep the established typed one-argument paths free of boxing. The // checked variants preserve each converter's validation/getter order. - if (expr.arguments.length === 1 && bi.member !== "fileURLToPathBuffer") { - const arg = lowerer.lowerExpr(expr.arguments[0]!); + if (expr.arguments.nodes.length === 1 && bi.member !== "fileURLToPathBuffer") { + const arg = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (bi.member === "fileURLToPath") { if (arg.type.kind === "url") return { kind: "libCall", fn: "url.fileURLToPathUrl", args: [arg], type: STRING, loc }; @@ -792,8 +792,8 @@ export function lowerFileUrlCall( }; } const args = [0, 1].map((index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; @@ -818,7 +818,7 @@ export function lowerLegacyUrlCall( ): IrExpr { const arity = bi.member === "parse" ? 3 : bi.member === "format" || bi.member === "resolve" ? 2 : 1; - if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length > arity) { + if (expr.arguments.nodes.some(ts.isSpreadElement) || expr.arguments.nodes.length > arity) { lowerer.noLowering( `url.${bi.member} with these arguments`, expr, @@ -827,7 +827,7 @@ export function lowerLegacyUrlCall( } const args: IrExpr[] = []; for (let index = 0; index < arity; index++) { - let arg = expr.arguments[index]; + let arg = expr.arguments.nodes[index]; while (arg && ts.isParenthesizedExpression(arg)) arg = arg.expression; // An inline urlObject or options literal builds as the checked-dynamic // object format() reads (nested query objects included). @@ -840,7 +840,7 @@ export function lowerLegacyUrlCall( ); } if (bi.member === "domainToASCII" || bi.member === "domainToUnicode") { - args.push({ kind: "boolLit", value: expr.arguments.length === 0, type: BOOL, loc }); + args.push({ kind: "boolLit", value: expr.arguments.nodes.length === 0, type: BOOL, loc }); } else { const warn = !insideNodeModules(expr.getSourceFile().fileName); args.push({ kind: "boolLit", value: warn, type: BOOL, loc }); @@ -855,7 +855,7 @@ export function isSearchParamsIterable(lowerer: Lowerer, node: ts.Expression): b if (lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "searchParams") return true; if ( !ts.isCallExpression(node) || - node.arguments.length !== 0 || + node.arguments.nodes.length !== 0 || !ts.isPropertyAccessExpression(node.expression) ) return false; diff --git a/packages/compiler/src/frontend/lowering/builtins/util-types.ts b/packages/compiler/src/frontend/lowering/builtins/util-types.ts index 4399c730e..2ca8e934c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/util-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/util-types.ts @@ -10,15 +10,15 @@ export function lowerUtilTypeCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with spread arguments`, expr); - const first = expr.arguments[0]; + const first = expr.arguments.nodes[0]; if (first !== undefined && lowerer.mapTypeOf(lowerer.typeOf(first))?.kind === "date") { // A Date value has no checked-dynamic form; its static type decides // every brand. The arguments still evaluate in order. return { kind: "seqExpr", - stmts: expr.arguments.map((arg): IrStmt => ({ + stmts: expr.arguments.nodes.map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc, @@ -28,15 +28,15 @@ export function lowerUtilTypeCall( loc, }; } - const input = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const input = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); const saved = lowerer.declareHiddenLocal("%utilTypeInput", DYN); return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: saved.id, init: input, loc }, - ...expr.arguments + ...expr.arguments.nodes .slice(1) .map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc })), ], diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index 2311e0f96..aa826cc53 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -40,7 +40,7 @@ export function lowerWorkerMetadata(member: string, loc: SrcLoc): IrExpr | null export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): IrExpr | null { const constructor = expression.expression; if (!isWorkerConstructor(lowerer.program, constructor)) return null; - const args = expression.arguments ?? []; + const args = expression.arguments?.nodes ?? []; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( "Worker constructor arguments", @@ -125,7 +125,7 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): function capturesStdio(options: ts.Expression | undefined): boolean { if (!options) return false; if (!ts.isObjectLiteralExpression(options)) return true; - return options.properties.some((option) => { + return options.properties.nodes.some((option) => { if (ts.isSpreadAssignment(option)) return true; if (!option.name) return false; const name = propertyNameText(option.name); @@ -146,11 +146,11 @@ const HEAP_LIMITS = new Set([ * with numbers (Node ignores other values; the runtime refuses the rest). */ function refuseHeapLimits(lowerer: Lowerer, options: ts.Expression | undefined): void { if (!options || !ts.isObjectLiteralExpression(options)) return; - for (const option of options.properties) { + for (const option of options.properties.nodes) { if (!ts.isPropertyAssignment(option) || propertyNameText(option.name) !== "resourceLimits") continue; if (!ts.isObjectLiteralExpression(option.initializer)) continue; - for (const limit of option.initializer.properties) { + for (const limit of option.initializer.properties.nodes) { if (!ts.isPropertyAssignment(limit)) continue; const name = propertyNameText(limit.name); if (name === undefined || !HEAP_LIMITS.has(name)) continue; @@ -194,7 +194,7 @@ export function lowerMessageChannelNew( expression: ts.NewExpression, ): IrExpr | null { if (!isWorkerThreadsClass(lowerer, expression, "MessageChannel")) return null; - if ((expression.arguments ?? []).length > 0) { + if ((expression.arguments?.nodes ?? []).length > 0) { lowerer.noLowering( "MessageChannel constructor arguments", expression, @@ -214,7 +214,7 @@ export function lowerMessageChannelNew( /** receiveMessageOnPort(port): { message } or undefined. */ export function lowerReceiveMessageOnPort(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`receiveMessageOnPort with ${args.length} arguments`, call); } @@ -244,7 +244,7 @@ export function lowerBroadcastChannelNew( expression: ts.NewExpression, ): IrExpr | null { if (!isWorkerThreadsClass(lowerer, expression, "BroadcastChannel")) return null; - const args = expression.arguments ?? []; + const args = expression.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `BroadcastChannel constructor with ${args.length} arguments`, @@ -296,7 +296,7 @@ export function lowerEnvironmentData( call: ts.CallExpression, member: "setEnvironmentData" | "getEnvironmentData", ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; const arity = member === "setEnvironmentData" ? 2 : 1; if (args.length > arity || args.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with ${args.length} arguments`, call); @@ -331,7 +331,7 @@ export function lowerEnvironmentData( /** markAsUncloneable(value): structuredClone and postMessage then throw * DataCloneError for the value (scr_json.c). It needs no worker runtime. */ export function lowerUncloneable(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`markAsUncloneable with ${args.length} arguments`, call); } @@ -354,7 +354,7 @@ export function lowerUntransferable( call: ts.CallExpression, member: "markAsUntransferable" | "isMarkedAsUntransferable", ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with ${args.length} arguments`, call); } diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 3119fa2ab..f5600b033 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -504,9 +504,9 @@ export function completeFuncValueArgs( mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required", })); shapes.push({ type: rest, mode: "rest" }); - return completeArgs(lowerer, call.arguments, shapes, loc, call); + return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call); } - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => lowerer.lowerExprExpecting(arg, funcType.params[index]), ); for (let i = args.length; i < funcType.params.length; i++) { @@ -553,7 +553,7 @@ export function lowerSpreadArgsCall( !(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined) ) return null; - if (!expr.arguments.some((a) => ts.isSpreadElement(a))) return null; + if (!expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) return null; if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null; const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression) @@ -563,7 +563,7 @@ export function lowerSpreadArgsCall( : "value"; // Lower every argument ONCE, in source order (the IR nests them in // exactly this order, so runtime evaluation order is JS's). - const parts = expr.arguments.map((a) => + const parts = expr.arguments.nodes.map((a) => ts.isSpreadElement(a) ? { spreadOf: a.expression, node: null, v: lowerer.lowerExpr(a.expression) } : { spreadOf: null, node: a as ts.Expression, v: lowerer.lowerExpr(a) }, diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 9375576f7..9d9c9668c 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -189,7 +189,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa executor && (ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) && ts.isNewExpression(construction) && - construction.arguments?.[0] === executor && + construction.arguments?.nodes[0] === executor && lowerer.isStdlibGlobal(construction.expression, "Promise"); if ( !nativeExecutor && @@ -634,10 +634,10 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // body (run()/discover() skip by the same test). Generic mixins still // register their generic signature below: non-mixin-shaped calls // degrade to the generic machinery's own per-site fences. - if (!decl.typeParameters && mixinFnShapeOf(lowerer, decl)) return; - const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + if (!decl.typeParameters?.nodes && mixinFnShapeOf(lowerer, decl)) return; + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = decl.asteriskToken !== undefined; - if (decl.typeParameters) { + if (decl.typeParameters?.nodes) { // Generic async composes: each monomorphized instance is an async // IrFunction like any other — its own spawn wrapper, its body // returning the resolved promise's inner (lowerGenericInstance). @@ -669,14 +669,14 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara } } - const params = lowerer.paramShapes(decl.parameters); + const params = lowerer.paramShapes(decl.parameters.nodes); // The zero-parameter declaration retains its dynRest ABI; value // functions use the full arguments slot even with no named parameters. appendArgumentsParameter( lowerer, decl, params, - decl.parameters.length > 0 ? "arguments" : "dynRest", + decl.parameters.nodes.length > 0 ? "arguments" : "dynRest", ); const nameBlame: ts.Node = decl.name ?? decl; const returnType = lowerer.declaredReturnType(decl, nameBlame); @@ -728,7 +728,7 @@ function appendArgumentsParameter( !bodyReadsArguments(node) ) return; - if (node.parameters.length > 0 || ts.isMethodDeclaration(node)) { + if (node.parameters.nodes.length > 0 || ts.isMethodDeclaration(node)) { const classMethod = ts.isMethodDeclaration(node) && (ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent)); @@ -765,7 +765,9 @@ export function lambdaSignature( isJsSourceFile(node.getSourceFile()) && !node.type && (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && - node.parameters.every((param) => !param.type && !param.dotDotDotToken && !param.initializer) && + node.parameters.nodes.every( + (param) => !param.type && !param.dotDotDotToken && !param.initializer, + ) && ts.isBinaryExpression(node.parent) && node.parent.right === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && @@ -775,11 +777,14 @@ export function lambdaSignature( ) { const receiver = lowerer.lowerExpr(node.parent.left.expression); if (receiver.type.kind === "dyn") { - const shapes: ParamShape[] = node.parameters.map(() => ({ type: DYN, mode: "required" })); + const shapes: ParamShape[] = node.parameters.nodes.map(() => ({ + type: DYN, + mode: "required", + })); return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) }; } } - if (node.typeParameters && !lowerer.erasingLambdas.has(node)) { + if (node.typeParameters?.nodes && !lowerer.erasingLambdas.has(node)) { // Generic function-like forms monomorphize only where a static home // exists: top-level generic function declarations, generic methods // (class and object-literal), and module-scope never-reassigned @@ -797,14 +802,14 @@ export function lambdaSignature( : "generic arrow/function expressions outside a never-reassigned module-scope binding (only `const f = (x: T) => ...` bindings and top-level generic function declarations monomorphize)", ); } - const shapes = lowerer.paramShapes(node.parameters); + const shapes = lowerer.paramShapes(node.parameters.nodes); // Error-first callbacks captured by native filesystem adapters receive // the selected runtime overload. Ambient contextual inference can pick // the string overload even when options request a Buffer. if ( (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isCallExpression(node.parent) && - node.parent.arguments.at(-1) === node + node.parent.arguments.nodes.at(-1) === node ) { const builtin = ts.isPropertyAccessExpression(node.parent.expression) ? lowerer.builtinMemberOf(node.parent.expression) @@ -817,7 +822,7 @@ export function lambdaSignature( builtinModuleFnOf(lowerer, builtin.module, builtin.member)?.fn === "fs.callbackCall" ) { for (let index = 0; index < shapes.length; index++) { - if (!node.parameters[index]?.type) { + if (!node.parameters.nodes[index]?.type) { shapes[index]!.type = DYN; if (shapes[index]!.bodyType) shapes[index]!.bodyType = DYN; } @@ -854,7 +859,7 @@ export function lambdaSignature( // that recursive layout instead of first inferring narrower arrays. const isAsyncLike = !ts.isMethodDeclaration(node) && - node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const innerRet = isAsyncLike && ret.kind === "promise" ? ret.inner : ret; // Union-inferred returns adopt too (a mixed-return body inferring a // SUB-union of the slot's union — adopting is a no-op when the two diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index d72792ae7..0fb5aadd3 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -287,12 +287,12 @@ export function isFreshExportsReset(lowerer: Lowerer, stmt: ts.ExpressionStateme targets.push(e.left); e = e.right; } - if (!ts.isObjectLiteralExpression(e) || e.properties.length !== 0) return false; + if (!ts.isObjectLiteralExpression(e) || e.properties.nodes.length !== 0) return false; if (targets.length !== 2) return false; const hasExports = targets.some((t) => isCjsExportsIdentifier(lowerer, t)); const hasModuleExports = targets.some((t) => isCjsModuleExports(lowerer, t)); if (!hasExports || !hasModuleExports) return false; - for (const prior of sf.statements) { + for (const prior of sf.statements.nodes) { if (prior === stmt) break; if (ts.isFunctionDeclaration(prior) || isRequireStatement(prior)) continue; if (ts.isExpressionStatement(prior) && ts.isStringLiteral(prior.expression)) continue; @@ -391,11 +391,11 @@ export function cjsExportCallIgnoresThis( // literal the statement names). let methodEntry: ts.MethodDeclaration | null = null; let shorthandOnly = false; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind !== "table") continue; const table = cjs.obj ?? cjsExportTargetLiteral(cjs.expr.right, sf); - for (const prop of table?.properties ?? []) { + for (const prop of table?.properties?.nodes ?? []) { if (prop.name === undefined || !ts.isIdentifier(prop.name) || prop.name.text !== name) continue; if (ts.isPropertyAssignment(prop)) values.push(prop.initializer); diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 7f31337f5..9b41f0012 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -75,7 +75,7 @@ export function cjsWholeExportValueOf( sf: ts.SourceFile, ): { assign: ts.BinaryExpression; rhs: ts.Expression } | null { let kept: ts.BinaryExpression | null = null; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) kept = cjs.expr; } @@ -241,7 +241,7 @@ export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.Bindin const spec = requireSpecOf(decl.initializer); if (spec === null || canonicalBuiltinModule(spec) !== null) return []; if (!isJsSourceFile(decl.getSourceFile())) return []; - return decl.name.elements.filter( + return decl.name.elements.nodes.filter( (e) => e.name !== undefined && ts.isObjectBindingPattern(e.name) && @@ -324,7 +324,7 @@ export function cjsNamespaceDestructureOf( const dep = lowerer.cjsModuleBindingDepOf(init); if (dep === null) return null; const elements: { name: ts.Identifier; key: string }[] = []; - for (const e of decl.name.elements) { + for (const e of decl.name.elements.nodes) { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return null; if (e.name === undefined || !ts.isIdentifier(e.name)) return null; const key = diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts index f7955fc23..93a1e3260 100644 --- a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -47,7 +47,7 @@ export interface TscInteropBinding { * level: the tsc helper declaration shape. */ function tscHelperDeclName(stmt: ts.Statement): string | null { if (!ts.isVariableStatement(stmt)) return null; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length !== 1) return null; const d = decls[0]!; if (!ts.isIdentifier(d.name) || !HELPERS.has(d.name.text) || d.initializer === undefined) @@ -74,7 +74,7 @@ function tscHelperDeclName(stmt: ts.Statement): string | null { /** True when the dependency stamps itself as an ES module translation. */ function stampsEsModule(dep: ts.SourceFile): boolean { - return dep.statements.some((s) => { + return dep.statements.nodes.some((s) => { if (!ts.isExpressionStatement(s)) return false; if (isEsModuleStamp(s.expression)) return true; const e = s.expression; @@ -106,12 +106,12 @@ export function tscInteropBindingOfDecl( if (!isCjsJsFile(sf, lowerer.program)) return null; let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; - if (!ts.isCallExpression(init) || init.arguments.length !== 1) return null; + if (!ts.isCallExpression(init) || init.arguments.nodes.length !== 1) return null; const callee = init.expression; if (!ts.isIdentifier(callee)) return null; if (callee.text !== "__importDefault" && callee.text !== "__importStar") return null; - if (!sf.statements.some((s) => tscHelperDeclName(s) === callee.text)) return null; - const spec = requireSpecOf(init.arguments[0]!); + if (!sf.statements.nodes.some((s) => tscHelperDeclName(s) === callee.text)) return null; + const spec = requireSpecOf(init.arguments.nodes[0]!); if (spec === null) return null; const dep = resolveImport(lowerer.program, sf, spec, "require") ?? @@ -177,7 +177,7 @@ export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boole if (name === null) return false; const sf = stmt.getSourceFile(); const helperSpans = new Map(); - for (const s of sf.statements) { + for (const s of sf.statements.nodes) { const n = tscHelperDeclName(s); if (n !== null) helperSpans.set(n, s); } diff --git a/packages/compiler/src/frontend/lowering/class-descriptors.ts b/packages/compiler/src/frontend/lowering/class-descriptors.ts index 36e280707..473ed2496 100644 --- a/packages/compiler/src/frontend/lowering/class-descriptors.ts +++ b/packages/compiler/src/frontend/lowering/class-descriptors.ts @@ -45,7 +45,7 @@ export function lowerClassDescriptorRead( ): IrExpr | null { if (!isDynTypedRefType(target.type)) return null; const info = lowerer.classes.get(target.type.className); - const key = call.arguments[1]!; + const key = call.arguments.nodes[1]!; if (!info || runtimeRooted(info) || !ts.isStringLiteral(key) || !bagOnlyProperty(info, key.text)) return null; const loc = locOf(call); @@ -79,7 +79,7 @@ export function lowerClassDataDescriptor( const safeName = (owner: ClassInfo, name: string): boolean => bagOnlyProperty(owner, name); const descriptor = (node: ts.Expression): boolean => ts.isObjectLiteralExpression(node) && - node.properties.every( + node.properties.nodes.every( (p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p) || @@ -88,15 +88,15 @@ export function lowerClassDataDescriptor( literalName(p.name) ?? "", ), ); - const descriptors = call.arguments[member === "defineProperty" ? 2 : 1]!; + const descriptors = call.arguments.nodes[member === "defineProperty" ? 2 : 1]!; if (member === "defineProperty") { - const key = call.arguments[1]!; + const key = call.arguments.nodes[1]!; if ((ts.isStringLiteral(key) && !safeName(info, key.text)) || !descriptor(descriptors)) return null; } else { if ( !ts.isObjectLiteralExpression(descriptors) || - !descriptors.properties.every((p) => { + !descriptors.properties.nodes.every((p) => { if (!ts.isPropertyAssignment(p)) return false; const name = literalName(p.name); return ( @@ -127,13 +127,13 @@ export function lowerClassDataDescriptor( { kind: "varDecl" as const, localId: key.id, - init: lowerer.lowerExprExpecting(call.arguments[1]!, DYN), + init: lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), loc, }, ] : []), { kind: "varDecl", localId: desc.id, init: lowerer.lowerExprExpecting(descriptors, DYN), loc }, - ...(key && !ts.isStringLiteral(call.arguments[1]!) + ...(key && !ts.isStringLiteral(call.arguments.nodes[1]!) ? [ { kind: "assign" as const, diff --git a/packages/compiler/src/frontend/lowering/class-instance-constructor.ts b/packages/compiler/src/frontend/lowering/class-instance-constructor.ts index 3b8e79398..a90ea90eb 100644 --- a/packages/compiler/src/frontend/lowering/class-instance-constructor.ts +++ b/packages/compiler/src/frontend/lowering/class-instance-constructor.ts @@ -43,7 +43,7 @@ export function lowerInstanceConstructorNew( const result = checkedClassConstruction( lowerer, constructor, - (expr.arguments ?? []).map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + (expr.arguments?.nodes ?? []).map((argument) => lowerer.lowerExprExpecting(argument, DYN)), loc, ); return { diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 75abeaf57..2a0e3a7eb 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -171,7 +171,7 @@ export function collectClassMethodMutations( const values = unwrap(loop.expression); if (!ts.isArrayLiteralExpression(values)) return null; const names: (string | number)[] = []; - for (const value of values.elements) { + for (const value of values.elements.nodes) { if (ts.isStringLiteralLike(value)) names.push(value.text); else if (ts.isNumericLiteral(value)) names.push(Number(value.text)); else return null; @@ -219,15 +219,15 @@ export function collectClassMethodMutations( const loop = statement.parent; if (!ts.isForOfStatement(loop) || loop.statement !== statement) return false; if (!ts.isVariableDeclarationList(loop.initializer)) return false; - const decl = loop.initializer.declarations[0]; + const decl = loop.initializer.declarations.nodes[0]; if (!decl || !ts.isIdentifier(decl.name) || !sameBinding(key, decl.name)) return false; const keys = unwrap(loop.expression); return ( ts.isCallExpression(keys) && ts.isPropertyAccessExpression(keys.expression) && lowerer.stdlibGlobalMember(keys.expression, "Object") === "keys" && - keys.arguments.length === 1 && - sameBinding(receiver, keys.arguments[0]!) + keys.arguments.nodes.length === 1 && + sameBinding(receiver, keys.arguments.nodes[0]!) ); }; const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { @@ -244,7 +244,7 @@ export function collectClassMethodMutations( computedWrites.push({ key: undefined, receiver, site }); return; } - for (const field of value.properties) { + for (const field of value.properties.nodes) { if (ts.isSpreadAssignment(field)) markFields(field.expression, site, receiver); else if (field.name && (ts.isIdentifier(field.name) || ts.isStringLiteralLike(field.name))) markName(field.name.text, site); @@ -265,12 +265,12 @@ export function collectClassMethodMutations( } else if (ts.isElementAccessExpression(target)) markKey(target.argumentExpression, site, target.expression); else if (ts.isArrayLiteralExpression(target)) { - for (const element of target.elements) { + for (const element of target.elements.nodes) { if (ts.isSpreadElement(element)) markTarget(element.expression, site); else if (!ts.isOmittedExpression(element)) markTarget(element, site); } } else if (ts.isObjectLiteralExpression(target)) { - for (const field of target.properties) { + for (const field of target.properties.nodes) { if (ts.isPropertyAssignment(field)) markTarget(field.initializer, site); else if (ts.isSpreadAssignment(field)) markTarget(field.expression, site); } @@ -323,7 +323,7 @@ export function collectClassMethodMutations( if (!ts.isPropertyAssignment(node) || !ts.isArrowFunction(unwrap(node.initializer))) receiverFields.add(node.name.text); } - if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.modifiers?.length) + if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.modifiers?.nodes.length) receiverFields.add(node.name.text); if ( ts.isMethodDeclaration(node) && @@ -439,12 +439,12 @@ export function collectClassMethodMutations( unknownKey = node; continue; } - const receiver = call.arguments[0]; - if (single && call.arguments[1]) markKey(call.arguments[1], call, receiver); - else if (objectMethod === "defineProperties" && call.arguments[1]) - markFields(call.arguments[1], call, receiver); + const receiver = call.arguments.nodes[0]; + if (single && call.arguments.nodes[1]) markKey(call.arguments.nodes[1], call, receiver); + else if (objectMethod === "defineProperties" && call.arguments.nodes[1]) + markFields(call.arguments.nodes[1], call, receiver); else if (objectMethod === "assign") - for (const source of call.arguments.slice(1)) markFields(source, call, receiver); + for (const source of call.arguments.nodes.slice(1)) markFields(source, call, receiver); } lowerer.checker.prefetchClassCollection( computedWrites.flatMap((write) => [ @@ -468,7 +468,7 @@ export function collectClassMethodMutations( for (const cls of typedClasses) { const symbol = cls.name ? lowerer.checker.getSymbolAtLocation(cls.name) : undefined; if (!symbol) { - const declares = cls.members.some( + const declares = cls.members.nodes.some( (member) => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && @@ -482,7 +482,7 @@ export function collectClassMethodMutations( const property = lowerer.checker.getPropertyOfType(type, name); if (!property || !lowerer.checker.declarationsOf(property).some(ts.isMethodDeclaration)) continue; - if (cls.typeParameters !== undefined && cls.typeParameters.length > 0) { + if (cls.typeParameters !== undefined && cls.typeParameters.nodes.length > 0) { owners = null; break; } @@ -517,7 +517,9 @@ export function collectClassMethodMutations( ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member) ) { - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) + if ( + member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + ) return null; } else return null; const symbol = lowerer.checker.getSymbolAtLocation(cls.name); diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 50278c3b7..10572377e 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -348,7 +348,7 @@ function reflectedMethodValue( if (existing) return existing; const declaration = info.decl; if (!declaration) return null; - const member = declaration.members.find((member) => { + const member = declaration.members.nodes.find((member) => { if ( (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.name || diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index 2f66a5203..c8e56f3cc 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -205,14 +205,15 @@ function protocolSignature( !signature || !declaration || !(ts.isMethodSignatureDeclaration(declaration) || ts.isMethodDeclaration(declaration)) || - (declaration.typeParameters?.length ?? 0) > 0 + (declaration.typeParameters?.nodes.length ?? 0) > 0 ) return null; - const declared = (declaration.parameters ?? []).filter( + const declared = (declaration.parameters.nodes ?? []).filter( (param) => !(ts.isIdentifier(param.name) && param.name.text === "this"), ); const symbols = signature.getParameters(); - if (symbols.length !== declared.length || call.arguments.length !== declared.length) return null; + if (symbols.length !== declared.length || call.arguments.nodes.length !== declared.length) + return null; const records = (id: string) => lowerer.shapes.get(id); const unions = (id: string) => lowerer.unions.get(id); const params: IrType[] = []; @@ -243,7 +244,7 @@ export function lowerProtocolMethodCall( recv.type.kind !== "dyn" || call.questionDotToken || access.questionDotToken || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; let signature: ProtocolSignature | null; @@ -266,11 +267,11 @@ export function lowerProtocolMethodCall( const stmts: IrStmt[] = receiverLocal ? [{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc }] : []; - const args = call.arguments.map((arg, i) => + const args = call.arguments.nodes.map((arg, i) => lowerer.coerceToExpected(lowerer.lowerExprExpecting(arg, params[i]!), params[i]!), ); const probed = - !args.every(effectFree) || !call.arguments.every((arg) => stableArgument(lowerer, arg)); + !args.every(effectFree) || !call.arguments.nodes.every((arg) => stableArgument(lowerer, arg)); const key = JSON.stringify([method, params.map(typeKey), typeKey(result), returnsVoid, probed]); let dispatch = lowerer.protocolDispatches.get(key); if (!dispatch) { diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 3c610c51a..9e69d08b7 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -29,9 +29,10 @@ export class ClassProtocols { node.kind === ts.SyntaxKind.ThisKeyword || ts.forEachChild(node, usesReceiver) === true; const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { - for (const clause of node.heritageClauses ?? []) + for (const clause of node.heritageClauses?.nodes ?? []) if (clause.token === ts.SyntaxKind.ImplementsKeyword) - for (const protocol of clause.types) add(this.checker.getTypeAtLocation(protocol)); + for (const protocol of clause.types.nodes) + add(this.checker.getTypeAtLocation(protocol)); const symbol = node.name ? this.checker.getSymbolAtLocation(node.name) : undefined; const instance = symbol ? this.checker.getDeclaredTypeOfSymbol(symbol) : undefined; for (const property of instance ? this.checker.getPropertiesOfType(instance) : []) { @@ -41,7 +42,7 @@ export class ClassProtocols { .some( (member) => ts.isMethodDeclaration(member) && - !member.modifiers?.some( + !member.modifiers?.nodes.some( (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, ), ) @@ -54,7 +55,7 @@ export class ClassProtocols { } if ( ts.isObjectLiteralExpression(node) && - node.properties.some( + node.properties.nodes.some( (property) => (ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || @@ -167,14 +168,14 @@ export class ClassProtocols { observed.push(node.expression); } else if (ts.isSwitchStatement(node)) { // `switch` matches its cases with ===. - const cases = node.caseBlock.clauses + const cases = node.caseBlock.clauses.nodes .filter(ts.isCaseClause) .map((clause) => clause.expression) .filter((expr) => !isNullishLiteral(expr) && !isPrimitiveLiteral(expr)); if (cases.length > 0) observed.push(node.expression, ...cases); } else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { const callee = node.expression; - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; const member = ts.isPropertyAccessExpression(callee) ? callee.name.text : null; const owner = ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) @@ -204,7 +205,8 @@ export class ClassProtocols { }; const visit = (node: ts.Node): void => { observe(node); - if (ts.isCallExpression(node) || ts.isNewExpression(node)) node.arguments?.forEach(flow); + if (ts.isCallExpression(node) || ts.isNewExpression(node)) + node.arguments?.nodes.forEach(flow); else if (ts.isVariableDeclaration(node) && node.type !== undefined) flow(node.initializer); else if (ts.isPropertyDeclaration(node) && node.type !== undefined) flow(node.initializer); else if (ts.isParameter(node) && node.type !== undefined) flow(node.initializer); @@ -212,7 +214,7 @@ export class ClassProtocols { else if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) flow(node.body); else if (ts.isYieldExpression(node)) flow(node.expression); else if (ts.isArrayLiteralExpression(node)) - node.elements.forEach((element) => { + node.elements.nodes.forEach((element) => { if (!ts.isSpreadElement(element) && !ts.isOmittedExpression(element)) flow(element); }); else if (ts.isPropertyAssignment(node)) flow(node.initializer); @@ -234,7 +236,7 @@ export class ClassProtocols { }); } if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) - node.parameters.forEach((param, index) => { + node.parameters.nodes.forEach((param, index) => { if (param.type !== undefined) callbackParams.push({ fn: node, index, name: param.name }); }); ts.forEachChild(node, visit); diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 1db709e91..403a4a3b4 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -360,7 +360,7 @@ export function reflectClassPrototype( const context = newFnCtx(false, null, null, DYN); lowerer.fnStack.push(context); try { - for (const member of info.decl.members) { + for (const member of info.decl.members.nodes) { if ( (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body || @@ -430,12 +430,12 @@ export function lowerClassPrototypeData( const parent = expr.parent; const descriptorKey = ts.isCallExpression(parent) && - parent.arguments[0] === expr && + parent.arguments.nodes[0] === expr && ts.isPropertyAccessExpression(parent.expression) && lowerer.stdlibGlobalMember(parent.expression, "Object") === "defineProperty" && - parent.arguments[1] && - ts.isStringLiteral(parent.arguments[1]) - ? parent.arguments[1].text + parent.arguments.nodes[1] && + ts.isStringLiteral(parent.arguments.nodes[1]) + ? parent.arguments.nodes[1].text : null; const name = ts.isPropertyAccessExpression(parent) && parent.expression === expr @@ -481,12 +481,12 @@ export function lowerClassPrototypeAssign( lowerer: Lowerer, call: ts.CallExpression, ): IrExpr | null { - const target = call.arguments[0]; + const target = call.arguments.nodes[0]; if (!target || !ts.isPropertyAccessExpression(target) || target.name.text !== "prototype") return null; const info = exactClassOfReceiver(lowerer, target.expression); if (!info) return null; - if (!ts.isExpressionStatement(call.parent) || call.arguments.some(ts.isSpreadElement)) { + if (!ts.isExpressionStatement(call.parent) || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "class prototype assignment results and spread arguments"); } const loc = locOf(call); @@ -507,7 +507,9 @@ export function lowerClassPrototypeAssign( target, "prototype data on generic, mixin, or runtime-provided classes", ); - const sources = call.arguments.slice(1).map((node) => lowerer.lowerExprExpecting(node, DYN)); + const sources = call.arguments.nodes + .slice(1) + .map((node) => lowerer.lowerExprExpecting(node, DYN)); return { kind: "libCall", fn: "dyn.assignPrototype", @@ -685,11 +687,11 @@ export function lowerClassPrototypeDescriptors( call: ts.CallExpression, member: string, ): IrExpr | null { - const target = call.arguments[0]; + const target = call.arguments.nodes[0]; if ( member !== "defineProperties" || - call.arguments.length !== 2 || - call.arguments.some(ts.isSpreadElement) || + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some(ts.isSpreadElement) || !target || !ts.isPropertyAccessExpression(target) || target.name.text !== "prototype" @@ -720,7 +722,7 @@ export function lowerClassPrototypeDescriptors( fn: "dyn.definePrototypeProps", args: [ prototype, - lowerer.lowerExprExpecting(call.arguments[1]!, DYN), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), prototypeProtectedKeys(lowerer, info, target, loc), ], type: DYN, diff --git a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts index cc658f872..310c8f389 100644 --- a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts +++ b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts @@ -65,7 +65,7 @@ export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, valu ); for (const member of [...definitions, ...initializers]) { if (ts.isClassStaticBlockDeclaration(member)) { - statements.push(...lowerer.lowerStmts(member.body.statements)); + statements.push(...lowerer.lowerStmts(member.body.statements.nodes)); continue; } if (!member.name) continue; diff --git a/packages/compiler/src/frontend/lowering/class-static-data.ts b/packages/compiler/src/frontend/lowering/class-static-data.ts index 0e8f64a7b..af5425727 100644 --- a/packages/compiler/src/frontend/lowering/class-static-data.ts +++ b/packages/compiler/src/frontend/lowering/class-static-data.ts @@ -46,7 +46,7 @@ export function classStaticDataFor( if (findStaticOn(lowerer, info, name) || findGenericStaticOn(lowerer, info, name)) return null; for (let c: ClassInfo | null = info; c; c = c.base) { if ( - c.decl?.members.some( + c.decl?.members?.nodes.some( (m) => m.name && ts.canHaveModifiers(m) && diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 1ff401b4e..78c7cb73d 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -407,7 +407,7 @@ export function recordToClassPlan( return null; } } - if (!info.ctor || info.ctor.body === undefined || info.ctor.body.statements.length > 0) + if (!info.ctor || info.ctor.body === undefined || info.ctor.body.statements.nodes.length > 0) return null; const props = info.paramProps ?? []; if (props.length !== info.ctorParams.length) return null; diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 9fc1902c7..5449b0943 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -53,7 +53,7 @@ export function collectionInput( if ( ts.isCallExpression(expression) && !expression.questionDotToken && - expression.arguments.length === 0 && + expression.arguments.nodes.length === 0 && ts.isPropertyAccessExpression(expression.expression) && !expression.expression.questionDotToken ) { diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts index e2bd82516..77f72dc15 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts @@ -13,8 +13,8 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo }); if (!decl) throw new Error("fixture has no class"); const cls = decl; - const ctor = cls.members.find(ts.isConstructorDeclaration) ?? null; - const fields = cls.members + const ctor = cls.members.nodes.find(ts.isConstructorDeclaration) ?? null; + const fields = cls.members.nodes .filter(ts.isPropertyDeclaration) .map((member) => ({ name: member.name.getText(), initializer: member.initializer })); const fieldNames = new Set(fields.map((field) => field.name)); @@ -28,7 +28,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo privateMethodBody: (call) => { if (!ts.isPropertyAccessExpression(call.expression)) return null; const name = call.expression.name.text; - const method = cls.members.find( + const method = cls.members.nodes.find( (member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && member.name.getText() === name, ); diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index 484920a1c..b9c8d7e07 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -89,10 +89,11 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) return true; if (ts.isCallExpression(node) && thisMember(node.expression)) { - if (node.arguments.some((arg) => mentionsReceiver(arg) && exposes(arg, depth))) return true; + if (node.arguments.nodes.some((arg) => mentionsReceiver(arg) && exposes(arg, depth))) + return true; const body = depth < HELPER_DEPTH ? inputs.privateMethodBody(node) : null; if (!body) return true; - return !followStatements(body.statements, depth + 1); + return !followStatements(body.statements.nodes, depth + 1); } if (thisMember(node)) { const name = node.name.text; @@ -137,7 +138,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca } return true; }; - const statements = inputs.ctor?.body?.statements ?? []; + const statements = inputs.ctor?.body?.statements?.nodes ?? []; let index = 0; if (inputs.derived) { // Own initializers run when super() returns. A base construction that @@ -149,7 +150,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca ts.isCallExpression(stmt.expression) && stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword; if (isSuper) { - if (stmt.expression.arguments.some((arg) => mentionsReceiver(arg))) return stop(); + if (stmt.expression.arguments.nodes.some((arg) => mentionsReceiver(arg))) return stop(); break; } // super() nested inside other statements: keep the analysis simple. diff --git a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts index b88de3795..a22a82bb1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts @@ -16,7 +16,7 @@ export function lowerArrayCallbackCall( access: ts.ElementAccessExpression, array: IrExpr, ): IrExpr | null { - if (array.type.kind !== "array" || call.arguments.some(ts.isSpreadElement)) return null; + if (array.type.kind !== "array" || call.arguments.nodes.some(ts.isSpreadElement)) return null; const elem = array.type.elem; const arms = elem.kind === "union" ? (lowerer.unions.get(elem.unionId)?.arms ?? []) : [elem]; const functions = arms.filter((arm) => arm.kind === "func"); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index a79864983..2e93e2fd5 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -74,7 +74,7 @@ export function lowerArrayHofCall( ): IrExpr { const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const booleanFilter = method === "filter" && @@ -341,7 +341,7 @@ function lowerFastCallback( // The optional-read prepass binds an unannotated element parameter to // the array's runtime value type. Rebind it to the plain element for // this lowering only. - const param = argNode.parameters[elemIndex]; + const param = argNode.parameters.nodes[elemIndex]; if (!param || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name)) return null; const symbol = param.type ? undefined : lowerer.checker.getSymbolAtLocation(param.name); @@ -563,7 +563,7 @@ export function lowerArrayUnionHofCall( if (!lowerer.isStdlibMember(access)) return null; const def = lowerer.unions.get(receiverT.unionId); if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "array")) return null; - if (call.arguments.length !== 1 || !call.arguments[0]) return null; + if (call.arguments.nodes.length !== 1 || !call.arguments.nodes[0]) return null; const arrays = def.arms as (IrType & { kind: "array" })[]; const valueArms: IrType[] = []; const addValueArm = (type: IrType): boolean => { @@ -586,7 +586,7 @@ export function lowerArrayUnionHofCall( valueArms.length === 1 ? valueArms[0]! : { kind: "union", unionId: lowerer.unions.intern(valueArms) }; - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const { fnArg, arity } = lowerArrayCallback(lowerer, argNode, [valueT], receiverT); const fnRet = fnArg.type.ret; if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) { @@ -1025,7 +1025,7 @@ export function lowerArrayFindLikeCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded // find's result must be reachable from the element itself; a predicate // that narrows to another layout keeps the optional element visit. @@ -1495,7 +1495,7 @@ export function lowerArrayFlatMapCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const present = presentElements(lowerer, call, elem); const { fnArg, arity } = lowerArrayCallback( @@ -1779,9 +1779,9 @@ export function lowerArrayReduceCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded - const hasInit = call.arguments.length === 2; + const hasInit = call.arguments.nodes.length === 2; const accT = lowerer.runtimeOptionalReduceTypes.get(call) ?? (hasInit ? lowerer.irTypeOf(call) : arrayValueType(lowerer, elem)); @@ -1818,7 +1818,7 @@ export function lowerArrayReduceCall( fast?.type ?? null, ); const args: IrExpr[] = [receiver, fnArg]; - if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments[1]!, accT)); + if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, accT)); // The fast closure is created last; making a closure has no observable // effect, so the initial value still evaluates right after the callback. if (fast) args.push(fast); @@ -2037,7 +2037,7 @@ export function lowerArraySortCall( const method = access.name.text === "toSorted" ? "toSorted" : "sort"; const copyFirst = method === "toSorted"; if ( - call.arguments.length === 0 && + call.arguments.nodes.length === 0 && (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool") ) { return { @@ -2049,15 +2049,15 @@ export function lowerArraySortCall( loc, }; } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments`, + `.${method} with ${call.arguments.nodes.length} arguments`, call, "default ordering supports string[], number[] and boolean[]; other element types need a comparator", ); } const receiver = lowerer.lowerExpr(access.expression); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, argNode); let unwrappedArg = argNode; while ( @@ -2205,8 +2205,8 @@ export function lowerDynArrayFilterCall( access: ts.PropertyAccessExpression, recv: IrExpr, ): IrExpr | null { - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); // The result element type, from what the call flows INTO: the // contextual array (or the single array arm of a contextual union — @@ -2270,8 +2270,8 @@ export function lowerDynArrayFlatMapCall( access: ts.PropertyAccessExpression, recv: IrExpr, ): IrExpr | null { - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const { fnArg, arity } = lowerArrayCallback(lowerer, argNode, [DYN], DYN); const ret = fnArg.type.ret; @@ -2631,7 +2631,7 @@ export function lowerTuplePredicateCall( reads: IrExpr[], ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const booleanFilter = method === "filter" && ts.isIdentifier(argNode) && diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index 57b8caf14..4f6e4ca43 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -144,7 +144,7 @@ export function arrayConcatHelper( }, ...append(varRef("a.0", arrT, loc), elem, node.expression), ...shape.flatMap((type, i) => - append(varRef(`x.${i}`, type ? arrayOf(type) : elem, loc), type, node.arguments[i]!), + append(varRef(`x.${i}`, type ? arrayOf(type) : elem, loc), type, node.arguments.nodes[i]!), ), { kind: "return", value: out, loc }, ]; diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 015cb2123..6072b58b7 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -448,12 +448,12 @@ export function lowerArrayOfCall( if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) { lowerer.badType(call, lowerer.typeOf(call)); } - if (call.arguments.some(ts.isSpreadElement)) { - return lowerArraySpreadItems(lowerer, call.arguments, result.elem, result, locOf(call)); + if (call.arguments.nodes.some(ts.isSpreadElement)) { + return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call)); } return { kind: "arrayLit", - elems: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), + elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), type: result, loc: locOf(call), }; @@ -795,7 +795,7 @@ export function lowerArrayFromCall( if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null; if (access.name.text !== "from") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if ( isJsSourceFile(call.getSourceFile()) && args.length >= 1 && @@ -884,9 +884,9 @@ export function lowerArrayFromCall( if ( args.length === 1 && ts.isObjectLiteralExpression(args[0]!) && - args[0]!.properties.length === 1 + args[0]!.properties.nodes.length === 1 ) { - const n = lowerLengthProp(lowerer, args[0]!.properties[0]!); + const n = lowerLengthProp(lowerer, args[0]!.properties.nodes[0]!); if (n) { if (n.type.kind !== "f64") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -941,7 +941,7 @@ export function lowerArrayFromCall( const valueT = arrayValueType(lowerer, arrT.elem); const firstParam = ts.isArrowFunction(mapper) || ts.isFunctionExpression(mapper) - ? mapper.parameters[0]?.name + ? mapper.parameters.nodes[0]?.name : undefined; const paramSymbol = firstParam && ts.isIdentifier(firstParam) @@ -1021,8 +1021,10 @@ export function lowerArrayFromCall( ); } const n = - args.length === 2 && ts.isObjectLiteralExpression(args[0]!) && args[0]!.properties.length === 1 - ? lowerLengthProp(lowerer, args[0]!.properties[0]!) + args.length === 2 && + ts.isObjectLiteralExpression(args[0]!) && + args[0]!.properties.nodes.length === 1 + ? lowerLengthProp(lowerer, args[0]!.properties.nodes[0]!) : null; if (!n) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts index db60cdf1b..7aeec62ed 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts @@ -43,7 +43,7 @@ export function lowerArrayFromAsyncCall( if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null; if (access.name.text !== "fromAsync") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `Array.fromAsync with ${args.length} arguments`, diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 40c2e730d..7ef96457c 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -318,7 +318,8 @@ export function lowerArrayMethodCall( if (lowerer.chainBlocked(access, call)) return null; const name = access.name.text; const iteratorMethod = - (name === "values" || name === "keys" || name === "entries") && call.arguments.length === 0; + (name === "values" || name === "keys" || name === "entries") && + call.arguments.nodes.length === 0; if ( !ARRAY_METHODS.has(name) && name !== "sort" && @@ -446,8 +447,8 @@ export function lowerArrayMethodCall( return lowerArraySortCall(lowerer, call, access, elem, receiverIr); } if (name === "toReversed") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`.toReversed with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`.toReversed with ${call.arguments.nodes.length} arguments`, call); } return { kind: "arrIntrinsic", @@ -459,8 +460,8 @@ export function lowerArrayMethodCall( }; } if (name === "reverse") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`.reverse with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`.reverse with ${call.arguments.nodes.length} arguments`, call); } return { kind: "arrIntrinsic", @@ -472,10 +473,10 @@ export function lowerArrayMethodCall( }; } if (name === "fill") { - if (call.arguments.length > 3 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.fill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.fill with ${call.arguments.nodes.length} arguments`, call); } - const value = call.arguments[0] ? lowerer.lowerExpr(call.arguments[0]) : null; + const value = call.arguments.nodes[0] ? lowerer.lowerExpr(call.arguments.nodes[0]) : null; if ( value && value.type.kind === "dyn" && @@ -485,7 +486,7 @@ export function lowerArrayMethodCall( ) { lowerer.noLowering( ".fill of a checked-dynamic reference into a static array", - call.arguments[0]!, + call.arguments.nodes[0]!, ); } const writeUndefined = @@ -495,35 +496,53 @@ export function lowerArrayMethodCall( lowerer.lowerExpr(access.expression), value, writeUndefined, - lowerArrayPosition(lowerer, call.arguments[1], numLit(0, loc), "array fill start"), - lowerArrayPosition(lowerer, call.arguments[2], numLit(Infinity, loc), "array fill end"), + lowerArrayPosition(lowerer, call.arguments.nodes[1], numLit(0, loc), "array fill start"), + lowerArrayPosition(lowerer, call.arguments.nodes[2], numLit(Infinity, loc), "array fill end"), receiverIr, loc, ); } if (name === "copyWithin") { - if (call.arguments.length > 3 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.copyWithin with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.copyWithin with ${call.arguments.nodes.length} arguments`, call); } return lowerArrayCopyWithin( lowerer.lowerExpr(access.expression), - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array copyWithin target"), - lowerArrayPosition(lowerer, call.arguments[1], numLit(0, loc), "array copyWithin start"), - lowerArrayPosition(lowerer, call.arguments[2], numLit(Infinity, loc), "array copyWithin end"), + lowerArrayPosition( + lowerer, + call.arguments.nodes[0], + numLit(0, loc), + "array copyWithin target", + ), + lowerArrayPosition( + lowerer, + call.arguments.nodes[1], + numLit(0, loc), + "array copyWithin start", + ), + lowerArrayPosition( + lowerer, + call.arguments.nodes[2], + numLit(Infinity, loc), + "array copyWithin end", + ), receiverIr, loc, ); } if (name === "with") { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.with with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.with with ${call.arguments.nodes.length} arguments`, call); } const receiver = lowerer.lowerExpr(access.expression); - const index = lowerPositionArgument(lowerer, call.arguments[0]!, numLit(0, loc)); + const index = lowerPositionArgument(lowerer, call.arguments.nodes[0]!, numLit(0, loc)); if (!primitivePositionType(lowerer, index.type)) { - lowerer.noLowering(`.with index of '${lowerer.fmt(index.type)}' values`, call.arguments[0]!); + lowerer.noLowering( + `.with index of '${lowerer.fmt(index.type)}' values`, + call.arguments.nodes[0]!, + ); } - const value = lowerer.lowerExpr(call.arguments[1]!); + const value = lowerer.lowerExpr(call.arguments.nodes[1]!); const receiverLocal = lowerer.declareHiddenLocal("%withRecv", receiverIr); const indexLocal = lowerer.declareHiddenLocal("%withIndex", index.type); const valueLocal = lowerer.declareHiddenLocal("%withValue", value.type); @@ -534,7 +553,7 @@ export function lowerArrayMethodCall( lowerer, indexRef, numLit(0, loc), - call.arguments[0]!, + call.arguments.nodes[0]!, "array with index", ); const stmts: IrStmt[] = [ @@ -588,7 +607,7 @@ export function lowerArrayMethodCall( loc, }; } - const coercedValue = lowerer.coerceInto(call.arguments[1]!, valueRef, elem); + const coercedValue = lowerer.coerceInto(call.arguments.nodes[1]!, valueRef, elem); return { kind: "seqExpr", stmts, @@ -608,17 +627,17 @@ export function lowerArrayMethodCall( const receiver = lowerer.lowerExpr(access.expression); const start = lowerArrayPosition( lowerer, - call.arguments[0], + call.arguments.nodes[0], numLit(0, loc), "array toSpliced start", ); const deleteCount = lowerArrayPosition( lowerer, - call.arguments[1], - numLit(call.arguments.length === 1 ? Infinity : 0, loc), + call.arguments.nodes[1], + numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc), "array toSpliced deleteCount", ); - const itemNodes = call.arguments.slice(2); + const itemNodes = call.arguments.nodes.slice(2); const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( @@ -709,7 +728,7 @@ export function lowerArrayMethodCall( | "reduce" | "reduceRight" ]; - if (call.arguments.length < arity[0]! || call.arguments.length > arity[1]!) { + if (call.arguments.nodes.length < arity[0]! || call.arguments.nodes.length > arity[1]!) { const hint = name === "map" || name === "filter" || @@ -721,7 +740,7 @@ export function lowerArrayMethodCall( ? "the thisArg parameter has no lowering — use an arrow function" : undefined; lowerer.noLowering( - `.${name} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, + `.${name} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, call, hint, ); @@ -742,8 +761,8 @@ export function lowerArrayMethodCall( // order (count snapshotted first, so the self-spread forms duplicate // like JS) and return the new length. const spreadArg = - call.arguments.length === 1 && ts.isSpreadElement(call.arguments[0]!) - ? call.arguments[0]! + call.arguments.nodes.length === 1 && ts.isSpreadElement(call.arguments.nodes[0]!) + ? call.arguments.nodes[0]! : null; if ((name === "push" || name === "unshift") && spreadArg && ts.isSpreadElement(spreadArg)) { let src = lowerer.lowerExpr(spreadArg.expression); @@ -790,11 +809,11 @@ export function lowerArrayMethodCall( loc, }; } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { // Finish argument evaluation before mutating the receiver. Each // spread snapshots its elements immediately, so a later argument // can mutate that source without changing already-collected values. - const items = lowerArraySpreadItems(lowerer, call.arguments, elem, receiverIr, loc); + const items = lowerArraySpreadItems(lowerer, call.arguments.nodes, elem, receiverIr, loc); return { kind: "arrIntrinsic", method: name === "push" ? "pushSpread" : "unshiftSpread", @@ -804,7 +823,7 @@ export function lowerArrayMethodCall( loc, }; } - const valueProbes = call.arguments.map((arg) => tryLowerExpression(lowerer, arg)); + const valueProbes = call.arguments.nodes.map((arg) => tryLowerExpression(lowerer, arg)); if ( valueProbes.some( (probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null, @@ -814,14 +833,14 @@ export function lowerArrayMethodCall( return lowerOptionalArrayPush( lowerer, receiver, - call.arguments.map((arg) => lowerer.lowerExpr(arg)), + call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)), elem, loc, ); } const items = lowerArrayValueItems( lowerer, - call.arguments.map((arg) => lowerer.lowerExpr(arg)), + call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)), elem, receiverIr, loc, @@ -838,7 +857,7 @@ export function lowerArrayMethodCall( // Inserted values flow into the element slot like an assignment would: // union-element arrays wrap plain arm values (coerceInto is inert // when the types already agree). - const args = call.arguments.map((a) => + const args = call.arguments.nodes.map((a) => name === "push" || name === "unshift" ? lowerer.lowerExprExpecting(a, elem) : lowerer.lowerExpr(a), @@ -886,7 +905,7 @@ export function lowerArrayMethodCall( const receiver = lowerer.lowerExpr(access.expression); const shape: (IrType | null)[] = []; const args: IrExpr[] = []; - for (const argNode of call.arguments) { + for (const argNode of call.arguments.nodes) { if (ts.isSpreadElement(argNode)) { lowerer.unsupported( "SC1090", @@ -928,26 +947,31 @@ export function lowerArrayMethodCall( if (name === "slice") { const receiver = lowerer.lowerExpr(access.expression); const args = [ - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array slice start"), - lowerArrayPosition(lowerer, call.arguments[1], numLit(Infinity, loc), "array slice end"), + lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array slice start"), + lowerArrayPosition( + lowerer, + call.arguments.nodes[1], + numLit(Infinity, loc), + "array slice end", + ), ]; return { kind: "arrIntrinsic", method: "slice", receiver, args, type: receiverIr, loc }; } if (name === "splice") { const receiver = lowerer.lowerExpr(access.expression); const args: IrExpr[] = [ - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array splice start"), + lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array splice start"), lowerArrayPosition( lowerer, - call.arguments[1], - numLit(call.arguments.length === 1 ? Infinity : 0, loc), + call.arguments.nodes[1], + numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc), "array splice deleteCount", ), ]; - if (call.arguments.length <= 2) { + if (call.arguments.nodes.length <= 2) { return { kind: "arrIntrinsic", method: "splice", receiver, args, type: receiverIr, loc }; } - const itemNodes = call.arguments.slice(2); + const itemNodes = call.arguments.nodes.slice(2); const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( @@ -1007,8 +1031,8 @@ export function lowerArrayMethodCall( if (name === "join") { if (elem.kind === "dyn") { const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN); - const args = call.arguments.length - ? [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)] + const args = call.arguments.nodes.length + ? [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)] : []; return lowerer.coerceToExpected( { @@ -1047,7 +1071,7 @@ export function lowerArrayMethodCall( ); } const receiver = lowerer.lowerExpr(access.expression); - const sep = lowerArrayJoinSeparator(lowerer, call.arguments[0], loc); + const sep = lowerArrayJoinSeparator(lowerer, call.arguments.nodes[0], loc); return { kind: "arrIntrinsic", method: "join", receiver, args: [sep], type: STRING, loc }; } if (name === "map" || name === "filter" || name === "forEach") { @@ -1247,8 +1271,8 @@ function lowerArrayFlatCall( receiverType: IrType & { kind: "array" }, ): IrExpr { const loc = locOf(call); - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.flat with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.flat with ${call.arguments.nodes.length} arguments`, call); } const elemCouldNest = (elem: IrType): boolean => elem.kind === "array" || @@ -1258,11 +1282,11 @@ function lowerArrayFlatCall( .get(elem.unionId) ?.arms.some((arm) => arm.kind === "array" || arm.kind === "dyn") ?? true)); - if (!elemCouldNest(receiverType.elem) && call.arguments[0]) { + if (!elemCouldNest(receiverType.elem) && call.arguments.nodes[0]) { const receiver = lowerer.lowerExpr(access.expression); const depth = lowerArrayPosition( lowerer, - call.arguments[0], + call.arguments.nodes[0], numLit(1, loc), "array flat depth", ); @@ -1286,7 +1310,9 @@ function lowerArrayFlatCall( loc, }; } - const depth = call.arguments[0] ? literalFlatDepth(lowerer.lowerExpr(call.arguments[0])) : 1; + const depth = call.arguments.nodes[0] + ? literalFlatDepth(lowerer.lowerExpr(call.arguments.nodes[0])) + : 1; if (depth === null) lowerer.noLowering(".flat with a nonconstant depth", call); let remaining = Number.isNaN(depth) || depth <= 0 ? 0 : Math.trunc(depth); let result: IrExpr = lowerer.lowerExpr(access.expression); @@ -1338,13 +1364,13 @@ function lowerArraySearchCall( ): IrExpr { const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`.${method} with spread arguments`, call); - const needle: IrExpr = call.arguments[0] - ? lowerer.lowerExpr(call.arguments[0]) + const needle: IrExpr = call.arguments.nodes[0] + ? lowerer.lowerExpr(call.arguments.nodes[0]) : { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; - const fromIndex: IrExpr = call.arguments[1] - ? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc)) + const fromIndex: IrExpr = call.arguments.nodes[1] + ? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc)) : method === "lastIndexOf" ? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc } : numLit(0, loc); @@ -1361,7 +1387,7 @@ function lowerArraySearchValues( fromIndex: IrExpr, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0] ?? call; + const argNode = call.arguments.nodes[0] ?? call; // A derived CLASS VALUE against a base-classval element widens (the same // pointer — identity search is exact); the coercion path owns the ABI gate. if ( @@ -1376,7 +1402,7 @@ function lowerArraySearchValues( // runtime implementation already has exact primitive/reference equality. if ( method !== "lastIndexOf" && - call.arguments.length === 1 && + call.arguments.nodes.length === 1 && elem.kind !== "union" && elem.kind !== "bigint" && typeEquals(needle.type, elem) @@ -1398,7 +1424,7 @@ function lowerArraySearchValues( if (valueT.kind !== "union") { if (method === "lastIndexOf") lowerer.noLowering(".lastIndexOf on an array of checked values", call); - if (call.arguments.length > 1) + if (call.arguments.nodes.length > 1) lowerer.noLowering(`.${method} with fromIndex on '${lowerer.fmt(elem)}' elements`, call); if (!typeEquals(needle.type, elem)) lowerer.badType(argNode, lowerer.typeOf(argNode)); return { @@ -1456,7 +1482,7 @@ function lowerArraySearchValues( searchNeedle.type, directNeedle, fromIndex.type, - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, loc, ); return { @@ -1919,10 +1945,10 @@ export function lowerTupleReadMethodCall( elem, ); if (search) { - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { return lowerer.noLowering(`tuple .${method} with spread or extra arguments`, call); } - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; const undefinedArg = arg ? lowerStaticallyUndefinedArgument(lowerer, arg) : null; let needle = arg ? (undefinedArg ?? lowerer.lowerExpr(arg)) : null; if (!needle || isUnitType(needle.type) || needle.type.kind === "void") { @@ -1934,10 +1960,10 @@ export function lowerTupleReadMethodCall( if (!wrapped) return lowerer.noLowering(`tuple .${method} missing search value`, call); needle = needle ? defaultAfterUndefined(needle, wrapped) : wrapped; } - const from = call.arguments[1] - ? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc)) + const from = call.arguments.nodes[1] + ? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc)) : numLit(method === "lastIndexOf" ? Infinity : 0, loc); - const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(needle.type)}:${typeKey(from.type)}:${call.arguments.length}`; + const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(needle.type)}:${typeKey(from.type)}:${call.arguments.nodes.length}`; let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { @@ -1978,7 +2004,7 @@ export function lowerTupleReadMethodCall( lowerer, varRef("from.0", from.type, loc), numLit(0, loc), - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, "tuple search position", ), loc, @@ -1991,13 +2017,13 @@ export function lowerTupleReadMethodCall( return { kind: "call", callee: helper, args: [receiver, needle, from], type: resultType, loc }; } if (method === "slice") { - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.slice with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.slice with ${call.arguments.nodes.length} arguments`, call); } - const args = call.arguments.map((arg) => lowerer.lowerExpr(arg)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)); args.forEach((arg, i) => { if (arg.type.kind !== "f64") - lowerer.badType(call.arguments[i]!, lowerer.typeOf(call.arguments[i]!)); + lowerer.badType(call.arguments.nodes[i]!, lowerer.typeOf(call.arguments.nodes[i]!)); }); const resultType = arrayOf(elem); const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; @@ -2039,14 +2065,14 @@ export function lowerTupleReadMethodCall( } return { kind: "call", callee: helper, args: [receiver, ...args], type: resultType, loc }; } - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments`, + `.${method} with ${call.arguments.nodes.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function", ); } - const callback = call.arguments[0]!; + const callback = call.arguments.nodes[0]!; // Check before the array HOF adapter can create a tuple-to-array copy. const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(callback)); if (signatures.some((signature) => signature.getParameters().length > 2)) { @@ -2210,10 +2236,10 @@ function lowerArrayAtCall( elem: IrType, ): IrExpr { const loc = locOf(call); - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(".at with spread arguments", call); const receiver = lowerer.lowerExpr(access.expression); - const indexNode = call.arguments[0]; + const indexNode = call.arguments.nodes[0]; const index = lowerPositionArgument(lowerer, indexNode, numLit(0, loc)); const resultT = lowerer.irTypeOf(call); if (resultT.kind !== "union") lowerer.badType(call, lowerer.typeOf(call)); // defensive: T | undefined always maps to a union diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 91cf0b263..7f1f758ca 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -50,15 +50,15 @@ function lowerBytesSortCall( const loc = locOf(call); const method = access.name.text; const copyFirst = method === "toSorted"; - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments on ${BYTES_ELEMENT_NAME[bytesT.elem]}`, + `.${method} with ${call.arguments.nodes.length} arguments on ${BYTES_ELEMENT_NAME[bytesT.elem]}`, call, ); } const receiver = lowerer.lowerExprExpecting(access.expression, bytesT); - const undefinedArg = call.arguments[0] - ? lowerStaticallyUndefinedArgument(lowerer, call.arguments[0]) + const undefinedArg = call.arguments.nodes[0] + ? lowerStaticallyUndefinedArgument(lowerer, call.arguments.nodes[0]) : null; const helperFor = ( key: string, @@ -77,7 +77,7 @@ function lowerBytesSortCall( } return helper; }; - if (call.arguments.length === 0 || undefinedArg) { + if (call.arguments.nodes.length === 0 || undefinedArg) { const helper = helperFor("default", 0, false, null); if (!undefinedArg || isSafeToDiscard(undefinedArg)) { return { kind: "call", callee: helper, args: [receiver], type: bytesT, loc }; @@ -109,7 +109,7 @@ function lowerBytesSortCall( loc, }; } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let fnArg = lowerer.lowerExpr(argNode); // As for Array sort: adapt a comparator over checked values to the // element ABI; arithmetic on inferred JS values still yields a number. @@ -189,7 +189,7 @@ export function lowerBytesNew( (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer") && lowerer.isStdlibSymbol(symbol) ) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering( "resizable ArrayBuffer construction", @@ -216,15 +216,15 @@ export function lowerBytesNew( const name = symbol.name; const type = bytesOf(elem); const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length === 0) return { kind: "bytesNew", source: null, type, loc }; if (args.length >= 2 && args.length <= 3 && !args.some(ts.isSpreadElement)) { return lowerArrayBufferView(lowerer, args, elem, loc); } if (args.length === 1 && !ts.isSpreadElement(args[0]!)) { const argNode = args[0]!; - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64)); + if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.nodes.some(ts.isSpreadElement)) { + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "bytesNew", source: seed, type, loc }; } @@ -277,9 +277,9 @@ export function lowerBytesStaticCall( ): IrExpr | null { if (lowerer.chainBlocked(call, access)) return null; if (lowerer.isStdlibGlobal(access.expression, "ArrayBuffer") && access.name.text === "isView") { - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("ArrayBuffer.isView argument count", call); - const arg = call.arguments[0]!; + const arg = call.arguments.nodes[0]!; return { kind: "libCall", fn: "arrayBuffer.isView", @@ -293,15 +293,15 @@ export function lowerBytesStaticCall( const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null; if ( - call.arguments.length < 1 || - call.arguments.length > 3 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 3 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering("TypedArray.from with this argument count", call); } - const node = call.arguments[0]!; + const node = call.arguments.nodes[0]!; const source = lowerer.lowerExpr(node); - if (call.arguments.length > 1) { + if (call.arguments.nodes.length > 1) { if (!["string", "array", "bytes"].includes(source.type.kind)) lowerer.noLowering("TypedArray.from mapper over this source representation", call); const loc = locOf(call); @@ -356,9 +356,9 @@ export function lowerBytesStaticCall( callee: key, args: [ lowerer.coerceInto(node, source, DYN), - lowerer.lowerExprExpecting(call.arguments[1]!, DYN), - call.arguments[2] - ? lowerer.lowerExprExpecting(call.arguments[2], DYN) + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), + call.arguments.nodes[2] + ? lowerer.lowerExprExpecting(call.arguments.nodes[2], DYN) : dynUndefinedExpr(loc), ], type: bytesOf(elem), @@ -414,7 +414,7 @@ function lowerArrayBufferView( } function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (!args.length || args.length > 3 || args.some(ts.isSpreadElement)) lowerer.noLowering("DataView constructor argument count", expr); return lowerArrayBufferView(lowerer, args, "dv", locOf(expr)); @@ -438,7 +438,7 @@ export function lowerBytesMethodCall( if (receiverIr?.kind !== "bytes") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); - const nArgs = call.arguments.length; + const nArgs = call.arguments.nodes.length; // Encoded stream callbacks can receive strings even when JavaScript's // contextual inference still describes their parameter as a Buffer. if (name === "toString" && nArgs <= 1 && isJsSourceFile(call.getSourceFile())) { @@ -451,8 +451,8 @@ export function lowerBytesMethodCall( fn: "dyn.toString", args: [ receiver, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), { kind: "strLit", value: access.getText(), type: STRING, loc }, ], @@ -492,7 +492,7 @@ export function lowerBytesMethodCall( }; } if (receiverIr.elem === "u8" && name === "with") { - if (nArgs !== 2 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.with with ${nArgs} arguments on Uint8Array`, call); } return { @@ -500,15 +500,15 @@ export function lowerBytesMethodCall( method: "with", receiver: lowerer.lowerExprExpecting(access.expression, receiverIr), args: [ - lowerer.lowerExprExpecting(call.arguments[0]!, F64), - lowerer.lowerExprExpecting(call.arguments[1]!, F64), + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64), ], type: receiverIr, loc, }; } if (name === "join") { - if (nArgs > 1 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.join with ${nArgs} arguments on typed arrays`, call); } const separatorDefault: IrExpr = { @@ -517,8 +517,8 @@ export function lowerBytesMethodCall( type: STRING, loc, }; - const separator = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], STRING, separatorDefault) + const separator = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], STRING, separatorDefault) : separatorDefault; return { kind: "bytesIntrinsic", @@ -536,7 +536,7 @@ export function lowerBytesMethodCall( const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); // A missing start is 0 and a missing end is the length (+Infinity // clamps to it), as for omitted arguments. - const args = call.arguments.map((a, i) => + const args = call.arguments.nodes.map((a, i) => lowerOptionalArgument(lowerer, a, F64, { kind: "numLit", value: i === 0 ? 0 : Infinity, @@ -563,13 +563,13 @@ export function lowerBytesMethodCall( return { kind: "bytesIntrinsic", method, receiver, args, type: receiverIr, loc }; } if (name === "copyWithin") { - if (nArgs > 3 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.copyWithin with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const args = [0, 0, Infinity].map((value, i) => { const fallback: IrExpr = { kind: "numLit", value, type: F64, loc }; - const node = call.arguments[i]; + const node = call.arguments.nodes[i]; return node ? lowerOptionalArgument(lowerer, node, F64, fallback) : fallback; }); return { kind: "bytesIntrinsic", method: "copyWithin", receiver, args, type: receiverIr, loc }; @@ -592,15 +592,15 @@ export function lowerBytesMethodCall( ) { // Typed arrays use element coercion and relative, clamped indices. // Buffer's same-named method keeps its pattern and offset validation. - if (nArgs > 3 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.fill with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const zero: IrExpr = { kind: "numLit", value: 0, type: F64, loc }; - const v = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], F64, zero) + const v = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], F64, zero) : zero; - const idx = call.arguments.slice(1).map((a, i) => + const idx = call.arguments.nodes.slice(1).map((a, i) => lowerOptionalArgument(lowerer, a, F64, { kind: "numLit", value: i === 0 ? 0 : Infinity, @@ -622,14 +622,14 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const src = lowerer.lowerExpr(call.arguments[0]!); + const src = lowerer.lowerExpr(call.arguments.nodes[0]!); const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; const args = [ - src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments[0]!, src, DYN), + src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments.nodes[0]!, src, DYN), ]; if (nArgs === 2) args.push( - lowerOptionalArgument(lowerer, call.arguments[1]!, F64, { + lowerOptionalArgument(lowerer, call.arguments.nodes[1]!, F64, { kind: "numLit", value: 0, type: F64, @@ -660,7 +660,7 @@ export function lowerBytesMethodCall( } if (nArgs > 3) lowerer.noLowering(`.toString with ${nArgs} arguments on Buffers`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const encNode = call.arguments[0]; + const encNode = call.arguments.nodes[0]; let method: IrBytesIntrinsicMethod = "toString"; let enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; if (encNode) { @@ -698,9 +698,9 @@ export function lowerBytesMethodCall( // receiver's length, because EXPLICIT negative ends clamp to empty // in Node and no in-band sentinel can represent "omitted" safely. if (nArgs > 1) { - const start = lowerer.lowerExprExpecting(call.arguments[1]!, F64); + const start = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64); if (nArgs === 3) { - const end = lowerer.lowerExprExpecting(call.arguments[2]!, F64); + const end = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, F64); return { kind: "bytesIntrinsic", method, @@ -741,7 +741,7 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offsetNode = call.arguments[required]; + const offsetNode = call.arguments.nodes[required]; const offset: IrExpr = offsetNode ? lowerer.lowerExprExpecting(offsetNode, F64) : { kind: "numLit", value: 0, type: F64, loc }; @@ -756,7 +756,7 @@ export function lowerBytesMethodCall( loc, }; } - const value = lowerer.lowerExprExpecting(call.arguments[0]!, BIGINT_T); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, BIGINT_T); return { kind: "libCall", fn: "bigint.bufferWrite", @@ -785,7 +785,7 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const kind: IrExpr = { kind: "strLit", value: numKind, type: STRING, loc }; - const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))]; + const args = [kind, ...call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64))]; if (nArgs === required) { args.push({ kind: "numLit", value: 0, type: F64, loc }); // Node's offset default } @@ -814,7 +814,7 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const kind: IrExpr = { kind: "strLit", value: varKind, type: STRING, loc }; - const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))]; + const args = [kind, ...call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64))]; return { kind: "bytesIntrinsic", method: write ? "writeNumVar" : "readNumVar", @@ -833,10 +833,10 @@ export function lowerBytesMethodCall( if (dvBigGetter !== undefined && receiverIr.elem === "u8") { if (nArgs < 1 || nArgs > 2) lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offset = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const offset = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); const sign: IrExpr = { kind: "boolLit", value: dvBigGetter.sign, type: BOOL, loc }; - const le: IrExpr = call.arguments[1] - ? lowerer.lowerExprExpecting(call.arguments[1], BOOL) + const le: IrExpr = call.arguments.nodes[1] + ? lowerer.lowerExprExpecting(call.arguments.nodes[1], BOOL) : { kind: "boolLit", value: false, type: BOOL, loc }; return { kind: "libCall", @@ -854,8 +854,8 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); // ToIndex(undefined) is ToIndex(NaN): a missing offset reads offset 0. - const args = [lowerToNumberArgument(lowerer, call.arguments[0]!)]; - if (nArgs === 2) args.push(lowerer.lowerExprExpecting(call.arguments[1]!, BOOL)); + const args = [lowerToNumberArgument(lowerer, call.arguments.nodes[0]!)]; + if (nArgs === 2) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BOOL)); return { kind: "bytesIntrinsic", method: dvGetter.method, receiver, args, type: F64, loc }; } // DataView setters — the getters' mirror: (byteOffset, value) plus the @@ -868,10 +868,10 @@ export function lowerBytesMethodCall( if ((name === "setBigUint64" || name === "setBigInt64") && receiverIr.elem === "u8") { if (nArgs < 2 || nArgs > 3) lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offset = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const value = lowerer.lowerExprExpecting(call.arguments[1]!, BIGINT_T); - const le: IrExpr = call.arguments[2] - ? lowerer.lowerExprExpecting(call.arguments[2], BOOL) + const offset = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BIGINT_T); + const le: IrExpr = call.arguments.nodes[2] + ? lowerer.lowerExprExpecting(call.arguments.nodes[2], BOOL) : { kind: "boolLit", value: false, type: BOOL, loc }; return { kind: "libCall", @@ -890,10 +890,10 @@ export function lowerBytesMethodCall( // The offset (ToIndex) and the value (ToNumber) treat a missing value // as NaN. const args = [ - lowerToNumberArgument(lowerer, call.arguments[0]!), - lowerToNumberArgument(lowerer, call.arguments[1]!), + lowerToNumberArgument(lowerer, call.arguments.nodes[0]!), + lowerToNumberArgument(lowerer, call.arguments.nodes[1]!), ]; - if (nArgs === 3) args.push(lowerer.lowerExprExpecting(call.arguments[2]!, BOOL)); + if (nArgs === 3) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[2]!, BOOL)); return { kind: "bytesIntrinsic", method: dvSetter.method, receiver, args, type: VOID, loc }; } return null; @@ -984,7 +984,7 @@ function lowerBufferInstanceMethod( name: string, loc: SrcLoc, ): IrExpr | null { - const args = call.arguments; + const args = call.arguments.nodes; const nArgs = args.length; if (args.some(ts.isSpreadElement)) return null; const isStringArg = (i: number): boolean => @@ -1041,10 +1041,12 @@ function lowerBufferInstanceMethod( const fast = nArgs >= 1 && argIrKind(0)?.kind === "bytes" && - call.arguments.slice(1).every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64"); + call.arguments.nodes + .slice(1) + .every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64"); if (fast) { const target = u8Arg(0); - const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); + const idx = call.arguments.nodes.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); const receiver = lowerer.lowerExprExpecting(access.expression, BYTES_U8); return { kind: "bytesIntrinsic", @@ -1121,7 +1123,7 @@ function lowerBufferInstanceMethod( if (nArgs < 1 || nArgs > 4) lowerer.noLowering(`.fill with ${nArgs} arguments`, call); // A trailing string-typed arg past the value is the encoding. let encNode: ts.Expression | undefined; - let idxNodes = call.arguments.slice(1); + let idxNodes = call.arguments.nodes.slice(1); if (idxNodes.length > 0 && isStringArg(nArgs - 1)) { encNode = idxNodes[idxNodes.length - 1]; idxNodes = idxNodes.slice(0, -1); @@ -1171,7 +1173,7 @@ function lowerBufferInstanceMethod( if (name === "copy") { if (nArgs < 1 || nArgs > 4) lowerer.noLowering(`.copy with ${nArgs} arguments`, call); const target = u8Arg(0); - const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); + const idx = call.arguments.nodes.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); const receiver = lowerer.lowerExprExpecting(access.expression, BYTES_U8); return { kind: "bytesIntrinsic", @@ -1192,7 +1194,7 @@ function lowerBufferInstanceMethod( // (str), (str, enc), (str, offset), (str, offset, enc), // (str, offset, length), (str, offset, length, enc). let encNode: ts.Expression | undefined; - let idxNodes = call.arguments.slice(1); + let idxNodes = call.arguments.nodes.slice(1); if (idxNodes.length > 0 && isStringArg(nArgs - 1)) { encNode = idxNodes[idxNodes.length - 1]; idxNodes = idxNodes.slice(0, -1); @@ -1299,7 +1301,7 @@ function knownBufferProducer( if ( !ts.isPropertyAccessExpression(callee) || callee.name.text !== "digest" || - node.arguments.length !== 0 + node.arguments.nodes.length !== 0 ) return false; const type = lowerer.mapTypeOf(lowerer.typeOf(callee.expression)); @@ -1328,7 +1330,7 @@ function lowerBufferStaticValue( if (!lowerer.isStdlibGlobal(access.expression, "Buffer")) return null; const member = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (member === "from") { if ( args.length >= 1 && @@ -1371,11 +1373,14 @@ function lowerBufferStaticValue( } if (args.length >= 1 && args.length <= 2 && !args.some(ts.isSpreadElement)) { const argNode = args[0]!; - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { + if ( + ts.isArrayLiteralExpression(argNode) && + !argNode.elements.nodes.some(ts.isSpreadElement) + ) { // A number[] literal (contextually typed by the lib's readonly // number[] slot — build it element-wise, the Set-seed pattern). if (args.length === 1) { - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "libCall", fn: "buffer.poolFromArray", args: [seed], type: BYTES_U8, loc }; } @@ -1521,8 +1526,8 @@ function lowerBufferStaticValue( } const argNode = args[0]!; let list: IrExpr; - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, BYTES_U8)); + if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.nodes.some(ts.isSpreadElement)) { + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, BYTES_U8)); list = { kind: "arrayLit", elems, type: arrayOf(BYTES_U8), loc }; } else { list = lowerer.lowerExpr(argNode); diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 96147cbe5..e87a5e1bc 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -56,7 +56,7 @@ export function lowerArrayCallback( lead[0]?.kind === "dyn" && (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) ) { - for (const p of argNode.parameters) { + for (const p of argNode.parameters.nodes) { if (!ts.isIdentifier(p.name) || p.type || p.initializer || p.dotDotDotToken) continue; const t = lowerer.checker.getTypeAtLocation(p.name); if ((t.flags & ts.TypeFlags.Any) !== 0 && !lowerer.chainNarrowedType.has(p.name)) { @@ -71,7 +71,7 @@ export function lowerArrayCallback( // handles the loop passes, whatever the contextual type spelled — if (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) { - argNode.parameters.forEach((_p, _i) => { + argNode.parameters.nodes.forEach((_p, _i) => { return; }); } @@ -85,7 +85,7 @@ export function lowerArrayCallback( const previousImplicit = lowerer.implicitParamTypes; let contextualTs: Map | null = null; if (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) { - argNode.parameters.forEach((param, i) => { + argNode.parameters.nodes.forEach((param, i) => { const expected = full[i]; if ( expected === undefined || @@ -137,7 +137,7 @@ export function lowerArrayCallback( // that stored layout; its names then see the optional values. const patterns: { node: ts.Node; previous: IrType | undefined }[] = []; if (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) { - argNode.parameters.forEach((param, i) => { + argNode.parameters.nodes.forEach((param, i) => { const expected = full[i]; if ( expected === undefined || diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index b6dec2913..7c4ddf220 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -48,20 +48,23 @@ export function lowerGroupByStaticCall( if (!lowerer.isStdlibMember(access)) return null; const what = isMap ? "Map.groupBy" : "Object.groupBy"; const loc = locOf(call); - if (call.arguments.length !== 2 || call.arguments.some((a) => ts.isSpreadElement(a))) { - lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call); + if ( + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + lowerer.noLowering(`${what} with ${call.arguments.nodes.length} arguments`, call); } - const items = lowerer.lowerExpr(call.arguments[0]!); + const items = lowerer.lowerExpr(call.arguments.nodes[0]!); const elem = groupedElement(lowerer, items.type); if (elem === null) { lowerer.noLowering( - `${what} over an iterable of type '${lowerer.checker.typeToString(lowerer.typeOf(call.arguments[0]!))}'`, - call.arguments[0]!, + `${what} over an iterable of type '${lowerer.checker.typeToString(lowerer.typeOf(call.arguments.nodes[0]!))}'`, + call.arguments.nodes[0]!, "arrays, Sets, Maps and generators are the supported items forms — spread other iterables first: [...items]", ); } const groupT = arrayOf(elem); - const fNode = call.arguments[1]!; + const fNode = call.arguments.nodes[1]!; // An unannotated JavaScript callback parameter takes the element. const f = isJsSourceFile(fNode.getSourceFile()) && @@ -74,7 +77,7 @@ export function lowerGroupByStaticCall( (f.type.params.length >= 1 && !typeEquals(f.type.params[0]!, elem)) || (f.type.params.length === 2 && f.type.params[1]!.kind !== "f64") ) { - lowerer.badType(call.arguments[1]!, lowerer.typeOf(call.arguments[1]!)); + lowerer.badType(call.arguments.nodes[1]!, lowerer.typeOf(call.arguments.nodes[1]!)); } const keyT = f.type.ret; // The call's own type keeps the callback's literal-union key type diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 8c8349c0b..a8b782b51 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -80,16 +80,16 @@ export function lowerMapMethodCall( ); } // The lib declares a thisArg parameter on forEach; unlowered — fenced. - if (name === "forEach" && call.arguments.length !== 1) { + if (name === "forEach" && call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.forEach with ${call.arguments.length} arguments`, + `.forEach with ${call.arguments.nodes.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function", ); } if (name === "get") { - const k = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.key); + const k = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.key); // The checker types the call `V | undefined`, which interns the // result union. `undefined` sorts LAST among all possible arm // typeKeys, so when V is itself a union its arms keep their tags in @@ -109,11 +109,11 @@ export function lowerMapMethodCall( return { kind: "mapIntrinsic", method: "get", receiver, args: [k], type, loc }; } if (name === "set") { - const k = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.key); + const k = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.key); const v = receiverIr.value.kind === "dyn" - ? lowerer.lowerCollectionKey(call.arguments[1]!, receiverIr.value) - : lowerer.lowerExprExpecting(call.arguments[1]!, receiverIr.value); + ? lowerer.lowerCollectionKey(call.arguments.nodes[1]!, receiverIr.value) + : lowerer.lowerExprExpecting(call.arguments.nodes[1]!, receiverIr.value); // A discarded set result needs no second receiver read or identity // owner. Keep the ordinary operand order: receiver, key, then value. let parent: ts.Node | undefined = call.parent; @@ -146,7 +146,7 @@ export function lowerMapMethodCall( }; } if (name === "has" || name === "delete") { - const k = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.key); + const k = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.key); return { kind: "mapIntrinsic", method: name, receiver, args: [k], type: BOOL, loc }; } if (name === "clear") { @@ -452,14 +452,14 @@ export function lowerMapForEachCall( bindUntyped = false, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const previousRuntime: { symbol: ts.Symbol; type: IrType | undefined }[] = []; const previousImplicit = lowerer.implicitParamTypes; let contextualTs: Map | null = null; if (bindUntyped && (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode))) { const expected = [mapT.value, mapT.key]; - argNode.parameters.slice(0, 2).forEach((param, i) => { + argNode.parameters.nodes.slice(0, 2).forEach((param, i) => { const type = expected[i]; if ( type === undefined || @@ -567,9 +567,9 @@ export function lowerSetMethodCall( "the lowered add() produces no value — call each add(v) as its own statement", ); } - if (name === "forEach" && call.arguments.length !== 1) { + if (name === "forEach" && call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.forEach with ${call.arguments.length} arguments`, + `.forEach with ${call.arguments.nodes.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function", ); @@ -581,11 +581,11 @@ export function lowerSetMethodCall( return lowerSetCombineCall(lowerer, call, name, receiver, receiverIr); } if (name === "add") { - const v = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.elem); + const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem); return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc }; } if (name === "has" || name === "delete") { - const v = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.elem); + const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem); return { kind: "setIntrinsic", method: name, receiver, args: [v], type: BOOL, loc }; } if (name === "clear") { @@ -606,7 +606,7 @@ export function lowerSetForEachCall( setT: IrType & { kind: "set" }, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const fnArg = lowerer.lowerExpr(argNode); if ( @@ -644,10 +644,10 @@ function lowerSetCombineCall( setT: IrType & { kind: "set" }, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]; - if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + const argNode = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { lowerer.noLowering( - `Set.prototype.${name} with ${call.arguments.length} arguments`, + `Set.prototype.${name} with ${call.arguments.nodes.length} arguments`, call, "exactly one Set argument is the supported form", ); @@ -918,8 +918,10 @@ export function lowerSetSeedNew( } // Constructor contextual types include Iterable, which has no value // representation. Build a literal at the actual Set element ABI instead. - if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) { - const elems = node.elements.map((element) => lowerer.lowerCollectionKey(element, setT.elem)); + if (ts.isArrayLiteralExpression(node) && !node.elements.nodes.some(ts.isSpreadElement)) { + const elems = node.elements.nodes.map((element) => + lowerer.lowerCollectionKey(element, setT.elem), + ); return { kind: "setNew", seed: { kind: "arrayLit", elems, type: arrayOf(setT.elem), loc }, @@ -1154,7 +1156,7 @@ export function lowerMapSeedNew( if (ts.isArrayLiteralExpression(argNode)) { if ( isJsSourceFile(argNode.getSourceFile()) && - argNode.elements.some((entry) => !ts.isArrayLiteralExpression(entry)) + argNode.elements.nodes.some((entry) => !ts.isArrayLiteralExpression(entry)) ) { return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT); } diff --git a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts index cb1978e51..f2967c298 100644 --- a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts @@ -23,7 +23,7 @@ export function execResultCaptureMask(lowerer: Lowerer, call: ts.CallExpression) const statement = decl.parent.parent; if ( ts.isVariableStatement(statement) && - statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + statement.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ) return null; const symbol = lowerer.resolveValueSymbol(decl.name); diff --git a/packages/compiler/src/frontend/lowering/containers/for-of.ts b/packages/compiler/src/frontend/lowering/containers/for-of.ts index 6aa838dea..79f970eb1 100644 --- a/packages/compiler/src/frontend/lowering/containers/for-of.ts +++ b/packages/compiler/src/frontend/lowering/containers/for-of.ts @@ -77,7 +77,7 @@ export function lowerForOfSearchParams( } const isVar = (list.flags & ts.NodeFlags.BlockScoped) === 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || isVar; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one lowerer.scopes.push(new Map()); try { const sp = lowerer.declareHiddenLocal("%spof", SP); @@ -108,13 +108,15 @@ export function lowerForOfSearchParams( yieldsPair && !isVar && // var pattern names assign hoisted slots — the generic desugar below owns them ts.isArrayBindingPattern(decl.name) && - decl.name.elements.length >= 1 && - decl.name.elements.length <= 2 && - decl.name.elements.every(isPlainIdent) + decl.name.elements.nodes.length >= 1 && + decl.name.elements.nodes.length <= 2 && + decl.name.elements.nodes.every(isPlainIdent) ) { // `for (const [k, v] of sp)` — bind straight from the reads; the // tuple never exists. `[k]` alone reads only the key. - const els = decl.name.elements as readonly (ts.BindingElement & { name: ts.Identifier })[]; + const els = decl.name.elements.nodes as readonly (ts.BindingElement & { + name: ts.Identifier; + })[]; const kLocal = lowerer.declareLocal(els[0]!.name, els[0]!.name.text, STRING, isLet); binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc }); if (els[1]) { @@ -260,7 +262,7 @@ export function lowerForOfArrayIter( } const isVar = (list.flags & ts.NodeFlags.BlockScoped) === 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || isVar; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one lowerer.scopes.push(new Map()); try { const arr = lowerer.declareHiddenLocal("%arof", arrT); @@ -293,13 +295,15 @@ export function lowerForOfArrayIter( yieldsPair && !isVar && // var pattern names assign hoisted slots — the generic desugar below owns them ts.isArrayBindingPattern(decl.name) && - decl.name.elements.length >= 1 && - decl.name.elements.length <= 2 && - decl.name.elements.every(isPlainIdent) + decl.name.elements.nodes.length >= 1 && + decl.name.elements.nodes.length <= 2 && + decl.name.elements.nodes.every(isPlainIdent) ) { // `for (const [i, v] of xs.entries())` — bind straight from the // reads; the tuple never exists. `[i]` alone reads only the index. - const els = decl.name.elements as readonly (ts.BindingElement & { name: ts.Identifier })[]; + const els = decl.name.elements.nodes as readonly (ts.BindingElement & { + name: ts.Identifier; + })[]; const kLocal = lowerer.declareLocal(els[0]!.name, els[0]!.name.text, F64, isLet); binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc }); if (els[1]) { @@ -493,7 +497,7 @@ function lowerForOfMapOrSet( } isVar = (list.flags & ts.NodeFlags.BlockScoped) === 0; isLet = (list.flags & ts.NodeFlags.Let) !== 0 || isVar; - decl = list.declarations[0]!; // the grammar allows exactly one + decl = list.declarations.nodes[0]!; // the grammar allows exactly one } lowerer.scopes.push(new Map()); try { @@ -603,13 +607,15 @@ function lowerForOfMapOrSet( yieldsPair && !isVar && // var pattern names assign hoisted slots — the generic desugar below owns them ts.isArrayBindingPattern(decl!.name) && - decl!.name.elements.length >= 1 && - decl!.name.elements.length <= 2 && - decl!.name.elements.every(isPlainIdent) + decl!.name.elements.nodes.length >= 1 && + decl!.name.elements.nodes.length <= 2 && + decl!.name.elements.nodes.every(isPlainIdent) ) { // `for (const [k, v] of m)` — bind straight from the primitives; // the tuple never exists. `[k]` alone reads only the key. - const els = decl!.name.elements as readonly (ts.BindingElement & { name: ts.Identifier })[]; + const els = decl!.name.elements.nodes as readonly (ts.BindingElement & { + name: ts.Identifier; + })[]; const kLocal = lowerer.declareLocal(els[0]!.name, els[0]!.name.text, keyT, isLet); binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc }); if (els[1]) { diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index cca4bd51b..38df368ac 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -92,7 +92,12 @@ export function lowerObjectIterOverIndexShape( member, argIr, shape, - objectEnumerationReceiver(lowerer, lowerer.lowerExpr(call.arguments[0]!), argIr, locOf(call)), + objectEnumerationReceiver( + lowerer, + lowerer.lowerExpr(call.arguments.nodes[0]!), + argIr, + locOf(call), + ), resultT, locOf(call), ); @@ -441,8 +446,8 @@ export function lowerObjectFromEntriesCall( callee.questionDotToken || !lowerer.isStdlibGlobal(callee.expression, "Object") || callee.name.text !== "fromEntries" || - call.arguments.length !== 1 || - ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length !== 1 || + ts.isSpreadElement(call.arguments.nodes[0]!) ) return null; @@ -451,7 +456,7 @@ export function lowerObjectFromEntriesCall( fn: "dyn.fromEntries", type: DYN, loc: locOf(call), - args: [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], }; } @@ -464,8 +469,9 @@ function lowerTypedObjectFromEntriesCall( if (call.questionDotToken || callee.questionDotToken) return null; if (!lowerer.isStdlibGlobal(callee.expression, "Object")) return null; if (callee.name.text !== "fromEntries") return null; - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) + return null; + const argNode = call.arguments.nodes[0]!; const argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (argIr?.kind !== "array") return null; const represented = tryLowerExpression(lowerer, argNode); @@ -1052,9 +1058,10 @@ export function lowerObjectAssignIndexShape( lowerer: Lowerer, call: ts.CallExpression, ): IrExpr | null { - if (call.arguments.length < 2 || call.arguments.some((a) => ts.isSpreadElement(a))) return null; + if (call.arguments.nodes.length < 2 || call.arguments.nodes.some((a) => ts.isSpreadElement(a))) + return null; const loc = locOf(call); - const targetIr = lowerer.mapTypeOf(lowerer.typeOf(call.arguments[0]!)); + const targetIr = lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[0]!)); if (targetIr?.kind !== "record") return null; const to = lowerer.shapes.get(targetIr.shapeId); if (!to?.indexValue || to.tuple) return null; @@ -1084,7 +1091,7 @@ export function lowerObjectAssignIndexShape( ovfLift: (WidthLift | "dyn") | null; } const plans: SourcePlan[] = []; - for (const argNode of call.arguments.slice(1)) { + for (const argNode of call.arguments.nodes.slice(1)) { const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (srcIr?.kind !== "record") return null; const from = lowerer.shapes.get(srcIr.shapeId); @@ -1252,9 +1259,9 @@ export function lowerObjectAssignIndexShape( lowerer.liftedFns.push(fn); return name; }; - let acc = lowerer.lowerExprExpecting(call.arguments[0]!, targetIr); + let acc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, targetIr); for (let i = 0; i < plans.length; i++) { - const src = lowerer.lowerExprExpecting(call.arguments[i + 1]!, { + const src = lowerer.lowerExprExpecting(call.arguments.nodes[i + 1]!, { kind: "record", shapeId: plans[i]!.fromId, }); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 79b91af64..aeefffe48 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -81,7 +81,7 @@ export function lowerIteratorHelperCall( if (!ts.isCallExpression(cur) || !ts.isPropertyAccessExpression(cur.expression)) return null; if (cur.questionDotToken || cur.expression.questionDotToken) return null; const name = cur.expression.name.text; - if ((name === "values" || name === "entries") && cur.arguments.length === 0) { + if ((name === "values" || name === "entries") && cur.arguments.nodes.length === 0) { const recvIr = lowerer.mapTypeOf(lowerer.typeOf(cur.expression.expression)); if (recvIr?.kind !== "array") return null; if (!lowerer.isStdlibMember(cur.expression)) return null; @@ -91,11 +91,11 @@ export function lowerIteratorHelperCall( } if ( !ITER_STAGES.has(name) || - cur.arguments.length !== 1 || - ts.isSpreadElement(cur.arguments[0]!) + cur.arguments.nodes.length !== 1 || + ts.isSpreadElement(cur.arguments.nodes[0]!) ) return null; - stages.push({ name, argNode: cur.arguments[0]!, access: cur.expression }); + stages.push({ name, argNode: cur.arguments.nodes[0]!, access: cur.expression }); cur = cur.expression.expression; } stages.reverse(); // source order @@ -154,9 +154,9 @@ export function lowerGeneratorHelperCall( if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); if (!isTerminal) { - const argNode = call.arguments[0]; - if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { - lowerer.noLowering(`.${name} with ${call.arguments.length} arguments`, call); + const argNode = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + lowerer.noLowering(`.${name} with ${call.arguments.nodes.length} arguments`, call); } // A stage on m.keys()/values()/entries() (or a Set's) walks the // collection inside the helper's own generator. @@ -205,7 +205,11 @@ export function lowerGeneratorHelperCall( let cur: ts.Expression = access.expression; while (isStageCall(lowerer, cur)) { const stageAccess = cur.expression as ts.PropertyAccessExpression; - stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + stages.push({ + name: stageAccess.name.text, + argNode: cur.arguments.nodes[0]!, + access: stageAccess, + }); cur = stageAccess.expression; } stages.reverse(); // source order @@ -271,7 +275,7 @@ export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression) if (nonNull) callee = (callee as ts.NonNullExpression).expression; if (!nonNull && call.questionDotToken === undefined) return null; if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null; - if (callee.name.text !== "return" || call.arguments.length !== 0) return null; + if (callee.name.text !== "return" || call.arguments.nodes.length !== 0) return null; if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null; const callT = lowerer.mapTypeOf(lowerer.typeOf(call)); const gen = lowerer.lowerExpr(callee.expression); @@ -321,8 +325,8 @@ export function lowerIteratorFromCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (access.name.text !== "from" || call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (access.name.text !== "from" || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; if (ts.isSpreadElement(argNode)) return null; const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); @@ -409,7 +413,11 @@ export function lowerForOfHelperChain( while (isStageCall(lowerer, cur)) { const stageAccess = cur.expression as ts.PropertyAccessExpression; if (stageAccess.name.text === "flatMap") return null; - stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + stages.push({ + name: stageAccess.name.text, + argNode: cur.arguments.nodes[0]!, + access: stageAccess, + }); cur = stageAccess.expression; } if (stages.length === 0) return null; @@ -533,9 +541,13 @@ export function lowerForOfHelperChain( * argument directly. */ function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null { if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 1 + ) return null; - const arg = expr.arguments[0]!; + const arg = expr.arguments.nodes[0]!; if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null; if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null; return lowerer.isStdlibMember(expr.expression) ? arg : null; @@ -553,7 +565,11 @@ function arrayIteratorCall( ? { receiver: fromArg, proj: "values" } : null; if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 0 + ) return null; const proj = expr.expression.name.text; if (proj !== "values" && proj !== "entries") return null; @@ -606,7 +622,11 @@ function collectionIteratorCall( : null; } if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 0 + ) return null; const proj = expr.expression.name.text; if (proj !== "keys" && proj !== "values" && proj !== "entries") return null; @@ -668,7 +688,8 @@ function isStageCall(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpr if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return false; if (expr.questionDotToken || expr.expression.questionDotToken) return false; if (!ITER_STAGES.has(expr.expression.name.text)) return false; - if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) return false; + if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!)) + return false; return ( isHelperReceiver(lowerer, expr.expression.expression) && lowerer.isStdlibMember(expr.expression) ); @@ -817,8 +838,8 @@ function lowerTerminal( ): { termArgs: IrExpr[]; resultT: IrType } { const termArgs: IrExpr[] = []; if (terminal === "toArray") { - if (call.arguments.length !== 0) - lowerer.noLowering(`.toArray with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`.toArray with ${call.arguments.nodes.length} arguments`, call); return { termArgs, resultT: arrayOf(elem) }; } if ( @@ -827,9 +848,9 @@ function lowerTerminal( terminal === "every" || terminal === "find" ) { - const argNode = call.arguments[0]; - if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { - lowerer.noLowering(`.${terminal} with ${call.arguments.length} arguments`, call); + const argNode = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + lowerer.noLowering(`.${terminal} with ${call.arguments.nodes.length} arguments`, call); } const f = lowerArrayCallback(lowerer, argNode, [elem], UNDEFINED_T).fnArg; if ( @@ -849,14 +870,14 @@ function lowerTerminal( }; } // reduce(f[, initial]) - const fNode = call.arguments[0]; + const fNode = call.arguments.nodes[0]; if ( - call.arguments.length < 1 || - call.arguments.length > 2 || + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || fNode === undefined || - call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { - lowerer.noLowering(`.reduce with ${call.arguments.length} arguments`, call); + lowerer.noLowering(`.reduce with ${call.arguments.nodes.length} arguments`, call); } // The accumulator binds the checker's result type. const accLead = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? elem; @@ -872,7 +893,7 @@ function lowerTerminal( lowerer.badType(fNode, lowerer.typeOf(fNode)); } const accT = (f.type as IrType & { kind: "func" }).ret; - if (call.arguments.length === 1 && !typeEquals(accT, elem)) { + if (call.arguments.nodes.length === 1 && !typeEquals(accT, elem)) { lowerer.noLowering( ".reduce without an initial value where the accumulator's type differs from the elements'", call, @@ -880,8 +901,8 @@ function lowerTerminal( ); } termArgs.push(f); - if (call.arguments[1] !== undefined) - termArgs.push(lowerer.lowerExprExpecting(call.arguments[1], accT)); + if (call.arguments.nodes[1] !== undefined) + termArgs.push(lowerer.lowerExprExpecting(call.arguments.nodes[1], accT)); return { termArgs, resultT: accT }; } diff --git a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts index 387ff81d3..f936e27cb 100644 --- a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts @@ -21,14 +21,14 @@ export function matchAllRowsReadByElements( const parent = outerValue(drain).parent; if ( ts.isCallExpression(parent) && - parent.arguments[0] === outerValue(drain) && + parent.arguments.nodes[0] === outerValue(drain) && ts.isPropertyAccessExpression(parent.expression) && parent.expression.name.text === "from" && lowerer.isStdlibGlobal(parent.expression.expression, "Array") ) { - const mapper = parent.arguments[1]; + const mapper = parent.arguments.nodes[1]; if (mapper === undefined) return rowsReadByElements(lowerer, parent, 0); - return parent.arguments.length === 2 && callbackReadsByElements(lowerer, mapper, 2); + return parent.arguments.nodes.length === 2 && callbackReadsByElements(lowerer, mapper, 2); } if (ts.isForOfStatement(parent) && parent.expression === outerValue(drain)) return forOfReadsByElements(lowerer, parent, companion); @@ -103,7 +103,7 @@ function rowsReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boo const literal = parent.parent; return ( ts.isArrayLiteralExpression(literal) && - literal.elements.length === 1 && + literal.elements.nodes.length === 1 && rowsReadByElements(lowerer, literal, depth + 1) ); } @@ -122,10 +122,13 @@ function rowsReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boo if (!ts.isCallExpression(call) || call.expression !== parent) return method === "length" && isValueRead(parent); if (ROW_CALLBACK_METHODS.has(method)) { - if (call.arguments.length !== 1 || !callbackReadsByElements(lowerer, call.arguments[0]!, 2)) + if ( + call.arguments.nodes.length !== 1 || + !callbackReadsByElements(lowerer, call.arguments.nodes[0]!, 2) + ) return false; } else if (ROW_SUBSET_METHODS.has(method) || ROW_PICK_METHODS.has(method)) { - if (call.arguments.some((a) => ts.isSpreadElement(a))) return false; + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) return false; } else { return false; } @@ -172,8 +175,8 @@ function forOfReadsByElements( companion: boolean, ): boolean { const list = loop.initializer; - if (!ts.isVariableDeclarationList(list) || list.declarations.length !== 1) return false; - const decl = list.declarations[0]!; + if (!ts.isVariableDeclarationList(list) || list.declarations.nodes.length !== 1) return false; + const decl = list.declarations.nodes[0]!; if (!ts.isIdentifier(decl.name)) return false; const symbol = lowerer.resolveValueSymbol(decl.name); if (!symbol || !bindingNeverReassigned(lowerer, symbol, decl)) return false; @@ -202,8 +205,8 @@ function callbackReadsByElements( let fn: ts.Node = node; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false; - if (fn.parameters.length > maxParams) return false; - const param = fn.parameters[0]; + if (fn.parameters.nodes.length > maxParams) return false; + const param = fn.parameters.nodes[0]; if (param === undefined) return true; if (param.dotDotDotToken !== undefined || param.initializer !== undefined) return false; if (ts.isObjectBindingPattern(param.name)) @@ -230,7 +233,7 @@ function constBindingReadsByElements( const statement = decl.parent.parent; if ( ts.isVariableStatement(statement) && - statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + statement.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ) return false; const symbol = lowerer.resolveValueSymbol(decl.name); @@ -267,8 +270,8 @@ function groupsProjectedParam(lowerer: Lowerer, param: ts.ParameterDeclaration): /** An object pattern that binds only `groups`. */ function groupsPattern(pattern: ts.ObjectBindingPattern): boolean { return ( - pattern.elements.length > 0 && - pattern.elements.every((el) => { + pattern.elements.nodes.length > 0 && + pattern.elements.nodes.every((el) => { if (el.dotDotDotToken !== undefined) return false; const prop = el.propertyName; if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index dd70eb197..832dc7def 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -738,7 +738,7 @@ export function lowerRegexMethodCall( }; const loc = locOf(call); if (receiverKind === "regex" && name === "toString") { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering("RegExp.prototype.toString with arguments", call); } return { @@ -751,20 +751,21 @@ export function lowerRegexMethodCall( }; } if (receiverKind === "regex" && name === "test") { - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("RegExp.prototype.test with surplus or spread arguments", call); } const receiver = lowerReceiver(); - const args = [lowerRegexSubject(lowerer, call.arguments[0], loc)]; + const args = [lowerRegexSubject(lowerer, call.arguments.nodes[0], loc)]; return { kind: "regexIntrinsic", method: "test", receiver, args, type: BOOL, loc }; } // `re.exec(s)` returns the exec-shaped string[] | null slice, with // numeric lastIndex state for global/sticky regexes. Nonparticipating // captures retain match's present undefined element state. if (receiverKind === "regex" && name === "exec") { - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) + return null; const re = lowerReceiver(); - const subject = lowerRegexSubject(lowerer, call.arguments[0], loc); + const subject = lowerRegexSubject(lowerer, call.arguments.nodes[0], loc); const resultT: IrType = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]), @@ -819,9 +820,9 @@ export function lowerRegexMethodCall( ); })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern match: the SC2020 fence - if (call.arguments.length !== 1) return null; + if (call.arguments.nodes.length !== 1) return null; const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); @@ -863,9 +864,9 @@ export function lowerRegexMethodCall( // regexes throw Node's exact TypeError at runtime, catchably // (replaceAll's stance). if (receiverKind === "string" && name === "matchAll") { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence - if (call.arguments.length !== 1) return null; + if (call.arguments.nodes.length !== 1) return null; const receiver = lowerReceiver(); const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); return { @@ -882,9 +883,9 @@ export function lowerRegexMethodCall( // — a fresh exec from position 0, so /g is irrelevant and /y anchors at // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence - if (call.arguments.length !== 1) return null; + if (call.arguments.nodes.length !== 1) return null; const receiver = lowerReceiver(); const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; @@ -897,9 +898,9 @@ export function lowerRegexMethodCall( receiverKind === "string" && (name === "replace" || name === "replaceAll" || name === "split") ) { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1120", call, @@ -913,11 +914,13 @@ export function lowerRegexMethodCall( name === "split" ? [ lowerer.lowerExprExpecting(arg0, { kind: "regex" }), - lowerSplitLimitArg(lowerer, call.arguments[1], loc), + lowerSplitLimitArg(lowerer, call.arguments.nodes[1], loc), ] : [ lowerer.lowerExprExpecting(arg0, { kind: "regex" }), - call.arguments[1] ? lowerer.lowerExpr(call.arguments[1]) : dynUndefinedExpr(loc), + call.arguments.nodes[1] + ? lowerer.lowerExpr(call.arguments.nodes[1]) + : dynUndefinedExpr(loc), ]; if (name !== "split") { const replacement = args[1]; @@ -946,14 +949,14 @@ export function lowerRegexMethodCall( if (!replacement || !templateType(replacement.type)) { lowerer.unsupported( "SC1120", - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, "replacement values that are not callable or convertible to a string", ); } // Array iteration can carry an explicit undefined even when the // checker spells string. RegExp replacement applies ToString once, // including when the pattern has no match; it does not call that value. - args[1] = coerceStringSearchValue(lowerer, replacement, call.arguments[1]!, loc); + args[1] = coerceStringSearchValue(lowerer, replacement, call.arguments.nodes[1]!, loc); } return { kind: "regexIntrinsic", @@ -983,10 +986,10 @@ function lowerReplaceCharChain(lowerer: Lowerer, call: ts.CallExpression): IrExp !ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined || (callee.name.text !== "replace" && callee.name.text !== "replaceAll") || - node.arguments.length !== 2 + node.arguments.nodes.length !== 2 ) break; - const [pattern, replacement] = node.arguments; + const [pattern, replacement] = node.arguments.nodes; if ( !pattern || !ts.isRegularExpressionLiteral(pattern) || @@ -1046,7 +1049,7 @@ export function lowerStringMethodCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, dynReceiver?: () => IrExpr, - argumentNodes: readonly ts.Expression[] = call.arguments, + argumentNodes: readonly ts.Expression[] = call.arguments.nodes, ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; if (dynReceiver === undefined && access.name.text === "localeCompare") @@ -1369,17 +1372,17 @@ function lowerLocaleCompareCall( if (receiverIr?.kind !== "string") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.localeCompare with ${call.arguments.length} arguments`, + `.localeCompare with ${call.arguments.nodes.length} arguments`, call, "the locales/options parameters select ICU collations that have no lowering — " + "pass exactly the comparison string", ); } const receiver = lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text); - const arg = lowerer.lowerExpr(call.arguments[0]!); + const arg = lowerer.lowerExpr(call.arguments.nodes[0]!); if (arg.type.kind !== "string") - lowerer.badType(call.arguments[0]!, lowerer.typeOf(call.arguments[0]!)); + lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!)); return { kind: "libCall", fn: "str.localeCompare", args: [receiver, arg], type: F64, loc }; } diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 1fdf09ef4..6856bc50b 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -155,7 +155,7 @@ function lowerStringReplacementValues( ) { lowerer.noLowering( `string .${method} callback signature`, - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, "use a callback returning string with a prefix of (match: string, offset: number, subject: string)", ); } diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index fb7bc7abc..6b0b54b55 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -23,8 +23,11 @@ export class DeferredModuleInitializers { defer(lowerer: Lowerer, statement: ts.Statement): IrStmt | null { let key: string; - if (ts.isVariableStatement(statement) && statement.declarationList.declarations.length === 1) { - const declaration = statement.declarationList.declarations[0]!; + if ( + ts.isVariableStatement(statement) && + statement.declarationList.declarations.nodes.length === 1 + ) { + const declaration = statement.declarationList.declarations.nodes[0]!; if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return null; const symbol = lowerer.checker.getSymbolAtLocation(declaration.name); const global = symbol && lowerer.globalsBySymbol.get(symbol); @@ -34,7 +37,7 @@ export class DeferredModuleInitializers { key = global.id; if ( ts.isObjectLiteralExpression(declaration.initializer) && - declaration.initializer.properties.every( + declaration.initializer.properties.nodes.every( (property) => (ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && @@ -260,11 +263,11 @@ function registryInitializer( if ( !ts.isPropertyAccessExpression(expression.expression) || lowerer.stdlibGlobalMember(expression.expression, "Object") !== "freeze" || - expression.arguments.length !== 1 || - !ts.isObjectLiteralExpression(expression.arguments[0]!) + expression.arguments.nodes.length !== 1 || + !ts.isObjectLiteralExpression(expression.arguments.nodes[0]!) ) return false; - expression = expression.arguments[0]!; + expression = expression.arguments.nodes[0]!; } const packageSource = npmStaticPackageOfPath(statement.getSourceFile().fileName) !== null; const initialized = (declaration: ts.Node): boolean => { @@ -324,7 +327,7 @@ function registryInitializer( const trivia = node.getSourceFile().text.slice(node.pos, node.getStart()); if (!/[@#]__PURE__/.test(trivia)) return false; if (!callable(node.expression)) return false; - return (node.arguments ?? []).every((argument) => value(argument, depth + 1)); + return (node.arguments?.nodes ?? []).every((argument) => value(argument, depth + 1)); } if ( ts.isPropertyAccessExpression(node) && @@ -358,11 +361,11 @@ function registryInitializer( ); } if (ts.isArrayLiteralExpression(node)) - return node.elements.every( + return node.elements.nodes.every( (element) => !ts.isSpreadElement(element) && value(element, depth + 1), ); if (ts.isObjectLiteralExpression(node)) - return node.properties.every((property) => { + return node.properties.nodes.every((property) => { if ( ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || @@ -459,11 +462,11 @@ function registryOwnerIsData( ts.isCallExpression(initializer) && ts.isPropertyAccessExpression(initializer.expression) && lowerer.stdlibGlobalMember(initializer.expression, "Object") === "freeze" && - initializer.arguments.length === 1; - if (frozen) initializer = (initializer as ts.CallExpression).arguments[0]!; + initializer.arguments.nodes.length === 1; + if (frozen) initializer = (initializer as ts.CallExpression).arguments.nodes[0]!; if ( !ts.isObjectLiteralExpression(initializer) || - !initializer.properties.every( + !initializer.properties.nodes.every( (property) => ts.isShorthandPropertyAssignment(property) || (ts.isPropertyAssignment(property) && @@ -527,7 +530,7 @@ function classWriteIsUnobserved( const source = statement.getSourceFile(); const declaration = info.decl!; if ( - declaration.members.some( + declaration.members.nodes.some( (member) => ts.isClassStaticBlockDeclaration(member) || (ts.isPropertyDeclaration(member) && @@ -539,7 +542,7 @@ function classWriteIsUnobserved( ) ) return false; - for (const earlier of source.statements) { + for (const earlier of source.statements.nodes) { if (earlier.getStart() <= declaration.getStart() || earlier.getStart() >= statement.getStart()) continue; if ( @@ -551,7 +554,7 @@ function classWriteIsUnobserved( continue; if ( ts.isVariableStatement(earlier) && - earlier.declarationList.declarations.every( + earlier.declarationList.declarations.nodes.every( (binding) => !binding.initializer || registryInitializer(lowerer, binding.initializer, earlier), ) diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index b71080a3f..13f3f2dbd 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -249,14 +249,14 @@ function runtimeProvided(info: ClassInfo): boolean { function exportedNames(files: readonly ts.SourceFile[]): ReadonlySet { const names = new Set(); for (const sf of files) - for (const statement of sf.statements) { + for (const statement of sf.statements.nodes) { if ( ts.isExportDeclaration(statement) && statement.moduleSpecifier === undefined && statement.exportClause && ts.isNamedExports(statement.exportClause) ) { - for (const element of statement.exportClause.elements) + for (const element of statement.exportClause.elements.nodes) names.add((element.propertyName ?? element.name).text); } else if (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)) { names.add(statement.expression.text); diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 2a9009aed..ceea9a3e3 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -67,7 +67,7 @@ export function scanDivergences( node.body !== undefined && node.name !== undefined && ts.isIdentifier(node.name) && - node.parameters.length > 0 + node.parameters.nodes.length > 0 ) { const list = fnsByName.get(node.name.text) ?? []; list.push(node); @@ -78,7 +78,8 @@ export function scanDivergences( const callee = node.expression; if (ts.isPropertyAccessExpression(callee)) { const member = callee.name.text; - if (member === "localeCompare" && node.arguments.length === 1) localeCompares.push(node); + if (member === "localeCompare" && node.arguments.nodes.length === 1) + localeCompares.push(node); const recv = callee.expression; if (ts.isIdentifier(recv)) { if ( @@ -86,7 +87,7 @@ export function scanDivergences( (recv.text === "Object" && (member === "keys" || member === "values" || member === "entries")) ) { - if (node.arguments[0] !== undefined) observers.push(node.arguments[0]); + if (node.arguments.nodes[0] !== undefined) observers.push(node.arguments.nodes[0]); } else if ( recv.text === "console" && (member === "log" || @@ -96,9 +97,10 @@ export function scanDivergences( member === "debug" || member === "dir") ) { - for (const a of node.arguments) if (!isLiteralLike(a)) observers.push(a); + for (const a of node.arguments.nodes) if (!isLiteralLike(a)) observers.push(a); } else if (recv.text === "Date" && member === "parse") { - if (node.arguments[0] !== undefined) dates.push({ call: node, arg: node.arguments[0] }); + if (node.arguments.nodes[0] !== undefined) + dates.push({ call: node, arg: node.arguments.nodes[0] }); } } } @@ -106,9 +108,9 @@ export function scanDivergences( if ( ts.isIdentifier(node.expression) && node.expression.text === "Date" && - node.arguments?.length === 1 + node.arguments?.nodes.length === 1 ) { - dates.push({ call: node, arg: node.arguments[0]! }); + dates.push({ call: node, arg: node.arguments.nodes[0]! }); } } else if (ts.isForInStatement(node)) { observers.push(node.expression); @@ -127,12 +129,12 @@ export function scanDivergences( if ( element !== undefined && ts.isArrayLiteralExpression(node.initializer) && - node.initializer.elements.some(isExistingObjectExpression) + node.initializer.elements.nodes.some(isExistingObjectExpression) ) arrayDecls.push({ decl: node, element }); } } else if (ts.isObjectLiteralExpression(node)) { - if (node.properties.length >= 1 && node.properties.every(isPlainProperty)) + if (node.properties.nodes.length >= 1 && node.properties.nodes.every(isPlainProperty)) literals.push(node); } else if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) { casts.push(node); @@ -162,7 +164,7 @@ export function scanDivergences( const identityUsesOf = (fn: ts.FunctionLikeDeclaration): (IdentityUse | null)[] => { let uses = usesByFn.get(fn); if (uses === undefined) { - uses = fn.parameters.map((p) => + uses = fn.parameters.nodes.map((p) => ts.isIdentifier(p.name) && fn.body !== undefined ? identityUse(fn.body, p.name.text) : null, ); usesByFn.set(fn, uses); @@ -181,9 +183,9 @@ export function scanDivergences( if (decl === undefined || !candidates.includes(decl as ts.FunctionLikeDeclaration)) continue; const fn = decl as ts.FunctionLikeDeclaration; const uses = identityUsesOf(fn); - call.arguments.forEach((arg, i) => { + call.arguments.nodes.forEach((arg, i) => { const use = uses[i]; - const param = fn.parameters[i]; + const param = fn.parameters.nodes[i]; if (use === null || use === undefined || param === undefined || param.type === undefined) return; if (!isExistingObjectExpression(arg)) return; @@ -259,7 +261,7 @@ export function scanDivergences( if (scope === undefined) continue; const use = elementWrite(scope, name, decl.end); if (use === null) continue; - for (const item of (decl.initializer as ts.ArrayLiteralExpression).elements) { + for (const item of (decl.initializer as ts.ArrayLiteralExpression).elements.nodes) { if (!isExistingObjectExpression(item)) continue; const copy = safely(() => widthCopyBetween(lowerer, item, element)); if (copy === undefined || copy === null) continue; @@ -303,7 +305,7 @@ export function scanDivergences( if (namedObjectDeclaration(checker, contextual) === null) continue; const declared = safely(() => checker.getPropertiesOfType(contextual).map((p) => p.name)); if (declared === undefined) continue; - const written = lit.properties.map((p) => propertyNameText(p)!); + const written = lit.properties.nodes.map((p) => propertyNameText(p)!); const assigned = binding === null ? new Set() : (assignedMembers.get(binding) ?? new Set()); const finding = keyOrderFinding(lowerer, contextual, declared, written, assigned); @@ -324,7 +326,7 @@ export function scanDivergences( // ── one-argument localeCompare over text outside the collation table ─ for (const call of localeCompares) { const access = call.expression as ts.PropertyAccessExpression; - for (const operand of [access.expression, call.arguments[0]!]) { + for (const operand of [access.expression, call.arguments.nodes[0]!]) { const text = stringLiteralText(operand); if (text !== null && !isCollationCovered(text)) { out.push(localeCompareDivergenceDiag(text, locOf(operand))); @@ -348,7 +350,7 @@ export function scanDivergences( let fn: ts.Node | undefined = cast.parent; while (fn !== undefined && !ts.isFunctionLike(fn)) fn = fn.parent; if (fn === undefined || !ts.isFunctionLike(fn)) continue; - const index = fn.parameters.findIndex( + const index = fn.parameters.nodes.findIndex( (p) => ts.isIdentifier(p.name) && p.name.text === inner.text, ); if (index < 0) continue; @@ -376,7 +378,7 @@ export function scanDivergences( const sites = decl === undefined ? undefined : downcasts.get(decl); if (sites === undefined) continue; for (const site of sites) { - const arg = call.arguments[site.index]; + const arg = call.arguments.nodes[site.index]; if (arg === undefined) continue; const argType = safely(() => checker.getTypeAtLocation(arg)); const source = argType === undefined ? null : classDeclarationOfType(checker, argType); @@ -443,7 +445,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp iterated = init.initializer; } if (!ts.isArrayLiteralExpression(iterated)) return []; - return iterated.elements.filter((e) => stringLiteralText(e) !== null); + return iterated.elements.nodes.filter((e) => stringLiteralText(e) !== null); } /** A checker query that fails here only loses a warning. */ @@ -529,7 +531,7 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null if ( node !== root && ts.isFunctionLike(node) && - node.parameters.some((p) => ts.isIdentifier(p.name) && p.name.text === name) + node.parameters.nodes.some((p) => ts.isIdentifier(p.name) && p.name.text === name) ) return "skip"; if (ts.isBinaryExpression(node)) { @@ -611,9 +613,9 @@ function arrayElementTypeNode(node: ts.TypeNode): ts.TypeNode | undefined { ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && (node.typeName.text === "Array" || node.typeName.text === "ReadonlyArray") && - node.typeArguments?.length === 1 + node.typeArguments?.nodes.length === 1 ) - return node.typeArguments[0]; + return node.typeArguments.nodes[0]; return undefined; } @@ -657,7 +659,7 @@ function elementWrite(root: ts.Node, name: string, from: number): string | null node.expression.text === name && ts.isVariableDeclarationList(node.initializer) ) { - const binding = node.initializer.declarations[0]?.name; + const binding = node.initializer.declarations.nodes[0]?.name; if (binding !== undefined && ts.isIdentifier(binding)) { const use = identityUse(node.statement, binding.text); if (use?.kind === "write") { @@ -837,8 +839,10 @@ function classExtends( let cur: ts.ClassLikeDeclaration | null = sub; for (let guard = 0; cur !== null && guard < 64; guard++) { if (cur === base) return true; - const heritage = cur.heritageClauses?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword); - const expr = heritage?.types[0]?.expression; + const heritage = cur.heritageClauses?.nodes.find( + (h) => h.token === ts.SyntaxKind.ExtendsKeyword, + ); + const expr = heritage?.types?.nodes[0]?.expression; if (expr === undefined) return false; const symbol = lowerer.checker.getSymbolAtLocation(expr); if (symbol === undefined) return false; diff --git a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts index 2d09efa1a..e3b9bf025 100644 --- a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts +++ b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts @@ -21,12 +21,12 @@ export function isNativeProxyInitializer( export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length !== 2 || args.some(ts.isSpreadElement)) lowerer.noLowering("new Proxy with this argument list", expr); const handler = args[1]!; if (isJsSourceFile(expr.getSourceFile()) && ts.isObjectLiteralExpression(handler)) { - for (const prop of handler.properties) { + for (const prop of handler.properties.nodes) { const callback = ts.isMethodDeclaration(prop) ? prop : ts.isPropertyAssignment(prop) && @@ -39,7 +39,7 @@ export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrEx // ProxyHandler types include symbols, which this boundary // explicitly refuses; they must not force a copying record // parameter or an unboxable string|symbol closure signature. - for (const param of callback.parameters) { + for (const param of callback.parameters.nodes) { if (ts.isIdentifier(param.name) && !param.type && !param.dotDotDotToken) lowerer.checkedCallbackParams.add(param); } diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 78d783a11..216069362 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -71,7 +71,7 @@ export function foldedStringKeyOf(lowerer: Lowerer, expr: ts.Expression): string // fallback (a template the checker DID type as one literal). let out = expr.head.text; let folded = true; - for (const span of expr.templateSpans) { + for (const span of expr.templateSpans.nodes) { const part = foldedStringKeyOf(lowerer, span.expression); if (part === null) { folded = false; @@ -167,7 +167,7 @@ function pureKeyExpr(lowerer: Lowerer, expr: ts.Expression): boolean { ); } if (ts.isTemplateExpression(expr)) - return expr.templateSpans.every((s) => pureKeyExpr(lowerer, s.expression)); + return expr.templateSpans.nodes.every((s) => pureKeyExpr(lowerer, s.expression)); return false; } @@ -246,7 +246,7 @@ export function lowerDynObjectLiteral( : { kind: "libCall", fn: "dyn.assign", args: [acc, chunk], type: DYN, loc }; fields = []; }; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if ( ts.isPropertyAssignment(prop) && !ts.isComputedPropertyName(prop.name) && @@ -511,7 +511,7 @@ function fenceClosureProbe( const fieldType: IrType & { kind: "func" } = slotType !== undefined && slotType.kind === "func" ? slotType - : { kind: "func", params: inner.parameters.map(() => DYN), ret: DYN }; + : { kind: "func", params: inner.parameters.nodes.map(() => DYN), ret: DYN }; if (slotType !== undefined && slotType.kind !== "func") return null; const diagsBefore = lowerer.diags.length; try { @@ -562,7 +562,7 @@ function literalUnionArmOf( if (!tsType.isUnionType()) return null; const props: { name: string; type: ts.Type }[] = []; let hasSpread = false; - for (const p of expr.properties) { + for (const p of expr.properties.nodes) { if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { props.push({ name: propNameText(lowerer, p.name), type: lowerer.typeOf(p.initializer) }); } else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) { @@ -698,11 +698,11 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some( + expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && ((ts.isArrayLiteralExpression(prop.initializer) && - prop.initializer.elements.length === 0) || + prop.initializer.elements.nodes.length === 0) || neverTaintedJsType(lowerer, prop.initializer, lowerer.typeOf(prop.initializer))), ) ) { @@ -711,7 +711,7 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some( + expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && lowerer.mapTypeOf(lowerer.typeOf(prop.initializer))?.kind === "array" && @@ -726,7 +726,7 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some((prop) => { + expr.properties.nodes.some((prop) => { let value = ts.isPropertyAssignment(prop) ? prop.initializer : ts.isShorthandPropertyAssignment(prop) @@ -754,8 +754,8 @@ export function lowerObjectLiteral( // literal in checked native storage instead of erasing its prototype ABI. if ( isJsSourceFile(expr.getSourceFile()) && - (expr.properties.length === 0 || - expr.properties.some( + (expr.properties.nodes.length === 0 || + expr.properties.nodes.some( (prop) => ts.isMethodDeclaration(prop) || ts.isGetAccessorDeclaration(prop) || @@ -782,7 +782,7 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some( + expr.properties.nodes.some( (property) => ts.isSpreadAssignment(property) && (lowerer.builtinNamespaceModuleOf(property.expression)?.startsWith("path") || @@ -802,7 +802,7 @@ export function lowerObjectLiteral( // only arises in checked-dynamic JS. if ( isJsSourceFile(expr.getSourceFile()) && - expr.properties.some((p) => { + expr.properties.nodes.some((p) => { const n = ts.isSpreadAssignment(p) ? undefined : p.name; return n !== undefined && ts.isComputedPropertyName(n); }) @@ -813,7 +813,7 @@ export function lowerObjectLiteral( // message even when they also make the literal's type unmappable // (a getter or a `this`-returning method would otherwise surface as an // opaque type fence on the whole literal). - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isSpreadAssignment(prop)) { // Supported shapes: FULL spreads of known record shapes read as // identifiers or other side-effect-free reads, all BEFORE any @@ -935,7 +935,7 @@ export function lowerObjectLiteral( // preserves keys, evaluation order, and fields overwritten afterwards. if ( mapped?.kind === "dyn" && - expr.properties.some( + expr.properties.nodes.some( (p) => ts.isSpreadAssignment(p) && lowerer.mapTypeOf(lowerer.typeOf(p.expression))?.kind === "dyn", ) @@ -954,7 +954,7 @@ export function lowerObjectLiteral( // object): `{}` carries no shape information, so the literal builds as // its OWN type, exactly the unmappable-context fallback below. An // empty LITERAL keeps the context (the shapes agree). - if (mapped?.kind === "record" && expr.properties.length > 0) { + if (mapped?.kind === "record" && expr.properties.nodes.length > 0) { const ctxShape = lowerer.shapes.get(mapped.shapeId); if (ctxShape && ctxShape.fields.length === 0 && !ctxShape.indexValue && !ctxShape.tuple) { mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped; @@ -970,7 +970,7 @@ export function lowerObjectLiteral( // type carries but the shape dropped (settled value/reason, // generic-callable members) belongs to the drop paths below, and // index-signature contexts keep every key (overflow capture). - if (mapped?.kind === "record" && expr.properties.length > 0) { + if (mapped?.kind === "record" && expr.properties.nodes.length > 0) { const ctxShape = lowerer.shapes.get(mapped.shapeId); if (ctxShape && !ctxShape.indexValue && !ctxShape.tuple) { const names = new Set(ctxShape.fields.map((f) => f.name)); @@ -979,7 +979,7 @@ export function lowerObjectLiteral( return members.some((m) => lowerer.checker.getPropertyOfType(m, name) !== undefined); }; const extraOf = (text: string): boolean => !names.has(text) && !ctxHasProp(text); - const extra = expr.properties.some((p) => { + const extra = expr.properties.nodes.some((p) => { if (ts.isSpreadAssignment(p) || !p.name) return false; if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) return extraOf(p.name.text); if (ts.isNumericLiteral(p.name)) return extraOf(String(Number(p.name.text))); @@ -1036,7 +1036,7 @@ export function lowerObjectLiteral( !armShape?.tuple && // `{} | undefined` supplies no layout for a populated options // literal, just as a bare empty-record context supplies none. - (expr.properties.length === 0 || + (expr.properties.nodes.length === 0 || (armShape && (armShape.fields.length > 0 || armShape.indexValue))) ) { mapped = recordArms[0]!; @@ -1062,7 +1062,7 @@ export function lowerObjectLiteral( if ( ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId) || - expr.properties.some(ts.isSpreadAssignment) + expr.properties.nodes.some(ts.isSpreadAssignment) ) { const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms); if (arm) mapped = arm; @@ -1080,13 +1080,13 @@ export function lowerObjectLiteral( if ( isJsSourceFile(expr.getSourceFile()) && (!mapped || - expr.properties.some( + expr.properties.nodes.some( (p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p), )) && isCjsExportTableLiteral(expr) ) { const fields: { name: string; value: IrExpr }[] = []; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) continue; const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name; if (!propName || !(ts.isIdentifier(propName) || ts.isStringLiteral(propName))) continue; @@ -1135,7 +1135,9 @@ export function lowerObjectLiteral( if ( expected && !isJsSourceFile(expr.getSourceFile()) && - expr.properties.every((p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) + expr.properties.nodes.every( + (p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p), + ) ) { const own = lowerer.shapes.get(type.shapeId)!; const target = lowerer.shapes.get(expected.shapeId)!; @@ -1159,7 +1161,7 @@ export function lowerObjectLiteral( // A Date's toJSON() result keeps its null in a field with no declared // destination: the lib types say string. if (!expected) { - for (const property of expr.properties) { + for (const property of expr.properties.nodes) { const value = ts.isPropertyAssignment(property) ? property.initializer : ts.isShorthandPropertyAssignment(property) @@ -1196,7 +1198,7 @@ export function lowerObjectLiteral( // accessor names — the documented divergence). if (isJsSourceFile(expr.getSourceFile())) { const accessorNames = new Set( - expr.properties + expr.properties.nodes .filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) .map((p) => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : "", @@ -1224,14 +1226,14 @@ export function lowerObjectLiteral( expr.parent.initializer === expr && ts.isVariableStatement(expr.parent?.parent?.parent) && ts.isSourceFile(expr.parent?.parent?.parent?.parent); - const assignedProperties = expr.properties.filter( + const assignedProperties = expr.properties.nodes.filter( (prop): prop is ts.PropertyAssignment | ts.ShorthandPropertyAssignment => ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop), ); if ( topLevelJsDecl && isJsSourceFile(expr.getSourceFile()) && - assignedProperties.length === expr.properties.length + assignedProperties.length === expr.properties.nodes.length ) { const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name))); if (shape.fields.some((field) => !provided.has(field.name))) { @@ -1261,11 +1263,11 @@ export function lowerObjectLiteral( !shape.indexValue && !shape.tuple && !shapeHasAccessorSlots(shape) && - expr.properties.length >= 2 && - ts.isSpreadAssignment(expr.properties[0]!) && - !conditionalSpreadOf(expr.properties[0]!.expression) && - !andSpreadOf(lowerer, expr.properties[0]!.expression) && - expr.properties + expr.properties.nodes.length >= 2 && + ts.isSpreadAssignment(expr.properties.nodes[0]!) && + !conditionalSpreadOf(expr.properties.nodes[0]!.expression) && + !andSpreadOf(lowerer, expr.properties.nodes[0]!.expression) && + expr.properties.nodes .slice(1) .every( (p) => @@ -1277,8 +1279,8 @@ export function lowerObjectLiteral( (ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)), ) ) { - const spread = expr.properties[0]! as ts.SpreadAssignment; - const props = expr.properties.slice(1) as ( + const spread = expr.properties.nodes[0]! as ts.SpreadAssignment; + const props = expr.properties.nodes.slice(1) as ( | ts.PropertyAssignment | ts.ShorthandPropertyAssignment )[]; @@ -1334,12 +1336,12 @@ export function lowerObjectLiteral( shape.indexValue && shape.fields.length === 0 && !shape.tuple && - (expr.properties.some((p) => ts.isSpreadAssignment(p)) || - expr.properties.some(isRuntimeComputedKey)) + (expr.properties.nodes.some((p) => ts.isSpreadAssignment(p)) || + expr.properties.nodes.some(isRuntimeComputedKey)) ) { const contributors: IndexMergeContributor[] = []; let mergeable = true; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isSpreadAssignment(prop)) { const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { @@ -1463,7 +1465,7 @@ export function lowerObjectLiteral( } // Unfoldable computed keys outside the index-signature merge path: // no record shape can hold a runtime-decided name. - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (isRuntimeComputedKey(prop)) { lowerer.unsupported( "SC1090", @@ -1481,14 +1483,14 @@ export function lowerObjectLiteral( !shape.indexValue && !shape.tuple && shape.fields.length > 0 && - expr.properties.length > 0 && - expr.properties.every( + expr.properties.nodes.length > 0 && + expr.properties.nodes.every( (p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression) && !andSpreadOf(lowerer, p.expression), ) && - expr.properties.some((p) => { + expr.properties.nodes.some((p) => { const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression)); return ( t?.kind === "dyn" || (t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue) @@ -1515,7 +1517,7 @@ export function lowerObjectLiteral( const fields: { name: string; value: IrExpr; overflow?: true }[] = []; const spreadStmts: IrStmt[] = []; - const hasSpread = expr.properties.some(ts.isSpreadAssignment); + const hasSpread = expr.properties.nodes.some(ts.isSpreadAssignment); const captured = new Set(); const capture = (value: IrExpr): IrExpr => { const local = lowerer.declareHiddenLocal("%spreadValue", value.type); @@ -1594,7 +1596,7 @@ export function lowerObjectLiteral( // Member names DROPPED from the shape (JS deferral — unlowerable pure // member reads narrow away; see the catch in the property loop). const droppedNames = new Set(); - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { // A later contributor can mutate an earlier source or overwrite a // value with effects. Snapshot pending fields at their source position, // before evaluating the next contributor, even if they are overwritten. @@ -1672,7 +1674,7 @@ export function lowerObjectLiteral( let srcNode: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression; const srcLowered = capture( - prop === expr.properties[0] && leadingSpreadLowered !== null + prop === expr.properties.nodes[0] && leadingSpreadLowered !== null ? leadingSpreadLowered : lowerer.lowerExpr(srcNode), ); @@ -1732,7 +1734,7 @@ export function lowerObjectLiteral( ); } const laterNames = new Set(); - for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { + for (const later of expr.properties.nodes.slice(expr.properties.nodes.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) continue; @@ -1968,7 +1970,7 @@ export function lowerObjectLiteral( // exactly like Node does. Conditional spreads don't count (their // empty arm defines nothing) — their own collision fences hold. const laterNames = new Set(); - for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { + for (const later of expr.properties.nodes.slice(expr.properties.nodes.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) continue; @@ -2214,7 +2216,7 @@ export function lowerObjectLiteral( // reads; the field promotion below adopts that layout. (fieldType?.kind === "record" && ts.isObjectLiteralExpression(init) && - init.properties.length > 0 + init.properties.nodes.length > 0 ? (() => { const nested = lowerer.lowerObjectLiteral(init, fieldType); return lowerer.isRuntimeOptionalRecordVariant(nested.type, fieldType) @@ -2347,7 +2349,7 @@ export function lowerObjectLiteral( * fields needs a different result layout and stays on the ordinary path. */ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr | null { if (isJsSourceFile(expr.getSourceFile())) return null; - const spread = expr.properties[0]; + const spread = expr.properties.nodes[0]; if (!spread || !ts.isSpreadAssignment(spread)) return null; let sourceNode = spread.expression; while (ts.isParenthesizedExpression(sourceNode) || ts.isSatisfiesExpression(sourceNode)) @@ -2361,7 +2363,7 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "record")) return null; const props: (ts.PropertyAssignment | ts.ShorthandPropertyAssignment)[] = []; const names: string[] = []; - for (const prop of expr.properties.slice(1)) { + for (const prop of expr.properties.nodes.slice(1)) { if ( (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) || (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) @@ -2557,9 +2559,9 @@ export function andSpreadOf( ) ) return null; - if (right.properties.length === 0) return "unsupported"; + if (right.properties.nodes.length === 0) return "unsupported"; const props: CondSpreadProp[] = []; - for (const p of right.properties) { + for (const p of right.properties.nodes) { if ( (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && p.name && @@ -2595,12 +2597,14 @@ export function conditionalSpreadOf( if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) { return "unsupported"; } - const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0; - const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0; + const trueCarries = + whenFalse.properties.nodes.length === 0 && whenTrue.properties.nodes.length > 0; + const falseCarries = + whenTrue.properties.nodes.length === 0 && whenFalse.properties.nodes.length > 0; if (!trueCarries && !falseCarries) return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar const carrier = trueCarries ? whenTrue : whenFalse; const props: CondSpreadProp[] = []; - for (const p of carrier.properties) { + for (const p of carrier.properties.nodes) { if ( (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && p.name && @@ -2643,7 +2647,7 @@ function lowerDeclaredSpreadMerge( shape?: IrRecordShape; } const srcs: Src[] = []; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { const spread = prop as ts.SpreadAssignment; // caller-checked: all spreads const value = lowerer.lowerExpr(spread.expression); if (value.type.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 55feafc62..930e1a7cb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -253,7 +253,7 @@ export function lowerOptionalChain( } } const spreads: { arg: number; what: string }[] = []; - const args = expr.arguments.map((arg, index) => { + const args = expr.arguments.nodes.map((arg, index) => { if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN); spreads.push({ arg: index, what: arg.expression.getText() }); return lowerer.lowerExprExpecting(arg.expression, DYN); @@ -429,7 +429,7 @@ export function lowerOptionalChain( if (dotNode === expr && ts.isCallExpression(expr)) { if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode)); const params = narrowed.params; - const args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); + const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc }; return lowerer.finishOptionalChain(expr, id, receiver, body, loc); } diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index a3cf5c0ce..f413f4680 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -148,7 +148,7 @@ export function declaredReturnType( let mappedReturn = lowerer.mapTypeOf(retTsType); if ( isJsSourceFile(decl.getSourceFile()) && - decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && !decl.asteriskToken && mappedReturn?.kind !== "promise" ) { @@ -240,8 +240,8 @@ export function declaredReturnType( !hasExplicitJsDocReturn(decl) && mappedReturn !== null && promiseCarriesDyn(lowerer, mappedReturn) && - !decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && - decl.parameters.some((param) => { + !decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.parameters.nodes.some((param) => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(param.name)); return mapped === null || mapped.kind === "dyn"; }) @@ -278,7 +278,7 @@ export function declaredReturnType( // Native-only members (such as Int32Array layout offsets) cannot cross // that boundary; their factories keep the native record ABI. const asyncReturn = - decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && mappedReturn?.kind === "promise"; const recordReturn = asyncReturn && mappedReturn?.kind === "promise" ? mappedReturn.inner : mappedReturn; @@ -303,7 +303,7 @@ export function declaredReturnType( recordReturn !== undefined && inferredRecordReturn && lowerer.dynConvertible(recordReturn) && - decl.parameters.every((p) => { + decl.parameters.nodes.every((p) => { const mt = lowerer.mapTypeOf(lowerer.typeOf(p)); return mt === null || mt.kind === "dyn"; }) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 67dfb2408..bf5f2563d 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -207,11 +207,11 @@ function requireStableGenericBinding( export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDeclaration): void { const typeParams = declaredTypeParameterSymbols( lowerer, - decl.typeParameters!, + decl.typeParameters!.nodes, decl, "this function form", ); - checkGenericParameterNames(lowerer, decl.parameters, true); + checkGenericParameterNames(lowerer, decl.parameters.nodes, true); const nameText = decl.name?.text ?? "%default"; // nameless = the default export (checked by collectSignatureInner) const symbol = declSymbolOf(lowerer, decl); if (!symbol) lowerer.unsupported("SC1090", decl, "this function form"); @@ -245,7 +245,7 @@ export function lowerGenericCall( const instance = info.implicitParams ? implicitCallInstance(lowerer, expr, info) : genericCallInstance(lowerer, expr, info); - const args = lowerer.completeArgs(expr.arguments, instance.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } @@ -279,7 +279,7 @@ export function genericCallInstance( ); } } - if (!rsig || rsig.getParameters().length !== info.decl.parameters.length) { + if (!rsig || rsig.getParameters().length !== info.decl.parameters.nodes.length) { lowerer.unsupported("SC1090", expr, "this call form"); } // Per-param shapes from the RESOLVED signature (types substituted) plus @@ -288,9 +288,9 @@ export function genericCallInstance( // paramShape rules, applied to post-substitution types. const params = paramShapes( lowerer, - info.decl.parameters, + info.decl.parameters.nodes, rsig, - (_param, i) => expr.arguments[i] ?? expr, + (_param, i) => expr.arguments.nodes[i] ?? expr, ); const retTs = lowerer.checker.getReturnTypeOfSignature(rsig); const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs); @@ -438,7 +438,7 @@ export function genericLocalClassBinding(lowerer: Lowerer, node: ts.Node): IrTyp /** Whether `node` is declared inside a function with type parameters. */ function insideGenericFunction(node: ts.Node): boolean { for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) - if (ts.isFunctionLike(parent) && (parent.typeParameters?.length ?? 0) > 0) return true; + if (ts.isFunctionLike(parent) && (parent.typeParameters?.nodes.length ?? 0) > 0) return true; return false; } @@ -556,7 +556,7 @@ function internGenericInstance( function keyofConstrainedTypeParams(info: GenericFnInfo): Set { if (info.keyofTps) return info.keyofTps; const out = new Set(); - info.decl.typeParameters?.forEach((tpDecl, i) => { + info.decl.typeParameters?.nodes.forEach((tpDecl, i) => { const sym = info.typeParams[i]; if (!sym || tpDecl.constraint === undefined) return; if ( @@ -586,7 +586,7 @@ export function inferTypeParamBindings( tsBindings?: Map, ): Map { const bindings = new Map(); - expr.typeArguments?.forEach((ta, i) => { + expr.typeArguments?.nodes.forEach((ta, i) => { const tp = info.typeParams[i]; if (!tp) return; const taT = lowerer.checker.getTypeFromTypeNode(ta); @@ -597,7 +597,13 @@ export function inferTypeParamBindings( } }); unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings); - bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters, bindings, tsBindings); + bindDefaultTypeParams( + lowerer, + info.typeParams, + info.decl.typeParameters?.nodes, + bindings, + tsBindings, + ); return bindings; } @@ -847,7 +853,7 @@ export function lowerGenericInstance( // body returns the resolved promise's INNER type, calls enter through // the instance's own spawn wrapper (the emitter routes by fn.async), // and awaits park this instance's fibers. - const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const nameBlame: ts.Node = (ts.isArrowFunction(decl) ? undefined : decl.name) ?? decl; const isGenerator = decl.asteriskToken !== undefined; if (isAsync && !isGenerator && inst.returnType.kind !== "promise") { @@ -909,12 +915,12 @@ export function lowerGenericInstance( } // Default-param initializers lower per instance, with the bindings // threaded — a default mentioning T resolves like any body expression. - const declared = lowerer.declareParams(decl.parameters, inst.params); + const declared = lowerer.declareParams(decl.parameters.nodes, inst.params); params.push(...declared.params); const body = [...receiverInit, ...declared.prologue]; const bodyBlock = blockBodyOf(decl); if (bodyBlock) { - body.push(...lowerer.lowerStmts(bodyBlock.statements)); + body.push(...lowerer.lowerStmts(bodyBlock.statements.nodes)); if (fnCtx.inferReturn) { bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl); inst.returnType = bodyReturn; @@ -1053,7 +1059,7 @@ export function lowerGenericFnValue( unifySignatureBindings(lowerer, info, target, bindings); const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info); if (hofBinding) bindings.set(hofBinding.typeParam, hofBinding.type); - bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters, bindings); + bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters?.nodes, bindings); // A pinning signature that itself keeps type parameters (`let g: (x: // T) => T = identity` — storing the generic signature as such) binds // nothing: mapType answers null for an unsubstituted parameter. @@ -1076,13 +1082,13 @@ function runtimeOptionalHofGenericBinding( while (argument.parent && ts.isParenthesizedExpression(argument.parent)) argument = argument.parent; const call = argument.parent; - if (!ts.isCallExpression(call) || call.arguments[0] !== argument) return null; + if (!ts.isCallExpression(call) || call.arguments.nodes[0] !== argument) return null; if (!ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "map") return null; if (!lowerer.isStdlibMember(call.expression)) return null; const receiver = lowerer.mapTypeOf(lowerer.typeOf(call.expression.expression)); if (receiver?.kind !== "array") return null; - const parameter = info.decl.parameters[0]; + const parameter = info.decl.parameters.nodes[0]; if (!parameter || !ts.isIdentifier(parameter.name)) return null; const parameterType = lowerer.typeOf(parameter.name); if ((parameterType.flags & ts.TypeFlags.TypeParameter) === 0) return null; @@ -1138,7 +1144,7 @@ function genericValueInstance( try { const declSig = lowerer.checker.getSignatureFromDeclaration(info.decl); if (!declSig) lowerer.unsupported("SC1090", ref, "this function form"); - const params = paramShapes(lowerer, info.decl.parameters, declSig); + const params = paramShapes(lowerer, info.decl.parameters.nodes, declSig); const retTs = lowerer.checker.getReturnTypeOfSignature(declSig); const returnType = lowerer.mapTypeOf(retTs); if (!returnType) { @@ -1300,11 +1306,11 @@ export function implicitPatternsOf( lowerer: Lowerer, decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction, ): (ImplicitPattern | null)[] { - return decl.parameters.map((param): ImplicitPattern | null => { + return decl.parameters.nodes.map((param): ImplicitPattern | null => { if (!ts.isObjectBindingPattern(param.name) || !decl.body) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; const elements: ImplicitPattern["elements"] = []; - for (const element of param.name.elements) { + for (const element of param.name.elements.nodes) { if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name)) return null; const key = element.propertyName ?? element.name; @@ -1341,7 +1347,7 @@ function implicitPatternBinding( const values = new Map(); /** Shorthand properties name the binding they read. */ const shorthand = new Map(); - for (const property of literal.properties) { + for (const property of literal.properties.nodes) { if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) { values.set(property.name.text, property.name); const read = lowerer.checker.getShorthandAssignmentValueSymbol(property); @@ -1407,13 +1413,13 @@ export function implicitAnyParamSymbolsOf( ): (ts.Symbol | null)[] | null { if (!decl.body) return null; if (decl.asteriskToken) return null; - if (decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; + if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; if (decl.typeParameters !== undefined) return null; // real generics own the machinery - if (decl.parameters.length === 0) return null; + if (decl.parameters.nodes.length === 0) return null; // The variadic-`arguments` form keeps its dynRest story whole. if (bodyReadsArguments(decl)) return null; let hasImplicitParams = false; - const out = decl.parameters.map((param): ts.Symbol | null => { + const out = decl.parameters.nodes.map((param): ts.Symbol | null => { if (!ts.isIdentifier(param.name)) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; if (param.name.text === "this") return null; @@ -1456,7 +1462,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // Infer their result from the specialized body as well. if ( info.member?.kind === "static" && - info.decl.parameters.some( + info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] && lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "object", @@ -1464,7 +1470,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | ) return null; const broadCallbackSymbols = new Set( - info.decl.parameters.flatMap((param, index) => { + info.decl.parameters.nodes.flatMap((param, index) => { const symbol = info.implicitParams?.[index]; return symbol !== null && symbol !== undefined && @@ -1507,7 +1513,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // container instead of checking every element against that guess. if (!hasExplicitJsDocReturn(info.decl)) return null; if ( - info.decl.parameters.some( + info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] && lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "array", @@ -1661,8 +1667,8 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp if ( ts.isObjectLiteralExpression(arg) && isJsSourceFile(arg.getSourceFile()) && - (arg.properties.length === 0 || - arg.properties.some( + (arg.properties.nodes.length === 0 || + arg.properties.nodes.some( (property) => ts.isMethodDeclaration(property) || ts.isAccessor(property) || @@ -1726,9 +1732,9 @@ export function implicitCallInstance( /** The destructured names' bound types, which the object's own * representation does not always carry. */ const patternKey: string[] = []; - info.decl.parameters.forEach((param, i) => { + info.decl.parameters.nodes.forEach((param, i) => { const pattern = patterns[i]; - const patternArg = call.arguments[i]; + const patternArg = call.arguments.nodes[i]; if (pattern && patternArg && !ts.isSpreadElement(patternArg)) { // The argument's object type binds each destructured name, so a // class passed as `{ Base }` keeps its static class type. @@ -1751,7 +1757,7 @@ export function implicitCallInstance( let bound: IrType = DYN; const declared = lowerer.paramShape(param); if ((lowerer.typeOf(param.name).flags & ts.TypeFlags.Any) === 0) bound = declared.type; - const arg = call.arguments[i]; + const arg = call.arguments.nodes[i]; if (arg && !ts.isSpreadElement(arg)) { const stored = storedImplicitArgumentType(lowerer, arg); if (stored?.kind === "dyn") { @@ -1891,15 +1897,15 @@ export function implicitDefaultInstance( blame: ts.Node, info: GenericFnInfo, ): GenericInstance { - const shapes: ParamShape[] = info.decl.parameters.map((param, i) => + const shapes: ParamShape[] = info.decl.parameters.nodes.map((param, i) => info.implicitParams![i] ? { type: DYN, mode: "required" as const } : lowerer.paramShape(param), ); const argTypes = new Map(); // Newly bindable nominal class parameters must not retain their JSDoc // receiver type in the checked fallback. Keep existing broad JS inference. - for (let i = 0; i < info.decl.parameters.length; i++) { + for (let i = 0; i < info.decl.parameters.nodes.length; i++) { const symbol = info.implicitParams?.[i]; - const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters[i]!.name))?.kind; + const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters.nodes[i]!.name))?.kind; if (symbol && (kind === "object" || kind === "array")) { argTypes.set(symbol, lowerer.checker.getUnknownType()); } @@ -1935,7 +1941,7 @@ function internImplicitInstance( ); } const rendered = shapes.map((s) => lowerer.fmt(s.type)).join(", "); - const dynamicBroadPromise = info.decl.parameters.some( + const dynamicBroadPromise = info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] !== null && info.implicitParams?.[index] !== undefined && @@ -1976,7 +1982,9 @@ function internImplicitInstance( // this exact committed instance body here; repeated instantiations of // the same declaration find the facade memos warm. lowerer.checker.prefetchRoots([ - ...info.decl.parameters.flatMap((param) => (param.initializer ? [param.initializer] : [])), + ...info.decl.parameters.nodes.flatMap((param) => + param.initializer ? [param.initializer] : [], + ), ...(info.decl.body ? [info.decl.body] : []), ]); const fn = lowerer.lowerGenericInstance(info, inst); @@ -2179,7 +2187,7 @@ export function objLitGenericFnInfoOf( const existing = lowerer.objLitGenericFns.get(fnNode); if (existing) return existing; if (fnNode.asteriskToken) lowerer.unsupported("SC1071", blame); - if (fnNode.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { + if (fnNode.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { lowerer.unsupported("SC1090", blame, "async object-literal generic methods"); } // `this` is the receiver object — records don't model it (the @@ -2198,11 +2206,11 @@ export function objLitGenericFnInfoOf( } const typeParams = declaredTypeParameterSymbols( lowerer, - fnNode.typeParameters!, + fnNode.typeParameters!.nodes, blame, "this method form", ); - checkGenericParameterNames(lowerer, fnNode.parameters, false); + checkGenericParameterNames(lowerer, fnNode.parameters.nodes, false); const info: GenericFnInfo = { decl: fnNode, baseName: name, @@ -2312,7 +2320,12 @@ export function bindingGenericFnInfoOf( const typeParams: ts.Symbol[] = []; if (fnNode.typeParameters !== undefined) { typeParams.push( - ...declaredTypeParameterSymbols(lowerer, fnNode.typeParameters, fnNode, "this function form"), + ...declaredTypeParameterSymbols( + lowerer, + fnNode.typeParameters.nodes, + fnNode, + "this function form", + ), ); } else { // The CONTEXTUAL shape (bindingContextualGenericFnNodeOf): the type @@ -2328,7 +2341,7 @@ export function bindingGenericFnInfoOf( typeParams.push(tpSym); } } - checkGenericParameterNames(lowerer, fnNode.parameters, true); + checkGenericParameterNames(lowerer, fnNode.parameters.nodes, true); const stmt = decl.parent?.parent; // declarator → list → statement (nsPathPrefix wants the statement) const info: GenericFnInfo = { decl: fnNode, diff --git a/packages/compiler/src/frontend/lowering/import-meta.ts b/packages/compiler/src/frontend/lowering/import-meta.ts index 6d011b5ee..baf550e6d 100644 --- a/packages/compiler/src/frontend/lowering/import-meta.ts +++ b/packages/compiler/src/frontend/lowering/import-meta.ts @@ -65,7 +65,7 @@ export function importMetaBindingSource( ): IrExpr | null { if (!isImportMeta(initializer) || !ts.isObjectBindingPattern(pattern)) return null; const fields: { name: string; value: IrExpr }[] = []; - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.dotDotDotToken) lowerer.unsupported( "SC1031", diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index 522563355..32bb71858 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -168,7 +168,7 @@ function tableTypes(lowerer: Lowerer, type: ts.Type, blame: ts.Node): TableTypes export function lowerInternTableNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!ts.isIdentifier(expr.expression)) return null; if (experimentalImportOf(lowerer.program, expr.expression) !== "InternTable") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const arg = args[0]; if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) lowerer.noLowering(`new InternTable with ${args.length} arguments`, expr); @@ -265,11 +265,14 @@ export function lowerInternTableCall(lowerer: Lowerer, expr: ts.CallExpression): if (expr.questionDotToken !== undefined) lowerer.noLowering(`InternTable.${member}?.() calls`, expr, `call table.${member}() directly`); const arity = member === "get" || member === "has" ? 1 : 2; - if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a))) - lowerer.noLowering(`InternTable.${member} with ${expr.arguments.length} arguments`, expr); + if ( + expr.arguments.nodes.length !== arity || + expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) + lowerer.noLowering(`InternTable.${member} with ${expr.arguments.nodes.length} arguments`, expr); const types = tableTypes(lowerer, lowerer.typeOf(callee.expression), expr); const table = receiverOf(lowerer, callee); - const keyNode = expr.arguments[0]!; + const keyNode = expr.arguments.nodes[0]!; const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), types.key); if (member === "has") return { kind: "toBool", operand: find(handleOf(table, loc), key, loc), type: BOOL, loc }; @@ -285,7 +288,7 @@ export function lowerInternTableCall(lowerer: Lowerer, expr: ts.CallExpression): loc, }; } - const valueNode = expr.arguments[1]!; + const valueNode = expr.arguments.nodes[1]!; const expected = member === "getOrInsert" ? types.value : funcOf([types.key], types.value); if ( member === "getOrInsertComputed" && @@ -399,10 +402,10 @@ function inlineArrowBody( value: IrType, ): IrExpr | null { if (!ts.isArrowFunction(node) || ts.isBlock(node.body)) return null; - if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; - const param = node.parameters[0]; + if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; + const param = node.parameters.nodes[0]; if ( - node.parameters.length > 1 || + node.parameters.nodes.length > 1 || (param !== undefined && (!ts.isIdentifier(param.name) || param.initializer !== undefined || diff --git a/packages/compiler/src/frontend/lowering/js-class-fields.ts b/packages/compiler/src/frontend/lowering/js-class-fields.ts index 4df513c91..2f726ce97 100644 --- a/packages/compiler/src/frontend/lowering/js-class-fields.ts +++ b/packages/compiler/src/frontend/lowering/js-class-fields.ts @@ -10,7 +10,7 @@ const PROPERTY_REFLECTION = new Set(["deleteProperty", "freeze", "seal", "preven function freshLiteralArgument(callee: ts.PropertyAccessExpression): boolean { const call = callee.parent; if (!ts.isCallExpression(call) || call.expression !== callee) return false; - let argument = call.arguments[0]; + let argument = call.arguments.nodes[0]; while (argument && ts.isParenthesizedExpression(argument)) argument = argument.expression; return ( argument !== undefined && @@ -101,12 +101,12 @@ export function constructionPrefix( ): Construction | null { const created = new Set(base?.created ?? []); let sealed = base?.sealed ?? true; - const ctor = decl.members.find( + const ctor = decl.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); - if (ctor?.parameters.some((parameter) => usesReceiver(parameter))) sealed = false; - const statements = ctor?.body?.statements ?? []; + if (ctor?.parameters?.nodes.some((parameter) => usesReceiver(parameter))) sealed = false; + const statements = ctor?.body?.statements?.nodes ?? []; // A derived constructor's statements before super() cannot reach the // instance; the field declarations run when super() returns. let at = 0; @@ -119,12 +119,12 @@ export function constructionPrefix( ); if (superAt < 0) return null; const call = (statements[superAt] as ts.ExpressionStatement).expression as ts.CallExpression; - if (call.arguments.some(usesReceiver)) sealed = false; + if (call.arguments.nodes.some(usesReceiver)) sealed = false; at = superAt + 1; } - for (const member of decl.members) { + for (const member of decl.members.nodes) { if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name)) continue; - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) + if (member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) continue; if (member.initializer && usesReceiver(member.initializer)) sealed = false; if (sealed) created.add(member.name.text); diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index 1d7ab6eee..8f17a2448 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -57,10 +57,11 @@ function isAssignmentOperator(kind: ts.SyntaxKind): boolean { * no heritage, decorators or other members, a constructor body that * returns a value at its own level and never mentions `this`/`super`. */ export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boolean { - if (decl.heritageClauses !== undefined && decl.heritageClauses.length > 0) return false; - if (decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.Decorator)) return false; + if (decl.heritageClauses !== undefined && decl.heritageClauses.nodes.length > 0) return false; + if (decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.Decorator)) + return false; let ctor: ts.ConstructorDeclaration | undefined; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if (ts.isSemicolonClassElement(member)) continue; if (!ts.isConstructorDeclaration(member) || ctor !== undefined) return false; ctor = member; @@ -83,7 +84,7 @@ export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boo node.forEachChild(visit); }; ctor.body.forEachChild(visit); - for (const parameter of ctor.parameters) parameter.forEachChild(visit); + for (const parameter of ctor.parameters.nodes) parameter.forEachChild(visit); return returnsValue && !observesThis; } @@ -97,7 +98,7 @@ function insideArrow(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean { function markNested(literal: ts.ObjectLiteralExpression, out: Set): void { if (out.has(literal)) return; out.add(literal); - for (const property of literal.properties) { + for (const property of literal.properties.nodes) { if (!ts.isPropertyAssignment(property)) continue; const value = unparenthesized(property.initializer); if (ts.isObjectLiteralExpression(value)) markNested(value, out); @@ -128,7 +129,7 @@ export function jsDynamicDeclarations( const init = unparenthesized(node.initializer); // An empty literal's binding is already checked-dynamic // (jsOpenObjectType), and its type describes no layout. - if (!ts.isObjectLiteralExpression(init) || init.properties.length === 0) return; + if (!ts.isObjectLiteralExpression(init) || init.properties.nodes.length === 0) return; const symbol = checker.getSymbolAtLocation(node.name); if (symbol !== undefined) literals.set(symbol, init); return; @@ -158,7 +159,7 @@ export function jsDynamicDeclarations( callee.expression.text === "Object" && (callee.name.text === "defineProperty" || callee.name.text === "defineProperties") ) - receiver(node.arguments[0]); + receiver(node.arguments.nodes[0]); } }, SCANNED_KINDS, @@ -195,8 +196,8 @@ export function lowerValueReturningConstructorNew( : undefined; if (decl === undefined) return null; const loc = locOf(expr); - const ctor = decl.members.find(ts.isConstructorDeclaration)!; - const simple = ctor.parameters.every( + const ctor = decl.members.nodes.find(ts.isConstructorDeclaration)!; + const simple = ctor.parameters.nodes.every( (p) => ts.isIdentifier(p.name) && p.dotDotDotToken === undefined && p.initializer === undefined, ); if (!simple || !ts.isSourceFile(decl.parent)) { @@ -206,7 +207,7 @@ export function lowerValueReturningConstructorNew( "constructing a class whose constructor returns a value, with parameter defaults, rest or patterns, or declared inside a function", ); } - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) { lowerer.unsupported( "SC1090", @@ -223,11 +224,14 @@ export function lowerValueReturningConstructorNew( lowerer.currentClass = null; lowerer.fnStack.push(newFnCtx(false, null, null, DYN)); try { - const shapes: ParamShape[] = ctor.parameters.map(() => ({ type: DYN, mode: "required" })); - const declared = lowerer.declareParams(ctor.parameters, shapes); + const shapes: ParamShape[] = ctor.parameters.nodes.map(() => ({ + type: DYN, + mode: "required", + })); + const declared = lowerer.declareParams(ctor.parameters.nodes, shapes); const body: IrStmt[] = [ ...declared.prologue, - ...lowerer.lowerStmts(ctor.body!.statements), + ...lowerer.lowerStmts(ctor.body!.statements.nodes), { kind: "runtimeFence", code: "SC1090", @@ -248,12 +252,12 @@ export function lowerValueReturningConstructorNew( lowerer.currentClass = savedClass; } } - const lowered = ctor.parameters.map((_, i) => + const lowered = ctor.parameters.nodes.map((_, i) => args[i] !== undefined ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc), ); // Surplus arguments still evaluate, left to right, after the others. const surplus: IrStmt[] = args - .slice(ctor.parameters.length) + .slice(ctor.parameters.nodes.length) .map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) })); const result = lowerer.declareHiddenLocal("%constructed", DYN); const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc }; diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index fc5fab24d..b212ae473 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -77,8 +77,8 @@ export function lifetimeRegionType(lowerer: Lowerer): IrType { export function lowerLifetimeRegionNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!ts.isIdentifier(expr.expression)) return null; if (experimentalImportOf(lowerer.program, expr.expression) !== "LifetimeRegion") return null; - if ((expr.arguments?.length ?? 0) !== 0) - lowerer.noLowering(`new LifetimeRegion with ${expr.arguments!.length} arguments`, expr); + if ((expr.arguments?.nodes.length ?? 0) !== 0) + lowerer.noLowering(`new LifetimeRegion with ${expr.arguments!.nodes.length} arguments`, expr); const type = lifetimeRegionType(lowerer); return { kind: "new", className: LIFETIME_REGION_CLASS, args: [], type, loc: locOf(expr) }; } @@ -102,8 +102,11 @@ export function lowerLifetimeRegionCall(lowerer: Lowerer, expr: ts.CallExpressio ); const region = lowerer.coerceInto(callee.expression, lowerer.lowerExpr(callee.expression), type); if (member === "release") { - if (expr.arguments.length !== 0) - lowerer.noLowering(`LifetimeRegion.release with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 0) + lowerer.noLowering( + `LifetimeRegion.release with ${expr.arguments.nodes.length} arguments`, + expr, + ); return { kind: "intrinsic", name: "region.release", @@ -121,9 +124,9 @@ export function lowerLifetimeRegionCall(lowerer: Lowerer, expr: ts.CallExpressio loc, }; } - const arg = expr.arguments[0]; - if (expr.arguments.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) - lowerer.noLowering(`LifetimeRegion.run with ${expr.arguments.length} arguments`, expr); + const arg = expr.arguments.nodes[0]; + if (expr.arguments.nodes.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) + lowerer.noLowering(`LifetimeRegion.run with ${expr.arguments.nodes.length} arguments`, expr); const resultTs = lowerer.typeOf(expr); const mapped = resultTs.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(resultTs); if (mapped === null) lowerer.badType(expr, resultTs); diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 9d434eae1..8c49db594 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -271,11 +271,11 @@ function lowerAssertLooseEqual( ): IrExpr { const surface = negated ? "assert.notEqual" : "assert.equal"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]; - const bNode = expr.arguments[1]; + const aNode = expr.arguments.nodes[0]; + const bNode = expr.arguments.nodes[1]; if (!aNode || !bNode) throw new InternalCompilerError(`${surface} arity fence returned unexpectedly`); const left = lowerer.lowerExpr(aNode); @@ -329,7 +329,7 @@ function lowerAssertLooseEqual( }); const actual = inspectOnFailure(a); const expected = inspectOnFailure(b); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); const result: IrExpr = { kind: "libCall", fn: "assert.looseResult", @@ -352,7 +352,7 @@ function lowerComparedOperands( loc: SrcLoc, ): { a: IrExpr; b: IrExpr } { const emptyArrayLit = (n: ts.Expression): boolean => - ts.isArrayLiteralExpression(n) && n.elements.length === 0; + ts.isArrayLiteralExpression(n) && n.elements.nodes.length === 0; const emptyOf = (t: IrType): IrExpr => ({ kind: "arrayLit", elems: [], type: t, loc }); // A dyn other side adopts number[] — the dyn array a boxed empty // literal builds is elementless, so any elem type serves. @@ -408,13 +408,13 @@ function lowerAssertLooseDeep( ): IrExpr { const surface = negated ? "assert.notDeepEqual" : "assert.deepEqual"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]!; - const bNode = expr.arguments[1]!; + const aNode = expr.arguments.nodes[0]!; + const bNode = expr.arguments.nodes[1]!; const { a, b } = lowerComparedOperands(lowerer, aNode, bNode, loc); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); refuseUnrenderedDeepOperands( lowerer, surface, @@ -532,13 +532,13 @@ function boxDeepOperand( function lowerAssertPartialDeep(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const surface = "assert.partialDeepStrictEqual"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]!; - const bNode = expr.arguments[1]!; + const aNode = expr.arguments.nodes[0]!; + const bNode = expr.arguments.nodes[1]!; const { a, b } = lowerComparedOperands(lowerer, aNode, bNode, loc); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); refuseUnrenderedDeepOperands( lowerer, surface, @@ -888,14 +888,14 @@ export function lowerAssertDirectCall( * the same bytes at compile time). */ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, expr.expression.getText()); - if (expr.arguments.length < 1 || expr.arguments.length > 2) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `${expr.expression.getText()} with ${expr.arguments.length} arguments`, + `${expr.expression.getText()} with ${expr.arguments.nodes.length} arguments`, expr, "the supported forms are assert(value) and assert(value, message)", ); } - const valueNode = expr.arguments[0]!; + const valueNode = expr.arguments.nodes[0]!; const stmts: IrStmt[] = []; const value = stableValue(lowerer, stmts, lowerer.lowerExpr(valueNode), "%assertOkValue", loc); const pass = stableValue( @@ -905,7 +905,7 @@ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): "%assertOkPass", loc, ); - const msgNode = expr.arguments[1]; + const msgNode = expr.arguments.nodes[1]; let msg: IrExpr; let generated = false; if (!msgNode || (ts.isIdentifier(msgNode) && msgNode.text === "undefined")) { @@ -936,16 +936,16 @@ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): * assert.ok over a false literal. */ function lowerAssertFail(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, "assert.fail"); - if (expr.arguments.length > 1) { + if (expr.arguments.nodes.length > 1) { lowerer.noLowering( - `assert.fail with ${expr.arguments.length} arguments`, + `assert.fail with ${expr.arguments.nodes.length} arguments`, expr, "the supported form is fail(message?) — the deprecated fail(actual, expected, ...) has no lowering", ); } let msg: IrExpr; let generated: IrExpr; - const msgNode = expr.arguments[0]; + const msgNode = expr.arguments.nodes[0]; const stmts: IrStmt[] = []; if (!msgNode || (ts.isIdentifier(msgNode) && msgNode.text === "undefined")) { msg = { kind: "strLit", value: "Failed", type: STRING, loc }; @@ -996,13 +996,13 @@ function lowerAssertEqual( ): IrExpr { const surface = `assert.${deep ? (negated ? "notDeepStrictEqual" : "deepStrictEqual") : negated ? "notStrictEqual" : "strictEqual"}`; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]!; - const bNode = expr.arguments[1]!; + const aNode = expr.arguments.nodes[0]!; + const bNode = expr.arguments.nodes[1]!; const { a, b } = lowerComparedOperands(lowerer, aNode, bNode, loc); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); // An Assert instance with skipPrototype: Node compares own properties // without the prototype check, so operands of different static types (two // classes, a class and a record) can be equal. They compare as @@ -1155,8 +1155,8 @@ function lowerAssertEqual( // the call through a binding whose DECLARED constructor differs from the // value's runtime one compares by the declared brand (SEMANTICS.md). if (a.type.kind === "bytes" && typeEquals(a.type, b.type)) { - const brandA = bytesBrandOf(lowerer, expr.arguments[0]!); - const brandB = bytesBrandOf(lowerer, expr.arguments[1]!); + const brandA = bytesBrandOf(lowerer, expr.arguments.nodes[0]!); + const brandB = bytesBrandOf(lowerer, expr.arguments.nodes[1]!); if (!deep) { // Brands shape only the failure HEADER (same-structure vs // reference-equal expectation); an unclassifiable side defers to the @@ -1173,7 +1173,7 @@ function lowerAssertEqual( if (!skipPrototype && (brandA === null || brandB === null)) { lowerer.noLowering( `${surface} of typed-array values whose constructor is not in the static type`, - brandA === null ? expr.arguments[0]! : expr.arguments[1]!, + brandA === null ? expr.arguments.nodes[0]! : expr.arguments.nodes[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", ); } @@ -1350,19 +1350,19 @@ function lowerAssertMatch( ): IrExpr { const surface = negated ? "assert.doesNotMatch" : "assert.match"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const s = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const re = lowerer.lowerExpr(expr.arguments[1]!); + const s = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const re = lowerer.lowerExpr(expr.arguments.nodes[1]!); if (re.type.kind !== "regex") { lowerer.noLowering( `${surface} with a non-regex pattern`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, "the second argument must be a regular expression", ); } - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); const neg: IrExpr = { kind: "boolLit", value: negated, type: BOOL, loc }; return { kind: "libCall", fn: "assert.match", args: [s, re, neg, msg, hasMsg], type: VOID, loc }; } @@ -1478,7 +1478,7 @@ function classifyThrowsExpected( let obj: ts.Expression = node; while (ts.isParenthesizedExpression(obj)) obj = obj.expression; if (ts.isObjectLiteralExpression(obj)) { - if (obj.properties.length === 0) { + if (obj.properties.nodes.length === 0) { lowerer.noLowering( `${surface} with an empty expected object`, obj, @@ -1488,7 +1488,7 @@ function classifyThrowsExpected( // Duplicate keys keep the LAST value (JS object-literal semantics); // property order is preserved for argument evaluation. const byId = new Map(); - for (const prop of obj.properties) { + for (const prop of obj.properties.nodes) { const named = ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop) ? prop : null; if (!named || !(ts.isIdentifier(named.name) || ts.isStringLiteral(named.name))) { @@ -1585,18 +1585,18 @@ function lowerAssertThrows( ): IrExpr { const surface = doesNot ? "assert.doesNotThrow" : "assert.throws"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 1 || expr.arguments.length > 3) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 3) { lowerer.noLowering( - `${surface} with ${expr.arguments.length} arguments`, + `${surface} with ${expr.arguments.nodes.length} arguments`, expr, `the supported forms are ${surface}(fn), ${surface}(fn, expected), and ${surface}(fn, expected, message)`, ); } - const fn = lowerer.lowerExpr(expr.arguments[0]!); + const fn = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (fn.type.kind !== "func" || fn.type.params.length !== 0) { lowerer.noLowering( `${surface} with this callback shape`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the callback must be a zero-parameter function", ); } @@ -1637,14 +1637,14 @@ function lowerAssertRejects( doesNot: boolean, ): IrExpr { const surface = doesNot ? "assert.doesNotReject" : "assert.rejects"; - if (expr.arguments.length < 1 || expr.arguments.length > 3) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 3) { lowerer.noLowering( - `${surface} with ${expr.arguments.length} arguments`, + `${surface} with ${expr.arguments.nodes.length} arguments`, expr, `the supported forms are ${doesNot ? "doesNotReject" : "rejects"}(fn), (fn, expected), and (fn, expected, message)`, ); } - const recv = lowerer.lowerExpr(expr.arguments[0]!); + const recv = lowerer.lowerExpr(expr.arguments.nodes[0]!); const recvIsPromise = recv.type.kind === "promise"; if ( !recvIsPromise && @@ -1652,7 +1652,7 @@ function lowerAssertRejects( ) { lowerer.noLowering( `${surface} with this first argument`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "an async (promise-returning) zero-parameter function or a promise are the lowered forms " + "(a bare thenable — an object with a then method — has no lowering: wrap it, e.g. Promise.resolve().then(() => thenable))", ); @@ -1728,7 +1728,7 @@ function refuseNoErrorShape( // statement of the same rule. lowerer.noLowering( `${surface} with an object-shape expectation`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, "Node rejects this form when the callback fails — use a class, a regex, a validation callback, or a bare call", ); } @@ -1743,7 +1743,7 @@ function throwsArguments( expr: ts.CallExpression, loc: SrcLoc, ): { expected: ThrowsExpected; msg: IrExpr; hasMsg: IrExpr } { - const expectedNode = expr.arguments[1]; + const expectedNode = expr.arguments.nodes[1]; // A literal `undefined` expected counts as omitted (Node reads the // message from the THIRD argument in that spelling). const expected: ThrowsExpected = @@ -1752,20 +1752,20 @@ function throwsArguments( : { form: "bare" }; if (expected.form === "message") { if ( - expr.arguments.length === 3 && + expr.arguments.nodes.length === 3 && surface !== "assert.doesNotThrow" && surface !== "assert.doesNotReject" ) { lowerer.noLowering( `${surface} with a string expected argument AND a message`, - expr.arguments[2]!, + expr.arguments.nodes[2]!, "Node rejects this form (the string IS the message) — use an expected shape second", ); } - if (expr.arguments[2]) { + if (expr.arguments.nodes[2]) { // No-error assertions ignore the third value in the string-second // form, while ordinary argument evaluation still runs it once. - const ignored = lowerer.lowerExpr(expr.arguments[2]); + const ignored = lowerer.lowerExpr(expr.arguments.nodes[2]); const message = lowerer.declareHiddenLocal("%assertNoErrorMessage", STRING); return { expected, @@ -1784,7 +1784,7 @@ function throwsArguments( } return { expected, msg: expected.msg, hasMsg: expected.hasMsg }; } - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); return { expected, msg, hasMsg }; } @@ -2218,14 +2218,14 @@ function ifErrorLibFn( * an interned helper. Anything else fences. */ function lowerAssertIfError(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, "assert.ifError"); - if (expr.arguments.length !== 1) { + if (expr.arguments.nodes.length !== 1) { lowerer.noLowering( - `assert.ifError with ${expr.arguments.length} arguments`, + `assert.ifError with ${expr.arguments.nodes.length} arguments`, expr, "the supported form is ifError(value)", ); } - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (argT && isUnitType(argT)) { // null/undefined can never throw, and a unit-typed expression has no diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index 1bd074dd7..c20f32480 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -81,10 +81,10 @@ function literalBooleanOption( fallback: boolean, api: string, ): boolean { - if (call.arguments.length === 0) return fallback; - const argumentNode = call.arguments[0]; + if (call.arguments.nodes.length === 0) return fallback; + const argumentNode = call.arguments.nodes[0]; if ( - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || argumentNode === undefined || ts.isSpreadElement(argumentNode) ) { @@ -101,7 +101,7 @@ function literalBooleanOption( return fallback; } let answer = fallback; - for (const member of argument.properties) { + for (const member of argument.properties.nodes) { const name = propertyName(member); if (name !== option) { const value = ts.isPropertyAssignment(member) ? member.initializer : null; @@ -305,7 +305,7 @@ export function checkForAwaitBinding( if (!isConst && !isLet) { lowerer.unsupported("SC1030", list, "'var' loop bindings in 'for await' (use const)"); } - const declaration = list.declarations[0]; + const declaration = list.declarations.nodes[0]; if (!declaration) throw new InternalCompilerError("for-await declaration list has no binding"); if (!ts.isIdentifier(declaration.name)) lowerer.unsupported("SC1031", declaration.name); return { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 5c1b47e6a..67ca01b3f 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -248,7 +248,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "Object" && lowerer.isStdlibGlobal(expr.expression, "Object") ) { - const object = lowerObjectConstructor(lowerer, expr.arguments, loc); + const object = lowerObjectConstructor(lowerer, expr.arguments.nodes, loc); if (object) return object; } @@ -537,7 +537,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "queueMicrotask" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { // Node: queueMicrotask() throws ERR_INVALID_ARG_TYPE at runtime // (the undefined callback) — the Dyn form delivers exactly that. return { @@ -548,7 +548,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { loc, }; } - const raw = expr.arguments[0]!; + const raw = expr.arguments.nodes[0]!; const cb = lowerer.lowerExpr(raw); if (cb.type.kind === "dyn") { return { kind: "libCall", fn: "timers.queueMicrotaskDyn", args: [cb], type: VOID, loc }; @@ -588,11 +588,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "structuredClone" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { return { kind: "libCall", fn: "dyn.cloneMissing", args: [], type: DYN, loc }; } - if (expr.arguments.length > 2) { - lowerer.noLowering(`structuredClone with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 2) { + lowerer.noLowering(`structuredClone with ${expr.arguments.nodes.length} arguments`, expr); } const toDynArg = (a: ts.Expression | undefined): IrExpr => { if (!a) return dynUndefinedExpr(loc); @@ -607,16 +607,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } return conv; }; - const valueNode = expr.arguments[0]!; + const valueNode = expr.arguments.nodes[0]!; // A NON-EMPTY transfer array of static values: nothing static is // transferable, so the call is Node's DataCloneError — decided here // (the list's values need no dyn representation to fail). An EMPTY // literal transfer list is a no-op member and drops. { - let optNode = expr.arguments[1]; + let optNode = expr.arguments.nodes[1]; while (optNode && ts.isParenthesizedExpression(optNode)) optNode = optNode.expression; if (optNode && ts.isObjectLiteralExpression(optNode)) { - const tr = optNode.properties.find( + const tr = optNode.properties.nodes.find( (p): p is ts.PropertyAssignment => ts.isPropertyAssignment(p) && p.name !== undefined && @@ -626,11 +626,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if ( tr && ts.isArrayLiteralExpression(tr.initializer) && - tr.initializer.elements.length > 0 + tr.initializer.elements.nodes.length > 0 ) { // An ArrayBuffer first member may be a Buffer pool slab, which // Node reports as untransferable. - const first = tr.initializer.elements[0]!; + const first = tr.initializer.elements.nodes[0]!; const firstType = ts.isSpreadElement(first) ? undefined : lowerer.typeOf(first); const parts = !firstType ? [] @@ -656,7 +656,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } } } - const optsArg = toDynArg(expr.arguments[1]); + const optsArg = toDynArg(expr.arguments.nodes[1]); // A DOMException value clones through its own runtime arm — the // typed result keeps the class (instanceof, .code, throwability). const valueT = lowerer.mapTypeOf(lowerer.typeOf(valueNode)); @@ -719,10 +719,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // constants ("", false, 0). Wrapper objects stay fenced except where // the receiver is immediately converted by a direct String method call. if (lowerer.isStdlibGlobal(expr.expression, "BigInt")) { - if (expr.arguments.length !== 1) { - lowerer.noLowering(`BigInt with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`BigInt with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; // Captured optional storage can change after TypeScript's guard. // BigInt consumes the stored value, including its nullish tag. const arg = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode); @@ -745,10 +745,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { : lowerer.isStdlibGlobal(expr.expression, "Boolean") ? "Boolean" : "Number"; - if (expr.arguments.length > 1) { - lowerer.noLowering(`${name} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 1) { + lowerer.noLowering(`${name} with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]; + const argNode = expr.arguments.nodes[0]; if (!argNode) { if (name === "String") return { kind: "strLit", value: "", type: STRING, loc }; if (name === "Boolean") return { kind: "boolLit", value: false, type: BOOL, loc }; @@ -815,10 +815,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const receiver = varRef(local.id, DYN, loc); const key = lowerer.lowerExprExpecting(access.argumentExpression, DYN); const callee: IrExpr = { kind: "dynKeyGet", value: receiver, key, type: DYN, loc }; - const args = expr.arguments.map((arg) => + const args = expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(ts.isSpreadElement(arg) ? arg.expression : arg, DYN), ); - const spreads = expr.arguments.flatMap((arg, index) => + const spreads = expr.arguments.nodes.flatMap((arg, index) => ts.isSpreadElement(arg) ? [{ arg: index, what: arg.expression.getText() }] : [], ); return { @@ -852,7 +852,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // all (fs.watch, os.cpus, ...) fence with the module-qualified name. if (ts.isIdentifier(expr.expression)) { if (expr.expression.text === "Array" && lowerer.isStdlibGlobal(expr.expression, "Array")) { - return lowerArrayConstructor(lowerer, expr, expr.arguments); + return lowerArrayConstructor(lowerer, expr, expr.arguments.nodes); } // A call through a compile-time util.promisify projection. execFile // keeps its custom result helper; ordinary builtins route through @@ -900,9 +900,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (threadsImportOf(lowerer.program, expr.expression) === "publish") { // @scriptc/threads publish(value): the same value, its graph made // immortal and immutable (backend/llvm/publish.ts). - if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) - lowerer.noLowering(`publish with ${expr.arguments.length} arguments`, expr); - const value = lowerer.lowerExpr(expr.arguments[0]!); + if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!)) + lowerer.noLowering(`publish with ${expr.arguments.nodes.length} arguments`, expr); + const value = lowerer.lowerExpr(expr.arguments.nodes[0]!); return { kind: "intrinsic", name: "threads.publish", args: [value], type: value.type, loc }; } const bi = lowerer.builtinImportOf(expr.expression); @@ -933,10 +933,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "Symbol" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { - if (expr.arguments.length > 1) { - lowerer.noLowering(`Symbol with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 1) { + lowerer.noLowering(`Symbol with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]; + const argNode = expr.arguments.nodes[0]; // A literal `undefined` argument IS the no-description form // (Symbol(undefined).description is undefined, like Symbol()). if (!argNode || (ts.isIdentifier(argNode) && argNode.text === "undefined")) { @@ -973,7 +973,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer, expr.expression.text, expr, - expr.arguments, + expr.arguments.nodes, loc, ); if (parsed !== null) return parsed; @@ -996,14 +996,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { const name = expr.expression.text; - if (expr.arguments.length !== 1) { + if (expr.arguments.nodes.length !== 1) { lowerer.noLowering( - `${name} with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `${name} with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, ); } const loc = locOf(expr); - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; if (name === "decodeURIComponent" || name === "decodeURI") { const d = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode); const s = lowerer.ensureString(d, argNode); @@ -1038,13 +1038,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { const name = expr.expression.text; - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { return { kind: "libCall", fn: "str.b64Missing", args: [], type: STRING, loc }; } - if (expr.arguments.length > 1) { - lowerer.noLowering(`${name} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 1) { + lowerer.noLowering(`${name} with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; const v = lowerer.lowerExpr(argNode); let data: IrExpr; if (v.type.kind === "dyn") { @@ -1071,20 +1071,20 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // result exits to the declared static type. A user function // shadowing the name has a different, non-stdlib symbol. const islFn = lowerer.stdlibGlobalFnOf(expr.expression); - if (islFn && expr.arguments.length !== islFn.args.length) { + if (islFn && expr.arguments.nodes.length !== islFn.args.length) { const name = expr.expression.text; lowerer.noLowering( - `${name} with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `${name} with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, ); } - if (islFn && expr.arguments.length === islFn.args.length) { + if (islFn && expr.arguments.nodes.length === islFn.args.length) { // The static forms above already took the exactly-typed argument // (a string for parseFloat, a number for isFinite): name the // argument type that refused the call, so the message states the // condition rather than reading as if the global itself were // unsupported. - const argText = loweredArgumentText(lowerer, expr.arguments[0]!); + const argText = loweredArgumentText(lowerer, expr.arguments.nodes[0]!); const exact = expr.expression.text === "isFinite" ? "number" : "string"; lowerer.refuseUnsupportedApi( `'${expr.expression.text}' with a '${argText}' argument`, @@ -1104,8 +1104,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // typeof tests) applies inside it, tsc's call-site narrowing types // the guarded branch, and a body construct with no caught lowering // fences per site at its own location. - if (ts.isIdentifier(expr.expression) && expr.arguments.length === 1) { - const caughtArg = lowerer.caughtLocalOf(expr.arguments[0]!); + if (ts.isIdentifier(expr.expression) && expr.arguments.nodes.length === 1) { + const caughtArg = lowerer.caughtLocalOf(expr.arguments.nodes[0]!); if (caughtArg) { const inlined = lowerCaughtPredicateCall(lowerer, expr, caughtArg); if (inlined) return inlined; @@ -1134,7 +1134,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // the declaration's value and applies through a runtime-built // argument list. Typed .ts spreads keep completeArgs' rest packing. const jsSpreadArgs = - expr.arguments.some((a) => ts.isSpreadElement(a)) && isJsSourceFile(expr.getSourceFile()); + expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && + isJsSourceFile(expr.getSourceFile()); if (lowerer.isTopLevelFnSymbol(expr.expression) && !lowerer.peekLocal(expr.expression)) { // `import g = N.f; g()` — the alias's own source-order guards // (a no-op for every non-import= binding). @@ -1146,9 +1147,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (generic && !(jsSpreadArgs && generic.implicitParams)) return lowerer.lowerGenericCall(expr, generic); const sig = generic ? null : lowerer.fnSigOf(expr.expression); - if (sig && !(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments))) { + if ( + sig && + !(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments.nodes)) + ) { lowerer.noteEdge(sig.name); - const args = lowerer.completeArgs(expr.arguments, sig.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); const symbol = lowerer.resolveValueSymbol(expr.expression); const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; // A bare call supplies undefined as this, even inside a callback @@ -1157,7 +1161,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if ( declaration && ts.isFunctionDeclaration(declaration) && - declaration.parameters.some(isThisParameter) + declaration.parameters.nodes.some(isThisParameter) ) { const callee = lowerer.lowerExpr(expr.expression); return reconcileOverloadReturn(lowerer, expr, { @@ -1250,7 +1254,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const sig = lowerer.fnSigOf(nsMember); if (sig) { lowerer.noteEdge(sig.name); - const args = lowerer.completeArgs(expr.arguments, sig.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); return reconcileOverloadReturn(lowerer, expr, { kind: "call", callee: sig.name, @@ -1313,7 +1317,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const sig = lowerer.fnSigOf(nameId); if (sig) { lowerer.noteEdge(sig.name); - const args = lowerer.completeArgs(expr.arguments, sig.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); return reconcileOverloadReturn(lowerer, expr, { kind: "call", callee: sig.name, @@ -1532,13 +1536,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (intrinsic) return intrinsic; if (isJsSourceFile(expr.getSourceFile())) { const staticField = lowerStaticFieldRead(lowerer, expr.expression); - if (staticField?.type.kind === "dyn" && !expr.arguments.some(ts.isSpreadElement)) { + if (staticField?.type.kind === "dyn" && !expr.arguments.nodes.some(ts.isSpreadElement)) { return { kind: "dynCall", callee: staticField, receiver: lowerer.lowerExprExpecting(expr.expression.expression, DYN), calleeName: v8CalleeText(expr.expression), - args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(expr), }; @@ -1551,8 +1555,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr)); return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) }; } - if (callee.type.kind === "dyn" && !expr.arguments.some((arg) => ts.isSpreadElement(arg))) { - const args = expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + if ( + callee.type.kind === "dyn" && + !expr.arguments.nodes.some((arg) => ts.isSpreadElement(arg)) + ) { + const args = expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "dynCall", callee, @@ -1628,7 +1635,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ts.isElementAccessExpression(expr.expression) && !expr.expression.questionDotToken && !expr.questionDotToken && - expr.arguments.length === 0 + expr.arguments.nodes.length === 0 ) { const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression); if (iterator) return iterator; @@ -1680,7 +1687,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const key = raw.type.kind === "string" ? raw : lowerer.coerceInto(access.argumentExpression, raw, DYN); const callee: IrExpr = { kind: "dynKeyGet", value: receiver, key, type: DYN, loc }; - const spread = expr.arguments.some(ts.isSpreadElement) + const spread = expr.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, expr, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -1693,7 +1700,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { callee, receiver, calleeName: access.getText(), - args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -1739,7 +1746,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (virtual) lowerer.noteVirtualEdge(info, memberName); else lowerer.noteEdge(`%${found.declarer.def.name}.${memberName}`); const receiver = lowerer.lowerExpr(expr.expression.expression); - const args = lowerer.completeArgs(expr.arguments, found.sig.params, locOf(expr), expr); + const args = lowerer.completeArgs( + expr.arguments.nodes, + found.sig.params, + locOf(expr), + expr, + ); const callArgs = [ lowerer.upcastTo(receiver, virtual ? info.def.name : found.declarer.def.name), ...args, @@ -1794,7 +1806,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // function" TypeError. The result is dyn (checked per use like every // any-origin value). Spread arguments keep their fence. if (callee.type.kind === "dyn") { - if (expr.arguments.some((a) => ts.isSpreadElement(a))) { + if (expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) { // The runtime-arity lane: a dyn callee is already boxed — the // spread-marked dynCall applies through a fresh dyn argument // array (lowerSpreadArgsCall). Sources outside it keep the fence. @@ -1802,7 +1814,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (spreadServed) return spreadServed; lowerer.unsupported("SC1090", expr, "spread arguments in calls through 'unknown' values"); } - const args = expr.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = expr.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression) @@ -1821,7 +1833,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { kind: "dynCall", callee: lowerer.coerceToExpected(callee, DYN), calleeName: v8CalleeText(expr.expression), - args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -1842,7 +1854,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // or marshals the callee and applies through a runtime-built argument // list (lowerSpreadArgsCall). Shapes outside its lanes fall through // to the historical fences. - if (expr.arguments.some((a) => ts.isSpreadElement(a))) { + if (expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) { const spreadServed = lowerSpreadArgsCall(lowerer, expr, callee, loc); if (spreadServed) return spreadServed; } @@ -1854,17 +1866,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // JS arity exactly (the thunk ignores extras). Result dyn, checked // per use like every any-origin value. if ( - (expr.arguments.length > callee.type.params.length || callee.type.rest === true) && + (expr.arguments.nodes.length > callee.type.params.length || callee.type.rest === true) && (isJsSourceFile(expr.getSourceFile()) || (callee.type.rest === true && callee.type.restAbi === undefined)) && - !expr.arguments.some((a) => ts.isSpreadElement(a)) && + !expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && canBoxFuncIntoDyn( callee.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id), ) ) { - const args = expr.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = expr.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); const calleeName = ts.isIdentifier(expr.expression) ? expr.expression.text : "value"; const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc }; return { kind: "dynCall", callee: boxed, calleeName, args, type: DYN, loc }; @@ -1887,14 +1899,15 @@ function lowerCheckedCallableValue( callee.type.kind !== "func" || !( (callee.type.rest === true && callee.type.restAbi === undefined) || - (isJsSourceFile(call.getSourceFile()) && call.arguments.length > callee.type.params.length) + (isJsSourceFile(call.getSourceFile()) && + call.arguments.nodes.length > callee.type.params.length) ) || !lowerer.dynConvertible(callee.type) ) return null; callee = { kind: "dynFrom", value: callee, type: DYN, loc }; } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { const spread = lowerSpreadArgsCall(lowerer, call, callee, loc); if (spread) return spread; lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); @@ -1903,7 +1916,7 @@ function lowerCheckedCallableValue( kind: "dynCall", callee, calleeName: v8CalleeText(call.expression), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -1918,18 +1931,19 @@ function lowerRegexMethodCallWithOptionalArg( if ( lowered?.kind !== "regexIntrinsic" || lowered.method !== "test" || - call.arguments.length !== 1 + call.arguments.nodes.length !== 1 ) return lowered; const subject = lowered.args[0]; if (subject?.type.kind !== "union" || lowerer.armTag(subject.type.unionId, UNDEFINED_T) < 0) return lowered; - if (lowerer.mapTypeOf(lowerer.typeOf(call.arguments[0]!))?.kind !== "string") return lowered; + if (lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[0]!))?.kind !== "string") + return lowered; const present = lowerer.stripUndefinedArm(subject.type); if (present.kind !== "string") return lowered; return { ...lowered, - args: [lowerer.ensureString(subject, call.arguments[0]!)], + args: [lowerer.ensureString(subject, call.arguments.nodes[0]!)], }; } @@ -1952,7 +1966,7 @@ function lowerBufferStaticCallWithNarrowedArg( // A callback can store a wider union than the checker sees at this use // (including an added undefined arm). Extract the exact arm proven by // control-flow narrowing, not merely the union with undefined removed. - const narrowed = lowerer.mapTypeOf(lowerer.typeOf(call.arguments[0]!)); + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[0]!)); if ( !narrowed || !( @@ -1964,7 +1978,7 @@ function lowerBufferStaticCallWithNarrowedArg( ) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "a Buffer constructor argument whose narrowed type is not a stored source arm", ); } @@ -1976,7 +1990,7 @@ function lowerBufferStaticCallWithNarrowedArg( if (!helper) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "a Buffer constructor argument whose narrowed type is not a stored source arm", ); } @@ -2049,7 +2063,7 @@ function lowerStringMethodCallWithOptionalArgs( call: ts.CallExpression, access: ts.PropertyAccessExpression, receiver?: () => IrExpr, - argumentNodes: readonly ts.Expression[] = call.arguments, + argumentNodes: readonly ts.Expression[] = call.arguments.nodes, ): IrExpr | null { const lowered = lowerStringMethodCall(lowerer, call, access, receiver, argumentNodes); if (lowered?.kind !== "strIntrinsic") return lowered; @@ -2275,7 +2289,7 @@ function immediatePrimitiveWrapperToString(lowerer: Lowerer, node: ts.Expression !lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(wrapped.expression) ?? undefined) ) return null; - const args = wrapped.arguments ?? []; + const args = wrapped.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) return null; const loc = locOf(wrapped); const primitive: IrExpr = @@ -2346,7 +2360,7 @@ function immediateObjectTagReceiver( (ts.isCallExpression(value) || ts.isNewExpression(value)) && ts.isIdentifier(value.expression) && (value.expression.text === "Array" || value.expression.text === "Error") && - (value.arguments?.length ?? 0) === 0 && + (value.arguments?.nodes.length ?? 0) === 0 && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(value.expression) ?? undefined) ) { return { tag: value.expression.text, effect: { kind: "numLit", value: 0, type: F64, loc } }; @@ -2357,7 +2371,7 @@ function immediateObjectTagReceiver( lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(value.expression) ?? undefined) ) { const name = value.expression.text; - const args = value.arguments ?? []; + const args = value.arguments?.nodes ?? []; // JavaScript dates use checked native handles, so their IR type alone // cannot recover the tag through an immediate Object(...) wrapper. if (name === "Date") return { tag: "Date", effect: lowerer.lowerExpr(value) }; @@ -2388,7 +2402,7 @@ function immediateObjectTagReceiver( value.expression.text === "Object" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(value.expression) ?? undefined) ) { - const args = value.arguments ?? []; + const args = value.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const first = args[0]; const inner = @@ -2455,7 +2469,7 @@ function lowerArrayPrototypeBorrowCall( access.questionDotToken || access.name.text !== "call" || !ts.isPropertyAccessExpression(access.expression) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; const method = access.expression; @@ -2470,10 +2484,10 @@ function lowerArrayPrototypeBorrowCall( ) return null; const loc = locOf(call); - const receiver = call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0]!, DYN) + const receiver = call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN) : (dynUndefinedExpr(loc) as IrExpr); - const args = call.arguments.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "libCall", fn: "dyn.arrayProtoCall", @@ -2502,15 +2516,15 @@ function lowerObjectOwnPrototypeCall( prototype.name.text !== "prototype" || !lowerer.isStdlibGlobal(prototype.expression, "Object") || !lowerer.isStdlibMember(method) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; return lowerObjectOwnCall( lowerer, call, - call.arguments[0], - call.arguments[1], - call.arguments.slice(2), + call.arguments.nodes[0], + call.arguments.nodes[1], + call.arguments.nodes.slice(2), method.name.text, ); } @@ -2525,7 +2539,7 @@ function lowerObjectOwnMethodCall( call.questionDotToken || access.questionDotToken || (method !== "hasOwnProperty" && method !== "propertyIsEnumerable") || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; if (!lowerer.isStdlibMember(access)) { @@ -2536,8 +2550,8 @@ function lowerObjectOwnMethodCall( lowerer, call, access.expression, - call.arguments[0], - call.arguments.slice(1), + call.arguments.nodes[0], + call.arguments.nodes.slice(1), method, access, ); @@ -2791,13 +2805,13 @@ function lowerObjectPrototypeCall( prototype.name.text !== "prototype" || !lowerer.isStdlibGlobal(prototype.expression, "Object") || !lowerer.isStdlibMember(method) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; const loc = locOf(call); - if (call.arguments.length === 0) + if (call.arguments.nodes.length === 0) return { kind: "strLit", value: "[object Undefined]", type: STRING, loc }; - const receiverNode = call.arguments[0]!; + const receiverNode = call.arguments.nodes[0]!; const immediate = immediateObjectTagReceiver(lowerer, receiverNode); const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode); const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type); @@ -2817,7 +2831,7 @@ function lowerObjectPrototypeCall( stmts.push({ kind: "varDecl", localId: local.id, init: receiver, loc }); ref = varRef(local.id, receiver.type, loc); } - for (const extra of call.arguments.slice(1)) { + for (const extra of call.arguments.nodes.slice(1)) { stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(extra), loc: locOf(extra) }); } const result: IrExpr = @@ -2863,7 +2877,7 @@ function lowerDynamicStringPrototypeCall( }; const overrides: { node: ts.Expression; previous: IrExpr | undefined }[] = []; try { - for (const node of call.arguments.slice(1)) { + for (const node of call.arguments.nodes.slice(1)) { const undefinedValue = lowerStaticallyUndefinedArgument(lowerer, node); const value = undefinedValue ?? lowerer.lowerExpr(node); let ref: IrExpr; @@ -2889,7 +2903,7 @@ function lowerDynamicStringPrototypeCall( call, methodAccess, () => converted, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); return result ? { kind: "seqExpr", stmts, result, type: result.type, loc } : null; } finally { @@ -2918,7 +2932,7 @@ function lowerStringPrototypeCall( !lowerer.isStdlibMember(methodAccess) ) return null; - if (call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; const entry = STR_METHODS[methodAccess.name.text]; const indexMethod = STRING_INDEX_METHODS.has(methodAccess.name.text) ? (methodAccess.name.text as "at" | "codePointAt") @@ -2941,12 +2955,12 @@ function lowerStringPrototypeCall( resultType, loc, ); - const receiverNode = call.arguments[0]; + const receiverNode = call.arguments.nodes[0]; if (!receiverNode) return nullishError; // Receiver coercion runs after .call has evaluated its arguments; keep // contextual wrapper lowering on forms without later method arguments. const immediateWrapper = - entry?.maxArgs === 0 && call.arguments.length === 1 + entry?.maxArgs === 0 && call.arguments.nodes.length === 1 ? immediatePrimitiveWrapperToString(lowerer, receiverNode) : null; const receiverValue = immediateWrapper ?? lowerer.lowerExpr(receiverNode); @@ -2960,7 +2974,10 @@ function lowerStringPrototypeCall( ?.arms.every((arm) => isUnitType(arm) || arm.kind === "void") ?? false)); if (nullish) { - const values = [receiverValue, ...call.arguments.slice(1).map((arg) => lowerer.lowerExpr(arg))]; + const values = [ + receiverValue, + ...call.arguments.nodes.slice(1).map((arg) => lowerer.lowerExpr(arg)), + ]; return { kind: "seqExpr", stmts: values @@ -2995,7 +3012,7 @@ function lowerStringPrototypeCall( // padding uses a helper that delays conversion until every argument is ready. const objectWithoutMethodArgs = entry?.maxArgs === 0 && - call.arguments.length === 1 && + call.arguments.nodes.length === 1 && (receiverType.kind === "record" || receiverType.kind === "array" || receiverType.kind === "object"); @@ -3006,7 +3023,7 @@ function lowerStringPrototypeCall( entry.method as "padStart" | "padEnd", lowerer.coerceToExpected(receiverValue, DYN), receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } if (!scalar && !objectWithoutMethodArgs) { @@ -3022,7 +3039,7 @@ function lowerStringPrototypeCall( indexMethod, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); if (!entry) return null; if (padding) { @@ -3032,7 +3049,7 @@ function lowerStringPrototypeCall( entry.method as "padStart" | "padEnd", receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } if (entry.method === "split") { @@ -3041,7 +3058,7 @@ function lowerStringPrototypeCall( call, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } const receiver = lowerer.ensureString(receiverValue, receiverNode); @@ -3050,7 +3067,7 @@ function lowerStringPrototypeCall( call, methodAccess, () => receiver, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } @@ -3325,7 +3342,7 @@ function lowerDynReceiverMethodCall( type: DYN, loc, }; - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments in callable property calls"); } return { @@ -3336,7 +3353,7 @@ function lowerDynReceiverMethodCall( callee: member, receiver, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }, @@ -3407,11 +3424,11 @@ function lowerDynReceiverMethodCall( if (recv.type.kind !== "dyn") return null; if ( access.name.text === "normalize" && - call.arguments.length <= 1 && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length <= 1 && + !call.arguments.nodes.some(ts.isSpreadElement) ) { const loc = locOf(call); - const hasArgument = call.arguments.length === 1; + const hasArgument = call.arguments.nodes.length === 1; const name = `%dyn.string.normalize:${hasArgument ? 1 : 0}`; if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const value = varRef("receiver", DYN, loc); @@ -3475,8 +3492,8 @@ function lowerDynReceiverMethodCall( callee: name, args: [ recv, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), ], type: DYN, @@ -3525,8 +3542,8 @@ function lowerDynReceiverMethodCall( } // The argument is a radix for bigint and an encoding for Buffer. // Preserve its runtime value so receiver dispatch can choose correctly. - if (access.name.text === "toString" && call.arguments.length <= 1) { - const argument = call.arguments[0]; + if (access.name.text === "toString" && call.arguments.nodes.length <= 1) { + const argument = call.arguments.nodes[0]; return { kind: "libCall", fn: "dyn.toString", @@ -3542,7 +3559,7 @@ function lowerDynReceiverMethodCall( // Every dyn kind's prototype chain declares valueOf, and the runtime // answers each kind's own: primitives and plain objects themselves, a // Date its time value, an own or inherited valueOf member its result. - if (access.name.text === "valueOf" && call.arguments.length === 0) { + if (access.name.text === "valueOf" && call.arguments.nodes.length === 0) { return { kind: "libCall", fn: "dyn.valueOf", @@ -3658,13 +3675,13 @@ function lowerDynReceiverMethodCall( { kind: "varDecl", localId: receiverLocal.id, init: recv, loc }, { kind: "varDecl", localId: calleeLocal.id, init: stringRefusal(member), loc }, ]; - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { const spread = lowerSpreadArgsCall(lowerer, call, callee, loc); if (spread?.kind === "dynCall") return { kind: "seqExpr", stmts, result: { ...spread, receiver }, type: DYN, loc }; lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } - const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); return { kind: "seqExpr", stmts, @@ -3966,7 +3983,7 @@ export function lowerDynDispatchMethodCall( lowerer.chainBlocked(call, access) ) return null; - if (call.arguments.some((arg) => ts.isSpreadElement(arg))) { + if (call.arguments.nodes.some((arg) => ts.isSpreadElement(arg))) { const loc = locOf(call); const local = lowerer.declareHiddenLocal("%spreadReceiver", DYN); const receiver = varRef(local.id, DYN, loc); @@ -3996,11 +4013,13 @@ export function lowerDynDispatchMethodCall( lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } const predicate = - method === "filter" && call.arguments[0] ? lowerer.lowerExpr(call.arguments[0]) : null; + method === "filter" && call.arguments.nodes[0] + ? lowerer.lowerExpr(call.arguments.nodes[0]) + : null; if (arrayReceiver && predicate?.type.kind === "func" && predicate.type.ret.kind === "void") { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "'.filter()' with a void-returning predicate (the callback return value is erased before its truthiness can be tested)", ); } @@ -4012,15 +4031,15 @@ export function lowerDynDispatchMethodCall( // at the callback boundary; valid callers preserve the original prefix. if ( method === "push" && - call.arguments.length === 1 && + call.arguments.nodes.length === 1 && ts.isIdentifier(access.expression) && ts.isExpressionStatement(call.parent) && ts.isBlock(call.parent.parent) ) { const receiverSymbol = lowerer.checker.getSymbolAtLocation(access.expression); const block = call.parent.parent; - const statementIndex = block.statements.indexOf(call.parent); - const next = statementIndex >= 0 ? block.statements[statementIndex + 1] : undefined; + const statementIndex = block.statements.nodes.indexOf(call.parent); + const next = statementIndex >= 0 ? block.statements.nodes[statementIndex + 1] : undefined; let returned = next && ts.isReturnStatement(next) ? next.expression : undefined; while (returned && ts.isParenthesizedExpression(returned)) returned = returned.expression; const apply = @@ -4030,7 +4049,7 @@ export function lowerDynDispatchMethodCall( returned.expression.name.text === "apply" ? returned : null; - const argsNode = apply?.arguments[1]; + const argsNode = apply?.arguments?.nodes[1]; const sameArgs = receiverSymbol !== undefined && argsNode !== undefined && @@ -4056,7 +4075,7 @@ export function lowerDynDispatchMethodCall( ), ) ) { - const value = lowerer.lowerExpr(call.arguments[0]!); + const value = lowerer.lowerExpr(call.arguments.nodes[0]!); if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) { const saved = lowerer.declareHiddenLocal("%applyTail", DYN); return { @@ -4070,7 +4089,7 @@ export function lowerDynDispatchMethodCall( recv: { kind: "varRef", localId: saved.id, type: DYN, loc: locOf(access.expression) }, method, calleeName: access.getText(), - args: [dynUndefinedExpr(locOf(call.arguments[0]!))], + args: [dynUndefinedExpr(locOf(call.arguments.nodes[0]!))], type: DYN, loc: locOf(call), }, @@ -4080,7 +4099,7 @@ export function lowerDynDispatchMethodCall( } } } - const args = call.arguments.map((arg, i) => { + const args = call.arguments.nodes.map((arg, i) => { if (i === 0 && predicate) return lowerer.coerceInto(arg, predicate, DYN); const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, arg); return undefinedArg @@ -4136,8 +4155,8 @@ function lowerArrayIsArrayCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Array") !== "isArray") return null; - if (call.arguments.length !== 1) return null; // the stdlib chokepoint fences - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; // the stdlib chokepoint fences + const argNode = call.arguments.nodes[0]!; const arg = lowerer.lowerExpr(argNode); const loc = locOf(call); if (arg.type.kind === "dyn") { @@ -4225,12 +4244,12 @@ function lowerCaughtPredicateCall( : undefined; if (!symbol || !decl || !decl.body) return null; if (!decl.type || !ts.isTypePredicateNode(decl.type)) return null; - if (decl.parameters.length !== 1) return null; - const param = decl.parameters[0]!; + if (decl.parameters.nodes.length !== 1) return null; + const param = decl.parameters.nodes[0]!; if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return null; const paramSymbol = lowerer.checker.getSymbolAtLocation(param.name); if (!paramSymbol) return null; - const ret = decl.body.statements.length === 1 ? decl.body.statements[0] : undefined; + const ret = decl.body.statements.nodes.length === 1 ? decl.body.statements.nodes[0] : undefined; if (!ret || !ts.isReturnStatement(ret) || !ret.expression) { lowerer.unsupported( "SC1090", @@ -4272,8 +4291,8 @@ function lowerNumberToStringCall( if (lowerer.chainBlocked(call, access)) return null; if ( access.name.text !== "toString" || - call.arguments.length > 1 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 1 || + call.arguments.nodes.some(ts.isSpreadElement) ) return null; const recvKind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind; @@ -4289,8 +4308,8 @@ function lowerNumberToStringCall( fn: "dyn.toString", args: [ operand, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(locOf(call)), { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }, ], @@ -4298,9 +4317,9 @@ function lowerNumberToStringCall( loc: locOf(call), }; } - if (call.arguments.length === 1) { + if (call.arguments.nodes.length === 1) { if (operand.type.kind !== "f64") return null; - const arg = call.arguments[0]!; + const arg = call.arguments.nodes[0]!; // A number radix needs no box: ToIntegerOrInfinity of a number is a // truncation, which the typed entry point applies itself. if (!ts.isSpreadElement(arg) && lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "f64") { @@ -4349,7 +4368,7 @@ function lowerUnionToStringCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "toString" || call.arguments.length !== 0) return null; + if (access.name.text !== "toString" || call.arguments.nodes.length !== 0) return null; const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (recvT?.kind !== "union") return null; if (!lowerer.isStdlibMember(access)) return null; @@ -4389,7 +4408,7 @@ function lowerDefaultToStringCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "toString" || call.arguments.length !== 0) return null; + if (access.name.text !== "toString" || call.arguments.nodes.length !== 0) return null; if (!lowerer.isStdlibMember(access)) return null; const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (!recvT) return null; @@ -4458,7 +4477,7 @@ function pureObjectToStringReceiver(node: ts.Expression): boolean { ) { e = e.expression; } - if (ts.isObjectLiteralExpression(e) && e.properties.length === 0) return true; + if (ts.isObjectLiteralExpression(e) && e.properties.nodes.length === 0) return true; return pureReceiverNode(e); } @@ -4474,9 +4493,9 @@ function lowerClassHasOwnPropertyCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "hasOwnProperty" || call.arguments.length !== 1) return null; + if (access.name.text !== "hasOwnProperty" || call.arguments.nodes.length !== 1) return null; if (!ts.isIdentifier(access.expression)) return null; - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; if (!ts.isStringLiteralLike(argNode)) return null; if (!lowerer.isStdlibMember(access)) return null; const sym = lowerer.resolveValueSymbol(access.expression); @@ -4484,7 +4503,7 @@ function lowerClassHasOwnPropertyCall( if (!info || !info.decl) return null; // builtin/runtime classes keep the fence const key = argNode.text; const own = new Set(["prototype", "name", "length"]); - for (const m of info.decl.members) { + for (const m of info.decl.members.nodes) { const isStatic = ts.canHaveModifiers(m) && (ts.getModifiers(m) ?? []).some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword); @@ -4564,8 +4583,8 @@ function lowerPrimitiveProtoCall( const recvKind = lowerer.mapTypeOf(lowerer.typeOf(recv))?.kind; if ( (name === "toFixed" || name === "toExponential") && - call.arguments.length <= 1 && - !call.arguments.some(ts.isSpreadElement) && + call.arguments.nodes.length <= 1 && + !call.arguments.nodes.some(ts.isSpreadElement) && isJsSourceFile(call.getSourceFile()) ) { const operand = lowerer.lowerExpr(recv); @@ -4578,14 +4597,14 @@ function lowerPrimitiveProtoCall( recv: lowerer.coerceToExpected(operand, DYN), method: name, calleeName: `${recv.getText()}.${name}`, - args: call.arguments.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc: locOf(call), }; } if (recvKind !== "f64" && recvKind !== "bool" && recvKind !== "string") return null; const loc = locOf(call); - if (name === "toString" && call.arguments.length === 0) { + if (name === "toString" && call.arguments.nodes.length === 0) { const operand = lowerer.lowerExpr(recv); if (operand.type.kind === "string") return operand; // identity if (operand.type.kind !== "f64" && operand.type.kind !== "bool") return null; @@ -4594,16 +4613,16 @@ function lowerPrimitiveProtoCall( if ( (name === "toExponential" || name === "toFixed") && recvKind === "f64" && - call.arguments.length === 0 + call.arguments.nodes.length === 0 ) { const operand = lowerer.lowerExpr(recv); if (operand.type.kind !== "f64") return null; const fn = name === "toExponential" ? "num.toExponential" : "num.toFixed0"; return { kind: "libCall", fn, args: [operand], type: STRING, loc }; } - if (name === "toFixed" && recvKind === "f64" && call.arguments.length === 1) { + if (name === "toFixed" && recvKind === "f64" && call.arguments.nodes.length === 1) { const operand = lowerer.lowerExpr(recv); - let digits = lowerer.lowerExpr(call.arguments[0]!); + let digits = lowerer.lowerExpr(call.arguments.nodes[0]!); // The optional parameter also admits undefined. Exact unit values // become the default 0 after preserving any evaluation effects; an // optional number selects 0 at runtime through the same narrowed @@ -4642,7 +4661,7 @@ function lowerPrimitiveProtoCall( // host environment's default, which a compiled binary cannot carry), // other locales (ICU data the binary does not embed), options bags. if (name === "toLocaleString" && recvKind === "f64") { - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { lowerer.noLowering( "Number.prototype.toLocaleString without a locale", call, @@ -4650,7 +4669,7 @@ function lowerPrimitiveProtoCall( 'pass it explicitly: x.toLocaleString("en-US")', ); } - if (call.arguments.length > 1) { + if (call.arguments.nodes.length > 1) { lowerer.noLowering( "Number.prototype.toLocaleString with an options bag", call, @@ -4658,7 +4677,7 @@ function lowerPrimitiveProtoCall( 'digits, grouping) — x.toLocaleString("en-US")', ); } - const locNode = call.arguments[0]!; + const locNode = call.arguments.nodes[0]!; if ( ts.isSpreadElement(locNode) || !ts.isStringLiteralLike(locNode) || @@ -4680,16 +4699,16 @@ function lowerPrimitiveProtoCall( if ( name === "charAt" && recvKind === "string" && - call.arguments.length === 1 && - !ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + !ts.isSpreadElement(call.arguments.nodes[0]!) ) { const receiver = lowerer.lowerExpr(recv); if (receiver.type.kind !== "string") return null; - const idx = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const idx = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); return { kind: "strIntrinsic", method: "charAt", receiver, args: [idx], type: STRING, loc }; } - if (name !== "hasOwnProperty" || call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (name !== "hasOwnProperty" || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; if (!ts.isStringLiteralLike(argNode)) return null; const key = argNode.text; if (recvKind === "string") { @@ -4737,7 +4756,7 @@ function implicitReceiverOverloadReturn( if ( !ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression) || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.some(ts.isSpreadElement) ) { return null; } @@ -4754,12 +4773,12 @@ function implicitReceiverOverloadReturn( if (!decl || !(ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) || decl.body) { return false; } - const params = decl.parameters.filter((param) => !isThisParameter(param)); + const params = decl.parameters.nodes.filter((param) => !isThisParameter(param)); const required = params.filter( (param) => !param.questionToken && !param.initializer && !param.dotDotDotToken, ).length; const maximum = params.some((param) => param.dotDotDotToken) ? Infinity : params.length; - return expr.arguments.length >= required && expr.arguments.length <= maximum; + return expr.arguments.nodes.length >= required && expr.arguments.nodes.length <= maximum; }); if (matches.length !== 1) return null; return lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(matches[0]!)); @@ -4791,7 +4810,7 @@ function isSoleAbstractMethod(decl: ts.FunctionDeclaration | ts.MethodDeclaratio const owner = decl.parent; if (!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) return false; const name = decl.name.getText(); - return !owner.members.some( + return !owner.members.nodes.some( (member) => member !== decl && ts.isMethodDeclaration(member) && member.name.getText() === name, ); } @@ -4905,7 +4924,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp const loc = locOf(expr); const pieces = ts.isNoSubstitutionTemplateLiteral(expr.template) ? [expr.template] - : [expr.template.head, ...expr.template.templateSpans.map((s) => s.literal)]; + : [expr.template.head, ...expr.template.templateSpans.nodes.map((s) => s.literal)]; for (const p of pieces) { if (!templateEscapesValid(templateRawTextOf(p))) { lowerer.unsupported( @@ -4924,7 +4943,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp ); const values: readonly ts.Expression[] = ts.isNoSubstitutionTemplateLiteral(expr.template) ? [] - : expr.template.templateSpans.map((s) => s.expression); + : expr.template.templateSpans.nodes.map((s) => s.expression); // Direct call of a top-level declared function — the plain-call fast // path with the strings array as the leading completed argument. if (ts.isIdentifier(expr.tag) && !lowerer.isSelfReference(expr.tag)) { @@ -5167,7 +5186,7 @@ function markModuleNsHandlerParams( let e = handler; while (ts.isParenthesizedExpression(e)) e = e.expression; if (!ts.isArrowFunction(e) && !ts.isFunctionExpression(e)) return; - for (const p of e.parameters) { + for (const p of e.parameters.nodes) { if (ts.isIdentifier(p.name) && !p.dotDotDotToken && !p.initializer) { lowerer.moduleNsParamOverrides.set(p, type); } @@ -5210,15 +5229,15 @@ export function lowerPromiseMethodCall( ); }; const passthrough = - call.arguments.length === 0 || - (call.arguments.length === 1 && isAbsentHandler(call.arguments[0])); + call.arguments.nodes.length === 0 || + (call.arguments.nodes.length === 1 && isAbsentHandler(call.arguments.nodes[0])); if ( - call.arguments.length !== 1 && + call.arguments.nodes.length !== 1 && !passthrough && - !(member === "then" && call.arguments.length === 2) + !(member === "then" && call.arguments.nodes.length === 2) ) { lowerer.noLowering( - `${member} with ${call.arguments.length} arguments`, + `${member} with ${call.arguments.nodes.length} arguments`, call, member === "then" ? "the supported form takes a fulfillment handler and an optional rejection handler" @@ -5293,9 +5312,9 @@ export function lowerPromiseMethodCall( } if (member === "then") { - if (call.arguments.length === 2) { - const fulfilledNode = call.arguments[0]!; - const rejectedNode = call.arguments[1]!; + if (call.arguments.nodes.length === 2) { + const fulfilledNode = call.arguments.nodes[0]!; + const rejectedNode = call.arguments.nodes[1]!; const fulfilledAbsent = isAbsentHandler(fulfilledNode); const rejectedAbsent = isAbsentHandler(rejectedNode); const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -5542,8 +5561,9 @@ export function lowerPromiseMethodCall( // contextual type spelled (a module-namespace type has no mapping — // the handle is the value's only story, isIslandExpr's local rule). - if (inner.kind === "moduleNs") markModuleNsHandlerParams(lowerer, call.arguments[0]!, inner); - let cb = lowerer.lowerExpr(call.arguments[0]!); + if (inner.kind === "moduleNs") + markModuleNsHandlerParams(lowerer, call.arguments.nodes[0]!, inner); + let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); // A TYPED handler on a DYN-settling promise (the tracePromise // result's `.then((value) => ...)` — the checker's generic // instantiation typed the parameter, but the settled value is a @@ -5585,7 +5605,7 @@ export function lowerPromiseMethodCall( } lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, `then handlers receiving '${lowerer.fmt(v.type)}' values through an untyped handler (the settled value cannot cross the checked-dynamic tree boundary)`, ); }; @@ -5623,7 +5643,7 @@ export function lowerPromiseMethodCall( value: { kind: "dynCall", callee: { kind: "varRef", localId: cbLocal.id, type: DYN, loc }, - calleeName: jsFuncNameOf(call.arguments[0]!, lowerer) ?? "onFulfilled", + calleeName: jsFuncNameOf(call.arguments.nodes[0]!, lowerer) ?? "onFulfilled", args: handlerArgs, type: DYN, loc, @@ -5660,7 +5680,7 @@ export function lowerPromiseMethodCall( if (cb.type.kind !== "func" || cb.type.params.length > 1) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "then handlers with more than one parameter (the two-argument onRejected form has no lowering — chain .catch(...) instead)", ); } @@ -5672,7 +5692,7 @@ export function lowerPromiseMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, `then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`, ); } @@ -5768,7 +5788,7 @@ export function lowerPromiseMethodCall( } if (member === "finally") { - const cb = lowerer.lowerExpr(call.arguments[0]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[0]!); if (lowerer.dynConvertible(promT) && lowerer.dynConvertible(cb.type)) { const result: IrExpr = { kind: "dynInvoke", @@ -5784,7 +5804,7 @@ export function lowerPromiseMethodCall( if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "finally callbacks with parameters or a return value (use () => { ... })", ); } @@ -5878,7 +5898,7 @@ export function lowerPromiseMethodCall( (isJsSourceFile(call.getSourceFile()) && (inner.kind === "void" || lowerer.dynConvertible(inner)))) ) { - let cb = lowerer.lowerExpr(call.arguments[0]!); + let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); if ( cb.type.kind === "func" && canBoxFuncIntoDyn( @@ -5929,7 +5949,7 @@ export function lowerPromiseMethodCall( value: { kind: "dynCall", callee: { kind: "varRef", localId: cbLocal.id, type: DYN, loc }, - calleeName: jsFuncNameOf(call.arguments[0]!, lowerer) ?? "onRejected", + calleeName: jsFuncNameOf(call.arguments.nodes[0]!, lowerer) ?? "onRejected", args: [ { kind: "caughtToDyn", @@ -5978,23 +5998,23 @@ export function lowerPromiseMethodCall( // .catch: the handler must be INLINE — its parameter becomes the // catch binding. - let handlerNode: ts.Expression = call.arguments[0]!; + let handlerNode: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(handlerNode)) handlerNode = handlerNode.expression; if (!ts.isArrowFunction(handlerNode) && !ts.isFunctionExpression(handlerNode)) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "catch handlers that are not inline function literals (the handler's parameter " + "becomes a typed-catch binding, which only an inline `(e) => ...` can receive)", ); } - if (handlerNode.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { + if (handlerNode.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { lowerer.unsupported("SC1090", handlerNode, "async catch handlers"); } - if (handlerNode.parameters.length > 1) { + if (handlerNode.parameters.nodes.length > 1) { lowerer.unsupported("SC1090", handlerNode, "catch handlers with more than one parameter"); } - const param = handlerNode.parameters[0]; + const param = handlerNode.parameters.nodes[0]; if (param && (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer)) { lowerer.unsupported("SC1062", param); } @@ -6052,7 +6072,7 @@ export function lowerPromiseMethodCall( } const hb = handlerNode.body; if (ts.isBlock(hb)) { - catchBody = lowerer.lowerStmts(hb.statements); + catchBody = lowerer.lowerStmts(hb.statements.nodes); } else if (R.kind === "void") { catchBody = [{ kind: "exprStmt", expr: lowerer.lowerExpr(hb), loc: locOf(hb) }]; } else { @@ -6144,9 +6164,9 @@ export function lowerFilterNarrowCall( const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (receiverIr?.kind !== "array") return null; if (!lowerer.isStdlibMember(access)) return null; - if (call.arguments.length !== 1) return null; // the generic path's arity fence + if (call.arguments.nodes.length !== 1) return null; // the generic path's arity fence const elem = receiverIr.elem; - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const isBooleanArg = @@ -6379,21 +6399,21 @@ function lowerSymbolStaticCall( const member = access.name.text; const loc = locOf(call); if (member === "for") { - if (call.arguments.length !== 1) { - lowerer.noLowering(`Symbol.for with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`Symbol.for with ${call.arguments.nodes.length} arguments`, call); } - const key = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const key = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "sym.for", args: [key], type: SYMBOL_T, loc }; } if (member === "keyFor") { - if (call.arguments.length !== 1) { - lowerer.noLowering(`Symbol.keyFor with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`Symbol.keyFor with ${call.arguments.nodes.length} arguments`, call); } - const sym = lowerer.lowerExpr(call.arguments[0]!); + const sym = lowerer.lowerExpr(call.arguments.nodes[0]!); if (sym.type.kind !== "symbol") { lowerer.noLowering( `Symbol.keyFor of a '${lowerer.fmt(sym.type)}' value`, - call.arguments[0]!, + call.arguments.nodes[0]!, "the argument must be symbol-typed", ); } @@ -6421,12 +6441,12 @@ function lowerBigIntStaticCall( if (!lowerer.isStdlibGlobal(access.expression, "BigInt")) return null; const name = access.name.text; if (name !== "asUintN" && name !== "asIntN") return null; - if (call.arguments.length !== 2) { - lowerer.noLowering(`BigInt.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2) { + lowerer.noLowering(`BigInt.${name} with ${call.arguments.nodes.length} arguments`, call); } const loc = locOf(call); - const bits = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const value = lowerer.lowerExprExpecting(call.arguments[1]!, BIGINT_T); + const bits = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BIGINT_T); return { kind: "libCall", fn: name === "asUintN" ? "bigint.asUintN" : "bigint.asIntN", @@ -6447,11 +6467,11 @@ function lowerBigIntMethodCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "bigint") return null; - if (name === "valueOf" && call.arguments.length === 0) return receiver; - if (name === "toString" && call.arguments.length <= 1) { + if (name === "valueOf" && call.arguments.nodes.length === 0) return receiver; + if (name === "toString" && call.arguments.nodes.length <= 1) { const defaultRadix: IrExpr = { kind: "numLit", value: 10, type: F64, loc }; - const radix: IrExpr = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], F64, defaultRadix) + const radix: IrExpr = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], F64, defaultRadix) : defaultRadix; return { kind: "libCall", fn: "bigint.toString", args: [receiver, radix], type: STRING, loc }; } @@ -6472,12 +6492,12 @@ function lowerSymbolMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "toString" && call.arguments.length === 0) { + if (name === "toString" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "symbol") return null; return { kind: "libCall", fn: "sym.toString", args: [receiver], type: STRING, loc }; } - if (name === "valueOf" && call.arguments.length === 0) { + if (name === "valueOf" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "symbol") return null; return receiver; @@ -6720,12 +6740,12 @@ function lowerRegExpStaticCall( if (!lowerer.isStdlibGlobal(access.expression, "RegExp")) return null; if (access.name.text !== "escape") return null; if (!lowerer.isStdlibMember(access)) return null; - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { - lowerer.noLowering(`RegExp.escape with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { + lowerer.noLowering(`RegExp.escape with ${call.arguments.nodes.length} arguments`, call); } - const arg = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const arg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); if (arg.type.kind !== "string") - lowerer.badType(call.arguments[0]!, lowerer.typeOf(call.arguments[0]!)); + lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!)); return { kind: "libCall", fn: "regexp.escape", args: [arg], type: STRING, loc: locOf(call) }; } @@ -6761,7 +6781,7 @@ function lowerIntlNumberFormatCall( ) { return null; } - ctorArgs = recv.arguments ?? []; + ctorArgs = recv.arguments?.nodes ?? []; } else { return null; } @@ -6793,13 +6813,13 @@ function lowerIntlNumberFormatCall( "everything else is ICU data the binary does not carry", ); } - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.noLowering( - `Intl.NumberFormat("en-US").format with ${call.arguments.length} arguments`, + `Intl.NumberFormat("en-US").format with ${call.arguments.nodes.length} arguments`, call, ); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; if (lowerer.mapTypeOf(lowerer.typeOf(argNode))?.kind !== "f64") { lowerer.noLowering( `Intl.NumberFormat("en-US").format over a '${lowerer.checker.typeToString(lowerer.typeOf(argNode))}'`, @@ -6848,10 +6868,10 @@ function lowerObjectStaticCall( (member === "getOwnPropertyNames" || member === "getOwnPropertySymbols" || member === "getOwnPropertyDescriptors") && - call.arguments.length === 1 && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length === 1 && + !call.arguments.nodes.some(ts.isSpreadElement) ) { - let object = lowerer.lowerExpr(call.arguments[0]!); + let object = lowerer.lowerExpr(call.arguments.nodes[0]!); if ( object.type.kind === "string" || object.type.kind === "f64" || @@ -6875,12 +6895,12 @@ function lowerObjectStaticCall( } if ( (member === "getPrototypeOf" || member === "setPrototypeOf") && - call.arguments.length === (member === "getPrototypeOf" ? 1 : 2) && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length === (member === "getPrototypeOf" ? 1 : 2) && + !call.arguments.nodes.some(ts.isSpreadElement) ) { - let object = lowerer.lowerExpr(call.arguments[0]!); + let object = lowerer.lowerExpr(call.arguments.nodes[0]!); if (object.type.kind === "func") { - const fnName = jsFuncNameOf(call.arguments[0]!, lowerer); + const fnName = jsFuncNameOf(call.arguments.nodes[0]!, lowerer); object = { kind: "dynFrom", value: object, @@ -6897,7 +6917,7 @@ function lowerObjectStaticCall( (id) => lowerer.unions.get(id), ) ) - object = lowerer.coerceInto(call.arguments[0]!, object, DYN); + object = lowerer.coerceInto(call.arguments.nodes[0]!, object, DYN); if (object.type.kind === "dyn") { const loc = locOf(call); if (member === "getPrototypeOf") { @@ -6919,7 +6939,7 @@ function lowerObjectStaticCall( ...(identityOnly ? { prototypeIdentityOnly: true as const } : {}), }; } - const prototype = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + const prototype = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (prototype.type.kind === "dyn") return { kind: "libCall", @@ -6930,8 +6950,8 @@ function lowerObjectStaticCall( }; } } - if (member === "getOwnPropertyDescriptor" && call.arguments.length === 2) { - const [receiver, key] = call.arguments; + if (member === "getOwnPropertyDescriptor" && call.arguments.nodes.length === 2) { + const [receiver, key] = call.arguments.nodes; if ( receiver && key && @@ -6971,16 +6991,19 @@ function lowerObjectStaticCall( // is (any, any) — and JS evaluates the arguments either way). if (member === "is") { - if (call.arguments.length !== 2 || call.arguments.some((a) => ts.isSpreadElement(a))) { + if ( + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { lowerer.noLowering( - `Object.is with ${call.arguments.length} arguments`, + `Object.is with ${call.arguments.nodes.length} arguments`, call, "exactly two arguments are the lowered form (JS treats a missing one as undefined — pass it explicitly)", ); } const loc = locOf(call); - const leftNode = call.arguments[0]!; - const rightNode = call.arguments[1]!; + const leftNode = call.arguments.nodes[0]!; + const rightNode = call.arguments.nodes[1]!; const left = lowerer.lowerExpr(leftNode); const right = lowerer.lowerExpr(rightNode); const lk = left.type.kind; @@ -7106,18 +7129,18 @@ function lowerObjectStaticCall( // Native checked objects retain a live prototype and can install an // own-property descriptor table during creation. if (member === "create") { - if (call.arguments.some((a) => ts.isSpreadElement(a))) { + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) { lowerer.noLowering("Object.create with spread arguments", call); } - if (call.arguments.length < 1 || call.arguments.length > 2) { - lowerer.noLowering(`Object.create with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { + lowerer.noLowering(`Object.create with ${call.arguments.nodes.length} arguments`, call); } const loc = locOf(call); - let protoNode: ts.Expression = call.arguments[0]!; + let protoNode: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(protoNode)) protoNode = protoNode.expression; const nullProto = protoNode.kind === ts.SyntaxKind.NullKeyword; - if (nullProto && call.arguments.length === 1) { + if (nullProto && call.arguments.nodes.length === 1) { return { kind: "libCall", fn: "dyn.objCreateNullProto", args: [], type: DYN, loc }; } let proto = nullProto @@ -7150,9 +7173,9 @@ function lowerObjectStaticCall( ) proto = lowerer.coerceInto(protoNode, proto, DYN); if (proto.type.kind === "dyn") { - if (call.arguments.length === 1) + if (call.arguments.nodes.length === 1) return { kind: "libCall", fn: "dyn.objCreate", args: [proto], type: DYN, loc }; - const descriptors = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + const descriptors = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (descriptors.type.kind === "dyn") return { kind: "libCall", @@ -7171,10 +7194,10 @@ function lowerObjectStaticCall( if (prototype) return prototype; if ( isJsSourceFile(call.getSourceFile()) && - call.arguments.length > 0 && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 0 && + !call.arguments.nodes.some(ts.isSpreadElement) ) { - const target = tryLowerExpression(lowerer, call.arguments[0]!); + const target = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if ( target && ((target.type.kind === "func" && lowerer.dynConvertible(target.type)) || @@ -7188,7 +7211,9 @@ function lowerObjectStaticCall( lowerer.coerceToExpected(target, DYN), { kind: "dynArrLit", - elems: call.arguments.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + elems: call.arguments.nodes + .slice(1) + .map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }, @@ -7205,14 +7230,17 @@ function lowerObjectStaticCall( // exactly the source literal's properties, which IS the source // literal evaluated (both fresh, no alias can tell them apart). // Everything else keeps the spread hint (stdlibMemberFence). - if (call.arguments.length === 2 && !call.arguments.some((a) => ts.isSpreadElement(a))) { - let target: ts.Expression = call.arguments[0]!; + if ( + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + let target: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(target)) target = target.expression; - let source: ts.Expression = call.arguments[1]!; + let source: ts.Expression = call.arguments.nodes[1]!; while (ts.isParenthesizedExpression(source)) source = source.expression; if ( ts.isObjectLiteralExpression(target) && - target.properties.length === 0 && + target.properties.nodes.length === 0 && ts.isObjectLiteralExpression(source) ) { return lowerer.lowerExpr(source); @@ -7232,17 +7260,20 @@ function lowerObjectStaticCall( // optional field must not erase the target's value — Node copies own // keys only — while an explicit `k: undefined` does). Everything // else keeps the spread hint. - if (call.arguments.length === 2 && !call.arguments.some((a) => ts.isSpreadElement(a))) { - const tProbe = tryLowerExpression(lowerer, call.arguments[0]!); - const sProbe = tryLowerExpression(lowerer, call.arguments[1]!); + if ( + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + const tProbe = tryLowerExpression(lowerer, call.arguments.nodes[0]!); + const sProbe = tryLowerExpression(lowerer, call.arguments.nodes[1]!); // CHECKED-DYNAMIC target and source (the JS file-scope // object-literal identity story): the runtime dyn copy — own // members of the source land on the target, which returns. if (tProbe?.type.kind === "dyn") { const loc = locOf(call); - const target = lowerer.lowerExpr(call.arguments[0]!); - const rawSource = lowerer.lowerExpr(call.arguments[1]!); - fenceSymbolFieldCopy(lowerer, call.arguments[1]!, rawSource.type); + const target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const rawSource = lowerer.lowerExpr(call.arguments.nodes[1]!); + fenceSymbolFieldCopy(lowerer, call.arguments.nodes[1]!, rawSource.type); const source = lowerer.coerceToExpected(rawSource, DYN); if (target.type.kind === "dyn" && source.type.kind === "dyn") { return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc }; @@ -7266,8 +7297,8 @@ function lowerObjectStaticCall( }); if (ok) { const loc = locOf(call); - const target = lowerer.lowerExpr(call.arguments[0]!); - const source = lowerer.lowerExpr(call.arguments[1]!); + const target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const source = lowerer.lowerExpr(call.arguments.nodes[1]!); if (target.type.kind === "record" && source.type.kind === "record") { const helper = recordAssignHelper( lowerer, @@ -7299,15 +7330,17 @@ function lowerObjectStaticCall( // unobservable), or a nullish unit (Node's ToObject TypeError // throws at the call, catchably); aliased record targets keep the // fence — their identity could not survive the conversion. - if (call.arguments.length >= 1 && !ts.isSpreadElement(call.arguments[0]!)) { - let targetNode: ts.Expression = call.arguments[0]!; + if (call.arguments.nodes.length >= 1 && !ts.isSpreadElement(call.arguments.nodes[0]!)) { + let targetNode: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(targetNode)) targetNode = targetNode.expression; const freshLiteralTarget = ts.isObjectLiteralExpression(targetNode); - const tProbe = freshLiteralTarget ? null : tryLowerExpression(lowerer, call.arguments[0]!); + const tProbe = freshLiteralTarget + ? null + : tryLowerExpression(lowerer, call.arguments.nodes[0]!); const tKind = tProbe?.type.kind; if (freshLiteralTarget || tKind === "dyn" || tKind === "nullT" || tKind === "undefinedT") { const loc = locOf(call); - const target = lowerer.lowerExprExpecting(call.arguments[0]!, DYN); + const target = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); if (target.type.kind === "dyn") { const t = lowerer.declareHiddenLocal("%oat", DYN); const p = lowerer.declareHiddenLocal("%oap", DYN); @@ -7327,7 +7360,7 @@ function lowerObjectStaticCall( // SINGLE LAST argument; every other spread position drives // the real iterator protocol, whose failure describes the // value — the two runtime variants, picked here by position. - const sources = call.arguments.slice(1); + const sources = call.arguments.nodes.slice(1); const spreadCount = sources.filter((a) => ts.isSpreadElement(a)).length; let ok = true; for (let i = 0; i < sources.length; i++) { @@ -7396,12 +7429,12 @@ function lowerObjectStaticCall( return null; } if ( - (member === "defineProperty" && call.arguments.length === 3) || - (member === "getOwnPropertyDescriptor" && call.arguments.length === 2) + (member === "defineProperty" && call.arguments.nodes.length === 3) || + (member === "getOwnPropertyDescriptor" && call.arguments.nodes.length === 2) ) { - if (call.arguments.some((a) => ts.isSpreadElement(a))) return null; - let target = tryLowerExpression(lowerer, call.arguments[0]!); - const targetNode = call.arguments[0]!; + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) return null; + let target = tryLowerExpression(lowerer, call.arguments.nodes[0]!); + const targetNode = call.arguments.nodes[0]!; // The checker gives C.prototype the instance type, but its lowered // value is the shared prototype table, not an instance to unbox. const prototypeTarget = @@ -7425,7 +7458,7 @@ function lowerObjectStaticCall( target = lowerer.coerceToExpected(target, DYN); } // Error instances share a live property table with their checked view. - const keyNode = call.arguments[1]!; + const keyNode = call.arguments.nodes[1]!; if ( target?.type.kind === "object" && target.type.className !== "%DOMException" && @@ -7436,12 +7469,12 @@ function lowerObjectStaticCall( target = lowerer.coerceToExpected(target, DYN); } if (target?.type.kind === "func" && lowerer.dynConvertible(target.type)) { - const fnName = jsFuncNameOf(call.arguments[0]!, lowerer); + const fnName = jsFuncNameOf(call.arguments.nodes[0]!, lowerer); target = { kind: "dynFrom", value: target, type: DYN, - loc: locOf(call.arguments[0]!), + loc: locOf(call.arguments.nodes[0]!), ...(fnName !== null ? { fnName } : {}), }; } @@ -7452,8 +7485,8 @@ function lowerObjectStaticCall( if (!target || (target.type.kind !== "dyn" && !isUnitType(target.type) && !descriptorPrimitive)) return null; if (target.type.kind !== "dyn") - target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) }; - const key = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; + const key = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (key.type.kind !== "dyn") return null; if (member === "getOwnPropertyDescriptor") { return { @@ -7464,7 +7497,7 @@ function lowerObjectStaticCall( loc: locOf(call), }; } - const descriptor = lowerer.lowerExprExpecting(call.arguments[2]!, DYN); + const descriptor = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN); if (descriptor.type.kind !== "dyn") return null; return { kind: "libCall", @@ -7483,12 +7516,12 @@ function lowerObjectStaticCall( // static shapes have no property table to extend. if ( member === "defineProperties" && - call.arguments.length === 2 && - !call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { const prototype = lowerClassPrototypeDescriptors(lowerer, call, member); if (prototype) return prototype; - let target = tryLowerExpression(lowerer, call.arguments[0]!); + let target = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if (target) { const native = lowerClassDataDescriptor(lowerer, call, member, target); if (native) return native; @@ -7506,20 +7539,20 @@ function lowerObjectStaticCall( (id) => lowerer.unions.get(id), ) ) { - const fnName = jsFuncNameOf(call.arguments[0]!, lowerer); + const fnName = jsFuncNameOf(call.arguments.nodes[0]!, lowerer); target = { kind: "dynFrom", value: target, type: DYN, - loc: locOf(call.arguments[0]!), + loc: locOf(call.arguments.nodes[0]!), ...(fnName !== null ? { fnName } : {}), }; } if (target && isUnitType(target.type)) { - target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) }; + target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; } if (target?.type.kind === "dyn") { - const descs = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + const descs = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (descs.type.kind === "dyn") { return { kind: "libCall", @@ -7534,10 +7567,10 @@ function lowerObjectStaticCall( } if ( ["preventExtensions", "isExtensible", "seal", "isSealed"].includes(member) && - call.arguments.length === 1 && - !ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + !ts.isSpreadElement(call.arguments.nodes[0]!) ) { - const value = lowerer.lowerExpr(call.arguments[0]!); + const value = lowerer.lowerExpr(call.arguments.nodes[0]!); if ( value.type.kind === "string" || value.type.kind === "f64" || @@ -7581,15 +7614,15 @@ function lowerObjectStaticCall( // other reference can write). Primitives pass through per ES2015. // Aliased objects keep a fence: a later write through the original // reference would need the runtime frozen bit (strict mode throws). - if (member === "isFrozen" && call.arguments.length === 1) { - const value = lowerer.lowerExprExpecting(call.arguments[0]!, DYN); + if (member === "isFrozen" && call.arguments.nodes.length === 1) { + const value = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); return { kind: "libCall", fn: "dyn.isFrozen", args: [value], type: BOOL, loc: locOf(call) }; } if (member === "freeze") { - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { - lowerer.noLowering(`Object.freeze with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { + lowerer.noLowering(`Object.freeze with ${call.arguments.nodes.length} arguments`, call); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let inner: ts.Expression = argNode; while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner)) inner = inner.expression; @@ -7644,11 +7677,11 @@ function lowerObjectStaticCall( // do too. if ( member === "hasOwn" && - call.arguments.length === 2 && - !call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { - const recvNode = call.arguments[0]!; - const keyNode = call.arguments[1]!; + const recvNode = call.arguments.nodes[0]!; + const keyNode = call.arguments.nodes[1]!; const probed = tryLowerExpression(lowerer, recvNode); const arrayKey = probed?.type.kind === "array" ? lowerer.lowerExpr(keyNode) : null; if (probed?.type.kind === "array" && arrayKey?.type.kind === "f64") { @@ -7706,8 +7739,9 @@ function lowerObjectStaticCall( return { kind: "call", callee: helper, args: [receiver, key], type: BOOL, loc }; } if (member !== "keys" && member !== "values" && member !== "entries") return null; - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) + return null; + const argNode = call.arguments.nodes[0]!; // util.types and node:util/types list Node's pinned key census // (UTIL_TYPES_KEYS): the object's own keys in Node's order, or the module // namespace's export names sorted with "default". values/entries would @@ -8288,11 +8322,12 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE const shape = lowerer.shapes.get(mapped.shapeId); const callField = shape?.fields.find((f) => f.name === "%call"); if (!shape || !callField || callField.type.kind !== "func") return null; - if (call.arguments.length < 2 || call.arguments.some((a) => ts.isSpreadElement(a))) return null; + if (call.arguments.nodes.length < 2 || call.arguments.nodes.some((a) => ts.isSpreadElement(a))) + return null; const loc = locOf(call); const values = new Map(); - values.set("%call", lowerer.lowerExprExpecting(call.arguments[0]!, callField.type)); - for (const argNode of call.arguments.slice(1)) { + values.set("%call", lowerer.lowerExprExpecting(call.arguments.nodes[0]!, callField.type)); + for (const argNode of call.arguments.nodes.slice(1)) { let src: ts.Expression = argNode; while (ts.isParenthesizedExpression(src) || ts.isAsExpression(src) || ts.isTypeAssertion(src)) src = src.expression; @@ -8303,7 +8338,7 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE "Object.assign sources other than plain object literals when building a function-with-properties value", ); } - for (const prop of src.properties) { + for (const prop of src.properties.nodes) { const nameOk = (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)); @@ -8449,7 +8484,7 @@ export function lowerRecordFieldCall( ) { const receiver = target && receiverLocal ? lowerer.coerceToExpected(target.obj, DYN) : undefined; - const spread = call.arguments.some(ts.isSpreadElement) + const spread = call.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -8463,7 +8498,7 @@ export function lowerRecordFieldCall( callee, ...(receiver ? { receiver } : {}), calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8534,7 +8569,7 @@ function lowerUrlStaticCall( if (callee.name.text === "canParse" || callee.name.text === "parse") { return lowerUrlFactory(lowerer, call, callee.name.text); } - if (callee.name.text !== "revokeObjectURL" || call.arguments.length !== 0) return null; + if (callee.name.text !== "revokeObjectURL" || call.arguments.nodes.length !== 0) return null; return nodeThrowExpr( 1, "ERR_MISSING_ARGS", @@ -8577,8 +8612,8 @@ function lowerInlineArrowBind( ): IrExpr | null { if ( access.name.text !== "bind" || - call.arguments.length > 1 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 1 || + call.arguments.nodes.some(ts.isSpreadElement) ) return null; let arrow = access.expression; @@ -8597,8 +8632,8 @@ function lowerInlineArrowBind( return null; const type = callee.type; const loc = locOf(call); - const firstDefault = arrow.parameters.findIndex((p) => p.initializer !== undefined); - const length = firstDefault < 0 ? arrow.parameters.length : firstDefault; + const firstDefault = arrow.parameters.nodes.findIndex((p) => p.initializer !== undefined); + const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; @@ -8691,7 +8726,7 @@ function lowerInlineArrowBind( const stmts: IrStmt[] = [{ kind: "varDecl", localId: saved.id, init: callee, loc }]; // The receiver is ignored by the arrow, but its effects and throws // happen after creating the arrow and before returning the bound value. - if (call.arguments[0]) stmts.push(lowerer.lowerExprStatement(call.arguments[0])); + if (call.arguments.nodes[0]) stmts.push(lowerer.lowerExprStatement(call.arguments.nodes[0])); const result: IrExpr = { kind: "call", callee: name, @@ -8712,8 +8747,8 @@ function lowerArrowFunctionApply( ): IrExpr | null { if ( access.name.text !== "apply" || - call.arguments.length !== 2 || - call.arguments.some(ts.isSpreadElement) || + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some(ts.isSpreadElement) || arrowSignatureOf(lowerer, access.expression) === null ) { return null; @@ -8731,14 +8766,14 @@ function lowerArrowFunctionApply( ) { return null; } - const argsList = lowerer.lowerExpr(call.arguments[1]!); + const argsList = lowerer.lowerExpr(call.arguments.nodes[1]!); if (argsList.type.kind !== "dyn") return null; const loc = locOf(call); const savedCallee = lowerer.declareHiddenLocal("%applyFn", callee.type); const savedArgs = lowerer.declareHiddenLocal("%applyArgs", DYN); const stmts: IrStmt[] = [ { kind: "varDecl", localId: savedCallee.id, init: callee, loc }, - { kind: "exprStmt", expr: lowerer.lowerExpr(call.arguments[0]!), loc }, + { kind: "exprStmt", expr: lowerer.lowerExpr(call.arguments.nodes[0]!), loc }, { kind: "varDecl", localId: savedArgs.id, init: argsList, loc }, ]; const args = callee.type.params.map((param, index): IrExpr => { @@ -8749,7 +8784,7 @@ function lowerArrowFunctionApply( type: DYN, loc, }; - return lowerer.coerceInto(call.arguments[1]!, read, param); + return lowerer.coerceInto(call.arguments.nodes[1]!, read, param); }); const invoked: IrExpr = { kind: "callValue", @@ -8789,7 +8824,7 @@ function lowerObjLitGenericMethodCall( SP_BRAND_METHODS.has(access.expression.name.text) && lowerer.isStdlibMember(access.expression) ) { - const thisArg = call.arguments[0]; + const thisArg = call.arguments.nodes[0]; const thisT = thisArg ? lowerer.mapTypeOf(lowerer.typeOf(thisArg)) : { kind: "undefinedT" as const }; @@ -8883,7 +8918,7 @@ function lowerObjLitGenericMethodCall( if ( value?.type.kind === "func" && lowerer.dynConvertible(value.type) && - !call.arguments.some(ts.isSpreadElement) + !call.arguments.nodes.some(ts.isSpreadElement) ) { const loc = locOf(call); return { @@ -8891,7 +8926,7 @@ function lowerObjLitGenericMethodCall( recv: { kind: "dynFrom", value, type: DYN, loc }, method: name, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8919,7 +8954,7 @@ function lowerObjLitGenericMethodCall( // dispatches by name to the runtime object's member, which a generic // method answers with its erased instance; the result converts to this // call's instantiated return type. - if (onInterface && !call.arguments.some(ts.isSpreadElement)) { + if (onInterface && !call.arguments.nodes.some(ts.isSpreadElement)) { const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind === "dyn") { const loc = locOf(call); @@ -8928,7 +8963,7 @@ function lowerObjLitGenericMethodCall( recv: receiver, method: name, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8952,7 +8987,7 @@ function lowerObjLitGenericMethodCall( const info = objLitGenericFnInfoOf(lowerer, call, name, found); const instance = genericCallInstance(lowerer, call, info); const loc = locOf(call); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } @@ -9171,14 +9206,14 @@ export function lowerObjectMethodCall( if (callee?.type.kind === "func") { if ( ((callee.type.rest === true && callee.type.restAbi === undefined) || - call.arguments.length > callee.type.params.length) && + call.arguments.nodes.length > callee.type.params.length) && isJsSourceFile(call.getSourceFile()) && lowerer.dynConvertible(callee.type) ) { const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc }; const spread = lowerSpreadArgsCall(lowerer, call, boxed, loc); if (spread?.kind === "dynCall") return finish({ ...spread, ...receiver }); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return finish({ kind: "dynCall", callee: boxed, @@ -9193,10 +9228,10 @@ export function lowerObjectMethodCall( return finish({ kind: "callValue", callee, ...receiver, args, type: callee.type.ret, loc }); } if (callee?.type.kind === "dyn") { - if (call.arguments.some((a) => ts.isSpreadElement(a))) { + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) { lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } - const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); return finish({ kind: "dynCall", callee, @@ -9228,7 +9263,7 @@ export function lowerObjectMethodCall( type: DYN, loc, }; - const spread = call.arguments.some(ts.isSpreadElement) + const spread = call.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -9241,7 +9276,7 @@ export function lowerObjectMethodCall( callee, receiver, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -9262,7 +9297,7 @@ export function lowerObjectMethodCall( const receiver = lowerReceiver(); const local = lowerer.declareHiddenLocal("%callbackReceiver", receiver.type); const value: IrExpr = { kind: "varRef", localId: local.id, type: receiver.type, loc }; - const args = lowerer.completeArgs(call.arguments, found.sig.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call); const virtual = lowerer.overrideBelow(info, method); if (virtual) lowerer.noteVirtualEdge(info, method); else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`); @@ -9286,7 +9321,7 @@ export function lowerObjectMethodCall( lowerer, value, method, - call.arguments, + call.arguments.nodes, fallback, loc, access.getText(), @@ -9318,7 +9353,7 @@ export function lowerObjectMethodCall( if (lowerer.overrideBelow(info, method)) lowerer.noteVirtualEdge(info, method); else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`); const receiver = lowerReceiver(); - const args = lowerer.completeArgs(call.arguments, found.sig.params, locOf(call), call); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, locOf(call), call); if (lowerer.overrideBelow(info, method)) { return reconcileOverloadReturn(lowerer, call, { kind: "virtualCall", @@ -9379,7 +9414,7 @@ function lowerUnionObjectMethodCall( type: DYN, loc, }; - const spread = call.arguments.some(ts.isSpreadElement) + const spread = call.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -9392,7 +9427,7 @@ function lowerUnionObjectMethodCall( callee, receiver, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -9423,7 +9458,7 @@ function lowerUnionObjectMethodCall( lowerer.lowerExpr(access.expression), receiverT, ); - const args = lowerer.completeArgs(call.arguments, shapes, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, shapes, loc, call); const helper = unionObjectMethodHelper( lowerer, call, @@ -9473,7 +9508,7 @@ function lowerUnionObjectDynFieldCall( if (!info || info.fields.get(field)?.kind !== "dyn") return null; plans.push({ arm, info }); } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } const loc = locOf(call); @@ -9545,7 +9580,7 @@ function lowerUnionObjectDynFieldCall( lowerer.liftedFns.push({ name: helper, params, returnType: DYN, locals, body, loc }); } const callee: IrExpr = { kind: "call", callee: helper, args: [receiverRef], type: DYN, loc }; - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc }], diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 093e7eeef..770c74840 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -162,7 +162,7 @@ import { reflectedClassStaticMethodValue } from "./class-method-values.js"; export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { const body = member.body; - const last = body?.statements.at(-1); + const last = body?.statements?.nodes.at(-1); if (!body || !last || !ts.isReturnStatement(last)) return false; let valid = true; const visit = (node: ts.Node): void => { @@ -302,7 +302,7 @@ export function storedClassValueType( if ( (ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) && isJsSourceFile(callee.getSourceFile()) && - !callee.typeParameters?.length && + !callee.typeParameters?.nodes.length && !lowerer.preparingClassFactories.has(callee) ) { const signature = lowerer.checker.getSignatureFromDeclaration(callee); @@ -371,7 +371,7 @@ export function storedClassValueType( if ( !factory || !isJsSourceFile(factory.getSourceFile()) || - factory.typeParameters?.length || + factory.typeParameters?.nodes.length || !ts.isVariableDeclaration(factory.parent) || factory.parent.initializer !== factory || !ts.isVariableDeclarationList(factory.parent.parent) || @@ -764,8 +764,8 @@ function symbolSlotReturnType( if (fnLike.type !== undefined || !fnLike.body) return null; if (!isJsSourceFile(fnLike.getSourceFile())) return null; if (fnLike.asteriskToken !== undefined) return null; - if (fnLike.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; - const stmts = fnLike.body.statements; + if (fnLike.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; + const stmts = fnLike.body.statements.nodes; const last = stmts[stmts.length - 1]; if (!last || !ts.isReturnStatement(last)) return null; const returns: ts.ReturnStatement[] = []; @@ -1092,7 +1092,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E while (ts.isParenthesizedExpression(node)) node = node.expression; if (ts.isCallExpression(node) && type.kind === "bytes") { let prototypeArgument = false; - for (const argument of node.arguments) + for (const argument of node.arguments.nodes) ts.walkPreorder(argument, (part) => { if ( !ts.isPropertyAccessExpression(part) || @@ -1119,7 +1119,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E owner = owner.parent; if (owner && (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && owner.name) { const className = owner.name.text; - for (const statement of owner.getSourceFile().statements) { + for (const statement of owner.getSourceFile().statements.nodes) { if ( !ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) @@ -1428,9 +1428,9 @@ export const EMITTER_API_MEMBERS: ReadonlySet = new Set([ /** The decorators of a class-like or member node (they live in * `modifiers` since TS 4.8). */ export function decoratorNodesOf(n: ts.Node): ts.Decorator[] { - return (((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []) as ts.Node[]).filter( - (m): m is ts.Decorator => m.kind === ts.SyntaxKind.Decorator, - ); + return ( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []) as ts.Node[] + ).filter((m): m is ts.Decorator => m.kind === ts.SyntaxKind.Decorator); } /** The AMBIENT name a decorator expression's evaluation throws on, or @@ -1488,13 +1488,13 @@ export function guaranteedDecorationThrow( if (v === "opaque") return null; if (v !== "effectFree") return v; } - const heritage = decl.heritageClauses?.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) - ?.types[0]; + const heritage = decl.heritageClauses?.nodes.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) + ?.types?.nodes[0]; if (heritage) { const h = stripParens(heritage.expression); if (h.kind !== ts.SyntaxKind.NullKeyword && !ts.isIdentifier(h)) return null; } - for (const member of decl.members) { + for (const member of decl.members.nodes) { for (const d of decoratorNodesOf(member)) { const v = decoratorVerdict(d); if (v === "opaque") return null; @@ -1531,9 +1531,9 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) // A forward type reference can demand this class before collection // reaches its earlier base declaration. Collect that dependency first; // a later same-file base remains subject to the runtime order boundary. - const heritage = decl.heritageClauses?.find( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; const reference = heritage && ts.isPropertyAccessExpression(heritage) ? heritage.name : heritage; const baseSymbol = @@ -1563,10 +1563,10 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) // classStaticBlock13/28 miscompiles). const hasDeclTimeCode = (n: ts.Node): boolean => ts.isClassStaticBlockDeclaration(n) || - ((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []).some( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some( (m) => m.kind === ts.SyntaxKind.Decorator, ); - if (symbol && (hasDeclTimeCode(decl) || decl.members.some(hasDeclTimeCode))) { + if (symbol && (hasDeclTimeCode(decl) || decl.members.nodes.some(hasDeclTimeCode))) { lowerer.reportDeferred(symbol); } // A poisoned BASE this class EXTENDS must report EAGERLY for the same @@ -1580,9 +1580,9 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) // reports. Resolution runs under the collect pass's guard (this is // collectProgram), so the lookup neither flushes nor fences. { - const baseIdent = decl.heritageClauses - ?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword) - ?.types.map((t) => t.expression) + const baseIdent = decl.heritageClauses?.nodes + .find((h) => h.token === ts.SyntaxKind.ExtendsKeyword) + ?.types?.nodes.map((t) => t.expression) .filter(ts.isIdentifier)[0]; const baseSym = baseIdent ? lowerer.resolveValueSymbol(baseIdent) : null; if (baseSym) lowerer.reportDeferred(baseSym); @@ -1597,7 +1597,7 @@ function inferredEmptyCollectionFieldType( ): IrType | null { if ( !ts.isNewExpression(initializer) || - (initializer.arguments?.length ?? 0) !== 0 || + (initializer.arguments?.nodes.length ?? 0) !== 0 || !ts.isIdentifier(initializer.expression) ) return null; @@ -1637,10 +1637,10 @@ function inferredEmptyCollectionFieldType( if ( collection === "Map" && node.expression.name.text === "set" && - node.arguments.length === 2 + node.arguments.nodes.length === 2 ) { - const key = mappedArgument(node.arguments[0]!); - const value = mappedArgument(node.arguments[1]!); + const key = mappedArgument(node.arguments.nodes[0]!); + const value = mappedArgument(node.arguments.nodes[1]!); if ( key !== null && value !== null && @@ -1659,9 +1659,9 @@ function inferredEmptyCollectionFieldType( (node.expression.name.text === "add" || node.expression.name.text === "has" || node.expression.name.text === "delete") && - node.arguments.length === 1 + node.arguments.nodes.length === 1 ) { - const element = mappedArgument(node.arguments[0]!); + const element = mappedArgument(node.arguments.nodes[0]!); if ( element !== null && isSupportedSetElem( @@ -1673,7 +1673,7 @@ function inferredEmptyCollectionFieldType( } return undefined; }; - for (const member of decl.members) { + for (const member of decl.members.nodes) { const body = ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member) || ts.isAccessor(member) ? member.body @@ -1745,7 +1745,7 @@ export function collectClassShapeInner( const classDecoratorNodes: ts.Decorator[] = []; { classDecoratorNodes.push(...decoratorNodesOf(decl)); - const decoratedMembers = decl.members.filter((m) => decoratorNodesOf(m).length > 0); + const decoratedMembers = decl.members.nodes.filter((m) => decoratorNodesOf(m).length > 0); if (classDecoratorNodes.length > 0 || decoratedMembers.length > 0) { // Node itself cannot execute decorator syntax in a JavaScript // source (V8 has not shipped the proposal; the type-stripping @@ -1811,7 +1811,7 @@ export function collectClassShapeInner( : ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member) ? "accessor decorators" : ts.isPropertyDeclaration(member) - ? member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AccessorKeyword) + ? member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AccessorKeyword) ? "auto-accessor decorators" : "field decorators" : "member decorators"; @@ -1912,13 +1912,13 @@ export function collectClassShapeInner( "a generic base whose type arguments have no concrete layout", ); } - for (const clause of inst || mixin || computedBase ? [] : (decl.heritageClauses ?? [])) { + for (const clause of inst || mixin || computedBase ? [] : (decl.heritageClauses?.nodes ?? [])) { // `implements` is pure type-world: tsc checked the conformance and // the clause erases — nothing about the runtime class changes. // (Assigning an instance INTO an interface-typed slot is a separate // question, owned by the shape-coercion fences at those sites.) if (clause.token === ts.SyntaxKind.ImplementsKeyword) continue; - const t = clause.types[0]; + const t = clause.types.nodes[0]; if (familyMode && t) { const reference = ts.isPropertyAccessExpression(t.expression) ? t.expression.name @@ -1953,14 +1953,14 @@ export function collectClassShapeInner( globalError.def.name !== "%DOMException" && globalError.def.name !== "%AggregateError" ) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = globalError; continue; } const emitterBase = lowerer.builtinEmitterInfoOf(resolved); const streamBaseNs = builtinStreamInfoOf(lowerer, resolved); if (emitterBase || streamBaseNs) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = (emitterBase ?? streamBaseNs)!; continue; } @@ -1983,7 +1983,7 @@ export function collectClassShapeInner( `extending the namespace member '${t.expression.name.text}' (no class lowering)`, ); } - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = nsBase; continue; } @@ -2001,7 +2001,8 @@ export function collectClassShapeInner( if (!t.expression.questionDotToken) { const propBase = propertyAssignedClassInfoOf(lowerer, memberSym); if (propBase) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) + lowerer.unsupported("SC1090", t, "extending generic classes"); const baseDecl = propBase.decl; if ( baseDecl != null && @@ -2042,7 +2043,7 @@ export function collectClassShapeInner( // so its statics queue ahead of the derived class's — the // recursion order delivers exactly that). if (t && ts.isClassExpression(t.expression)) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); const baseExpr = lowerer.lowerClassExpressionInfo(t.expression); base = baseExpr; continue; @@ -2053,7 +2054,7 @@ export function collectClassShapeInner( // through the monomorphized chain (lower-mixins.ts). A call whose // callee is NOT a mixin function keeps the computed-expression // fence below. - if (t && ts.isCallExpression(t.expression) && !t.typeArguments) { + if (t && ts.isCallExpression(t.expression) && !t.typeArguments?.nodes) { const mixinBase = lowerer.mixinCallClassInfoOf(t.expression); if (mixinBase) { base = mixinBase; @@ -2166,7 +2167,7 @@ export function collectClassShapeInner( base = instBase; continue; } - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = named; if (!base) { if (adoptCallableBase(t.expression)) continue; @@ -2179,9 +2180,9 @@ export function collectClassShapeInner( } if (base?.localClass && !functionLocalClass(decl) && !computedBase && !factoryBaseExpression) { - const heritage = decl.heritageClauses?.find( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]; + )?.types?.nodes[0]; if (heritage) factoryBaseExpression = heritage.expression; else lowerer.unsupported("SC1090", decl, "extending a function-local class outside a function"); @@ -2256,13 +2257,15 @@ export function collectClassShapeInner( const checkedStatics = isJsSourceFile(decl.getSourceFile()) && ((base && hasRuntimeStatics(base)) || - decl.members.some( + decl.members.nodes.some( (member) => ts.isAccessor(member) && !ts.isPrivateIdentifier(member.name) && - member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), )) && - !decl.members.some( + !decl.members.nodes.some( (member) => member.name && ts.isPrivateIdentifier(member.name) && @@ -2301,12 +2304,12 @@ export function collectClassShapeInner( if (fnNode.asteriskToken) lowerer.unsupported("SC1071", member); const mName = (member.name as ts.Identifier | ts.PrivateIdentifier).text; const typeParams: ts.Symbol[] = []; - for (const tp of fnNode.typeParameters!) { + for (const tp of fnNode.typeParameters!.nodes) { const sym = lowerer.checker.getSymbolAtLocation(tp.name); if (!sym) lowerer.unsupported("SC1090", member, "this method form"); typeParams.push(sym); } - for (const param of fnNode.parameters) { + for (const param of fnNode.parameters.nodes) { if ( !ts.isIdentifier(param.name) && !ts.isObjectBindingPattern(param.name) && @@ -2323,7 +2326,7 @@ export function collectClassShapeInner( instances: new Map(), }); }; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if ( (functionLocalClass(decl) || checkedStatics) && isJsSourceFile(decl.getSourceFile()) && @@ -2491,7 +2494,7 @@ export function collectClassShapeInner( if (errorRooted && memberName && classMemberNameOf(lowerer, memberName) === "cause") { if ( ts.isPropertyDeclaration(member) && - !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) + !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) ) { // A TypeScript redeclaration is the JavaScript field: Node // defines the Error's own `cause` (undefined without an @@ -2586,12 +2589,12 @@ export function collectClassShapeInner( !inst && !mixin && ts.isMethodDeclaration(member) && - !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) + !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) ) { const reason = emitOverrideShapeReason(lowerer, member); if (reason === null) { - const eventSym = lowerer.checker.getSymbolAtLocation(member.parameters[0]!.name); - const restSym = lowerer.checker.getSymbolAtLocation(member.parameters[1]!.name); + const eventSym = lowerer.checker.getSymbolAtLocation(member.parameters.nodes[0]!.name); + const restSym = lowerer.checker.getSymbolAtLocation(member.parameters.nodes[1]!.name); if (eventSym && restSym) { emitOverride = { decl: member, eventSym, restSym }; continue; @@ -2850,11 +2853,11 @@ export function collectClassShapeInner( // the ordinary undefined-armed union machinery. let type = lowerer.irTypeOf(member.name); if (isJsSourceFile(member.getSourceFile()) && !member.initializer) { - const constructor = decl.members.find( + const constructor = decl.members.nodes.find( (part): part is ts.ConstructorDeclaration => ts.isConstructorDeclaration(part) && !!part.body, ); - for (const statement of constructor?.body?.statements ?? []) { + for (const statement of constructor?.body?.statements?.nodes ?? []) { if ( !ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) || @@ -3008,8 +3011,8 @@ export function collectClassShapeInner( // FIRST (before every declared field, as undefined), and the // assignments run after super() and the field initializers, in // parameter order (see paramPropInitStmts). - for (const p of member.parameters) { - const isParamProp = p.modifiers?.some( + for (const p of member.parameters.nodes) { + const isParamProp = p.modifiers?.nodes.some( (m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.PrivateKeyword || @@ -3020,7 +3023,7 @@ export function collectClassShapeInner( if (!isParamProp) { // Non-keyword modifiers (parameter decorators) are rejected // by tsc under standard decorators; defensive. - if (p.modifiers?.length) lowerer.unsupported("SC1090", p, "this parameter form"); + if (p.modifiers?.nodes.length) lowerer.unsupported("SC1090", p, "this parameter form"); continue; } // tsc rejects binding patterns (TS1187) and rest params @@ -3300,7 +3303,7 @@ export function collectClassShapeInner( // Defaults stay local to each declaration, so only inherited // required/optional slots participate in this refinement. for (let i = 0; i < shapes.length; i++) { - const parameter = member.parameters[i]; + const parameter = member.parameters.nodes[i]; const inherited = overridden.sig.params[i]; if ( parameter && @@ -3364,16 +3367,16 @@ export function collectClassShapeInner( const returnOnly = overridden.sig.params.length === shapes.length && overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) && - !member.parameters.some((parameter, index) => { + !member.parameters.nodes.some((parameter, index) => { if (!parameter.initializer) return false; - const baseMember = overridden.declarer.decl?.members.find( + const baseMember = overridden.declarer.decl?.members?.nodes.find( (candidate) => ts.isMethodDeclaration(candidate) && classMemberNameOf(lowerer, candidate.name) === mName, ); const inherited = baseMember && ts.isMethodDeclaration(baseMember) - ? baseMember.parameters[index]?.initializer + ? baseMember.parameters.nodes[index]?.initializer : undefined; if (!inherited) return false; const ownType = lowerer.mapTypeOf(lowerer.typeOf(parameter.initializer)); @@ -3407,7 +3410,7 @@ export function collectClassShapeInner( lowerer.unionReturnOverrides.add(member); } const asyncMember = - member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; // Async methods dispatch STATICALLY (the body enters through its // fiber spawn wrapper; vtable slots hold raw implementations) — // an override chain touching an async method on either end would @@ -3532,7 +3535,7 @@ export function collectClassShapeInner( sig = { params: [], ret }; } else { // tsc rejects optional/default/rest setter params (TS1051-53). - sig = { params: lowerer.paramShapes(member.parameters), ret: VOID }; + sig = { params: lowerer.paramShapes(member.parameters.nodes), ret: VOID }; } if (lowerer.checkedVirtualJsAccessors.has(member)) { if (isGet) sig.ret = DYN; @@ -3604,7 +3607,7 @@ export function collectClassShapeInner( // slot (a nullable instance pointer) starts empty and whose reads check // it. Fields assigned before any exposure keep their plain reads. const derived = - decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ?? + decl.heritageClauses?.nodes.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ?? false; // Generic instances and mixins share layouts across declarations, so // they only report whether construction exposes the instance. @@ -3633,7 +3636,7 @@ export function collectClassShapeInner( const callee = call.expression; if (!ts.isPropertyAccessExpression(callee)) return null; const name = callee.name.text; - const declaration = decl.members.find( + const declaration = decl.members.nodes.find( (member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && member.body !== undefined && @@ -3665,7 +3668,7 @@ export function collectClassShapeInner( const deferredInitFields = new Set(base?.deferredInitFields ?? []); if (unguardedFields.length > 0) { const topAssigned = new Set(); - for (const stmt of ctor?.body?.statements ?? []) { + for (const stmt of ctor?.body?.statements?.nodes ?? []) { if ( ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && @@ -3852,7 +3855,7 @@ export function collectClassShapeInner( ) : null; if (ctor?.body) { - for (const stmt of ctor.body.statements) { + for (const stmt of ctor.body.statements.nodes) { const lhs = ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && @@ -4090,10 +4093,10 @@ export function collectClassShapeInner( // fence, never a mis-typed array. const mixinForwarding = mixin !== undefined && ctor !== null && mixinForwardingCtor(lowerer, ctor); - if (mixin && ctor && !mixinForwarding && ctor.parameters.some((p) => p.dotDotDotToken)) { + if (mixin && ctor && !mixinForwarding && ctor.parameters.nodes.some((p) => p.dotDotDotToken)) { lowerer.unsupported( "SC1090", - ctor.parameters.find((p) => p.dotDotDotToken)!, + ctor.parameters.nodes.find((p) => p.dotDotDotToken)!, "mixin constructors whose rest parameter does anything but forward (`super(...args)` as the first statement is the compiled shape)", ); } @@ -4102,7 +4105,7 @@ export function collectClassShapeInner( // synthesized constructor forwards the same params to super). const ctorParams: ParamShape[] = ctor && !mixinForwarding - ? lowerer.paramShapes(ctor.parameters) + ? lowerer.paramShapes(ctor.parameters.nodes) : (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : [])); if ( ctor && @@ -4126,13 +4129,13 @@ export function collectClassShapeInner( jsName: jsNameOverride ?? decl.name?.text ?? "", ...(base ? { base: base.def.name } : {}), jsLength: - ctor?.parameters.findIndex( + ctor?.parameters?.nodes.findIndex( (parameter) => parameter.initializer || parameter.dotDotDotToken, ) === -1 - ? ctor.parameters.length + ? ctor.parameters.nodes.length : Math.max( 0, - ctor?.parameters.findIndex( + ctor?.parameters?.nodes.findIndex( (parameter) => parameter.initializer || parameter.dotDotDotToken, ) ?? 0, ), @@ -4216,7 +4219,7 @@ export function collectClassShapeInner( } if (familyMode) { const typeParams: ts.Symbol[] = []; - for (const tp of decl.typeParameters!) { + for (const tp of decl.typeParameters.nodes) { const sym = lowerer.checker.getSymbolAtLocation(tp.name); if (!sym) lowerer.unsupported("SC1090", tp, "this type parameter form"); typeParams.push(sym); @@ -4985,7 +4988,7 @@ function constructorMethodReads( const out: ts.Node[] = []; let owner: ts.ClassLikeDeclaration | undefined = decl; for (let next = base; owner; owner = next?.decl ?? undefined, next = next?.base ?? null) { - for (const member of owner.members) { + for (const member of owner.members.nodes) { if ( (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body && @@ -5424,14 +5427,17 @@ export function lowerClassExpressionInfo( } if (ts.isFunctionLike(p)) local = true; } - if (local && expr.members.some((member) => member.name && ts.isPrivateIdentifier(member.name))) { + if ( + local && + expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name)) + ) { lowerer.unsupported("SC1090", expr, "function-local classes with private brands"); } if ( local && - (expr.typeParameters?.length || + (expr.typeParameters?.nodes.length || decoratorNodesOf(expr).length || - expr.members.some( + expr.members.nodes.some( (member) => decoratorNodesOf(member).length || (!isJsSourceFile(expr.getSourceFile()) && @@ -5439,7 +5445,7 @@ export function lowerClassExpressionInfo( ts .getModifiers(member) ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) || - (ts.isMethodDeclaration(member) && member.typeParameters?.length), + (ts.isMethodDeclaration(member) && member.typeParameters?.nodes.length), )) ) { lowerer.unsupported( @@ -5455,9 +5461,9 @@ export function lowerClassExpressionInfo( lowerer.fnStack.length > 0 && !lowerer.computedClassBases.has(lowerer.classNamer(expr)) ) { - const heritage = expr.heritageClauses?.find( + const heritage = expr.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; if ( heritage && (ts.isCallExpression(heritage) || @@ -5489,9 +5495,9 @@ export function lowerClassExpressionInfo( }); info.localClass = { context: null, bodies: new Map(), captureSources: [], ready: false }; if (info.base?.localClass) { - const baseExpression = expr.heritageClauses?.find( + const baseExpression = expr.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; if (baseExpression) info.localClass.baseExpression = baseExpression; } } @@ -5526,7 +5532,7 @@ export function lowerClassExpressionInfo( (ts.isVariableDeclaration(holder) && holder.initializer !== undefined && ts.isVariableDeclarationList(holder.parent) && - holder.parent.declarations.length === 1 && + holder.parent.declarations.nodes.length === 1 && ts.isVariableStatement(holder.parent.parent) && ts.isSourceFile(holder.parent.parent.parent)) || (ts.isBinaryExpression(holder) && @@ -5556,7 +5562,7 @@ export function lowerClassExpressionInfo( export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): IrExpr { if ( decoratorNodesOf(expr).length > 0 || - expr.members.some((m) => decoratorNodesOf(m).length > 0) + expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0) ) { if (!isJsSourceFile(expr.getSourceFile()) && expr.typeParameters === undefined) { const thrown = guaranteedDecorationThrow(lowerer, expr); @@ -5854,7 +5860,7 @@ function countAssignmentsTo(assign: ts.BinaryExpression): number { }; const target = canon(assign.left); let n = 0; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExpressionStatement(stmt)) continue; const e = stmt.expression; if ( @@ -5997,7 +6003,7 @@ function staticCallOn( recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, calleeName: access.name.text, - args: call.arguments.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc, }; @@ -6044,7 +6050,7 @@ function staticCallOn( call, staticMethodForReceiver(lowerer, access, gfound.info, info, throughValue), ); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } if (throughValue && staticShadowBelow(info, access.name.text)) { @@ -6061,7 +6067,7 @@ function staticCallOn( call, staticMethodForReceiver(lowerer, access, specialized.info, info, throughValue), ); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } if (found.field !== undefined) { @@ -6075,7 +6081,7 @@ function staticCallOn( loc, }; const params = found.field.type.params; - const args = call.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); + const args = call.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); for (let i = args.length; i < params.length; i++) { const absent = omittedArgFor(lowerer, params[i]!, loc); if (!absent) { @@ -6113,14 +6119,14 @@ function staticCallOn( return { kind: "call", callee: instance.name, - args: lowerer.completeArgs(call.arguments, instance.params, loc, call), + args: lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call), type: instance.returnType, loc, }; } const fnName = `%${found.declarer.def.name}.static:${access.name.text}`; lowerer.noteEdge(fnName); - const args = lowerer.completeArgs(call.arguments, found.method.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, found.method.params, loc, call); return { kind: "call", callee: fnName, args, type: found.method.ret, loc }; } @@ -6165,15 +6171,15 @@ function directRuntimeStaticCall( } } if (!declarer || !member || !declarer.decl) return null; - if (member.asteriskToken || member.typeParameters) return null; - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)) + if (member.asteriskToken || member.typeParameters?.nodes) return null; + if (member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return null; if (hasLexicalSuper(member) || capturesEnclosingScope(lowerer, member, declarer.decl)) return null; if (memberAssigned(lowerer, member, name) || bodyReadsArguments(member)) return null; const implicit = - member.parameters.length === 0 ? null : implicitAnyParamSymbolsOf(lowerer, member, true); - if (member.parameters.length > 0 && !implicit) return null; + member.parameters.nodes.length === 0 ? null : implicitAnyParamSymbolsOf(lowerer, member, true); + if (member.parameters.nodes.length > 0 && !implicit) return null; let calls = declarer.runtimeStaticCalls; if (!calls) { calls = new Map(); @@ -6203,7 +6209,7 @@ function directRuntimeStaticCall( lowered = { kind: "call", callee: instance.name, - args: lowerer.completeArgs(call.arguments, instance.params, locOf(call), call), + args: lowerer.completeArgs(call.arguments.nodes, instance.params, locOf(call), call), type: instance.returnType, loc: locOf(call), }; @@ -6456,11 +6462,11 @@ function abstractMemberSignature( lowerer: Lowerer, member: ts.MethodDeclaration, ): { shapes: ParamShape[]; ret: IrType } { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (!ts.isIdentifier(param.name)) lowerer.unsupported("SC1031", param); } return { - shapes: lowerer.paramShapes(member.parameters), + shapes: lowerer.paramShapes(member.parameters.nodes), ret: lowerer.declaredReturnType(member, member.name), }; } @@ -6595,7 +6601,7 @@ export function collectInheritedJsFieldWrites( ts.walkPreorder(file, (node) => { if ( (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) || - !node.heritageClauses?.length + !node.heritageClauses?.nodes.length ) return; const writes = new Map(); @@ -6628,8 +6634,10 @@ export function collectInheritedJsFieldWrites( } part.forEachChild(scan); }; - for (const member of node.members) { - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) + for (const member of node.members.nodes) { + if ( + member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + ) continue; if (ts.isPropertyDeclaration(member) && member.initializer) { const name = ts.isPrivateIdentifier(member.name) @@ -6692,14 +6700,16 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou lowerer.speculativeTypeQueries(() => collectClassMethodMutations(lowerer, files)); const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { - const accessors = node.members.filter( + const accessors = node.members.nodes.filter( (member): member is ts.GetAccessorDeclaration | ts.SetAccessorDeclaration => (ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) && ts.isIdentifier(member.name) && - !member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + !member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), ); if ( - node.heritageClauses?.length && + node.heritageClauses?.nodes.length && accessors.length && isJsSourceFile(node.getSourceFile()) ) { @@ -6722,7 +6732,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou ) { lowerer.checkedVirtualJsAccessors.add(declaration); lowerer.checkedVirtualJsAccessors.add(member); - for (const twin of node.members) + for (const twin of node.members.nodes) if ( (ts.isGetAccessorDeclaration(twin) || ts.isSetAccessorDeclaration(twin)) && ts.isIdentifier(twin.name) && @@ -6737,13 +6747,15 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou }; if (instance) markAccessors(instance); } - const members = node.members.filter( + const members = node.members.nodes.filter( (member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && - !member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + !member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), ); - if (node.heritageClauses?.length && members.length > 0) { + if (node.heritageClauses?.nodes.length && members.length > 0) { const symbol = lowerer.typeOf(node).getSymbol(); const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; const seen = new Set(); @@ -6764,25 +6776,27 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou lowerer.virtualJsMethods.add(declaration); if ( isJsSourceFile(member.getSourceFile()) && - !member.typeParameters && + !member.typeParameters?.nodes && !member.asteriskToken && - !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && + !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && !bodyReadsArguments(member) && - member.parameters.every((p) => !p.dotDotDotToken) + member.parameters.nodes.every((p) => !p.dotDotDotToken) ) { lowerer.virtualJsMethodArity.set( declaration, Math.max( lowerer.virtualJsMethodArity.get(declaration) ?? 0, - member.parameters.length, + member.parameters.nodes.length, ), ); const checked = - !declaration.typeParameters && + !declaration.typeParameters?.nodes && !declaration.asteriskToken && - !declaration.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && + !declaration.modifiers?.nodes.some( + (m) => m.kind === ts.SyntaxKind.AsyncKeyword, + ) && !bodyReadsArguments(declaration) && - [...member.parameters, ...declaration.parameters].every( + [...member.parameters.nodes, ...declaration.parameters.nodes].every( (p) => !p.dotDotDotToken, ); if (checked) { @@ -6840,7 +6854,8 @@ function checkedJsOverride( ): boolean { return [base, member].every( (method) => - lowerer.checkedJsFile(method.getSourceFile()) && method.parameters.every(documentedParameter), + lowerer.checkedJsFile(method.getSourceFile()) && + method.parameters.nodes.every(documentedParameter), ); } @@ -6972,7 +6987,7 @@ export function lowerClassGenericMethodCall( loc, } : upcastTo(lowerer, receiver, declarer.def.name); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, @@ -7004,19 +7019,19 @@ export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrE * form with an exact static story under monomorphization (the * instantiation adopts the base's ABI; see collectClassShapeInner). */ function mixinForwardingCtor(lowerer: Lowerer, ctor: ts.ConstructorDeclaration): boolean { - if (ctor.parameters.length !== 1 || !ctor.body) return false; - const p = ctor.parameters[0]!; + if (ctor.parameters.nodes.length !== 1 || !ctor.body) return false; + const p = ctor.parameters.nodes[0]!; if (!p.dotDotDotToken || !ts.isIdentifier(p.name)) return false; const paramName = p.name; const paramSym = lowerer.checker.getSymbolAtLocation(paramName); if (!paramSym) return false; - const first = ctor.body.statements[0]; + const first = ctor.body.statements.nodes[0]; if (!first || !ts.isExpressionStatement(first) || !ts.isCallExpression(first.expression)) return false; const call = first.expression; if (call.expression.kind !== ts.SyntaxKind.SuperKeyword) return false; - if (call.arguments.length !== 1) return false; - const a = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return false; + const a = call.arguments.nodes[0]!; if (!ts.isSpreadElement(a) || !ts.isIdentifier(a.expression)) return false; const spreadIdent = a.expression; if (lowerer.checker.getSymbolAtLocation(spreadIdent) !== paramSym) return false; @@ -7173,7 +7188,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { // The default-param prologue runs FIRST — before field initializers // and (in a derived class) before super(): JS evaluates parameter // defaults on entry, ahead of everything the body does. - const declared = lowerer.declareParams(info.ctor.parameters, info.ctorParams); + const declared = lowerer.declareParams(info.ctor.parameters.nodes, info.ctorParams); params.push(...declared.params); if (info.ctorParams.at(-1)?.mode === "arguments") { const local = lowerer.declareHiddenLocal("%arguments", DYN); @@ -7188,7 +7203,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { // parameter property sees undefined). body.push(...lowerer.fieldInitStmts(info, thisLocal)); body.push(...paramPropInitStmts(lowerer, info, thisLocal)); - if (info.ctor.body) body.push(...lowerer.lowerStmts(info.ctor.body.statements)); + if (info.ctor.body) body.push(...lowerer.lowerStmts(info.ctor.body.statements.nodes)); } else if (info.ctor.body) { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); } @@ -7241,7 +7256,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { * private, static and abstract members are not prototype string keys. */ function classPrototypeNames(decl: ts.ClassLikeDeclaration): string[] { const names: string[] = []; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if ( !ts.isMethodDeclaration(member) && !ts.isGetAccessorDeclaration(member) && @@ -7685,7 +7700,7 @@ function lowerClassMethodMemberInner( const params: IrParam[] = [{ localId: thisLocal.id, name: "this", type: thisType }]; // `this` is declared first, so method parameter DEFAULTS may use it // (JS allows this in method defaults; it is param 0 here). - const declared = lowerer.declareParams(fnLike.parameters, sig.params); + const declared = lowerer.declareParams(fnLike.parameters.nodes, sig.params); params.push(...declared.params); // A shorter JS override still implements every inherited ABI slot. // These tail parameters have no source binding or default prologue. @@ -7695,7 +7710,7 @@ function lowerClassMethodMemberInner( params.push({ localId: local.id, name, type: shape.type }); if (shape.mode === "arguments") fnCtx.argumentsLocal = local; } - const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements)]; + const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements.nodes)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike)); const fn: IrFunction = { name: `%${className}.${mName}`, @@ -7730,7 +7745,7 @@ export function lowerStaticMethod( const entry = info.staticMethods?.get(name); if (!entry?.member.body) return null; const isAsync = - entry.member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + entry.member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = entry.member.asteriskToken !== undefined; if (isGenerator && (entry.ret.kind !== "generator" || (entry.ret.async === true) !== isAsync)) { lowerer.badType(entry.member.name, lowerer.typeOf(entry.member.name)); @@ -7757,13 +7772,13 @@ export function lowerStaticMethod( (keyword) => `'${keyword}' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead)`, ); - const declared = lowerer.declareParams(entry.member.parameters, entry.params); + const declared = lowerer.declareParams(entry.member.parameters.nodes, entry.params); if (entry.params.at(-1)?.mode === "arguments") { const local = lowerer.declareHiddenLocal("%arguments", DYN); declared.params.push({ localId: local.id, name: "%arguments", type: DYN }); fnCtx.argumentsLocal = local; } - const body = [...declared.prologue, ...lowerer.lowerStmts(entry.member.body.statements)]; + const body = [...declared.prologue, ...lowerer.lowerStmts(entry.member.body.statements.nodes)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(entry.member)); const fn: IrFunction = { name: `%${info.def.name}.static:${name}`, @@ -8008,7 +8023,7 @@ export function lowerDerivedCtorBody( }, ]; if (info.callableBase) { - if (superCall.arguments.some(ts.isSpreadElement)) + if (superCall.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1090", superCall, @@ -8016,14 +8031,14 @@ export function lowerDerivedCtorBody( ); const pack: IrExpr = { kind: "dynArrLit", - elems: superCall.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + elems: superCall.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; statements.push(lowerer.superCallStmt(info, thisLocal, [pack], loc)); } else { const base = superBaseOf(info)!; - if (base.builtinEmitter && superCall.arguments.length > 0) + if (base.builtinEmitter && superCall.arguments.nodes.length > 0) lowerer.unsupported( "SC1090", superCall, @@ -8035,7 +8050,7 @@ export function lowerDerivedCtorBody( lowerer, info, base, - superCall.arguments, + superCall.arguments.nodes, thisLocal, loc, superCall, @@ -8047,10 +8062,10 @@ export function lowerDerivedCtorBody( !base.builtinError && !base.builtinEmitter && base.ctorParams.length === 0 && - superCall.arguments.some(ts.isSpreadElement); + superCall.arguments.nodes.some(ts.isSpreadElement); if (surplusSpread) { const evaluated = lowerer.completeArgs( - superCall.arguments, + superCall.arguments.nodes, [{ mode: "dynRest", type: DYN }], loc, superCall, @@ -8062,20 +8077,20 @@ export function lowerDerivedCtorBody( !base.builtinError && !base.builtinEmitter && !base.ctorParams.some((param) => param.mode === "rest" || param.mode === "dynRest") && - !superCall.arguments.some(ts.isSpreadElement) && - superCall.arguments.length > base.ctorParams.length; + !superCall.arguments.nodes.some(ts.isSpreadElement) && + superCall.arguments.nodes.length > base.ctorParams.length; let args = surplusSpread ? [] : forward !== undefined ? forward : base.builtinError - ? lowerer.errorConstructorArgs(superCall.arguments, loc, superCall) + ? lowerer.errorConstructorArgs(superCall.arguments.nodes, loc, superCall) : base.builtinEmitter ? [] : lowerer.completeArgs( surplus - ? superCall.arguments.slice(0, base.ctorParams.length) - : superCall.arguments, + ? superCall.arguments.nodes.slice(0, base.ctorParams.length) + : superCall.arguments.nodes, base.ctorParams, loc, superCall, @@ -8086,7 +8101,7 @@ export function lowerDerivedCtorBody( statements.push({ kind: "varDecl", localId: local.id, init: arg, loc }); return { kind: "varRef", localId: local.id, type: arg.type, loc }; }); - for (const argument of superCall.arguments.slice(base.ctorParams.length)) { + for (const argument of superCall.arguments.nodes.slice(base.ctorParams.length)) { statements.push({ kind: "exprStmt", expr: lowerer.lowerExpr(argument), @@ -8130,7 +8145,7 @@ export function lowerDerivedCtorBody( }; }; try { - const result = lowerer.lowerStmts(info.ctor!.body!.statements); + const result = lowerer.lowerStmts(info.ctor!.body!.statements.nodes); result.push({ kind: "if", cond: { @@ -8332,7 +8347,7 @@ export function lowerSuperMethodCall( const instance = genericCallInstance(lowerer, call, gfound.info); const loc = locOf(call); const thisRef: IrExpr = { kind: "varRef", localId: thisL.id, type: thisL.type, loc }; - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, @@ -8395,7 +8410,7 @@ export function lowerSuperMethodCall( callee, receiver: lowerer.coerceToExpected(thisRef, DYN), calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8412,7 +8427,7 @@ export function lowerSuperMethodCall( }; } lowerer.noteEdge(`%${found.declarer.def.name}.${access.name.text}`); - const args = lowerer.completeArgs(call.arguments, found.sig.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call); return { kind: "call", callee: `%${found.declarer.def.name}.${access.name.text}`, @@ -8675,25 +8690,25 @@ function lowerProgramClassNew( // zero arguments (the options bag fences, like the super() form). // Stream subclasses come first: their chain roots at the emitter // too, but the message should name the stream story. - if (inheritsBuiltinStreamCtor(lowerer, info) && (expr.arguments ?? []).length > 0) { + if (inheritsBuiltinStreamCtor(lowerer, info) && (expr.arguments?.nodes ?? []).length > 0) { lowerer.noLowering( `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`, - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "declare a constructor that passes an inline options object to super(...)", ); } - if (lowerer.inheritsBuiltinEmitterCtor(info) && (expr.arguments ?? []).length > 0) { + if (lowerer.inheritsBuiltinEmitterCtor(info) && (expr.arguments?.nodes ?? []).length > 0) { lowerer.unsupported( "SC1090", - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "EventEmitter constructor options ('captureRejections')", ); } // A ctor-less chain into a builtin error base inherits both arguments; // each synthesized constructor forwards the same pair to its base. const args = lowerer.inheritsBuiltinErrorCtor(info) - ? lowerer.errorConstructorArgs(expr.arguments ?? [], loc, expr) - : lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); + ? lowerer.errorConstructorArgs(expr.arguments?.nodes ?? [], loc, expr) + : lowerer.completeArgs(expr.arguments?.nodes ?? [], info.ctorParams, loc, expr); for (let current: ClassInfo | null = info; current; current = current.base) { if (current.def.baseValueGlobal) return { @@ -8780,7 +8795,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { .declarationsOf(symbol) .find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); if (declaration && ts.isFunctionDeclaration(declaration) && declaration.body) { - const last = declaration.body.statements.at(-1); + const last = declaration.body.statements.nodes.at(-1); if ( last && ts.isReturnStatement(last) && @@ -8827,7 +8842,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { expr.expression.name.text === "Segmenter" && lowerer.isStdlibGlobal(expr.expression.expression, "Intl") ) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const undefinedArg = (arg: ts.Expression): boolean => (lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0; if (args.length > 2 || (args[0] && !undefinedArg(args[0]))) { @@ -8843,7 +8858,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { !undefinedArg(options) && !( ts.isObjectLiteralExpression(options) && - options.properties.every( + options.properties.nodes.every( (property) => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && @@ -8894,10 +8909,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { : memberSym; const emitterInfo = lowerer.builtinEmitterInfoOf(resolved); if (emitterInfo) { - if ((expr.arguments ?? []).length > 0) { + if ((expr.arguments?.nodes ?? []).length > 0) { lowerer.unsupported( "SC1090", - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "EventEmitter constructor options ('captureRejections')", ); } @@ -8919,7 +8934,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const zlibModeNs = zlibStreamModeOf(lowerer, resolved); if (zlibModeNs !== null) { const what = `new zlib.${zlibClassName(zlibModeNs)}`; - return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibModeNs, what, expr, loc); + return lowerZlibStreamNew(lowerer, expr.arguments?.nodes ?? [], zlibModeNs, what, expr, loc); } // `new N.C(...)` / `new a.Point(...)` — construction through a // USER namespace qualifier (import= alias chains included): the @@ -9005,7 +9020,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; below(info); const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc }; - const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); + const args = lowerer.completeArgs( + expr.arguments?.nodes ?? [], + info.ctorParams, + loc, + expr, + ); return { kind: "newValue", callee, @@ -9046,7 +9066,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const identifier = ts.isIdentifier(expr.expression) ? expr.expression : expr.expression.name; const symbol = ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null; if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Array")) { - return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); + return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } if (isNativeProxyInitializer(lowerer, expr)) return lowerNativeProxy(lowerer, expr); // `new Error(msg?)` and its standard subclasses: the @@ -9061,7 +9081,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `cause` own-property record) / "Error" for absent, and the legacy // numeric code from the name table. if (errInfo && errInfo.def.name === "%DOMException") { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering(`new DOMException with ${args.length} arguments`, expr); } @@ -9100,7 +9120,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (errInfo && errInfo.def.name === "%AssertionError") { // new AssertionError(options): Node validates the options object and // generates the message from operator, actual and expected. - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new AssertionError with this argument list`, expr); } @@ -9113,7 +9133,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; } if (errInfo && errInfo.def.name === "%SuppressedError") { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new SuppressedError with this argument list`, expr); } @@ -9128,7 +9148,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; } if (errInfo && errInfo.def.name === "%AggregateError") { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new AggregateError with this argument list`, expr); } @@ -9142,7 +9162,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; } if (errInfo) { - const args = lowerer.errorConstructorArgs(expr.arguments ?? [], loc, expr); + const args = lowerer.errorConstructorArgs(expr.arguments?.nodes ?? [], loc, expr); return { kind: "libCall", fn: "error.newOptions", @@ -9156,10 +9176,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // (@types/node's captureRejections) has no lowering. const emitterInfo = lowerer.builtinEmitterInfoOf(symbol); if (emitterInfo) { - if ((expr.arguments ?? []).length > 0) { + if ((expr.arguments?.nodes ?? []).length > 0) { lowerer.unsupported( "SC1090", - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "EventEmitter constructor options ('captureRejections')", ); } @@ -9180,7 +9200,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const zlibMode = zlibStreamModeOf(lowerer, symbol); if (zlibMode !== null) { const what = `new zlib.${zlibClassName(zlibMode)}`; - return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibMode, what, expr, loc); + return lowerZlibStreamNew(lowerer, expr.arguments?.nodes ?? [], zlibMode, what, expr, loc); } // `new URL(input)`: the WHATWG URL class (stdlib/@types provenance — // a user's own `class URL` resolves through classBySymbol below). @@ -9190,7 +9210,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // so bad input throws Node's catchable SyntaxError at construction. // Checked arguments retain native regexes and perform runtime coercion. if (symbol && symbol.name === "RegExp" && lowerer.isStdlibSymbol(symbol)) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering(`new RegExp with ${args.length} arguments`, expr); } @@ -9270,7 +9290,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // for every getter and toISOString; identity and setters stay // fenced, so copying the scalar cannot create an observable lie. if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement) || args.length > 1) { lowerer.noLowering( `new Date with ${args.length} arguments`, @@ -9324,7 +9344,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8, // Node's default. if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol); } @@ -9397,22 +9417,22 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // fence (SC2009, which names Map slots at value positions elsewhere). // Both Array() and new Array() share the elements/count lowering. if (symbol?.name === "Object" && lowerer.isStdlibSymbol(symbol)) { - const object = lowerObjectConstructor(lowerer, expr.arguments ?? [], loc); + const object = lowerObjectConstructor(lowerer, expr.arguments?.nodes ?? [], loc); if (object) return object; } if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) { - return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); + return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } // A const alias (`const M = Map`) constructs the same Map. if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Map")) { - const seedArg = (expr.arguments?.length ?? 0) === 1 ? expr.arguments![0]! : null; + const seedArg = (expr.arguments?.nodes.length ?? 0) === 1 ? expr.arguments!.nodes[0]! : null; const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression => ts.isArrayLiteralExpression(el) && - el.elements.length === 2 && - !el.elements.some(ts.isSpreadElement); + el.elements.nodes.length === 2 && + !el.elements.nodes.some(ts.isSpreadElement); const entriesLit = - seedArg && ts.isArrayLiteralExpression(seedArg) && seedArg.elements.every(isPairLit) - ? seedArg.elements.filter(isPairLit) + seedArg && ts.isArrayLiteralExpression(seedArg) && seedArg.elements.nodes.every(isPairLit) + ? seedArg.elements.nodes.filter(isPairLit) : null; let tsType = lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); @@ -9429,8 +9449,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `satisfies` checks a view but retains the expression's own type. const contextualSeed = !ts.isSatisfiesExpression(parent) && - (expr.typeArguments?.length ?? 0) === 0 && - (entriesLit !== null || (expr.arguments?.length ?? 0) === 0); + (expr.typeArguments?.nodes.length ?? 0) === 0 && + (entriesLit !== null || (expr.arguments?.nodes.length ?? 0) === 0); // JavaScript's `new Map()` has no type-argument syntax: the no-arg // constructor overload pins Map whatever the JSDoc says // (`@type` on the declaration types the VARIABLE, not this @@ -9449,7 +9469,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const seeded = lowerMapSeedNew(lowerer, seedArg, mapped); if (seeded) return seeded; } - if ((expr.arguments?.length ?? 0) > 0 && !entriesLit) { + if ((expr.arguments?.nodes.length ?? 0) > 0 && !entriesLit) { lowerer.noLowering( "new Map(entries)", expr, @@ -9460,11 +9480,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (mapped?.kind === "map") { if (!entriesLit) return { kind: "mapNew", type: mapped, loc }; const seed = entriesLit.map((pair) => ({ - key: lowerer.lowerCollectionKey(pair.elements[0]!, mapped.key), + key: lowerer.lowerCollectionKey(pair.elements.nodes[0]!, mapped.key), value: mapped.value.kind === "dyn" - ? lowerer.lowerCollectionKey(pair.elements[1]!, mapped.value) - : lowerer.lowerExprExpecting(pair.elements[1]!, mapped.value), + ? lowerer.lowerCollectionKey(pair.elements.nodes[1]!, mapped.value) + : lowerer.lowerExprExpecting(pair.elements.nodes[1]!, mapped.value), })); return { kind: "mapNew", seed, type: mapped, loc }; } @@ -9479,7 +9499,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // keeps the compile fence. if ( isJsSourceFile(expr.getSourceFile()) && - (expr.arguments?.length ?? 0) === 0 && + (expr.arguments?.nodes.length ?? 0) === 0 && targs.length > 0 && targs.every((t) => (t.flags & ts.TypeFlags.Any) !== 0) ) { @@ -9517,7 +9537,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && lowerer.isStdlibSymbol(symbol) ) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.unsupported( "SC1090", @@ -9541,22 +9561,22 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { let mapped = lowerer.mapTypeOf(tsType); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "set" && fieldType?.kind === "set") mapped = fieldType; - if (mapped?.kind === "set" && (expr.arguments?.length ?? 0) === 1) { - const seeded = lowerSetSeedNew(lowerer, expr.arguments![0]!, mapped); + if (mapped?.kind === "set" && (expr.arguments?.nodes.length ?? 0) === 1) { + const seeded = lowerSetSeedNew(lowerer, expr.arguments!.nodes[0]!, mapped); if (seeded) return seeded; } if ( !mapped && isJsSourceFile(expr.getSourceFile()) && - (expr.arguments?.length ?? 0) <= 1 && - !expr.arguments?.some(ts.isSpreadElement) + (expr.arguments?.nodes.length ?? 0) <= 1 && + !expr.arguments?.nodes.some(ts.isSpreadElement) ) { return { kind: "libCall", fn: "dyn.nativeSetNew", args: [ - expr.arguments?.[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + expr.arguments?.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), ], type: DYN, @@ -9571,12 +9591,14 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if ( !mapped && isJsSourceFile(expr.getSourceFile()) && - (expr.arguments?.length ?? 0) === 1 && - ts.isArrayLiteralExpression(expr.arguments![0]!) && - !(expr.arguments![0] as ts.ArrayLiteralExpression).elements.some(ts.isSpreadElement) + (expr.arguments?.nodes.length ?? 0) === 1 && + ts.isArrayLiteralExpression(expr.arguments!.nodes[0]!) && + !(expr.arguments!.nodes[0] as ts.ArrayLiteralExpression).elements.nodes.some( + ts.isSpreadElement, + ) ) { - const lit = expr.arguments![0] as ts.ArrayLiteralExpression; - const elems = lit.elements.map((el) => lowerer.lowerExpr(el)); + const lit = expr.arguments!.nodes[0] as ts.ArrayLiteralExpression; + const elems = lit.elements.nodes.map((el) => lowerer.lowerExpr(el)); const first = elems[0]; if ( first !== undefined && @@ -9588,7 +9610,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return { kind: "setNew", seed, type: setT, loc }; } } - if ((expr.arguments?.length ?? 0) > 0) { + if ((expr.arguments?.nodes.length ?? 0) > 0) { lowerer.noLowering( "new Set(values)", expr, @@ -9612,7 +9634,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // an f64 handle (type-mapper.ts), the Channel story. Construction options // ({ defaultValue, name }) have no lowering yet. if (symbol?.name === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol)) { - if ((expr.arguments?.length ?? 0) > 0) { + if ((expr.arguments?.nodes.length ?? 0) > 0) { lowerer.noLowering( "new AsyncLocalStorage(options)", expr, @@ -9625,7 +9647,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (symbol?.name === "Promise" && lowerer.isStdlibSymbol(symbol)) { const type = lowerer.irTypeOf(expr); if (type.kind !== "promise") lowerer.badType(expr, lowerer.typeOf(expr)); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length !== 1) { lowerer.unsupported("SC1090", expr, "Promise construction without an executor"); } @@ -9697,12 +9719,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // constructing forms would owe the deprecation warning. if ( expr.expression.text === "Buffer" && - expr.arguments?.length === 2 && - !expr.arguments.some(ts.isSpreadElement) && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind !== "string" && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "string" + expr.arguments?.nodes.length === 2 && + !expr.arguments.nodes.some(ts.isSpreadElement) && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "string" && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" ) { - const first = lowerer.lowerExpr(expr.arguments[0]!); + const first = lowerer.lowerExpr(expr.arguments.nodes[0]!); if ( first.kind === "unitLit" || first.type.kind === "dyn" || @@ -9714,7 +9736,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // throw only via the ctor body's later reads — it does NOT // observe it before throwing, so dropping it is exact for // effect-free operands; effectful ones keep the fence. - const enc = lowerer.lowerExpr(expr.arguments[1]!); + const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); if (enc.kind === "strLit" || isSafeToRepeat(enc)) { return { kind: "libCall", @@ -9778,7 +9800,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return bi?.module === "crypto" && bi.member === "X509Certificate"; })()); if (isX509) { - const args: readonly ts.Expression[] = expr.arguments ?? []; + const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; if (args.length !== 1) { lowerer.noLowering( "X509Certificate with this argument shape", @@ -9918,7 +9940,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // time (EventEmitter's options have no lowering at all). if ( (info.builtinStream !== undefined || info.builtinEmitter) && - (expr.arguments?.length ?? 0) > 0 + (expr.arguments?.nodes.length ?? 0) > 0 ) { lowerer.unsupported( "SC1090", @@ -9927,7 +9949,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); } defineClassValueConstruct(lowerer, info); - const args = lowerer.completeArgs(expr.arguments ?? [], classValueParams(info), loc, expr); + const args = lowerer.completeArgs( + expr.arguments?.nodes ?? [], + classValueParams(info), + loc, + expr, + ); const built: IrExpr = { kind: "newValue", callee, @@ -10012,7 +10039,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { function lowerCheckedConstructor(lowerer: Lowerer, expr: ts.NewExpression, callee: IrExpr): IrExpr { const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) { const packed = lowerer.completeArgs(args, [{ type: DYN, mode: "dynRest" }], loc, expr)[0]!; return checkedClassConstructionPacked(lowerer, callee, packed, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts index 71b9183d5..f811d13b6 100644 --- a/packages/compiler/src/frontend/lowering/lower-comptime.ts +++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts @@ -50,12 +50,12 @@ function describeComptimeValue(v: unknown): string { export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // 1. Inline function literal only (tsc guarantees the arity/type; the // literal-ness is our requirement — source extraction needs the source). - let cb: ts.Expression | undefined = expr.arguments[0]; + let cb: ts.Expression | undefined = expr.arguments.nodes[0]; while (cb && ts.isParenthesizedExpression(cb)) cb = cb.expression; if (!cb || !(ts.isArrowFunction(cb) || ts.isFunctionExpression(cb))) { lowerer.unsupported( "SC1090", - expr.arguments[0] ?? expr, + expr.arguments.nodes[0] ?? expr, "comptime arguments other than inline function literals " + "(write the callback directly in the call: comptime(() => ...))", ); @@ -63,7 +63,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr // 2. Async/generator callbacks have no bakeable result. if ( (ts.isFunctionExpression(cb) && cb.asteriskToken) || - cb.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) + cb.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) { lowerer.unsupported("SC1090", cb, "async or generator comptime callbacks"); } diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index 20f5129b4..a7fac4f51 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -67,11 +67,11 @@ type OptionRole = "stdout" | "stderr" | "ignoreErrors"; export function lowerConsoleNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!isConsoleCtor(lowerer, expr.expression)) return null; const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const entries: { role: OptionRole; node: ts.Expression | undefined }[] = []; const first = args[0]; if (first && ts.isObjectLiteralExpression(first)) { - for (const prop of first.properties) { + for (const prop of first.properties.nodes) { let name: string | null = null; let value: ts.Expression | null = null; if ( @@ -349,11 +349,15 @@ export function lowerConsoleOutput( // formatter IS util.format — route through the format lowering and // print its one string. Specifier-free first strings keep the // plain space-joined path below (identical output, cheaper). - const first = expr.arguments[0]; - if (expr.arguments.length > 1 && isStringLiteralLike(first) && hasFormatSpecifier(first.text)) { + const first = expr.arguments.nodes[0]; + if ( + expr.arguments.nodes.length > 1 && + isStringLiteralLike(first) && + hasFormatSpecifier(first.text) + ) { return consoleWrite(stdout, [lowerFormatCall(lowerer, expr, loc, false)], loc); } - const args = expr.arguments.map((a) => + const args = expr.arguments.nodes.map((a) => lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc), ); return consoleWrite(stdout, args, loc); @@ -479,9 +483,9 @@ function lowerConsoleTimeLog( loc: SrcLoc, ): IrExpr { const stmts: IrStmt[] = []; - const raw = rawLabel(lowerer, member, expr.arguments[0]); + const raw = rawLabel(lowerer, member, expr.arguments.nodes[0]); const rawRef = raw === null ? null : hoist(lowerer, stmts, "%consoleLabel", raw, loc); - const data = expr.arguments.slice(1); + const data = expr.arguments.nodes.slice(1); const dataRefs: { node: ts.Expression; value: IrExpr }[] = []; if (member === "timeLog") { for (const node of data) { @@ -546,7 +550,7 @@ function lowerConsoleTimeLog( * so a format string still formats. */ function lowerConsoleAssert(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const stmts: IrStmt[] = []; - const [valueNode, ...args] = expr.arguments; + const [valueNode, ...args] = expr.arguments.nodes; const ok = valueNode === undefined ? boolLit(false, loc) @@ -635,7 +639,7 @@ function lowerConsoleAssert(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcL * in the runtime (cli_table's layout over util.inspect cells). */ function lowerConsoleTable(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const stmts: IrStmt[] = []; - const [dataNode, propsNode, ...rest] = expr.arguments; + const [dataNode, propsNode, ...rest] = expr.arguments.nodes; if (dataNode === undefined) return consoleWrite(true, [strLit("undefined", loc)], loc); let data = lowerer.lowerExpr(dataNode); let props = @@ -707,7 +711,7 @@ export function lowerConsoleMethodCall( member: ConsoleMethodMember, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; switch (member) { case "dirxml": return lowerConsoleOutput(lowerer, expr, true, "console.dirxml", loc); diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 06c83da44..54f9d158b 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -132,7 +132,7 @@ export function lowerDgramDnsModuleCall( ): IrExpr | null { if (bi.module === "dns") return lowerDnsModuleCall(lowerer, expr, bi, loc); if (bi.module !== "dgram") return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createSocket") { if (args.length !== 1) { lowerer.noLowering( @@ -172,7 +172,7 @@ export function lowerDgramDnsModuleCall( } let sawType = false; let reuseAddr: IrExpr | null = null; - for (const prop of arg.properties) { + for (const prop of arg.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { lowerer.noLowering( "createSocket options with computed keys, spreads, or shorthand entries", @@ -289,7 +289,7 @@ function lowerDnsModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; const dyn = (node: ts.Expression): IrExpr => lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN); // A callback whose second parameter is typed (string, LookupAddress[]) @@ -382,7 +382,7 @@ export function lowerDgramMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "bind" || name === "connect") { requireStatementPosition(lowerer, call, `socket.${name}(...)`); // bind(port[, address][, cb]) / connect(port[, address][, cb]) — the @@ -675,7 +675,7 @@ export function lowerPromisifiedDnsCall( member: string, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`the promisified dns.${member} with spread arguments`, expr); } diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts index d9a30028e..03e22e684 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -96,7 +96,7 @@ export function lowerEnumAccess( // member (merged declarations scan in declaration order). if (ts.isStringLiteralLike(key)) { for (const d of decls) { - for (const member of d.members) { + for (const member of d.members.nodes) { if (memberNameText(member) === key.text) return lowerEnumMemberRead(lowerer, expr, member); } } @@ -191,7 +191,7 @@ function lowerEnumReverseRead( } let name: string | null = null; for (const d of decls) { - for (const member of d.members) { + for (const member of d.members.nodes) { const v = lowerer.checker.getConstantValue(member); if (typeof v === "number" && v === idx) name = memberNameText(member); } @@ -242,7 +242,7 @@ function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null { * the declaration fences on that member. */ export function lowerEnumDeclaration(lowerer: Lowerer, stmt: ts.EnumDeclaration): null { if (isAmbientEnumDecl(stmt)) return null; - for (const member of stmt.members) { + for (const member of stmt.members.nodes) { if (lowerer.checker.getConstantValue(member) === undefined) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index e73bbd4ce..b8297fe91 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -288,7 +288,7 @@ function emitterEvents(lowerer: Lowerer): Map { const member = node.expression.name.text; const isEmit = member === "emit"; if (!isEmit && !REGISTER_MEMBERS.has(member)) return; - const arg0 = node.arguments[0]; + const arg0 = node.arguments.nodes[0]; if (!arg0) return; let recvT: IrType | null = null; let nameT: ts.Type | null = null; @@ -327,12 +327,13 @@ function emitterEvents(lowerer: Lowerer): Map { if (isEmit) { mergeEmit( name, - node.arguments.slice(1).map((a) => lowerer.mapTypeOf(lowerer.typeOf(a))), + node.arguments.nodes.slice(1).map((a) => lowerer.mapTypeOf(lowerer.typeOf(a))), isJsSourceFile(sf), ); - } else if (node.arguments[1]) { - const cbCt = lowerer.typeOf(node.arguments[1]); - const cbT = lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, node.arguments[1], cbCt); + } else if (node.arguments.nodes[1]) { + const cbCt = lowerer.typeOf(node.arguments.nodes[1]); + const cbT = + lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, node.arguments.nodes[1], cbCt); if ( cbT?.kind !== "dyn" && (cbT?.kind !== "func" || cbT.params.some((p) => p.kind !== "dyn")) @@ -503,7 +504,7 @@ function staticStringSuffixOf( while (ts.isParenthesizedExpression(expression)) expression = expression.expression; if (ts.isStringLiteralLike(expression)) return expression.text; if (ts.isTemplateExpression(expression)) - return expression.templateSpans.at(-1)?.literal.text ?? ""; + return expression.templateSpans.nodes.at(-1)?.literal.text ?? ""; if (ts.isIdentifier(expression)) { const symbol = lowerer.resolveValueSymbol(expression); if (!symbol || seen.has(symbol)) return null; @@ -935,8 +936,11 @@ function lowerListenerArg( ): IrExpr { // Unannotated non-empty parameter lists have no static types (checker // `any`; dyn in JS sources) — say so before the blanket type fence speaks. - if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.parameters.length > 0) { - for (const p of node.parameters) { + if ( + (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && + node.parameters.nodes.length > 0 + ) { + for (const p of node.parameters.nodes) { if (p.type === undefined && ts.isIdentifier(p.name)) { const mapped = (() => { try { @@ -1009,7 +1013,7 @@ export function lowerEmitterMethodCall( ): IrExpr | null { const member = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const lowerReceiver = (): IrExpr => { if (superRecv) return superRecv.thisRef; const receiver = lowerer.lowerExpr(access.expression); @@ -1643,22 +1647,22 @@ export function emitOverrideShapeReason( if ((member as { questionToken?: ts.Node }).questionToken !== undefined) return "optional method declarations have no lowering"; if (member.typeParameters !== undefined) return "the compiled shape declares no type parameters"; - for (const m of member.modifiers ?? []) { + for (const m of member.modifiers?.nodes ?? []) { if (m.kind === ts.SyntaxKind.AsyncKeyword) return "async methods answer a Promise, not emit's boolean"; if (m.kind === ts.SyntaxKind.AbstractKeyword) return "abstract declarations have no body to lower"; if (m.kind === ts.SyntaxKind.Decorator) return "decorated methods have no lowering"; } - if (member.parameters.length !== 2) return "it must declare exactly (event, ...args)"; - const p0 = member.parameters[0]!; - const p1 = member.parameters[1]!; + if (member.parameters.nodes.length !== 2) return "it must declare exactly (event, ...args)"; + const p0 = member.parameters.nodes[0]!; + const p1 = member.parameters.nodes[1]!; if ( !ts.isIdentifier(p0.name) || p0.dotDotDotToken !== undefined || p0.questionToken !== undefined || p0.initializer !== undefined || - (p0.modifiers?.length ?? 0) > 0 + (p0.modifiers?.nodes.length ?? 0) > 0 ) { return "the event parameter must be a plain identifier"; } @@ -1666,7 +1670,7 @@ export function emitOverrideShapeReason( !p1.dotDotDotToken || !ts.isIdentifier(p1.name) || p1.initializer !== undefined || - (p1.modifiers?.length ?? 0) > 0 + (p1.modifiers?.nodes.length ?? 0) > 0 ) { return "the second parameter must be a plain rest parameter (...args)"; } @@ -1732,7 +1736,11 @@ function isEmitForwardSpread( const sp = id.parent; if (!ts.isSpreadElement(sp) || sp.expression !== id) return false; const call = sp.parent; - if (!ts.isCallExpression(call) || call.arguments.length !== 2 || call.arguments[1] !== sp) + if ( + !ts.isCallExpression(call) || + call.arguments.nodes.length !== 2 || + call.arguments.nodes[1] !== sp + ) return false; const callee = call.expression; if ( @@ -1742,7 +1750,7 @@ function isEmitForwardSpread( ) { return false; } - const a0 = call.arguments[0]!; + const a0 = call.arguments.nodes[0]!; if (!ts.isIdentifier(a0)) return false; try { if (lowerer.checker.getSymbolAtLocation(a0) !== eventSym) return false; @@ -1893,7 +1901,7 @@ export function lowerEmitOverrideSpec(lowerer: Lowerer, req: EmitSpecRequest): I params.push({ localId: p.id, name: `%ee${i}`, type: t }); return p; }); - const eventDecl = ov.decl.parameters[0]!; + const eventDecl = ov.decl.parameters.nodes[0]!; const eventLocal = lowerer.declareLocal( eventDecl.name, ts.isIdentifier(eventDecl.name) ? eventDecl.name.text : "event", @@ -1911,7 +1919,7 @@ export function lowerEmitOverrideSpec(lowerer: Lowerer, req: EmitSpecRequest): I restSym: ov.restSym, tupleParams, }; - const body = [...prologue, ...lowerer.lowerStmts(ov.decl.body.statements)]; + const body = [...prologue, ...lowerer.lowerStmts(ov.decl.body.statements.nodes)]; return { name: `%${className}.emit:${event}`, params, @@ -1940,9 +1948,9 @@ export function emitSpecSuperForward( access: ts.PropertyAccessExpression, ): IrExpr | null { const ctx = lowerer.emitSpecCtx; - if (!ctx || access.name.text !== "emit" || call.arguments.length !== 2) return null; - const a0 = call.arguments[0]!; - const a1 = call.arguments[1]!; + if (!ctx || access.name.text !== "emit" || call.arguments.nodes.length !== 2) return null; + const a0 = call.arguments.nodes[0]!; + const a1 = call.arguments.nodes[1]!; if (!ts.isIdentifier(a0) || !ts.isSpreadElement(a1) || !ts.isIdentifier(a1.expression)) return null; if (lowerer.checker.getSymbolAtLocation(a0) !== ctx.eventSym) return null; @@ -2192,7 +2200,7 @@ function eventsOnceBoxer( * array. EventTarget and stream receivers, computed names and options are * refused. */ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering("events.once without an emitter and an event name", call); } @@ -2302,7 +2310,7 @@ export function lowerEventsEmitterHelper( member: string, loc: SrcLoc, ): IrExpr | null { - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`events.${member} with spread arguments`, call); } diff --git a/packages/compiler/src/frontend/lowering/lower-event-target.ts b/packages/compiler/src/frontend/lowering/lower-event-target.ts index e6230009d..79bd59968 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-target.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-target.ts @@ -274,13 +274,13 @@ export function lowerEventTargetMethodCall( ): IrExpr { const member = access.name.text; const loc = locOf(call); - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with spread arguments`, call); } const target = eventTargetRootOf(info) === "target"; if (target) noteEventListeners(lowerer, loc); const { stmts, ref } = singleReceiver(lowerer, receiverExpr, loc); - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => target && index === 1 && member !== "dispatchEvent" ? lowerer.lowerLiveWebValue(arg) : lowerer.lowerExprExpecting(arg, DYN), diff --git a/packages/compiler/src/frontend/lowering/lower-expando.ts b/packages/compiler/src/frontend/lowering/lower-expando.ts index 218cc61f6..5e4c43021 100644 --- a/packages/compiler/src/frontend/lowering/lower-expando.ts +++ b/packages/compiler/src/frontend/lowering/lower-expando.ts @@ -247,7 +247,7 @@ function nsOwnedMember( ) { if (ts.isModuleDeclaration(p)) { return !( - p.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) || + p.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) || p.getSourceFile().isDeclarationFile ); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d46226384..09407f71f 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -539,7 +539,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const pieces: IrExpr[] = []; const headRaw = templateRawTextOf(t.head); if (headRaw !== "") pieces.push({ kind: "strLit", value: headRaw, type: STRING, loc }); - for (const span of t.templateSpans) { + for (const span of t.templateSpans.nodes) { pieces.push( lowerer.caughtToString(span.expression) ?? lowerer.ensureString(lowerer.lowerExpr(span.expression), span.expression), @@ -2226,7 +2226,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { fn = ts.isSourceFile(fn) ? undefined : fn.parent; } if (fn !== undefined) { - const fixedArity = (fn as ts.FunctionLikeDeclaration).parameters.every( + const fixedArity = (fn as ts.FunctionLikeDeclaration).parameters.nodes.every( (p) => p.questionToken === undefined && p.initializer === undefined && @@ -2235,7 +2235,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (fixedArity) { return { kind: "numLit", - value: (fn as ts.FunctionLikeDeclaration).parameters.length, + value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.length, type: F64, loc, }; @@ -2994,11 +2994,11 @@ export function lowerTernary( if ( ts.isCallExpression(c) && ts.isPropertyAccessExpression(c.expression) && - c.arguments.length === 1 && - ts.isIdentifier(c.arguments[0]!) && + c.arguments.nodes.length === 1 && + ts.isIdentifier(c.arguments.nodes[0]!) && lowerer.stdlibGlobalMember(c.expression, "Array") === "isArray" ) { - const argIdent = c.arguments[0] as ts.Identifier; + const argIdent = c.arguments.nodes[0] as ts.Identifier; const sym = lowerer.resolveValueSymbol(argIdent); const t = lowerer.checker.getTypeAtLocation(argIdent); const constituents = t.isUnionType() ? ts.constituentTypes(t) : []; @@ -3072,7 +3072,7 @@ export function lowerTernary( }; const emptyUntypedArrayArm = (e: ts.Expression): boolean => { const lit = armLiteral(e); - if (!lit || lit.elements.length !== 0) return false; + if (!lit || lit.elements.nodes.length !== 0) return false; const t = lowerer.checker.getContextualType(lit) ?? lowerer.typeOf(lit); if (!lowerer.mapTypeOf(t)) return true; // never[] MAPS (the f64 representation for the uninhabited) but @@ -4057,7 +4057,7 @@ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): const call = arg.parent; if ( !ts.isCallExpression(call) || - call.arguments[0] !== arg || + call.arguments.nodes[0] !== arg || !ts.isPropertyAccessExpression(call.expression) ) return false; @@ -4111,7 +4111,7 @@ function assertedElementKeepsUndefined(lowerer: Lowerer, node: ts.NonNullExpress lowerer.checker.isArrayType(lowerer.typeOf(receiver)); if ( ts.isCallExpression(parent) && - parent.arguments.includes(outer) && + parent.arguments.nodes.includes(outer) && ts.isPropertyAccessExpression(parent.expression) && (parent.expression.name.text === "push" || parent.expression.name.text === "unshift") && isArray(parent.expression.expression) @@ -5855,11 +5855,11 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean return stored?.kind === "dyn" && (lowerer.typeOf(expr).flags & ts.TypeFlags.Object) !== 0; } if (ts.isArrayLiteralExpression(expr)) - return expr.elements.some( + return expr.elements.nodes.some( (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element), ); if (ts.isObjectLiteralExpression(expr)) - return expr.properties.some( + return expr.properties.nodes.some( (property) => (ts.isPropertyAssignment(property) && jsHoldsDynObject(lowerer, property.initializer)) || (ts.isShorthandPropertyAssignment(property) && @@ -5955,7 +5955,7 @@ export function lowerArrayLiteral( // no ambiguity: build the literal as that arm and let the slot's // coercion wrap it. Two array arms would need tsc's own best-fit // choice — none arises in practice; the fence stands. - if (ctxUnion && expr.elements.length === 0) { + if (ctxUnion && expr.elements.nodes.length === 0) { const def = lowerer.unions.get(ctxUnion.unionId); const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? []; if (arrayArms.length === 1) mapped = arrayArms[0]!; @@ -5967,11 +5967,11 @@ export function lowerArrayLiteral( // arity-matching tuple (the option-table `default: [{ value: [] }]` // shape): build AS that arm, exactly the empty-literal rule above; // the slot's coercion wraps it. - if (ctxUnion && expr.elements.length > 0) { + if (ctxUnion && expr.elements.nodes.length > 0) { // A unit-only-element array (`(null | undefined)[]`) cannot hold // non-unit elements — neither as the literal's own mapping nor as // a competing union arm. - const nonUnitElems = expr.elements.some( + const nonUnitElems = expr.elements.nodes.some( (el) => !ts.isOmittedExpression(el) && el.kind !== ts.SyntaxKind.NullKeyword && @@ -5990,7 +5990,7 @@ export function lowerArrayLiteral( (a.kind === "array" && !(nonUnitElems && lowerer.unitOnlyElem(a.elem))) || (a.kind === "record" && !!lowerer.shapes.get(a.shapeId)?.tuple && - lowerer.shapes.get(a.shapeId)!.fields.length === expr.elements.length), + lowerer.shapes.get(a.shapeId)!.fields.length === expr.elements.nodes.length), ); if (arms.length === 1) mapped = arms[0]!; } @@ -6002,7 +6002,7 @@ export function lowerArrayLiteral( !expected && mapped?.kind === "array" && mapped.elem.kind === "string" && - expr.elements.some((element) => isDateJsonSource(lowerer, element)) + expr.elements.nodes.some((element) => isDateJsonSource(lowerer, element)) ) mapped = { kind: "array", elem: dateJsonType(lowerer) }; // An array literal inside an npm-static implicit-any instance can @@ -6018,12 +6018,12 @@ export function lowerArrayLiteral( (mapped === null || mapped.kind === "dyn" || (mapped.kind === "array" && mapped.elem.kind === "dyn")) && - expr.elements.length > 0 && - expr.elements.every( + expr.elements.nodes.length > 0 && + expr.elements.nodes.every( (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { - const elements = expr.elements.map((element) => tryLowerExpression(lowerer, element)); + const elements = expr.elements.nodes.map((element) => tryLowerExpression(lowerer, element)); const first = elements[0]; if ( first !== undefined && @@ -6047,7 +6047,7 @@ export function lowerArrayLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.elements.some( + expr.elements.nodes.some( (element) => ts.isSpreadElement(element) && tryLowerExpression(lowerer, element.expression)?.type.kind === "dyn", @@ -6061,7 +6061,7 @@ export function lowerArrayLiteral( !expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind !== "dyn" && - expr.elements.some( + expr.elements.nodes.some( (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element), ) ) @@ -6071,7 +6071,7 @@ export function lowerArrayLiteral( // mapType's own `[] as const` rule. if ( mapped === null && - expr.elements.length === 0 && + expr.elements.nodes.length === 0 && (tsType.flags & ts.TypeFlags.Any) !== 0 && underConstAssertion(expr) ) { @@ -6084,7 +6084,7 @@ export function lowerArrayLiteral( isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array" && mapped.elem.kind === "union" && - expr.elements.some(ts.isObjectLiteralExpression) + expr.elements.nodes.some(ts.isObjectLiteralExpression) ) mapped = DYN; @@ -6096,11 +6096,11 @@ export function lowerArrayLiteral( if (mapped?.kind === "record") { const shape = lowerer.shapes.get(mapped.shapeId); if (shape?.tuple) { - const spread = expr.elements.find(ts.isSpreadElement); + const spread = expr.elements.nodes.find(ts.isSpreadElement); if (spread) { return lowerTupleSpreadLiteral(lowerer, expr, mapped, shape); } - if (expr.elements.length !== shape.fields.length) { + if (expr.elements.nodes.length !== shape.fields.length) { // tsc padded an UNDER-LENGTH literal against an optional-element // tuple context (`options || []` with `options?: [string?, // number?]` — the instantiated type spells every position, the @@ -6108,7 +6108,7 @@ export function lowerArrayLiteral( lowerer.badType(expr, tsType); } const byName = new Map(shape.fields.map((f) => [f.name, f.type])); - const fields = expr.elements.map((el, i) => { + const fields = expr.elements.nodes.map((el, i) => { const fieldType = byName.get(String(i)); if (!fieldType) lowerer.badType(el, lowerer.typeOf(el)); return { name: String(i), value: lowerer.lowerExprExpecting(el, fieldType) }; @@ -6132,14 +6132,14 @@ export function lowerArrayLiteral( mapped?.kind === "dyn" || lowerer.checkerAnyArray(expr) ) { - if (expr.elements.some(ts.isSpreadElement)) { + if (expr.elements.nodes.some(ts.isSpreadElement)) { // Native collection drains return vectors of retained dyn // elements. Reuse the ordinary spread builder, then expose the // resulting array through the established unknown[] representation. const vector = lowerArrayLiteral(lowerer, expr, arrayOf(DYN) as IrType & { kind: "array" }); return lowerer.coerceInto(expr, vector, DYN); } - const elems = expr.elements.map((el): IrExpr => { + const elems = expr.elements.nodes.map((el): IrExpr => { if (ts.isSpreadElement(el)) { lowerer.unsupported( "SC1090", @@ -6167,7 +6167,7 @@ export function lowerArrayLiteral( // build that layout directly. An explicit destination keeps its own. if (!expected && type.elem.kind === "record") { let variant: IrType | null = null; - for (const candidate of expr.elements) { + for (const candidate of expr.elements.nodes) { let x: ts.Expression = candidate; while (ts.isParenthesizedExpression(x)) x = x.expression; if (!ts.isObjectLiteralExpression(x)) continue; @@ -6189,8 +6189,8 @@ export function lowerArrayLiteral( const holes = new Set(); const runtimeOptionalElements = new Set(); let needsStateBuild = false; - for (let i = 0; i < expr.elements.length; i++) { - const candidate = expr.elements[i]!; + for (let i = 0; i < expr.elements.nodes.length; i++) { + const candidate = expr.elements.nodes[i]!; if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate)) continue; const probe = tryLowerExpression(lowerer, candidate); if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) { @@ -6198,7 +6198,7 @@ export function lowerArrayLiteral( needsStateBuild = true; } } - const elems: IrExpr[] = expr.elements.map((el, i): IrExpr => { + const elems: IrExpr[] = expr.elements.nodes.map((el, i): IrExpr => { if (ts.isSpreadElement(el)) { // `[...xs, b]`: xs must be an array of the literal's own element // type — its elements copy in at construction (a fresh array, @@ -6443,7 +6443,7 @@ function lowerTupleSpreadLiteral( stmts.push({ kind: "varDecl", localId: temp.id, init: coerced, loc: at }); fields.push({ name, value: varRef(temp.id, expected, at) }); }; - for (const element of expr.elements) { + for (const element of expr.elements.nodes) { if (!ts.isSpreadElement(element)) { const expected = byName.get(String(fields.length)); if (!expected) lowerer.badType(element, lowerer.typeOf(element)); @@ -9277,7 +9277,7 @@ export function stringWrapperToString(lowerer: Lowerer, node: ts.Expression): Ir if (!ts.isNewExpression(e) || !ts.isIdentifier(e.expression)) return null; const symbol = lowerer.resolveValueSymbol(e.expression); if (symbol?.name !== "String" || !lowerer.isStdlibSymbol(symbol)) return null; - const args = e.arguments ?? []; + const args = e.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) return null; if (args.length === 0) return { kind: "strLit", value: "", type: STRING, loc: locOf(e) }; const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, args[0]!); @@ -9300,7 +9300,7 @@ export function lowerTemplate(lowerer: Lowerer, expr: ts.TemplateExpression): Ir if (expr.head.text !== "") { pieces.push({ kind: "strLit", value: expr.head.text, type: STRING, loc }); } - for (const span of expr.templateSpans) { + for (const span of expr.templateSpans.nodes) { pieces.push( stringWrapperToString(lowerer, span.expression) ?? lowerer.caughtToString(span.expression) ?? @@ -9346,7 +9346,7 @@ export function lowerAsExpression( ts.isAsExpression(expr) && isConstAssertionTypeNode(expr.type) && ts.isArrayLiteralExpression(expr.expression) && - expr.expression.elements.length === 0 + expr.expression.elements.nodes.length === 0 ) { return { kind: "arrayLit", @@ -12512,7 +12512,7 @@ function lowerReplacedExportsRead( return null; } let replaced = false; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (!cjs) continue; if (cjs.kind === "table" && cjsExportDiscardReason(stmt) === null) replaced = true; @@ -12599,7 +12599,7 @@ function lowerCjsExportTableThisMember( ) { return null; } - const sibling = n.parent.properties.find( + const sibling = n.parent.properties.nodes.find( (p): p is ts.GetAccessorDeclaration => ts.isGetAccessorDeclaration(p) && ts.isIdentifier(p.name) && p.name.text === expr.name.text, ); @@ -13556,7 +13556,7 @@ function lowerPrivateIn( let staticBrand = false; for (let n: ts.Node | undefined = priv.parent; n; n = n.parent) { if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) { - const owner = n.members.find((m) => { + const owner = n.members.nodes.find((m) => { const name = (m as { name?: ts.PropertyName }).name; return name !== undefined && ts.isPrivateIdentifier(name) && name.text === pname; }); @@ -14411,11 +14411,11 @@ function staticRegexPatternOf(lowerer: Lowerer, e: ts.Expression): string | null expr.expression.text === "RegExp" && lowerer.isStdlibGlobal(expr.expression, "RegExp") && expr.arguments !== undefined && - expr.arguments.length >= 1 && - (ts.isStringLiteral(expr.arguments[0]!) || - ts.isNoSubstitutionTemplateLiteral(expr.arguments[0]!)) + expr.arguments.nodes.length >= 1 && + (ts.isStringLiteral(expr.arguments.nodes[0]!) || + ts.isNoSubstitutionTemplateLiteral(expr.arguments.nodes[0]!)) ) { - return (expr.arguments[0] as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text; + return (expr.arguments.nodes[0] as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text; } if (ts.isIdentifier(expr)) { const init = constInitializerOf(lowerer, expr); @@ -14489,7 +14489,7 @@ function staticRegexFlagsOf(lowerer: Lowerer, node: ts.Expression): string | nul ts.isIdentifier(expr.expression) && lowerer.isStdlibGlobal(expr.expression, "RegExp") ) { - const flags = expr.arguments?.[1]; + const flags = expr.arguments?.nodes[1]; if (!flags) return ""; if (ts.isStringLiteral(flags) || ts.isNoSubstitutionTemplateLiteral(flags)) return flags.text; } @@ -14515,17 +14515,17 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) { const name = expr.expression.name.text; const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression)); - if (name === "exec" && expr.arguments.length === 1 && recvT?.kind === "regex") { + if (name === "exec" && expr.arguments.nodes.length === 1 && recvT?.kind === "regex") { return expr.expression.expression; } if ( name === "match" && - expr.arguments.length === 1 && + expr.arguments.nodes.length === 1 && (recvT?.kind === "string" || (recvT !== null && recvT !== undefined && nullableStringType(lowerer, recvT))) && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "regex" + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "regex" ) { - return expr.arguments[0]!; + return expr.arguments.nodes[0]!; } } // A matchAll ROW by element access: rows[i] with rows tracing to the @@ -14592,7 +14592,7 @@ function matchAllCallbackRegexOf( param: ts.ParameterDeclaration, ): ts.Expression | null { const fn = param.parent; - if (!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.parameters[0] !== param) + if (!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.parameters.nodes[0] !== param) return null; // A reassigned parameter can hold a row of another regex. if (ts.isIdentifier(param.name)) { @@ -14606,17 +14606,17 @@ function matchAllCallbackRegexOf( const method = call.expression.name.text; if ( MATCH_ROW_CALLBACK_METHODS.has(method) && - call.arguments[0] === site && + call.arguments.nodes[0] === site && lowerer.isStdlibMember(call.expression) ) return matchAllRegexOf(lowerer, call.expression.expression); if ( method === "from" && - call.arguments[1] === site && + call.arguments.nodes[1] === site && lowerer.isStdlibGlobal(call.expression.expression, "Array") && - call.arguments[0] !== undefined + call.arguments.nodes[0] !== undefined ) - return matchAllRegexOf(lowerer, call.arguments[0]); + return matchAllRegexOf(lowerer, call.arguments.nodes[0]); return null; } @@ -14645,15 +14645,15 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "matchAll" && - expr.arguments.length === 1 && + expr.arguments.nodes.length === 1 && lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression))?.kind === "string" && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "regex" + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "regex" ) { - return expr.arguments[0]!; + return expr.arguments.nodes[0]!; } // The eager-spread spelling: `[...s.matchAll(re)]`. - if (ts.isArrayLiteralExpression(expr) && expr.elements.length === 1) { - const only = expr.elements[0]!; + if (ts.isArrayLiteralExpression(expr) && expr.elements.nodes.length === 1) { + const only = expr.elements.nodes[0]!; if (ts.isSpreadElement(only)) return matchAllRegexOf(lowerer, only.expression); } if ( @@ -15818,10 +15818,10 @@ export function uniqueSymbolKeyOf( if (!ts.isIdentifier(init.expression) || init.expression.text !== "Symbol") return null; if (!lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(init.expression))) return null; const arg = - init.arguments.length === 0 + init.arguments.nodes.length === 0 ? null - : init.arguments.length === 1 - ? init.arguments[0]! + : init.arguments.nodes.length === 1 + ? init.arguments.nodes[0]! : undefined; if (arg === undefined) return null; if (arg !== null && !ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) @@ -16356,7 +16356,7 @@ export function lowerPromiseRejectCall( if (call.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Promise") !== "reject") return null; const loc = locOf(call); - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( "Promise.reject with this argument count", call, @@ -16369,7 +16369,7 @@ export function lowerPromiseRejectCall( // returning `T | Promise`) names its one promise arm. const maybePromise = contextual ? maybePromiseUnionOf(lowerer, contextual) : null; const type = maybePromise ? maybePromise.promise : contextual; - const reasonNode = call.arguments[0]!; + const reasonNode = call.arguments.nodes[0]!; // A CHECKED-DYNAMIC reason (`Promise.reject(value)` where value rode // an untyped binding — the tracingChannel suite's shape): the checked-dynamic tree // value IS the rejection payload (the thrown-dyn representation, so @@ -16434,18 +16434,18 @@ export function lowerPromiseAllTupleCall( ): IrExpr | null { if (call.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Promise") !== "all") return null; - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if ( !argNode || !ts.isArrayLiteralExpression(argNode) || - argNode.elements.some(ts.isSpreadElement) || - argNode.elements.length === 0 + argNode.elements.nodes.some(ts.isSpreadElement) || + argNode.elements.nodes.length === 0 ) { return null; } // The claim test runs on checker types only (no side effects): every // element the same representable promise type. - const mapped = argNode.elements.map((el) => lowerer.mapTypeOf(lowerer.typeOf(el))); + const mapped = argNode.elements.nodes.map((el) => lowerer.mapTypeOf(lowerer.typeOf(el))); const first = mapped[0]; if (first?.kind !== "promise") return null; if (!mapped.every((m) => m?.kind === "promise" && typeEquals(m.inner, first.inner))) { @@ -16454,7 +16454,7 @@ export function lowerPromiseAllTupleCall( const loc = locOf(call); const inner = first.inner; const entryT: IrType = { kind: "promise", inner }; - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, entryT)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, entryT)); const entries: IrExpr = { kind: "arrayLit", elems, type: arrayOf(entryT), loc }; const type: IrType = inner.kind === "void" diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index b6d0a9863..7cc448994 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -171,10 +171,10 @@ export function lowerLambda( // A generic lambda used as a value has no per-call instantiation: it // lowers once with its type parameters bound to `unknown`, the erased // instance JavaScript itself runs. - if (node.typeParameters?.length && !lowerer.erasingLambdas.has(node)) { + if (node.typeParameters?.nodes.length && !lowerer.erasingLambdas.has(node)) { const previous = lowerer.typeParamBindings; const bindings = new Map(previous ?? []); - for (const parameter of node.typeParameters) { + for (const parameter of node.typeParameters.nodes) { const symbol = lowerer.checker.getSymbolAtLocation(parameter.name); if (symbol) bindings.set(symbol, DYN); } @@ -253,7 +253,7 @@ export function lowerLambda( // the lifted body returns the inner T (a `return v` fulfills with v). const isAsync = !ts.isAccessor(node) && - node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = node.asteriskToken !== undefined; if (isAsync && !isGenerator && funcType.ret.kind !== "promise") lowerer.badType(node, lowerer.typeOf(node)); @@ -303,11 +303,11 @@ export function lowerLambda( const receiver = (checkedReceiver || isJsSourceFile(node.getSourceFile()) || - node.parameters.some(isThisParameter)) && + node.parameters.nodes.some(isThisParameter)) && !ts.isArrowFunction(node) ? declareCallReceiver(lowerer, node, isGenerator) : null; - const { params, prologue } = lowerer.declareParams(node.parameters, shapes); + const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes); if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, @@ -323,7 +323,7 @@ export function lowerLambda( let body: IrStmt[]; if (ts.isBlock(node.body!)) { - body = lowerer.lowerStmts(node.body!.statements); + body = lowerer.lowerStmts(node.body!.statements.nodes); } else { // Bare-expression arrow body: `x => e` is `x => { return e; }` // (or an expression statement when the signature returns void — or @@ -494,10 +494,10 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I lowerer.fnStack.push(ctx); try { const receiver = - decl.parameters.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) + decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) : null; - const { params, prologue } = lowerer.declareParams(decl.parameters, sig.params); + const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params); if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, @@ -506,7 +506,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I params, prologue, locOf(decl), - sig.params.length > decl.parameters.length && + sig.params.length > decl.parameters.nodes.length && (sig.params[sig.params.length - 1]!.mode === "dynRest" || sig.params[sig.params.length - 1]!.mode === "arguments"), ); @@ -518,7 +518,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I "function declarations whose block body the frontend cannot locate", ); } - const body = [...prologue, ...lowerer.lowerStmts(bodyBlock.statements)]; + const body = [...prologue, ...lowerer.lowerStmts(bodyBlock.statements.nodes)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl)); const fn: IrFunction = { name: sig.name, diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index a9b467a6a..6bf414f29 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -112,7 +112,7 @@ export function lowerGenMethodCall( const name = access.name.text; if (name !== "next" && name !== "return" && name !== "throw") return null; if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "generator") return null; - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments"); } // Iterator helpers and the built-in iterators have no throw method; an @@ -131,7 +131,7 @@ export function lowerGenMethodCall( const genT = gen.type; const recT = resultRecordOf(lowerer, genT); const resultT: IrType = genT.async ? { kind: "promise", inner: recT } : recT; - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; let arg: IrExpr | null = null; if (name === "next") { if (argNode === undefined) { @@ -379,7 +379,7 @@ export function generatorLoopBinding( lowerer.unsupported("SC1090", stmt.initializer, "for-of assignment to this target"); } const list = stmt.initializer; - const decl = list.declarations[0]!; + const decl = list.declarations.nodes[0]!; const isLet = (list.flags & ts.NodeFlags.Let) !== 0; // The iterator result uses checked storage when its return channel is // unknown. After the done test, restore the declared yield shape for diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 37233bcdc..be7f21c1c 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1608,7 +1608,7 @@ function parseInspectOptions( "the options must be an object literal ({ depth, colors: false, ... }) so the knobs resolve at compile time", ); } - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { lowerer.noLowering( `${surface} options in this form`, @@ -1789,14 +1789,14 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo /** util.inspect(value[, options]) — the spoke's inspect entry. */ function lowerInspectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length < 1 || expr.arguments.length > 2) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `util.inspect with ${expr.arguments.length} arguments`, + `util.inspect with ${expr.arguments.nodes.length} arguments`, expr, "the supported forms are inspect(value) and inspect(value, options)", ); } - return lowerInspectValue(lowerer, expr.arguments[0]!, expr.arguments[1], loc, false); + return lowerInspectValue(lowerer, expr.arguments.nodes[0]!, expr.arguments.nodes[1], loc, false); } /** `inspect(value, options)` over one value node: the shared body of @@ -2047,10 +2047,10 @@ export function lowerFormatCall( argNodesOverride?: readonly ts.Expression[], literalPrefix = "", ): IrExpr { - const argNodes = [...(argNodesOverride ?? expr.arguments)]; + const argNodes = [...(argNodesOverride ?? expr.arguments.nodes)]; if (withOptions) { const opts = argNodes.shift(); - if (!opts || !ts.isObjectLiteralExpression(opts) || opts.properties.length > 0) { + if (!opts || !ts.isObjectLiteralExpression(opts) || opts.properties.nodes.length > 0) { lowerer.noLowering( "util.formatWithOptions with a non-empty options literal", opts ?? expr, @@ -2394,21 +2394,21 @@ export function lowerUtilModuleCall( switch (bi.member) { case "styleText": case "isDeepStrictEqual": { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`util.${bi.member} with spread arguments`, expr); const stmts: IrStmt[] = []; const args: IrExpr[] = []; // Save all inputs before evaluating surplus arguments: comparison may // read mutable objects or throw, so it must happen after every argument. for (let i = 0; i < 3; i++) { - const input: IrExpr = expr.arguments[i] - ? lowerer.lowerExprExpecting(expr.arguments[i]!, DYN) + const input: IrExpr = expr.arguments.nodes[i] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN) : dynUndefinedExpr(loc); const saved = lowerer.declareHiddenLocal("%utilInput", DYN); stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); args.push(varRef(saved.id, DYN, loc)); } - for (const arg of expr.arguments.slice(3)) + for (const arg of expr.arguments.nodes.slice(3)) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc }); const type = bi.member === "styleText" ? STRING : BOOL; return { @@ -2426,10 +2426,10 @@ export function lowerUtilModuleCall( case "debug": return lowerUtilCallableCall(lowerer, expr, bi.member, loc); case "getSystemErrorMap": { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("util.getSystemErrorMap with spread arguments", expr); const result = lowerSystemErrorMap(lowerer, loc); - const stmts: IrStmt[] = expr.arguments.map((arg) => ({ + const stmts: IrStmt[] = expr.arguments.nodes.map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc, @@ -2440,24 +2440,24 @@ export function lowerUtilModuleCall( case "getSystemErrorMessage": case "stripVTControlCharacters": case "toUSVString": { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`util.${bi.member} with spread arguments`, expr); - const input = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const input = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : { kind: "dynFrom" as const, value: { kind: "unitLit" as const, unit: "undefined" as const, type: UNDEFINED_T, loc }, type: DYN, loc, }; - if (expr.arguments.length <= 1) + if (expr.arguments.nodes.length <= 1) return { kind: "libCall", fn: `util.${bi.member}`, args: [input], type: STRING, loc }; const saved = lowerer.declareHiddenLocal("%utilInput", DYN); return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: saved.id, init: input, loc }, - ...expr.arguments + ...expr.arguments.nodes .slice(1) .map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc })), ], @@ -2481,11 +2481,11 @@ export function lowerUtilModuleCall( case "parseArgs": return lowerParseArgsCall(lowerer, expr, loc); case "parseEnv": { - if (expr.arguments.length > 1 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 1 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("util.parseEnv with spread or extra arguments", expr); } - const input: IrExpr = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const input: IrExpr = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); const parsed: IrExpr = { kind: "libCall", @@ -2526,7 +2526,7 @@ function lowerUtilCallableCall( member: string, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`util.${member} with spread arguments`, expr); const fn = member === "inherits" @@ -2540,7 +2540,7 @@ function lowerUtilCallableCall( // Program classes keep static prototypes: there is no function object // whose prototype chain the runtime could relink or wrap. const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0; - for (const arg of expr.arguments.slice(0, classArgs)) { + for (const arg of expr.arguments.nodes.slice(0, classArgs)) { if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval") lowerer.noLowering( `util.${member} over a class constructor`, @@ -2559,14 +2559,14 @@ function lowerUtilCallableCall( const stmts: IrStmt[] = []; const args: IrExpr[] = []; for (let i = 0; i < arity; i++) { - const input: IrExpr = expr.arguments[i] - ? lowerer.lowerExprExpecting(expr.arguments[i]!, DYN) + const input: IrExpr = expr.arguments.nodes[i] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN) : dynUndefinedExpr(loc); const saved = lowerer.declareHiddenLocal("%utilInput", DYN); stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); args.push(varRef(saved.id, DYN, loc)); } - for (const arg of expr.arguments.slice(arity)) + for (const arg of expr.arguments.nodes.slice(arity)) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc }); const type = fn === "util.inherits" ? VOID : DYN; const result: IrExpr = { kind: "libCall", fn, args, type, loc }; @@ -2601,16 +2601,16 @@ function callbackifyErrorExcludesNull(lowerer: Lowerer, expr: ts.CallExpression) * variants and a null-prototype values object); statically typed member * reads leave that tree through the ordinary dyn checks. */ function lowerParseArgsCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 1 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 1 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `util.parseArgs with ${expr.arguments.length} arguments`, + `util.parseArgs with ${expr.arguments.nodes.length} arguments`, expr, "the lowered form is parseArgs() or parseArgs(config)", ); } let config: IrExpr; - const configNode = expr.arguments[0]; + const configNode = expr.arguments.nodes[0]; if (!configNode) { config = { kind: "dynObjLit", fields: [], type: DYN, loc }; } else { @@ -2684,7 +2684,7 @@ function lowerGetCallSitesCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 0) return null; + if (expr.arguments.nodes.length !== 0) return null; if (!isJsSourceFile(expr.getSourceFile())) return null; const frame = (): IrExpr => { const field = (name: string, value: IrExpr): { key: IrExpr; value: IrExpr } => ({ diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index af5a1ab46..5677b2812 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -116,8 +116,8 @@ function mixinFnShapeInner( ): MixinFnShape | null { if (fn.asteriskToken) return null; if (ts.getModifiers(fn)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; - if (fn.parameters.length !== 1) return null; - const p = fn.parameters[0]!; + if (fn.parameters.nodes.length !== 1) return null; + const p = fn.parameters.nodes[0]!; if (!ts.isIdentifier(p.name) || p.dotDotDotToken || p.questionToken || p.initializer) return null; const paramSym = lowerer.checker.getSymbolAtLocation(p.name); if (!paramSym || !fn.body) return null; @@ -131,7 +131,7 @@ function mixinFnShapeInner( const e = unparen(fn.body); if (ts.isClassExpression(e)) classNode = e; } else { - const stmts = fn.body.statements; + const stmts = fn.body.statements.nodes; if (stmts.length === 1 && ts.isReturnStatement(stmts[0]!) && stmts[0]!.expression) { const e = unparen(stmts[0]!.expression!); if (ts.isClassExpression(e)) classNode = e; @@ -153,17 +153,17 @@ function mixinFnShapeInner( // A generic class would mint a FAMILY per call; decorators are // declaration-time calls with per-evaluation semantics — both // disqualify (decorator-composed mixins are a named non-goal). - if (classNode.typeParameters) return null; - const mods = (classNode as { modifiers?: readonly ts.Node[] }).modifiers ?? []; + if (classNode.typeParameters?.nodes) return null; + const mods = (classNode as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []; if (mods.some((m) => m.kind === ts.SyntaxKind.Decorator)) return null; // Heritage: exactly `extends `. - const clauses = classNode.heritageClauses ?? []; + const clauses = classNode.heritageClauses?.nodes ?? []; if (clauses.length !== 1 || clauses[0]!.token !== ts.SyntaxKind.ExtendsKeyword) return null; - const types = clauses[0]!.types; + const types = clauses[0]!.types.nodes; if (types.length !== 1) return null; const t = types[0]!; - if (t.typeArguments || !ts.isIdentifier(t.expression)) return null; + if (t.typeArguments?.nodes || !ts.isIdentifier(t.expression)) return null; if (lowerer.checker.getSymbolAtLocation(t.expression) !== paramSym) return null; // The parameter's flow must be fully traceable: the extends clause is @@ -191,7 +191,7 @@ function mixinFnShapeInner( const refSym = lowerer.checker.getSymbolAtLocation(p.type.typeName); if ( refSym && - fn.typeParameters?.some((tp) => lowerer.checker.getSymbolAtLocation(tp.name) === refSym) + fn.typeParameters?.nodes.some((tp) => lowerer.checker.getSymbolAtLocation(tp.name) === refSym) ) { paramTypeParam = refSym; } @@ -415,7 +415,7 @@ function instantiateMixinCall( "mixin calls whose base chain re-enters their own instantiation (a cyclic extends)", ); } - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.unsupported( "SC1090", call, @@ -449,7 +449,7 @@ function instantiateMixinCall( } } - const base = mixinBaseClassOf(lowerer, call.arguments[0]!); + const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( call.getSourceFile(), `%mx${call.getStart()}.${shape.classNode.name?.text ?? ""}`, @@ -611,7 +611,8 @@ function pinnedMixinCallPosition(call: ts.CallExpression): boolean { n = p, p = p.parent ) { if (ts.isParenthesizedExpression(p)) continue; - if (ts.isCallExpression(p) && p.arguments.length === 1 && p.arguments[0] === n) continue; + if (ts.isCallExpression(p) && p.arguments.nodes.length === 1 && p.arguments.nodes[0] === n) + continue; if (ts.isVariableDeclaration(p) && p.initializer === n) { return ts.isVariableDeclarationList(p.parent) && (p.parent.flags & ts.NodeFlags.Const) !== 0; } @@ -629,12 +630,13 @@ function staticsEvalStatementOf(call: ts.CallExpression): ts.Statement | null { let n: ts.Node = call; for (let p: ts.Node | undefined = n.parent; p !== undefined; n = p, p = p.parent) { if (ts.isParenthesizedExpression(p)) continue; - if (ts.isCallExpression(p) && p.arguments.length === 1 && p.arguments[0] === n) continue; + if (ts.isCallExpression(p) && p.arguments.nodes.length === 1 && p.arguments.nodes[0] === n) + continue; if ( ts.isVariableDeclaration(p) && p.initializer === n && ts.isVariableDeclarationList(p.parent) && - p.parent.declarations.length === 1 && + p.parent.declarations.nodes.length === 1 && ts.isVariableStatement(p.parent.parent) && ts.isSourceFile(p.parent.parent.parent) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index 2022d8232..4f446bb89 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -657,7 +657,7 @@ export function lowerMockFn( const fields = mockContextFields(lowerer, ctxType!)!; const { fn } = fields; requireUndefinedResultArm(lowerer, fields, call, what); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length === 0 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -717,7 +717,7 @@ export function lowerMockMethod( tracker: MockTracker, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 2 || args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -844,8 +844,8 @@ export function lowerMockTrackerCall( member: "restoreAll" | "reset", tracker: MockTracker, ): IrExpr { - if (call.arguments.length > 0) { - lowerer.noLowering(`MockTracker.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 0) { + lowerer.noLowering(`MockTracker.${member} with ${call.arguments.nodes.length} arguments`, call); } const fn = tracker ? member === "reset" @@ -893,7 +893,7 @@ export function lowerMockTimersCall( sym: ts.Symbol | null, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const optionalNumber = (fallback: number): IrExpr[] => { if (args.length > 1) lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call); @@ -935,9 +935,9 @@ export function lowerMockTimersCall( * (1 = `now` given, 2 = it is a Date). */ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] { const loc = locOf(call); - const options = call.arguments[0]; - if (call.arguments.length > 1) { - lowerer.noLowering(`${what}.enable with ${call.arguments.length} arguments`, call); + const options = call.arguments.nodes[0]; + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`${what}.enable with ${call.arguments.nodes.length} arguments`, call); } if (options === undefined) { return [strLit(MOCK_TIMERS_ALL_APIS, loc), numLit(0, loc), numLit(0, loc)]; @@ -953,7 +953,7 @@ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: let listed = false; let now: IrExpr = numLit(0, loc); let flags = 0; - for (const prop of options.properties) { + for (const prop of options.properties.nodes) { // `{ apis: [...] }`, `{ "now": n }` and the `{ now }` shorthand. const init = ts.isPropertyAssignment(prop) ? prop.initializer @@ -974,7 +974,7 @@ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: ); } listed = true; - for (const el of list.elements) { + for (const el of list.elements.nodes) { if (!ts.isStringLiteralLike(el)) { lowerer.noLowering( `${what}.enable with a non-literal API name`, @@ -1046,7 +1046,7 @@ export function lowerMockContextCall( const { fn, calls, once, onceEntry } = mockContextFields(lowerer, ctxType)!; const loc = locOf(call); const member = access.name.text; - const args = call.arguments; + const args = call.arguments.nodes; const arity = (min: number, max: number = min) => { if (args.length < min || args.length > max || args.some(ts.isSpreadElement)) lowerer.noLowering(`MockFunctionContext.${member} with ${args.length} arguments`, call); diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 128652bd2..bc963c9ec 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -243,7 +243,7 @@ export function splitFiles(lowerer: Lowerer): FileParts[] { const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry]; return files.map((sf) => { const fp: FileParts = { sf, fnDecls: [], classDecls: [], topStmts: [] }; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); else if (ts.isClassDeclaration(stmt)) fp.classDecls.push(stmt); // Namespaces: ambient/type-only ones are zero-runtime and skip; @@ -293,10 +293,10 @@ function dynamicProgramImportsOf(program: ts.Program, sf: ts.SourceFile): ts.Sou if ( ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments[0] !== undefined && - ts.isStringLiteralLike(node.arguments[0]) + node.arguments.nodes[0] !== undefined && + ts.isStringLiteralLike(node.arguments.nodes[0]) ) { - const dep = dynamicImportProgramTargetOf(program, sf, node.arguments[0].text); + const dep = dynamicImportProgramTargetOf(program, sf, node.arguments.nodes[0].text); if (dep && !seen.has(dep)) { seen.add(dep); out.push(dep); @@ -367,9 +367,9 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { const computed = new Set(); for (const fp of parts) for (const decl of fp.classDecls) { - const heritage = decl.heritageClauses?.find( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; if (heritage && ts.isCallExpression(heritage)) computed.add(decl); } // Descendants need the computed ancestor's completed layout before @@ -385,9 +385,9 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) for (const decl of fp.classDecls) { if (computed.has(decl)) continue; - const heritage = decl.heritageClauses?.find( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; const base = heritage && ts.isIdentifier(heritage) ? lowerer.resolveValueSymbol(heritage) : undefined; if (!base || !computedSymbols.has(base)) continue; @@ -440,9 +440,9 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { const previousSink = lowerer.diagSink; const deferred: import("../../diagnostics/diagnostic.js").ScrDiagnostic[] = []; lowerer.diagSink = deferred; - const expression = decl.heritageClauses!.find( + const expression = decl.heritageClauses!.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )!.types[0]!.expression; + )!.types.nodes[0]!.expression; const context = newFnCtx(false, null, null, DYN); lowerer.fnStack.push(context); try { @@ -515,7 +515,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { const entryPackageFile = entryPackageFilePredicate(lowerer.entry.fileName); for (const fp of parts) { - for (const stmt of fp.sf.statements) { + for (const stmt of fp.sf.statements.nodes) { // NAMED re-exports from npm packages (`export { isUrl } from // "url-or-path"`): use the same package bindings as direct imports. const reexport = @@ -525,7 +525,7 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { ts.isStringLiteral(stmt.moduleSpecifier) && stmt.exportClause !== undefined && ts.isNamedExports(stmt.exportClause) && - stmt.exportClause.elements.some((e) => !e.isTypeOnly) && + stmt.exportClause.elements.nodes.some((e) => !e.isTypeOnly) && !stmt.moduleSpecifier.text.startsWith("#") ? stmt.exportClause : null; @@ -546,7 +546,7 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { clause?.namedBindings && ts.isNamedImports(clause.namedBindings) && !clause.name && - clause.namedBindings.elements.every((e) => e.isTypeOnly) + clause.namedBindings.elements.nodes.every((e) => e.isTypeOnly) ) { continue; } @@ -633,11 +633,11 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { if ( ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.length >= 1 && - node.arguments[0] !== undefined && - ts.isStringLiteralLike(node.arguments[0]) + node.arguments.nodes.length >= 1 && + node.arguments.nodes[0] !== undefined && + ts.isStringLiteralLike(node.arguments.nodes[0]) ) { - const spec = node.arguments[0].text; + const spec = node.arguments.nodes[0].text; if (lowerer.externalTypes.has(spec)) { return undefined; } @@ -648,7 +648,7 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { // module has no runtime namespace object — the per-site fence // says so. Only non-declaration source files count; a sibling // .d.mts typing shipped JS is the EMBED case, not this one. - const modSym = lowerer.checker.getSymbolAtLocation(node.arguments[0]); + const modSym = lowerer.checker.getSymbolAtLocation(node.arguments.nodes[0]); const ownModule = modSym !== undefined && lowerer.checker @@ -684,7 +684,7 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { * and kept .json files out of the module order. */ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) { - for (const stmt of fp.sf.statements) { + for (const stmt of fp.sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const clause = stmt.importClause; if (!clause?.name || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; @@ -1071,8 +1071,8 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi ts.isPropertyAccessExpression(e.expression) && ["freeze", "seal", "preventExtensions"].includes(e.expression.name.text) && lowerer.stdlibGlobalMember(e.expression, "Object") !== null && - e.arguments[0] && - ts.isArrayLiteralExpression(e.arguments[0]) + e.arguments.nodes[0] && + ts.isArrayLiteralExpression(e.arguments.nodes[0]) ) return true; // The built-in Array constructor can create an evolving array whose @@ -1165,7 +1165,7 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi } if ( (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) && - !e.typeParameters?.length && + !e.typeParameters?.nodes.length && isJsSourceFile(e.getSourceFile()) ) return true; @@ -1287,7 +1287,7 @@ export function collectGlobals( !isJsSourceFile(sf) && inner.typeParameters === undefined && (decoratorNodesOf(inner).length > 0 || - inner.members.some((m) => decoratorNodesOf(m).length > 0)) && + inner.members.nodes.some((m) => decoratorNodesOf(m).length > 0)) && guaranteedDecorationThrow(lowerer, inner) !== null ) { continue; @@ -1470,7 +1470,7 @@ export function collectGlobals( !( requireSpecOf(rhs) !== null && inlineProgramRequireOf(lowerer, rhs) !== null && - !sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + !sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "member") ) && cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { @@ -1501,7 +1501,7 @@ export function collectGlobals( continue; } if (cjs?.kind === "table" && cjs.obj) { - for (const prop of cjs.obj.properties) { + for (const prop of cjs.obj.properties.nodes) { if (ts.isShorthandPropertyAssignment(prop)) continue; // alias plumbing if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer)) continue; if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { @@ -1579,8 +1579,8 @@ export function collectGlobals( if ( isJsSourceFile(sf) && (isDeadTscHelperDecl(lowerer, stmt) || - (stmt.declarationList.declarations.length === 1 && - tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null)) + (stmt.declarationList.declarations.nodes.length === 1 && + tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations.nodes[0]!) !== null)) ) { continue; } @@ -1595,14 +1595,14 @@ export function collectGlobals( isRequireStatement(stmt) && !( ts.isVariableStatement(stmt) && - stmt.declarationList.declarations.some( + stmt.declarationList.declarations.nodes.some( (decl) => isNativeFfiRequire(lowerer, decl.initializer) || isJsonRequireDecl(lowerer, decl), ) ) ) { if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) { - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); } } @@ -1614,7 +1614,7 @@ export function collectGlobals( const isLetList = (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { for (const element of requireNestedPatternsOf(decl)) { for (const nameNode of boundIdentifiersOf(element.name!)) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); @@ -1646,7 +1646,8 @@ export function collectGlobals( // Node's ReferenceError at the access (lower-exprs), and the // statement itself lowers to nothing. if ( - ts.getCombinedModifierFlags(stmt.declarationList.declarations[0]!) & ts.ModifierFlags.Ambient + ts.getCombinedModifierFlags(stmt.declarationList.declarations.nodes[0]!) & + ts.ModifierFlags.Ambient ) continue; const list = stmt.declarationList; @@ -1661,8 +1662,8 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(stmt, list.declarations[0]); - for (const decl of list.declarations) { + const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); + for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement // lowering emits nothing by the same test — see @@ -2010,7 +2011,7 @@ export function collectGlobals( nameNode === decl.name && decl.initializer && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.some( + decl.initializer.properties.nodes.some( (p) => !ts.isSpreadAssignment(p) && (ts.isMethodDeclaration(p) || @@ -2057,7 +2058,7 @@ export function collectGlobals( nameNode === decl.name && decl.initializer !== undefined && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.every((p) => { + decl.initializer.properties.nodes.every((p) => { if (ts.isSpreadAssignment(p)) { return ( ts.isIdentifier(p.expression) && @@ -2181,7 +2182,7 @@ export function collectGlobals( const generic = member && lowerer.genericFnOf(member); const fallback = (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) && - !initializer.typeParameters?.length + !initializer.typeParameters?.nodes.length ? lowerer.lambdaSignature(initializer).funcType : member && lowerer.globalOf(member)?.type.kind === "dyn" ? DYN @@ -2274,7 +2275,7 @@ export function collectGlobals( !hasJsTypeAnnotation(decl) && decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) && - !decl.initializer.typeParameters?.length + !decl.initializer.typeParameters?.nodes.length ) { type = lowerer.lambdaSignature(decl.initializer).funcType; } @@ -2348,7 +2349,7 @@ export function collectGlobals( ts.isCallExpression(decl.initializer) && ts.isPropertyAccessExpression(decl.initializer.expression) && lowerer.stdlibGlobalMember(decl.initializer.expression, "Object") === "assign" && - decl.initializer.arguments.some( + decl.initializer.arguments.nodes.some( (arg) => ts.isIdentifier(arg) && lowerer.globalOf(arg)?.type.kind === "dyn", ) ) { @@ -2601,7 +2602,7 @@ function collectNestedVarGlobals( return undefined; }); for (const list of lists) { - for (const decl of list.declarations) { + for (const decl of list.declarations.nodes) { for (const nameNode of boundIdentifiersOf(decl.name)) { const diagsBefore = lowerer.diags.length; try { @@ -2707,7 +2708,7 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): !isJsSourceFile(stmt.getSourceFile()) && inner.typeParameters === undefined && (decoratorNodesOf(inner).length > 0 || - inner.members.some((m) => decoratorNodesOf(m).length > 0)) && + inner.members.nodes.some((m) => decoratorNodesOf(m).length > 0)) && guaranteedDecorationThrow(lowerer, inner) !== null ) { return { kind: "exprStmt", expr: lowerer.lowerExpr(stmt.expression), loc: locOf(stmt) }; diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 69d050510..7c1d70ed0 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -77,7 +77,7 @@ function nsTypeOnlyBody(decl: ts.ModuleDeclaration): boolean { while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined) return true; if (!ts.isModuleBlock(body)) return true; - return body.statements.every( + return body.statements.nodes.every( (s) => ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || @@ -129,7 +129,7 @@ export function collectNamespaceStmt( while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined || !ts.isModuleBlock(body)) return; // bodyless non-ambient: tsc-rejected lowerer.nsBlocks.set(body, "flattened"); - for (const s of body.statements) { + for (const s of body.statements.nodes) { if (ts.isFunctionDeclaration(s)) fp.fnDecls.push(s); else if (ts.isClassDeclaration(s)) fp.classDecls.push(s); else if (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) continue; @@ -146,7 +146,7 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined || !ts.isModuleBlock(body)) return; lowerer.nsBlocks.set(body, "typeOnly"); - for (const s of body.statements) { + for (const s of body.statements.nodes) { if (ts.isModuleDeclaration(s)) registerTypeOnlyBlocks(lowerer, s); } } diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index 292b38e87..68f11b948 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -557,10 +557,10 @@ export function lowerRequireCacheKeys( call: ts.CallExpression, member: string, ): IrExpr | null { - const argument = call.arguments[0]; + const argument = call.arguments.nodes[0]; if ( member !== "keys" || - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || argument === undefined || !isRequireCacheExpr(lowerer, argument) ) diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index d46e7244f..aedd123c6 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -708,7 +708,7 @@ export function lowerNetModuleCall( // Members with ordinary surfaces.ts table entries (the autoselect // budget pair) ride the generic path — null hands the dispatch back. if (builtinModuleFnOf(lowerer, "net", bi.member)) return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { if (args.length === 0) { return { kind: "libCall", fn: "net.createServer", args: [], type: NETSERVER_T, loc }; @@ -883,7 +883,7 @@ function lowerNetConnectOptions( member: string, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; const optsNode = args[0] as ts.ObjectLiteralExpression; const isJs = isJsSourceFile(expr.getSourceFile()); // The checked-dynamic option-bag route (JS sources): a literal with @@ -893,7 +893,7 @@ function lowerNetConnectOptions( // port/host/autoSelectFamily), with the compiler-rendered fence as the // post-validation tail. if (isJs) { - const needsBag = optsNode.properties.some( + const needsBag = optsNode.properties.nodes.some( (p) => (!ts.isPropertyAssignment(p) && !ts.isShorthandPropertyAssignment(p)) || (ts.isPropertyAssignment(p) && ts.isComputedPropertyName(p.name)) || @@ -944,7 +944,7 @@ function lowerNetConnectOptions( type: NETSOCKET_T, loc, }); - for (const prop of optsNode.properties) { + for (const prop of optsNode.properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -1147,9 +1147,9 @@ function lowerServerCloseBind( if (!lowerer.isStdlibMember(closeAccess)) return null; const loc = locOf(call); const recvNode = closeAccess.expression; - const thisArg = call.arguments[0]; + const thisArg = call.arguments.nodes[0]; if ( - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || !thisArg || !ts.isIdentifier(recvNode) || !ts.isIdentifier(thisArg) || @@ -1352,7 +1352,7 @@ function lowerNetServerMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "getConnections") { // cb(null, count) on the next tick; answers the server. if (args.length !== 1) { @@ -1451,7 +1451,7 @@ function lowerNetServerMethodCall( let host: IrExpr | null = null; let v6only: IrExpr | null = null; let reusePort: IrExpr | null = null; - for (const prop of (args[0] as ts.ObjectLiteralExpression).properties) { + for (const prop of (args[0] as ts.ObjectLiteralExpression).properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -2152,8 +2152,8 @@ function lowerEncodedDataListener(lowerer: Lowerer, node: ts.Expression): IrExpr let fn = node; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return null; - if (fn.parameters.length !== 1) return null; - const param = fn.parameters[0]!; + if (fn.parameters.nodes.length !== 1) return null; + const param = fn.parameters.nodes[0]!; if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return null; const symbol = lowerer.checker.getSymbolAtLocation(param.name); if (symbol === undefined) return null; @@ -2184,7 +2184,7 @@ function lowerNetSocketMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if ((name === "ref" || name === "unref") && args.length === 0) { // Whether the socket keeps the event loop alive. requireStatementPosition(lowerer, call, `socket.${name}()`); @@ -2868,7 +2868,7 @@ function h2EndStreamOption(lowerer: Lowerer, node: ts.Expression | undefined): b ); } let end: boolean | undefined; - for (const prop of (node as ts.ObjectLiteralExpression).properties) { + for (const prop of (node as ts.ObjectLiteralExpression).properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { lowerer.noLowering( "h2 options with spreads or computed keys", @@ -2907,7 +2907,7 @@ function lowerH2SessionMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const receiver = () => coerceToHandle(lowerer, access.expression, HTTP2SESSION_T); if (name === "request") { const pairs = @@ -3169,7 +3169,7 @@ function lowerH2StreamMethodCall( if (!lowerer.isStdlibMember(access) && !compatReqStream) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const receiver = () => receiverOverride ?? coerceToHandle(lowerer, access.expression, HTTP2STREAM_T); if (name === "respond") { @@ -3949,7 +3949,7 @@ export function lowerServerProperty( } if (expr.name.text !== "port") return null; const recv = expr.expression; - if (!ts.isCallExpression(recv) || recv.questionDotToken || recv.arguments.length !== 0) + if (!ts.isCallExpression(recv) || recv.questionDotToken || recv.arguments.nodes.length !== 0) return null; if (!ts.isPropertyAccessExpression(recv.expression)) return null; const inner = recv.expression; @@ -4051,7 +4051,7 @@ function lowerHttpServerOptions( let allowMissingHost = false; let keepAliveTimeoutBuffer: IrExpr | null = null; let maxHeaderSize: IrExpr | null = null; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -4169,7 +4169,7 @@ function lowerHttpCreateServerForms( what: string, loc: SrcLoc, ): IrExpr { - const args: readonly ts.Expression[] = expr.arguments ?? []; + const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -4335,7 +4335,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE let timeout: IrExpr = numLit(-1, loc); let port: IrExpr = numLit(-1, loc); let scheduling: IrExpr = lowerer.coerceToExpected(strLit("lifo", loc), DYN); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering( `${api} with ${args.length} arguments`, @@ -4352,7 +4352,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE "spell the options as an object literal at the construction site", ); } - for (const prop of optsNode.properties) { + for (const prop of optsNode.properties.nodes) { if (ts.isSpreadAssignment(prop)) { lowerer.noLowering(`${api} with an options spread`, prop, "write each option inline"); } @@ -4485,9 +4485,9 @@ function lowerHttpModuleCall( if (bi.member === "validateHeaderName") { requireStatementPosition(lowerer, expr, "http.validateHeaderName(...)"); if ( - expr.arguments.length < 1 || - expr.arguments.length > 2 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 2 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "http.validateHeaderName arguments", @@ -4495,24 +4495,24 @@ function lowerHttpModuleCall( "pass a header name and an optional label", ); } - const name = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const name = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const label = - expr.arguments.length === 2 - ? lowerer.lowerExprExpecting(expr.arguments[1]!, STRING) + expr.arguments.nodes.length === 2 + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING) : strLit("Header name", loc); return { kind: "libCall", fn: "http.validateHeaderName", args: [name, label], type: VOID, loc }; } if (bi.member === "validateHeaderValue") { requireStatementPosition(lowerer, expr, "http.validateHeaderValue(...)"); - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "http.validateHeaderValue arguments", expr, "pass a header name and a value", ); } - const name = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const value = lowerer.lowerExprExpecting(expr.arguments[1]!, DYN); + const name = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const value = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, DYN); return { kind: "libCall", fn: "http.validateHeaderValue", @@ -4556,7 +4556,7 @@ function lowerTlsServerOptions( const loc = locOf(node); let cert: IrExpr | null = null; let key: IrExpr | null = null; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -4681,7 +4681,7 @@ function tlsLiteralNeedsRuntimeWalk( includeHttpTimeoutOptions = false, ): boolean { const jsSource = isJsSourceFile(node.getSourceFile()); - return node.properties.some( + return node.properties.nodes.some( (p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && @@ -4735,7 +4735,7 @@ function lowerTlsServerOptionsOrDyn( * callback (and 'secureConnect'/'connect' listeners) fires * post-handshake — Node's secureConnect timing. */ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; const FORMS_HINT = "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/rejectUnauthorized/ca/servername"; if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { @@ -4847,7 +4847,7 @@ function lowerTlsModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -5178,7 +5178,7 @@ function lowerHttpsModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -5279,7 +5279,7 @@ function lowerHttp2ModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { // The REAL h2c server (scr_http2.c). An options object literal is // accepted and ignored (h2 session-tuning knobs; the honest-defaults @@ -5355,7 +5355,7 @@ function lowerHttp2ModuleCall( lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!)).length === 0 ) { if (ts.isObjectLiteralExpression(args[1]!)) { - for (const pr of (args[1] as ts.ObjectLiteralExpression).properties) { + for (const pr of (args[1] as ts.ObjectLiteralExpression).properties.nodes) { // Plain `name: value` and shorthand `{ ca }` both carry the // option; a shorthand's value is the same-named identifier. let name: string; @@ -5570,14 +5570,14 @@ function lowerHttp2ModuleCall( const ok = ts.isObjectLiteralExpression(whenTrue) && ts.isObjectLiteralExpression(whenFalse) && - whenFalse.properties.length === 0 && - whenTrue.properties.length === 1 && - ts.isPropertyAssignment(whenTrue.properties[0]!) && - (ts.isIdentifier(whenTrue.properties[0]!.name) || - ts.isStringLiteral(whenTrue.properties[0]!.name)) && - (whenTrue.properties[0]!.name as ts.Identifier | ts.StringLiteral).text === + whenFalse.properties.nodes.length === 0 && + whenTrue.properties.nodes.length === 1 && + ts.isPropertyAssignment(whenTrue.properties.nodes[0]!) && + (ts.isIdentifier(whenTrue.properties.nodes[0]!.name) || + ts.isStringLiteral(whenTrue.properties.nodes[0]!.name)) && + (whenTrue.properties.nodes[0]!.name as ts.Identifier | ts.StringLiteral).text === "SNICallback" && - whenTrue.properties[0]!.initializer.getText() === condText; + whenTrue.properties.nodes[0]!.initializer.getText() === condText; if (!ok) { lowerer.noLowering( "createSecureServer options with a conditional spread", @@ -5595,7 +5595,7 @@ function lowerHttp2ModuleCall( `spreads must be inline object literals here; ${SNI_HINT}`, ); } - for (const p of inner.properties) lowerOption(p); + for (const p of inner.properties.nodes) lowerOption(p); return; } let initializer: ts.Expression | null; @@ -5671,14 +5671,14 @@ function lowerHttp2ModuleCall( e.kind === ts.SyntaxKind.FalseKeyword; const isLiteralObjectOfLiterals = (e: ts.Expression): boolean => ts.isObjectLiteralExpression(e) && - e.properties.every( + e.properties.nodes.every( (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && isScalarLiteral(p.initializer), ); if (k === "settings" && initializer !== null && ts.isObjectLiteralExpression(initializer)) { - for (const setting of initializer.properties) { + for (const setting of initializer.properties.nodes) { if ( ts.isPropertyAssignment(setting) && ts.isIdentifier(setting.name) && @@ -5731,7 +5731,7 @@ function lowerHttp2ModuleCall( ); // An undocumented key, dropped like Node drops it. }; - for (const prop of optsNode.properties) lowerOption(prop); + for (const prop of optsNode.properties.nodes) lowerOption(prop); if (cert === null || key === null) { lowerer.noLowering( "createSecureServer without both cert and key", @@ -5846,7 +5846,7 @@ function lowerHttpClientCall( const c = lowerer.lowerCondition(binding!.cond); return binding!.trueSecure ? c : { kind: "unary", op: "!", operand: c, type: BOOL, loc }; }; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length < 1 || args.length > 2) { lowerer.noLowering( `${member} with ${args.length} arguments`, @@ -5934,7 +5934,7 @@ function lowerHttpClientCall( let servername: IrExpr | null = null; /** maxHeaderSize: the response head's size bound. */ let maxHeaderSize: IrExpr | null = null; - for (const prop of optsNode.properties) { + for (const prop of optsNode.properties.nodes) { // Shorthand entries ({ port } for { port: port }) are the natural // spelling at these call sites — the shorthand's VALUE lowering // resolves the local binding (lowerShorthandValue), not the property @@ -6441,7 +6441,7 @@ function lowerClientHeadersOption(lowerer: Lowerer, node: ts.Expression): IrExpr const loc = locOf(node); if (ts.isObjectLiteralExpression(node)) { const elems: IrExpr[] = []; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( @@ -6493,8 +6493,8 @@ function lowerHttpTrailersOption(lowerer: Lowerer, node: ts.Expression): IrExpr if (!ts.isArrayLiteralExpression(node)) return lowerClientHeadersOption(lowerer, node); const loc = locOf(node); const flat: IrExpr[] = []; - for (const entry of node.elements) { - if (!ts.isArrayLiteralExpression(entry) || entry.elements.length !== 2) { + for (const entry of node.elements.nodes) { + if (!ts.isArrayLiteralExpression(entry) || entry.elements.nodes.length !== 2) { lowerer.noLowering( "addTrailers pair list", entry, @@ -6502,8 +6502,8 @@ function lowerHttpTrailersOption(lowerer: Lowerer, node: ts.Expression): IrExpr ); } const pair = entry as ts.ArrayLiteralExpression; - const name = pair.elements[0]!; - const value = pair.elements[1]!; + const name = pair.elements.nodes[0]!; + const value = pair.elements.nodes[1]!; if (ts.isSpreadElement(name) || ts.isSpreadElement(value)) { lowerer.noLowering( "addTrailers pair with spread", @@ -6528,7 +6528,7 @@ function lowerHttpClientMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setHeader") { requireStatementPosition(lowerer, call, "request.setHeader(...)"); if (args.length !== 2) @@ -7038,7 +7038,7 @@ function lowerHttpReqMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setTimeout") { requireStatementPosition(lowerer, call, "message.setTimeout(...)"); if (args.length < 1 || args.length > 2) @@ -7239,7 +7239,7 @@ function lowerHttpResMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setTimeout") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -7298,7 +7298,7 @@ function lowerHttpResMethodCall( let pairs: IrExpr; if (ts.isObjectLiteralExpression(node)) { const elems: IrExpr[] = []; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( @@ -7542,7 +7542,7 @@ function lowerHttpResMethodCall( } const names: IrExpr[] = []; const values: IrExpr[] = []; - for (const prop of headersNode.properties) { + for (const prop of headersNode.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 9fbe87c3d..92d9f0690 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -214,8 +214,8 @@ function visitRequestInitConstSelectedValues( // a statically traced spread) hides earlier values exactly as it does at // runtime; a conditional/missing spread keeps the earlier contributor // in the possible-value set. - for (let i = expr.properties.length - 1; i >= 0; i--) { - const property = expr.properties[i]!; + for (let i = expr.properties.nodes.length - 1; i >= 0; i--) { + const property = expr.properties.nodes[i]!; if (ts.isSpreadAssignment(property)) { const defines = visitRequestInitConstSelectedValues( lowerer, @@ -238,7 +238,7 @@ function visitRequestInitConstSelectedValues( return false; } if (selector.kind === "index" && ts.isArrayLiteralExpression(expr)) { - const element = expr.elements[selector.index]; + const element = expr.elements.nodes[selector.index]; if (element === undefined || ts.isOmittedExpression(element) || ts.isSpreadElement(element)) { return false; } @@ -343,7 +343,7 @@ function requestInitConstBindingSource( selectors.unshift({ kind: "property", key }); } else if (ts.isArrayBindingPattern(pattern)) { if (current.dotDotDotToken) return null; - const index = pattern.elements.indexOf(current); + const index = pattern.elements.nodes.indexOf(current); if (index < 0) return null; selectors.unshift({ kind: "index", index }); } else { @@ -464,8 +464,8 @@ function fenceRequestInitObject( // effective contributor for each dictionary member; a definite later // `cache: undefined`, for example, suppresses an earlier unsupported // cache value just as WebIDL observes at runtime. - for (let index = init.properties.length - 1; index >= 0; index--) { - const prop = init.properties[index]!; + for (let index = init.properties.nodes.length - 1; index >= 0; index--) { + const prop = init.properties.nodes[index]!; if (ts.isSpreadAssignment(prop)) { for (const row of rows) { if (shadowed.has(row.member)) continue; @@ -569,12 +569,12 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp if (call.questionDotToken) return null; const symbol = lowerer.resolveValueSymbol(callee); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - if (call.arguments.length < 1) return null; + if (call.arguments.nodes.length < 1) return null; const loc = locOf(call); - const initNode = call.arguments[1]; + const initNode = call.arguments.nodes[1]; if (initNode !== undefined) fenceRequestInitValue(lowerer, initNode); { - const inputNode = call.arguments[0]!; + const inputNode = call.arguments.nodes[0]!; const input = lowerer.lowerExpr(inputNode); let init: IrExpr; if (initNode === undefined) { @@ -608,7 +608,7 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp { kind: "varDecl", localId: inputLocal.id, init: input, loc }, { kind: "varDecl", localId: initLocal.id, init, loc }, ]; - for (const argument of call.arguments.slice(2)) { + for (const argument of call.arguments.nodes.slice(2)) { if (ts.isSpreadElement(argument)) { lowerer.noLowering( "spread surplus arguments on static fetch()", @@ -660,7 +660,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio if ( access.questionDotToken || (member !== "json" && member !== "text" && member !== "bytes" && member !== "arrayBuffer") || - call.arguments.length !== 0 + call.arguments.nodes.length !== 0 ) { return null; } @@ -1132,7 +1132,7 @@ export function fenceFetchObjectBinding(lowerer: Lowerer, pattern: ts.ObjectBind fenceFetchObjectMembers( lowerer, pattern, - pattern.elements.flatMap((element) => { + pattern.elements.nodes.flatMap((element) => { if (element.dotDotDotToken) return []; const member = fetchObjectBindingMemberName(lowerer, element); return member === null ? [] : [{ member, node: element }]; @@ -1151,7 +1151,7 @@ export function fenceFetchObjectAssignment( fenceFetchObjectMembers( lowerer, source, - target.properties.flatMap((property) => { + target.properties.nodes.flatMap((property) => { const member = fetchObjectAssignmentMemberName(lowerer, property); return member === null ? [] : [{ member, node: property }]; }), @@ -1195,7 +1195,7 @@ export function lowerFetchElementMethodCall( call.questionDotToken || !ts.isElementAccessExpression(call.expression) || call.expression.questionDotToken || - call.arguments.some((argument) => ts.isSpreadElement(argument)) + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) ) { return null; } @@ -1233,7 +1233,7 @@ export function lowerFetchElementMethodCall( const loc = locOf(call); const receiverLocal = lowerer.declareHiddenLocal("%fetchReceiver", DYN); const keyLocal = lowerer.declareHiddenLocal("%fetchMember", STRING); - const argumentValues = call.arguments.map((argument, index) => + const argumentValues = call.arguments.nodes.map((argument, index) => fetchMethodArgumentIsLive(resolved.owner, members, index) ? lowerLiveWebValue(lowerer, argument) : lowerer.lowerExprExpecting(argument, DYN), @@ -1272,7 +1272,7 @@ export function lowerFetchElementMethodCall( })); const staticResponsePromise: IrType | null = resolved.owner === "Response" && - call.arguments.length === 0 && + call.arguments.nodes.length === 0 && members.every((member) => member === "text" || member === "bytes") ? (() => { // The checker spells this call as @@ -1409,7 +1409,7 @@ function staticFirstArgWithSurplusArgs( first: IrExpr, ): IrExpr { const loc = locOf(call); - if (call.arguments.length <= 1) return first; + if (call.arguments.nodes.length <= 1) return first; const firstLocal = lowerer.declareHiddenLocal("%fetchArg", first.type); const firstRef: IrExpr = { kind: "varRef", @@ -1418,7 +1418,7 @@ function staticFirstArgWithSurplusArgs( loc, }; const stmts: IrStmt[] = [{ kind: "varDecl", localId: firstLocal.id, init: first, loc }]; - for (const argument of call.arguments.slice(1)) { + for (const argument of call.arguments.nodes.slice(1)) { if (ts.isSpreadElement(argument)) { lowerer.noLowering( "spread surplus arguments on the static fetch companion APIs", @@ -1472,14 +1472,14 @@ export function lowerStaticFetchCompanionCall( noteEventListeners(lowerer, loc); switch (call.expression.name.text) { case "timeout": - if (call.arguments[0] && ts.isSpreadElement(call.arguments[0])) { + if (call.arguments.nodes[0] && ts.isSpreadElement(call.arguments.nodes[0])) { return null; } return staticCallWithSurplusArgs( lowerer, call, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), (first) => ({ kind: "libCall", @@ -1490,7 +1490,7 @@ export function lowerStaticFetchCompanionCall( }), ); case "abort": - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "libCall", fn: "fetch.abortNow", @@ -1499,11 +1499,11 @@ export function lowerStaticFetchCompanionCall( loc, }; } - if (ts.isSpreadElement(call.arguments[0]!)) return null; + if (ts.isSpreadElement(call.arguments.nodes[0]!)) return null; return staticCallWithSurplusArgs( lowerer, call, - lowerLiveWebValue(lowerer, call.arguments[0]!), + lowerLiveWebValue(lowerer, call.arguments.nodes[0]!), (first) => ({ kind: "libCall", fn: "fetch.abortNow", @@ -1513,14 +1513,14 @@ export function lowerStaticFetchCompanionCall( }), ); case "any": - if (call.arguments[0] && ts.isSpreadElement(call.arguments[0])) { + if (call.arguments.nodes[0] && ts.isSpreadElement(call.arguments.nodes[0])) { return null; } return staticCallWithSurplusArgs( lowerer, call, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), (first) => ({ kind: "libCall", @@ -1537,9 +1537,9 @@ export function lowerStaticFetchCompanionCall( if ( root.text === "ReadableStream" && call.expression.name.text === "from" && - (!call.arguments[0] || !ts.isSpreadElement(call.arguments[0])) + (!call.arguments.nodes[0] || !ts.isSpreadElement(call.arguments.nodes[0])) ) { - if (!call.arguments[0]) { + if (!call.arguments.nodes[0]) { return { kind: "libCall", fn: "fetch.streamFrom", @@ -1548,7 +1548,7 @@ export function lowerStaticFetchCompanionCall( loc, }; } - let source = lowerer.lowerExpr(call.arguments[0]!); + let source = lowerer.lowerExpr(call.arguments.nodes[0]!); // A readonly tuple satisfies the fallback's readonly-array overload, // but maps to a monomorphic record in IR. Reuse the ordinary // tuple→array width conversion with the resolved generic parameter so @@ -1576,8 +1576,8 @@ export function lowerStaticFetchCompanionCall( source.type.kind === "dyn"; if (!preserve) { lowerer.noLowering( - `ReadableStream.from() over ${lowerer.checker.typeToString(lowerer.typeOf(call.arguments[0]!))} iterables`, - call.arguments[0]!, + `ReadableStream.from() over ${lowerer.checker.typeToString(lowerer.typeOf(call.arguments.nodes[0]!))} iterables`, + call.arguments.nodes[0]!, "arrays, Uint8Array, and strings are supported — spread other synchronous iterables into an array first: [...iterable]", ); } @@ -1607,7 +1607,7 @@ export function lowerStaticAbortControllerCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.some((argument) => ts.isSpreadElement(argument)) || + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken || staticResponseMemberName(lowerer, access) !== "abort" @@ -1627,8 +1627,8 @@ export function lowerStaticAbortControllerCall( const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "dyn") return null; const loc = locOf(call); - const first = call.arguments[0] - ? lowerLiveWebValue(lowerer, call.arguments[0]) + const first = call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) : dynUndefinedExpr(loc); const args = [staticFirstArgWithSurplusArgs(lowerer, call, first)]; return lowerStaticFixedFetchMethodCall( @@ -1660,7 +1660,7 @@ export function lowerStaticReadableStreamControllerCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length > 1 || + call.arguments.nodes.length > 1 || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken ) { @@ -1682,7 +1682,9 @@ export function lowerStaticReadableStreamControllerCall( if (recv.type.kind !== "dyn") return null; const loc = locOf(call); const args = [ - call.arguments[0] ? lowerLiveWebValue(lowerer, call.arguments[0]) : dynUndefinedExpr(loc), + call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) + : dynUndefinedExpr(loc), ]; return lowerStaticFixedFetchMethodCall( lowerer, @@ -1713,8 +1715,8 @@ export function lowerStaticReadableStreamCancelCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length > 1 || - call.arguments.some((argument) => ts.isSpreadElement(argument)) || + call.arguments.nodes.length > 1 || + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken || staticResponseMemberName(lowerer, access) !== "cancel" @@ -1729,7 +1731,9 @@ export function lowerStaticReadableStreamCancelCall( if (receiver.type.kind !== "dyn") return null; const loc = locOf(call); const args = [ - call.arguments[0] ? lowerLiveWebValue(lowerer, call.arguments[0]) : dynUndefinedExpr(loc), + call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) + : dynUndefinedExpr(loc), ]; return lowerStaticFixedFetchMethodCall( lowerer, @@ -1761,8 +1765,8 @@ export function lowerStaticAbortSignalListenerCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length < 2 || - call.arguments.some((arg) => ts.isSpreadElement(arg)) || + call.arguments.nodes.length < 2 || + call.arguments.nodes.some((arg) => ts.isSpreadElement(arg)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken ) { @@ -1786,7 +1790,7 @@ export function lowerStaticAbortSignalListenerCall( if (recv.type.kind !== "dyn") return null; const loc = locOf(call); noteEventListeners(lowerer, loc); - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => index === 1 ? lowerLiveWebValue(lowerer, arg) : lowerer.lowerExprExpecting(arg, DYN), ); return lowerStaticFixedFetchMethodCall( @@ -1814,7 +1818,7 @@ export function lowerStaticAbortSignalListenerCall( export function lowerEventTargetNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const name = stdlibGlobalNameOf(lowerer, expr.expression); if (name !== "EventTarget" && name !== "Event" && name !== "CustomEvent") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); if (name === "EventTarget") noteEventListeners(lowerer, loc); @@ -1849,7 +1853,7 @@ export function lowerAbortControllerNew(lowerer: Lowerer, expr: ts.NewExpression } const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some((argument) => ts.isSpreadElement(argument))) return null; const loc = locOf(expr); noteEventListeners(lowerer, loc); @@ -1884,7 +1888,7 @@ export function lowerStaticReadableStreamNew( } const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) return null; const loc = locOf(expr); @@ -1907,7 +1911,7 @@ export function lowerStaticReadableStreamNew( export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const name = stdlibGlobalNameOf(lowerer, expr.expression); if (name !== "Request" && name !== "Headers") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); @@ -1931,7 +1935,7 @@ export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; return lowerResponseConstruction(lowerer, args, locOf(expr), false); } @@ -1944,8 +1948,9 @@ export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpressio const access = call.expression; if (access.questionDotToken || access.name.text !== "json") return null; if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null; - if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null; - return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true); + if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length === 0) + return null; + return lowerResponseConstruction(lowerer, call.arguments.nodes, locOf(call), true); } function lowerResponseConstruction( @@ -2110,7 +2115,7 @@ export function lowerStaticReadableStreamReaderCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length !== 0 || + call.arguments.nodes.length !== 0 || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken ) { @@ -2152,7 +2157,7 @@ export function lowerStaticReadableStreamReaderCall( export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.expression.kind !== ts.SyntaxKind.ImportKeyword) return null; - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) { lowerer.unsupported( "SC1090", @@ -2161,7 +2166,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression "build time — the specifier must be a string literal)", ); } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.unsupported("SC1090", call, "dynamic import() with import attributes"); } const res = lowerer.dynImports.get(`${call.getSourceFile().fileName}\u0000${arg.text}`); @@ -2354,14 +2359,14 @@ export function lowerStdlibMethodCall( const sigMember = lowerer.stdlibGlobalMember(access, "AbortSignal"); if ( (sigMember === "timeout" || sigMember === "abort" || sigMember === "any") && - call.arguments.every((argument) => !ts.isSpreadElement(argument)) + call.arguments.nodes.every((argument) => !ts.isSpreadElement(argument)) ) { lowerer.refuseUnsupportedApi(`'AbortSignal.${sigMember}'`, call); } } if ( lowerer.stdlibGlobalMember(access, "ReadableStream") === "from" && - call.arguments.every((argument) => !ts.isSpreadElement(argument)) + call.arguments.nodes.every((argument) => !ts.isSpreadElement(argument)) ) { lowerer.refuseUnsupportedApi("'ReadableStream.from'", call); } @@ -2374,10 +2379,10 @@ export function lowerStdlibMethodCall( // for undefined, instead of trusting the checker's number type. const mathArg = (a: ts.Expression): IrExpr => lowerToNumberArgument(lowerer, a); if (staticMath && name === "hypot") { - const elems = call.arguments.map((a) => + const elems = call.arguments.nodes.map((a) => ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(F64)) : mathArg(a), ); - const spreads = call.arguments.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); + const spreads = call.arguments.nodes.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); const packed: IrExpr = { kind: "arrayLit", elems, @@ -2387,7 +2392,7 @@ export function lowerStdlibMethodCall( }; return { kind: "libCall", fn: staticMath.fn, args: [packed], type: F64, loc }; } - if (staticMath && call.arguments.every((a) => !ts.isSpreadElement(a))) { + if (staticMath && call.arguments.nodes.every((a) => !ts.isSpreadElement(a))) { // Math.max/Math.min at ANY plain arity — Node's are variadic. The // spec's reduction is a left fold of the same NaN-poisoning // ±0-ordered scalar compare the two-arg form lowers to, so n @@ -2398,18 +2403,18 @@ export function lowerStdlibMethodCall( // arguments are the fold's seed: -Infinity for max, +Infinity for // min, exactly Node. if (name === "max" || name === "min") { - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "numLit", value: name === "max" ? -Infinity : Infinity, type: F64, loc }; } - const args = call.arguments.map(mathArg); + const args = call.arguments.nodes.map(mathArg); let acc = args[0]!; for (let i = 1; i < args.length; i++) { acc = { kind: "libCall", fn: staticMath.fn, args: [acc, args[i]!], type: F64, loc }; } return acc; } - if (call.arguments.length === staticMath.arity) { - const args = call.arguments.map(mathArg); + if (call.arguments.nodes.length === staticMath.arity) { + const args = call.arguments.nodes.map(mathArg); return { kind: "libCall", fn: staticMath.fn, args, type: F64, loc }; } } @@ -2420,10 +2425,10 @@ export function lowerStdlibMethodCall( if ( isMath && (name === "max" || name === "min") && - call.arguments.length === 1 && - ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + ts.isSpreadElement(call.arguments.nodes[0]!) ) { - const spread = call.arguments[0]! as ts.SpreadElement; + const spread = call.arguments.nodes[0]! as ts.SpreadElement; let src = lowerer.lowerExpr(spread.expression); // JavaScript array call results retain checked storage so aliases stay // shared. This read-only fold can extract the inferred numeric array. @@ -2449,7 +2454,7 @@ export function lowerStdlibMethodCall( }; } const mathFn = isMath ? own(STDLIB_SURFACE.math.fns, name) : undefined; - if (mathFn && call.arguments.length === mathFn.args.length) { + if (mathFn && call.arguments.nodes.length === mathFn.args.length) { lowerer.refuseUnsupportedApi(`'Math.${name}'`, call); } const kind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind; @@ -2459,7 +2464,7 @@ export function lowerStdlibMethodCall( : kind === "string" ? own(STDLIB_SURFACE.string, name) : undefined; - if (!entry || call.arguments.length !== entry.args.length) return null; + if (!entry || call.arguments.nodes.length !== entry.args.length) return null; if (!lowerer.isStdlibMember(access)) return null; lowerer.refuseUnsupportedApi(`'.${name}()' on ${kind === "f64" ? "numbers" : "strings"}`, call); } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 3a0958ae6..53e41f259 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -242,7 +242,7 @@ function npmStaticReturnedEmptyArrayType( !ts.isIdentifier(decl.name) || decl.initializer === undefined || !ts.isArrayLiteralExpression(decl.initializer) || - decl.initializer.elements.length !== 0 || + decl.initializer.elements.nodes.length !== 0 || !isJsSourceFile(decl.getSourceFile()) || npmStaticPackageOfPath(decl.getSourceFile().fileName) === null || lowerer.ctx.returnType.kind !== "array" @@ -568,7 +568,7 @@ function lowerUsingStatement(lowerer: Lowerer, stmt: ts.VariableStatement): Usin ); } const registrations: UsingRegistration[] = []; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); if (decl.initializer === undefined) lowerer.unsupported("SC1090", decl, "a 'using' declaration without an initializer"); @@ -596,7 +596,7 @@ function lowerUsingStatement(lowerer: Lowerer, stmt: ts.VariableStatement): Usin function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean { if (!ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return false; if ((stmt.declarationList.flags & ts.NodeFlags.Const) === 0) return false; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length === 0) return false; return decls.every((d) => { let init = d.initializer; @@ -842,7 +842,7 @@ function isAbruptStatement(stmt: ts.Statement): boolean { function skipsWhenUnreachable(stmt: ts.Statement): boolean { if (ts.isFunctionDeclaration(stmt)) return stmt.typeParameters === undefined; if (ts.isVariableStatement(stmt)) - return !(stmt.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword); + return !(stmt.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword); return !( ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt) || @@ -991,7 +991,8 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b * resume the caller and thread its captures normally. */ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): boolean { const decl = lowerer.checker.valueDeclarationOf(symbol); - if (!decl || !ts.isFunctionDeclaration(decl) || !decl.body || decl.typeParameters) return false; + if (!decl || !ts.isFunctionDeclaration(decl) || !decl.body || decl.typeParameters?.nodes) + return false; for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) { const entry = lowerer.activeStmtLists[i]!; const idx = entry.stmts.indexOf(decl); @@ -1258,7 +1259,7 @@ export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void list = stmt.initializer; } if (!list) return; - for (const decl of list.declarations) { + for (const decl of list.declarations.nodes) { for (const nameNode of boundIdentifiersOf(decl.name)) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (symbol) lowerer.blockedBindings.add(symbol); @@ -1286,7 +1287,7 @@ export function lowerScopedBlock(lowerer: Lowerer, stmt: ts.Statement): IrStmt[] lowerer.scopes.push(new Map()); try { return withRuntimeOptionalScope(lowerer, () => - ts.isBlock(stmt) ? lowerer.lowerStmts(stmt.statements) : lowerer.lowerStmts([stmt]), + ts.isBlock(stmt) ? lowerer.lowerStmts(stmt.statements.nodes) : lowerer.lowerStmts([stmt]), ); } finally { lowerer.scopes.pop(); @@ -1359,7 +1360,7 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt if (ts.isVariableStatement(stmt)) { // `declare const` — ambient: no storage, no init (collectGlobals // skipped it; reads throw Node's ReferenceError at the access). - const first = stmt.declarationList.declarations[0]; + const first = stmt.declarationList.declarations.nodes[0]; if (first && ts.getCombinedModifierFlags(first) & ts.ModifierFlags.Ambient) return null; return lowerer.lowerVarStatement(stmt); } @@ -1603,7 +1604,7 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement): const isConst = (list.flags & ts.NodeFlags.Const) !== 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; - return list.declarations.flatMap((decl) => { + return list.declarations.nodes.flatMap((decl) => { const lowered = lowerVarDeclarator(lowerer, stmt, decl, isConst, isLet); // A require binding earlier code can read is initialized from here on // (require-initialization.ts). @@ -1765,7 +1766,7 @@ export function numericIteratorSourceOf( if (expr === undefined) return null; let init = expr; while (ts.isParenthesizedExpression(init)) init = init.expression; - if (!ts.isCallExpression(init) || init.questionDotToken || init.arguments.length !== 0) + if (!ts.isCallExpression(init) || init.questionDotToken || init.arguments.nodes.length !== 0) return null; let receiver: ts.Expression | null = null; @@ -1872,7 +1873,7 @@ function lowerMatchAllDrainDecl( const loc = locOf(decl); const access = call.expression as ts.PropertyAccessExpression; const receiver = lowerer.lowerExpr(access.expression); - const re = lowerer.lowerExpr(call.arguments[0]!); + const re = lowerer.lowerExpr(call.arguments.nodes[0]!); const idxsT = arrayOf(F64); const idxs = lowerer.declareHiddenLocal("%midxs", idxsT); const local = lowerer.declareLocal(decl.name, decl.name.text, rowsT, false); @@ -2132,11 +2133,11 @@ export function matchGroupsPatternSource( ): IrExpr | null { if ( !ts.isObjectBindingPattern(pattern) || - pattern.elements.length === 0 || + pattern.elements.nodes.length === 0 || !(row.type.kind === "dyn" ? isJsSourceFile(pattern.getSourceFile()) : isMatchSliceType(lowerer, row.type)) || - !pattern.elements.every((el) => { + !pattern.elements.nodes.every((el) => { if (el.dotDotDotToken !== undefined || el.name === undefined) return false; const prop = el.propertyName; if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; @@ -2209,8 +2210,8 @@ export function lowerBindingPattern( srcType.kind === "union" && lowerer.unions.get(srcType.unionId)?.arms.every((arm) => arm.kind === "array") ) { - for (let i = 0; i < pattern.elements.length; i++) { - const el = pattern.elements[i]!; + for (let i = 0; i < pattern.elements.nodes.length; i++) { + const el = pattern.elements.nodes[i]!; if (ts.isOmittedExpression(el) || el.name === undefined) continue; if (el.dotDotDotToken) lowerer.unsupported("SC1031", el, "rest elements over a union of array layouts"); @@ -2231,7 +2232,7 @@ export function lowerBindingPattern( // shape — the positional twin of object destructuring below. const tupleShape = srcType.kind === "record" ? lowerer.shapes.get(srcType.shapeId) : undefined; if (srcType.kind === "record" && tupleShape?.tuple) { - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { // Holes: OmittedExpression in 5.9.3, a nameless BindingElement in // 7 (parity finding 2) — either way the position is skipped. if (ts.isOmittedExpression(el) || el.name === undefined) return; @@ -2306,7 +2307,7 @@ export function lowerBindingPattern( loc, }); const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: citPat.iterT, loc }); - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { const stepLoc = locOf(el); const step = lowerer.classIteratorNextCall(citPat, itRef(), stepLoc); if (ts.isOmittedExpression(el) || el.name === undefined) { @@ -2391,7 +2392,7 @@ export function lowerBindingPattern( out.push({ kind: "varDecl", localId: pack.id, - init: pattern.elements.some( + init: pattern.elements.nodes.some( (element) => ts.isBindingElement(element) && element.dotDotDotToken, ) ? { @@ -2404,14 +2405,14 @@ export function lowerBindingPattern( : { kind: "dynIterN", value: srcRef(), - count: pattern.elements.length, + count: pattern.elements.nodes.length, ...(dynSpell ? { notIterableMessage: dynSpell } : {}), type: DYN, loc, }, loc, }); - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { if (ts.isOmittedExpression(el) || el.name === undefined) return; // hole: the position skips const elLoc = locOf(el); if (el.dotDotDotToken) { @@ -2475,7 +2476,7 @@ export function lowerBindingPattern( // fresh number[] (never another typed array), exactly the iterator // protocol's Array accumulation. if (srcType.kind === "bytes") { - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { if (ts.isOmittedExpression(el) || el.name === undefined) return; const loc = locOf(el); if (el.dotDotDotToken) { @@ -2523,7 +2524,7 @@ export function lowerBindingPattern( // like the string case above. Element defaults keep a fence: they // run between iterator steps and could mutate the live collection. if (srcType.kind === "map" || srcType.kind === "set") { - const defaulted = pattern.elements.find( + const defaulted = pattern.elements.nodes.find( (el): el is ts.BindingElement => ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken, ); @@ -2556,7 +2557,7 @@ export function lowerBindingPattern( `array destructuring of non-array values (the source is ${srcType.kind}-typed)`, ); } - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { if (ts.isOmittedExpression(el) || el.name === undefined) return; // hole: position skipped, like JS over an array const loc = locOf(el); // Array rest drains values from the iterator, so holes become @@ -2593,7 +2594,7 @@ export function lowerBindingPattern( }); return; } - if (pattern.elements.length === 0) { + if (pattern.elements.nodes.length === 0) { // `var {} = e`, `for (const {} of ns)`: RequireObjectCoercible and // nothing else — a pure no-op past the source's own evaluation for // every statically non-nullish kind, record or not (the assignment @@ -2640,12 +2641,12 @@ export function lowerBindingPattern( return; } if (srcType.kind === "dyn") { - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { const keys: IrExpr[] = []; - for (const sibling of pattern.elements) { + for (const sibling of pattern.elements.nodes) { if (sibling === el || sibling.dotDotDotToken) continue; const key = sibling.propertyName ?? sibling.name; const name = @@ -2733,7 +2734,7 @@ export function lowerBindingPattern( const info = lowerer.classes.get(srcType.className); if (!info) lowerer.flushDeferredClass(srcType.className); if (info) { - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { @@ -2742,7 +2743,7 @@ export function lowerBindingPattern( // CopyDataProperties copies own enumerable properties, which // for a class instance are exactly the fields). const consumed = new Set(); - for (const sib of pattern.elements) { + for (const sib of pattern.elements.nodes) { if (sib === el || sib.name === undefined || sib.dotDotDotToken) continue; const keyNode = sib.propertyName ?? (ts.isIdentifier(sib.name) ? sib.name : null); if (keyNode === null) continue; // defensive: a pattern target always carries a propertyName @@ -2850,7 +2851,7 @@ export function lowerBindingPattern( // naming that. Callers vetted the source as a checker-typed string — // builtin identity tokens fence before reaching here. if (srcType.kind === "string") { - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { @@ -2917,7 +2918,7 @@ export function lowerBindingPattern( const shape = lowerer.shapes.get(srcType.shapeId); if (!shape) throw new InternalCompilerError(`lowerer bug: destructuring unknown shape ${srcType.shapeId}`); - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; // 7 spells elisions as nameless elements // `{ a, ...rest }`: the rest binds a FRESH record of the unconsumed // fields (JS's CopyDataProperties makes a fresh object too, own @@ -3209,7 +3210,7 @@ function lowerMatchResultObjectPattern( out: IrStmt[], ): void { let groups: { name: string; index: number }[] | null | undefined; - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { @@ -3350,7 +3351,7 @@ function staticStringPattern( pattern: ts.ArrayBindingPattern | ts.ObjectBindingPattern, ): boolean { if (ts.isArrayBindingPattern(pattern)) { - return pattern.elements.every((el) => { + return pattern.elements.nodes.every((el) => { if (ts.isOmittedExpression(el) || el.name === undefined) return true; if (el.dotDotDotToken) { // The rest binds string[]: identifier targets and nested ARRAY @@ -3362,7 +3363,7 @@ function staticStringPattern( return staticStringPattern(lowerer, el.name); }); } - return pattern.elements.every((el) => { + return pattern.elements.nodes.every((el) => { if (el.name === undefined) return true; if (el.dotDotDotToken || !ts.isIdentifier(el.name)) return false; const prop = el.propertyName ?? el.name; @@ -3460,7 +3461,7 @@ function stdlibGlobalTokenDestructure( * of a token. */ value?: IrExpr; }[] = []; - for (const el of decl.name.elements) { + for (const el of decl.name.elements.nodes) { if (el.name === undefined) continue; if (el.dotDotDotToken) { lowerer.unsupported( @@ -3916,7 +3917,7 @@ function objectRestValue( const consumed = new Set(); let computedSibling = false; if (ts.isObjectBindingPattern(pattern)) { - for (const sib of pattern.elements) { + for (const sib of pattern.elements.nodes) { if (sib === el || sib.dotDotDotToken) continue; const key = sib.propertyName ?? sib.name; if (key && ts.isComputedPropertyName(key)) computedSibling = true; @@ -4291,7 +4292,7 @@ export function lowerVarDeclList( const isConst = (list.flags & ts.NodeFlags.Const) !== 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0; if (!isConst && !isLet) { - const out = list.declarations.flatMap((decl) => { + const out = list.declarations.nodes.flatMap((decl) => { // `for (var {} = {}, [a] = xs; ...)`: pattern declarators ride the // declaration desugar (var names assign their hoisted slots). if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { @@ -4305,7 +4306,7 @@ export function lowerVarDeclList( return { kind: "block", body: out, loc: locOf(list) }; } const out: IrStmt[] = []; - for (const decl of list.declarations) { + for (const decl of list.declarations.nodes) { if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { // Destructuring produces nested initialization blocks; copying // their captured bindings still needs a distinct head-scope plan. @@ -4503,7 +4504,7 @@ export function lowerVarDecl( !isJsSourceFile(decl.getSourceFile()) && !decl .getSourceFile() - .statements.some( + .statements.nodes.some( (s) => ts.isImportDeclaration(s) || ts.isExportDeclaration(s) || @@ -4820,7 +4821,7 @@ export function lowerVarDecl( !decl.type && !hasJsTypeAnnotation(decl) && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.some( + decl.initializer.properties.nodes.some( (property) => ts.isMethodDeclaration(property) || ts.isAccessor(property) || @@ -4978,7 +4979,7 @@ export function lowerVarDecl( ) { let initExpr: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(initExpr)) initExpr = initExpr.expression; - if (ts.isArrayLiteralExpression(initExpr) && initExpr.elements.length === 0) { + if (ts.isArrayLiteralExpression(initExpr) && initExpr.elements.nodes.length === 0) { const t = lowerer.typeOf(decl.name); if (lowerer.checker.isArrayType(t)) { const elem = lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]; @@ -5318,7 +5319,7 @@ function lowerVariableInitializer( if (expected?.kind === "dyn") return lowerDynObjectLiteral(lowerer, value); const previous = new Map(); - for (const property of value.properties) { + for (const property of value.properties.nodes) { if (!ts.isPropertyAssignment(property) || !ts.isComputedPropertyName(property.name)) continue; const keyNode = property.name.expression; const optional = @@ -5429,7 +5430,7 @@ function jsObjectBindingExpands( targetSymbol && aliases.has(targetSymbol) && ts.isObjectLiteralExpression(value) && - value.properties.some( + value.properties.nodes.some( (field) => !(ts.isPropertyAssignment(field) || ts.isShorthandPropertyAssignment(field)) || !(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) || @@ -5445,7 +5446,7 @@ function jsObjectBindingExpands( node.expression.name.text === "setPrototypeOf" && lowerer.isStdlibGlobal(node.expression.expression, "Object") ) { - const target = node.arguments[0]; + const target = node.arguments.nodes[0]; const sym = target && ts.isIdentifier(target) ? lowerer.resolveValueSymbol(target) : undefined; if (sym && aliases.has(sym)) expands = true; @@ -5555,7 +5556,7 @@ function immediateAbsenceGuard( if (!statement || !ts.isVariableStatement(statement)) return null; const container = statement.parent; const statements = - ts.isBlock(container) || ts.isSourceFile(container) ? container.statements : null; + ts.isBlock(container) || ts.isSourceFile(container) ? container.statements.nodes : null; if (!statements) return null; const index = statements.indexOf(statement); const next = index >= 0 ? statements[index + 1] : undefined; @@ -5614,7 +5615,7 @@ function falsyExitRuntimeOptionalLocals(lowerer: Lowerer, stmt: ts.IfStatement): if (stmt.elseStatement) return []; let tail = stmt.thenStatement; while (ts.isBlock(tail)) { - const last = tail.statements[tail.statements.length - 1]; + const last = tail.statements.nodes[tail.statements.nodes.length - 1]; if (!last) return []; tail = last; } @@ -5662,7 +5663,7 @@ export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt lowerer.scopes.push(new Map()); try { const cases: { test: IrExpr | null; body: IrStmt[] }[] = []; - for (const clause of stmt.caseBlock.clauses) { + for (const clause of stmt.caseBlock.clauses.nodes) { let test: IrExpr | null = null; if (ts.isCaseClause(clause)) { test = lowerer.lowerExpr(clause.expression); @@ -5678,7 +5679,10 @@ export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt test, body: lowerer.inCtl( "switch", - () => withRuntimeOptionalScope(lowerer, () => lowerer.lowerStmts(clause.statements)), + () => + withRuntimeOptionalScope(lowerer, () => + lowerer.lowerStmts(clause.statements.nodes), + ), labels, ), }); @@ -5780,7 +5784,7 @@ function lowerBranchSwitch( // Calls/assignments in a case test may mutate a captured discriminant // binding. The checker still narrows that binding as if it were the // snapshot. Keep the original tagged value in those case bodies. - const effectful = stmt.caseBlock.clauses.some( + const effectful = stmt.caseBlock.clauses.nodes.some( (clause) => ts.isCaseClause(clause) && !ts.isStringLiteral(clause.expression) && @@ -5798,13 +5802,13 @@ function lowerBranchSwitch( if (symbol) lowerer.unstableSwitchBindings.add(symbol); lowerer.scopes.push(new Map()); try { - const cases = stmt.caseBlock.clauses.map((clause) => ({ + const cases = stmt.caseBlock.clauses.nodes.map((clause) => ({ test: ts.isCaseClause(clause) ? compare(stableDisc, lowerer.lowerExpr(clause.expression), clause.expression) : null, body: lowerer.inCtl( "switch", - () => withRuntimeOptionalScope(lowerer, () => lowerer.lowerStmts(clause.statements)), + () => withRuntimeOptionalScope(lowerer, () => lowerer.lowerStmts(clause.statements.nodes)), labels, ), })); @@ -5895,12 +5899,12 @@ function isStrictDelete(node: ts.DeleteExpression): boolean { ts.isFunctionLike(parent) && parent.body && ts.isBlock(parent.body) && - hasUseStrictDirective(parent.body.statements) + hasUseStrictDirective(parent.body.statements.nodes) ) return true; if (ts.isSourceFile(parent)) { return ( - hasUseStrictDirective(parent.statements) || + hasUseStrictDirective(parent.statements.nodes) || (!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent))) ); } @@ -6179,7 +6183,7 @@ function lowerCjsExportStatement( if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc }; const sf = cjs.expr.getSourceFile(); let root: IrGlobal | undefined; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const whole = cjsExportAssignmentOf(stmt); if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue; root = lowerer.globalsByDeclNode.get(whole.expr); @@ -6270,7 +6274,7 @@ function lowerCjsExportStatement( ); } }; - for (const prop of table.properties) { + for (const prop of table.properties.nodes) { if (ts.isShorthandPropertyAssignment(prop)) { fenceMutable(prop.name as ts.Identifier); continue; // alias plumbing @@ -7333,7 +7337,7 @@ function droppedPureStdlibStatement(lowerer: Lowerer, expr: ts.Expression): bool if (!isStdlibMember(lowerer, proto) || !isStdlibMember(lowerer, callee)) return false; const root = stdlibGlobalNameOf(lowerer, strip(proto.expression)); if (root !== "Array" && root !== "String") return false; - return expr.arguments.every((a) => sideEffectFreeOptionValue(a)); + return expr.arguments.nodes.every((a) => sideEffectFreeOptionValue(a)); } if (ts.isPropertyAccessExpression(expr)) { return stdlibGlobalNameOf(lowerer, strip(expr.expression)) !== null; @@ -7516,8 +7520,8 @@ function destructuringAssignInto( // globals and strings alike). { const patternEmpty = ts.isObjectLiteralExpression(target) - ? target.properties.length === 0 - : target.elements.length === 0; + ? target.properties.nodes.length === 0 + : target.elements.nodes.length === 0; if (!patternEmpty && rhs !== null && isJsSourceFile(rhs.getSourceFile())) { const globalName = stdlibGlobalNameOf(lowerer, rhs); if (globalName !== null) { @@ -7573,7 +7577,7 @@ function destructuringAssignInto( ); } let firstProp: string | undefined; - const p0 = target.properties[0]; + const p0 = target.properties.nodes[0]; if (p0 !== undefined) { if (ts.isShorthandPropertyAssignment(p0)) firstProp = (p0.name as ts.Identifier).text; else if (ts.isPropertyAssignment(p0)) { @@ -7595,7 +7599,7 @@ function destructuringAssignInto( loc, }); } - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { const piece = destructAssignPiece(lowerer, prop); const read: IrExpr = { kind: "dynKeyGet", @@ -7613,7 +7617,7 @@ function destructuringAssignInto( } return { stmts: out, value }; } - if (target.properties.length === 0) { + if (target.properties.nodes.length === 0) { // `({} = e)`: RequireObjectCoercible and nothing else. A source the // type system already proves non-nullish is a pure no-op past the // RHS's own evaluation; possibly-nullish unions would throw in JS @@ -7643,13 +7647,13 @@ function destructuringAssignInto( if (!lowerer.classes.get(objT.className)) lowerer.flushDeferredClass(objT.className); const info = lowerer.classes.get(objT.className); if (info) { - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { const propLoc = locOf(prop); if (ts.isSpreadAssignment(prop)) { let restTo: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(restTo)) restTo = restTo.expression; const consumed = new Set(); - for (const p of target.properties) { + for (const p of target.properties.nodes) { if (ts.isShorthandPropertyAssignment(p)) consumed.add((p.name as ts.Identifier).text); else if (ts.isPropertyAssignment(p)) { const n = patternKeyNameOf(lowerer, p.name); @@ -7786,7 +7790,7 @@ function destructuringAssignInto( // properties keep the declaration path's named fences. Callers vetted // the source as a checker-typed string. if (init.type.kind === "string") { - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { const propLoc = locOf(prop); if (ts.isSpreadAssignment(prop)) { lowerer.unsupported( @@ -7861,7 +7865,7 @@ function destructuringAssignInto( const shape = lowerer.shapes.get(srcType.shapeId); if (!shape) throw new InternalCompilerError(`lowerer bug: destructuring unknown shape ${srcType.shapeId}`); - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { // Shorthand `{ a }` assigns local a from field a; `{ a: x }` assigns // x from field a (a PropertyAssignment whose initializer is the // target — JS reuses the literal syntax with inverted roles). @@ -7881,7 +7885,7 @@ function destructuringAssignInto( ); } const consumed = new Set(); - for (const p of target.properties) { + for (const p of target.properties.nodes) { if (ts.isShorthandPropertyAssignment(p)) consumed.add((p.name as ts.Identifier).text); else if (ts.isPropertyAssignment(p)) { const n = patternKeyNameOf(lowerer, p.name); @@ -8227,7 +8231,7 @@ function lowerArrayAssignInto( // array divergence). The EMPTY pattern skips the split — GetIterator // + close, nothing observable — through the iterable early-return // below. Callers vetted the source as a checker-typed string. - if (srcType.kind === "string" && target.elements.length > 0) { + if (srcType.kind === "string" && target.elements.nodes.length > 0) { const charsT = arrayOf(STRING); const chars = lowerer.declareHiddenLocal("%dchars", charsT); out.push({ @@ -8245,13 +8249,13 @@ function lowerArrayAssignInto( // consumes exactly the elements the inner pattern alone would, so the // unwrap is exact. Identifier rest operands pack in the element loop // below. - let elements: readonly ts.Expression[] = target.elements; + let elements: readonly ts.Expression[] = target.elements.nodes; if ( elements.length === 1 && ts.isSpreadElement(elements[0]!) && ts.isArrayLiteralExpression(elements[0]!.expression) ) { - elements = elements[0]!.expression.elements; + elements = elements[0]!.expression.elements.nodes; } // The element-read builder per source kind; null when the source // cannot destructure. @@ -8573,7 +8577,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { while (ts.isParenthesizedExpression(src)) src = src.expression; if ( ts.isCallExpression(src) && - src.arguments.length === 0 && + src.arguments.nodes.length === 0 && !src.questionDotToken && ts.isPropertyAccessExpression(src.expression) && !src.expression.questionDotToken && @@ -8637,7 +8641,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { const constIdentBinding = ts.isVariableDeclarationList(stmt.initializer) && (stmt.initializer.flags & ts.NodeFlags.Const) !== 0 && - ts.isIdentifier(stmt.initializer.declarations[0]!.name); + ts.isIdentifier(stmt.initializer.declarations.nodes[0]!.name); if (constIdentBinding) { const call = directMatchAllCallOf(lowerer, src); if (call) return lowerForOfMatchAll(lowerer, stmt, call, null); @@ -8668,7 +8672,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { if ( ts.isCallExpression(e) && !e.questionDotToken && - e.arguments.length === 0 && + e.arguments.nodes.length === 0 && ts.isPropertyAccessExpression(e.expression) && !e.expression.questionDotToken && e.expression.name.text === "values" && @@ -9181,7 +9185,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { const resourceLoop = (list.flags & ts.NodeFlags.Using) !== 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one // `for (const [k, v] of pairs)` — a destructuring loop variable: the // element binds to a hidden per-iteration local and the pattern // desugars to reads of it at the top of the body, exactly the @@ -9362,7 +9366,7 @@ function lowerForOfStoredNumericIterator( lowerer.unsupported("SC1090", list, "'using' declarations (dispose-at-scope-exit semantics)"); } const isLet = list !== null && (list.flags & ts.NodeFlags.Let) !== 0; - const decl = list ? list.declarations[0]! : null; + const decl = list ? list.declarations.nodes[0]! : null; if (decl && !ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const loc = locOf(stmt); @@ -9499,13 +9503,13 @@ function directMatchAllCallOf(lowerer: Lowerer, e: ts.Expression): ts.CallExpres if ( ts.isCallExpression(src) && !src.questionDotToken && - src.arguments.length === 1 && + src.arguments.nodes.length === 1 && ts.isPropertyAccessExpression(src.expression) && !src.expression.questionDotToken && src.expression.name.text === "matchAll" && lowerer.isStdlibMember(src.expression) && lowerer.mapTypeOf(lowerer.typeOf(src.expression.expression))?.kind === "string" && - lowerer.mapTypeOf(lowerer.typeOf(src.arguments[0]!))?.kind === "regex" + lowerer.mapTypeOf(lowerer.typeOf(src.arguments.nodes[0]!))?.kind === "regex" ) { return src; } @@ -9537,7 +9541,7 @@ function lowerForOfMatchAll( stored: { rows: IrExpr; idxsLocalId: string } | null, ): IrStmt { const list = stmt.initializer as ts.VariableDeclarationList; - const decl = list.declarations[0]!; + const decl = list.declarations.nodes[0]!; const name = decl.name as ts.Identifier; const loc = locOf(stmt); const rowT = arrayOf(STRING); @@ -9551,7 +9555,7 @@ function lowerForOfMatchAll( if (call !== null) { const access = call.expression as ts.PropertyAccessExpression; const receiver = lowerer.lowerExpr(access.expression); - const re = lowerer.lowerExpr(call.arguments[0]!); + const re = lowerer.lowerExpr(call.arguments.nodes[0]!); const idxs = lowerer.declareHiddenLocal("%midxs", idxsT); idxsLocalId = idxs.id; prelude.push({ @@ -9708,7 +9712,7 @@ function lowerForOfClassIterator( lowerer.unsupported("SC1090", list, "'using' declarations (dispose-at-scope-exit semantics)"); } const isLet = list !== null && (list.flags & ts.NodeFlags.Let) !== 0; - const decl = list ? list.declarations[0]! : null; + const decl = list ? list.declarations.nodes[0]! : null; if (decl && !ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const loc = locOf(stmt); lowerer.scopes.push(new Map()); @@ -9940,7 +9944,7 @@ function lowerForOfDyn( } const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; - const decl = list.declarations[0]!; + const decl = list.declarations.nodes[0]!; if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { const tmp = lowerer.declareHiddenLocal("%destr", DYN); binds.push({ kind: "varDecl", localId: tmp.id, init: elemInit(), loc }); @@ -10123,7 +10127,7 @@ function lowerForOfString( lowerer.unsupported("SC1090", list, "'using' declarations (dispose-at-scope-exit semantics)"); } const isLet = list !== null && (list.flags & ts.NodeFlags.Let) !== 0; - const decl = list ? list.declarations[0]! : null; // the grammar allows exactly one + const decl = list ? list.declarations.nodes[0]! : null; // the grammar allows exactly one const loc = locOf(stmt); lowerer.scopes.push(new Map()); try { @@ -10489,7 +10493,7 @@ function lowerForInOverKeys( ); } const list = stmt.initializer; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; @@ -10602,7 +10606,7 @@ function lowerForInArray( }, ]; } else { - const decl = stmt.initializer.declarations[0]!; + const decl = stmt.initializer.declarations.nodes[0]!; if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const isLet = (stmt.initializer.flags & ts.NodeFlags.Let) !== 0 || @@ -10713,7 +10717,7 @@ function lowerForAwaitStdin(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // threading it through a shared hoisted slot has no user yet. if (!isConst && !isLet) lowerer.unsupported("SC1030", list, "'var' loop bindings in 'for await' (use const)"); - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const loc = locOf(stmt); const promiseT: IrType = { kind: "promise", inner: BYTES_U8 }; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 5f3d56114..7639e0974 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -275,7 +275,7 @@ function lowerStreamCallback( ): IrExpr { const loc = locOf(node); const { shapes, funcType } = lowerer.lambdaSignature(node); - if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { + if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { lowerer.unsupported("SC1090", node, `async stream '${which}' callbacks`); } if (funcType.params.length > fullTuple.length) { @@ -309,10 +309,10 @@ function lowerStreamCallback( const thisLocal = ts.isArrowFunction(node) ? lowerer.declareHiddenLocal("this", thisType) : lowerer.declareThis(thisType); - const { params, prologue } = lowerer.declareParams(node.parameters, shapes); + const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes); let body: IrStmt[]; if (node.body !== undefined && ts.isBlock(node.body)) { - body = lowerer.lowerStmts(node.body.statements); + body = lowerer.lowerStmts(node.body.statements.nodes); } else if (node.body !== undefined) { // Bare-expression bodies (`read: () => this.push(null)`): the value // is discarded when the signature returns void — Node ignores @@ -627,7 +627,7 @@ function parseStreamOptions( } const tuples = cbTuples(lowerer); const present = new Map(); - for (const prop of optsLit?.properties ?? []) { + for (const prop of optsLit?.properties?.nodes ?? []) { const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name; const name = propName && ts.isIdentifier(propName) @@ -932,7 +932,7 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C const cls = info.def.name; if (!RUNTIME_STREAM_CLASSES.has(cls)) lowerer.unsupported("SC1090", expr, `constructing ${cls}`); const thisType: IrType = { kind: "object", className: cls }; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering( `new ${info.def.name.slice(1)} with ${args.length} arguments`, @@ -1410,15 +1410,15 @@ export function lowerStreamStaticCall( if ( member === "toWeb" && cls === "%Readable" && - call.arguments.length === 2 && + call.arguments.nodes.length === 2 && isJsSourceFile(call.getSourceFile()) && - ts.isObjectLiteralExpression(call.arguments[1]!) + ts.isObjectLiteralExpression(call.arguments.nodes[1]!) ) { - for (const p of call.arguments[1]!.properties) { + for (const p of call.arguments.nodes[1]!.properties.nodes) { if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type") { const t = lowerer.typeOf(p.initializer); if (t.isStringLiteralType() && t.value !== "bytes") { - lowerer.lowerExpr(call.arguments[0]!); // evaluation order (the stream argument) + lowerer.lowerExpr(call.arguments.nodes[0]!); // evaluation order (the stream argument) return nodeThrowExpr( 1, "ERR_INVALID_ARG_VALUE", @@ -1436,7 +1436,7 @@ export function lowerStreamStaticCall( if (cls !== "%Readable") { lowerer.noLowering(`${cls.slice(1)}.from`, call, "Readable.from is the lowered form"); } - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 2) { lowerer.noLowering( `Readable.from with ${args.length} arguments`, @@ -1955,7 +1955,7 @@ function fromOptions( if (!ts.isObjectLiteralExpression(node)) return null; let flags = 0; let hwm: ts.Expression | null = null; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { if ( !ts.isPropertyAssignment(prop) || !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) @@ -2062,7 +2062,7 @@ function lowerStreamArg( argTypeThrow("body", PIPELINE_STAGE_EXPECTED); } } else if ( - (ts.isObjectLiteralExpression(node) && node.properties.length === 0) || + (ts.isObjectLiteralExpression(node) && node.properties.nodes.length === 0) || v.type.kind === "record" || v.type.kind === "string" || v.type.kind === "f64" || @@ -2193,7 +2193,7 @@ function eosOptions( "pass true or false", ); }; - for (const p of node.properties) { + for (const p of node.properties.nodes) { const key = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) ? p.name.text @@ -2236,7 +2236,7 @@ function pipelineOptions( node: ts.ObjectLiteralExpression, ): { end: boolean; signal: IrExpr | null } { const out = { end: true, signal: null as IrExpr | null }; - for (const p of node.properties) { + for (const p of node.properties.nodes) { const key = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) ? p.name.text @@ -2279,7 +2279,7 @@ export function lowerStreamModuleCall( loc: SrcLoc, ): IrExpr | null { if (bi.module === "stream/promises") { - const args = call.arguments; + const args = call.arguments.nodes; // finished(stream) → a pending void promise the terminal watcher // settles (rejected with the stream's error, or // ERR_STREAM_PREMATURE_CLOSE — the callback form's status, boxed @@ -2389,7 +2389,7 @@ export function lowerStreamModuleCall( return null; } if (bi.module === "stream/consumers") { - const args = call.arguments; + const args = call.arguments.nodes; // text/json/buffer/arrayBuffer(stream) → a pending promise the // accumulate-and-settle machinery resolves at the terminal point (the // utf8 decode / the parsed JSON dyn tree / the concatenated bytes / an @@ -2440,7 +2440,7 @@ export function lowerStreamModuleCall( return null; } if (bi.module !== "stream") return null; - const args = call.arguments; + const args = call.arguments.nodes; if (bi.member === "getDefaultHighWaterMark") { // Node 24: 65536 bytes / 16 objects — compile-time literals only. @@ -2657,7 +2657,7 @@ function lowerPipelineStages( for (let i = first; i < end; i++) { const fn = stageArgs[i] as ts.FunctionExpression | ts.ArrowFunction; const isLast = destFn && i === end - 1; - const isAsync = (fn.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); const isGen = ts.isFunctionExpression(fn) && fn.asteriskToken !== undefined; if (isLast && isGen) { // pipeline() pumps it into an object-mode PassThrough, whose @@ -2680,10 +2680,10 @@ function lowerPipelineStages( ); } const optionsAt = srcT === null ? 0 : 1; - if (fn.parameters.length > optionsAt + 1) { + if (fn.parameters.nodes.length > optionsAt + 1) { lowerer.noLowering( "pipeline stages with parameters after the options argument", - fn.parameters[optionsAt + 1]!, + fn.parameters.nodes[optionsAt + 1]!, "a stage takes the source and { signal }", ); } @@ -2693,7 +2693,7 @@ function lowerPipelineStages( if (params[optionsAt]!.kind !== "dyn") { lowerer.noLowering( "a pipeline stage's options argument with this type", - fn.parameters[optionsAt]!, + fn.parameters.nodes[optionsAt]!, "type it as { signal: AbortSignal }", ); } @@ -2897,7 +2897,7 @@ function lowerStageFunction( fn: ts.FunctionExpression | ts.ArrowFunction, srcT: IrType | null, ): { closure: IrExpr; out: IrType & ({ kind: "generator" } | { kind: "promise" }) } { - const param = srcT === null ? undefined : fn.parameters[0]; + const param = srcT === null ? undefined : fn.parameters.nodes[0]; const symbol = param && ts.isIdentifier(param.name) ? lowerer.checker.getSymbolAtLocation(param.name) @@ -2955,7 +2955,7 @@ function lowerStageFunction( * the wrong position, a single Readable or Writable (Duplex.from wraps * those), and functions, iterables or web streams have no lowering. */ function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; const duplex: IrType = { kind: "object", className: "%Duplex" }; if (args.some((a) => ts.isSpreadElement(a))) { lowerer.noLowering("compose with spread arguments", call, "pass the streams directly"); @@ -3048,9 +3048,9 @@ function lowerOperatorCallback( ); if ( (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && - node.parameters.length > maxParams + node.parameters.nodes.length > maxParams ) { - optionsParam(node.parameters[maxParams]!); + optionsParam(node.parameters.nodes[maxParams]!); } const v = lowerer.lowerExpr(node); if (v.type.kind === "dyn") return v; @@ -3135,7 +3135,7 @@ function lowerStreamOperator( member: string, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const recv = lowerer.lowerExpr(access.expression); const readable: IrType = { kind: "object", className: "%Readable" }; const opts = (i: number): IrExpr => @@ -3267,7 +3267,7 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc "{ end: } inline is the supported form", ); } - for (const prop of options.properties) { + for (const prop of options.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name) || prop.name.text !== "end") { lowerer.noLowering("a pipe option other than 'end'", prop); } @@ -3292,7 +3292,7 @@ export function lowerStreamMethodCall( const sides = streamSidesOf(lowerer, info); if (!sides) return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const readableSide = sides === "r" || sides === "rw"; const writableSide = sides === "w" || sides === "rw"; const cls = info.def.name.slice(1); diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index c31e674af..9677de6ff 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -173,7 +173,7 @@ function lowerTestOptions( ); } const out = noOptions(); - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { if ( !(ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) && !ts.isShorthandPropertyAssignment(prop) @@ -353,7 +353,7 @@ function splitTestArgs( expr: ts.CallExpression, what: string, ): { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } { - const args = expr.arguments; + const args = expr.arguments.nodes; const isFn = (a: ts.Expression): boolean => lowerer.checker.getCallSignatures(lowerer.typeOf(a)).length > 0; const isOptions = (a: ts.Expression): boolean => ts.isObjectLiteralExpression(a); @@ -589,7 +589,7 @@ function lowerHook( loc: SrcLoc, ): IrExpr { requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length !== 1) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -726,7 +726,7 @@ function lowerSnapshotAssert( return varRef(local.id, type, loc); }; // Arguments evaluate in order before the assertion serializes, as in Node. - const valueNode = call.arguments[0]; + const valueNode = call.arguments.nodes[0]; const input = slot( "%snapshotInput", DYN, @@ -734,7 +734,7 @@ function lowerSnapshotAssert( ); let path: IrExpr | null = null; if (member === "fileSnapshot") { - const pathNode = call.arguments[1]; + const pathNode = call.arguments.nodes[1]; path = slot( "%snapshotPath", STRING, @@ -742,11 +742,11 @@ function lowerSnapshotAssert( ); } const serializers = - snapshotSerializers(lowerer, call.arguments[member === "snapshot" ? 1 : 2], what)?.map((fn) => - slot("%snapshotSerializer", DYN, fn), + snapshotSerializers(lowerer, call.arguments.nodes[member === "snapshot" ? 1 : 2], what)?.map( + (fn) => slot("%snapshotSerializer", DYN, fn), ) ?? null; - if (call.arguments.length > (member === "snapshot" ? 2 : 3)) { - lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > (member === "snapshot" ? 2 : 3)) { + lowerer.noLowering(`${what} with ${call.arguments.nodes.length} arguments`, call); } const text: IrExpr = { kind: "toString", @@ -813,7 +813,7 @@ function snapshotSerializers( lowerer.noLowering(`${what} options that are not an object literal`, options, hint); } let fns: IrExpr[] | null = null; - for (const prop of options.properties) { + for (const prop of options.properties.nodes) { const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null; if ( name !== "serializers" || @@ -825,7 +825,7 @@ function snapshotSerializers( if (!ts.isArrayLiteralExpression(list)) { lowerer.noLowering(`${what} with a non-literal serializers list`, list, hint); } - fns = list.elements.map((el) => { + fns = list.elements.nodes.map((el) => { if (ts.isSpreadElement(el)) lowerer.noLowering(`${what} with a spread serializer`, el, hint); return lowerer.lowerExprExpecting(el, DYN); }); @@ -1023,7 +1023,7 @@ export function lowerTestMethodCall( // The TestContext receiver surface. if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "testCtx") return null; if (!lowerer.isStdlibMember(access)) return null; - const args = call.arguments; + const args = call.arguments.nodes; if (member === "test") { const receiver = lowerer.lowerExpr(access.expression); return lowerRegistration(lowerer, call, "t.test(...)", loc, 0, receiver); diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index c2a54dfc0..740ff66f1 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -284,14 +284,14 @@ export const TIMER_MODULE_MEMBERS: ReadonlySet = new Set([ * path. Null when the argument might be a real handle. */ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr | null { const noop: IrExpr = { kind: "libCall", fn: "timers.clearNoop", args: [], type: VOID, loc }; - if (expr.arguments.length === 0) return noop; - if (expr.arguments.length !== 1) return null; - let arg = expr.arguments[0]!; + if (expr.arguments.nodes.length === 0) return noop; + if (expr.arguments.nodes.length !== 1) return null; + let arg = expr.arguments.nodes[0]!; // `{} as never` / parenthesized spellings: the cast changes no value. while (ts.isAsExpression(arg) || ts.isTypeAssertion(arg) || ts.isParenthesizedExpression(arg)) arg = arg.expression; if (arg.kind === ts.SyntaxKind.NullKeyword) return noop; - if (ts.isObjectLiteralExpression(arg) && arg.properties.length === 0) return noop; + if (ts.isObjectLiteralExpression(arg) && arg.properties.nodes.length === 0) return noop; if (ts.isIdentifier(arg)) { const t = lowerer.mapTypeOf(lowerer.typeOf(arg)); if (t && (t.kind === "nullT" || t.kind === "undefinedT")) return noop; @@ -369,7 +369,7 @@ function lowerTimerCall( // Delayed timers share callback adaptation and the 1ms default. Trailing // arguments are captured now and delivered when the callback fires. if (member === "setTimeout" || member === "setInterval") { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { lowerer.noLowering( `${member} with 0 arguments`, expr, @@ -377,19 +377,25 @@ function lowerTimerCall( ); } const cb = - expr.arguments.length > 2 - ? makeTimerArgsThunk(lowerer, expr.arguments[0]!, expr.arguments.slice(2), member, loc) + expr.arguments.nodes.length > 2 + ? makeTimerArgsThunk( + lowerer, + expr.arguments.nodes[0]!, + expr.arguments.nodes.slice(2), + member, + loc, + ) : adaptZeroArgTimerCallback( lowerer, - lowerer.lowerExpr(expr.arguments[0]!), - expr.arguments[0]!, + lowerer.lowerExpr(expr.arguments.nodes[0]!), + expr.arguments.nodes[0]!, loc, ); // An omitted delay is NaN, Node's undefined: the loop clamps it to // 1ms, and mock.timers queues it to run at the next tick. const ms: IrExpr = - expr.arguments.length >= 2 - ? lowerer.lowerExpr(expr.arguments[1]!) + expr.arguments.nodes.length >= 2 + ? lowerer.lowerExpr(expr.arguments.nodes[1]!) : { kind: "numLit", value: NaN, type: F64, loc }; if (member === "setInterval") { return { kind: "libCall", fn: "timers.setInterval", args: [cb, ms], type: F64, loc }; @@ -410,10 +416,10 @@ function lowerTimerCall( if (member === "clearTimeout") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearTimeout with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearTimeout with ${expr.arguments.nodes.length} arguments`, expr); } - let handle = lowerer.lowerExpr(expr.arguments[0]!); + let handle = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (handle.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { const stored = lowerer.declareHiddenLocal("%timer.clear", DYN); const ref = varRef(stored.id, DYN, loc); @@ -423,7 +429,7 @@ function lowerTimerCall( result: { kind: "ternary", cond: { kind: "dynTest", test: "number", value: ref, type: BOOL, loc }, - then: lowerer.coerceInto(expr.arguments[0]!, ref, F64), + then: lowerer.coerceInto(expr.arguments.nodes[0]!, ref, F64), else_: numLit(0, loc), type: F64, loc, @@ -436,7 +442,7 @@ function lowerTimerCall( if (handle.type.kind !== "f64") { lowerer.noLowering( `clearTimeout of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Timeout setTimeout returned, or undefined or null", ); } @@ -445,14 +451,14 @@ function lowerTimerCall( if (member === "clearInterval") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearInterval with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearInterval with ${expr.arguments.nodes.length} arguments`, expr); } - const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments.nodes[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearInterval of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Timeout setInterval returned, or undefined or null", ); } @@ -462,7 +468,7 @@ function lowerTimerCall( // the f64 immediate id (its own space — clearTimeout of an Immediate // no-ops, like Node); the callback adapts like setTimeout's. if (member === "setImmediate") { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { lowerer.noLowering( "setImmediate with 0 arguments", expr, @@ -470,18 +476,18 @@ function lowerTimerCall( ); } const cb = - expr.arguments.length > 1 + expr.arguments.nodes.length > 1 ? makeTimerArgsThunk( lowerer, - expr.arguments[0]!, - expr.arguments.slice(1), + expr.arguments.nodes[0]!, + expr.arguments.nodes.slice(1), "setImmediate", loc, ) : adaptZeroArgTimerCallback( lowerer, - lowerer.lowerExpr(expr.arguments[0]!), - expr.arguments[0]!, + lowerer.lowerExpr(expr.arguments.nodes[0]!), + expr.arguments.nodes[0]!, loc, ); return { kind: "libCall", fn: "timers.setImmediate", args: [cb], type: F64, loc }; @@ -489,14 +495,14 @@ function lowerTimerCall( if (member === "clearImmediate") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearImmediate with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearImmediate with ${expr.arguments.nodes.length} arguments`, expr); } - const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments.nodes[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearImmediate of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Immediate setImmediate returned, or undefined or null", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-url.ts b/packages/compiler/src/frontend/lowering/lower-url.ts index f12d6b267..ee7e4a0af 100644 --- a/packages/compiler/src/frontend/lowering/lower-url.ts +++ b/packages/compiler/src/frontend/lowering/lower-url.ts @@ -35,7 +35,7 @@ function lowerUrlOperation( expr: ts.NewExpression | ts.CallExpression, operation: "new" | "canParse" | "parse", ): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const loc = locOf(expr); const resultType = operation === "new" ? URL_T : operation === "canParse" ? BOOL : DYN; const checkedResult = (value: IrExpr): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 1873ac361..b0985e36d 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -677,7 +677,7 @@ export interface LowerStats { export function boundIdentifiersOf(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const out: ts.Identifier[] = []; - for (const el of name.elements) { + for (const el of name.elements.nodes) { // Elisions: OmittedExpression in 5.9.3, a NAMELESS BindingElement in 7 // (the parity battery's pinned finding 2) — both spell "no binding". if (ts.isOmittedExpression(el) || el.name === undefined) continue; @@ -694,7 +694,7 @@ export function countBodyStatements(root: ts.Node): number { let n = 0; const visit = (node: ts.Node): void => { if (ts.isBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) { - n += node.statements.length; + n += node.statements.nodes.length; } else if ( (ts.isIfStatement(node) || ts.isForStatement(node) || @@ -1185,7 +1185,7 @@ export function staticImportNamespaceType( } else break; } if (!ts.isCallExpression(e) || e.expression.kind !== ts.SyntaxKind.ImportKeyword) return null; - const arg = e.arguments[0]; + const arg = e.arguments.nodes[0]; if (!arg || !ts.isStringLiteralLike(arg)) return null; const builtin = canonicalBuiltinModule(arg.text); let moduleId: string | null = builtin === null ? null : `builtin:${builtin}`; @@ -1502,15 +1502,15 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { !isJsSourceFile(decl.getSourceFile()) ) return null; - const hasRest = decl.parameters.some((p) => p.dotDotDotToken !== undefined); + const hasRest = decl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined); if ( !hasRest && (ts.isArrowFunction(decl) || !bodyReadsArguments(decl) || - (decl.parameters.length > 0 && !isNodeEsmFile(decl.getSourceFile(), lowerer.program))) + (decl.parameters.nodes.length > 0 && !isNodeEsmFile(decl.getSourceFile(), lowerer.program))) ) return null; - const shapes = paramShapes(lowerer, decl.parameters); + const shapes = paramShapes(lowerer, decl.parameters.nodes); const retType = lowerer.checker.getReturnTypeOfSignature(sigs[0]!); const ret = retType.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined) @@ -1546,7 +1546,7 @@ function pureSingleCallSignatureOf(lowerer: Lowerer, t: ts.Type): ts.Signature | (() => { const sigDecl = lowerer.checker.signatureDeclaration(sigs[0]!); const declParams = - sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined; + sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if (declParams !== undefined && declParams.length !== sigs[0]!.getParameters().length) return false; // tsgo never synthesizes the `arguments` pseudo-rest into the @@ -3029,10 +3029,10 @@ export class Lowerer { if ( !this.classes.has(this.classNamer(decl)) && !this.collectingExprClasses.has(decl) && - decl.heritageClauses?.some( + decl.heritageClauses?.nodes.some( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword && - clause.types.some((base) => { + clause.types.nodes.some((base) => { const expression = base.expression; if (ts.isCallExpression(expression)) return true; if (ts.isIdentifier(expression) && this.isStdlibGlobal(expression, expression.text)) @@ -3071,7 +3071,7 @@ export class Lowerer { continue; if (this.localClassInstantiations.some((entry) => entry.owner === parent)) break; if ( - parent.typeParameters?.length || + parent.typeParameters?.nodes.length || (implicitMonoFile(parent.getSourceFile()) && monomorphizedImplicit(this, parent)) ) return null; @@ -3405,7 +3405,7 @@ export class Lowerer { } if (ts.isCallExpression(value)) { if (value.expression.kind === ts.SyntaxKind.ImportKeyword) { - const spec = value.arguments[0]; + const spec = value.arguments.nodes[0]; return spec !== undefined && ts.isStringLiteralLike(spec) && this.externalTypes.has(spec.text) @@ -3415,9 +3415,9 @@ export class Lowerer { if ( ts.isIdentifier(value.expression) && value.expression.text === "require" && - value.arguments.length === 1 + value.arguments.nodes.length === 1 ) { - const spec = value.arguments[0]!; + const spec = value.arguments.nodes[0]!; if (ts.isStringLiteralLike(spec) && this.externalTypes.has(spec.text)) return spec.text; } return this.externalTypeSpecifierOf(value.expression); @@ -4113,7 +4113,7 @@ export class Lowerer { * frame either). Prepended to the entry's %init. */ punycodeLinkWarning(): IrStmt[] { const imports = this.moduleOrder.some((sf) => - sf.statements.some( + sf.statements.nodes.some( (stmt) => ((ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) || (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) && @@ -4259,7 +4259,7 @@ export class Lowerer { const declared = this.mapTypeOf(this.typeOf(node.name)); if (declared?.kind !== "record") return undefined; let widened: IrType = declared; - for (const property of init.properties) { + for (const property of init.properties.nodes) { const value = ts.isPropertyAssignment(property) ? property.initializer : ts.isShorthandPropertyAssignment(property) @@ -4285,9 +4285,9 @@ export class Lowerer { * initializer as the matching absence probe. */ analyzeGuardedRecordReadGlobals(parts: FileParts[]): void { for (const { sf } of parts) { - for (const statement of sf.statements) { + for (const statement of sf.statements.nodes) { if (!ts.isVariableStatement(statement)) continue; - for (const decl of statement.declarationList.declarations) { + for (const decl of statement.declarationList.declarations.nodes) { if (!ts.isIdentifier(decl.name) || !isGuardedRecordRead(this, decl)) continue; const global = this.globalOf(decl.name); const symbol = this.resolveValueSymbol(decl.name); @@ -4429,7 +4429,7 @@ export class Lowerer { (ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name ? symbolOf(fn.name) : null; - const index = (fn as ts.FunctionLikeDeclaration).parameters.indexOf(parameter); + const index = (fn as ts.FunctionLikeDeclaration).parameters.nodes.indexOf(parameter); return (symbol && signatureBySymbol.get(symbol)?.params[index]?.type) ?? null; }; const returnTypeOf = (fn: ts.FunctionDeclaration | ts.MethodDeclaration): IrType | null => { @@ -4638,12 +4638,12 @@ export class Lowerer { if (isJsSourceFile(sf) || sf.isDeclarationFile) continue; ts.walkPreorder(sf, (node) => { if (!ts.isClassDeclaration(node)) return undefined; - const genericClass = (node.typeParameters?.length ?? 0) > 0; - for (const member of node.members) + const genericClass = (node.typeParameters?.nodes.length ?? 0) > 0; + for (const member of node.members.nodes) if ( ts.isMethodDeclaration(member) && member.body && - (genericClass || (member.typeParameters?.length ?? 0) > 0) + (genericClass || (member.typeParameters?.nodes.length ?? 0) > 0) ) genericClassMethods.push(member); return undefined; @@ -4718,7 +4718,7 @@ export class Lowerer { !this.isStdlibGlobal(init.expression.expression, "Object") ) return false; - const object = init.arguments[0]; + const object = init.arguments.nodes[0]; const objectType = object ? this.mapTypeOf(this.typeOf(object)) : null; return objectType?.kind === "dyn"; }; @@ -4803,7 +4803,7 @@ export class Lowerer { if (ts.isCallExpression(e) && this.runtimeOptionalReduceTypes.has(e)) return true; if ( ts.isCallExpression(e) && - e.arguments.length === 0 && + e.arguments.nodes.length === 0 && ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "stringify" && this.isStdlibGlobal(e.expression.expression, "JSON") @@ -4971,7 +4971,7 @@ export class Lowerer { // A fresh literal carries the fields its values can leave undefined. if (ts.isObjectLiteralExpression(e)) { const out = spreadFieldsOf(e); - for (const prop of e.properties) { + for (const prop of e.properties.nodes) { if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) continue; const name = prop.name; if (!(ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))) @@ -5006,7 +5006,7 @@ export class Lowerer { * the properties a later member writes explicitly. */ const spreadFieldsOf = (object: ts.ObjectLiteralExpression): Set => { const out = new Set(); - for (const prop of object.properties) { + for (const prop of object.properties.nodes) { if (ts.isSpreadAssignment(prop)) { const fields = fieldsOfValue(prop.expression); if (fields !== undefined) for (const field of fields) out.add(field); @@ -5025,7 +5025,7 @@ export class Lowerer { const sourceType = this.mapTypeOf(this.typeOf(init)); if (sourceType?.kind !== "array") return false; let changed = false; - name.elements.forEach((el) => { + name.elements.nodes.forEach((el) => { if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return; if (ts.isIdentifier(el.name)) { const symbol = symbolOf(el.name); @@ -5066,7 +5066,7 @@ export class Lowerer { const indexValue = shape?.indexValue; if (!shape || shape.tuple || !indexValue || indexValue.kind === "dyn") return false; let changed = false; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (el.dotDotDotToken || el.initializer || !ts.isIdentifier(el.name)) continue; const key = el.propertyName ?? el.name; if (!ts.isIdentifier(key) && !ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) @@ -5211,7 +5211,7 @@ export class Lowerer { const promoteNode = (fn: ts.FunctionLikeDeclaration): void => { for (const parameterIndex of parameterIndices) { if (parameterIndex === undefined) continue; - const parameter = fn.parameters[parameterIndex]; + const parameter = fn.parameters.nodes[parameterIndex]; if (!parameter) continue; const symbol = ts.isIdentifier(parameter.name) ? symbolOf(parameter.name) : null; const checkerType = this.typeOf(parameter.name); @@ -5325,7 +5325,9 @@ export class Lowerer { ) ownerSymbol = symbolOf(owner.name); const ownerSig = ownerSymbol ? signatureBySymbol.get(ownerSymbol) : undefined; - const ownerIndex = ts.isFunctionLike(owner) ? owner.parameters.indexOf(declaration) : -1; + const ownerIndex = ts.isFunctionLike(owner) + ? owner.parameters.nodes.indexOf(declaration) + : -1; const slot = ownerIndex >= 0 ? ownerSig?.params[ownerIndex] : undefined; if (slot && !typeEquals(slot.type, promoted)) { slot.type = promoted; @@ -5347,7 +5349,7 @@ export class Lowerer { if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false; let changed = false; for (const parameterIndex of parameterIndices) { - const parameter = fn.parameters[parameterIndex]; + const parameter = fn.parameters.nodes[parameterIndex]; if (!parameter) continue; const checkerType = this.typeOf(parameter.name); if ((checkerType.flags & ts.TypeFlags.TypeParameter) !== 0) continue; @@ -5383,7 +5385,7 @@ export class Lowerer { const fn = peel(callback); let param: IrType | null; if (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) { - const parameter = fn.parameters[index]; + const parameter = fn.parameters.nodes[index]; if (!parameter?.type || parameter.dotDotDotToken) return true; const checkerType = this.typeOf(parameter.name); if ((checkerType.flags & ts.TypeFlags.TypeParameter) !== 0) return true; @@ -5565,12 +5567,12 @@ export class Lowerer { else if (ts.isReturnStatement(node) && node.expression && dynamicValue(node.expression)) target(contextualOrNull(node.expression)); else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { - for (const arg of node.arguments ?? []) + for (const arg of node.arguments?.nodes ?? []) if (!ts.isSpreadElement(arg) && dynamicValue(arg)) target(contextualOrNull(arg)); } else if (ts.isPropertyAssignment(node) && dynamicValue(node.initializer)) target(contextualOrNull(node.initializer)); else if (ts.isArrayLiteralExpression(node)) { - for (const element of node.elements) + for (const element of node.elements.nodes) if ( !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && @@ -5796,7 +5798,7 @@ export class Lowerer { ts.isObjectLiteralExpression(peel(node.initializer)) ) { const object = peel(node.initializer) as ts.ObjectLiteralExpression; - for (const prop of object.properties) { + for (const prop of object.properties.nodes) { if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) continue; const name = prop.name; @@ -5892,9 +5894,9 @@ export class Lowerer { if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { const callbackIndices = hofCallbackIndices( node.expression.name.text, - node.arguments.length >= 2, + node.arguments.nodes.length >= 2, ); - const callback = node.arguments[0]; + const callback = node.arguments.nodes[0]; if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) { const receiverNode = node.expression.expression; const receiverTs = this.typeOf(receiverNode); @@ -5957,7 +5959,7 @@ export class Lowerer { changed = true; if ( (method === "reduce" || method === "reduceRight") && - ((!elementsPresent && node.arguments.length < 2) || + ((!elementsPresent && node.arguments.nodes.length < 2) || callbackReturnsOptional(callback)) ) { if (promoteHofCallback(callback, [0, 1])) changed = true; @@ -5991,7 +5993,7 @@ export class Lowerer { if (!value || !(ts.isArrowFunction(value) || ts.isFunctionExpression(value))) return; // Optional, defaulted and rest parameters keep their own ABI. if ( - value.parameters.some( + value.parameters.nodes.some( (parameter) => parameter.questionToken !== undefined || parameter.initializer !== undefined || @@ -5999,10 +6001,10 @@ export class Lowerer { ) ) return; - const indices = node.arguments.flatMap((arg, i) => + const indices = node.arguments.nodes.flatMap((arg, i) => !ts.isSpreadElement(arg) && - value.parameters[i] !== undefined && - !value.parameters[i]!.dotDotDotToken && + value.parameters.nodes[i] !== undefined && + !value.parameters.nodes[i]!.dotDotDotToken && mayBeOptional(arg) ? [i] : [], @@ -6019,10 +6021,10 @@ export class Lowerer { if ( callee && isJsSourceFile(callee.getSourceFile()) && - !node.arguments.some(ts.isSpreadElement) + !node.arguments.nodes.some(ts.isSpreadElement) ) { for (let i = 0; i < sig.params.length; i++) { - const arg = node.arguments[i]; + const arg = node.arguments.nodes[i]; if ( arg && (this.typeOf(arg).flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0 @@ -6034,7 +6036,7 @@ export class Lowerer { const widened = addUndefined(shape.type); if (typeEquals(shape.type, widened)) continue; shape.type = widened; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; provenance?.note("parameter", parameter, node, "(omitted or undefined argument)"); if (parameter) { for (const bound of boundIdentifiersOf(parameter.name)) { @@ -6054,7 +6056,7 @@ export class Lowerer { (shape) => shape.mode === "rest" || shape.mode === "dynRest" || shape.mode === "arguments", ); - node.arguments.forEach((arg, argumentIndex) => { + node.arguments.nodes.forEach((arg, argumentIndex) => { const i = restAt >= 0 && argumentIndex >= restAt ? restAt : argumentIndex; const shape = sig.params[i]; const callbackSlot = shape?.type; @@ -6074,7 +6076,7 @@ export class Lowerer { const fields = fieldsOfValue(arg)!; for (const target of familyBySymbol.get(symbol) ?? [symbol]) { const parameterShape = signatureBySymbol.get(target)?.params[i]; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; if (!parameterShape || parameterShape.type.kind !== "record") continue; if (!parameter || !ts.isIdentifier(parameter.name)) continue; const bound = symbolOf(parameter.name); @@ -6123,7 +6125,7 @@ export class Lowerer { const widened = arrayOf(addUndefined(rest.type.elem)); if (typeEquals(rest.type, widened)) continue; rest.type = widened; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; provenance?.note("rest-parameter", parameter, arg); if (parameter && ts.isIdentifier(parameter.name)) { const bound = symbolOf(parameter.name); @@ -6148,7 +6150,7 @@ export class Lowerer { optionalParams.set(target, set); if (set.size !== before) { changed = true; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; provenance?.note( "parameter", parameter, @@ -6358,7 +6360,7 @@ export class Lowerer { for (const [symbol, params] of optionalParams) { const decl = functionDeclBySymbol.get(symbol); for (const index of [...params]) { - const name = decl?.parameters[index]?.name; + const name = decl?.parameters?.nodes[index]?.name; const param = name && ts.isIdentifier(name) ? symbolOf(name) : null; if (param && nanCoded.nanCodedBinding(param)) params.delete(index); } @@ -6610,8 +6612,8 @@ export class Lowerer { continue; const symbol = callableSymbolOf(node.expression); const sig = symbol ? signatureBySymbol.get(symbol) : undefined; - if (!sig || node.arguments.some(ts.isSpreadElement)) continue; - node.arguments.forEach((arg, i) => { + if (!sig || node.arguments.nodes.some(ts.isSpreadElement)) continue; + node.arguments.nodes.forEach((arg, i) => { let e = arg; while (ts.isParenthesizedExpression(e)) e = e.expression; if (!ts.isNonNullExpression(e) || !isIndexedRead(e.expression)) return; @@ -7025,7 +7027,9 @@ export class Lowerer { fp.fnDecls .filter((decl) => decl.body !== undefined && decl.typeParameters === undefined) .flatMap((decl) => - decl.parameters.flatMap((param) => (param.initializer ? [param.initializer] : [])), + decl.parameters.nodes.flatMap((param) => + param.initializer ? [param.initializer] : [], + ), ), ), ); @@ -7065,10 +7069,10 @@ export class Lowerer { const bodyRoots = (...roots: (ts.Node | undefined | null)[]): ts.Node[] => roots.filter((root) => root !== undefined && root !== null); const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => - bodyRoots(...decl.parameters.map((param) => param.initializer), decl.body); + bodyRoots(...decl.parameters.nodes.map((param) => param.initializer), decl.body); const classCtorRoots = (info: ClassInfo): ts.Node[] => bodyRoots( - ...(info.ctor?.parameters ?? []).map((param) => param.initializer), + ...(info.ctor?.parameters?.nodes ?? []).map((param) => param.initializer), info.ctor?.body, ...info.fieldOrder.map((field) => field.initializer), ); @@ -9544,13 +9548,13 @@ export class Lowerer { // belong to the array the program keeps, rather than a typed snapshot. if ( ts.isArrayLiteralExpression(x) && - x.elements.every( + x.elements.nodes.every( (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { return { kind: "dynArrLit", - elems: x.elements.map((element) => this.lowerExprExpecting(element, DYN)), + elems: x.elements.nodes.map((element) => this.lowerExprExpecting(element, DYN)), type: DYN, loc: locOf(x), }; @@ -9596,7 +9600,7 @@ export class Lowerer { // a unit-only-element array — a checker type that degraded to one // (`[]`-flavored inference over a populated literal) carries no // element information. - const nonUnitElems = x.elements.some( + const nonUnitElems = x.elements.nodes.some( (el) => !ts.isOmittedExpression(el) && el.kind !== ts.SyntaxKind.NullKeyword && @@ -9608,20 +9612,20 @@ export class Lowerer { (a.kind === "array" && !(nonUnitElems && this.unitOnlyElem(a.elem))) || (a.kind === "record" && !!this.shapes.get(a.shapeId)?.tuple && - this.shapes.get(a.shapeId)!.fields.length === x.elements.length && - !x.elements.some(ts.isSpreadElement)), + this.shapes.get(a.shapeId)!.fields.length === x.elements.nodes.length && + !x.elements.nodes.some(ts.isSpreadElement)), ); let arm = arms.length === 1 ? arms[0] : undefined; if ( !arm && arms.length > 1 && - x.elements.length > 0 && - x.elements.every((el) => !ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) + x.elements.nodes.length > 0 && + x.elements.nodes.every((el) => !ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) ) { const ownType = this.mapTypeOf(this.typeOf(x)); arm = arms.find((candidate) => ownType !== null && typeEquals(candidate, ownType)); if (!arm) { - const elements = x.elements.map((el) => tryLowerExpression(this, el)); + const elements = x.elements.nodes.map((el) => tryLowerExpression(this, el)); arm = arms.find( (candidate) => candidate.kind === "array" && @@ -9661,8 +9665,8 @@ export class Lowerer { if ( !ts.isNewExpression(value) || !ts.isIdentifier(value.expression) || - (value.arguments?.length ?? 0) !== 0 || - (value.typeArguments?.length ?? 0) !== 0 + (value.arguments?.nodes.length ?? 0) !== 0 || + (value.typeArguments?.nodes.length ?? 0) !== 0 ) return null; const symbol = this.resolveValueSymbol(value.expression); @@ -10236,7 +10240,7 @@ export class Lowerer { const typeNodes: ts.Node[] = []; const symbolRoots: ts.Node[] = []; for (const decl of decls) { - for (const member of decl.members) { + for (const member of decl.members.nodes) { if ( (ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member) || @@ -10244,18 +10248,18 @@ export class Lowerer { ts.isSetAccessor(member)) && (!ts.isMethodDeclaration(member) || member.typeParameters === undefined) ) { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (param.initializer) defaultTypeNodes.push(param.initializer); } } } if (!isJsSourceFile(decl.getSourceFile())) continue; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if (ts.isConstructorDeclaration(member)) { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (param.initializer) typeNodes.push(param.initializer); } - for (const stmt of member.body?.statements ?? []) { + for (const stmt of member.body?.statements?.nodes ?? []) { if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; if (stmt.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue; @@ -10277,7 +10281,7 @@ export class Lowerer { ts.isGetAccessor(member) || ts.isSetAccessor(member) ) { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (param.initializer) typeNodes.push(param.initializer); } // npm-static implicit-any classification resolves body parameter @@ -10528,7 +10532,7 @@ export class Lowerer { includeAbstract = false, ): Generator<{ mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }> { if (!info.decl) return; // builtin error classes: runtime-provided bodies - for (const member of info.decl.members) { + for (const member of info.decl.members.nodes) { const fnLike = ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member) ? member @@ -10756,7 +10760,7 @@ export class Lowerer { if ( abi.kind === "dyn" && ts.isObjectBindingPattern(decl.name) && - decl.name.elements.length === 0 + decl.name.elements.nodes.length === 0 ) { prologue.push({ kind: "exprStmt", @@ -11036,7 +11040,7 @@ export class Lowerer { vd && ts.isParameter(vd) && ts.isIdentifier(vd.name) && - vd.modifiers?.some( + vd.modifiers?.nodes.some( (m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.PrivateKeyword || diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index b84672406..4270457d5 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -213,15 +213,15 @@ function adjacentDeclarations(a: ts.VariableDeclaration, b: ts.VariableDeclarati const lb = b.parent; if (!ts.isVariableDeclarationList(la) || !ts.isVariableDeclarationList(lb)) return false; if (la === lb) { - const ia = la.declarations.indexOf(a); - const ib = la.declarations.indexOf(b); + const ia = la.declarations.nodes.indexOf(a); + const ib = la.declarations.nodes.indexOf(b); return Math.abs(ia - ib) === 1; } const sa = la.parent; const sb = lb.parent; if (!ts.isVariableStatement(sa) || !ts.isVariableStatement(sb) || sa.parent !== sb.parent) return false; - if (la.declarations.length !== 1 || lb.declarations.length !== 1) return false; + if (la.declarations.nodes.length !== 1 || lb.declarations.nodes.length !== 1) return false; const list = sa.parent; if ( !ts.isBlock(list) && @@ -230,7 +230,7 @@ function adjacentDeclarations(a: ts.VariableDeclaration, b: ts.VariableDeclarati !ts.isDefaultClause(list) ) return false; - const statements = (list as { statements: ts.NodeArray }).statements; + const statements = (list as { statements: ts.NodeArray }).statements.nodes; return Math.abs(statements.indexOf(sa) - statements.indexOf(sb)) === 1; } @@ -609,8 +609,8 @@ export class NanCodedReads { const na = this.fixedLength(sa); const nb = this.fixedLength(sb); if (!na || !nb) return false; - const la = na.arguments![0]!; - const lb = nb.arguments![0]!; + const la = na.arguments!.nodes[0]!; + const lb = nb.arguments!.nodes[0]!; // The same literal length. const literal = (e: ts.Expression): number | null => { const x = peelTransparent(e); @@ -695,10 +695,10 @@ export class NanCodedReads { !ts.isIdentifier(init.expression) || !TYPED_ARRAYS.has(init.expression.text) || !this.host.isStdlibGlobal(init.expression, init.expression.text) || - init.typeArguments || - init.arguments?.length !== 1 || - ts.isSpreadElement(init.arguments[0]!) || - !this.host.isNumber(init.arguments[0]!) + init.typeArguments?.nodes || + init.arguments?.nodes.length !== 1 || + ts.isSpreadElement(init.arguments.nodes[0]!) || + !this.host.isNumber(init.arguments.nodes[0]!) ) return null; const name = decl.name; @@ -753,7 +753,7 @@ export class NanCodedReads { this.fnEligibility.set(symbol, null); const decl = this.host.declarationOf(symbol); if (!decl || (!ts.isFunctionDeclaration(decl) && !ts.isMethodDeclaration(decl))) return null; - if (!decl.body || decl.asteriskToken || decl.typeParameters || !decl.name) return null; + if (!decl.body || decl.asteriskToken || decl.typeParameters?.nodes || !decl.name) return null; if (!ts.isIdentifier(decl.name)) return null; const sf = decl.getSourceFile(); if (isJsSourceFile(sf) || sf.isDeclarationFile) return null; @@ -762,7 +762,7 @@ export class NanCodedReads { return null; if (ts.isMethodDeclaration(decl)) { const cls = decl.parent; - if (!ts.isClassDeclaration(cls) || (cls.heritageClauses?.length ?? 0) > 0) return null; + if (!ts.isClassDeclaration(cls) || (cls.heritageClauses?.nodes.length ?? 0) > 0) return null; if (this.host.methodOverridden(symbol)) return null; } // Sloppy-mode `arguments` aliases parameters. @@ -806,7 +806,7 @@ export class NanCodedReads { !ts.isCallExpression(call) || call.expression !== callee || call.questionDotToken || - call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { ok = false; break; @@ -863,12 +863,12 @@ export class NanCodedReads { if (!ts.isFunctionDeclaration(fn) && !ts.isMethodDeclaration(fn)) return null; const fnSymbol = fn.name ? this.symbolOf(fn.name) : null; if (!fnSymbol || !this.eligibleFunction(fnSymbol)) return null; - const index = fn.parameters.indexOf(decl); + const index = fn.parameters.nodes.indexOf(decl); if (index < 0 || !this.host.paramIsNumber(fnSymbol, index)) return null; nameNode = decl.name; if (decl.initializer) slot.sources.push(decl.initializer); for (const call of this.callSites.get(fnSymbol) ?? []) { - const arg = call.arguments[index]; + const arg = call.arguments.nodes[index]; if (arg) slot.sources.push(arg); else if (!decl.initializer) slot.sources.push(null); } @@ -1020,11 +1020,11 @@ export class NanCodedReads { return symbol ? this.returnSlot(symbol) : null; } if (ts.isCallExpression(p) && p.expression !== e) { - const index = p.arguments.indexOf(e); + const index = p.arguments.nodes.indexOf(e); const fn = this.calleeSymbol(p); if (index < 0 || !fn) return null; const decl = this.eligibleFunction(fn); - const param = decl?.parameters[index]; + const param = decl?.parameters?.nodes[index]; if (!param || !ts.isIdentifier(param.name)) return null; const symbol = this.symbolOf(param.name); return symbol ? this.bindingSlot(symbol) : null; @@ -1493,7 +1493,7 @@ export class NanCodedReads { ) return true; if (ts.isSwitchStatement(p)) { - return p.caseBlock.clauses.every( + return p.caseBlock.clauses.nodes.every( (clause) => !ts.isCaseClause(clause) || this.plainNumber(clause.expression, 0) !== null, ); } diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 5bd563d9d..500267263 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -198,8 +198,8 @@ export class NarrowingStability { this.lowerer.isStdlibGlobal(skipOuter(callee.expression), "console") ) return true; - const index = call.arguments?.indexOf(argument) ?? -1; - const params = declaration.parameters ?? []; + const index = call.arguments?.nodes.indexOf(argument) ?? -1; + const params = declaration.parameters?.nodes ?? []; if (index < 0 || params.length === 0) return false; const param = params[Math.min(index, params.length - 1)]!; if (index >= params.length && param.dotDotDotToken === undefined) return false; @@ -665,21 +665,21 @@ export class NarrowingStability { ); } if (ts.isClassLikeDeclaration(node)) { - if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.Decorator)) return true; - for (const clause of node.heritageClauses ?? []) { - for (const heritage of clause.types) { + if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.Decorator)) return true; + for (const clause of node.heritageClauses?.nodes ?? []) { + for (const heritage of clause.types.nodes) { const e = heritage.expression; if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return true; } } - return node.members.some( + return node.members.nodes.some( (member) => ts.isClassStaticBlockDeclaration(member) || (member.name !== undefined && ts.isComputedPropertyName(member.name)) || - member.modifiers?.some((m) => m.kind === ts.SyntaxKind.Decorator) === true || + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.Decorator) === true || (ts.isPropertyDeclaration(member) && member.initializer !== undefined && - member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) === true), + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) === true), ); } return true; @@ -688,12 +688,12 @@ export class NarrowingStability { private isInertLibraryCall(call: ts.CallExpression): boolean { if (call.expression.kind === ts.SyntaxKind.SuperKeyword) return false; if (call.expression.kind === ts.SyntaxKind.ImportKeyword) return false; - if (!this.inertArguments(call.arguments)) return false; + if (!this.inertArguments(call.arguments.nodes)) return false; const callee = skipOuter(call.expression); if (ts.isIdentifier(callee)) { if (!INERT_GLOBAL_FUNCTIONS.has(callee.text)) return false; if (!this.lowerer.isStdlibGlobal(callee, callee.text)) return false; - return call.arguments.every((arg) => this.isPrimitive(arg)); + return call.arguments.nodes.every((arg) => this.isPrimitive(arg)); } if (!ts.isPropertyAccessExpression(callee)) return false; const method = callee.name.text; @@ -720,7 +720,7 @@ export class NarrowingStability { const callee = skipOuter(node.expression); if (!ts.isIdentifier(callee) || !INERT_CONSTRUCTORS.has(callee.text)) return false; if (!this.lowerer.isStdlibGlobal(callee, callee.text)) return false; - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; if (!this.inertArguments(args)) return false; return args.every((arg) => this.isPrimitive(arg) || this.isBuiltinIterable(arg)); } @@ -959,7 +959,7 @@ function widePosition(node: ts.Expression): WidePosition | null { if (ts.isForStatement(parent) && parent.condition === child) return "test"; if ( (ts.isCallExpression(parent) || ts.isNewExpression(parent)) && - parent.arguments?.some((arg) => arg === child) + parent.arguments?.nodes.some((arg) => arg === child) ) return "context"; // Only slots whose type IS the contextual type: an array or object @@ -997,7 +997,7 @@ function enclosingArgument( parent = parent.parent; } if (!(ts.isCallExpression(parent) || ts.isNewExpression(parent))) return null; - const argument = parent.arguments?.find((arg) => arg === child); + const argument = parent.arguments?.nodes.find((arg) => arg === child); return argument ? { call: parent, argument } : null; } @@ -1016,7 +1016,7 @@ function typedLiteral( if (!slot) return false; if (ts.isVariableDeclaration(slot)) return slot.initializer === literal && !!slot.type; if (ts.isCallExpression(slot) || ts.isNewExpression(slot)) - return slot.arguments?.some((arg) => arg === literal) ?? false; + return slot.arguments?.nodes.some((arg) => arg === literal) ?? false; if (ts.isBinaryExpression(slot)) return slot.operatorToken.kind === ts.SyntaxKind.EqualsToken && slot.right === literal; if (ts.isReturnStatement(slot)) { @@ -1180,13 +1180,13 @@ function forEachWriteTarget(target: ts.Node, visit: (target: ts.Expression) => v return; } if (ts.isArrayLiteralExpression(e)) { - for (const element of e.elements) { + for (const element of e.elements.nodes) { forEachWriteTarget(ts.isSpreadElement(element) ? element.expression : element, visit); } return; } if (ts.isObjectLiteralExpression(e)) { - for (const property of e.properties) { + for (const property of e.properties.nodes) { if (ts.isPropertyAssignment(property)) forEachWriteTarget(property.initializer, visit); else if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) visit(property.name); @@ -1227,7 +1227,7 @@ function reflectiveStore(call: ts.CallExpression): boolean { method === "deleteProperty" || method === "setPrototypeOf")); if (!reflective) return false; - const target = call.arguments[0]; + const target = call.arguments.nodes[0]; return target === undefined || !ts.isObjectLiteralExpression(skipOuter(target)); } diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 1c0cff2c5..3ccd54b64 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -176,7 +176,7 @@ function ffiDeclarationDiagnostic( loc, ); } - if (functionDecls.some((decl) => (decl.typeParameters?.length ?? 0) > 0)) { + if (functionDecls.some((decl) => (decl.typeParameters?.nodes.length ?? 0) > 0)) { return signatureDiag( binding.name, "generic ambient declarations cannot describe one fixed C ABI", @@ -474,18 +474,18 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr if (expr.questionDotToken !== undefined || expr.typeArguments !== undefined) { signatureError("native bindings support direct, non-generic calls only"); } - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { signatureError("spread arguments do not have a fixed native ABI"); } const expectedParams = ffiSourceParamTypes(binding.params); - if (expr.arguments.length !== expectedParams.length) { + if (expr.arguments.nodes.length !== expectedParams.length) { signatureError( - `this call passes ${expr.arguments.length} argument(s), but the native binding requires exactly ${expectedParams.length}`, + `this call passes ${expr.arguments.nodes.length} argument(s), but the native binding requires exactly ${expectedParams.length}`, ); } const expectedReturn = ffiClassType(binding.returns); const sourceParams = ffiSourceParams(binding); - const args = expr.arguments.map((arg, i) => { + const args = expr.arguments.nodes.map((arg, i) => { const sourceParam = sourceParams[i]!; const expected = expectedParams[i]!; const lowered = lowerer.lowerExprExpecting(arg, expected); diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index 0e3eaefd7..ac83dccd8 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -45,7 +45,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { !bindingNeverReassigned(lowerer, symbol, owner) || !isJsSourceFile(declaration.getSourceFile()) || declaration.asteriskToken || - declaration.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) + declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) return false; const objectReturn = (expression: ts.Expression): boolean => @@ -54,7 +54,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { ts.isArrowFunction(expression) || ts.isFunctionExpression(expression) || ts.isNewExpression(expression); - const last = declaration.body.statements.at(-1); + const last = declaration.body.statements.nodes.at(-1); if (!last || !ts.isReturnStatement(last) || !last.expression || !objectReturn(last.expression)) return false; let safe = true; @@ -76,7 +76,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { safe = false; ts.forEachChild(node, (child) => visit(child, nestedArrow)); }; - for (const parameter of declaration.parameters) visit(parameter); + for (const parameter of declaration.parameters.nodes) visit(parameter); visit(declaration.body); return safe; } @@ -104,7 +104,7 @@ export function lowerObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): return { kind: "call", callee: signature.name, - args: lowerer.completeArgs(expr.arguments ?? [], signature.params, loc, expr), + args: lowerer.completeArgs(expr.arguments?.nodes ?? [], signature.params, loc, expr), type: signature.returnType, loc, }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index 5375f3792..2f84e988a 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -52,7 +52,7 @@ export interface EarlyReference { function boundNames(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const out: ts.Identifier[] = []; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (ts.isOmittedExpression(el) || el.name === undefined) continue; out.push(...boundNames(el.name)); } @@ -105,10 +105,10 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { for (const sf of lowerer.moduleOrder) { if (sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) continue; const declared: { decl: ts.VariableDeclaration; stmt: ts.VariableStatement }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) === 0) continue; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { if (decl.initializer === undefined || requireSpecOf(decl.initializer) === null) continue; if (requireEarlyReference(lowerer.program, decl) === null) continue; declared.push({ decl, stmt }); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts index d087a2d61..d3acaff6b 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts @@ -67,7 +67,7 @@ export function indexReadInBounds( ) { // A value-preserving wrapper. } else if (ts.isCallExpression(parent) || ts.isNewExpression(parent)) { - const args = parent.arguments ?? []; + const args = parent.arguments?.nodes ?? []; const at = args.indexOf(child as ts.Expression); if (at < 0 || (ts.isCallExpression(parent) && parent.questionDotToken)) return false; before.push(parent.expression, ...args.slice(0, at)); @@ -80,7 +80,7 @@ export function indexReadInBounds( ts.isVariableDeclaration(parent) && parent.initializer === child && ts.isVariableDeclarationList(parent.parent) && - parent.parent.declarations[0] === parent && + parent.parent.declarations.nodes[0] === parent && ts.isVariableStatement(parent.parent.parent) ) { return boundedBefore(parent.parent.parent, { array, index, facts }, before); @@ -108,7 +108,7 @@ function boundedBefore(statement: ts.Statement, operands: Operands, start: ts.No let parent: ts.Node | undefined = statement.parent; while (parent && !isFunctionBoundary(parent)) { if (isStatementList(parent)) { - const statements = (parent as { statements: ts.NodeArray }).statements; + const statements = (parent as { statements: ts.NodeArray }).statements.nodes; const at = statements.indexOf(child as ts.Statement); for (let i = at - 1; i >= 0; i--) { const statement = statements[i]!; @@ -290,7 +290,7 @@ function assertsSlot(node: ts.Node, operands: Operands): boolean { n.forEachChild(visit); }; if (ts.isVariableStatement(node)) { - for (const declaration of node.declarationList.declarations) + for (const declaration of node.declarationList.declarations.nodes) if (declaration.initializer) visit(declaration.initializer); } else if (ts.isExpressionStatement(node)) visit(node.expression); else if (!ts.isStatement(node)) visit(node); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index e6781be73..2fa8785c9 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -127,7 +127,7 @@ export class ArrayElementStates { /** `[a, ...rest] = source` copies the remaining indices into a new * array: holes become undefined. */ private scanRestPattern(pattern: ts.ArrayBindingPattern): boolean { - const last = pattern.elements[pattern.elements.length - 1]; + const last = pattern.elements.nodes[pattern.elements.nodes.length - 1]; if (!last || ts.isOmittedExpression(last) || !last.dotDotDotToken) return false; // A parameter or nested pattern has no source expression to judge. const owner = pattern.parent; @@ -145,7 +145,7 @@ export class ArrayElementStates { private scanRestAssignment(node: ts.BinaryExpression): boolean { const pattern = peel(node.left); if (!ts.isArrayLiteralExpression(pattern)) return false; - const last = pattern.elements[pattern.elements.length - 1]; + const last = pattern.elements.nodes[pattern.elements.nodes.length - 1]; if (!last || !ts.isSpreadElement(last)) return false; if (!this.mayYieldUndefined(node.right)) return false; return this.addUndefinedTo(last.expression); @@ -159,7 +159,7 @@ export class ArrayElementStates { if (ts.isArrayLiteralExpression(e)) { let undefinedValue = false; let holes = false; - for (const element of e.elements) { + for (const element of e.elements.nodes) { if (ts.isOmittedExpression(element)) holes = true; else if (this.valueMayBeUndefined(element)) undefinedValue = true; } @@ -187,7 +187,7 @@ export class ArrayElementStates { /** Contents of an array a call creates; undefined when the call is not a * known array constructor or method. */ private builtContents(node: ts.CallExpression | ts.NewExpression): Contents | null | undefined { - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; const callee = peel(node.expression); if (ts.isIdentifier(callee)) { if (!this.host.isGlobal(callee, "Array")) return undefined; @@ -352,7 +352,7 @@ export class ArrayElementStates { if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && !buildsArray(node)) return false; if (consumedWhereBuilt(node)) return false; // An empty literal holds nothing. - if (ts.isArrayLiteralExpression(node) && node.elements.length === 0) return false; + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 0) return false; const contents = this.contents(node); if (contents === null || (!contents.undefined && !contents.holes)) return false; const keys = this.keysAsUsed(node); @@ -490,7 +490,7 @@ export class ArrayElementStates { private scanCall(node: ts.CallExpression | ts.NewExpression): boolean { let changed = false; - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; // Spreading a sparse array into a rest parameter fills the holes. if (args.some((arg) => ts.isSpreadElement(arg))) { for (const arg of args) { @@ -593,7 +593,7 @@ export class ArrayElementStates { ts.isCallExpression(source) && ts.isPropertyAccessExpression(source.expression) && (source.expression.name.text === "values" || source.expression.name.text === "entries") && - source.arguments.length === 0 + source.arguments.nodes.length === 0 ) { const receiver = this.host.arrayKeys(source.expression.expression); if (receiver === null || receiver.length > 0) source = source.expression.expression; @@ -614,7 +614,7 @@ export class ArrayElementStates { name.forEachChild(mark); }; if (ts.isVariableDeclarationList(initializer)) { - for (const declaration of initializer.declarations) mark(declaration.name); + for (const declaration of initializer.declarations.nodes) mark(declaration.name); } else mark(initializer); return changed; } @@ -690,12 +690,12 @@ function consumedWhereBuilt(node: ts.Expression): boolean { ) { const arrayAt = ARRAY_ARGUMENT_AT.get(parent.name.text); if (arrayAt === undefined) return true; - const callback = parent.parent.arguments[0]; + const callback = parent.parent.arguments.nodes[0]; return ( callback !== undefined && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && - callback.parameters.length <= arrayAt && - !callback.parameters.some((parameter) => parameter.dotDotDotToken) && + callback.parameters.nodes.length <= arrayAt && + !callback.parameters.nodes.some((parameter) => parameter.dotDotDotToken) && ts.isArrowFunction(callback) ); } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts b/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts index 82d944fcd..a19febe3b 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts @@ -20,7 +20,7 @@ function proofs(body: string): boolean[] { ts.isIdentifier(node.expression) && node.expression.text === "use" ) { - const arg = node.arguments[0]; + const arg = node.arguments.nodes[0]; if (arg && ts.isIdentifier(arg)) results.push( presentAtUse(arg, { diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts index cdcc7bf5c..768932ad3 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts @@ -54,7 +54,7 @@ export function reassignedBindings( } else if (js && ts.isIdentifier(node) && node.text === "arguments") { for (let owner = node.parent; owner; owner = owner.parent) { if (!ts.isFunctionLike(owner) || ts.isArrowFunction(owner)) continue; - for (const parameter of owner.parameters) + for (const parameter of owner.parameters.nodes) for (const bound of boundNames(parameter.name)) mark(bound); break; } @@ -68,7 +68,7 @@ export function reassignedBindings( function boundNames(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const names: ts.Identifier[] = []; - for (const element of name.elements) + for (const element of name.elements.nodes) if (!ts.isOmittedExpression(element) && element.name) names.push(...boundNames(element.name)); return names; } @@ -137,7 +137,7 @@ export function presentAtUse(use: ts.Identifier, bindings: PresenceBindings): bo if (condition && implies(condition, true, bindings)) return true; } else if (ts.isCallExpression(parent) || ts.isNewExpression(parent)) { // Arguments evaluate after the callee and earlier arguments. - const args = parent.arguments ?? []; + const args = parent.arguments?.nodes ?? []; const index = args.indexOf(child as ts.Expression); if (index >= 0 && !isOptionalChainMember(parent)) { if (dereferences(parent.expression, bindings)) return true; @@ -145,7 +145,7 @@ export function presentAtUse(use: ts.Identifier, bindings: PresenceBindings): bo } } if (isStatementList(parent) && !ts.isFunctionDeclaration(child)) { - const statements = (parent as { statements: ts.NodeArray }).statements; + const statements = (parent as { statements: ts.NodeArray }).statements.nodes; const index = statements.indexOf(child as ts.Statement); for (let i = index - 1; i >= 0; i--) { if (statementProvesPresence(statements[i]!, bindings)) return true; @@ -278,7 +278,7 @@ function statementProvesPresence(statement: ts.Statement, bindings: PresenceBind } if (ts.isExpressionStatement(statement)) return dereferences(statement.expression, bindings); if (ts.isVariableStatement(statement)) { - for (const declaration of statement.declarationList.declarations) { + for (const declaration of statement.declarationList.declarations.nodes) { if (declaration.initializer && dereferences(declaration.initializer, bindings)) return true; } return false; @@ -300,7 +300,7 @@ function exitsAbruptly(statement: ts.Statement): boolean { if (ts.isBlock(statement)) { // A labeled statement inside could be the target of an inner break, // which resumes after it; only plain trailing exits count. - const last = statement.statements[statement.statements.length - 1]; + const last = statement.statements.nodes[statement.statements.nodes.length - 1]; return last !== undefined && exitsAbruptly(last); } if (ts.isIfStatement(statement)) @@ -342,11 +342,11 @@ export function dereferences(expression: ts.Expression, bindings: PresenceBindin if (!e.questionDotToken && isBinding(e.expression, bindings)) return true; if (dereferences(e.expression, bindings)) return true; if (isOptionalChainMember(e)) return false; - return e.arguments.some((arg) => !ts.isSpreadElement(arg) && dereferences(arg, bindings)); + return e.arguments.nodes.some((arg) => !ts.isSpreadElement(arg) && dereferences(arg, bindings)); } if (ts.isNewExpression(e)) { if (dereferences(e.expression, bindings)) return true; - return (e.arguments ?? []).some( + return (e.arguments?.nodes ?? []).some( (arg) => !ts.isSpreadElement(arg) && dereferences(arg, bindings), ); } @@ -381,6 +381,6 @@ export function dereferences(expression: ts.Expression, bindings: PresenceBindin } if (ts.isConditionalExpression(e)) return dereferences(e.condition, bindings); if (ts.isTemplateExpression(e)) - return e.templateSpans.some((span) => dereferences(span.expression, bindings)); + return e.templateSpans.nodes.some((span) => dereferences(span.expression, bindings)); return false; } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts index 2f5badb20..adecfa5a9 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts @@ -226,13 +226,13 @@ export class ArrayOwnership { const arrayAt = ARRAY_ARGUMENT_AT.get(method); if (arrayAt !== undefined) { // An inline callback that cannot see its array argument. - const callback = call.arguments[0]; + const callback = call.arguments.nodes[0]; return ( callback !== undefined && (ts.isArrowFunction(callback) || (ts.isFunctionExpression(callback) && !usesArguments(callback))) && - callback.parameters.length <= arrayAt && - !callback.parameters.some((parameter) => parameter.dotDotDotToken) + callback.parameters.nodes.length <= arrayAt && + !callback.parameters.nodes.some((parameter) => parameter.dotDotDotToken) ); } return this.host.isArray(user.expression); @@ -243,14 +243,14 @@ export class ArrayOwnership { user.operatorToken.kind === ts.SyntaxKind.EqualsToken ) return this.isFreshArray(user.right); - if (ts.isCallExpression(user) && user.arguments[0] === use) { + if (ts.isCallExpression(user) && user.arguments.nodes[0] === use) { const callee = peel(user.expression); return ( ts.isPropertyAccessExpression(callee) && ((callee.name.text === "hasOwn" && this.host.isGlobal(callee.expression, "Object")) || (callee.name.text === "isArray" && this.host.isGlobal(callee.expression, "Array")) || (callee.name.text === "from" && - user.arguments.length === 1 && + user.arguments.nodes.length === 1 && this.host.isGlobal(callee.expression, "Array"))) ); } @@ -348,10 +348,10 @@ export class ArrayOwnership { this.host.isGlobal(callee.expression, "Reflect")); const clone = ts.isIdentifier(callee) && this.host.isGlobal(callee, "structuredClone"); if (walker || clone) - for (const arg of node.arguments) + for (const arg of node.arguments.nodes) this.reflected.push(ts.isSpreadElement(arg) ? arg.expression : arg); } else if (ts.isObjectBindingPattern(node)) { - for (const element of node.elements) { + for (const element of node.elements.nodes) { const name = element.propertyName ?? element.name; // A rest element copies every remaining field. const key = diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts index 334f94d4f..bad2523c8 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts @@ -135,8 +135,8 @@ export class RecordFlowClasses { parameters(): { symbol: ts.Symbol; fn: ts.Symbol; index: number }[] { const out: { symbol: ts.Symbol; fn: ts.Symbol; index: number }[] = []; for (const [fn, decl] of this.functions) { - for (let index = 0; index < decl.parameters.length; index++) { - const symbol = this.host.symbolOf(decl.parameters[index]!.name); + for (let index = 0; index < decl.parameters.nodes.length; index++) { + const symbol = this.host.symbolOf(decl.parameters.nodes[index]!.name); if (symbol && this.bindingSlots.has(symbol)) out.push({ symbol, fn, index }); } } @@ -287,7 +287,7 @@ export class RecordFlowClasses { const key = `${i}:${name}`; if (visitedLiterals.has(key)) continue; visitedLiterals.add(key); - for (const property of this.literalSources[i]!.literal.properties) { + for (const property of this.literalSources[i]!.literal.properties.nodes) { if (ts.isSpreadAssignment(property)) { this.markOpen(field); continue; @@ -364,11 +364,11 @@ export class RecordFlowClasses { if (file.isDeclarationFile || isJsFile(file)) continue; ts.walkPreorder(file, (node) => { if (!ts.isFunctionDeclaration(node) || !node.name || !node.body) return undefined; - if (node.asteriskToken || node.typeParameters?.length) return undefined; + if (node.asteriskToken || node.typeParameters?.nodes.length) return undefined; const flags = ts.getCombinedModifierFlags(node); if (flags & (ts.ModifierFlags.Export | ts.ModifierFlags.Async | ts.ModifierFlags.Ambient)) return undefined; - if (node.parameters.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) + if (node.parameters.nodes.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) return undefined; if (usesArguments(node.body)) return undefined; const symbol = this.host.symbolOf(node.name); @@ -380,7 +380,7 @@ export class RecordFlowClasses { !!call && ts.isCallExpression(call) && call.expression === reference && - !call.arguments.some(ts.isSpreadElement) + !call.arguments.nodes.some(ts.isSpreadElement) ); }); if (calledByName) { @@ -404,10 +404,10 @@ export class RecordFlowClasses { for (;;) { if (seen.has(current)) return true; seen.add(current); - const heritage = current.heritageClauses?.find( + const heritage = current.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, ); - const base = heritage?.types[0]?.expression; + const base = heritage?.types?.nodes[0]?.expression; if (!base) return false; const symbol = ts.isIdentifier(base) ? this.host.symbolOf(base) : null; const target = symbol ? this.host.declarationOf(symbol) : undefined; @@ -436,13 +436,13 @@ export class RecordFlowClasses { // classes (`_read`, `handleEvent`) with no call site to see. if (PROTOCOL_METHODS.has(node.name.text) || this.extendsForeignClass(owner)) return undefined; - if (node.asteriskToken || node.typeParameters?.length) return undefined; + if (node.asteriskToken || node.typeParameters?.nodes.length) return undefined; if ( ts.getCombinedModifierFlags(node) & (ts.ModifierFlags.Async | ts.ModifierFlags.Abstract) ) return undefined; - if (node.parameters.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) + if (node.parameters.nodes.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) return undefined; if (usesArguments(node.body)) return undefined; const symbol = this.host.symbolOf(node.name); @@ -472,7 +472,7 @@ export class RecordFlowClasses { !!call && ts.isCallExpression(call) && call.expression === parent && - !call.arguments.some(ts.isSpreadElement) + !call.arguments.nodes.some(ts.isSpreadElement) ); } // Any other member, property or key of this name could reach the @@ -505,7 +505,7 @@ export class RecordFlowClasses { if (symbol) this.bindingSlots.set(symbol, this.newSlot(name)); } for (const [symbol, decl] of this.functions) { - for (const parameter of decl.parameters) { + for (const parameter of decl.parameters.nodes) { if (!this.host.mayBeRecord(parameter.name)) continue; const bound = this.host.symbolOf(parameter.name); if (bound) this.bindingSlots.set(bound, this.newSlot(parameter.name)); @@ -518,10 +518,12 @@ export class RecordFlowClasses { if (family[0] !== symbol || family.length < 2) continue; const decls = family.map((member) => this.functions.get(member)!); const first = decls[0]!; - for (let i = 0; i < first.parameters.length; i++) { - const head = this.slotOf(first.parameters[i]!.name); + for (let i = 0; i < first.parameters.nodes.length; i++) { + const head = this.slotOf(first.parameters.nodes[i]!.name); for (const decl of decls.slice(1)) { - const other = decl.parameters[i] ? this.slotOf(decl.parameters[i]!.name) : undefined; + const other = decl.parameters.nodes[i] + ? this.slotOf(decl.parameters.nodes[i]!.name) + : undefined; if (head !== undefined && other !== undefined) this.union(head, other); else if (head !== undefined) this.markOpen(head); else if (other !== undefined) this.markOpen(other); @@ -579,10 +581,10 @@ export class RecordFlowClasses { if (declaration.questionToken) this.markOpen(slot); if (declaration.initializer) this.source(declaration.initializer, slot); const fn = declaration.parent as Callable; - const index = fn.parameters.indexOf(declaration); + const index = fn.parameters.nodes.indexOf(declaration); const fnSymbol = fn.name ? this.host.symbolOf(fn.name) : null; for (const call of fnSymbol ? this.callsOf(fnSymbol) : []) { - const arg = call.arguments[index]; + const arg = call.arguments.nodes[index]; if (arg) this.source(arg, slot); else if (!declaration.initializer) this.markOpen(slot); } @@ -763,14 +765,16 @@ export class RecordFlowClasses { const symbol = this.host.symbolOf(callee); const fn = symbol ? this.functions.get(symbol) : undefined; if (!fn) return false; - const parameter = fn.parameters[user.arguments.indexOf(value as ts.Expression)]; + const parameter = + fn.parameters.nodes[user.arguments.nodes.indexOf(value as ts.Expression)]; return !!parameter && this.isMember(parameter.name); } if (ts.isPropertyAccessExpression(callee)) { const symbol = this.host.symbolOf(callee.name); const method = symbol ? this.functions.get(symbol) : undefined; if (method) { - const parameter = method.parameters[user.arguments.indexOf(value as ts.Expression)]; + const parameter = + method.parameters.nodes[user.arguments.nodes.indexOf(value as ts.Expression)]; return !!parameter && this.isMember(parameter.name); } } diff --git a/packages/compiler/src/frontend/lowering/split-consumers.ts b/packages/compiler/src/frontend/lowering/split-consumers.ts index b5d5e9200..11443051c 100644 --- a/packages/compiler/src/frontend/lowering/split-consumers.ts +++ b/packages/compiler/src/frontend/lowering/split-consumers.ts @@ -40,8 +40,8 @@ export function isPrivateSplitConsumer( init.expression.name.text !== "split" || !lowerer.isStdlibMember(init.expression) || lowerer.mapTypeOf(lowerer.typeOf(init.expression.expression))?.kind !== "string" || - init.arguments.length === 0 || - lowerer.mapTypeOf(lowerer.typeOf(init.arguments[0]!))?.kind !== "string" + init.arguments.nodes.length === 0 || + lowerer.mapTypeOf(lowerer.typeOf(init.arguments.nodes[0]!))?.kind !== "string" ) return false; diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index 43368556c..acb41514f 100644 --- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts +++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts @@ -113,7 +113,7 @@ function exits(node: ts.Statement): boolean { ) return true; if (ts.isBlock(node)) { - const last = node.statements[node.statements.length - 1]; + const last = node.statements.nodes[node.statements.nodes.length - 1]; return last !== undefined && exits(last); } return false; @@ -542,7 +542,7 @@ export class StringIndexBounds { return true; } } else if (ts.isBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) { - const statements = node.statements; + const statements = node.statements.nodes; for (let i = statements.indexOf(child as ts.Statement) - 1; i >= 0; i--) { const statement = statements[i]!; if ( diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 1ab731a09..e43748b39 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -140,7 +140,7 @@ export function sideEffectFreeOptionValue(node: ts.Expression): boolean { return sideEffectFreeOptionValue(e.operand); } if (ts.isObjectLiteralExpression(e)) { - return e.properties.every( + return e.properties.nodes.every( (p) => ts.isShorthandPropertyAssignment(p) || (ts.isPropertyAssignment(p) && @@ -149,7 +149,7 @@ export function sideEffectFreeOptionValue(node: ts.Expression): boolean { ); } if (ts.isArrayLiteralExpression(e)) { - return e.elements.every((el) => !ts.isSpreadElement(el) && sideEffectFreeOptionValue(el)); + return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && sideEffectFreeOptionValue(el)); } return false; } diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index d3bd70d2c..f4f536af9 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -109,12 +109,12 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS if (init && (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init))) { const target = classSymbolKeyOf(lowerer, init); if (!target || bindingWritten(lowerer, sym, decl.getSourceFile())) return null; - const preceding = list.declarations.slice(0, list.declarations.indexOf(decl)); + const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); if ( bindingEarlyUse7( lowerer.program, decl.getSourceFile(), - decl.getSourceFile().statements.indexOf(list.parent), + decl.getSourceFile().statements.nodes.indexOf(list.parent), decl, preceding, ) !== null @@ -131,10 +131,10 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const root = registered ? callee.expression : callee; if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null; const arg = - init.arguments.length === 0 + init.arguments.nodes.length === 0 ? null - : init.arguments.length === 1 - ? init.arguments[0]! + : init.arguments.nodes.length === 1 + ? init.arguments.nodes[0]! : undefined; if (arg === undefined || (registered && arg === null)) return null; const description = arg === null ? "" : constantSymbolDescription(lowerer, arg); @@ -143,7 +143,7 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS // module-scope declaration above can name one fixed layout slot. const sf = decl.getSourceFile(); if (bindingWritten(lowerer, sym, sf)) return null; - const preceding = list.declarations.slice(0, list.declarations.indexOf(decl)); + const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); // Creating a function value does not execute its body. The general // require-publication analysis conservatively enters those bodies, but a // prefix containing only imports and function definitions cannot call @@ -164,19 +164,24 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS ); }; const prefixIsInert = - sf.statements - .slice(0, sf.statements.indexOf(list.parent)) + sf.statements.nodes + .slice(0, sf.statements.nodes.indexOf(list.parent)) .every( (statement) => ts.isImportDeclaration(statement) || ts.isFunctionDeclaration(statement) || (ts.isVariableStatement(statement) && - statement.declarationList.declarations.every(inertInitializer)), + statement.declarationList.declarations.nodes.every(inertInitializer)), ) && preceding.every(inertInitializer); if ( !prefixIsInert && - bindingEarlyUse7(lowerer.program, sf, sf.statements.indexOf(list.parent), decl, preceding) !== - null + bindingEarlyUse7( + lowerer.program, + sf, + sf.statements.nodes.indexOf(list.parent), + decl, + preceding, + ) !== null ) return null; // A registry name denotes one symbol even when several source bindings diff --git a/packages/compiler/src/frontend/lowering/symbol-methods.ts b/packages/compiler/src/frontend/lowering/symbol-methods.ts index 093ad058b..f6a8e68e4 100644 --- a/packages/compiler/src/frontend/lowering/symbol-methods.ts +++ b/packages/compiler/src/frontend/lowering/symbol-methods.ts @@ -26,6 +26,6 @@ export function lowerClassSymbolMethodCall( if (!method) return null; const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - const args = lowerer.completeArgs(call.arguments, method.sig.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, method.sig.params, loc, call); return accessorCall(lowerer, type.className, name, receiver, args, method.sig.ret, loc); } diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts index 9247703f5..6ec9af036 100644 --- a/packages/compiler/src/frontend/lowering/thread-slots.ts +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -83,7 +83,7 @@ function rootOf(info: ClassInfo): ClassInfo { function familyOf(lowerer: Lowerer, expr: ts.NewExpression): Family { const known = lowerer.threadSlotFamilies.get(expr); if (known !== undefined) return known; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const arg = args[0]; if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) lowerer.noLowering(`new ThreadSlots with ${args.length} arguments`, expr); @@ -211,8 +211,11 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): if (callee.questionDotToken !== undefined || expr.questionDotToken !== undefined) lowerer.noLowering(`ThreadSlots.${member} through ?.`, expr, `call slots.${member}() directly`); const arity = member === "set" ? 2 : 1; - if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a))) - lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.length} arguments`, expr); + if ( + expr.arguments.nodes.length !== arity || + expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) + lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.nodes.length} arguments`, expr); const init = declaringNew(lowerer, callee.expression); if (init === null) lowerer.unsupported( @@ -229,7 +232,7 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): lowerer.lowerExpr(callee.expression), slotsType, ); - const keyNode = expr.arguments[0]!; + const keyNode = expr.arguments.nodes[0]!; const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), family.key); const name: IrExpr = { kind: "strLit", value: family.name, type: STRING, loc }; const args = [name, receiver, key]; @@ -244,7 +247,7 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): case "delete": return { kind: "intrinsic", name: `slots.${member}`, args, type: { kind: "bool" }, loc }; case "set": { - const valueNode = expr.arguments[1]!; + const valueNode = expr.arguments.nodes[1]!; const value = lowerer.coerceInto(valueNode, lowerer.lowerExpr(valueNode), family.value); // `set` returns the slots; a statement discards them. const type = ts.isExpressionStatement(expr.parent) ? VOID : slotsType; diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index 41e3b2070..c445f1751 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -52,7 +52,7 @@ export function earlyReadVarSymbols( ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) ) return undefined; - for (const declaration of node.declarations) { + for (const declaration of node.declarations.nodes) { for (const name of boundIdentifiersOf(declaration.name)) { const symbol = checker.getSymbolAtLocation(name); if (symbol === undefined || !excludesUndefined(lowerer, name)) continue; @@ -101,9 +101,9 @@ function hoistingScopeOf(node: ts.Node): Scope { } function statementsOf(scope: Scope): readonly ts.Statement[] { - if (ts.isSourceFile(scope)) return scope.statements; + if (ts.isSourceFile(scope)) return scope.statements.nodes; const body = scope.body; - return body !== undefined && ts.isBlock(body) ? body.statements : []; + return body !== undefined && ts.isBlock(body) ? body.statements.nodes : []; } /** Whether the binding's declared type has no undefined value. */ @@ -197,7 +197,7 @@ class ScopeAnalysis { : null; if (list !== null) { const loop = !ts.isVariableStatement(s) && !ts.isForStatement(s); - for (const d of list.declarations) { + for (const d of list.declarations.nodes) { if ( (d.initializer !== undefined || loop) && boundIdentifiersOf(d.name).some((n) => checker.getSymbolAtLocation(n) === symbol) diff --git a/packages/compiler/src/frontend/module-source-candidates.ts b/packages/compiler/src/frontend/module-source-candidates.ts index dd468c421..7250a0954 100644 --- a/packages/compiler/src/frontend/module-source-candidates.ts +++ b/packages/compiler/src/frontend/module-source-candidates.ts @@ -36,7 +36,7 @@ export function moduleSourceCandidates( } if (ts.isCallExpression(node)) { result.calls.push(node); - const arg = node.arguments[0]; + const arg = node.arguments.nodes[0]; if ( (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === "require")) && diff --git a/packages/compiler/src/frontend/module-syntax.ts b/packages/compiler/src/frontend/module-syntax.ts index f57f2c947..ac7f550cf 100644 --- a/packages/compiler/src/frontend/module-syntax.ts +++ b/packages/compiler/src/frontend/module-syntax.ts @@ -94,7 +94,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { ts.isImportDeclaration(n) && n.importClause?.namedBindings !== undefined && ts.isNamedImports(n.importClause.namedBindings) && - n.importClause.namedBindings.elements.some((el) => el.name.text === "__require") + n.importClause.namedBindings.elements.nodes.some((el) => el.name.text === "__require") ) { requireHelperImport = n.moduleSpecifier.text; } @@ -102,12 +102,14 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { ts.isExportDeclaration(n) && n.exportClause !== undefined && ts.isNamedExports(n.exportClause) && - n.exportClause.elements.some((el) => (el.propertyName ?? el.name).text === "__require") + n.exportClause.elements.nodes.some( + (el) => (el.propertyName ?? el.name).text === "__require", + ) ) { requireHelperReexport = n.moduleSpecifier.text; } } else if (ts.isCallExpression(n)) { - const arg = n.arguments[0]; + const arg = n.arguments.nodes[0]; if ( n.expression.kind === ts.SyntaxKind.ImportKeyword && arg !== undefined && @@ -119,7 +121,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { n.expression.name.text === "resolve" && ts.isMetaProperty(n.expression.expression) && n.expression.expression.keywordToken === ts.SyntaxKind.ImportKeyword && - n.arguments.length >= 1 && + n.arguments.nodes.length >= 1 && arg !== undefined && ts.isStringLiteralLike(arg) ) { @@ -132,7 +134,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { n.expression.argumentExpression.text === "require")) && ts.isIdentifier(n.expression.expression) && n.expression.expression.text === "module" && - n.arguments.length === 1 && + n.arguments.nodes.length === 1 && arg !== undefined && ts.isStringLiteralLike(arg) ) { @@ -140,7 +142,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { } else if ( ts.isIdentifier(n.expression) && (n.expression.text === "require" || n.expression.text === "__require") && - n.arguments.length === 1 && + n.arguments.nodes.length === 1 && arg !== undefined && ts.isStringLiteralLike(arg) ) { @@ -177,14 +179,14 @@ export function sourceImportsOfFile(sf: ts.SourceFile): { spec: string; typeOnly : n.isTypeOnly; out.push({ spec: n.moduleSpecifier.text, typeOnly }); } else if (ts.isCallExpression(n)) { - const arg = n.arguments[0]; + const arg = n.arguments.nodes[0]; if ( arg !== undefined && ts.isStringLiteralLike(arg) && (n.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(n.expression) && n.expression.text === "require" && - n.arguments.length === 1)) + n.arguments.nodes.length === 1)) ) { out.push({ spec: arg.text, typeOnly: false }); } diff --git a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts index 0bd4b551e..89cd0fda1 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts @@ -46,8 +46,8 @@ for (const [name, source] of helperSources) { function declaration(stmt: ts.Statement): ts.VariableDeclaration | ts.FunctionDeclaration | null { if (ts.isFunctionDeclaration(stmt)) return stmt; - if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.length === 1) { - return stmt.declarationList.declarations[0]!; + if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.nodes.length === 1) { + return stmt.declarationList.declarations.nodes[0]!; } return null; } @@ -109,7 +109,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) string, { stmt: ts.Statement; decl: ts.VariableDeclaration | ts.FunctionDeclaration } >(); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const decl = declaration(stmt); if (!decl?.name || !ts.isIdentifier(decl.name)) continue; if (declarations.has(decl.name.text)) return null; @@ -145,23 +145,27 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) return null; - if (call.arguments.length !== 1) return null; - const factory = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const factory = call.arguments.nodes[0]!; if ( !ts.isFunctionExpression(factory) || factory.name || factory.asteriskToken || - factory.modifiers?.length || - factory.parameters.length !== 2 || - factory.body.statements.length !== 1 + factory.modifiers?.nodes.length || + factory.parameters.nodes.length !== 2 || + factory.body.statements.nodes.length !== 1 ) return null; if ( - factory.parameters.some((p) => !ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken) + factory.parameters.nodes.some( + (p) => !ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken, + ) ) return null; - const [exportsName, moduleName] = factory.parameters.map((p) => (p.name as ts.Identifier).text); - const statement = factory.body.statements[0]!; + const [exportsName, moduleName] = factory.parameters.nodes.map( + (p) => (p.name as ts.Identifier).text, + ); + const statement = factory.body.statements.nodes[0]!; if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression)) return null; const assignment = statement.expression; @@ -213,13 +217,13 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) return null; - const [requireCall, mode] = call.arguments; + const [requireCall, mode] = call.arguments.nodes; if ( - call.arguments.length !== 2 || + call.arguments.nodes.length !== 2 || !requireCall || !ts.isCallExpression(requireCall) || !ts.isIdentifier(requireCall.expression) || - requireCall.arguments.length !== 0 || + requireCall.arguments.nodes.length !== 0 || !mode || !ts.isNumericLiteral(mode) || Number(mode.text) !== 1 diff --git a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts index d4c15da22..4a84dcfeb 100644 --- a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts @@ -53,7 +53,7 @@ export function applyNpmStaticJsDocNamepaths( const replacements = new Map(); const visit = (node: ts.Node): void => { for (const doc of node.jsDoc ?? []) - for (const tag of doc.tags ?? []) { + for (const tag of doc.tags?.nodes ?? []) { const expression = tag.typeExpression; if (expression?.kind !== ts.SyntaxKind.JSDocTypeExpression) continue; const start = expression.getStart(sourceFile); @@ -108,13 +108,13 @@ export function applyNpmStaticNullableClassFields( source: string, ): NpmStaticOverloadRewrite | null { const constructors = new Set(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (ts.isClassDeclaration(statement) && statement.name) constructors.add(statement.name.text); if (ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly) { const clause = statement.importClause; if (clause?.name) constructors.add(clause.name.text); if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const binding of clause.namedBindings.elements) { + for (const binding of clause.namedBindings.elements.nodes) { if (!binding.isTypeOnly) constructors.add(binding.name.text); } } @@ -128,9 +128,9 @@ export function applyNpmStaticNullableClassFields( const rejectAssignedName = (node: ts.Node): void => { if (ts.isIdentifier(node)) constructors.delete(node.text); else if (ts.isArrayLiteralExpression(node)) - for (const item of node.elements) rejectAssignedName(item); + for (const item of node.elements.nodes) rejectAssignedName(item); else if (ts.isObjectLiteralExpression(node)) - for (const item of node.properties) { + for (const item of node.properties.nodes) { if (ts.isPropertyAssignment(item)) rejectAssignedName(item.initializer); else if (ts.isShorthandPropertyAssignment(item)) rejectAssignedName(item.name); else if (ts.isSpreadAssignment(item)) rejectAssignedName(item.expression); @@ -157,13 +157,13 @@ export function applyNpmStaticNullableClassFields( }; rejectMutable(sourceFile); const inserts: { offset: number; text: string }[] = []; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (!ts.isClassDeclaration(statement)) continue; const fields = new Map< string, { member: ts.PropertyDeclaration; className?: string; invalid: boolean } >(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || @@ -182,7 +182,7 @@ export function applyNpmStaticNullableClassFields( const bindingNames = (name: ts.BindingName): void => { if (ts.isIdentifier(name)) shadowed.add(name.text); else - for (const element of name.elements) + for (const element of name.elements.nodes) if (ts.isBindingElement(element) && element.name !== undefined) bindingNames(element.name); }; @@ -199,7 +199,7 @@ export function applyNpmStaticNullableClassFields( shadowed.add(node.name.text); ts.forEachChild(node, collectBindings); }; - for (const member of statement.members) collectBindings(member); + for (const member of statement.members.nodes) collectBindings(member); const targetField = (node: ts.Node): string | null => { if (ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword) return node.name.text; @@ -271,7 +271,7 @@ export function applyNpmStaticNullableClassFields( if (ts.isDeleteExpression(node)) invalidate(node.expression); ts.forEachChild(node, visit); }; - for (const member of statement.members) { + for (const member of statement.members.nodes) { if (hasModifier(member, ts.SyntaxKind.StaticKeyword)) continue; if ( (ts.isConstructorDeclaration(member) || @@ -308,15 +308,15 @@ export function applyNpmStaticFindReturnWidening( source: string, ): NpmStaticOverloadRewrite | null { const insertions: { offset: number; length: number }[] = []; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (!ts.isClassDeclaration(statement)) continue; - const constructor = statement.members.find( + const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); if (constructor?.body === undefined) continue; const arrayFields = new Set(); - for (const bodyStatement of constructor.body.statements) { + for (const bodyStatement of constructor.body.statements.nodes) { if ( !ts.isExpressionStatement(bodyStatement) || !ts.isBinaryExpression(bodyStatement.expression) @@ -331,9 +331,9 @@ export function applyNpmStaticFindReturnWidening( ) arrayFields.add(left.name.text); } - for (const member of statement.members) { - if (!ts.isMethodDeclaration(member) || member.body?.statements.length !== 1) continue; - const returned = member.body.statements[0]; + for (const member of statement.members.nodes) { + if (!ts.isMethodDeclaration(member) || member.body?.statements?.nodes.length !== 1) continue; + const returned = member.body.statements.nodes[0]; if ( !returned || !ts.isReturnStatement(returned) || @@ -355,7 +355,7 @@ export function applyNpmStaticFindReturnWidening( returnType === undefined || !ts.isTypeReferenceNode(returnType) || !ts.isIdentifier(returnType.typeName) || - (returnType.typeArguments?.length ?? 0) !== 0 + (returnType.typeArguments?.nodes.length ?? 0) !== 0 ) continue; insertions.push({ offset: returnType.getEnd(), length: " | undefined".length }); @@ -384,7 +384,7 @@ const SAFE_KEYWORD_TYPES = new Set([ function lastJsDocTag(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined { const docs = node.jsDoc; if (docs === undefined || docs.length === 0) return undefined; - for (const tag of docs[docs.length - 1]!.tags ?? []) { + for (const tag of docs[docs.length - 1]!.tags?.nodes ?? []) { if (tag.kind === kind) return tag; } return undefined; @@ -416,7 +416,7 @@ function jsDocReturnTypeOf(node: ts.Node): ts.TypeNode | undefined { const type = jsDocTypeOf(node); if (type === undefined) return undefined; if (ts.isTypeLiteralNode(type)) { - return type.members.find(ts.isCallSignatureDeclaration)?.type; + return type.members.nodes.find(ts.isCallSignatureDeclaration)?.type; } return ts.isFunctionTypeNode(type) ? type.type : undefined; } @@ -447,23 +447,23 @@ function safeTypeText( return safeTypeText(node.type, sourceFile, className); } if (ts.isUnionTypeNode(node)) { - const arms = node.types.map((type) => safeTypeText(type, sourceFile, className)); + const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); return arms.some((arm) => arm === null) ? null : arms.join(" | "); } if ( ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Record" && - node.typeArguments?.length === 2 && - node.typeArguments[0]?.kind === ts.SyntaxKind.StringKeyword && - node.typeArguments[1]?.kind === ts.SyntaxKind.StringKeyword + node.typeArguments?.nodes.length === 2 && + node.typeArguments.nodes[0]?.kind === ts.SyntaxKind.StringKeyword && + node.typeArguments.nodes[1]?.kind === ts.SyntaxKind.StringKeyword ) { return "Record"; } return ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === className && - (node.typeArguments?.length ?? 0) === 0 + (node.typeArguments?.nodes.length ?? 0) === 0 ? className : null; } @@ -476,7 +476,7 @@ function overloadSignature( if ( !ts.isIdentifier(method.name) || method.type === undefined || - (method.typeParameters?.length ?? 0) !== 0 || + (method.typeParameters?.nodes.length ?? 0) !== 0 || hasModifier(method, ts.SyntaxKind.StaticKeyword) || hasModifier(method, ts.SyntaxKind.PrivateKeyword) || hasModifier(method, ts.SyntaxKind.ProtectedKeyword) @@ -486,7 +486,7 @@ function overloadSignature( const returnType = safeTypeText(method.type, sourceFile, className); if (returnType === null) return null; const parameters: NpmStaticOverloadParameter[] = []; - for (const parameter of method.parameters) { + for (const parameter of method.parameters.nodes) { if ( !ts.isIdentifier(parameter.name) || parameter.name.text === "this" || @@ -513,11 +513,11 @@ export function parseNpmStaticDeclarationOverloads( sourceFile: ts.SourceFile, ): NpmStaticDeclarationOverloads { const classes = new Map>(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( !ts.isClassDeclaration(statement) || statement.name === undefined || - (statement.typeParameters?.length ?? 0) !== 0 || + (statement.typeParameters?.nodes.length ?? 0) !== 0 || !hasModifier(statement, ts.SyntaxKind.ExportKeyword) || hasModifier(statement, ts.SyntaxKind.DefaultKeyword) ) { @@ -525,7 +525,7 @@ export function parseNpmStaticDeclarationOverloads( } const className = statement.name.text; const groups = new Map(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue; const group = groups.get(member.name.text) ?? []; group.push(member); @@ -558,23 +558,23 @@ function nullableSelfType( sourceFile: ts.SourceFile, className: string, ): string | null { - if (!ts.isUnionTypeNode(node) || node.types.length !== 2) return null; - const arms = node.types.map((type) => safeTypeText(type, sourceFile, className)); + if (!ts.isUnionTypeNode(node) || node.types.nodes.length !== 2) return null; + const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); return arms.includes(className) && arms.includes("null") ? `${className} | null` : null; } function nullableVoidCallbackType(node: ts.TypeNode): string | null { - if (!ts.isUnionTypeNode(node) || node.types.length !== 2) return null; + if (!ts.isUnionTypeNode(node) || node.types.nodes.length !== 2) return null; let callback = false; let nullable = false; - for (let arm of node.types) { + for (let arm of node.types.nodes) { while (ts.isParenthesizedTypeNode(arm)) arm = arm.type; if (ts.isLiteralTypeNode(arm) && arm.literal.kind === ts.SyntaxKind.NullKeyword) nullable = true; else if ( ts.isFunctionTypeNode(arm) && - arm.parameters.length === 0 && - (arm.typeParameters?.length ?? 0) === 0 && + arm.parameters.nodes.length === 0 && + (arm.typeParameters?.nodes.length ?? 0) === 0 && arm.type?.kind === ts.SyntaxKind.VoidKeyword ) callback = true; @@ -588,11 +588,11 @@ export function parseNpmStaticDeclarationProperties( sourceFile: ts.SourceFile, ): NpmStaticDeclarationProperties { const classes = new Map>(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( !ts.isClassDeclaration(statement) || statement.name === undefined || - (statement.typeParameters?.length ?? 0) !== 0 || + (statement.typeParameters?.nodes.length ?? 0) !== 0 || !hasModifier(statement, ts.SyntaxKind.ExportKeyword) || hasModifier(statement, ts.SyntaxKind.DefaultKeyword) ) { @@ -600,7 +600,7 @@ export function parseNpmStaticDeclarationProperties( } const className = statement.name.text; const properties = new Map(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || @@ -626,7 +626,7 @@ export function parseNpmStaticDeclarationProperties( * caller's package-bounded resolution. Bare type dependencies deliberately * do not inherit the opted package's declaration trust. */ export function npmStaticDeclarationReexports(sourceFile: ts.SourceFile): readonly string[] { - return sourceFile.statements.flatMap((statement) => + return sourceFile.statements.nodes.flatMap((statement) => ts.isExportDeclaration(statement) && statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier) && @@ -639,13 +639,13 @@ export function npmStaticDeclarationReexports(sourceFile: ts.SourceFile): readon function requireSpecifier(expression: ts.Expression | undefined): string | null { const argument = expression !== undefined && ts.isCallExpression(expression) - ? expression.arguments[0] + ? expression.arguments.nodes[0] : undefined; return expression !== undefined && ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && expression.expression.text === "require" && - expression.arguments.length === 1 && + expression.arguments.nodes.length === 1 && argument !== undefined && ts.isStringLiteralLike(argument) ? argument.text @@ -661,14 +661,14 @@ export function npmStaticRuntimeClassTargets( classNames: ReadonlySet, ): ReadonlyMap { const localClasses = new Set( - sourceFile.statements.flatMap((statement) => + sourceFile.statements.nodes.flatMap((statement) => ts.isClassDeclaration(statement) && statement.name !== undefined ? [statement.name.text] : [], ), ); const linked = new Map(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (ts.isVariableStatement(statement)) { - for (const declaration of statement.declarationList.declarations) { + for (const declaration of statement.declarationList.declarations.nodes) { const specifier = requireSpecifier(declaration.initializer); if ( specifier === null || @@ -676,7 +676,7 @@ export function npmStaticRuntimeClassTargets( !ts.isObjectBindingPattern(declaration.name) ) continue; - for (const element of declaration.name.elements) { + for (const element of declaration.name.elements.nodes) { if (element.dotDotDotToken !== undefined || !ts.isIdentifier(element.name)) continue; const imported = element.propertyName !== undefined && ts.isIdentifier(element.propertyName) @@ -695,7 +695,7 @@ export function npmStaticRuntimeClassTargets( statement.importClause?.namedBindings !== undefined && ts.isNamedImports(statement.importClause.namedBindings) ) { - for (const element of statement.importClause.namedBindings.elements) { + for (const element of statement.importClause.namedBindings.elements.nodes) { if (element.isTypeOnly) continue; linked.set(element.name.text, { imported: element.propertyName?.text ?? element.name.text, @@ -719,7 +719,7 @@ export function npmStaticRuntimeClassTargets( } else if (localClasses.has(local)) targets.set(exported, { specifier: null, localName: local }); }; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( ts.isClassDeclaration(statement) && statement.name !== undefined && @@ -738,7 +738,7 @@ export function npmStaticRuntimeClassTargets( ? statement.moduleSpecifier.text : undefined; if (specifier !== undefined && !specifier.startsWith(".")) continue; - for (const element of statement.exportClause.elements) { + for (const element of statement.exportClause.elements.nodes) { if (!statement.isTypeOnly && !element.isTypeOnly) { record( element.name.text, @@ -773,7 +773,7 @@ export function npmStaticRuntimeClassTargets( left.name.text === "exports" && ts.isObjectLiteralExpression(right) ) { - for (const property of right.properties) { + for (const property of right.properties.nodes) { if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) record(property.name.text, property.name.text); else if ( @@ -791,7 +791,7 @@ export function npmStaticRuntimeClassTargets( function exportedClassNames(sourceFile: ts.SourceFile): ReadonlySet { const names = new Set(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( ts.isClassDeclaration(statement) && statement.name !== undefined && @@ -805,7 +805,7 @@ function exportedClassNames(sourceFile: ts.SourceFile): ReadonlySet { statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause) ) { - for (const element of statement.exportClause.elements) + for (const element of statement.exportClause.elements.nodes) names.add(element.propertyName?.text ?? element.name.text); continue; } @@ -833,7 +833,7 @@ function exportedClassNames(sourceFile: ts.SourceFile): ReadonlySet { left.name.text === "exports" && ts.isObjectLiteralExpression(right) ) { - for (const property of right.properties) { + for (const property of right.properties.nodes) { if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) names.add(property.name.text); else if ( @@ -864,13 +864,13 @@ function implementationComment( signatures: readonly NpmStaticOverloadSignature[], ): string | null { if ( - method.parameters.some( + method.parameters.nodes.some( (parameter) => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken !== undefined, ) ) return null; const maxParams = Math.max(...signatures.map((signature) => signature.parameters.length)); - if (method.parameters.length !== maxParams) return null; + if (method.parameters.nodes.length !== maxParams) return null; const params: string[] = []; for (let index = 0; index < maxParams; index++) { const types = [ @@ -882,7 +882,7 @@ function implementationComment( ), ]; if (types.length === 0) return null; - const parameter = method.parameters[index]; + const parameter = method.parameters.nodes[index]; if (parameter === undefined || !ts.isIdentifier(parameter.name)) return null; const name = parameter.name.text; const optional = signatures.some((signature) => { @@ -900,9 +900,9 @@ function implementationComment( function directReturn(statement: ts.Statement): ts.ReturnStatement | null { if (ts.isReturnStatement(statement)) return statement; return ts.isBlock(statement) && - statement.statements.length === 1 && - ts.isReturnStatement(statement.statements[0]!) - ? statement.statements[0]! + statement.statements.nodes.length === 1 && + ts.isReturnStatement(statement.statements.nodes[0]!) + ? statement.statements.nodes[0]! : null; } @@ -926,16 +926,16 @@ function overloadArrayBackingField( ): { field: string; type: string } | null { if ( method.body === undefined || - method.parameters.length !== 1 || - !ts.isIdentifier(method.parameters[0]!.name) + method.parameters.nodes.length !== 1 || + !ts.isIdentifier(method.parameters.nodes[0]!.name) ) return null; const getters = signatures.filter( (signature) => signature.parameters.length === 0 && signature.returnType.endsWith("[]"), ); if (getters.length !== 1) return null; - const parameter = method.parameters[0]!.name.text; - for (const statement of method.body.statements) { + const parameter = method.parameters.nodes[0]!.name.text; + for (const statement of method.body.statements.nodes) { if (!ts.isIfStatement(statement) || !undefinedParameterTest(statement.expression, parameter)) continue; const returned = directReturn(statement.thenStatement); @@ -960,7 +960,7 @@ export function applyNpmStaticDeclarationOverloads( if (declarations.size === 0) return null; const exported = exportedClassNames(sourceFile); const inserts: { offset: number; text: string }[] = []; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( !ts.isClassDeclaration(statement) || statement.name === undefined || @@ -969,12 +969,12 @@ export function applyNpmStaticDeclarationOverloads( continue; const classOverloads = declarations.get(statement.name.text); if (classOverloads === undefined) continue; - const constructor = statement.members.find( + const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); const projectedFields = new Set(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name) || @@ -995,7 +995,7 @@ export function applyNpmStaticDeclarationOverloads( constructor?.body !== undefined && !projectedFields.has(backing.field) ) { - for (const bodyStatement of constructor.body.statements) { + for (const bodyStatement of constructor.body.statements.nodes) { if ( !ts.isExpressionStatement(bodyStatement) || !ts.isBinaryExpression(bodyStatement.expression) @@ -1005,7 +1005,7 @@ export function applyNpmStaticDeclarationOverloads( if ( operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isArrayLiteralExpression(right) || - right.elements.length !== 0 || + right.elements.nodes.length !== 0 || !ts.isPropertyAccessExpression(left) || left.expression.kind !== ts.SyntaxKind.ThisKeyword || left.name.text !== backing.field @@ -1039,7 +1039,7 @@ export function applyNpmStaticDeclarationOverloads( inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); else if ( jsDocs.length === 0 && - member.parameters.length === 0 && + member.parameters.nodes.length === 0 && SCALAR_METHOD_RETURNS.has(signatures[0]!.returnType) ) { inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); @@ -1088,7 +1088,7 @@ export function applyNpmStaticDeclarationProperties( const exported = exportedClassNames(sourceFile); const inserts: { offset: number; text: string }[] = []; const classes = new Map(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (ts.isClassDeclaration(statement) && statement.name !== undefined) classes.set(statement.name.text, statement); } @@ -1100,9 +1100,9 @@ export function applyNpmStaticDeclarationProperties( if (collecting.has(name)) return new Map(); collecting.add(name); const statement = classes.get(name); - const base = statement?.heritageClauses?.find( + const base = statement?.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; const properties = new Map(); if (base && ts.isIdentifier(base) && classes.has(base.text)) { // Self-link types belong to their own class. Only the callback ABI @@ -1122,8 +1122,8 @@ export function applyNpmStaticDeclarationProperties( const callbackInitializer = (node: ts.Expression): boolean => { if ( !ts.isArrowFunction(node) || - node.parameters.length !== 0 || - (node.typeParameters?.length ?? 0) !== 0 || + node.parameters.nodes.length !== 0 || + (node.typeParameters?.nodes.length ?? 0) !== 0 || hasModifier(node, ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(node.body) ) @@ -1139,11 +1139,11 @@ export function applyNpmStaticDeclarationProperties( }; return returnsValue(node.body) !== true; }; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (!ts.isClassDeclaration(statement) || statement.name === undefined) continue; const properties = propertiesOf(statement.name.text); if (properties.size === 0) continue; - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || @@ -1162,12 +1162,12 @@ export function applyNpmStaticDeclarationProperties( if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue; inserts.push({ offset: member.getStart(sourceFile), text: `/** @type {${type}} */ ` }); } - const constructor = statement.members.find( + const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); if (constructor?.body === undefined) continue; - for (const bodyStatement of constructor.body.statements) { + for (const bodyStatement of constructor.body.statements.nodes) { if ( !ts.isExpressionStatement(bodyStatement) || !ts.isBinaryExpression(bodyStatement.expression) diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts index b71d6ca33..84a58d561 100644 --- a/packages/compiler/src/frontend/npm-static-prune.ts +++ b/packages/compiler/src/frontend/npm-static-prune.ts @@ -189,7 +189,7 @@ export function planNpmStaticReexports( if (pureMemo.get(sf) === false || !filePure(sf)) return false; visited.add(sf); const edges: { spec: string; kind?: "import" | "require" }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( ts.isImportDeclaration(stmt) && stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword && @@ -205,8 +205,8 @@ export function planNpmStaticReexports( edges.push({ spec: stmt.moduleSpecifier.text }); } ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.length !== 1) return; - const arg = node.arguments[0]; + if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return; + const arg = node.arguments.nodes[0]; if (!arg || !ts.isStringLiteralLike(arg)) return; if (node.expression.kind === ts.SyntaxKind.ImportKeyword) edges.push({ spec: arg.text, kind: "import" }); @@ -247,7 +247,7 @@ export function planNpmStaticReexports( while (queue.length > 0) { const sf = queue.shift()!; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt)) { if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue; const clause = stmt.importClause; @@ -258,7 +258,7 @@ export function planNpmStaticReexports( (clause.namedBindings !== undefined && (ts.isNamespaceImport(clause.namedBindings) || (ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.length === 0))) || + clause.namedBindings.elements.nodes.length === 0))) || // `import { type A }` still loads the module, like `import {}`. typeOnlyNamedImport(stmt) ) { @@ -266,7 +266,7 @@ export function planNpmStaticReexports( } else { if (clause.name !== undefined) request(dep, "default"); if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const element of clause.namedBindings.elements) { + for (const element of clause.namedBindings.elements.nodes) { if (!element.isTypeOnly) request(dep, (element.propertyName ?? element.name).text); } } @@ -286,8 +286,8 @@ export function planNpmStaticReexports( } } ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.length !== 1) return undefined; - const arg = node.arguments[0]; + if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return undefined; + const arg = node.arguments.nodes[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) return undefined; if ( node.expression.kind === ts.SyntaxKind.ImportKeyword || @@ -308,7 +308,7 @@ export function planNpmStaticReexports( const pruned = new Set(); for (const sf of demanded.keys()) { - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( !ts.isExportDeclaration(stmt) || stmt.moduleSpecifier === undefined || diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index 21b379893..7938471f3 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -84,15 +84,15 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } * the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */ function getterReturnOf(fn: ts.Node): ts.Expression | null { let body: ts.ConciseBody | undefined; - if (ts.isArrowFunction(fn) && fn.parameters.length === 0) body = fn.body; - else if (ts.isFunctionExpression(fn) && fn.parameters.length === 0) body = fn.body; - else if (ts.isMethodDeclaration(fn) && fn.parameters.length === 0 && fn.body !== undefined) + if (ts.isArrowFunction(fn) && fn.parameters.nodes.length === 0) body = fn.body; + else if (ts.isFunctionExpression(fn) && fn.parameters.nodes.length === 0) body = fn.body; + else if (ts.isMethodDeclaration(fn) && fn.parameters.nodes.length === 0 && fn.body !== undefined) body = fn.body; else return null; if (body === undefined) return null; if (!ts.isBlock(body)) return body; // expression-bodied arrow - if (body.statements.length !== 1) return null; - const ret = body.statements[0]!; + if (body.statements.nodes.length !== 1) return null; + const ret = body.statements.nodes[0]!; return ts.isReturnStatement(ret) && ret.expression !== undefined ? ret.expression : null; } @@ -103,11 +103,11 @@ function exportCallOf( call: ts.CallExpression, ): { target: string; entries: [string, ts.Expression | null][] } | null { if (calleeNameOf(call) !== "__export") return null; - if (call.arguments.length !== 2) return null; - const [target, table] = call.arguments as unknown as [ts.Expression, ts.Expression]; + if (call.arguments.nodes.length !== 2) return null; + const [target, table] = call.arguments.nodes as unknown as [ts.Expression, ts.Expression]; if (!ts.isIdentifier(target) || !ts.isObjectLiteralExpression(table)) return null; const entries: [string, ts.Expression | null][] = []; - for (const prop of table.properties) { + for (const prop of table.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; let name: string; if (ts.isIdentifier(prop.name)) name = prop.name.text; @@ -135,8 +135,8 @@ function definePropertyExportOf( ) { return null; } - if (call.arguments.length !== 3) return null; - const [recv, nameArg, desc] = call.arguments as unknown as [ + if (call.arguments.nodes.length !== 3) return null; + const [recv, nameArg, desc] = call.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -145,7 +145,7 @@ function definePropertyExportOf( if (!ts.isStringLiteral(nameArg) || !ts.isObjectLiteralExpression(desc)) return null; const name = nameArg.text; let value: ts.Expression | null = null; - for (const p of desc.properties) { + for (const p of desc.properties.nodes) { if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) { if (p.name.text === "value") value = p.initializer; else if (p.name.text === "get") value = getterReturnOf(p.initializer); @@ -196,7 +196,7 @@ interface ToEsmPlan { * formatting are free; the SHAPE is the contract. */ function recognizedToEsmDecl(stmt: ts.Statement): boolean { if (!ts.isVariableStatement(stmt)) return false; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length !== 1) return false; const d = decls[0]!; if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined) @@ -204,9 +204,9 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { let init: ts.Expression = d.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if (!ts.isArrowFunction(init)) return false; - if (init.parameters.length < 2 || init.parameters.length > 3) return false; - const p0 = init.parameters[0]!; - const p1 = init.parameters[1]!; + if (init.parameters.nodes.length < 2 || init.parameters.nodes.length > 3) return false; + const p0 = init.parameters.nodes[0]!; + const p1 = init.parameters.nodes[1]!; if (!ts.isIdentifier(p0.name) || !ts.isIdentifier(p1.name)) return false; const mod = p0.name.text; const isNodeMode = p1.name.text; @@ -224,10 +224,10 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { expr.expression.text !== "__copyProps" ) return false; - if (expr.arguments.length < 2) return false; - const from = expr.arguments[1]!; + if (expr.arguments.nodes.length < 2) return false; + const from = expr.arguments.nodes[1]!; if (!ts.isIdentifier(from) || from.text !== mod) return false; - let to: ts.Expression = expr.arguments[0]!; + let to: ts.Expression = expr.arguments.nodes[0]!; while (ts.isParenthesizedExpression(to)) to = to.expression; if (!ts.isConditionalExpression(to)) return false; const condText = to.condition.getText(); @@ -240,12 +240,12 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { stamp.expression.text !== "__defProp" ) return false; - if (stamp.arguments.length !== 3) return false; - const nameArg = stamp.arguments[1]!; + if (stamp.arguments.nodes.length !== 3) return false; + const nameArg = stamp.arguments.nodes[1]!; if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default") return false; - const desc = stamp.arguments[2]!; + const desc = stamp.arguments.nodes[2]!; if (!ts.isObjectLiteralExpression(desc)) return false; - return desc.properties.some( + return desc.properties.nodes.some( (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -278,10 +278,10 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH degrade: reason, }); - const declStmts = sf.statements.filter( + const declStmts = sf.statements.nodes.filter( (s) => ts.isVariableStatement(s) && - s.declarationList.declarations.some( + s.declarationList.declarations.nodes.some( (d) => ts.isIdentifier(d.name) && d.name.text === "__toESM", ), ); @@ -299,7 +299,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH const callees = new Set(calls.map((c) => c.expression)); const declNames = new Set(); for (const s of declStmts) { - for (const d of (s as ts.VariableStatement).declarationList.declarations) { + for (const d of (s as ts.VariableStatement).declarationList.declarations.nodes) { if (ts.isIdentifier(d.name) && d.name.text === "__toESM") declNames.add(d.name); } } @@ -312,12 +312,12 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH * module (pad the access) rather than a member read of its `default`. */ const bindings = new Map(); for (const call of calls) { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; const spec = arg0 !== undefined ? bareRequireSpecOf(arg0) : null; - if (spec === null || call.arguments.length > 2) return fail(TO_ESM_ARG_DEGRADE); + if (spec === null || call.arguments.nodes.length > 2) return fail(TO_ESM_ARG_DEGRADE); let isNodeMode = false; - if (call.arguments.length === 2) { - const modeArg = call.arguments[1]!; + if (call.arguments.nodes.length === 2) { + const modeArg = call.arguments.nodes[1]!; if (ts.isNumericLiteral(modeArg)) isNodeMode = Number(modeArg.text) !== 0; else if (modeArg.kind === ts.SyntaxKind.TrueKeyword) isNodeMode = true; else if (modeArg.kind === ts.SyntaxKind.FalseKeyword) isNodeMode = false; @@ -511,7 +511,7 @@ export function rewriteBundlerCjsSyntax( if (!starSpecsInOrder.includes(spec)) starSpecsInOrder.push(spec); }; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExpressionStatement(stmt)) continue; const e = stmt.expression; // `module.exports = ` @@ -524,10 +524,10 @@ export function rewriteBundlerCjsSyntax( if ( ts.isCallExpression(rhs) && calleeNameOf(rhs) === "__toCommonJS" && - rhs.arguments.length === 1 && - ts.isIdentifier(rhs.arguments[0]!) + rhs.arguments.nodes.length === 1 && + ts.isIdentifier(rhs.arguments.nodes[0]!) ) { - esbuildMainTarget = (rhs.arguments[0] as ts.Identifier).text; + esbuildMainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text; tablePos = stmt.getStart(sf); neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); sawBundleShape = true; @@ -601,8 +601,8 @@ export function rewriteBundlerCjsSyntax( } // `__exportStar(require('spec'), exports);` — the tsc/tslib spelling — // and `__export(require('spec'))`, the legacy twin. - if ((name === "__exportStar" || name === "__export") && e.arguments.length >= 1) { - const spec = bareRequireSpecOf(e.arguments[0]!); + if ((name === "__exportStar" || name === "__export") && e.arguments.nodes.length >= 1) { + const spec = bareRequireSpecOf(e.arguments.nodes[0]!); if (spec !== null) { addStarSpec(spec); neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); @@ -612,8 +612,8 @@ export function rewriteBundlerCjsSyntax( } // `__reExport(TARGET, require('spec')[, module.exports]);` — esbuild's // `export * from` plumbing. - if (name === "__reExport" && e.arguments.length >= 2) { - const spec = bareRequireSpecOf(e.arguments[1]!); + if (name === "__reExport" && e.arguments.nodes.length >= 2) { + const spec = bareRequireSpecOf(e.arguments.nodes[1]!); if (spec !== null) { addStarSpec(spec); neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); @@ -733,7 +733,7 @@ export function rewriteBundlerCjsSyntax( * fence the whole statement). Everything else snapshots via a tail * const. */ const immutableTopLevel = new Set(); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) immutableTopLevel.add(stmt.name.text); else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) @@ -742,7 +742,7 @@ export function rewriteBundlerCjsSyntax( ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0 ) { - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if (ts.isIdentifier(d.name)) immutableTopLevel.add(d.name.text); } } @@ -821,9 +821,9 @@ export function rewriteBundlerCjsSyntax( "__importDefault", ]); const helperDecls = new Map(); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length !== 1) continue; const d = decls[0]!; if (ts.isIdentifier(d.name) && HELPER_NAMES.has(d.name.text)) { diff --git a/packages/compiler/src/frontend/npm-static-subtyping.ts b/packages/compiler/src/frontend/npm-static-subtyping.ts index efe8fb6b9..a81cbf263 100644 --- a/packages/compiler/src/frontend/npm-static-subtyping.ts +++ b/packages/compiler/src/frontend/npm-static-subtyping.ts @@ -55,15 +55,15 @@ export function isNpmStaticSubclassArgument( if (node.end < diagnostic.pos || node.getStart(source) > diagnostic.end) return "skip"; if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return undefined; const index = - node.arguments?.findIndex( + node.arguments?.nodes.findIndex( (argument) => argument.getStart(source) === diagnostic.pos && argument.end === diagnostic.end, ) ?? -1; if (index < 0) return undefined; const parameter = checker.getResolvedSignature(node)?.getParameters()[index]; - if (parameter && node.arguments) + if (parameter && node.arguments?.nodes) accepted = subtype( - checker.getTypeAtLocation(node.arguments[index]!), + checker.getTypeAtLocation(node.arguments.nodes[index]!), checker.getTypeOfSymbol(parameter), ); return undefined; diff --git a/packages/compiler/src/frontend/pipeline.test.ts b/packages/compiler/src/frontend/pipeline.test.ts index 902441c96..6140d2948 100644 --- a/packages/compiler/src/frontend/pipeline.test.ts +++ b/packages/compiler/src/frontend/pipeline.test.ts @@ -14,7 +14,7 @@ const loader: ProgramLoader = () => { // Resource-ownership tests inject only the load fields the orchestration // reads. Real program loading and native execution have integration tests. const load = { - entry: { fileName: "/main.ts", text: "", statements: [] }, + entry: { fileName: "/main.ts", text: "", statements: { pos: 0, end: 0, nodes: [] } }, moduleOrder: [], dispose: vi.fn(), } as unknown as ReturnType; diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index f7ab33429..024c69d98 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -146,7 +146,7 @@ function detectAutoPackages( const owner = npmStaticPackageOfPath(sf.fileName); const viaTransitive = owner !== null && transitive.has(owner); const edges: { spec: string; loc: SrcLoc }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( (isImportDeclaration(stmt) || isExportDeclaration(stmt)) && stmt.moduleSpecifier && @@ -167,7 +167,7 @@ function detectAutoPackages( // anywhere in the package, nested ones included. if (viaTransitive) { for (const call of moduleSourceCandidates(load.program, sf).calls) { - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; if ( arg !== undefined && isStringLiteralLike(arg) && diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index 8fec2f486..c1bd183f2 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -344,8 +344,8 @@ test("repeated cycle edges reuse a file's binding index", () => { try { const main = load.entry; const peer = load.program.getSourceFile(join(directory, "peer.ts"))!; - const mainImports = main.statements.filter(ts.isImportDeclaration); - const peerImports = peer.statements.filter(ts.isImportDeclaration); + const mainImports = main.statements.nodes.filter(ts.isImportDeclaration); + const peerImports = peer.statements.nodes.filter(ts.isImportDeclaration); const edges = new Map([ [main, mainImports.map((stmt) => ({ dep: peer, stmt }))], [peer, peerImports.map((stmt) => ({ dep: main, stmt }))], diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 7d443318d..bde168c38 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -589,13 +589,13 @@ function createRequireProgramRoots7(program: ts.Program): string[] { for (const node of moduleSourceCandidates(program, sf).calls) { if ( node.questionDotToken !== undefined || - node.arguments.length !== 1 || - !ts.isStringLiteralLike(node.arguments[0]!) || + node.arguments.nodes.length !== 1 || + !ts.isStringLiteralLike(node.arguments.nodes[0]!) || !ts.isIdentifier(node.expression) ) { continue; } - calls.push({ callee: node.expression, spec: node.arguments[0]!.text }); + calls.push({ callee: node.expression, spec: node.arguments.nodes[0]!.text }); } // Root discovery only needs the callee's binding. A first-miss lookup // otherwise fetches symbols and types for every identifier in the file, @@ -1098,8 +1098,8 @@ function requireSpecOf7(node: ts.Node): string | null { } if (!ts.isCallExpression(value)) return null; if (!ts.isIdentifier(value.expression) || value.expression.text !== "require") return null; - if (value.arguments.length !== 1) return null; - const arg = value.arguments[0]!; + if (value.arguments.nodes.length !== 1) return null; + const arg = value.arguments.nodes[0]!; return ts.isStringLiteral(arg) ? arg.text : null; } @@ -1111,7 +1111,7 @@ function requireSpecOf7(node: ts.Node): string | null { function isRequireStatement7(stmt: ts.Statement): boolean { if (ts.isExpressionStatement(stmt)) return requireSpecOf7(stmt.expression) !== null; if (!ts.isVariableStatement(stmt)) return false; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; return ( decls.length > 0 && decls.every((d) => d.initializer !== undefined && requireSpecOf7(d.initializer) !== null) @@ -1129,7 +1129,7 @@ function requiresOf7( } if (!ts.isVariableStatement(stmt)) return []; const out: { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] = []; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null; if (spec !== null) out.push({ spec, node: decl, decl }); } @@ -1150,8 +1150,8 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { ) { return false; } - if (expr.arguments.length !== 3) return false; - const [recv, nameArg, desc] = expr.arguments as unknown as [ + if (expr.arguments.nodes.length !== 3) return false; + const [recv, nameArg, desc] = expr.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -1165,8 +1165,8 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { recv.name.text === "exports"); if (!isExports) return false; if (!ts.isStringLiteral(nameArg) || nameArg.text !== "__esModule") return false; - if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 1) return false; - const p = desc.properties[0]!; + if (!ts.isObjectLiteralExpression(desc) || desc.properties.nodes.length !== 1) return false; + const p = desc.properties.nodes[0]!; return ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -1187,7 +1187,7 @@ function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false; // A replacement export can have setters or non-writable function // properties. Only the wrapper's original object gets this exception. - if (sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "table")) return false; + if (sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "table")) return false; if (left.name.text === "__proto__" || left.name.text === "__esModule") return false; let receiver = left.expression; if (isModuleExportsAccess(receiver)) { @@ -1268,7 +1268,7 @@ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) { const call = s.expression as ts.CallExpression; const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier; - let receiver = call.arguments[0]!; + let receiver = call.arguments.nodes[0]!; if (ts.isPropertyAccessExpression(receiver)) receiver = receiver.expression; if ( !sourceBinding7(program, object) && @@ -1280,7 +1280,7 @@ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (pureCjsExport7(program, s)) return true; } if (ts.isVariableStatement(s)) { - return s.declarationList.declarations.every(purePrefixDecl7); + return s.declarationList.declarations.nodes.every(purePrefixDecl7); } return false; } @@ -1310,7 +1310,7 @@ export function bindingEarlyUse7( const bound: ts.Identifier[] = []; if (ts.isIdentifier(decl.name)) bound.push(decl.name); else if (ts.isObjectBindingPattern(decl.name)) { - for (const el of decl.name.elements) + for (const el of decl.name.elements.nodes) if (el.name !== undefined && ts.isIdentifier(el.name)) bound.push(el.name); } const bindings = new Map(); @@ -1319,7 +1319,7 @@ export function bindingEarlyUse7( if (sym) bindings.set(sym, id.text); } if (bindings.size === 0) return null; - const stmts = sf.statements; + const stmts = sf.statements.nodes; const hoisted = new Map(); for (const s of stmts) { if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name) { @@ -1393,9 +1393,9 @@ function requireEarlyReference7( const stmt = decl.parent?.parent; const sf = decl.getSourceFile(); if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { - const stmts = sf.statements; + const stmts = sf.statements.nodes; const k = stmts.indexOf(stmt); - const declarations = stmt.declarationList.declarations; + const declarations = stmt.declarationList.declarations.nodes; // Earlier declarators run too: `const x = read(), dep = require('./dep')`. const precedingDecls = declarations.slice(0, declarations.indexOf(decl)); let firstRunnable = program.analysis.requireFirstRunnable.get(sf); @@ -1446,7 +1446,7 @@ export function requireBindingEarlyUse7( if (sym) bindings.add(sym); } if (bindings.size === 0) return null; - const stmts = sf.statements; + const stmts = sf.statements.nodes; const cjs = isCjsJsFile7(sf, program); const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { const parent = id.parent; @@ -1471,7 +1471,7 @@ export function requireBindingEarlyUse7( if (ts.isArrowFunction(e) || ts.isFunctionExpression(e) || ts.isClassExpression(e)) { out.push(e); } else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties) { + for (const p of e.properties.nodes) { if ( (ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || @@ -1489,7 +1489,7 @@ export function requireBindingEarlyUse7( if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name !== undefined) { addUnit(s.name, s); } else if (i < k && ts.isVariableStatement(s)) { - for (const d of s.declarationList.declarations) { + for (const d of s.declarationList.declarations.nodes) { if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue; const out: ts.Node[] = []; storedCallables(d.initializer, out); @@ -1521,7 +1521,7 @@ export function requireBindingEarlyUse7( for (const s of stmts.slice(0, k)) { ts.walkPreorder(s, (node) => { if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJob(node.expression)) { - for (const arg of node.arguments ?? []) callbacks(arg); + for (const arg of node.arguments?.nodes ?? []) callbacks(arg); } return undefined; }); @@ -1702,7 +1702,7 @@ export function requireBindingEarlyUse7( return; } if (ts.isObjectLiteralExpression(value)) { - for (const p of value.properties) { + for (const p of value.properties.nodes) { if (ts.isShorthandPropertyAssignment(p)) { const sym = checker.getShorthandAssignmentValueSymbol(p); if (sym !== undefined && bindings.has(sym)) hit = p.name as ts.Identifier; @@ -1823,9 +1823,9 @@ export function requireBindingEarlyUse7( ts.isPropertyAccessExpression(e.expression) && isWrapperName(e.expression.expression, "Object") && e.expression.name.text === "defineProperty" && - e.arguments.length === 3 + e.arguments.nodes.length === 3 ) { - const [recv, key, desc] = e.arguments as unknown as [ + const [recv, key, desc] = e.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -1834,7 +1834,7 @@ export function requireBindingEarlyUse7( if (!ts.isObjectLiteralExpression(desc)) return false; const keyTarget: Target = { kind: "key", key: key.text }; const link = (node: ts.Node): void => linkTo(keyTarget, node); - for (const p of desc.properties) { + for (const p of desc.properties.nodes) { if ( (ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p)) && ts.isIdentifier(p.name) && @@ -2032,8 +2032,8 @@ function staticCreateRequireUse7(expr: ts.Expression): boolean { ts.isCallExpression(call) && call.expression === use && !call.questionDotToken && - call.arguments.length === 1 && - ts.isStringLiteralLike(call.arguments[0]!) + call.arguments.nodes.length === 1 && + ts.isStringLiteralLike(call.arguments.nodes[0]!) ); } @@ -2072,11 +2072,13 @@ function stableCreateRequireBindingReason7( return "its createRequire binding is reassigned, escapes, or uses a computed specifier"; } } - const preceding = stmt.declarationList.declarations.slice( + const preceding = stmt.declarationList.declarations.nodes.slice( 0, - stmt.declarationList.declarations.indexOf(decl), + stmt.declarationList.declarations.nodes.indexOf(decl), ); - if (bindingEarlyUse7(program, sf, sf.statements.indexOf(stmt), decl, preceding) !== null) { + if ( + bindingEarlyUse7(program, sf, sf.statements.nodes.indexOf(stmt), decl, preceding) !== null + ) { return "its createRequire binding can be used before initialization"; } return null; @@ -2093,11 +2095,11 @@ function isCreateRequireBaseCall7(program: ts.Program, expr: ts.Expression): boo init.questionDotToken !== undefined || !ts.isIdentifier(init.expression) || !isCreateRequireImport7(program, init.expression) || - init.arguments.length !== 1 + init.arguments.nodes.length !== 1 ) { return false; } - const base = stripRequireCasts7(init.arguments[0]!); + const base = stripRequireCasts7(init.arguments.nodes[0]!); if (ts.isIdentifier(base) && base.text === "__filename") return !sourceBinding7(program, base); return ( ts.isPropertyAccessExpression(base) && @@ -2124,7 +2126,7 @@ function npmStaticModuleImportReason7( if (!bindings || !ts.isNamedImports(bindings)) return null; const checker = program.getTypeChecker(); const sf = stmt.getSourceFile(); - for (const specifier of bindings.elements) { + for (const specifier of bindings.elements.nodes) { if ((specifier.propertyName?.text ?? specifier.name.text) !== "createRequire") continue; const imported = checker.getSymbolAtLocation(specifier.name); if (!imported) return "its createRequire import could not be resolved"; @@ -2217,7 +2219,7 @@ const CJS_WRAPPER_NAMES = new Set(["require", "module", "exports", "__dirname", function bindingDeclaresCjsWrapper7(name: ts.BindingName): ts.Identifier | null { if (ts.isIdentifier(name)) return CJS_WRAPPER_NAMES.has(name.text) ? name : null; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (el.name === undefined) continue; const hit = bindingDeclaresCjsWrapper7(el.name); if (hit !== null) return hit; @@ -2239,7 +2241,7 @@ function insideFunctionLike7(node: ts.Node): boolean { * import/export forms, import.meta, top-level await/for-await, or a top-level * lexical declaration that would redeclare a CommonJS wrapper binding. */ function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null { - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt; if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly) { return stmt; @@ -2255,7 +2257,7 @@ function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null { ts.isVariableStatement(stmt) && (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 ) { - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { const hit = bindingDeclaresCjsWrapper7(decl.name); if (hit !== null) return hit; } @@ -2394,7 +2396,7 @@ function selfImportTdzFences7( }; if (clause.name !== undefined) checkNamed(clause.name); if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings)) { - for (const el of clause.namedBindings.elements) { + for (const el of clause.namedBindings.elements.nodes) { if (!el.isTypeOnly) checkNamed(el.name); } } @@ -2572,7 +2574,7 @@ function inTypePosition7(node: ts.Node): boolean { * coercion of a user object) disqualifies the module's cycles. */ function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | null { const nonInert = topLevelInertness7(program); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const offence = nonInert(stmt); if (offence !== null) return offence; } @@ -2618,7 +2620,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile); }; const hasDecorator = (n: ts.Node): boolean => - ((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []).some( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some( (m) => m.kind === ts.SyntaxKind.Decorator, ); /** No function-like node anywhere in the subtree — arguments to builtin @@ -2636,11 +2638,11 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod }; const inertClass = (c: ts.ClassLikeDeclaration): ts.Node | null => { if (hasDecorator(c)) return c; - for (const h of c.heritageClauses ?? []) { + for (const h of c.heritageClauses?.nodes ?? []) { if (h.token !== ts.SyntaxKind.ExtendsKeyword) continue; - for (const t of h.types) if (!inert(t.expression)) return t.expression; + for (const t of h.types.nodes) if (!inert(t.expression)) return t.expression; } - for (const m of c.members) { + for (const m of c.members.nodes) { if (ts.isClassStaticBlockDeclaration(m)) return m; if (hasDecorator(m)) return m; const memberName = (m as { name?: ts.PropertyName }).name; @@ -2699,7 +2701,9 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod return dtsRooted(e) && inert(e.argumentExpression) && primitiveTyped(e.argumentExpression); } if (ts.isTemplateExpression(e)) { - return e.templateSpans.every((s) => inert(s.expression) && primitiveTyped(s.expression)); + return e.templateSpans.nodes.every( + (s) => inert(s.expression) && primitiveTyped(s.expression), + ); } if (ts.isVoidExpression(e) || ts.isTypeOfExpression(e)) return inert(e.expression); if (ts.isPrefixUnaryExpression(e)) { @@ -2721,9 +2725,9 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod if (ts.isConditionalExpression(e)) return inert(e.condition) && inert(e.whenTrue) && inert(e.whenFalse); if (ts.isArrayLiteralExpression(e)) - return e.elements.every((el) => !ts.isSpreadElement(el) && inert(el)); + return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inert(el)); if (ts.isObjectLiteralExpression(e)) { - return e.properties.every((p) => { + return e.properties.nodes.every((p) => { if (ts.isShorthandPropertyAssignment(p)) return true; if (!ts.isPropertyAssignment(p) || ts.isComputedPropertyName(p.name)) return false; return inert(p.initializer); @@ -2731,7 +2735,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod } if (ts.isCallExpression(e) || ts.isNewExpression(e)) { if (!dtsRooted(e.expression) || !ts.isIdentifier(chainRoot7(e.expression))) return false; - const args = e.arguments ?? []; + const args = e.arguments?.nodes ?? []; // A CALLABLE argument is admissible when it is itself dts-rooted: // a builtin-owned function value (the `promisify(fs.readFile)` // at every cycle member's top level) is runtime-implemented — even @@ -2763,11 +2767,13 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod return null; } if (ts.isEnumDeclaration(stmt)) { - return stmt.members.find((m) => m.initializer !== undefined && !inert(m.initializer)) ?? null; + return ( + stmt.members.nodes.find((m) => m.initializer !== undefined && !inert(m.initializer)) ?? null + ); } if (ts.isClassDeclaration(stmt)) return inertClass(stmt); if (ts.isVariableStatement(stmt)) { - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(d.name)) return d.name; // patterns can invoke getters/iterators if (d.initializer !== undefined && !inert(d.initializer)) return d.initializer; } @@ -2820,7 +2826,7 @@ function backEdgeUseOffence7( if (clause.namedBindings !== undefined) { if (ts.isNamespaceImport(clause.namedBindings)) bindingNames.push(clause.namedBindings.name); else - for (const el of clause.namedBindings.elements) + for (const el of clause.namedBindings.elements.nodes) if (!el.isTypeOnly) bindingNames.push(el.name); } for (const bindingName of bindingNames) { @@ -3016,7 +3022,7 @@ function cjsCycleRefusal7( if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return true; if (ts.isClassExpression(e)) return classSyncParts7(e).every(inertPart); if (ts.isObjectLiteralExpression(e)) { - return e.properties.every((p) => + return e.properties.nodes.every((p) => ts.isShorthandPropertyAssignment(p) ? true : ts.isPropertyAssignment(p) @@ -3028,7 +3034,7 @@ function cjsCycleRefusal7( ); } if (ts.isArrayLiteralExpression(e)) { - return e.elements.every((el) => !ts.isSpreadElement(el) && inertExpr(el)); + return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inertExpr(el)); } if (ts.isPropertyAccessExpression(e)) return builtinRoot(e); if (ts.isVoidExpression(e)) return literal(e.expression); @@ -3036,13 +3042,13 @@ function cjsCycleRefusal7( return ( requireSpecOf7(e) === null && builtinRoot(e.expression) && - (e.arguments ?? []).every(literal) + (e.arguments?.nodes ?? []).every(literal) ); } return false; }; const inertPart = (part: ts.Node): boolean => { - if (ts.isHeritageClause(part)) return part.types.every((t) => inertExpr(t.expression)); + if (ts.isHeritageClause(part)) return part.types.nodes.every((t) => inertExpr(t.expression)); if (ts.isComputedPropertyName(part)) return inertExpr(part.expression); return ts.isExpression(part) && inertExpr(part); }; @@ -3080,17 +3086,17 @@ function cjsCycleRefusal7( call.expression.expression.text === "Object" && !sourceBinding7(program, call.expression.expression) && call.expression.name.text === "defineProperty" && - call.arguments.length === 3 && - exportObject(call.arguments[0]!) && - ts.isStringLiteralLike(call.arguments[1]!) && - inertExpr(call.arguments[2]!) + call.arguments.nodes.length === 3 && + exportObject(call.arguments.nodes[0]!) && + ts.isStringLiteralLike(call.arguments.nodes[1]!) && + inertExpr(call.arguments.nodes[2]!) ); }; for (const m of comp) { if (!isCjsJsFile7(m, program)) { return `${m.fileName} is not a CommonJS module — require cycles are admitted between CommonJS modules only`; } - const stmts = m.statements; + const stmts = m.statements.nodes; // The first statement that can run another module's code. let firstEntry = stmts.length; for (let i = 0; i < stmts.length; i++) { @@ -3101,7 +3107,7 @@ function cjsCycleRefusal7( (ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)) || (ts.isClassDeclaration(st) && classSyncParts7(st).every(inertPart)) || (ts.isVariableStatement(st) && - st.declarationList.declarations.every( + st.declarationList.declarations.nodes.every( (d) => ts.isIdentifier(d.name) && (d.initializer === undefined || inertExpr(d.initializer)), )) || @@ -3236,7 +3242,7 @@ function exactCycleRefusal7( if (hasTopLevelAwait7(m)) { return `${m.fileName} uses top-level await inside the cycle`; } - for (const stmt of m.statements) { + for (const stmt of m.statements.nodes) { if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) { return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`; } @@ -3430,7 +3436,7 @@ function computeCycleEarlyBindings7( const names = new Set(); let dirty = !known; evaluation.forEach((m, rank) => { - for (const stmt of m.statements) { + for (const stmt of m.statements.nodes) { const offence = nonInert(stmt); const exposed = dirty || offence !== null; const declared: { name: ts.Identifier; kind: CycleEarlyBinding["kind"] }[] = []; @@ -3442,7 +3448,7 @@ function computeCycleEarlyBindings7( ) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { for (const name of boundNames7(d.name)) { declared.push({ name, kind: lexical ? "lexical" : "var" }); } @@ -3614,7 +3620,7 @@ function moduleEarlyBindingsOf7( units.push(e); immediate.set(e, classSyncParts7(e)); } else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties) { + for (const p of e.properties.nodes) { if ( ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || @@ -3630,11 +3636,11 @@ function moduleEarlyBindingsOf7( } else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units); } } else if (ts.isArrayLiteralExpression(e)) { - for (const el of e.elements) if (!ts.isSpreadElement(el)) storedIn(el, units); + for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units); } else if (ts.isCallExpression(e)) { // `const f = wrap(callback)` where wrap only returns closures over // the callback: it runs when the stored result is called. - e.arguments.forEach((arg, index) => { + e.arguments.nodes.forEach((arg, index) => { let fn: ts.Expression = arg; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; if ( @@ -3645,7 +3651,7 @@ function moduleEarlyBindingsOf7( }); } }; - sf.statements.forEach((stmt, index) => { + sf.statements.nodes.forEach((stmt, index) => { if (ambient(stmt)) return; if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { store(stmt.name, [stmt]); @@ -3655,7 +3661,7 @@ function moduleEarlyBindingsOf7( candidateNames.add(stmt.name.text); } else if (ts.isVariableStatement(stmt)) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if (ts.isIdentifier(d.name) && d.initializer !== undefined) { const units: ts.Node[] = []; storedIn(d.initializer, units); @@ -3702,7 +3708,7 @@ function moduleEarlyBindingsOf7( while (ts.isParenthesizedExpression(e)) e = e.expression; if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e); else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties) { + for (const p of e.properties.nodes) { if (ts.isMethodDeclaration(p)) stored.add(p); else if (ts.isPropertyAssignment(p)) callbacks(p.initializer); } @@ -3713,7 +3719,7 @@ function moduleEarlyBindingsOf7( (ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJobCallee7(checker, node.expression) ) { - for (const arg of node.arguments ?? []) callbacks(arg); + for (const arg of node.arguments?.nodes ?? []) callbacks(arg); } return undefined; }); @@ -3793,7 +3799,7 @@ function moduleEarlyBindingsOf7( }); } }; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isClassDeclaration(stmt)) { // The binding initializes once the class definition, static // initializers included, has run. @@ -3803,7 +3809,7 @@ function moduleEarlyBindingsOf7( } else if (ts.isVariableStatement(stmt)) { // Declarators run in order: a later declarator's initializer cannot // observe an earlier binding before it was initialized. - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { visit(d); drain(); report(d); @@ -3848,10 +3854,11 @@ function returnsOnlyClosuresOver7( if (fn === undefined) return false; const like = fn as ts.FunctionLikeDeclaration; const body = like.body; - const param = like.parameters[index]; + const param = like.parameters.nodes[index]; if (body === undefined || !ts.isBlock(body) || param === undefined) return false; if (!ts.isIdentifier(param.name) || param.dotDotDotToken !== undefined) return false; - if (like.parameters.slice(0, index).some((p) => p.dotDotDotToken !== undefined)) return false; + if (like.parameters.nodes.slice(0, index).some((p) => p.dotDotDotToken !== undefined)) + return false; const paramName = param.name.text; let deferred = true; ts.walkPreorder(body, (node) => { @@ -3897,11 +3904,11 @@ function returnsOnlyClosuresOver7( * call or a construction. */ function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] { const decorators = (n: ts.Node): ts.Node[] => - ((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []).filter( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).filter( (m) => m.kind === ts.SyntaxKind.Decorator, ); - const parts: ts.Node[] = [...decorators(c), ...(c.heritageClauses ?? [])]; - for (const m of c.members) { + const parts: ts.Node[] = [...decorators(c), ...(c.heritageClauses?.nodes ?? [])]; + for (const m of c.members.nodes) { parts.push(...decorators(m)); const memberName = (m as { name?: ts.PropertyName }).name; if (memberName !== undefined && ts.isComputedPropertyName(memberName)) parts.push(memberName); @@ -4027,7 +4034,7 @@ function foldablePosition7(ref: ts.Identifier): boolean { function boundNames7(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const out: ts.Identifier[] = []; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (ts.isOmittedExpression(el) || el.name === undefined) continue; out.push(...boundNames7(el.name)); } @@ -4040,7 +4047,7 @@ function typeOnlyNamespace7(decl: ts.ModuleDeclaration): boolean { let body = decl.body; while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined || !ts.isModuleBlock(body)) return true; - return body.statements.every( + return body.statements.nodes.every( (s) => ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || @@ -4274,7 +4281,7 @@ function preflight7(load: LoadResult): { names = new Set(); builtinNamespaceNames.set(d.fileName, names); const sf = p.getSourceFile(d.fileName); - for (const stmt of sf?.statements ?? []) { + for (const stmt of sf?.statements?.nodes ?? []) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue; const bindings = stmt.importClause?.namedBindings; @@ -4485,7 +4492,7 @@ function preflight7(load: LoadResult): { if (ambientModuleDecls === null) { ambientModuleDecls = { names: new Set(), patterns: [] }; for (const f of program.getSourceFiles()) { - for (const s of f.statements) { + for (const s of f.statements.nodes) { if (!ts.isModuleDeclaration(s) || !ts.isStringLiteral(s.name)) continue; const name = s.name.text; if (name.includes("*")) ambientModuleDecls.patterns.push(name); @@ -4536,7 +4543,7 @@ function preflight7(load: LoadResult): { edges.set(sf, deps); refusals.set(sf, []); depPositions.set(sf, []); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportEqualsDeclaration(stmt)) { // The ENTITY form (`import a = A`, `import x = N.y`) is namespace // alias plumbing — the lowerer resolves references through it and @@ -4898,7 +4905,7 @@ function preflight7(load: LoadResult): { } { const jsRequireFile = isJsSourceFileName(sf.fileName); - const stmts = sf.statements; + const stmts = sf.statements.nodes; // A binding that code running before the require can reach. Node // initializes it AT the require: a const/let read earlier throws // ReferenceError, which the lowering reproduces with an @@ -4987,7 +4994,7 @@ function preflight7(load: LoadResult): { pattern: ts.ObjectBindingPattern, module: string | null, ): boolean => - pattern.elements.every((e) => { + pattern.elements.nodes.every((e) => { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; if (e.name !== undefined && ts.isIdentifier(e.name)) return true; // A nested pattern under a named export of a program module @@ -5363,7 +5370,7 @@ function cjsNamedImportLinkCheck( spec: string; dep: ts.SourceFile; }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( !ts.isImportDeclaration(stmt) || stmt.moduleSpecifier === undefined || @@ -5376,7 +5383,7 @@ function cjsNamedImportLinkCheck( const spec = stmt.moduleSpecifier.text; const dep = cjsDepOf(sf, spec); if (dep === null) continue; - for (const e of clause.namedBindings.elements) { + for (const e of clause.namedBindings.elements.nodes) { if (e.isTypeOnly) continue; const nameNode = e.propertyName ?? e.name; entries.push({ local: e.name.text, exportName: nameNode.text, nameNode, spec, dep }); @@ -5387,14 +5394,14 @@ function cjsNamedImportLinkCheck( if (!visible(e.dep, e.exportName)) return { exportName: e.exportName, spec: e.spec, nameNode: e.nameNode, sf }; } - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue; if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) continue; const spec = stmt.moduleSpecifier.text; const dep = cjsDepOf(sf, spec); if (dep === null) continue; - for (const e of stmt.exportClause.elements) { + for (const e of stmt.exportClause.elements.nodes) { if (e.isTypeOnly) continue; const nameNode = e.propertyName ?? e.name; if (!visible(dep, nameNode.text)) return { exportName: nameNode.text, spec, nameNode, sf }; @@ -5411,7 +5418,7 @@ function cjsNamedImportLinkCheck( const dfs = (sf: ts.SourceFile): BadCjsImport | null => { if (visited.has(sf)) return null; visited.add(sf); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; @@ -5456,7 +5463,7 @@ function cjsNamedImportLinkCheck( // it is CommonJS (module_job.js consults the ROOT job's moduleRequests/ // commonJsDeps — a deep module's failure keeps the generic wording). let flavored = false; - for (const stmt of entry.statements) { + for (const stmt of entry.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if ( @@ -5600,7 +5607,7 @@ function analyzeEsmNamedImportLinks( // symbols make the name unavailable. `default` never star-reexports. if (name === "default") return undefined; let starExport: ts.Symbol | undefined; - for (const stmt of dep.statements) { + for (const stmt of dep.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly || stmt.exportClause !== undefined) continue; if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; @@ -5623,7 +5630,7 @@ function analyzeEsmNamedImportLinks( nameNode: ts.Node; dep: ts.SourceFile; }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const clause = stmt.importClause; if (clause === undefined || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; @@ -5640,7 +5647,7 @@ function analyzeEsmNamedImportLinks( }); } if (clause.namedBindings === undefined || !ts.isNamedImports(clause.namedBindings)) continue; - for (const element of clause.namedBindings.elements) { + for (const element of clause.namedBindings.elements.nodes) { if (element.isTypeOnly) continue; const nameNode = element.propertyName ?? element.name; imports.push({ @@ -5667,7 +5674,7 @@ function analyzeEsmNamedImportLinks( } } - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue; const moduleSpecifier = stmt.moduleSpecifier; const exportClause = stmt.exportClause; @@ -5676,7 +5683,7 @@ function analyzeEsmNamedImportLinks( const spec = moduleSpecifier.text; const dep = resolveEdge(sf, spec); if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue; - for (const element of exportClause.elements) { + for (const element of exportClause.elements.nodes) { if (element.isTypeOnly) continue; const nameNode = element.propertyName ?? element.name; if (runtimeExport(dep, nameNode.text) === undefined) { @@ -5694,7 +5701,7 @@ function analyzeEsmNamedImportLinks( const dfs = (sf: ts.SourceFile): BadEsmImport | null => { if (visited.has(sf)) return null; visited.add(sf); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; @@ -5792,12 +5799,12 @@ function earlierLinkCrash( if (sf === undefined) return first.loc.start < second.loc.start ? first : second; const rank = (crash: StartupCrash): [number, number] => { const entries: { local: string; start: number; kind: 0 | 1 }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier)) { const clause = stmt.importClause; if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const element of clause.namedBindings.elements) { + for (const element of clause.namedBindings.elements.nodes) { if (!element.isTypeOnly) { entries.push({ local: element.name.text, @@ -5813,7 +5820,7 @@ function earlierLinkCrash( const direct = entries.findIndex((entry) => entry.start === crash.loc.start); if (direct >= 0) return [0, direct]; let reexport = 0; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const exportClause = ts.isExportDeclaration(stmt) ? stmt.exportClause : undefined; if ( !ts.isExportDeclaration(stmt) || @@ -5822,7 +5829,7 @@ function earlierLinkCrash( !ts.isNamedExports(exportClause) ) continue; - for (const element of exportClause.elements) { + for (const element of exportClause.elements.nodes) { if ( !element.isTypeOnly && (element.propertyName ?? element.name).getStart(sf) === crash.loc.start @@ -5890,7 +5897,7 @@ export function isJsSourceFile(sf: ts.SourceFile): boolean { export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean): boolean { if (!isJsSourceFile(sf) || isNodeModulesPath(sf.fileName)) return false; let checked = projectCheckJs; - const head = sf.text.slice(0, sf.statements[0]?.getStart(sf) ?? sf.text.length); + const head = sf.text.slice(0, sf.statements.nodes[0]?.getStart(sf) ?? sf.text.length); for (const line of head.split("\n")) { const directive = /^\s*\/\/\s*@ts-(no)?check\b/.exec(line); if (directive) checked = directive[1] === undefined; @@ -5903,7 +5910,7 @@ export function orderedImportsOf( sf: ts.SourceFile, ): { stmt: ts.Statement; dep: ts.SourceFile | null }[] { const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue; if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue; @@ -6025,7 +6032,7 @@ export function cjsVoidInitPreambleReals( Extract, { kind: "member" }> >(); for (const name of names) { - const real = sf.statements + const real = sf.statements.nodes .map((s) => cjsExportAssignmentOf(s)) .find( (other) => @@ -6085,7 +6092,7 @@ export function cjsExportDiscardReason(stmt: ts.Statement): string | null { if (!ts.isSourceFile(sf)) return null; const mine = cjsExportAssignmentOf(stmt); if (!mine) return null; - const stmts = sf.statements; + const stmts = sf.statements.nodes; let lastTable = -1; for (let i = 0; i < stmts.length; i++) { if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i; @@ -6123,15 +6130,15 @@ export function cjsExportTargetLiteral( ts.isNewExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === "Proxy" && - (e.arguments?.length ?? 0) >= 1 + (e.arguments?.nodes.length ?? 0) >= 1 ) { - return cjsExportTargetLiteral(e.arguments![0]!, sf); + return cjsExportTargetLiteral(e.arguments!.nodes[0]!, sf); } if (ts.isIdentifier(e)) { - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; if ((stmt.declarationList.flags & ts.NodeFlags.Const) === 0) continue; - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if ( d.name !== undefined && ts.isIdentifier(d.name) && @@ -6157,7 +6164,7 @@ export function cjsClassExprWholeExportOf( sf: ts.SourceFile, ): { classExpr: ts.ClassExpression; stmt: ts.Statement } | null { let last: { classExpr: ts.ClassExpression; stmt: ts.Statement } | null = null; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (!cjs || cjs.kind !== "table") continue; let rhs: ts.Expression = cjs.expr.right; @@ -6190,7 +6197,7 @@ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolea if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false; const sf = obj.getSourceFile(); if (!isJsSourceFile(sf)) return false; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) { return cjsExportTargetLiteral(cjs.expr.right, sf) === obj; @@ -6294,7 +6301,7 @@ function receiverRoot(text: string, pos: number): string | null { /** Whether `name` is bound in `fileName` by an import of a Node builtin. */ function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): boolean { const sf = load.program.getSourceFile(fileName); - for (const stmt of sf?.statements ?? []) { + for (const stmt of sf?.statements?.nodes ?? []) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue; const clause = stmt.importClause; @@ -6304,7 +6311,7 @@ function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): b if ( bindings && ts.isNamedImports(bindings) && - bindings.elements.some((e) => e.name.text === name) + bindings.elements.nodes.some((e) => e.name.text === name) ) return true; } diff --git a/packages/compiler/src/frontend/services.test.ts b/packages/compiler/src/frontend/services.test.ts index 03afcd655..bd172bbc5 100644 --- a/packages/compiler/src/frontend/services.test.ts +++ b/packages/compiler/src/frontend/services.test.ts @@ -80,7 +80,7 @@ test("syntax sessions reuse one connection and returned ASTs survive edits and c services.close(); services.close(); expect(connections[0]!.close).toHaveBeenCalledTimes(1); - expect(before.statements[0]!.getText()).toBe(before.text); + expect(before.statements.nodes[0]!.getText()).toBe(before.text); }); test("program hosts remain usable after their creating services close", () => { diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index ded7c5225..248b3f922 100644 --- a/packages/compiler/src/frontend/tier-analysis.ts +++ b/packages/compiler/src/frontend/tier-analysis.ts @@ -360,9 +360,9 @@ function subjectOf(node: ts.Node): ts.Node { ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && REFLECTION_NAMESPACES.has(callee.expression.text) && - current.arguments.length > 0 + current.arguments.nodes.length > 0 ) { - return current.arguments[0]!; + return current.arguments.nodes[0]!; } return callee; } diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index 9ed4ee6fa..e2440b787 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterAll, beforeAll, expect, test } from "vitest"; import type { SourceFile, Node } from "typescript/unstable/ast"; -import { AstFile, AstNode } from "./ast-node.js"; +import { AstFile, AstNode, AstNodeList } from "./ast-node.js"; import { AstKind, KIND_NODE_LIST, @@ -229,11 +229,15 @@ for (const name of [...Object.keys(cases), "model.ts"]) { expect(node.getLeadingTriviaWidth()).toBe(expected.getLeadingTriviaWidth()); expect(ids(node.jsDoc)).toEqual(ids(expected.jsDoc)); for (const property of astChildNames(node.kind).split(",").filter(Boolean)) { - const actual = Reflect.get(node, property) as AstNode | AstNode[] | undefined; + const actual = Reflect.get(node, property) as AstNode | AstNodeList | undefined; const reference = Reflect.get(expected, property) as OracleNode | OracleNode[] | undefined; if (Array.isArray(reference)) { - expect(ids(actual as AstNode[]), `${name}:${index}.${property}`).toEqual(ids(reference)); - expect(Reflect.get(node, property)).toBe(actual); + expect(ids((actual as AstNodeList).nodes), `${name}:${index}.${property}`).toEqual( + ids(reference), + ); + expect((Reflect.get(node, property) as AstNodeList).nodes).toBe( + (actual as AstNodeList).nodes, + ); } else { expect((actual as AstNode | undefined)?.index, `${name}:${index}.${property}`).toBe( reference?.index, @@ -256,7 +260,7 @@ for (const name of [...Object.keys(cases), "model.ts"]) { node.forEachChild( () => 0, (list) => { - actualLists.push(list.map((n) => n.index)); + actualLists.push(list.nodes.map((n) => n.index)); return 0; }, ); @@ -322,7 +326,7 @@ test("checker handles reject cross-file, wrong-kind and nil identities", () => { test("direct node slots preserve lazy parents, identity and invalid-index checks", () => { let materialized = 0; - const file = new AstFile(decoded.get("main.ts")!.bytes, undefined, () => { + const file = new AstFile(decoded.get("main.ts")!.bytes, () => { materialized++; }); let index = file.wire.nodeCount - 1; @@ -347,7 +351,7 @@ test("direct node slots preserve lazy parents, identity and invalid-index checks test("named child access shares node and list identities without materializing siblings", () => { let materialized = 0; - const file = new AstFile(decoded.get("main.ts")!.bytes, undefined, () => { + const file = new AstFile(decoded.get("main.ts")!.bytes, () => { materialized++; }); let index = 1; @@ -366,9 +370,11 @@ test("named child access shares node and list identities without materializing s expect(node.child("__proto__")).toBeUndefined(); expect(node.childList("typeParameters")).toBeUndefined(); const parameters = node.childList("parameters"); - expect(parameters?.map((parameter) => parameter.index)).toEqual(file.wire.list(paramsIndex)); - expect(node.child("parameters")).toBe(parameters); - expect(node.parameters).toBe(parameters); + expect(parameters?.nodes.map((parameter) => parameter.index)).toEqual( + file.wire.list(paramsIndex), + ); + expect((node.child("parameters") as AstNodeList | undefined)?.nodes).toBe(parameters?.nodes); + expect(node.parameters?.nodes).toBe(parameters?.nodes); const body = node.childNode("body"); expect(body?.index).toBe(bodyIndex); expect(node.child("body")).toBe(body); @@ -414,7 +420,7 @@ test("a source view requires a source-file root", () => { const nodes = words.getUint32(HEADER_OFFSET_NODES, true); words.setUint32(nodes + NODE_LEN, AstKind.Identifier, true); expect(() => new AstFile(bytes).sourceFile).toThrow("expected a source file root"); - expect(decoded.get("empty.ts")!.file.root.statements).toEqual([]); + expect(decoded.get("empty.ts")!.file.root.statements?.nodes).toEqual([]); }); test("preorder traversal preserves depth, skipped subtrees and early termination", () => { @@ -483,7 +489,7 @@ test("selective preorder preserves semantic order and depth without materializin }; visit(oracle.getOrCreateNodeAtIndex(1), 0); let materialized = 0; - const file = new AstFile(bytes, undefined, () => { + const file = new AstFile(bytes, () => { materialized++; }); const actual: [number, number][] = []; diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index d12c199d4..92ab5e4b8 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -16,8 +16,21 @@ import { import type { SourceFile } from "./ast-types.js"; import { SyntaxKind, NodeFlags } from "./enums.js"; +/** A list of child nodes with the list's own position, the shape of tsc-ts's + * NodeList. The file keeps the nodes array, not this object: each read of a + * list field returns a new AstNodeList over the same array, so `nodes` keeps + * its identity and a decoded file holds no list objects. */ +export class AstNodeList { + constructor( + readonly pos: number, + readonly end: number, + readonly nodes: readonly T[], + ) {} +} + /** One immutable AST response owns one object per node and one array per - * list. Parent access is lazy, so resolving a checker handle near the bottom + * list. + * Parent access is lazy, so resolving a checker handle near the bottom * of a deep tree never recursively constructs its ancestors. */ export class AstFile { readonly wire: AstWireFile; @@ -34,7 +47,6 @@ export class AstFile { constructor( bytes: Uint8Array, - private readonly listMetadata?: (nodes: AstNode[], pos: number, end: number) => void, private readonly materialized?: () => void, ) { this.wire = new AstWireFile(bytes); @@ -63,14 +75,15 @@ export class AstFile { return node; } - list(index: number): AstNode[] { - if (Object.hasOwn(this.lists, index)) return this.lists[index]!; - const nodes: AstNode[] = []; - for (const child of this.wire.list(index)) nodes.push(this.node(child)); - this.listMetadata?.(nodes, this.wire.pos(index) >>> 0, this.wire.end(index) >>> 0); - this.lists[index] = nodes; - this.materialized?.(); - return nodes; + list(index: number): AstNodeList { + let nodes = this.lists[index]; + if (nodes === undefined) { + nodes = []; + for (const child of this.wire.list(index)) nodes.push(this.node(child)); + this.lists[index] = nodes; + this.materialized?.(); + } + return new AstNodeList(this.wire.pos(index) >>> 0, this.wire.end(index) >>> 0, nodes); } namedChild(node: AstNode, name: string): number { @@ -190,11 +203,11 @@ export class AstNode { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.node(index); } - childList(name: string): AstNode[] | undefined { + childList(name: string): AstNodeList | undefined { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.list(index); } - child(name: string): AstNode | AstNode[] | undefined { + child(name: string): AstNode | AstNodeList | undefined { const index = this.file.namedChild(this, name); if (index === 0) return undefined; return this.file.wire.kind(index) === KIND_NODE_LIST @@ -213,7 +226,7 @@ export class AstNode { throw new AstDecodeError("sibling belongs to another parent"); const kind = wire.kind(index); if (kind === KIND_NODE_LIST) { - for (const node of file.list(index)) indices.push(node.index); + for (const node of file.list(index).nodes) indices.push(node.index); } else if (kind !== AstKind.JSDoc) { indices.push(index); } @@ -222,7 +235,7 @@ export class AstNode { forEachChild( visitNode: (node: AstNode) => T, - visitList?: (nodes: readonly AstNode[]) => T, + visitList?: (nodes: AstNodeList) => T, ): T | undefined { const wire = this.file.wire; // Walk the immutable sibling links directly. Repeated semantic scans @@ -237,7 +250,7 @@ export class AstNode { const result = visitList(list); if (result) return result; } else { - for (const node of list) { + for (const node of list.nodes) { const result = visitNode(node); if (result) return result; } @@ -395,7 +408,7 @@ export class AstNode { } get modifierFlags(): number { let result = 0; - for (const modifier of this.childList("modifiers") ?? []) + for (const modifier of this.childList("modifiers")?.nodes ?? []) result |= astModifierFlag(modifier.kind); return result; } @@ -439,7 +452,7 @@ export class AstNode { get argumentExpression(): AstNode | undefined { return this.childNode("argumentExpression"); } - get arguments(): readonly AstNode[] | undefined { + get arguments(): AstNodeList | undefined { return this.childList("arguments"); } get assertsModifier(): AstNode | undefined { @@ -448,7 +461,7 @@ export class AstNode { get asteriskToken(): AstNode | undefined { return this.childNode("asteriskToken"); } - get attributes(): AstNode | readonly AstNode[] | undefined { + get attributes(): AstNode | AstNodeList | undefined { return this.child("attributes"); } get awaitModifier(): AstNode | undefined { @@ -466,13 +479,13 @@ export class AstNode { get checkType(): AstNode | undefined { return this.childNode("checkType"); } - get children(): AstNode | readonly AstNode[] | undefined { + get children(): AstNode | AstNodeList | undefined { return this.child("children"); } get className(): AstNode | undefined { return this.childNode("className"); } - get clauses(): readonly AstNode[] | undefined { + get clauses(): AstNodeList | undefined { return this.childList("clauses"); } get closingElement(): AstNode | undefined { @@ -484,7 +497,7 @@ export class AstNode { get colonToken(): AstNode | undefined { return this.childNode("colonToken"); } - get comment(): readonly AstNode[] | undefined { + get comment(): AstNodeList | undefined { return this.childList("comment"); } get condition(): AstNode | undefined { @@ -496,7 +509,7 @@ export class AstNode { get declarationList(): AstNode | undefined { return this.childNode("declarationList"); } - get declarations(): readonly AstNode[] | undefined { + get declarations(): AstNodeList | undefined { return this.childList("declarations"); } get defaultType(): AstNode | undefined { @@ -508,7 +521,7 @@ export class AstNode { get elementType(): AstNode | undefined { return this.childNode("elementType"); } - get elements(): readonly AstNode[] | undefined { + get elements(): AstNodeList | undefined { return this.childList("elements"); } get elseStatement(): AstNode | undefined { @@ -547,7 +560,7 @@ export class AstNode { get head(): AstNode | undefined { return this.childNode("head"); } - get heritageClauses(): readonly AstNode[] | undefined { + get heritageClauses(): AstNodeList | undefined { return this.childList("heritageClauses"); } get importClause(): AstNode | undefined { @@ -574,10 +587,10 @@ export class AstNode { get literal(): AstNode | undefined { return this.childNode("literal"); } - get members(): readonly AstNode[] | undefined { + get members(): AstNodeList | undefined { return this.childList("members"); } - get modifiers(): readonly AstNode[] | undefined { + get modifiers(): AstNodeList | undefined { return this.childList("modifiers"); } get moduleReference(): AstNode | undefined { @@ -622,13 +635,13 @@ export class AstNode { get parameterName(): AstNode | undefined { return this.childNode("parameterName"); } - get parameters(): readonly AstNode[] | undefined { + get parameters(): AstNodeList | undefined { return this.childList("parameters"); } get postfixToken(): AstNode | undefined { return this.childNode("postfixToken"); } - get properties(): readonly AstNode[] | undefined { + get properties(): AstNodeList | undefined { return this.childList("properties"); } get propertyName(): AstNode | undefined { @@ -649,7 +662,7 @@ export class AstNode { get statement(): AstNode | undefined { return this.childNode("statement"); } - get statements(): readonly AstNode[] | undefined { + get statements(): AstNodeList | undefined { return this.childList("statements"); } get tag(): AstNode | undefined { @@ -658,13 +671,13 @@ export class AstNode { get tagName(): AstNode | undefined { return this.childNode("tagName"); } - get tags(): readonly AstNode[] | undefined { + get tags(): AstNodeList | undefined { return this.childList("tags"); } get template(): AstNode | undefined { return this.childNode("template"); } - get templateSpans(): readonly AstNode[] | undefined { + get templateSpans(): AstNodeList | undefined { return this.childList("templateSpans"); } get thenStatement(): AstNode | undefined { @@ -685,7 +698,7 @@ export class AstNode { get type(): AstNode | undefined { return this.childNode("type"); } - get typeArguments(): readonly AstNode[] | undefined { + get typeArguments(): AstNodeList | undefined { return this.childList("typeArguments"); } get typeExpression(): AstNode | undefined { @@ -697,10 +710,10 @@ export class AstNode { get typeParameter(): AstNode | undefined { return this.childNode("typeParameter"); } - get typeParameters(): readonly AstNode[] | undefined { + get typeParameters(): AstNodeList | undefined { return this.childList("typeParameters"); } - get types(): readonly AstNode[] | undefined { + get types(): AstNodeList | undefined { return this.childList("types"); } get value(): AstNode | undefined { diff --git a/packages/compiler/src/frontend/ts7/ast-types.generated.ts b/packages/compiler/src/frontend/ts7/ast-types.generated.ts index 41de9b115..de6ff892f 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.generated.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.generated.ts @@ -1343,7 +1343,7 @@ export interface JsxText extends Node { export interface SyntaxList extends Node { readonly flags: NodeFlags; readonly kind: SyntaxKind.SyntaxList; - readonly children: readonly Node[]; + readonly children: NodeArray; } export interface JSDoc extends Node { diff --git a/packages/compiler/src/frontend/ts7/ast-types.ts b/packages/compiler/src/frontend/ts7/ast-types.ts index 04af72566..6d421e58d 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.ts @@ -1,4 +1,4 @@ -import type { AstNode } from "./ast-node.js"; +import type { AstNode, AstNodeList } from "./ast-node.js"; import { SyntaxKind } from "./enums.js"; import type { EndOfFile, @@ -11,7 +11,9 @@ import type { export * from "./ast-types.generated.js"; export type Node = AstNode; -export type NodeArray = readonly T[]; +/** A node list. The type is the list class itself, not a structural shape: + * native code represents a class and a record type differently. */ +export type NodeArray = AstNodeList; export type Path = string; export type __String = string; export interface TextRange { diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index c97dbe4c3..ec26aa0b0 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -186,7 +186,7 @@ export function canHaveModifiers(node: Node): boolean { /** The node's modifier tokens, decorators excluded (5.9.3's getModifiers). */ export function getModifiers(node: Node): readonly Modifier[] | undefined { - const modifiers = node.modifiers; + const modifiers = node.modifiers?.nodes; if (modifiers === undefined) return undefined; return modifiers.filter((m): m is Modifier => m.kind !== SyntaxKind.Decorator); } @@ -232,7 +232,7 @@ function modifierToFlag(kind: number): number { } function modifierFlagsOfNode(node: Node): number { - const modifiers = node.modifiers; + const modifiers = node.modifiers?.nodes; let flags = ModifierFlags.None as number; if (modifiers !== undefined) { for (const m of modifiers) flags |= modifierToFlag(m.kind); diff --git a/packages/compiler/src/frontend/ts7/census-check.ts b/packages/compiler/src/frontend/ts7/census-check.ts index 84673720e..1aac06744 100644 --- a/packages/compiler/src/frontend/ts7/census-check.ts +++ b/packages/compiler/src/frontend/ts7/census-check.ts @@ -63,7 +63,7 @@ type CensusTypes = [ ts.SignatureDeclaration, ts.Signature, ts.RegularExpressionLiteral, - ts.NodeArray, + readonly ts.Node[], ts.ForStatement, ts.ExportAssignment, ts.Declaration, diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index 53b960b91..d9b6e44a1 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -52,9 +52,9 @@ function connect(files: Record) { } function declaration(root: SourceFile, name: string): AstNode { - for (const statement of root.statements) { + for (const statement of root.statements.nodes) { if (statement.name?.text === name) return statement; - for (const node of statement.declarationList?.declarations ?? []) + for (const node of statement.declarationList?.declarations?.nodes ?? []) if (node.name?.text === name) return node; } throw new Error(`Missing declaration ${name}`); diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index bf83b367c..f3ef51a3d 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -173,7 +173,7 @@ function isDeferredExecutableRoot(node: Node, walk: PrefetchWalk): boolean { return ( parent.kind === SyntaxKind.PropertyDeclaration && parent.initializer === node && - !parent.modifiers?.some((modifier) => modifier.kind === SyntaxKind.StaticKeyword) + !parent.modifiers?.nodes.some((modifier) => modifier.kind === SyntaxKind.StaticKeyword) ); } diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts index c917a6f66..cb05db50c 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts @@ -82,9 +82,9 @@ test("synthetic type roots and descendants match the pinned SDK without claiming const expectedRoot = sdk.program.getSourceFile(file)!; let checked = 0; for (let index = 1; index < 7; index++) { - const statement = root.statements[index]!; + const statement = root.statements.nodes[index]!; const expectedStatement = expectedRoot.statements[index]!; - const node = statement.declarationList!.declarations![0]!.name!; + const node = statement.declarationList!.declarations!.nodes[0]!.name!; const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0] .name as SdkNode; const type = project.checker.getTypeAtLocation(node)!; @@ -121,7 +121,7 @@ test("generic, predicate, rest and this-parameter signature fragments match the const root = project.program.getSourceFile(file)!; const expectedRoot = sdk.program.getSourceFile(file)!; for (let index = 7; index < 11; index++) { - const node = root.statements[index]!; + const node = root.statements.nodes[index]!; const expectedNode = expectedRoot.statements[index]!; const signature = project.checker.getSignatureFromDeclaration(node)!; const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!; @@ -138,7 +138,7 @@ test("generic, predicate, rest and this-parameter signature fragments match the expect(actual.kind).toBe(SyntaxKind.FunctionType); expect(shape(actual)).toEqual(shape(expected)); expect(actual.file.root).toBe(actual); - const parameters = actual.parameters!; + const parameters = actual.parameters!.nodes; expect(parameters.length).toBeGreaterThan(0); for (const parameter of parameters) { expect(parameter.parent).toBe(actual); diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts index 47c6d4a82..ceaceb113 100644 --- a/packages/compiler/src/frontend/ts7/session.test.ts +++ b/packages/compiler/src/frontend/ts7/session.test.ts @@ -288,7 +288,7 @@ test("a released latest snapshot cannot lend stale ASTs to the same project", () source = "export const third = true;"; const third = session.updateSnapshot({ fileChanges: { created: [file] } }); expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); - expect(old.statements[0]!.getText()).toBe("export const value = 1;"); + expect(old.statements.nodes[0]!.getText()).toBe("export const value = 1;"); } finally { session.close(); rmSync(dir, { recursive: true, force: true }); diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 12dae3c21..4933da9d6 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -52,7 +52,6 @@ export class Ts7Session { constructor( private readonly rpc: Ts7RpcClient, collectTiming = false, - readonly listMetadata?: (nodes: AstNode[], pos: number, end: number) => void, ) { this.timing = new Ts7Timing(collectTiming); } @@ -280,7 +279,7 @@ export class Ts7SessionProgram { if (source === undefined) { const bytes = this.session.binary("getSourceFile", JSON.stringify(this.query(file))); if (bytes.length !== 0) { - const ast = new AstFile(bytes, this.session.listMetadata, () => { + const ast = new AstFile(bytes, () => { this.session.timing.materialized(); }); this.session.timing.fetched(Math.max(0, ast.wire.nodeCount - 2)); diff --git a/packages/compiler/src/frontend/ts7/source-parser.test.ts b/packages/compiler/src/frontend/ts7/source-parser.test.ts index e416d2920..1fae05ff6 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.test.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.test.ts @@ -41,14 +41,14 @@ test("each parse owns its text and nodes across same-path edits, deletion and cl expect(second).not.toBe(first); const other = parser.parse("other.js", "exports.third = 3;", "js"); const empty = parser.parse("input.ts", "", "ts"); - expect(empty.statements).toEqual([]); + expect(empty.statements.nodes).toEqual([]); const restored = parser.parse("input.ts", first.text, "ts"); expect(restored.text).toBe(first.text); parser.close(); parser.close(); - expect(first.statements[0]!.getText()).toBe("export const first = 1;"); - expect(second.statements[0]!.parent).toBe(second); - expect(other.statements[0]!.getText()).toBe("exports.third = 3;"); + expect(first.statements.nodes[0]!.getText()).toBe("export const first = 1;"); + expect(second.statements.nodes[0]!.parent).toBe(second); + expect(other.statements.nodes[0]!.getText()).toBe("exports.third = 3;"); expect(() => parser.parse("late.ts", "", "ts")).toThrow("closed"); }); @@ -113,7 +113,7 @@ for (const { name, kind, source } of syntaxCases) { expect(file.text).toBe(source); expect(file.isDeclarationFile).toBe(oracle.isDeclarationFile); expect( - file.statements.map((statement) => ({ + file.statements.nodes.map((statement) => ({ start: statement.getStart(), end: statement.end, text: statement.getText(), @@ -152,14 +152,16 @@ test("optional declarations expose question tokens without confusing definite as "class A { optional?: number; definite!: number; method?(): void; } type M = { [K in keyof T]-?: T[K] };", "ts", ); - const declaration = file.statements[0]!; + const declaration = file.statements.nodes[0]!; if (!ts.isClassDeclaration(declaration)) throw new Error("expected class"); - expect(declaration.members[0]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); - expect(declaration.members[0]!.questionToken).toBe(declaration.members[0]!.postfixToken); - expect(declaration.members[1]!.questionToken).toBeUndefined(); - expect(declaration.members[1]!.postfixToken?.kind).toBe(ts.SyntaxKind.ExclamationToken); - expect(declaration.members[2]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); - const alias = file.statements[1]!; + expect(declaration.members.nodes[0]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); + expect(declaration.members.nodes[0]!.questionToken).toBe( + declaration.members.nodes[0]!.postfixToken, + ); + expect(declaration.members.nodes[1]!.questionToken).toBeUndefined(); + expect(declaration.members.nodes[1]!.postfixToken?.kind).toBe(ts.SyntaxKind.ExclamationToken); + expect(declaration.members.nodes[2]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); + const alias = file.statements.nodes[1]!; if (!ts.isTypeAliasDeclaration(alias) || !ts.isMappedTypeNode(alias.type)) throw new Error("expected mapped type"); expect(alias.type.questionToken?.kind).toBe(ts.SyntaxKind.MinusToken); @@ -192,16 +194,16 @@ test("JSDoc callable types cannot replace method body views", () => { true, ts5.ScriptKind.JS, ); - const native = file.statements[0]!; + const native = file.statements.nodes[0]!; const expected = oracle.statements[0]!; if (!ts.isClassDeclaration(native) || !ts5.isClassDeclaration(expected)) throw new Error("expected class"); - expect(native.members.map((member) => member.body?.getText())).toEqual( + expect(native.members.nodes.map((member) => member.body?.getText())).toEqual( expected.members.map((member) => ts5.isMethodDeclaration(member) ? member.body?.getText() : undefined, ), ); - for (const member of native.members) { + for (const member of native.members.nodes) { expect(member.body?.kind).toBe(ts.SyntaxKind.Block); expect(member.body?.parent).toBe(member); } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index b7fa17b7b..123a5d1a0 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2038,7 +2038,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // A GENERIC class's instance type (`Box`) maps to the concrete // INSTANTIATION's class (`Box%0`), registered on demand — the Lowerer // hook owns the instance table (monomorphization by flow). - if (classDecl.typeParameters) { + if (classDecl.typeParameters?.nodes) { const instance = ctx.genericClassInstance?.(classDecl, widened, (argument) => mapType(argument, ctx)) ?? null; @@ -2093,7 +2093,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the instantiation's classval. Uninstantiated `typeof Box` keeps the // type parameter in the construct signature's return — unmapped (the // family has no thunk and no single constructor ABI). - if (classDecl.typeParameters) { + if (classDecl.typeParameters?.nodes) { const ctorSigs = checker.getConstructSignatures(widened); if (ctorSigs.length !== 1) return null; const inst = mapType(checker.getReturnTypeOfSignature(ctorSigs[0]!), ctx); @@ -4299,7 +4299,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null { const sigDecl = checker.signatureDeclaration(sig); const declParams = - sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined; + sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if ( declParams !== undefined && runtimeParameterCount(declParams) !== sig.getParameters().length @@ -4349,7 +4349,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null (ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) && construction && ts.isNewExpression(construction) && - construction.arguments?.[0] === executor && + construction.arguments?.nodes[0] === executor && ts.isIdentifier(construction.expression) && construction.expression.text === "Promise" && constructorSymbol && @@ -4869,9 +4869,11 @@ function constAssertedEmptyArrayProp(p: ts.Symbol, ctx: TypeMapperCtx): boolean const init = d.initializer; // `a: [] as const` pins the field type by itself, wherever the literal sits. if (ts.isAsExpression(init) && isConstAssertionTypeNode(init.type)) { - return ts.isArrayLiteralExpression(init.expression) && init.expression.elements.length === 0; + return ( + ts.isArrayLiteralExpression(init.expression) && init.expression.elements.nodes.length === 0 + ); } - if (!ts.isArrayLiteralExpression(init) || init.elements.length > 0) return false; + if (!ts.isArrayLiteralExpression(init) || init.elements.nodes.length > 0) return false; return underConstAssertion(d); } @@ -5041,10 +5043,13 @@ function evalMappedTemplate( .declarationsOf(alias) .find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) : undefined; - if (aliasDecl?.typeParameters && aliasArgs.length === aliasDecl.typeParameters.length) { + if ( + aliasDecl?.typeParameters?.nodes && + aliasArgs.length === aliasDecl.typeParameters.nodes.length + ) { scope = new Map(aliases); for (let i = 0; i < aliasArgs.length; i++) { - const param = checker.getSymbolAtLocation(aliasDecl.typeParameters[i]!.name); + const param = checker.getSymbolAtLocation(aliasDecl.typeParameters.nodes[i]!.name); const arg = evalMappedTemplate(aliasArgs[i]!, keys, aliases, ctx); if (!param || !arg) return null; scope.set(param, arg); @@ -5540,7 +5545,7 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null else if ( (ts.isClassDeclaration(owner) || ts.isInterfaceDeclaration(owner)) && owner.name !== undefined && - (owner.typeParameters?.length ?? 0) === 0 + (owner.typeParameters?.nodes.length ?? 0) === 0 ) { const ownerSymbol = checker.getSymbolAtLocation(owner.name); if (ownerSymbol) source = checker.getDeclaredTypeOfSymbol(ownerSymbol); @@ -6211,7 +6216,7 @@ function mapRecordTypeInner( } let writeT: IrType | null = null; if (setDecl) { - const param = (setDecl as ts.SetAccessorDeclaration).parameters[0]; + const param = (setDecl as ts.SetAccessorDeclaration).parameters.nodes[0]; if (!param) return null; writeT = mapType(checker.getTypeAtLocation(param), ctx); // A void/unit-typed write slot has no value form; DIVERGENT @@ -6568,7 +6573,7 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): if ( sigDecl !== undefined && ts.isFunctionLike(sigDecl) && - (runtimeParameterCount(sigDecl.parameters) !== sig.getParameters().length || + (runtimeParameterCount(sigDecl.parameters.nodes) !== sig.getParameters().length || bodyReadsArguments(sigDecl)) ) { return `the function shape is supported, but its signature is variadic ('arguments'-reading), and a compiled signature is fixed-arity`; diff --git a/packages/compiler/src/frontend/type-only-imports.ts b/packages/compiler/src/frontend/type-only-imports.ts index 003d4ccf7..f9eb67a39 100644 --- a/packages/compiler/src/frontend/type-only-imports.ts +++ b/packages/compiler/src/frontend/type-only-imports.ts @@ -30,8 +30,8 @@ export function typeOnlyNamedImport(stmt: ts.ImportDeclaration): boolean { clause.name === undefined && clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.length > 0 && - clause.namedBindings.elements.every((e) => e.isTypeOnly) + clause.namedBindings.elements.nodes.length > 0 && + clause.namedBindings.elements.nodes.every((e) => e.isTypeOnly) ); } @@ -45,7 +45,7 @@ interface ModuleReference { function declarationReferencesOf(sf: ts.SourceFile): ModuleReference[] { const refs: ModuleReference[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt)) { if (ts.isStringLiteral(stmt.moduleSpecifier)) refs.push({ spec: stmt.moduleSpecifier.text, erased: erasedTypeOnlyImport(stmt) }); @@ -73,7 +73,7 @@ function declarationReferencesOf(sf: ts.SourceFile): ModuleReference[] { function callReferencesOf(program: ts.Program, sf: ts.SourceFile): ModuleReference[] { const refs: ModuleReference[] = []; for (const node of moduleSourceCandidates(program, sf).calls) { - const arg = node.arguments[0]; + const arg = node.arguments.nodes[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) continue; refs.push({ spec: arg.text, diff --git a/packages/compiler/src/frontend/worker-target.ts b/packages/compiler/src/frontend/worker-target.ts index 38c3d80a1..c395da593 100644 --- a/packages/compiler/src/frontend/worker-target.ts +++ b/packages/compiler/src/frontend/worker-target.ts @@ -17,10 +17,10 @@ function workerNamespace(program: ts.Program, expression: ts.Expression): boolea ts.isIdentifier(expression.expression) && expression.expression.text === "require" && isNodeGlobal(program, expression.expression) && - expression.arguments.length === 1 && - ts.isStringLiteralLike(expression.arguments[0]!) + expression.arguments.nodes.length === 1 && + ts.isStringLiteralLike(expression.arguments.nodes[0]!) ) - return canonicalBuiltinModule(expression.arguments[0]!.text) === "worker_threads"; + return canonicalBuiltinModule(expression.arguments.nodes[0]!.text) === "worker_threads"; if (!ts.isIdentifier(expression)) return false; if (isBuiltinNamespaceImport(program, expression, "worker_threads")) return true; const checker = program.getTypeChecker(), @@ -122,8 +122,8 @@ export function workerTargetPaths(program: ts.Program, files: readonly ts.Source ); for (const construction of constructions) { const expression = construction.expression; - if (!isWorkerConstructor(program, expression) || !construction.arguments?.[0]) continue; - const path = workerModulePath(program, construction.arguments[0]); + if (!isWorkerConstructor(program, expression) || !construction.arguments?.nodes[0]) continue; + const path = workerModulePath(program, construction.arguments.nodes[0]); if (path !== null) targets.add(workerSourcePath(path)); } } diff --git a/packages/compiler/src/ir/traverse.test.ts b/packages/compiler/src/ir/traverse.test.ts index e72176ef6..874e7846e 100644 --- a/packages/compiler/src/ir/traverse.test.ts +++ b/packages/compiler/src/ir/traverse.test.ts @@ -30,23 +30,23 @@ afterAll(() => program.dispose()); function childType(type: ts.TypeNode): boolean { if (ts.isTypeReferenceNode(type)) return ["IrExpr", "IrStmt"].includes(type.typeName.getText()); if (ts.isArrayTypeNode(type)) return childType(type.elementType); - if (ts.isUnionTypeNode(type)) return type.types.some(childType); + if (ts.isUnionTypeNode(type)) return type.types.nodes.some(childType); if (ts.isTypeLiteralNode(type)) - return type.members.some( + return type.members.nodes.some( (member) => ts.isPropertySignature(member) && member.type && childType(member.type), ); return false; } for (const name of ["IrExpr", "IrStmt"]) { - const alias = source.statements.find( + const alias = source.statements.nodes.find( (node): node is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(node) && node.name.text === name, )!; if (!ts.isUnionTypeNode(alias.type)) throw new Error("IR must be a discriminated union"); - for (const variant of alias.type.types) { + for (const variant of alias.type.types.nodes) { if (!ts.isTypeLiteralNode(variant)) throw new Error("IR variants must be records"); - const members = variant.members.filter(ts.isPropertySignature); + const members = variant.members.nodes.filter(ts.isPropertySignature); const tag = members.find((member) => member.name.getText() === "kind")!.type!; const kind = JSON.parse(tag.getText()) as string; test(`${name}.${kind}: every declared child is visited and rewritten, metadata is preserved`, () => { @@ -61,7 +61,8 @@ for (const name of ["IrExpr", "IrStmt"]) { expected.push(node); return node; } - if (ts.isUnionTypeNode(type)) return presence ? value(type.types.find(childType)!) : null; + if (ts.isUnionTypeNode(type)) + return presence ? value(type.types.nodes.find(childType)!) : null; if (ts.isArrayTypeNode(type)) return [value(type.elementType), value(type.elementType)]; if (ts.isTypeLiteralNode(type)) { const result: Record = { @@ -69,7 +70,7 @@ for (const name of ["IrExpr", "IrStmt"]) { drop: true, overflow: true, }; - for (const member of type.members) { + for (const member of type.members.nodes) { if (ts.isPropertySignature(member) && member.type && childType(member.type)) result[member.name.getText()] = value(member.type); } @@ -231,7 +232,7 @@ test("module traversal covers every declared type slot", () => { }; if (name === "SrcLoc") return loc; if (active.has(name)) throw new Error(`unexpected recursive schema reference: ${name}`); - const declaration = source.statements.find( + const declaration = source.statements.nodes.find( (item) => (ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)) && item.name.text === name, @@ -239,21 +240,21 @@ test("module traversal covers every declared type slot", () => { if (!declaration) return undefined; active.add(name); const result = ts.isInterfaceDeclaration(declaration) - ? fields(declaration.members) + ? fields(declaration.members.nodes) : ts.isTypeAliasDeclaration(declaration) ? slots(declaration.type) : undefined; active.delete(name); return result; } - if (ts.isTypeLiteralNode(node)) return fields(node.members); + if (ts.isTypeLiteralNode(node)) return fields(node.members.nodes); if (ts.isArrayTypeNode(node)) return [slots(node.elementType)]; if (ts.isParenthesizedTypeNode(node)) return slots(node.type); - if (ts.isIntersectionTypeNode(node)) return slots(node.types[0]!); + if (ts.isIntersectionTypeNode(node)) return slots(node.types.nodes[0]!); if (ts.isUnionTypeNode(node)) { // Metadata unions can choose any arm: the schema puts executable and // typed alternatives first, followed by null/unit alternatives. - return slots(node.types[0]!); + return slots(node.types.nodes[0]!); } return undefined; } @@ -271,11 +272,14 @@ test("module traversal covers every declared type slot", () => { } return result; } - const declaration = source.statements.find( + const declaration = source.statements.nodes.find( (item): item is ts.InterfaceDeclaration => ts.isInterfaceDeclaration(item) && item.name.text === "IrModule", )!; - const module = { ...fields(declaration.members), sourceFile: loc.file } as unknown as IrModule; + const module = { + ...fields(declaration.members.nodes), + sourceFile: loc.file, + } as unknown as IrModule; const seen: IrType[] = []; expect( everyModuleNode(module, { @@ -303,14 +307,14 @@ test("module traversal covers every declared type slot", () => { }); test("type traversal visits all structural children and leaves named shapes as references", () => { - const alias = source.statements.find( + const alias = source.statements.nodes.find( (node): node is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(node) && node.name.text === "IrType", )!; if (!ts.isUnionTypeNode(alias.type)) throw new Error("IrType must be a union"); - for (const variant of alias.type.types) { + for (const variant of alias.type.types.nodes) { if (!ts.isTypeLiteralNode(variant)) throw new Error("IrType variants must be records"); - const members = variant.members.filter(ts.isPropertySignature); + const members = variant.members.nodes.filter(ts.isPropertySignature); const kind = JSON.parse( members.find((member) => member.name.getText() === "kind")!.type!.getText(), ) as string; diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts index 610a389f1..95fc28647 100644 --- a/packages/compiler/src/wasm/discover.ts +++ b/packages/compiler/src/wasm/discover.ts @@ -182,13 +182,13 @@ class Discovery { const dir = dirname(sf.fileName); if (ts.isCallExpression(node)) { const callee = node.expression; - const first = node.arguments[0]; + const first = node.arguments.nodes[0]; if (first === undefined) return null; // require.resolve / createRequire(...).resolve / import.meta.resolve if (ts.isPropertyAccessExpression(callee) && callee.name.text === "resolve") { const specifier = wasmLiteral(first); if (specifier === null) { - return this.joinedPath(dir, node.arguments); + return this.joinedPath(dir, node.arguments.nodes); } const receiver = callee.expression; if (ts.isMetaProperty(receiver) || isRequireLike(receiver)) { @@ -206,14 +206,14 @@ class Discovery { (callee.name.text === "join" || callee.name.text === "resolve")) || (ts.isIdentifier(callee) && (callee.text === "join" || callee.text === "resolve")) ) { - return this.joinedPath(dir, node.arguments); + return this.joinedPath(dir, node.arguments.nodes); } return null; } if (ts.isNewExpression(node)) { // new URL("./x.wasm", import.meta.url) if (!ts.isIdentifier(node.expression) || node.expression.text !== "URL") return null; - const args = node.arguments; + const args = node.arguments?.nodes; if (args === undefined || args.length !== 2) return null; const specifier = wasmLiteral(args[0]!); const base = args[1]!; @@ -339,7 +339,7 @@ function isRequireLike(node: ts.Expression): boolean { /** The bytes of an array literal of integer literals, or null. */ function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null { - const elements = node.elements; + const elements = node.elements.nodes; const out = new Uint8Array(elements.length); for (let i = 0; i < elements.length; i++) { const element = elements[i]!; diff --git a/packages/compiler/test/ts7/facade.test.ts b/packages/compiler/test/ts7/facade.test.ts index 4b092a858..94bab8b31 100644 --- a/packages/compiler/test/ts7/facade.test.ts +++ b/packages/compiler/test/ts7/facade.test.ts @@ -381,15 +381,15 @@ void top; const { proxy, counts, calls } = countingChecker(w.p7.project.checker); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const functions = sf.statements.filter(ad.isFunctionDeclaration); - const cls = sf.statements.find(ad.isClassDeclaration)!; + const functions = sf.statements.nodes.filter(ad.isFunctionDeclaration); + const cls = sf.statements.nodes.find(ad.isClassDeclaration)!; const withClass = functions[2]!; - const nested = withClass.body!.statements.find(ad.isClassDeclaration)!; + const nested = withClass.body!.statements.nodes.find(ad.isClassDeclaration)!; const reachedBody = functions[0]!.body!; const deadBody = functions[1]!.body!; - const nestedMethodBody = nested.members.find(ad.isMethodDeclaration)!.body!; - const defaultValue = functions[0]!.parameters[0]!.initializer!; - const fieldValue = cls.members.find(ad.isPropertyDeclaration)!.initializer!; + const nestedMethodBody = nested.members.nodes.find(ad.isMethodDeclaration)!.body!; + const defaultValue = functions[0]!.parameters.nodes[0]!.initializer!; + const fieldValue = cls.members.nodes.find(ad.isPropertyDeclaration)!.initializer!; const inside = (node: Node, root: Node): boolean => node.getStart() >= root.getStart() && node.end <= root.end; @@ -445,7 +445,7 @@ const item: Alias = identity({ value: 1 }); const { proxy, counts, calls } = countingChecker(raw); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const fn = sf.statements.find(ad.isFunctionDeclaration)!; + const fn = sf.statements.nodes.find(ad.isFunctionDeclaration)!; const references: Node[] = []; ad.walkPreorder(sf, (node) => { if (ad.isTypeReferenceNode(node)) references.push(node.typeName); @@ -458,8 +458,8 @@ const item: Alias = identity({ value: 1 }); calls[method]!.flatMap(([nodes]) => (Array.isArray(nodes) ? nodes : [nodes])) as Node[]; expect(queried("getTypeAtLocation").some((node) => references.includes(node))).toBe(false); expect(queried("getSymbolAtLocation").some((node) => references.includes(node))).toBe(false); - const child = sf.statements.filter(ad.isClassDeclaration)[1]!; - const base = child.heritageClauses![0]!.types[0]!.expression; + const child = sf.statements.nodes.filter(ad.isClassDeclaration)[1]!; + const base = child.heritageClauses!.nodes[0]!.types.nodes[0]!.expression; expect(queried("getTypeAtLocation")).toContain(base); const reference = references[0]!; @@ -485,8 +485,8 @@ test("declaration misses stay direct while explicit whole-file prefetch remains const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; expect(sf.isDeclarationFile).toBe(true); - const declaration = sf.statements.find(ad.isInterfaceDeclaration)!; - const name = declaration.members.find(ad.isPropertySignature)!.name; + const declaration = sf.statements.nodes.find(ad.isInterfaceDeclaration)!; + const name = declaration.members.nodes.find(ad.isPropertySignature)!.name; expect(facade.getSymbolAtLocation(name)).toBe(raw.getSymbolAtLocation(name)); expect(facade.getTypeAtLocation(name)).toBe(raw.getTypeAtLocation(name)); expect(calls["getSymbolAtLocation"]).toEqual([[name]]); @@ -517,7 +517,7 @@ export function dead(input: number): number { const { proxy, counts, calls } = countingChecker(w.p7.project.checker); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const body = sf.statements.find(ad.isFunctionDeclaration)!.body!; + const body = sf.statements.nodes.find(ad.isFunctionDeclaration)!.body!; const identifiers: Node[] = []; ad.walkPreorder(body, (node) => { if (ad.isIdentifier(node)) identifiers.push(node); @@ -551,7 +551,7 @@ ${locals} const { proxy, counts, calls } = countingChecker(w.p7.project.checker); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const body = sf.statements.find(ad.isFunctionDeclaration)!.body!; + const body = sf.statements.nodes.find(ad.isFunctionDeclaration)!.body!; const identifiers: Node[] = []; ad.walkPreorder(body, (node) => { if (ad.isIdentifier(node)) identifiers.push(node); @@ -600,8 +600,8 @@ console.log("ok"); const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const cls = sf.statements.find(ad.isClassDeclaration)!; - const ctor = cls.members.find(ad.isConstructorDeclaration)!; + const cls = sf.statements.nodes.find(ad.isClassDeclaration)!; + const ctor = cls.members.nodes.find(ad.isConstructorDeclaration)!; const insideCtor = (node: Node): boolean => node.getStart() >= ctor.getStart() && node.end <= ctor.end; @@ -630,9 +630,9 @@ test("signature collection batches exact types of deferred function defaults", ( const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const initializers = sf.statements + const initializers = sf.statements.nodes .filter(ad.isFunctionDeclaration) - .map((decl) => decl.parameters[0]!.initializer!); + .map((decl) => decl.parameters.nodes[0]!.initializer!); lowerToIr(w.p7, sf, [sf]); @@ -665,10 +665,10 @@ test("class-shape collection batches deferred method default types", () => { const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const cls = sf.statements.find(ad.isClassDeclaration)!; - const initializers = cls.members + const cls = sf.statements.nodes.find(ad.isClassDeclaration)!; + const initializers = cls.members.nodes .filter(ad.isMethodDeclaration) - .map((member) => member.parameters[0]!.initializer!); + .map((member) => member.parameters.nodes[0]!.initializer!); lowerToIr(w.p7, sf, [sf]); @@ -703,7 +703,7 @@ console.log(pick(42)); const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const fn = sf.statements.find(ad.isFunctionDeclaration)!; + const fn = sf.statements.nodes.find(ad.isFunctionDeclaration)!; const insideBody = (node: Node): boolean => node.getStart() >= fn.body!.getStart() && node.end <= fn.body!.end; @@ -747,7 +747,7 @@ console.log(reached(1)); // lowering and this assertion observe the same memo/batch traffic. (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const deadBody = sf.statements + const deadBody = sf.statements.nodes .filter(ad.isFunctionDeclaration) .find((decl) => decl.name?.text === "dead")!.body!; const insideDeadBody = (node: Node): boolean => @@ -799,7 +799,7 @@ export function f(input: number): number { ); worlds.push(w); const sf = w.p7.getSourceFile(w.files[0]!)!; - const body = sf.statements.find(ad.isFunctionDeclaration)!.body!; + const body = sf.statements.nodes.find(ad.isFunctionDeclaration)!.body!; const identifiers: Node[] = []; ad.walkPreorder(body, (node) => { if (ad.isIdentifier(node)) identifiers.push(node); diff --git a/packages/compiler/test/ts7/program-adapter.test.ts b/packages/compiler/test/ts7/program-adapter.test.ts index e7479c749..f76de2f8c 100644 --- a/packages/compiler/test/ts7/program-adapter.test.ts +++ b/packages/compiler/test/ts7/program-adapter.test.ts @@ -69,7 +69,7 @@ test("retains source-file BOM stripping", () => { const load = loadProgram(entry); try { - expect(load.entry.statements[0]?.getStart(load.entry)).toBe(0); + expect(load.entry.statements.nodes[0]?.getStart(load.entry)).toBe(0); expect(load.entry.text).toBe("const value = 1;\n"); } finally { load.dispose(); diff --git a/packages/compiler/test/ts7/rpc-api.test.ts b/packages/compiler/test/ts7/rpc-api.test.ts index e363567c2..d323bd901 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -88,23 +88,27 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () => expect(symbol!.declarations[0]!.resolve()).toBe(symbol!.declarations[0]!.resolve()); const declaration = symbol!.declarations[0]!.resolve()!; expect(declaration).toBeInstanceOf(AstNode); - const variable = source - .statements[0]! as import("../../src/frontend/ts7/ast-types.js").VariableStatement; - expect(variable.declarationList.declarations[0]).toBe(declaration); + const variable = source.statements + .nodes[0]! as import("../../src/frontend/ts7/ast-types.js").VariableStatement; + expect(variable.declarationList.declarations.nodes[0]).toBe(declaration); expect( - project.checker.getSymbolAtLocation(variable.declarationList.declarations[0]!.name), + project.checker.getSymbolAtLocation(variable.declarationList.declarations.nodes[0]!.name), ).toBe(symbol); - expect(project.checker.getTypeAtLocation(variable.declarationList.declarations[0]!.name)).toBe( - first, - ); + expect( + project.checker.getTypeAtLocation(variable.declarationList.declarations.nodes[0]!.name), + ).toBe(first); const expectedSource = expected.getProject(config)!.program.getSourceFile(path)!; - expect(source.statements.map((node) => [node.kind, node.pos, node.end])).toEqual( + expect(source.statements.nodes.map((node) => [node.kind, node.pos, node.end])).toEqual( expectedSource.statements.map((node) => [node.kind, node.pos, node.end]), ); - // Native arrays retain element identity; SDK-only properties no longer - // need a JavaScript Object.assign adapter on production source lists. - expect(Array.isArray(source.statements)).toBe(true); - expect(Object.hasOwn(source.statements, "pos")).toBe(false); + // A list carries its own position, as tsc-ts's NodeList does, and its + // nodes are a plain array that retains element identity. + expect([source.statements.pos, source.statements.end]).toEqual([ + expectedSource.statements.pos, + expectedSource.statements.end, + ]); + expect(Array.isArray(source.statements.nodes)).toBe(true); + expect(Object.hasOwn(source.statements.nodes, "pos")).toBe(false); expect(Object.hasOwn(source.statements, "transformFlags")).toBe(false); expect(source.referencedFiles).toBe(source.referencedFiles); expect(source.imports).toBe(source.imports); @@ -180,6 +184,10 @@ test("native semantic registries preserve project scope and snapshot lifetimes", writeFileSync(path, source); const api = new Ts7Api({ cwd: dir, fs: fallback }); const sdk = new API({ cwd: dir }); + // scriptc's node lists keep their nodes in `nodes`; the SDK's lists are arrays. + type SdkNode = import("typescript/unstable/ast").Node; + const nodesOf = (list: unknown): readonly SdkNode[] => + Array.isArray(list) ? list : (list as { nodes: readonly SdkNode[] }).nodes; function facts(snapshot: Snapshot) { const a = snapshot.getProject(firstConfig)!; const b = snapshot.getProject(secondConfig)!; @@ -190,8 +198,8 @@ test("native semantic registries preserve project scope and snapshot lifetimes", const typeB = b.checker.getTypeAtPosition(path, answer)!; const fileA = a.program.getSourceFile(path)!; const fileB = b.program.getSourceFile(path)!; - const signatureA = a.checker.getSignatureFromDeclaration(fileA.statements[0]!)!; - const signatureB = b.checker.getSignatureFromDeclaration(fileB.statements[0]!)!; + const signatureA = a.checker.getSignatureFromDeclaration(nodesOf(fileA.statements)[0]!)!; + const signatureB = b.checker.getSignatureFromDeclaration(nodesOf(fileB.statements)[0]!)!; return { shape: { sharedSymbol: symbolA === symbolB, @@ -200,8 +208,10 @@ test("native semantic registries preserve project scope and snapshot lifetimes", sameDeclaration: symbolA.declarations[0]!.resolve() === symbolB.declarations[0]!.resolve(), explicitDeclaration: symbolB.declarations[0]!.resolve(b) === - (fileB.statements[1] as import("typescript/unstable/ast").VariableStatement) - .declarationList.declarations[0], + nodesOf( + (nodesOf(fileB.statements)[1] as import("typescript/unstable/ast").VariableStatement) + .declarationList.declarations, + )[0], texts: [a.checker.typeToString(typeA), b.checker.typeToString(typeB)], parameters: signatureA.getParameters().map((symbol) => symbol.name), }, diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index dfb811450..45a01c213 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -277,12 +277,12 @@ const getters = names const match = declarations.match(new RegExp(`get ${name}\\(\\): ([^;]+);`)); if (!match) throw new Error(`Missing AST child declaration: ${name}`); const type = match[1] - .replaceAll("RemoteNodeList", "readonly AstNode[]") + .replaceAll("RemoteNodeList", "AstNodeList") .replaceAll("RemoteNode", "AstNode"); const method = - type.includes("AstNode |") && type.includes("AstNode[]") + type.includes("AstNode |") && type.includes("AstNodeList") ? "child" - : type.includes("AstNode[]") + : type.includes("AstNodeList") ? "childList" : "childNode"; return ` get ${name}(): ${type} {\n return this.${method}(${JSON.stringify(name)});\n }`; diff --git a/scripts/generate-ts7-ast-views.mjs b/scripts/generate-ts7-ast-views.mjs index 3b5d3d66f..1525f6246 100644 --- a/scripts/generate-ts7-ast-views.mjs +++ b/scripts/generate-ts7-ast-views.mjs @@ -63,6 +63,8 @@ export function generateAstViews(root, packageRoot, version, check) { // The pinned native wire and RemoteNode getter encode one tag, despite // the SDK factory-oriented interface declaring an array of tags. if (key === "jsdocPropertyTags") type = "JSDocPropertyTag"; + // A list's children are a node list, as every other list field. + if (name === "SyntaxList" && key === "children") type = "NodeArray"; if (member.questionToken) type += " | undefined"; fields.set(key, ` readonly ${key}: ${type};`); } diff --git a/tests/fixtures/self-hosting/frontend-services-cases.ts b/tests/fixtures/self-hosting/frontend-services-cases.ts index 00bdcd230..0a28c740c 100644 --- a/tests/fixtures/self-hosting/frontend-services-cases.ts +++ b/tests/fixtures/self-hosting/frontend-services-cases.ts @@ -41,10 +41,10 @@ export function runFrontendServices(services: FrontendServices, input: string, o const before = host.createProgram([sourcePath], { noLib: true, types: [] }); try { const source = before.getSourceFile(sourcePath)!; - const statement = source.statements[0]!; + const statement = source.statements.nodes[0]!; check(isVariableStatement(statement), "native statement view"); if (isVariableStatement(statement)) { - const declaration = statement.declarationList.declarations[0]!; + const declaration = statement.declarationList.declarations.nodes[0]!; const name = declaration.name; check(isIdentifier(name), "native binding view"); if (isIdentifier(name)) { diff --git a/tests/fixtures/self-hosting/ts7-ast-views.ts b/tests/fixtures/self-hosting/ts7-ast-views.ts index 6f19cda1d..10f48485d 100644 --- a/tests/fixtures/self-hosting/ts7-ast-views.ts +++ b/tests/fixtures/self-hosting/ts7-ast-views.ts @@ -8,8 +8,8 @@ import { SyntaxKind } from "../../../packages/compiler/src/frontend/ts7/enums.js function check(value: boolean, message: string): void { if (!value) throw new Error(message); } function checkStatements(nodes: NodeArray, source: SourceFile): void { - check(nodes === source.statements, "typed statement array identity"); - for (const statement of nodes) { + check(nodes.nodes === source.statements.nodes, "typed statement list nodes identity"); + for (const statement of nodes.nodes) { check(statement.parent === source && statement.getSourceFile() === source, "typed source identity"); check(ts.isStatement(statement), "statement kind predicate"); } @@ -38,14 +38,14 @@ export function checkAstViews(file: AstFile): void { // one AstNode; reads must use its original lazy getters and caches. const body = (node as { body?: Node }).body; const name = (node as { name?: Node })["name"]; - const flags = (node as { modifiers?: readonly Node[] }).modifiers; + const flags = (node as { modifiers?: NodeArray }).modifiers; check(body === node.body && name === node.name, "structural child getter identity"); - check(flags === node.modifiers, "structural list getter identity"); + check(flags?.nodes === node.modifiers?.nodes, "structural list getter nodes identity"); if (body !== undefined) { bodies++; check(body.parent === node, "structural body parent"); } if (name !== undefined) { names++; check(name.parent === node, "structural name parent"); } if (flags !== undefined) { - modifiers += flags.length; - for (const modifier of flags) check(modifier.parent === node, "structural modifier parent"); + modifiers += flags.nodes.length; + for (const modifier of flags.nodes) check(modifier.parent === node, "structural modifier parent"); } if (ts.isIdentifier(node)) { identifiers += node.text.length; @@ -54,16 +54,16 @@ export function checkAstViews(file: AstFile): void { } if (ts.isStringLiteral(node)) strings += node.text.toUpperCase().length; if (ts.isVariableDeclarationList(node)) { - declarations += node.declarations.length; - for (const declaration of node.declarations) check(declaration.parent === node, "declaration identity"); + declarations += node.declarations.nodes.length; + for (const declaration of node.declarations.nodes) check(declaration.parent === node, "declaration identity"); } if (ts.isClassDeclaration(node)) { - for (const member of node.members) { - if (ts.isMethodDeclaration(member)) methods += member.parameters.length; + for (const member of node.members.nodes) { + if (ts.isMethodDeclaration(member)) methods += member.parameters.nodes.length; } } if (ts.isCallExpression(node)) { - calls += node.arguments.length; + calls += node.arguments.nodes.length; check(node.expression.parent === node, "call callee identity"); } if (ts.isBinaryExpression(node)) { diff --git a/tests/fixtures/self-hosting/ts7-checker-cases.ts b/tests/fixtures/self-hosting/ts7-checker-cases.ts index 6a2627e3c..52cb6981e 100644 --- a/tests/fixtures/self-hosting/ts7-checker-cases.ts +++ b/tests/fixtures/self-hosting/ts7-checker-cases.ts @@ -21,9 +21,9 @@ function check(value: boolean, message: string): void { } function declaration(root: SourceFile, name: string): AstNode { - for (const statement of root.statements) { + for (const statement of root.statements.nodes) { if (statement.name?.text === name) return statement; - for (const variable of statement.declarationList?.declarations ?? []) { + for (const variable of statement.declarationList?.declarations?.nodes ?? []) { if (variable.name?.text === name) return variable; } } @@ -87,12 +87,12 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.signatureDeclaration(resolved) === identity, "instantiated signature declaration"); check(facade.typeToString(facade.getReturnTypeOfSignature(resolved)).includes("n: number"), "instantiated return type"); - const ref = declaration(root, "ref").initializer!.properties![0]!; + const ref = declaration(root, "ref").initializer!.properties?.nodes[0]!; check(facade.getShorthandAssignmentValueSymbol(ref) === answerSymbol, "shorthand value symbol"); - const renamed = root.statements.find((statement) => statement.kind === SyntaxKind.ExportDeclaration)!.exportClause!.elements![0]!; + const renamed = root.statements.nodes.find((statement) => statement.kind === SyntaxKind.ExportDeclaration)!.exportClause!.elements?.nodes[0]!; const alias = facade.getSymbolAtLocation(renamed.name!)!; check(facade.getAliasedSymbol(alias) === answerSymbol, "export alias"); - const member = declaration(root, "Mode").members![0]!; + const member = declaration(root, "Mode").members?.nodes[0]!; check(facade.getConstantValue(member) === 3, "enum constant"); check(facade.getConstantValue(answer) === undefined, "absent constant"); const contextual = declaration(root, "contextual").initializer!; @@ -173,7 +173,7 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque batch.prefetchSymbolRoots([identity, identity]); batch.prefetchSymbolNodesExact([answerName, answerName]); batch.prefetchCollectionTypes([call, call]); - batch.prefetchClassCollection([box.members![0]!.initializer!], [box]); + batch.prefetchClassCollection([box.members?.nodes[0]!.initializer!], [box]); batch.prefetchRoots([identity.body!, identity.body!]); batch.prefetchSourceFile(root); check(batch.getTypeAtLocation(answerName) === answerType && batch.getSymbolAtLocation(answerName) === answerSymbol, "prefetched semantic identity"); diff --git a/tests/fixtures/self-hosting/ts7-checker-snapshots.ts b/tests/fixtures/self-hosting/ts7-checker-snapshots.ts index db57b95f2..949eff4b7 100644 --- a/tests/fixtures/self-hosting/ts7-checker-snapshots.ts +++ b/tests/fixtures/self-hosting/ts7-checker-snapshots.ts @@ -14,7 +14,7 @@ function protocolPath(path: string): string { } function exportedName(source: SourceFile) { - return source.statements[1]!.declarationList!.declarations![0]!.name!; + return source.statements.nodes[1]!.declarationList!.declarations?.nodes[0]!.name!; } /** Reuse one server, one source version and both old/new semantic graphs. diff --git a/tests/fixtures/self-hosting/ts7-client-cases.ts b/tests/fixtures/self-hosting/ts7-client-cases.ts index 2aba713c3..c98869785 100644 --- a/tests/fixtures/self-hosting/ts7-client-cases.ts +++ b/tests/fixtures/self-hosting/ts7-client-cases.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { Ts7RpcClient } from "../../../packages/compiler/src/frontend/ts7/rpc-client.js"; import { registerTs7FileSystem } from "../../../packages/compiler/src/frontend/ts7/rpc-filesystem.js"; import { Ts7Wire } from "../../../packages/compiler/src/frontend/ts7/rpc-wire.js"; -import { AstFile, AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js"; +import { AstFile, AstNode, AstNodeList } from "../../../packages/compiler/src/frontend/ts7/ast-node.js"; import { AstKind, KIND_NODE_LIST, astChildNames } from "../../../packages/compiler/src/frontend/ts7/ast-schema.generated.js"; import { decodeAstString } from "../../../packages/compiler/src/frontend/ts7/ast-bytes.js"; import { SyntaxKind, ScriptTarget, ModuleKind, ModuleResolutionKind, ModuleDetectionKind, syntaxKindName, scriptTargetName, moduleKindName, moduleResolutionKindName, moduleDetectionKindName } from "../../../packages/compiler/src/frontend/ts7/enums.js"; @@ -56,7 +56,7 @@ function checkSemanticSurrogateBoundary(): string { function checkAst(ast: AstFile, source: string): void { check(ast.root.text === source, "decoded source text"); check(ast.root.getSourceFile() === ast.root, "source identity"); - check(ast.root.statements === ast.root.statements, "stable child list"); + check(ast.root.statements?.nodes === ast.root.statements?.nodes, "stable child list nodes"); for (let index = 1; index < ast.wire.nodeCount; index++) { if (ast.wire.kind(index) === KIND_NODE_LIST) { ast.list(index); continue; } const node = ast.node(index); @@ -70,15 +70,15 @@ function checkAst(ast: AstFile, source: string): void { check(node.getFullStart() === node.pos && node.getEnd() === node.end, "span endpoints"); for (const name of astChildNames(node.kind).split(",")) { const child = node.child(name!); - if (Array.isArray(child)) { - for (const item of child) check(item.parent === node, "list child parent"); + if (child instanceof AstNodeList) { + for (const item of child.nodes) check(item.parent === node, "list child parent"); } else if (child !== undefined) check(child.parent === node, "named child parent"); } const seen: AstNode[] = []; node.forEachChild((child) => { seen.push(child); }); check(node.forEachChild((child) => child.index) === seen[0]?.index, "visitor early return"); let listElements = 0; - node.forEachChild(() => {}, (list) => { listElements += list.length; }); + node.forEachChild(() => {}, (list) => { listElements += list.nodes.length; }); check(listElements <= seen.length, "array visitor"); for (const doc of node.jsDoc ?? []) check(doc.kind === AstKind.JSDoc, "JSDoc children"); } @@ -249,17 +249,17 @@ export function runTs7Client(client: Ts7RpcClient, directory: string, report: st check(project.program.getSourceFileMetadata(file)!.isDefaultLibrary === false, "source metadata"); check(!project.program.isSourceFileFromExternalLibrary(root) && !project.program.isSourceFileDefaultLibrary(root), "source library flags"); checkAst(tree, content); - const statements = tree.root.statements!; - check(statements[1]!.declarationList!.declarations![0]!.initializer!.text === "\uFEFFhéllo 🌍", "string BOM retained"); + const statements = tree.root.statements!.nodes; + check(statements[1]!.declarationList!.declarations!.nodes[0]!.initializer!.text === "\uFEFFhéllo 🌍", "string BOM retained"); const box = statements[2]!; - check(box.getStart(undefined, true) < box.getStart() && box.members!.length === 2, "class JSDoc and members"); - check(box.members![0]!.modifierFlags !== 0, "readonly modifier flag"); - check(statements[3]!.declarationList!.declarations![0]!.initializer!.elements!.length === 40, "long native node list"); - const template = statements[4]!.declarationList!.declarations![0]!.initializer!; + check(box.getStart(undefined, true) < box.getStart() && box.members!.nodes.length === 2, "class JSDoc and members"); + check(box.members!.nodes[0]!.modifierFlags !== 0, "readonly modifier flag"); + check(statements[3]!.declarationList!.declarations!.nodes[0]!.initializer!.elements!.nodes.length === 40, "long native node list"); + const template = statements[4]!.declarationList!.declarations!.nodes[0]!.initializer!; check(template.head!.text === "head\n" && template.head!.rawText === "head\\n", "cooked and raw template text"); check(project.program.getSemanticDiagnostics(file).length === 0, "valid program diagnostics"); check(project.program.getSyntacticDiagnostics(file).length === 0 && project.program.getBindDiagnostics(file).length === 0, "parse and bind diagnostics"); - const declaration = root.statements![0]!.declarationList!.declarations![0]!; + const declaration = root.statements.nodes[0]!.declarationList!.declarations?.nodes[0]!; const identifier = declaration.name!; const checker = project.checker; const ownedType = checker.getTypeAtLocation(identifier)!; diff --git a/tests/fixtures/self-hosting/ts7-program-host-cases.ts b/tests/fixtures/self-hosting/ts7-program-host-cases.ts index de51d1fa2..44fb29a3b 100644 --- a/tests/fixtures/self-hosting/ts7-program-host-cases.ts +++ b/tests/fixtures/self-hosting/ts7-program-host-cases.ts @@ -60,10 +60,10 @@ export function runProgramHost(createHost: (options: Ts7HostOptions) => Ts7Host, check(program.getSourceFiles() === program.getSourceFiles(), "source file cache"); const checker = program.getTypeChecker(); check(checker === program.getTypeChecker(), "checker cache"); - const statement = source.statements[1]!; + const statement = source.statements.nodes[1]!; check(isVariableStatement(statement), "variable statement"); if (isVariableStatement(statement)) { - const name = statement.declarationList.declarations[0]!.name; + const name = statement.declarationList.declarations.nodes[0]!.name; check(name !== undefined && isIdentifier(name), "variable identifier"); if (name !== undefined && isIdentifier(name)) { const type = checker.getTypeAtLocation(name); diff --git a/tests/fixtures/self-hosting/ts7-semantic-cases.ts b/tests/fixtures/self-hosting/ts7-semantic-cases.ts index b2538c119..3fd34cefd 100644 --- a/tests/fixtures/self-hosting/ts7-semantic-cases.ts +++ b/tests/fixtures/self-hosting/ts7-semantic-cases.ts @@ -37,9 +37,9 @@ function check(value: boolean, message: string): void { } function declaration(root: AstNode, name: string): AstNode { - for (const statement of root.statements ?? []) { + for (const statement of root.statements?.nodes ?? []) { if (statement.name?.text === name) return statement; - for (const variable of statement.declarationList?.declarations ?? []) { + for (const variable of statement.declarationList?.declarations?.nodes ?? []) { if (variable.name?.text === name) return variable; } } @@ -135,7 +135,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const dict = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Dict").name!)); const index = checker.getIndexInfosOfType(dict)[0]!; check(index.keyType === intrinsicString && index.valueType === intrinsicNumber && index.isReadonly, "index signature types"); - check(index.declaration!.resolve() === declaration(root, "Dict").members![0], "index signature declaration"); + check(index.declaration!.resolve() === declaration(root, "Dict").members?.nodes[0], "index signature declaration"); const generic = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Generic").name!)); const genericParameter = generic.getTypeParameters()[0]!; check(genericParameter.isTypeParameter() && generic.getLocalTypeParameters()[0] === genericParameter && generic.getOuterTypeParameters().length === 0, "interface type parameters"); @@ -147,7 +147,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const signature = signatureAt(checker, identity); const signatureTypes = signature.getTypeParameters(); check(signatureTypes.length === 1 && signatureTypes[0]!.isTypeParameter(), "signature type parameters"); - check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarations[0]!.resolve() === identity.parameters![0], "signature parameters"); + check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarations[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); check(signature.declaration!.resolve() === identity, "signature declaration"); check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity"); check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query"); @@ -177,11 +177,11 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(!checker.isUnknownSymbol(answerSymbol) && !checker.isUndefinedSymbol(answerSymbol) && !checker.isArgumentsSymbol(answerSymbol), "well-known symbol exclusion"); const undefinedSymbol = checker.resolveName("undefined", SemanticSymbolFlags.Value, answerName)!; check(checker.isUndefinedSymbol(undefinedSymbol), "well-known undefined symbol"); - const shorthand = declaration(root, "ref").initializer!.properties![0]!; + const shorthand = declaration(root, "ref").initializer!.properties?.nodes[0]!; check(checker.getShorthandAssignmentValueSymbol(shorthand) === answerSymbol, "shorthand symbol"); const exportSpecifier = renamed.declarations[0]!.resolve()!; check(checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) === answerSymbol, "export specifier symbol"); - check(checker.getConstantValue(declaration(root, "Mode").members![0]!) === 3, "enum constant"); + check(checker.getConstantValue(declaration(root, "Mode").members?.nodes[0]!) === 3, "enum constant"); check(checker.getConstantValue(answerName) === undefined, "missing constant"); check(checker.getContextualType(declaration(root, "pair").initializer!) === pair, "contextual tuple type"); check(checker.getTypeFromTypeNode(declaration(root, "Choice").type!) === choice, "type-node query identity"); @@ -206,7 +206,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(syntheticType.file.node(1) === syntheticType, "synthetic root identity"); const syntheticSignature = checker.signatureToSignatureDeclaration(signature, AstKind.FunctionType, identity)!; check(syntheticSignature.kind === AstKind.FunctionType, "binary signature node"); - check(syntheticSignature.parameters!.length === 1 && syntheticSignature.parameters![0]!.name!.text === "value", "synthetic signature children"); + check(syntheticSignature.parameters?.nodes.length === 1 && syntheticSignature.parameters?.nodes[0]!.name!.text === "value", "synthetic signature children"); let syntheticRefusals = 0; try { syntheticType.getSourceFile(); } catch { syntheticRefusals++; } try { syntheticSignature.file.sourceFile; } catch { syntheticRefusals++; } @@ -218,7 +218,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(usages.length === 1 && usages[0]!.name.resolve()!.text === "identity" && usages[0]!.call!.resolve() === declaration(root, "called").initializer, "signature usage handles"); const completions = checker.getCompletionsAtPosition(root.fileName, root.text!.indexOf("Mode.Ready") + 5, { includeSymbol: true })!; const ready = completions.entries.find((entry) => entry.name === "Ready")!; - check(ready.symbol === symbolAt(checker, declaration(root, "Mode").members![0]!.name!), "completion symbol identity"); + check(ready.symbol === symbolAt(checker, declaration(root, "Mode").members?.nodes[0]!.name!), "completion symbol identity"); check(checker.getCompletionsAtPosition(root.fileName, answerName.getStart()) === undefined, "absent completions"); check(answerType.flags === SemanticTypeFlags.NumberLiteral, "literal flags pin"); check(checkSemanticRefinements(answerType) === "number:43", "native number literal refinement"); diff --git a/tests/fixtures/self-hosting/ts7-source-parser-cases.ts b/tests/fixtures/self-hosting/ts7-source-parser-cases.ts index 7bf796dd5..8a1fd23d8 100644 --- a/tests/fixtures/self-hosting/ts7-source-parser-cases.ts +++ b/tests/fixtures/self-hosting/ts7-source-parser-cases.ts @@ -32,7 +32,7 @@ export function runSourceParser(parser: Ts7SourceParser, input: string, output: nodes, optional, declaration: file.isDeclarationFile, - statements: file.statements.map((statement) => ({ + statements: file.statements.nodes.map((statement) => ({ text: statement.getText(), start: statement.getStart(), end: statement.end, })), imports: moduleSpecifiersOfFile(file), @@ -98,7 +98,7 @@ export function runSourceParser(parser: Ts7SourceParser, input: string, output: } parser.close(); parser.close(); - const versions = [original.statements[0]!.getText(), updated.statements[0]!.getText()]; + const versions = [original.statements.nodes[0]!.getText(), updated.statements.nodes[0]!.getText()]; let closed = false; try { parser.parse("late.ts", "", "ts"); } catch { closed = true; } writeFileSync(output, JSON.stringify({ files, packages, versions, closed })); diff --git a/tests/harness/stdlib-surface.test.ts b/tests/harness/stdlib-surface.test.ts index e91e7be4d..53c2704bf 100644 --- a/tests/harness/stdlib-surface.test.ts +++ b/tests/harness/stdlib-surface.test.ts @@ -97,9 +97,9 @@ const probeErrors = ts .filter((d) => d.fileName === sf.fileName && d.category === ts.DiagnosticCategory.Error); const decls = new Map(); -for (const stmt of sf.statements) { +for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; - const decl = stmt.declarationList.declarations[0]!; + const decl = stmt.declarationList.declarations.nodes[0]!; if (ts.isIdentifier(decl.name)) decls.set(decl.name.text, decl); } From c6e16d365b35dfb02f32ea1b22ed68b66ac6e2df Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 07:56:11 -0500 Subject: [PATCH 120/120] Read getTestContext's arguments through their nodes lowerGetTestContext (#1393) read expr.arguments as an array. Since #1394 an AST list's elements are in nodes, so the compiler did not type-check. --- packages/compiler/src/frontend/lowering/lower-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 5fc333abc..5c6011425 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -685,9 +685,9 @@ export function lowerNodeTestModuleCall( * and code reached only through timer, immediate, nextTick and * queueMicrotask callbacks). */ function lowerGetTestContext(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 0) { + if (expr.arguments.nodes.length > 0) { lowerer.noLowering( - `getTestContext with ${expr.arguments.length} arguments`, + `getTestContext with ${expr.arguments.nodes.length} arguments`, expr, "the supported form is getTestContext()", );