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:
+140
@@ -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())
|
||||
Reference in New Issue
Block a user