dev: run host syntax gates before starting DOSBox-X

There is an Open Watcom install on this host (C:\WATCOM19), and its Windows
build compiles the compiler's own sources in about a second. Every declaration
mismatch in the unification commits was found that way; each one would
otherwise have cost a DOSBox-X boot and a full compiler build to surface, with
a DOS-side message that names the wrong thing.

Add tools/tests/test_host_syntax.py: compile all twelve sources with the flags
build-dos.bat uses (-za -wx -wcd=202) and fail on any diagnostic. It skips when
Watcom is absent, so the suite still runs elsewhere. Two structural checks come
with it -- that build-dos.bat, the Makefile and the test agree on the source
list, and that no .c under fec/src is compiled by nothing. Both would have
caught check.c and emit_c.c quietly leaving the build when the M7 wrappers
included them textually.

ferro-test now runs these and the 8.3 name check before starting the VM, and
stops if they fail.

This is not verification and does not claim to be: wcc386 targets 32-bit where
the real build is 16-bit large model, so it sees syntax, types and declarations
and nothing about code generation. The DOS build and the milestone suite remain
the gate. It only moves the cheap failures earlier.
This commit is contained in:
2026-08-17 01:57:06 +09:00
parent 4adfe60574
commit eb85e9fa3d
2 changed files with 117 additions and 1 deletions
+12 -1
View File
@@ -62,7 +62,18 @@ def main() -> int:
# shell is sitting in -- an editable install plus a git worktree is enough # shell is sitting in -- an editable install plus a git worktree is enough
# to silently build and test the wrong tree. Say which tree this is. # to silently build and test the wrong tree. Say which tree this is.
print(f"ferro-test: building {ROOT}", file=sys.stderr) print(f"ferro-test: building {ROOT}", file=sys.stderr)
test_file = os.fspath(ROOT / "tools" / "tests" / "test_milestones_dosboxx.py") tests = ROOT / "tools" / "tests"
# The host gates run first and take under a second. A missing declaration
# or an 8.3-illegal name would otherwise be found only after a DOSBox-X
# boot and a full compiler build, and the DOS-side message for either is
# unhelpful. They do not replace the DOS run; they precede it.
gates = [os.fspath(tests / name) for name in
("test_host_syntax.py", "test_dos_names.py")]
if pytest.main([*gates, "-q", "--no-header"]) != 0:
print("ferro-test: host gates failed; not starting DOSBox-X",
file=sys.stderr)
return 1
test_file = os.fspath(tests / "test_milestones_dosboxx.py")
pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"] pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"]
if args.select: if args.select:
pytest_args.extend(["-k", args.select]) pytest_args.extend(["-k", args.select])
+105
View File
@@ -0,0 +1,105 @@
"""Compile the compiler's own sources on the host, as a syntax gate.
This is not verification. AGENTS.md is explicit that a host compiler's result
does not count, and it still does not: the DOS build and the milestone suite
decide whether anything works. What this buys is the turnaround. A missing
declaration or a signature that disagrees with its definition used to surface
only after a DOSBox-X boot and a full compiler build; here it surfaces in about
a second, with the line number.
It uses the same Open Watcom the project targets, just the Windows-hosted build,
and the same strictness as ``fec/build-dos.bat`` (``-za -wx -wcd=202``). The
target differs -- wcc386 is 32-bit where the DOS build is 16-bit large model --
so this catches syntax, types and declarations, not code generation or memory
model problems.
Skipped when Watcom is not installed, so the suite still runs anywhere.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
from pathlib import Path
import pytest
from ferrolang_vm.paths import ROOT
SRC = ROOT / "fec" / "src"
# Mirrors the compile order in fec/build-dos.bat.
SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own",
"check", "lower", "emit_c", "driver")
def _watcom() -> Path | None:
root = os.environ.get("WATCOM")
candidates = [Path(root)] if root else []
candidates.append(Path("C:/WATCOM19"))
for base in candidates:
if (base / "binnt" / "wcc386.exe").is_file():
return base
return None
@pytest.fixture(scope="session")
def watcom() -> Path:
base = _watcom()
if base is None:
pytest.skip("Open Watcom is not installed on the host; set WATCOM to enable")
return base
@pytest.fixture(scope="session")
def objdir(tmp_path_factory: pytest.TempPathFactory) -> Path:
return tmp_path_factory.mktemp("wcc")
@pytest.mark.parametrize("name", SOURCES)
def test_source_compiles_clean(name: str, watcom: Path, objdir: Path) -> None:
source = SRC / f"{name}.c"
if not source.is_file():
pytest.fail(f"{source} is missing but build-dos.bat compiles it")
env = dict(os.environ)
env["WATCOM"] = os.fspath(watcom)
env["INCLUDE"] = os.fspath(watcom / "h")
completed = subprocess.run(
[os.fspath(watcom / "binnt" / "wcc386.exe"), "-q", "-za", "-wx",
"-wcd=202", "-zq", f"-i={SRC}", os.fspath(source)],
cwd=objdir, capture_output=True, text=True, env=env, timeout=120,
)
output = (completed.stdout + completed.stderr).strip()
# -wx keeps warnings meaningful, so treat any diagnostic as a failure: the
# DOS build runs the same flags and stops on them.
assert completed.returncode == 0 and not output, (
f"{name}.c does not compile clean\n{output}"
)
def test_build_scripts_agree_on_sources() -> None:
"""The two build files and this test must name the same translation units.
They drifted apart while the M7 wrapper existed, which is how a source could
stop being compiled without anyone noticing.
"""
batch = (ROOT / "fec" / "build-dos.bat").read_text(encoding="utf-8", errors="replace")
makefile = (ROOT / "fec" / "Makefile").read_text(encoding="utf-8", errors="replace")
in_batch = set(re.findall(r"src\\(\w+)\.c", batch))
srcline = next(line for line in makefile.splitlines() if line.startswith("SRC ="))
in_make = set(re.findall(r"src/(\w+)\.c", srcline))
assert in_batch == set(SOURCES), f"build-dos.bat compiles {sorted(in_batch)}"
assert in_make == set(SOURCES), f"Makefile compiles {sorted(in_make)}"
def test_no_source_is_orphaned() -> None:
"""Every .c under fec/src must be compiled by something.
check_m7.c and emitcm7.c hid check.c and emit_c.c from the build by
including them textually; nothing flagged that they had stopped being
translation units of their own.
"""
on_disk = {p.stem for p in SRC.glob("*.c")}
assert on_disk == set(SOURCES), (
f"fec/src has {sorted(on_disk - set(SOURCES))} that no build step compiles"
)