#!/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()
