Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
128 changes: 128 additions & 0 deletions test-files/multi/test_harness_init_scale/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Generate the ≥300-module init-scale harness for #808.

Reproduces the conditions of #684 (Effect's Schema.ts ~310th-init bug) by
forcing N cross-module init calls in topological order and asserting per
module that:

- the exported numeric constant is still `typeof === "number"` after
all the upstream inits have run (no NaN-box tag leak),
- the exported class can be instantiated and its instance method
returns the expected string (no class_id collision / vtable smear).

Usage:
python3 generate.py [N] # default N = 300

Idempotent — re-running overwrites m000.ts ... m{N-1}.ts and index.ts.
Run from the repo root or this directory; output paths are anchored to
this script's location.
"""
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))

MODULE_TEMPLATE = '''\
// Auto-generated by generate.py — do not hand-edit; rerun the generator.
// Module #{i} in the #808 init-scale chain. Each module imports its
// predecessor so Perry's module init order has to walk the chain end
// to end (#684 conditions). The class+const pair gives the harness two
// independent state-isolation probes.
{import_line}
export const VAL_{i}: number = {value};

export class Cls_{i} {{
// `tag()` returns a string baked from the module index. After 300+
// inits, Perry must still dispatch this method on a `Cls_{i}`
// instance (not Cls_0 / Cls_299 / a phantom shape) and the return
// must be a string (not 0, undefined, or a NaN-box leak).
tag(): string {{
return "Cls_{i}";
}}
}}
'''

INDEX_TEMPLATE = '''\
// Auto-generated by generate.py — do not hand-edit; rerun the generator.
//
// #808 — ≥300-module init-scale harness. Walks the chain of {n} small
// modules and asserts state isolation:
//
// - typeof each exported constant === "number"
// - constant value matches the expected formula 1000 + i
// - `new Cls_i().tag()` returns "Cls_i" (no class_id smear)
//
// On any failure, prints the first failing module so the regression is
// pinpointed. On success, prints a single OK line so the parity diff
// stays tiny.

{imports}

let failures = 0;
let first_fail = "";

function fail(where: string): void {{
failures++;
if (first_fail === "") first_fail = where;
}}

{checks}

if (failures === 0) {{
console.log("init-scale: OK ({n} modules)");
}} else {{
console.log("init-scale: FAIL first=" + first_fail + " total=" + failures);
}}
'''


def gen_module(i: int, n: int) -> str:
# m000 has no predecessor; m_i imports m_{i-1} to force a topological
# chain. The import is value-not-just-type so the predecessor's init
# actually runs before this module's init.
pad = f"{i:03}"
prev = f"{i - 1:03}"
if i == 0:
import_line = "// (m000 has no predecessor — the chain starts here.)"
else:
import_line = f'import {{ VAL_{prev} }} from "./m{prev}";\n' \
f'// Reference the predecessor in a top-level expression to\n' \
f'// guarantee the import survives DCE and the predecessor\n' \
f'// init runs before ours.\n' \
f'const _chain_{pad}: number = VAL_{prev} + 1;\n' \
f'if (_chain_{pad} < 0) throw new Error("chain check");'
return MODULE_TEMPLATE.format(i=pad, value=1000 + i, import_line=import_line)


def gen_index(n: int) -> str:
imports_lines = []
checks_lines = []
for i in range(n):
idx = f"{i:03}"
imports_lines.append(f'import {{ VAL_{idx}, Cls_{idx} }} from "./m{idx}";')
checks_lines.append(
f'if (typeof VAL_{idx} !== "number" || VAL_{idx} !== {1000 + i}) fail("VAL_{idx}");\n'
f'if (new Cls_{idx}().tag() !== "Cls_{idx}") fail("Cls_{idx}");'
)
return INDEX_TEMPLATE.format(
n=n,
imports="\n".join(imports_lines),
checks="\n".join(checks_lines),
)


def main() -> None:
n = int(sys.argv[1]) if len(sys.argv) > 1 else 300
if n < 1:
sys.exit("N must be >= 1")
for i in range(n):
path = os.path.join(HERE, f"m{i:03}.ts")
with open(path, "w") as fh:
fh.write(gen_module(i, n))
with open(os.path.join(HERE, "index.ts"), "w") as fh:
fh.write(gen_index(n))
print(f"wrote {n} module files + index.ts under {HERE}")


if __name__ == "__main__":
main()
Loading
Loading