Files
doslang-mirror/tests/run.py
T
coolguy e5093e690d backend: i386 어셈블리를 내고 Windows 11 실행 파일을 만든다
fec --emit-asm -> wasm -> wlink -> .exe. 툴체인은 고정된 Open Watcom 그대로다.

레지스터 할당기가 없다. 임시값마다 스택 슬롯을 주고, 명령마다 피연산자를
고정 레지스터로 읽어 계산하고 다시 저장한다. 느린 코드지만 명백히 옳은
코드이고, 옳은 것이 먼저다. 나중에 할당기를 끼워도 나머지는 모른다 --
임시값이 어디 사는지만 바뀐다.

런타임 fec/rt/start.asm 은 진입 스텁과 fe_trap 이다. trap 은 이유와 파일과
줄을 stderr 에 쓰고 3으로 끝낸다.

처음으로 Ferro 프로그램이 실행됐다:

  1..10 합         -> 55
  (7*6-2)/4        -> 10
  루프+호출+분기   -> 1

tests/build.py 가 컴파일하고 링크하고 돌린다.
2026-08-17 05:55:34 +09:00

141 lines
5.4 KiB
Python

"""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", "resolve", "ir", "lower", "x86", "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())