From f6c3d030a80e2c827eb9686fc2125f52eded8608 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 13 Oct 2026 17:23:10 -0500 Subject: [PATCH] Run module initializers that only ++ and -- read --- .../deferred-module-initializers.test.ts | 30 +++++++++ .../lowering/deferred-module-initializers.ts | 12 +++- ...5065-global-increment-initializers.ts.json | 6 ++ .../5066-global-compound-updates.ts.json | 6 ++ .../5067-global-updates-in-functions.ts.json | 6 ++ .../5068-global-var-let-const-updates.ts.json | 6 ++ .../5069-global-update-modules/main.ts.json | 8 +++ .../5065-global-increment-initializers.ts | 48 ++++++++++++++ tests/corpus/5066-global-compound-updates.ts | 60 ++++++++++++++++++ .../5067-global-updates-in-functions.ts | 63 +++++++++++++++++++ .../5068-global-var-let-const-updates.ts | 44 +++++++++++++ .../5069-global-update-modules/counter.ts | 20 ++++++ .../corpus/5069-global-update-modules/main.ts | 5 ++ .../5069-global-update-modules/reexport.ts | 5 ++ 14 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5065-global-increment-initializers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5066-global-compound-updates.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5067-global-updates-in-functions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5068-global-var-let-const-updates.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5069-global-update-modules/main.ts.json create mode 100644 tests/corpus/5065-global-increment-initializers.ts create mode 100644 tests/corpus/5066-global-compound-updates.ts create mode 100644 tests/corpus/5067-global-updates-in-functions.ts create mode 100644 tests/corpus/5068-global-var-let-const-updates.ts create mode 100644 tests/corpus/5069-global-update-modules/counter.ts create mode 100644 tests/corpus/5069-global-update-modules/main.ts create mode 100644 tests/corpus/5069-global-update-modules/reexport.ts diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts index f392b02c7..53d6bac87 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts @@ -138,3 +138,33 @@ test("fills every empty copy with the exact file and span while preserving popul expect(populated.body).toEqual([value(1)]); for (const untouched of [otherFile, otherStart, otherEnd]) expect(untouched.body).toEqual([]); }); + +test("an update of the binding demands its initializer", () => { + const { initializers, pending, lowerer } = fixture(); + const updated = block(loc), + written = block({ ...loc, start: 2, end: 3 }); + const update = (id: string, prefix: boolean): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "incDec", op: "+", prefix, localId: id, type: F64, loc }, + loc, + }); + const functions = [ + fn("init", [updated, written]), + fn("use", [ + update("updated", false), + { + kind: "assign", + localId: "written", + value: { kind: "numLit", value: 1, type: F64, loc }, + loc, + }, + ]), + ]; + pending.set("updated", [{ loc, lower: () => [value(2)] }]); + pending.set("written", [{ loc: written.loc, lower: () => [value(3)] }]); + expect(initializers.process(lowerer, functions)).toBe(true); + expect(updated.body).toEqual([value(2)]); + // A plain write never observes the initial value. + expect(written.body).toEqual([]); + expect(pending.has("written")).toBe(true); +}); diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index 97760f497..fb7bc7abc 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -1,6 +1,6 @@ import * as ts from "../ts7/adapter.js"; import { everyStmtList } from "../../ir/traverse.js"; -import type { IrFunction, IrStmt, IrType } from "../../ir/ir.js"; +import type { IrExpr, IrFunction, IrStmt, IrType } from "../../ir/ir.js"; import { locOf } from "../program.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; import type { Lowerer } from "./lowerer.js"; @@ -164,7 +164,7 @@ export class DeferredModuleInitializers { stmt: () => true, expr: (expr) => { demandType(expr.type); - if (expr.kind === "varRef" && this.pending.has(expr.localId)) demanded.add(expr.localId); + if (readsBinding(expr) && this.pending.has(expr.localId)) demanded.add(expr.localId); if (expr.kind === "call") { const owner = classHelpers.get(expr.callee); if (owner) { @@ -186,7 +186,7 @@ export class DeferredModuleInitializers { stmt: () => true, expr: (expr) => { demandType(expr.type); - if (expr.kind === "varRef" && this.pending.has(expr.localId)) demanded.add(expr.localId); + if (readsBinding(expr) && this.pending.has(expr.localId)) demanded.add(expr.localId); if (expr.kind === "call") { const owner = classHelpers.get(expr.callee); if (owner) { @@ -241,6 +241,12 @@ export class DeferredModuleInitializers { } } +/** A read of a binding's current value: a plain read, or an update + * (`n++`, `--n`) that reads before it writes. */ +function readsBinding(expr: IrExpr): expr is IrExpr & { kind: "varRef" | "incDec" } { + return expr.kind === "varRef" || expr.kind === "incDec"; +} + function registryInitializer( lowerer: Lowerer, expression: ts.Expression, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5065-global-increment-initializers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5065-global-increment-initializers.ts.json new file mode 100644 index 000000000..398a6f8ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5065-global-increment-initializers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5065-global-increment-initializers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5066-global-compound-updates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5066-global-compound-updates.ts.json new file mode 100644 index 000000000..0122884f5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5066-global-compound-updates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5066-global-compound-updates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5067-global-updates-in-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5067-global-updates-in-functions.ts.json new file mode 100644 index 000000000..3363e017e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5067-global-updates-in-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5067-global-updates-in-functions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5068-global-var-let-const-updates.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5068-global-var-let-const-updates.ts.json new file mode 100644 index 000000000..56c5a280f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5068-global-var-let-const-updates.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5068-global-var-let-const-updates.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5069-global-update-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5069-global-update-modules/main.ts.json new file mode 100644 index 000000000..2975951ce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5069-global-update-modules/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/5069-global-update-modules/counter.ts", + "/tests/corpus/5069-global-update-modules/reexport.ts", + "/tests/corpus/5069-global-update-modules/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5065-global-increment-initializers.ts b/tests/corpus/5065-global-increment-initializers.ts new file mode 100644 index 000000000..bbfe7cd8f --- /dev/null +++ b/tests/corpus/5065-global-increment-initializers.ts @@ -0,0 +1,48 @@ +// Module-level bindings whose only later use is ++ or --: the initializer +// must still run before the update reads the binding. +function f(x: number): { done: boolean; v: number } { + return { done: x > 3, v: x }; +} +let tr = f(0); +let n = 2; +while (!tr.done) { + console.log(tr.v); + tr = f(n++); +} + +const id = (x: number): number => x; +let a = 2; +console.log("a++", id(a++)); +let b = 2; +console.log("b--", id(b--)); +let c = 2; +console.log("++c", id(++c)); +let d = 2; +console.log("--d", id(--d)); + +// Updates in statement position and loop conditions. +let ticks = 3; +while (ticks-- > 0) console.log("tick"); +let up = -2; +do console.log("up", up); while (++up < 1); +let steps = 10; +for (let i = 0; i < 3; i++) console.log("steps", steps--); + +// Floating-point and negative starting values. +let half = 0.5; +console.log("half++", half++); +let neg = -1; +console.log("--neg", --neg); +let big = 9007199254740991; +console.log("big++", big++); + +// BigInt updates. +let count = 10n; +console.log("count++", String(count++)); +let back = 1n; +console.log("--back", String(--back)); + +// Several declarators in one statement. +let x1 = 5, + x2 = 6; +console.log("x1++ x2--", x1++, x2--); diff --git a/tests/corpus/5066-global-compound-updates.ts b/tests/corpus/5066-global-compound-updates.ts new file mode 100644 index 000000000..f8363a4c6 --- /dev/null +++ b/tests/corpus/5066-global-compound-updates.ts @@ -0,0 +1,60 @@ +// Module-level bindings whose only later use is a compound, logical or +// destructuring update. +const show = (label: string, v: unknown): void => console.log(label, JSON.stringify(v)); +let e = 2; +show("e += 1", (e += 1)); +let f = 2; +show("f -= 3", (f -= 3)); +let g = 2; +show("g *= 5", (g *= 5)); +let h = 2; +show("h **= 3", (h **= 3)); +let i = 7; +show("i %= 4", (i %= 4)); +let j = 6; +show("j <<= 1", (j <<= 1)); +let k = 6; +show("k |= 1", (k |= 1)); +let m = 12; +show("m >>>= 2", (m >>>= 2)); +let s = "x"; +show("s += y", (s += "y")); +let t = ""; +show("t += 1", (t += 1)); + +let present: number | undefined = 4 as number | undefined; +show("present ??= 9", (present ??= 9)); +let absent: number | undefined = undefined as number | undefined; +show("absent ??= 9", (absent ??= 9)); +let label: string | null = null as string | null; +show("label ??= none", (label ??= "none")); +let zero = 0; +show("zero ||= 5", (zero ||= 5)); +let three = 3; +show("three &&= 5", (three &&= 5)); +let emptyText = ""; +show("emptyText ||= fallback", (emptyText ||= "fallback")); +let on = true; +show("on &&= false", (on &&= false)); + +let big = 3n; +show("big *= 4n", String((big *= 4n))); + +let p = 1; +let q = 10; +show("[p, q] = [q, p]", ([p, q] = [q, p])); +let first = 0; +let rest: number[] = []; +show("[first, ...rest]", ([first, ...rest] = [7, 8, 9])); +let left = "l"; +let right = "r"; +show("({ left, right } = ...)", ({ left, right } = { left: right + "!", right: left + "?" })); + +let arr = [1, 2, 3]; +show("arr[0]++", arr[0]++); +let obj = { n: 1, s: "a" }; +show("obj.n += 2", (obj.n += 2)); +show("obj.s += b", (obj.s += "b")); +let grid = [[1], [2]]; +grid[1][0] *= 10; +show("grid[1][0] *= 10", grid); diff --git a/tests/corpus/5067-global-updates-in-functions.ts b/tests/corpus/5067-global-updates-in-functions.ts new file mode 100644 index 000000000..03fecc144 --- /dev/null +++ b/tests/corpus/5067-global-updates-in-functions.ts @@ -0,0 +1,63 @@ +// Module-level bindings read only by updates inside functions, closures, +// class members, generators, async functions and callbacks. +function takeTicket(): number { + return ticket++; +} +let ticket = 100; +console.log("ticket", takeTicket(), takeTicket()); + +const nextId = (): number => ++lastId; +let lastId = 50; +console.log("lastId", nextId(), nextId()); + +let total = 1; +function accumulate(by: number): number { + return (total *= by); +} +console.log("total", accumulate(2), accumulate(3)); + +class Pool { + static take(): number { + return free--; + } + grow(by: number): number { + return (free += by); + } + get size(): number { + return ++free - 1; + } +} +let free = 7; +console.log("free", Pool.take(), new Pool().grow(10), new Pool().size); + +function* serials(): Generator { + for (let i = 0; i < 3; i++) yield serial++; +} +let serial = 1000; +console.log("serial", [...serials()].join(",")); + +async function nextAsync(): Promise { + await null; + return asyncCount++; +} +let asyncCount = 40; + +let mapped = 0; +const doubled = [1, 2, 3].map((x) => x * 2 + mapped++); +console.log("mapped", doubled.join(",")); + +function makeCounter(): () => number { + return () => shared--; +} +let shared = 3; +const c1 = makeCounter(); +const c2 = makeCounter(); +console.log("shared", c1(), c2(), c1()); + +let fallback: string | undefined = "kept" as string | undefined; +const pick = (): string => (fallback ??= "replaced"); +console.log("fallback", pick()); + +nextAsync() + .then((v) => nextAsync().then((w) => console.log("asyncCount", v, w))) + .catch(() => {}); diff --git a/tests/corpus/5068-global-var-let-const-updates.ts b/tests/corpus/5068-global-var-let-const-updates.ts new file mode 100644 index 000000000..f5f993506 --- /dev/null +++ b/tests/corpus/5068-global-var-let-const-updates.ts @@ -0,0 +1,44 @@ +// Top-level var, let and const bindings with later compound updates, +// including var hoisting. +var hits = 3; +console.log("hits++", hits++); +var level = 10; +function lower(): number { + return (level -= 4); +} +console.log("level", lower(), lower()); + +// A var updated only from a function called after its declaration. +var late = 5; +function early(): number { + return late++; +} +console.log("late", early(), early()); + +// Redeclared var keeps one binding. +var twice = 1; +var twice = twice + 1; +console.log("twice++", twice++); + +// const containers updated in place. +const counters = { a: 1, b: 2 }; +counters.a++; +console.log("counters.b *= 3", (counters.b *= 3), counters.a); +const list = [5, 6]; +console.log("list[1]--", list[1]--, list.join(",")); +const nested = { inner: { n: 0 } }; +console.log("nested.inner.n += 2", (nested.inner.n += 2)); + +// let updated in branches and loops only. +let flips = 0; +for (const v of [true, false, true]) if (v) flips++; +console.log("flips", 0); +let budget = 5; +while (budget > 0) budget -= 2; +console.log("budget", budget); +let tally = 1; +if (Math.random() < 2) tally += 41; +console.log("tally done"); +let ratio = 1; +for (let i = 0; i < 4; i++) ratio /= 2; +console.log("ratio", 1); diff --git a/tests/corpus/5069-global-update-modules/counter.ts b/tests/corpus/5069-global-update-modules/counter.ts new file mode 100644 index 000000000..593374249 --- /dev/null +++ b/tests/corpus/5069-global-update-modules/counter.ts @@ -0,0 +1,20 @@ +// Module-private bindings used only by exported functions. +let next = 10; +let calls = 0; +export function take(): number { + calls++; + return next++; +} +export function callsSoFar(): number { + return ++calls - 1; +} +var seq = 5; +export const bump = (): number => --seq; +let name = "id"; +export function label(): string { + return (name += "+"); +} +let cache: number | undefined; +export function cached(): number { + return (cache ??= 99); +} diff --git a/tests/corpus/5069-global-update-modules/main.ts b/tests/corpus/5069-global-update-modules/main.ts new file mode 100644 index 000000000..f3ffb78ac --- /dev/null +++ b/tests/corpus/5069-global-update-modules/main.ts @@ -0,0 +1,5 @@ +import { take, callsSoFar, label, cached } from "./counter.ts"; +import { bump, scaled } from "./reexport.ts"; +console.log(take(), take(), callsSoFar(), bump(), bump()); +console.log(label(), label(), cached(), cached()); +console.log(scaled(), scaled(), take()); diff --git a/tests/corpus/5069-global-update-modules/reexport.ts b/tests/corpus/5069-global-update-modules/reexport.ts new file mode 100644 index 000000000..5854ecd8b --- /dev/null +++ b/tests/corpus/5069-global-update-modules/reexport.ts @@ -0,0 +1,5 @@ +export { take, bump } from "./counter.ts"; +let local = 0.25; +export function scaled(): number { + return (local *= 2); +}