// Downlevel mode (ABI mode 1): down-levels enums, instantiated namespaces, parameter properties, `import x =` and // `export =`, matching ts.transpileModule's runtime behavior. strip.zena calls the hooks below from its handlers. // Every inserted text is free of line breaks, so lines are preserved; columns shift only inside these constructs. // // Enums and namespaces become `var E;(function(E){...}).call(E||(E={}),E);`, with the source's own braces as the // function body. Inside, `this` is the enum or namespace object, so a member or export named like the container // cannot shadow it. Enum members are also bound as locals so later initializers can name them bare. import { Memory } from 'zena:memory'; import { scan, seek, reseek, more, tb, ts, te, tt, tk, tnl, delta, rescanRegex, rescanTemplate, rescanGreater, isBinop, precOf, isAssign, T_EOF, T_NAME, T_NUM, T_STR, T_TEMPLATE, T_TEMPLATE_HEAD, T_TEMPLATE_TAIL, T_LBRACE, T_RBRACE, T_LPAREN, T_RPAREN, T_SEMI, T_LBRACK, T_RBRACK, T_DOT, T_QDOT, T_ELLIPSIS, T_COMMA, T_COLON, T_EQ, T_BANG, T_TILDE, T_INC, T_DEC, T_LT, T_GT, T_PLUS, T_MINUS, T_SLASH, T_SLASH_EQ, T_STARSTAR, T_SHL, T_BAR, T_CARET, T_AMP, tokenIn, } from './lex.zena'; import { keywordAt, K_CONST, K_ENUM, K_EXPORT, K_FUNCTION, K_TYPE, K_REQUIRE, K_SUPER, K_IN, K_INSTANCEOF, K_TYPEOF, K_VOID, K_DELETE, K_AWAIT, K_NEW, K_LET, K_STATIC, K_YIELD, K_IMPLEMENTS, K_INTERFACE, K_PRIVATE, K_PROTECTED, K_PUBLIC, K_FIRST_CONTEXTUAL, K_CLASS, K_VAR, K_NAMESPACE, K_MODULE, K_GLOBAL, K_DECLARE, keywordIn, KS_STRICT_RESERVED, KS_DL_DECL, TS_OPEN, TS_POSTFIX_UPDATE, } from './keywords.zena'; import { getSp, getNest, getBlankDepth, getVerbatim, getStmtNest, getAnchLen, fa, fb, fc, setA, setB, setC, setSt, pushFrame, pop, belowSt, belowA, belowB, setBelowA, adv, advQuiet, advBlank, emitTo, blankRange, copyThrough, anchorPush, anchorPushAt, anchorPop, anchorOut, anchorIn, anchorErr, anchorBlank, restoreFrom, markValue, enterBlank, endStmt, back, skipBalanced, skipTemplate, pushExpr, openBlock, importEqualsEnd, failAt, outReserve, outCommit, outInsert, dropTo, setNest, setErrLen, lookType, pruneNamespaceFallbacks, errorNamespaceFallback, getStmtUnits, spansLines, errorAtStmt, releaseDecorators, } from './strip.zena'; // strip.zena's values (module-level bindings cannot cross the import cycle). let S_TOP: i32 = 1; let S_BLOCK: i32 = 2; let S_EXPRSTMT: i32 = 4; let S_CLASSBODY: i32 = 26; let S_NAMESPACE: i32 = 30; let S_DL_ENUM: i32 = 40; let B_BLOCK: i32 = 1; let B_NAMESPACE: i32 = 2; let ANCH_IN: i32 = 1; let ANCH_SEMI: i32 = 2; // pendAdd's `live` carries this bit when the declarator had no initializer: `tsc` emits no member at // all for it (`export var x: number;` leaves `N.x` absent, not merely `undefined`), unlike every other // exported declarator, which it always assigns. Only a bare identifier can lack an initializer at all // (a destructuring pattern requires one), so this is only ever computed for that shape. let PEND_NOINIT: i32 = 256; // Enum member classes: TS writes a reverse mapping unless the value is a constant string. let C_NUM: i32 = 1; // a known number: reverse mapping let C_STR: i32 = 2; // syntactically a string: none let C_EVAL: i32 = 3; // constant to TS, type known only at run time: decided by typeof let C_OPAQUE: i32 = 4; // not constant to TS: reverse mapping // Name table kinds. let N_DECL: i32 = 1; // a class or function declared in scope `key` let N_ENUM: i32 = 2; // an enum declared in scope `key` let N_MEMBER: i32 = 3; // a member of the enum whose N_ENUM record is `key` let N_ENUMANY: i32 = 4; // an enum of this name in any scope let N_NS: i32 = 5; // a namespace declared in scope `key` let N_NSX: i32 = 6; // a namespace exported from the namespace whose record is `key` let N_NSEXP: i32 = 7; // a name exported from the namespace whose record is `key` // Ctor body block kinds while waiting for (3) or right after (4) the top-level `super(...)` statement. let B_CTORW: i32 = 3; let B_CTORS: i32 = 4; let DL_CLS: i32 = 2097152; // S_CLASSBODY.a: downlevel frame; bit 20 extends; bits 0..19 class anchor index + 1 export var dlExported: i32 = 0; var dlScope: i32 = 0; var dlScopeNext: i32 = 0; var dlHookSp: i32 = -1; var dlPendBase: i32 = 0; var dlNsRec: i32 = 0; // the N_NS/N_NSX record of the namespace body being parsed, 0 outside // Module-kind tracking (see "Module-kind tracking" below, near importEquals/exportAssign): whether this // file has committed to a value-level ESM import/export declaration (mkEsm) or a CommonJS-only `import x // =`/`export =` (mkCjs) so far, and whether the file is already known to mix both (mixed). Reset by // init() per file/stream. var mkEsm: boolean = false; var mkCjs: boolean = false; var mixed: boolean = false; // Scopes (dlScope ids) a fast-pathed namespace's own `namespaceEnd` must decline instead of wrapping in // an IIFE, because a *nested namespace* (not a module/global augmentation directly in its own body — // `namespaceEnd`'s own `tx[base + 8]` count covers that shape instead, see its own comment) itself // declined and restored its own span to literal, un-erased source text: `namespaceEnd`'s own propagation // (when it declines) adds the restored *parent* scope here, checked back by that scope's own // `namespaceEnd` later. Never cleared, same as the rest of this file's small never-shrinking lookup lists // (`nm`, `declSpan`, ...) — real files decline rarely, so this stays short. Entirely confined to this // file (no erase.wasm cost): nothing outside namespaceEnd itself ever writes or reads it. var declinedScope: FixedArray = new FixedArray(8, 0); var declinedScopeN: i32 = 0; function declineScope(scope: i32): void { var i = 0; while (i < declinedScopeN) { if (declinedScope[i] == scope) return; i = i + 1; } declinedScope = grow(declinedScope, declinedScopeN + 1); declinedScope[declinedScopeN] = scope; declinedScopeN = declinedScopeN + 1; } function scopeDeclined(scope: i32): boolean { var i = 0; while (i < declinedScopeN) { if (declinedScope[i] == scope) return true; i = i + 1; } return false; } // --------------------------------------------------------------------------- // Text stack: name entries [len, bytes...] and small ints, owned LIFO by the open constructs. var tx: FixedArray = new FixedArray(256, 0); var txLen: i32 = 0; // Returns `a` if it holds `need` entries, else a copy with at least twice the room. Every table's // capacity check goes through here, so a call site is one call instead of an inlined compare. function grow(a: FixedArray, need: i32): FixedArray { if (need <= a.length) return a; var cap = a.length * 2; while (cap < need) cap = cap * 2; let b = new FixedArray(cap, 0); var i = 0; while (i < a.length) { b[i] = a[i]; i = i + 1; } return b; } function txPush(v: i32): void { tx = grow(tx, txLen + 1); tx[txLen] = v; txLen = txLen + 1; } // Pushes input [a, b) as a name entry; returns its offset. function txName(a: i32, b: i32): i32 { let o = txLen; txPush(b - a); var i = a; while (i < b) { txPush(Memory.default.getU8(i + delta)); i = i + 1; } return o; } // Pushes input [a, b) in double quotes. function txQuoted(a: i32, b: i32): i32 { let o = txLen; txPush(b - a + 2); txPush(34); var i = a; while (i < b) { txPush(Memory.default.getU8(i + delta)); i = i + 1; } txPush(34); return o; } function txNext(o: i32): i32 { return o + 1 + tx[o]; } // BEGIN GENERATED // GENERATED by scripts/gen/downlevel-text.mjs from its template table. Do not edit by hand. let TEXT: i32 = 2304; function litVar(): void { tpl(0, 0); } function litConst(): void { tpl(4, 0); } function litModExp(): void { tpl(10, 0); } function litSemi(): void { tpl(25, 0); } function litRb(): void { tpl(27, 0); } function litDestr(): void { tpl(29, 0); } function litDestrEnd(): void { tpl(35, 0); } function tDestrEndAs(o: i32): void { tpl(43, o); } function tItem(o: i32): void { tpl(48, o); } function tName(o: i32): void { tpl(51, o); } function tFn(o: i32): void { tpl(53, o); } function litLb(): void { tpl(67, 0); } function tDotFn(o: i32): void { tpl(69, o); } function tCall(o: i32): void { tpl(88, o); } function tCallX(o: i32): void { tpl(109, o); } function tEiNum(o: i32): void { tpl(142, o); } function tEiEval(o: i32): void { tpl(171, o); } function tEiStr(o: i32): void { tpl(222, o); } function tEnNum(o: i32): void { tpl(239, o); } function tEnEval(o: i32): void { tpl(257, o); } function tThisIdx(o: i32): void { tpl(303, o); } function litAuto0(): void { tpl(311, 0); } function tAutoI(o: i32): void { tpl(314, o); } function tAutoN(o: i32): void { tpl(319, o); } function litAutoVoid(): void { tpl(330, 0); } function tAssign(o: i32): void { tpl(338, o); } function tLive(o: i32): void { tpl(348, o); } function tAmbientLive(o: i32): void { tpl(447, o); } function wInfinity(a: i32, b: i32): boolean { return isWord(a, b, 597, 8); } function wNaN(a: i32, b: i32): boolean { return isWord(a, b, 605, 3); } function wEval(a: i32, b: i32): boolean { return isWord(a, b, 608, 4); } function wArguments(a: i32, b: i32): boolean { return isWord(a, b, 612, 9); } // END GENERATED // Emits the template at offset `at` of the text, with `@` replaced by the name entry at `o`. function tpl(at: i32, o: i32): void { var i = TEXT + at; while (true) { let c = Memory.default.getU8(i); if (c == 126) return; if (c == 64) { nameOut(o); } else { Memory.default.setU8(outReserve(1), c); outCommit(1); } i = i + 1; } } function nameOut(o: i32): void { let len = tx[o]; var d = outReserve(len); var j = 0; while (j < len) { Memory.default.setU8(d, tx[o + 1 + j]); d = d + 1; j = j + 1; } outCommit(len); } // Input [a, b) equals the n bytes at offset `at` of the text. function isWord(a: i32, b: i32, at: i32, n: i32): boolean { if (b - a != n) return false; var i = 0; while (i < n) { if (Memory.default.getU8(TEXT + at + i) != Memory.default.getU8(a + i + delta)) return false; i = i + 1; } return true; } // --------------------------------------------------------------------------- // Name table: records [hash, kind, key, len, head, link, stamp, bytes...] with an open-addressing index. A member or // export record (`key` is its owner's record) is linked from its owner's `head`, newest first. Records are never // removed only when the namespace body they were declared in closes (prune), and scope ids are unique per body, // so names of closed scopes never match again. The index marks a removed record's slot with -1. var nm: FixedArray = new FixedArray(1024, 0); var nmLen: i32 = 1; var ht: FixedArray = new FixedArray(256, 0); var htCount: i32 = 0; // used index slots, removed ones included var nmCount: i32 = 0; // records function hashOf(kind: i32, key: i32, a: i32, b: i32): i32 { var h = kind * 31 + key * 1000003; var i = a; while (i < b) { h = (h ^ Memory.default.getU8(i + delta)) * 16777619; i = i + 1; } return h & 0x7fffffff; } function htInsert(r: i32): void { let mask = ht.length - 1; var i = nm[r] & mask; while (ht[i] != 0) i = (i + 1) & mask; ht[i] = r; } function nmFind(kind: i32, key: i32, a: i32, b: i32): i32 { let h = hashOf(kind, key, a, b); let mask = ht.length - 1; var i = h & mask; while (true) { let r = ht[i]; if (r == 0) return -1; if (r > 0 && nm[r] == h && nm[r + 1] == kind && nm[r + 2] == key && nm[r + 3] == b - a) { var j = 0; while (j < b - a && nm[r + 7 + j] == Memory.default.getU8(a + j + delta)) j = j + 1; if (j == b - a) return r; } i = (i + 1) & mask; } } function rehash(): void { var size = 256; while (size < (nmCount + 1) * 4) size = size * 2; ht = new FixedArray(size, 0); htCount = 0; var r = 1; while (r < nmLen) { htInsert(r); htCount = htCount + 1; r = r + 7 + nm[r + 3]; } } function nmAdd(kind: i32, key: i32, a: i32, b: i32): i32 { if ((htCount + 1) * 2 > ht.length) rehash(); let need = nmLen + 7 + (b - a); nm = grow(nm, need); let r = nmLen; nm[r] = hashOf(kind, key, a, b); nm[r + 1] = kind; nm[r + 2] = key; nm[r + 3] = b - a; nm[r + 4] = 0; nm[r + 5] = 0; nm[r + 6] = 0; if (kind == N_MEMBER || kind == N_NSEXP) { nm[r + 5] = nm[key + 4]; nm[key + 4] = r; } var j = 0; while (j < b - a) { nm[r + 7 + j] = Memory.default.getU8(a + j + delta); j = j + 1; } nmLen = need; htInsert(r); htCount = htCount + 1; nmCount = nmCount + 1; return r; } // Drops the records added since `mark` by a namespace body that just closed, except what a later declaration of // the same namespace, or a qualified enum reference, can still look up. function prune(mark: i32): void { if (nmLen == mark) return; let fwd = new FixedArray(nmLen - mark, 0); var r = mark; var w = mark; while (r < nmLen) { let k = nm[r + 1]; let size = 7 + nm[r + 3]; fwd[r - mark] = -1; if (k == N_NSX || k == N_NSEXP || k == N_ENUMANY) { fwd[r - mark] = w; w = w + size; } r = r + size; } let mask = ht.length - 1; r = mark; while (r < nmLen) { var i = nm[r] & mask; while (ht[i] != r) i = (i + 1) & mask; ht[i] = -1; r = r + 7 + nm[r + 3]; } r = mark; while (r < nmLen) { let size = 7 + nm[r + 3]; let d = fwd[r - mark]; if (d >= 0) { var j = 0; while (j < size) { nm[d + j] = nm[r + j]; j = j + 1; } if (nm[d + 4] >= mark) nm[d + 4] = fwd[nm[d + 4] - mark]; if (nm[d + 5] >= mark) nm[d + 5] = fwd[nm[d + 5] - mark]; } else { nmCount = nmCount - 1; } r = r + size; } if (dlNsRec != 0 && nm[dlNsRec + 4] >= mark) nm[dlNsRec + 4] = fwd[nm[dlNsRec + 4] - mark]; nmLen = w; r = mark; while (r < nmLen) { htInsert(r); htCount = htCount + 1; r = r + 7 + nm[r + 3]; } } // --------------------------------------------------------------------------- // Context. function kw(k: i32): boolean { return tt == T_NAME && tk == k; } // The statement on top sits directly in the module or a namespace body, where declarations merge. function inScope(): boolean { if (getSp() < 2 || getBlankDepth() > 0) return false; let s = belowSt(1); return s == S_TOP || (s == S_BLOCK && belowA(1) == B_NAMESPACE); } // `getVerbatim() == 0` excludes a `module N {}`-keyword statement's own declined body (`stmtNamespace`'s // "module N {} is always an error" branch, engine/strip.zena): it still opens a B_NAMESPACE block so its // own nested statements parse structurally, but everything inside is meant to pass through completely // unchanged (verbatim/declined), never down-leveled. Without this, `ambientVar` (below) still saw // `inNs()` true for a `declare module Name { export var x: T; }` whose name happens to use the `module` // keyword, and inserted a live-binding accessor into output text that was supposed to stay untouched. function inNs(): boolean { return getSp() >= 2 && getBlankDepth() == 0 && getVerbatim() == 0 && belowSt(1) == S_BLOCK && belowA(1) == B_NAMESPACE; } export function init(): void { dlExported = 0; dlScope = 0; dlScopeNext = 0; dlHookSp = -1; dlNsRec = 0; txLen = 0; nmLen = 1; htCount = 0; nmCount = 0; mkEsm = false; mkCjs = false; mixed = false; var i = 0; while (i < ht.length) { ht[i] = 0; i = i + 1; } } // A name already declared in this scope by a class, function, enum or namespace: a merging enum or namespace // reuses that binding, so it writes neither `var` nor `export`. function declared(a: i32, b: i32): boolean { return inScope() && nmFind(N_DECL, dlScope, a, b) >= 0; } function declare(a: i32, b: i32): void { if (inScope() && nmFind(N_DECL, dlScope, a, b) < 0) nmAdd(N_DECL, dlScope, a, b); } // At `export` used as a modifier: 1 when it must be blanked, 2 when the step must wait for input. Inside a // namespace body the declaration that follows is attached to the namespace instead. export function exportModifier(): i32 { if (inNs()) { dlExported = 1; return 1; } let m = tb; scan(); if (more == 0 && kw(K_CONST)) scan(); var r = 0; if (more == 0 && tt == T_NAME && tk == K_ENUM) { scan(); if (more == 0 && tt == T_NAME && declared(ts, te)) r = 1; } if (!back(m)) return 2; return r; } // Names exported by the namespace statement in progress, attached after it ends (flushed from hBlock). function pendAdd(live: i32, a: i32, b: i32): void { if (dlHookSp != getSp() - 1) { dlHookSp = getSp() - 1; dlPendBase = txLen; } txPush(live); txName(a, b); nsExport(a, b); } // Later declarations of the same namespace bind its earlier exports as locals. function nsExport(a: i32, b: i32): void { if (dlNsRec != 0 && nmFind(N_NSEXP, dlNsRec, a, b) < 0) nmAdd(N_NSEXP, dlNsRec, a, b); } var stamp: i32 = 0; var used: FixedArray = new FixedArray(32, 0); var usedN: i32 = 0; var kinds: FixedArray = new FixedArray(32, 0); // namespaceStart's own ancestor-merge destructure target, set during its lookahead and read during its // commit-phase dotted loop: the depth (0 = the outermost, non-dotted segment) of the *deepest existing* // ancestor along a dotted path that the full requested path doesn't itself reach (`namespace A.B.C {}` // reopening `A.B` while `A.B.C` itself is new) — `-1` when there is none to destructure (either nothing // along the path pre-exists, or the full path itself is the merge, which the ordinary `destructure()` at // the deepest level already covers). var ambDestrDepth: i32 = -1; // Lookahead from a body's `{`: the records of `kind` under `key` that the body names, each once. Merged // declarations bind only those as locals, so a long run of reopened declarations stays linear. // // `excludeShadow`, used only for a merge block's own destructure (namespaceStart): a depth-0 `class`/`let`/ // `const` declaration re-using one of these names doesn't just "use" the earlier merged member, it creates a // fresh lexical binding that the destructured local of the same name (`var {Point,}=this;`) would collide with // ("Identifier 'Point' has already been declared" — `var` is compatible with a later `var`/`function` of the // same name, which is why those two keywords aren't included, but never with `let`/`const`/`class`). That // occurrence, and the lexical declaration it introduces, must drop the name from `used` instead of adding it // (corpus/downlevel.jsonl, "namespace-merge-var-class-collision"). // // `writeOnly`, used only for declNames' own "is this uninitialized export ever touched again" check: a plain // read of the name (or a mention inside some other declarator's own type annotation, e.g. a self-referencing // interface merge — `export var X: X;`) never counts, only an actual write (`name =`/a compound-assign op, or // `name++`/`--name`) does — a read gets the same value either way (the namespace member and the disconnected // local both read as `undefined`), but a write needs the member to exist for the assignment to land on it // ("namespace-uninitialized-export-overcounted", corpus/downlevel.jsonl). function scanUsed(kind: i32, key: i32, excludeShadow: boolean, writeOnly: boolean): void { stamp = stamp + 1; usedN = 0; var depth = 0; var declKw = false; var pendingR = -1; // writeOnly: the record the previous token named, still waiting to see a write marker var wasIncDec = false; while (true) { let wasDeclKw = declKw; declKw = false; let prevR = pendingR; pendingR = -1; let hadIncDec = wasIncDec; wasIncDec = false; scan(); if (more == 1 || tt == T_EOF) return; if (writeOnly && prevR >= 0 && (isAssign(tt) || tt == T_INC || tt == T_DEC) && nm[prevR + 6] != stamp) { nm[prevR + 6] = stamp; used = grow(used, usedN + 1); used[usedN] = prevR; usedN = usedN + 1; } if (tt == T_NAME) { let r = nmFind(kind, key, ts, te); if (excludeShadow && depth == 0 && wasDeclKw) { if (r >= 0) { if (nm[r + 6] != stamp) { nm[r + 6] = stamp; } else { var k = 0; while (k < usedN) { if (used[k] == r) { used[k] = used[usedN - 1]; usedN = usedN - 1; break; } k = k + 1; } } } } else if (writeOnly) { if (r >= 0 && hadIncDec && nm[r + 6] != stamp) { nm[r + 6] = stamp; used = grow(used, usedN + 1); used[usedN] = r; usedN = usedN + 1; } pendingR = r; } else if (r >= 0 && nm[r + 6] != stamp) { nm[r + 6] = stamp; used = grow(used, usedN + 1); used[usedN] = r; usedN = usedN + 1; } if (excludeShadow && depth == 0 && (tk == K_CLASS || tk == K_LET || tk == K_CONST)) declKw = true; } else if (tt == T_INC || tt == T_DEC) { wasIncDec = true; } else if (tt == T_LBRACE || tt == T_TEMPLATE_HEAD) { kinds = grow(kinds, depth + 1); kinds[depth] = 0; if (tt == T_TEMPLATE_HEAD) kinds[depth] = 1; depth = depth + 1; } else if (tt == T_RBRACE) { if (depth == 0) return; if (kinds[depth - 1] == 1) { rescanTemplate(); if (more == 1) return; if (tt == T_TEMPLATE_TAIL) depth = depth - 1; } else { depth = depth - 1; } } } } // --------------------------------------------------------------------------- // A down-leveled namespace/enum IIFE's own parameter name. Normally it's just the construct's own name, but // `tsc`'s module/enum transform conservatively renames it (`name_1`, `name_2`, ...) whenever some declaration // inside the about-to-open body — at ANY depth, even a nested function's own unrelated local, matching `tsc`'s // own depth-insensitive `isUniqueLocalName` check, not real scope analysis — already binds that exact name // ("namespace-lexical-member-named-like-namespace", corpus/downlevel.jsonl). The outer `var ;` and the // merge call's own `||(={})` are never renamed, only the function's formal parameter, so this is // invisible outside the down-leveled span (which the oracle never line/shape-compares inside) — the exact // suffix `tsc` would have picked doesn't matter, only that the one picked here is actually collision-free. var declSpan: FixedArray = new FixedArray(64, 0); // pairs [a, b, a, b, ...], input spans var declSpanN: i32 = 0; // Set by declSpanScan's own walk (below) alongside its declaration-name collection: the number of depth-0 // `module`/`global` augmentation statements directly in the body — ts-blank-space's own, non-recursive // "conservatively a value" rule for exactly these (engine/strip.zena's stmtNamespace, which calls plain // `markValue` for one of these exactly once each, same as for an ordinary value-producing statement) — // read by namespaceStart/namespaceEnd (below) to tell "every mark `values` counted came from one of // these" apart from "something else also contributed", without strip.zena's shared `markValue` needing to // distinguish the two itself (which would cost erase mode bytes for a signal it never reads — see // declineModuleKeyword, above, for the identical reasoning applied to the outer statement itself). See // corpus/downlevel.jsonl, "namespace-uninstantiated-ambient-only-wrapped". var declSpanModuleOrGlobalN: i32 = 0; function declSpanAdd(a: i32, b: i32): void { declSpan = grow(declSpan, declSpanN + 2); declSpan[declSpanN] = a; declSpan[declSpanN + 1] = b; declSpanN = declSpanN + 2; } // Compares an input span against a tx name entry ([len, bytes...] at offset `o`). function sameAsEntry(a: i32, b: i32, o: i32): boolean { let len = tx[o]; if (b - a != len) return false; var i = 0; while (i < len) { if (Memory.default.getU8(a + i + delta) != tx[o + 1 + i]) return false; i = i + 1; } return true; } function declSpanHasEntry(o: i32): boolean { var i = 0; while (i < declSpanN) { if (sameAsEntry(declSpan[i], declSpan[i + 1], o)) return true; i = i + 2; } return false; } // Scans the upcoming namespace/enum body (cursor at its own `{`) for every declaration name at any depth, into // declSpan: a `var`/`let`/`const` declarator's first (simple) name, or a `class`/`function`/`namespace`/`module` // declaration's own name. A mere reference, or a name inside a string/template, is never a declaration and // never collected — only an actually-bound identifier can collide with the IIFE's own parameter. function declSpanScan(): void { declSpanN = 0; declSpanModuleOrGlobalN = 0; var depth = 0; var declKw = false; var atStart = true; while (true) { let wasDeclKw = declKw; let wasAtStart = atStart; declKw = false; atStart = false; scan(); if (more == 1 || tt == T_EOF) return; if (tt == T_NAME) { if (wasDeclKw) { declSpanAdd(ts, te); } else if (keywordIn(tk, KS_DL_DECL)) { declKw = true; } if (depth == 0 && wasAtStart) { if (tk == K_MODULE || tk == K_GLOBAL) declSpanModuleOrGlobalN = declSpanModuleOrGlobalN + 1; else if (tk == K_EXPORT || tk == K_DECLARE) atStart = true; } } else if (tt == T_LBRACE || tt == T_TEMPLATE_HEAD) { kinds = grow(kinds, depth + 1); kinds[depth] = 0; if (tt == T_TEMPLATE_HEAD) kinds[depth] = 1; depth = depth + 1; } else if (tt == T_RBRACE) { if (depth == 0) return; if (kinds[depth - 1] == 1) { rescanTemplate(); if (more == 1) return; if (tt == T_TEMPLATE_TAIL) { depth = depth - 1; if (depth == 0) atStart = true; } } else { depth = depth - 1; if (depth == 0) atStart = true; } } else if (tt == T_SEMI && depth == 0) { atStart = true; } } } // Pushes n (n >= 0) in decimal. Zena's `/` is a float division, so n / 10 is the exact reciprocal // multiply for any non-negative i32: n * ceil(2^35 / 10) >> 35. function txDecimal(n: i32): void { if (n >= 10) txDecimal((((n as i64) * (3435973837 as i64)) >> (35 as i64)) as i32); txPush(48 + n % 10); } // Builds "_" (n >= 1) at the current txLen; returns its offset. The caller rolls txLen back. function txSuffixed(a: i32, b: i32, n: i32): i32 { let o = txName(a, b); txPush(95); txDecimal(n); tx[o] = txLen - o - 1; return o; } // The `{a,b,}` items scanUsed found, shared by `destructure` (closes over `this`) and `destructureAs` // (closes over a dotted chain's own outer segment instead — see `ambDestrDepth` above). function destructureItems(): void { var i = 0; while (i < usedN) { let r = used[i]; let o = txLen; txPush(nm[r + 3]); var j = 0; while (j < nm[r + 3]) { txPush(nm[r + 7 + j]); j = j + 1; } tItem(o); txLen = o; i = i + 1; } } // `var {a,b,}=this;` for the names scanUsed found. function destructure(): void { litDestr(); destructureItems(); litDestrEnd(); } // `var {a,b,}=;` for the names scanUsed found, closing over a dotted chain's own ancestor // segment (its already-emitted IIFE parameter) instead of `this` — see `ambDestrDepth` above. function destructureAs(nameOff: i32): void { litDestr(); destructureItems(); tDestrEndAs(nameOff); } // An overload signature of an exported function: it is blanked and exports nothing. export function dropPending(): void { if (dlHookSp == getSp() - 1) { txLen = dlPendBase; dlHookSp = -1; } } export function hookSp(): i32 { return dlHookSp; } // At a statement boundary of the block whose frame is on top and that registered a hook. Returns true when the // step must wait for input. export function blockHook(): boolean { let k = fa(); if (k == B_NAMESPACE) { litSemi(); var i = dlPendBase; while (i < txLen) { let live = tx[i]; if ((live & PEND_NOINIT) == 0) { if ((live & 128) != 0) tLive(i + 1); else tAssign(i + 1); } i = txNext(i + 1); } txLen = dlPendBase; dlHookSp = -1; return false; } if (k == B_CTORS) { litSemi(); ctorAssigns(belowB(3)); dlHookSp = fb(); setA(B_BLOCK); return false; } if (tt == T_RBRACE) { dlHookSp = fb(); setA(B_BLOCK); return false; } if (kw(K_SUPER)) { let m = tb; scan(); let call = more == 0 && tt == T_LPAREN; if (!back(m)) return true; if (call) setA(B_CTORS); } return false; } // --------------------------------------------------------------------------- // Enums. Frame (the statement's): a = the N_ENUM record, b = text base, c = 1 member start / 2 after a member. // Text at base: [exported, prevClass, curClass, prevIdent, curIdent, enum name, prev member name, cur member name]. function reservedName(k: i32): boolean { if (k != 0 && k < K_FIRST_CONTEXTUAL) return true; return keywordIn(k, KS_STRICT_RESERVED); } // A name usable as a local `var`: an ASCII identifier that is not reserved in strict code. function bindable(a: i32, b: i32): boolean { if (b <= a) return false; var i = a; while (i < b) { let c = Memory.default.getU8(i + delta); let alpha = (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c == 95 || c == 36; if (!alpha && !(i > a && c >= 48 && c <= 57)) return false; i = i + 1; } if (reservedName(keywordAt(a + delta, b - a))) return false; return !wEval(a, b) && !wArguments(a, b); } export function enumStart(): void { let m = tb; let isConst = kw(K_CONST); if (isConst) scan(); if (more == 0) scan(); var ok = more == 0 && tt == T_NAME; let na = ts; let nb = te; if (ok) scan(); ok = ok && more == 0 && tt == T_LBRACE; usedN = 0; if (ok && inScope()) { let prior = nmFind(N_ENUM, dlScope, na, nb); if (prior >= 0) scanUsed(N_MEMBER, prior, false, false); } if (!back(m)) return; if (!ok) { failAt(47); return; } markValue(); let exported = dlExported; dlExported = 0; if (declared(na, nb)) { advBlank(); } else { declare(na, nb); emitTo(ts); litVar(); dropTo(ts + 3); blankRange(ts + 3, te, false); scan(); } if (isConst) advBlank(); var key = dlScope; if (!inScope()) { dlScopeNext = dlScopeNext + 1; key = -dlScopeNext; } var owner = nmFind(N_ENUM, key, ts, te); let merged = owner >= 0; if (!merged) { owner = nmAdd(N_ENUM, key, ts, te); if (nmFind(N_ENUMANY, 0, ts, te) < 0) nmAdd(N_ENUMANY, 0, ts, te); } if (exported != 0) nsExport(ts, te); let base = txLen; txPush(exported); txPush(0); txPush(0); txPush(0); txPush(0); txName(ts, te); adv(); tFn(base + 5); setNest(getNest() + 1); adv(); if (merged && usedN > 0) destructure(); setSt(S_DL_ENUM); setA(owner); setB(base); setC(1); } var cOwner: i32 = 0; var cBail: i32 = 0; // Classifier bits: 1 constant to TS's evaluator, 2 syntactically a string, 4 a known number. function clsName(a: i32, b: i32): i32 { if (nmFind(N_MEMBER, cOwner, a, b) >= 0) return 1; if (wInfinity(a, b) || wNaN(a, b)) return 5; return 0; } // Postfix operators after an operand. `n` counts them: `E.X` and `E["X"]` stay constant for a known enum E. function clsPostfix(r: i32, ent: i32): i32 { var n = 0; while (true) { if (more == 1 || cBail != 0) return 0; if (tt == T_DOT || tt == T_QDOT) { if (tt == T_QDOT) n = n + 1; scan(); if (more == 0 && tt == T_NAME) scan(); n = n + 1; continue; } if (tt == T_LBRACK) { let m = tb; scan(); let str = more == 0 && tt == T_STR; if (str) scan(); if (str && more == 0 && tt == T_RBRACK) { scan(); n = n + 1; continue; } if (more == 1) return 0; reseek(m); if (skipBalanced() < 0) return 0; n = n + 2; continue; } if (tt == T_LPAREN) { if (skipBalanced() < 0) return 0; n = n + 2; continue; } if (tt == T_TEMPLATE) { scan(); n = n + 2; continue; } if (tt == T_TEMPLATE_HEAD) { if (skipTemplate() < 0) return 0; n = n + 2; continue; } if ((tokenIn(TS_POSTFIX_UPDATE)) && tnl == 0) { scan(); n = n + 2; continue; } break; } if (n == 0) return r; if (n == 1) return ent; return 0; } function clsPrimary(): i32 { if (more == 1) return 0; if (tt == T_NUM) { scan(); return clsPostfix(5, 0); } if (tt == T_STR || tt == T_TEMPLATE) { scan(); return clsPostfix(3, 0); } if (tt == T_TEMPLATE_HEAD) { var ev = 1; while (true) { scan(); if (more == 1) return 0; let r = clsExpr(0); if ((r & 1) == 0) ev = 0; if (more == 1 || cBail != 0 || tt != T_RBRACE) { cBail = 1; return 2; } rescanTemplate(); if (more == 1) return 0; if (tt == T_TEMPLATE_TAIL) { scan(); return clsPostfix(2 | ev, 0); } } } if (tt == T_LPAREN) { scan(); let r = clsExpr(0); if (more == 1 || cBail != 0 || tt != T_RPAREN) { cBail = 1; return 0; } scan(); return clsPostfix(r, 0); } if (tt == T_NAME && (tk == 0 || tk >= K_FIRST_CONTEXTUAL)) { let a = ts; let b = te; scan(); var ent = 0; if (nmFind(N_ENUMANY, 0, a, b) >= 0) ent = 1; return clsPostfix(clsName(a, b), ent); } if (tt == T_SLASH || tt == T_SLASH_EQ) { rescanRegex(); if (more == 0) scan(); return clsPostfix(0, 0); } if (tt == T_LBRACK || tt == T_LBRACE) { if (skipBalanced() < 0) return 0; return clsPostfix(0, 0); } cBail = 1; return 0; } function clsUnary(): i32 { if (more == 1) return 0; if (tt == T_PLUS || tt == T_MINUS || tt == T_TILDE) { scan(); let r = clsUnary(); return (r & 1) * 5; } if (tt == T_BANG || tt == T_INC || tt == T_DEC || kw(K_TYPEOF) || kw(K_VOID) || kw(K_DELETE) || kw(K_AWAIT) || kw(K_NEW)) { scan(); clsUnary(); return 0; } if (tt == T_LT) { cBail = 1; return 0; } return clsPrimary(); } function evalOp(op: i32): boolean { return (op >= T_SHL && op <= T_STARSTAR) || op == T_BAR || op == T_CARET || op == T_AMP; } function clsExpr(minPrec: i32): i32 { var l = clsUnary(); while (more == 0 && cBail == 0) { if (tt == T_GT) { rescanGreater(); if (more == 1) return 0; } var p = 0; if (isBinop(tt)) p = precOf(tt); else if (kw(K_IN) || kw(K_INSTANCEOF)) p = 11; if (p == 0 || p < minPrec) break; let op = tt; scan(); var rp = p + 1; if (op == T_STARSTAR) rp = p; let r = clsExpr(rp); var v = 0; if ((l & 1) != 0 && (r & 1) != 0 && evalOp(op)) { v = 1; if (op != T_PLUS || (l & r & 4) != 0) v = 5; } if (op == T_PLUS && ((l | r) & 2) != 0) v = v | 2; l = v; } return l; } // The current token starts an initializer; read-only, the caller restores the lexer. function classify(owner: i32): i32 { cOwner = owner; cBail = 0; var r = clsExpr(0); if (cBail != 0) r = r & 2; else if (tt != T_COMMA && tt != T_RBRACE) r = 0; if ((r & 2) != 0) return C_STR; if ((r & 5) == 5) return C_NUM; if ((r & 1) != 0) return C_EVAL; return C_OPAQUE; } export function enumStep(): void { let base = fb(); let enumEnd = txNext(base + 5); if (fc() == 2) { enumTail(base, enumEnd); return; } if (tt == T_RBRACE) { setNest(getNest() - 1); adv(); if (tx[base] != 0) tCallX(base + 5); else tCall(base + 5); txLen = base; pop(); return; } // kind: 1 identifier kept in place, 2 identifier spelled as a string, 3 other literal name, // 4 reserved identifier, 5 computed `[literal]`. let m = tb; var kind = 0; var na = ts; var nb = te; if (tt == T_NAME) { if (reservedName(tk) || wEval(ts, te) || wArguments(ts, te)) kind = 4; else kind = 1; } else if (tt == T_STR) { if (bindable(ts + 1, te - 1)) kind = 2; else kind = 3; } else if (tt == T_NUM) { kind = 3; } var nameEnd = te; if (tt == T_LBRACK) { let open = tb; scan(); if (more == 0 && (tt == T_STR || tt == T_TEMPLATE)) { na = ts; nb = te; scan(); } if (more == 0 && tt == T_RBRACK) { kind = 5; nameEnd = te; scan(); } else if (more == 0) { // Not a literal, which TS rejects: the key expression is copied as it is. reseek(open); na = te; nameEnd = skipBalanced(); nb = nameEnd - 1; if (nameEnd >= 0) kind = 5; } } else if (more == 0) { scan(); } let hasInit = more == 0 && tt == T_EQ; var cls = C_NUM; if (hasInit) { scan(); if (more == 0) cls = classify(fa()); } if (!back(m)) return; if (kind == 0) { failAt(48); return; } let owner = fa(); let prevClass = tx[base + 1]; var cur = 0; if (kind == 1) { nmAdd(N_MEMBER, owner, ts, te); cur = txName(ts, te); adv(); } else if (kind == 2) { nmAdd(N_MEMBER, owner, ts + 1, te - 1); cur = txName(ts + 1, te - 1); emitTo(ts); tName(cur); blankRange(ts, te, false); scan(); } else { if (kind == 4) cur = txQuoted(na, nb); else cur = txName(na, nb); emitTo(ts); tThisIdx(cur); blankRange(ts, nameEnd, false); seek(nameEnd); scan(); } tx[base + 4] = 0; if (kind <= 2) tx[base + 4] = 1; setC(2); if (hasInit) { tx[base + 2] = cls; adv(); pushExpr(0, 2); return; } tx[base + 2] = C_NUM; if (prevClass == 0) { litAuto0(); } else if (prevClass == C_NUM || prevClass == C_EVAL) { if (tx[base + 3] != 0) tAutoI(enumEnd); else tAutoN(enumEnd); } else { litAutoVoid(); } } function enumTail(base: i32, enumEnd: i32): void { if (tt != T_COMMA && tt != T_RBRACE) { failAt(49); return; } let cls = tx[base + 2]; let ident = tx[base + 4]; var cur = enumEnd; if (tx[base + 1] != 0) cur = txNext(enumEnd); if (ident != 0) { if (cls == C_STR) tEiStr(cur); else if (cls == C_EVAL) tEiEval(cur); else tEiNum(cur); } else { if (cls == C_STR) litSemi(); else if (cls == C_EVAL) tEnEval(cur); else tEnNum(cur); } if (cur != enumEnd) { let len = tx[cur] + 1; var i = 0; while (i < len) { tx[enumEnd + i] = tx[cur + i]; i = i + 1; } } txLen = txNext(enumEnd); tx[base + 1] = cls; tx[base + 3] = ident; setC(1); if (tt == T_COMMA) advBlank(); } // --------------------------------------------------------------------------- // Namespaces. The S_NAMESPACE frame has b = 3 and c = text base: [parent scope, exported, name count, parent // record, first name start, first name end, name table mark, own start (UTF-16 units), depth-0 // module/global augmentation count, names...]. // The header tokens were verified by stmtNamespace's lookahead; `lead` is 1 after `export`. // Declines a `module N {}`/`export module N {}` statement that reached this function's own fast path // (stmtNamespace's own fast-path condition, engine/strip.zena, doesn't exclude the `module` keyword — see // namespaceStart's own call site below for why), matching erase mode's unconditional "module N {} is // always an error and left untouched" rule for the identical shape (ts-blank-space's own rule for any // `ModuleDeclaration` not itself flagged `Namespace`, confirmed in its `visitModule`) — one error at the // statement's own start, and the statement's full original text, untouched. Cursor is at the `module` // keyword itself (`m0` marks the statement's own true start, before any `export`). Returns false, leaving // the cursor exactly where it started, without committing anything, only when more input is needed — the // caller then falls through to its own (never reached in practice: declare is excluded by the fast path, // and `declineModuleKeyword` only exists for non-declare statements, see note above) namespace parsing, // which will retry this whole call from scratch once more input arrives, same as every other downlevel hook. function declineModuleKeyword(m0: i32, lead: i32): boolean { while (more == 0 && tt != T_LBRACE) scan(); if (more == 1) { reseek(m0); return false; } let end = skipBalanced(); if (end < 0) { reseek(m0); return false; } reseek(m0); releaseDecorators(); errorNamespaceFallback(getStmtUnits(), getStmtUnits() + 1); copyThrough(end); pop(); return true; } export function namespaceStart(start: i32, lead: i32): void { let exported = lead == 1 && inNs(); // Find the earlier declaration this one merges with, if any, and the exports of it the body names. let m0 = tb; if (lead == 1) scan(); // stmtNamespace's own fast-path condition (engine/strip.zena) deliberately doesn't exclude the `module` // keyword (as opposed to `namespace`) — doing so there would cost erase mode a few bytes for a boolean // it never uses, since `mode` is a genuine runtime parameter to `init`, never provably constant there. // Checked here instead, at zero cost to erase mode (this whole file is out of its build): `module N {}` // is unconditionally declined by ts-blank-space (confirmed in its `visitModule`) regardless of content, // the same as `isModule && !isString`'s own branch just below this call's own site in stmtNamespace — // matched exactly by `declineModuleKeyword` instead of ever starting this function's own rewriting. if (kw(K_MODULE) && declineModuleKeyword(m0, lead)) return; scan(); var kind0 = N_NS; var key0 = dlScope; if (exported) { kind0 = N_NSX; key0 = dlNsRec; } var prior = nmFind(kind0, key0, ts, te); // `lastFound`/`lastFoundDepth` track the deepest existing ancestor along the dotted chain even once // `prior` itself falls off it (the chain's own full path doesn't pre-exist, but a shallower prefix of // it does — "namespace A.B.C {}" reopening "A.B" while "A.B.C" itself is new): depth 0 is the outermost, // non-dotted segment. When the full path *does* pre-exist, `lastFound`/`lastFoundDepth` just end up // equal to `prior`/the total depth, so this subsumes the single-level case without changing it. var lastFound = prior; var lastFoundDepth = 0; var totalDepth = 0; scan(); while (more == 0 && tt == T_DOT) { scan(); totalDepth = totalDepth + 1; if (prior >= 0) { prior = nmFind(N_NSX, prior, ts, te); if (prior >= 0) { lastFound = prior; lastFoundDepth = totalDepth; } } scan(); } usedN = 0; declSpanN = 0; var moduleOrGlobalN = 0; if (more == 0) { let mBody = tb; declSpanScan(); moduleOrGlobalN = declSpanModuleOrGlobalN; // Only reseek back to the body's own `{` when declSpanScan actually reached its matching close (more == 0 // still): if it ran out of input instead, leave `more` set so the single combined back(m0) below reports // "need more input" the same way it already does for scanUsed, and this whole call gets retried from m0 // once more input arrives — not from mBody, which was never itself mark-protected. if (more == 0) reseek(mBody); } if (more == 0 && lastFound >= 0) scanUsed(N_NSEXP, lastFound, true, false); ambDestrDepth = -1; if (lastFoundDepth < totalDepth && usedN > 0) ambDestrDepth = lastFoundDepth; let ownUnits = getStmtUnits(); if (!back(m0)) return; releaseDecorators(); anchorPushAt(start, ANCH_IN | ANCH_SEMI); setSt(S_NAMESPACE); setB(3); let base = txLen; setC(base); txPush(dlScope); txPush(0); txPush(0); txPush(dlNsRec); let m = tb; if (lead == 1) scan(); scan(); let na = ts; let nb = te; back(m); let merges = declared(na, nb); txPush(na); txPush(nb); txPush(0); txPush(ownUnits); txPush(moduleOrGlobalN); if (lead == 1) { // `advQuiet`, not `adv`, for the same reason as importEquals's own leading `export`: this is the // statement's leading token, not its last. Latent here (never yet exercised, since a namespace body // always ends in `}`, matching `semiNeeded`'s stale value either way) but fixed for the same root // cause as importeq-asi-leak-onto-declare (corpus/downlevel.jsonl). if (exported || merges) advBlank(); else advQuiet(); if (exported) tx[base + 1] = 1; } if (merges) { advBlank(); } else { emitTo(ts); litVar(); dropTo(ts + 3); blankRange(ts + 3, te, false); scan(); } var kind = N_NS; var key = dlScope; if (exported) { kind = N_NSX; key = dlNsRec; nsExport(ts, te); } var rec = nmFind(kind, key, ts, te); var merged = rec >= 0; if (!merged) rec = nmAdd(kind, key, ts, te); var off = txName(ts, te); tx[base + 2] = 1; let savedLen = txLen; var fnOff = off; if (declSpanHasEntry(off)) { var n = 1; while (true) { let cand = txSuffixed(ts, te, n); if (!declSpanHasEntry(cand)) { fnOff = cand; break; } txLen = cand; n = n + 1; } } adv(); tFn(fnOff); txLen = savedLen; var dottedDepth = 0; var prevOff = fnOff; while (tt == T_DOT) { advBlank(); off = txName(ts, te); dottedDepth = dottedDepth + 1; tx[base + 2] = tx[base + 2] + 1; let inner = nmFind(N_NSX, rec, ts, te); merged = inner >= 0; if (merged) rec = inner; else rec = nmAdd(N_NSX, rec, ts, te); emitTo(ts); // `litLb` closes the *previous* segment's own signature (completing its "(function(name)" into // "(function(name){") — the one point where a statement can be inserted into that ancestor's own // scope, before this dot's `var @;(function(@)` (tDotFn) opens the next one. `ambDestrDepth` (set by // this call's own lookahead, above) names which ancestor, by depth, needs its already-merged exports // destructured as locals here, so a deeper, newly-opened segment's body can still reference them bare // (`namespace A.B.C {}` reopening `A.B`'s `B` while `A.B.C` itself is new); `prevOff` is that // ancestor's own emitted name. litLb(); if (dottedDepth - 1 == ambDestrDepth) destructureAs(prevOff); tDotFn(off); blankRange(ts, te, false); scan(); prevOff = off; } dlScopeNext = dlScopeNext + 1; dlScope = dlScopeNext; dlNsRec = rec; tx[base + 6] = nmLen; openBlock(B_NAMESPACE); if (merged && usedN > 0) destructure(); } export function namespaceEnd(values: i32): void { let base = fc(); // Two independent ways this namespace's own `values > 0` (strip.zena's shared, unmodified counter) can // be entirely explained by something this engine can't safely wrap in an IIFE, rather than real content: // // - Every mark came from one of this body's own depth-0 `module`/`global` augmentations // (`tx[base + 8]`, counted independently by this same lookahead's `declSpanScan`, against the *exact // same* `values` strip.zena's shared, unconditional `markValue` call for one of these built — see // declSpanScan's own comment). `values <= tx[base + 8]` (not `==`: a real statement that happened to // mark more than once would only make `values` larger, correctly falling through to the wrap branch // below, never mistaken for "explained entirely by augmentations") catches exactly our 4 fixed files // (moduleAugmentationGlobal7/7_1/8/8_1) without strip.zena's own shared code needing to distinguish // this from real content at all — that would cost erase mode bytes for a signal it never reads. // - A nested *namespace* (not a module/global augmentation directly in this body, which the bullet // above already covers) itself declined below and restored its own span to literal, un-erased source // text (`declineScope`, propagated into the parent scope right after `dlScope` is restored to it) — // `scopeDeclined` reads that back now that `dlScope` (still this namespace's own scope; the restore // to the parent's, `tx[base]`, happens further down) names it. // // Either way wins over real content elsewhere in the body, not just "neither is true": wrapping in an // IIFE is only safe once everything nested is known to erase to *something* (even just blank spaces), // and a decline leaves literal, un-erased source text sitting in this body instead — embedding that // inside a wrapping IIFE would emit invalid JS. See corpus/downlevel.jsonl, // "namespace-uninstantiated-ambient-only-wrapped". let declined = scopeDeclined(dlScope) || (values > 0 && values <= tx[base + 8]); if (values > 0 && !declined) { let count = tx[base + 2]; var i = count - 1; while (i >= 0) { var off = base + 9; var j = 0; while (j < i) { off = txNext(off); j = j + 1; } if (i < count - 1) litRb(); if (i > 0 || tx[base + 1] != 0) tCallX(off); else tCall(off); i = i - 1; } // Drop only the namespace/module fallback-collapse errors raised while this body was open (a // nested statement that couldn't fast-path itself, e.g. `public namespace X {}`'s own illegal // modifier — see errorNamespaceFallback, engine/strip.zena) — not every error, so an ordinary // unhandled construct inside this namespace (its own erasure declined, same as outside any // namespace) still gets reported instead of silently producing invalid, unflagged output. pruneNamespaceFallbacks(anchorErr()); prune(tx[base + 6]); dlScope = tx[base]; // Only an instantiated namespace binds its name; the input is still retained by the anchor. declare(tx[base + 4], tx[base + 5]); anchorPop(); markValue(); } else { prune(tx[base + 6]); restoreFrom(anchorOut(), anchorIn()); setErrLen(anchorErr()); if (declined) { // Match erase mode's own decline for the identical shape (a plain, non-fast-pathed namespace whose // "resolved by its own body" check, `hNamespace`'s `kind == 0` branch, hits the same unconditional // mark): restore this namespace's own literal original text, report one error at its own start // (`errorNamespaceFallback`, tagged the same way so an enclosing namespace that itself fast-pathed can // prune it rather than double-report), and propagate the decline — both to `values` (`markValue`, so // an enclosing namespace that *didn't* fast-path itself, `hNamespace`'s shared, mode-independent // `kind != 3` paths, still sees "has a value") and to the restored parent scope (`declineScope`, so an // enclosing namespace that *did* fast-path itself also declines rather than wrapping this still // literal, un-erased text inside its own IIFE). anchorPop(); markValue(); errorNamespaceFallback(tx[base + 7], tx[base + 7] + 1); declineScope(tx[base]); } else { anchorBlank(); } } dlScope = tx[base]; dlNsRec = tx[base + 3]; txLen = base; pop(); } // --------------------------------------------------------------------------- // Declarations inside a namespace body, and declaration names for merging. // The function head loop of stmtFunction: `n` tokens through the name. export function functionHead(n: i32): void { let exp = dlExported; dlExported = 0; let scoped = inScope(); var i = 0; while (i < n) { if (i == n - 1 && tt == T_NAME && !kw(K_FUNCTION)) { if (scoped) declare(ts, te); if (exp != 0) pendAdd(0, ts, te); } if (exp != 0 && kw(K_EXPORT)) advBlank(); else adv(); i = i + 1; } } // At a class declaration's name. export function className(): void { let exp = dlExported; dlExported = 0; if (getBlankDepth() > 0) return; declare(ts, te); if (exp != 0) pendAdd(0, ts, te); } // S_VARDECL flags for `var`/`let`/`const` k: 64 exported from a namespace, 128 also a live binding. export function varFlags(k: i32): i32 { if (dlExported == 0) return 0; dlExported = 0; if (k == K_CONST) return 64; return 192; } var bindA: FixedArray = new FixedArray(32, 0); var bindN: i32 = 0; function bindAdd(a: i32, b: i32): void { bindA = grow(bindA, bindN + 2); bindA[bindN] = a; bindA[bindN + 1] = b; bindN = bindN + 2; } // Skips a default value through the `,` or closer that ends it. function skipDefault(): void { while (more == 0 && tt != T_COMMA && tt != T_RBRACE && tt != T_RBRACK) { if (tokenIn(TS_OPEN)) { if (skipBalanced() < 0) return; } else if (tt == T_TEMPLATE_HEAD) { if (skipTemplate() < 0) return; } else { scan(); } } } function bindPattern(): void { if (more == 1) return; if (tt == T_NAME) { bindAdd(ts, te); scan(); return; } if (tt == T_LBRACE) { scan(); while (more == 0 && tt != T_RBRACE) { if (tt == T_ELLIPSIS) { scan(); bindPattern(); } else { let a = ts; let b = te; let isName = tt == T_NAME; if (tt == T_LBRACK) { if (skipBalanced() < 0) return; } else { scan(); } if (more == 0 && tt == T_COLON) { scan(); bindPattern(); } else if (isName) { bindAdd(a, b); } if (more == 0 && tt == T_EQ) { scan(); skipDefault(); } } if (more == 0 && tt == T_COMMA) scan(); } if (more == 0) scan(); return; } if (tt == T_LBRACK) { scan(); while (more == 0 && tt != T_RBRACK) { if (tt == T_COMMA) { scan(); continue; } if (tt == T_ELLIPSIS) scan(); bindPattern(); if (more == 0 && tt == T_EQ) { scan(); skipDefault(); } if (more == 0 && tt == T_COMMA) scan(); } if (more == 0) scan(); } } // At a declarator of an exported namespace variable: queues its bound names. True when input ran out. export function declNames(flags: i32): boolean { let m = tb; let simple = tt == T_NAME; bindN = 0; bindPattern(); if (!back(m)) return true; var hasInit = true; if (simple) { scan(); if (more == 0 && tt == T_COLON) { let end = lookType(); if (more == 0 && end >= 0) reseek(end); } if (more == 1) { seek(m); return true; } hasInit = tt == T_EQ; if (!back(m)) return true; } var pf = flags & 128; if (!hasInit && simple && bindN == 2 && dlNsRec != 0) { // Only a member that's never referenced again stays unmaterialized: `tsc` rewrites every later // reference (read or write) to go through the namespace object directly, so a later assignment // (`x = 1;` elsewhere in this body) does still need the member to exist. scanUsed's own depth // tracking is relative, not tied to starting right at the body's `{`, so it still stops at this // same body's own closing `}` even starting mid-body, here. nsExport(bindA[0], bindA[1]); let r = nmFind(N_NSEXP, dlNsRec, bindA[0], bindA[1]); if (r >= 0) { let m2 = tb; scanUsed(N_NSEXP, dlNsRec, false, true); var referenced = false; var j = 0; while (j < usedN) { if (used[j] == r) { referenced = true; break; } j = j + 1; } if (!back(m2)) return true; if (!referenced) pf = pf | PEND_NOINIT; } } var i = 0; while (i < bindN) { pendAdd(pf, bindA[i], bindA[i + 1]); i = i + 2; } return false; } // --------------------------------------------------------------------------- // `export declare var/let/const [: T][, [: T]]*;` directly inside an instantiated namespace body. // An ambient (declare) member has no runtime value of its own: nothing in the namespace body initializes it. // `tsc`'s checker still knows it's a member of the enclosing namespace, so every later bare reference to the // name gets qualified through the namespace object at emit time: a read gets `undefined` until something // writes it, and a write (from inside the body, from another merged declaration, or — the common case for an // ambient member, since the whole point of `declare` is "provided from outside" — from completely unrelated // code after the namespace closes, e.g. `(N as any).x = 5`) lands on the namespace object itself. Downlevel mode has // no checker and no general identifier-rewriting pass over the body, and can't see a write from outside the // body at all, so a plain disconnected local (this code's first attempt) silently reads stale/undefined // forever once something writes the real member from outside — a *silent wrong value*, worse than the // ReferenceError erasing the whole declaration used to throw. // // Instead, each exported name gets `declNames`/`varFlags`' own live-binding accessor machinery, but starting // NON-enumerable: `tAmbientLive` (like `tLive`, its getter/setter close over a real local so a bare reference // inside the body and a `this.` access from outside always see the same value) and, the first time // anything actually writes it — through `this`, whether from inside the body or completely outside it — its // own setter flips the SAME property to enumerable with a second, get/set-preserving `defineProperty` call. // `tsc` only ever creates the member once something assigns it; this matches that without knowing in advance // whether a write is coming, so a namespace whose ambient member genuinely never gets written (every file in // the corpus so far) stays correctly absent from `Object.keys`/structural comparison — not reintroducing // "namespace-uninitialized-export-overcounted"'s own over-eager materialization. The one gap this doesn't // close: a write *inside* the namespace body as a bare, unqualified assignment (`x = 5;`, not `this.x = 5`) // updates the closure variable directly, bypassing the setter, so it doesn't flip the property enumerable — // not exercised by any known case (the 13 conformance files in the baseline never write the ambient member at // all, in or out of the body); `tsc` itself rewrites every such in-body reference to go through the namespace // object too, which this architecture has no general way to do for an arbitrary expression position. // // A non-exported `declare var` is left on the ordinary erasure path below: `tsc` never qualifies a reference // to one (confirmed directly — it isn't a namespace member at all, just a free identifier left bare in the // reference's own output too), so downlevel mode's existing unqualified erasure already matches it. // // Returns 0 when this doesn't apply (not directly inside a namespace body, or not exported) — the caller falls // back to its own unqualified erasure; 1 when handled; 2 when input ran out (nothing committed; the caller // retries the whole statement once more input arrives). export function ambientVar(n: i32): i32 { if (!inNs()) return 0; let m = tb; var exported = false; var i = 0; while (i < n) { if (kw(K_EXPORT)) exported = true; scan(); i = i + 1; } if (!exported) { reseek(m); return 0; } if (more == 1) { seek(m); return 2; } // `tt` is the var/let/const keyword; move past it to the first declarator name. scan(); while (true) { if (more == 1) { seek(m); return 2; } if (tt != T_NAME) { reseek(m); return 0; } scan(); if (more == 1) { seek(m); return 2; } if (tt == T_COLON) { let end = lookType(); if (more == 1) { seek(m); return 2; } if (end < 0) { reseek(m); return 0; } reseek(end); if (more == 1) { seek(m); return 2; } } if (tt == T_COMMA) { scan(); continue; } break; } if (!back(m)) return 2; // Committing: the same walk, now with real output. Every declarator name is kept verbatim as a real local // (so a bare reference inside the body resolves to it); everything else (modifiers, the keyword, each type // annotation) is blanked. Each name's lazy-enumerable accessor is queued (as a [nameOff] entry in tx, // starting at regBase) rather than emitted inline, since they can only go after the whole `var a,b;` // statement — interleaving them between comma-separated names would not be valid JS. markValue(); var j = 0; while (j < n) { advBlank(); j = j + 1; } emitTo(ts); litVar(); dropTo(ts + 3); blankRange(ts + 3, te, false); scan(); let regBase = txLen; while (true) { txName(ts, te); adv(); if (tt == T_COLON) { let m2 = tb; let end = lookType(); back(m2); blankRange(ts, end, false); seek(end); scan(); } if (tt == T_COMMA) { adv(); continue; } break; } if (tt == T_SEMI) adv(); var o = regBase; while (o < txLen) { tAmbientLive(o); o = txNext(o); } txLen = regBase; pop(); return 1; } // --------------------------------------------------------------------------- // Module-kind tracking: a file mixes CommonJS-only syntax (`import x = require(...)`, // `export =`) with value-level ESM (`import`/`export` surviving erasure) can run in // neither module system (new ESM `import`/`export` alongside emitted `require`/ // `module.exports`), so downlevel must report instead of silently emitting it. See // README.md's downlevel-mode section and docs/downlevel-oracle.md for the contract. // // Three flags, reset by init() per file/stream: mkEsm/mkCjs record "has this kind been // committed to" (converted, or marked ESM, without reporting), mixed records "do we // already know this file is mixed" — once true it stays true for the rest of the file. // strip.zena calls moduleEsmSeen() at each value-level ESM import/export declaration // (its own classification already matches tsc's elision rules: `import type`/`export // type`/an all-type-only clause never reaches that call site) gated by its own // blankDepth==0 && verbatim==0, so ambient content (`declare module`/`declare global`, // entered via enterBlank) never calls it, matching the "nothing ambient counts" rule. // moduleCjsSeen() is called the same way, but only for a CommonJS-only construct itself // (`import x = require(...)`, `export =`); the exact call sites are importEquals() below // (only its `require(...)` form — `import x = A.B`, a plain namespace alias, is neither // CommonJS nor gated here) and strip.zena's stmtExport (every `export =`). // // Streaming means the engine can't always know a file is mixed before it has already // converted (and emitted) an earlier `import x =`/`export =`: that output can't be // un-emitted. The chosen fix is to report the *later* conflicting construct instead of // buffering: the first construct that reveals the mix is reported in place of (not // instead of fixing) whichever already-streamed construct it conflicts with, and once // `mixed` is true every further CommonJS-only construct is also reported (the normal // rule applies again once there is no more streaming ambiguity) while further ESM // declarations stay silent (only the one that revealed the mix is ever reported — the // contract only asks for CommonJS-only constructs to be flagged). This keeps the engine // fully streaming (no buffering, no replay) while guaranteeing at least one error for // any mixed file regardless of which kind comes first. // Called at a value-level ESM import/export declaration (one that survives type // erasure). Returns true exactly once per file, the first time a CommonJS-only construct // was already committed (mkCjs) when this ESM declaration is reached: the caller must // then report *this* declaration's own position instead of marking it as ESM, since the // earlier CommonJS construct is already converted and streamed. Returns false (and, the // first time, records the file as ESM-bearing) every other time, including every call // once `mixed` is already true — only the one ESM declaration that first reveals the mix // is ever reported. // // strip.zena's own call site (stmtDecl's "export as a plain modifier" branch) is reached // for both a true top-level `export class`/`function`/`enum`/`var`/`let`/`const` and the // exact same shape *inside* a namespace body (`namespace N { export function f() {} }`), // since both go through the same statement dispatch. Only the former is a real top-level // ESM module declaration; the latter is just a namespace member export (the namespace // merges it into its own object; it never changes whether the *file* is ESM or // CommonJS), so it must not count here. Checked with getSp()/belowSt() — already imported // from strip.zena for inScope()/inNs() above — rather than a parameter strip.zena itself // would have to compute and pass at every one of its own (simpler) call sites, to keep // those call sites themselves exactly as cheap as every other hooks-seam call (see // engine/hooks-erase.zena: this file is never linked into build/erase.wasm at all, so the // check costs nothing there either way, but computing it in strip.zena *would* have). export function moduleEsmSeen(): boolean { if (getSp() >= 2 && belowSt(1) != S_TOP) return false; if (mixed) return false; if (mkCjs) { mixed = true; return true; } mkEsm = true; return false; } // Called before converting an `import x = require(...)`/`export =`. Returns true when // this file is already known mixed, or becomes known mixed right now (an ESM declaration // was already committed): the caller must report this construct at its own statement // start and leave it unconverted (mirroring erase mode's own fallback) instead of // converting it. Returns false (and records the file as CommonJS-bearing) otherwise, // letting the caller proceed with the conversion. export function moduleCjsSeen(): boolean { if (mixed) return true; if (mkEsm) { mixed = true; return true; } mkCjs = true; return false; } // --------------------------------------------------------------------------- // `import x = require('y')`, `import x = A.B`, `export = e`. export function importEquals(): void { let m = tb; let exp = kw(K_EXPORT); if (exp) scan(); if (more == 0) scan(); var isType = false; if (more == 0 && kw(K_TYPE)) { let m2 = tb; scan(); isType = more == 0 && tt == T_NAME; if (!isType && more == 0) reseek(m2); } let na = ts; let nb = te; if (more == 0) scan(); if (more == 0 && tt == T_EQ) scan(); let isRequire = more == 0 && kw(K_REQUIRE); var end = -1; if (more == 0) { reseek(m); if (more == 0) end = importEqualsEnd(); } if (!back(m)) return; if (end < 0) { failAt(50); return; } if (isType) { enterBlank(true); copyThrough(end); endStmt(); return; } // `import x = require(...)` is the only `import x =` form that is itself a CommonJS-only construct // (needsCommonJS's own rule, test/downlevel.mjs): a plain namespace alias (`import x = A.B`) is unaffected // by module-kind tracking either way. See "Module-kind tracking" above moduleEsmSeen/moduleCjsSeen. if (isRequire && moduleCjsSeen()) { errorAtStmt(); copyThrough(end); pop(); return; } let ns = inNs(); if (exp) { // `advQuiet`, not `adv`: this `export` is the statement's leading token, not its last, so it must // not touch `semiNeeded` — doing so (via `adv`) left a stale value for the *next* statement to read, // leaking a spurious `;` onto a following blanked `declare namespace`/`declare enum` whenever this // statement's own real last token (copied through `copyThrough` below, which never touches // `semiNeeded` either) was already a semicolon. if (ns) advBlank(); else advQuiet(); } emitTo(ts); if (isRequire) { litConst(); dropTo(ts + 5); blankRange(ts + 5, te, false); } else { litVar(); dropTo(ts + 3); blankRange(ts + 3, te, false); } scan(); if (exp && ns) pendAdd(0, na, nb); copyThrough(end); pop(); } export function exportAssign(): void { markValue(); emitTo(ts); litModExp(); dropTo(te); scan(); setSt(S_EXPRSTMT); setC(0); adv(); } // --------------------------------------------------------------------------- // Parameter properties. A class body anchors its `{` so the fields can be inserted there once the constructor's // parameters are known; the anchor is released at the constructor body or the class end. export function classOpen(ext: i32): void { var a = DL_CLS | (ext << 20); if (getBlankDepth() == 0 && getVerbatim() == 0) { anchorPush(0); a = a | getAnchLen(); } pushFrame(S_CLASSBODY, a, txLen, getStmtNest()); } export function classEnd(): void { let a = fa(); let ai = a & 1048575; if (ai != 0 && ai == getAnchLen()) anchorPop(); txLen = fb(); } // The current token names a constructor parameter that had modifiers. export function paramProp(): void { if (tt == T_NAME) txName(ts, te); } function ctorAssigns(base: i32): void { var o = base; while (o < txLen) { tAssign(o); o = txNext(o); } } // Right after a constructor body's `{`: stack ... S_CLASSBODY, S_MEMBER, S_FUNC, S_BLOCK. export function ctorBody(): void { if (getSp() < 4 || belowSt(3) != S_CLASSBODY) return; let a = belowA(3); if ((a & DL_CLS) == 0) return; let base = belowB(3); let ai = a & 1048575; if (ai != 0) { if (ai == getAnchLen()) { var n = 0; var o = base; while (o < txLen) { n = n + tx[o] + 1; o = txNext(o); } if (n > 0) { var d = outInsert(anchorOut(), n); o = base; while (o < txLen) { var j = 0; while (j < tx[o]) { Memory.default.setU8(d, tx[o + 1 + j]); d = d + 1; j = j + 1; } Memory.default.setU8(d, 59); d = d + 1; o = txNext(o); } } anchorPop(); } setBelowA(3, a & -1048576); } if (getBlankDepth() != 0 || getVerbatim() != 0 || txLen == base) return; if (((a >> 20) & 1) == 0) { ctorAssigns(base); return; } setA(B_CTORW); setB(dlHookSp); dlHookSp = getSp(); } // strip.zena's own F_RESTRICT/F_ARROWBODY/F_STMTSTART, masked into ctxFlags below the same way the other // strip.zena values at the top of this file are (module-level bindings cannot cross the import cycle). let F_RESTRICT: i32 = 128; let F_ARROWBODY: i32 = 256; let F_STMTSTART: i32 = 512; // genericArrow's own erase/report/abort decision for a legacy `x` cast, called only when mode == 1 (strip.zena // checks that; this hook is never reached otherwise). `m` is the `<` token's own start, `end` is the cast's own // end (both already known to genericArrow); `ctxFlags` is `flags` masked to F_RESTRICT | F_ARROWBODY | // F_STMTSTART. Returns 0 (report, exactly as erase mode always does), 1 (safe to erase), or 2 (abort: // genericArrow must return having done nothing further - mirrors the lexer running out of input, or `back` // itself failing, while peeking the operand). export function castErase(m: i32, end: i32, ctxFlags: i32): i32 { if (ctxFlags == 0) return 1; // A pure lookahead past `end` to the operand's own start and first token, with no output effect either // way; back(m) always restores the lexer to `<` before genericArrow does anything else. reseek(end); if (more == 1) { back(m); return 2; } let opStart = ts; let opTt = tt; let opTk = tk; if (!back(m)) return 2; if ((ctxFlags & F_RESTRICT) != 0 && spansLines(m, opStart)) return 0; if ((ctxFlags & F_ARROWBODY) != 0 && opTt == T_LBRACE) return 0; if ((ctxFlags & F_STMTSTART) != 0 && (opTt == T_LBRACE || (opTt == T_NAME && (opTk == K_FUNCTION || opTk == K_CLASS)))) { return 0; } return 1; }