Run module initializers that only ++ and -- read

This commit is contained in:
Chris Tate
2026-10-13 17:23:10 -05:00
parent dfa9ae0f34
commit f6c3d030a8
14 changed files with 316 additions and 3 deletions
@@ -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);
});
@@ -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,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5065-global-increment-initializers.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5066-global-compound-updates.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5067-global-updates-in-functions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5068-global-var-let-const-updates.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/5069-global-update-modules/counter.ts",
"<repo>/tests/corpus/5069-global-update-modules/reexport.ts",
"<repo>/tests/corpus/5069-global-update-modules/main.ts"
],
"diags": []
}
@@ -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--);
@@ -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);
@@ -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<number, void, undefined> {
for (let i = 0; i < 3; i++) yield serial++;
}
let serial = 1000;
console.log("serial", [...serials()].join(","));
async function nextAsync(): Promise<number> {
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(() => {});
@@ -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);
@@ -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);
}
@@ -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());
@@ -0,0 +1,5 @@
export { take, bump } from "./counter.ts";
let local = 0.25;
export function scaled(): number {
return (local *= 2);
}