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 가 컴파일하고 링크하고 돌린다.
This commit is contained in:
2026-08-17 05:55:34 +09:00
parent 6ee3764667
commit e5093e690d
9 changed files with 662 additions and 7 deletions
+103
View File
@@ -0,0 +1,103 @@
"""Compile a Ferro program to a Windows executable and run it.
fec --emit-asm -> wasm -> wlink (+ the runtime, + kernel32) -> .exe
The toolchain is the pinned Open Watcom under `.dosboxx/watcom`, hosted: the
assembler and linker there produce PE binaries as happily as they produce DOS
ones. Nothing about this step needs a virtual machine.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
WATCOM = ROOT / ".dosboxx" / "watcom"
RUNTIME = ROOT / "fec" / "rt" / "start.asm"
def _env() -> dict:
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', '')}")
return env
def _run(cmd, cwd) -> subprocess.CompletedProcess:
return subprocess.run([str(c) for c in cmd], cwd=cwd, env=_env(),
capture_output=True, text=True, timeout=120)
def build(fec: Path, source: Path, out_dir: Path, no_checks: bool = False):
"""Returns (exe_path, log). exe_path is None when a step failed."""
out_dir.mkdir(parents=True, exist_ok=True)
stem = source.stem
asm = out_dir / f"{stem}.asm"
log = []
cmd = [fec, "--emit-asm", source, "-o", asm]
if no_checks:
cmd.append("--no-checks")
step = _run(cmd, out_dir)
log.append(("fec", step.returncode, step.stdout + step.stderr))
if step.returncode != 0 or not asm.is_file():
return None, log
wasm = WATCOM / "binnt" / "wasm.exe"
for src, obj in ((asm, out_dir / f"{stem}.obj"),
(RUNTIME, out_dir / "start.obj")):
step = _run([wasm, "-q", "-zq", src, f"-fo={obj}"], out_dir)
log.append(("wasm " + src.name, step.returncode,
step.stdout + step.stderr))
if step.returncode != 0:
return None, log
exe = out_dir / f"{stem}.exe"
step = _run([WATCOM / "binnt" / "wlink.exe",
"system", "nt",
"file", out_dir / f"{stem}.obj",
"file", out_dir / "start.obj",
"library", WATCOM / "lib386" / "nt" / "kernel32.lib",
"name", exe,
"option", "quiet"], out_dir)
log.append(("wlink", step.returncode, step.stdout + step.stderr))
if step.returncode != 0 or not exe.is_file():
return None, log
return exe, log
def run(exe: Path):
done = subprocess.run([str(exe)], capture_output=True, text=True,
timeout=30)
return done.returncode, done.stdout + done.stderr
def main() -> int:
if len(sys.argv) < 2:
print("usage: build.py <program.fe> [--no-checks]")
return 2
source = Path(sys.argv[1]).resolve()
fec = ROOT / ".build" / "fec.exe"
if not fec.is_file():
print("build the front end first: uv run python tests/run.py")
return 2
exe, log = build(fec, source, ROOT / ".build" / "out",
"--no-checks" in sys.argv)
for name, code, text in log:
if code != 0 or text.strip():
print(f"--- {name} (exit {code})")
print(text.rstrip())
if not exe:
return 1
code, text = run(exe)
if text:
print(text, end="")
print(f"{exe.name} exited {code}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -34,7 +34,7 @@ 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", "driver")
"check", "resolve", "ir", "lower", "x86", "driver")
# Fixtures live here until there is a code generator to run them against.
QUARANTINE = "pending-backend"