'ffec 에서 unsafe 가 몇 군데인가' 는 좋은 지표인데 세는 방법이 없으면 지표가 아니다. 이제 센다. unit unsafe *T unchecked std.io 6 1 0 std.sys 10 12 0 total 16 13 0 outside std 0 0 0 Ferro 렉서와 파서, interner, 아레나, 맵을 쓰는 프로그램 전부가 std 바깥에서 0 이다. 목표치를 이미 지키고 있었던 셈인데, 그것을 아무도 확인할 수 없었다. run.py 가 그 숫자를 검사한다. std.mem 과 std.sys 밖의 unsafe 와 *T 는 검사기가 약속한 것에 뚫린 구멍이므로 0 을 유지해야 하고, 늘어나면 알아채는 것이 아니라 빌드가 실패해야 한다. unsafe 블록 하나를 넣어 실패하는 것까지 확인했다. --report-instances 는 제네릭이 무엇이 됐는지 센다. interns.fe 는 20 인스턴스, 타입 4 개에 저장소 80 바이트, 메서드 16 개다. 245/245, 38/38.
173 lines
6.7 KiB
Python
173 lines
6.7 KiB
Python
"""Run every fixture through the front end and check what it reports.
|
|
|
|
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. It checks what the compiler says; what
|
|
the compiled programs actually do is `exec.py`.
|
|
"""
|
|
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", "checkexp", "checkstm", "checkgen", "checkcal", "checkpro",
|
|
"resolve", "ir", "lower", "lowerprn", "lowerexp", "lowerstm", "x86", "report", "driver")
|
|
|
|
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),
|
|
f"--std={ROOT / 'fec'}"],
|
|
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(FIXTURES.rglob("*.fe"))
|
|
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)")
|
|
if not args.select:
|
|
leak = unsafe_budget(fec)
|
|
if leak:
|
|
print(leak)
|
|
return 1
|
|
return 1 if failed else 0
|
|
|
|
|
|
# The programs the budget is measured on: the Ferro front end, and the ones
|
|
# that lean hardest on the standard library.
|
|
BUDGETED = ("exec/lexer/tree.fe", "exec/interns.fe", "exec/arenat.fe",
|
|
"exec/maps.fe", "exec/wordfreq.fe")
|
|
|
|
|
|
def unsafe_budget(fec: Path) -> str:
|
|
"""`unsafe` and `*T` belong to std.mem and std.sys. Anywhere else they are
|
|
a hole in what the checker promises, so the count outside std has to stay
|
|
at zero and a regression has to fail the build rather than be noticed."""
|
|
for rel in BUDGETED:
|
|
path = FIXTURES / rel
|
|
if not path.is_file():
|
|
return f"budget: {rel} is gone"
|
|
done = subprocess.run([str(fec), "--report-unsafe", str(path),
|
|
f"--std={ROOT / 'fec'}"],
|
|
capture_output=True, text=True)
|
|
line = [l for l in done.stdout.splitlines()
|
|
if l.startswith("outside std")]
|
|
if not line:
|
|
return f"budget: no report for {rel}\n{done.stdout}{done.stderr}"
|
|
counts = line[0].split()[2:]
|
|
if any(c != "0" for c in counts):
|
|
return (f"budget: {rel} has unsafe/raw pointers outside std: "
|
|
f"{line[0]}")
|
|
return ""
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|