dev: cache the compiler build and drop unused headers

Every run rebuilt fec from scratch inside DOS, about 14 seconds, even when no
source had changed. Key a cache on the hash of fec/src plus build-dos.bat and
restore FEC.EXE when it matches; the toolchain itself is pinned by
dosboxx.lock.json so it cannot drift under a hit. Only a passing build is
cached, and the batch skips BUILD.BAT on a hit because it would delete and
rebuild the executable it was just given.

Generated C included stdio.h unconditionally, but only the M4 writer runtime
reaches it. A 26-line unit was pulling in roughly 1900 lines of headers, paid
once per compile check. Emit it only when the runtime is emitted.

--only m6, 57 passed, over three changes:

    31s   before
    17s   core=dynamic
     2s   warm compiler cache

Cold runs still pay the build once.
This commit is contained in:
2026-08-17 01:17:40 +09:00
parent aaf31302d5
commit 044c0f0e98
3 changed files with 41 additions and 4 deletions
+6 -1
View File
@@ -1487,7 +1487,12 @@ void fe_emit_c_program(FeEmitter *e)
need_m4=node_uses_m4(e->check->ast->root);
for (type=e->check->types.types; type; type=type->next)
if (strcmp(type->name,"io.Writer")==0) need_m4=1;
fputs("/* generated by fec M4 */\n#include <stddef.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out);
/* stdio is only reached by the M4 writer runtime (fwrite/stdout/stderr).
Parsing it costs far more than the generated body -- a 26-line unit pulls
in about 1900 lines of headers -- so leave it out when nothing uses it. */
fputs("/* generated by fec M4 */\n#include <stddef.h>\n#include <stdlib.h>\n#include <string.h>\n", e->out);
if (need_m4) fputs("#include <stdio.h>\n", e->out);
fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out);
if (e->pointer_bits==16)
fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out);
else
+4 -1
View File
@@ -1327,7 +1327,10 @@ void fe_emit_c_program(FeEmitter *e)
need_m4=node_uses_m4(e->check->ast->root);
for (type=e->check->types.types;type;type=type->next)
if (strcmp(type->name,"io.Writer")==0) need_m4=1;
fputs("/* generated by fec M7 */\n#include <stddef.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out);
/* See emit_c.c: stdio only comes in with the M4 writer runtime. */
fputs("/* generated by fec M7 */\n#include <stddef.h>\n#include <stdlib.h>\n#include <string.h>\n",e->out);
if (need_m4) fputs("#include <stdio.h>\n",e->out);
fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out);
if (e->pointer_bits==16)
fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out);
else
+31 -2
View File
@@ -151,10 +151,14 @@ def _rc_batch() -> str:
return "\r\n".join(lines)
def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str:
def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool,
prebuilt: bool = False) -> str:
# The compiler build is the step that fails first and blocks everything after
# it, so its output is captured exactly like a case command's.
build = "call BUILD.BAT" if trace_dos else "call BUILD.BAT > RESULTS\\BUILD.LOG"
if prebuilt:
# FEC.EXE was restored from cache; BUILD.BAT would delete and rebuild it.
build = "echo OK>BUILD.OK"
lines = [
"@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT",
"set WATCOM=W:", "set INCLUDE=W:\\H",
@@ -247,6 +251,23 @@ class SuiteRun:
shutil.rmtree(self.root, ignore_errors=True)
def _compiler_key() -> str:
"""Hash of everything the compiler build reads.
Sources and the build batch only; the toolchain itself is pinned by
dosboxx.lock.json, so it cannot drift underneath a cache hit.
"""
digest = hashlib.sha256()
paths = sorted((ROOT / "fec" / "src").rglob("*"))
paths.append(ROOT / "fec" / "build-dos.bat")
for path in paths:
if not path.is_file():
continue
digest.update(path.name.encode("utf-8"))
digest.update(path.read_bytes())
return digest.hexdigest()[:16]
def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False,
trace_dos: bool = False) -> SuiteRun:
dosbox, watcom = resolve_tools()
@@ -254,6 +275,7 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False,
run_root = Path(tempfile.mkdtemp(prefix="suite-", dir=RUNS))
result = SuiteRun(run_root, cases, keep)
fec = result.fec
cached = CACHE / "compilers" / f"{_compiler_key()}.exe"
try:
shutil.copytree(ROOT / "fec" / "src", fec / "SRC")
shutil.copytree(ROOT / "fec" / "std", fec / "STD")
@@ -271,8 +293,11 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False,
f"[cpu]\ncore=dynamic\ncycles=max\n",
encoding="ascii",
)
if cached.is_file():
shutil.copy2(cached, fec / "FEC.EXE")
(fec / "RUN.BAT").write_text(
_batch(cases, show_dos=show_dos, trace_dos=trace_dos),
_batch(cases, show_dos=show_dos, trace_dos=trace_dos,
prebuilt=cached.is_file()),
encoding="ascii", newline="",
)
(fec / "RC.BAT").write_text(_rc_batch(), encoding="ascii", newline="")
@@ -293,6 +318,10 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False,
raise DosboxError(f"DOSBox-X exited with status {completed.returncode}")
if not (fec / "RUN.OK").is_file():
raise DosboxError("DOSBox-X did not complete the test batch")
built = fec / "FEC.EXE"
if not cached.is_file() and built.is_file() and result.result() == "PASS":
cached.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(built, cached)
return result
except Exception:
result.keep = True