refactor: keep the front end, drop everything downstream of it

The milestone structure had stopped describing the compiler and started
shaping it: m7.c, check_m7.c, tests/m2..m9, and a checker and emitter that had
each grown past 2,500 lines because there was nowhere else to put anything.
Restart from the pipeline instead.

What is left is the front end -- lexer, parser, types, ownership, semantic
analysis -- and the fixtures that describe it. The C backend, the DOSBox-X
runner, the milestone registry and the batch build are removed. The driver now
stops after semantic analysis; a code generator attaches where emit_c did.

Fixtures move from milestone directories to what they check:

    parse/     grammar            own/       ownership and borrowing
    types/     type rules         optional/  optionals and error unions
    format/    formatting, try    units/     units and visibility
    generic/   generics           pending-backend/

pending-backend/ holds the three fixtures that can only be checked by running
a program -- that the bounds check traps, that --no-checks removes it, and that
drops and defers actually fire, verified through a fake allocator. Those are
not front-end tests and are not pretending to be; they come back first when
there is a code generator.

tests/run.py replaces the DOSBox-X harness. It builds the front end with the
pinned Watcom's Windows-hosted driver and runs every fixture in about two
seconds, and it does something the old runner structurally could not: it reads
the `// ERROR:line:text` marker each fixture carries and checks the diagnostic
against it. Those markers have been in the tree all along, unverified, because
DOS could not redirect the compiler's stderr and only the exit code was ever
compared.

133/188 pass. The 55 failures are not regressions -- they are what was already
true and invisible:

- units (27) and generic (23): `import`, `comptime` and generic declarations
  parse and are then dropped on the floor. No pass looks at them. The old
  registry did not list these fixtures at all, so nothing said so.
- five in own/, optional/ and generic/: a marker disagrees with the diagnostic
  about the line or the wording. Each is either a wrong marker or a wrong
  diagnostic and has to be read individually.

Everything removed is in git history.
This commit is contained in:
2026-08-17 03:33:23 +09:00
parent 2696dd2abc
commit fb65152901
221 changed files with 149 additions and 5834 deletions
+140
View File
@@ -0,0 +1,140 @@
"""Run every fixture through the front end and check what it reports.
The compiler is a front end now -- lexer, parser, types, ownership -- so a
fixture is checked by running `fec` on it and looking at two things: whether it
was accepted, and, when it was rejected, whether the diagnostic is the one the
fixture asked for.
A fixture states its expectation in its first line:
// ERROR:8:self rejected at line 8, with "self" in the message
// ERROR:expected ';' rejected, message only -- the parse fixtures, where
the line is not the interesting part
A fixture with no marker whose name starts with `bad` must be rejected but does
not pin the message yet. Anything else must be accepted.
Fixtures under `parse/` are checked with --dump-ast rather than --check: they
exercise the grammar, and several are deliberately not well-typed.
This runs on the host in about a second. There is no VM: nothing here executes
generated code, because there is no code generator.
"""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
FIXTURES = ROOT / "fec" / "tests"
WATCOM = ROOT / ".dosboxx" / "watcom"
SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own",
"check", "driver")
# Fixtures live here until there is a code generator to run them against.
QUARANTINE = "pending-backend"
MARKER = re.compile(r"^//\s*ERROR:(?:(\d+):)?(.*)$")
@dataclass
class Expectation:
rejected: bool
line: int | None = None
text: str | None = None
def expectation(path: Path) -> Expectation:
first = path.read_text(encoding="utf-8", errors="replace").split("\n", 1)[0]
m = MARKER.match(first.strip())
if m:
line = int(m.group(1)) if m.group(1) else None
return Expectation(True, line, m.group(2).strip())
name = path.stem
return Expectation(name.startswith("bad") or "-bad-" in name or name.startswith("own-bad"))
def build(out: Path) -> Path:
"""Build the front end with the pinned toolchain, hosted."""
wcl = WATCOM / "binnt" / "wcl386.exe"
if not wcl.is_file():
sys.exit(f"pinned Open Watcom not found at {WATCOM}")
out.mkdir(parents=True, exist_ok=True)
env = dict(os.environ)
env.update(WATCOM=str(WATCOM), INCLUDE=f"{WATCOM / 'h'};{WATCOM / 'h' / 'nt'}",
PATH=f"{WATCOM / 'binnt'}{os.pathsep}{env.get('PATH', '')}")
src = ROOT / "fec" / "src"
cmd = [str(wcl), "-q", "-za", "-wx", "-bt=nt", "-fe=fec.exe", f"-i={src}"]
cmd += [str(src / f"{n}.c") for n in SOURCES]
done = subprocess.run(cmd, cwd=out, capture_output=True, text=True, env=env)
if done.returncode != 0 or (done.stdout + done.stderr).strip():
sys.exit("front end does not build clean:\n" + done.stdout + done.stderr)
return out / "fec.exe"
def run_case(fec: Path, path: Path) -> tuple[bool, str]:
want = expectation(path)
# The grammar fixtures are not all well-typed; stop after parsing.
mode = "--dump-ast" if path.parent.name == "parse" else "--check"
done = subprocess.run([str(fec), mode, str(path)],
capture_output=True, text=True, timeout=30)
output = (done.stdout + done.stderr).strip()
rejected = done.returncode != 0
if want.rejected != rejected:
verb = "accepted" if rejected else "rejected"
return False, f"expected to be {'rejected' if want.rejected else verb}"
if not want.rejected:
return True, ""
if want.line is None:
if want.text and want.text.lower() not in output.lower():
got = output.splitlines()[0] if output else "(silent)"
return False, f"marker wants {want.text!r}\n {got}"
return True, ""
# The marker pins where and roughly what, so a rule can be moved or reworded
# only deliberately.
first = output.split("\n", 1)[0] if output else ""
at = re.search(r":(\d+):\d+: error:", first)
if not at:
return False, f"no diagnostic to match marker\n got: {first or '(silent)'}"
if int(at.group(1)) != want.line:
return False, f"marker says line {want.line}, diagnostic is line {at.group(1)}\n {first}"
if want.text and want.text.lower() not in output.lower():
return False, f"marker wants {want.text!r}\n {first}"
return True, ""
def main() -> int:
ap = argparse.ArgumentParser(description="run the front-end fixtures")
ap.add_argument("-k", dest="select", help="only fixtures whose path contains this")
ap.add_argument("-v", dest="verbose", action="store_true")
args = ap.parse_args()
fec = build(ROOT / ".build")
cases = sorted(p for p in FIXTURES.rglob("*.fe") if QUARANTINE not in p.parts)
if args.select:
cases = [p for p in cases if args.select in p.as_posix()]
failed = []
for path in cases:
ok, why = run_case(fec, path)
rel = path.relative_to(FIXTURES).as_posix()
if ok:
if args.verbose:
print(f" ok {rel}")
else:
failed.append((rel, why))
for rel, why in failed:
print(f"FAIL {rel}: {why}")
marked = sum(1 for p in cases if expectation(p).line is not None)
print(f"\n{len(cases) - len(failed)}/{len(cases)} passed "
f"({marked} pin a line and message)")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())