feat(compiler): 启用 ir2mcode 和 sccf2target 库并实现 x86_64 代码生成

- 在 cbuild.toml 中启用 ir2mcode 和 sccf2target 依赖库
- 修改 justfile 中的构建命令,使用 release 模式并更新 tokei 统计排除 mcode 目录
- 重构 LIR 中的地址操作数类型,将 SCC_LIR_INSTR_KIND_ADDR 重命名为 SCC_LIR_INSTR_KIND_MEM
- 实现完整的 MIR 到 x86_64 机器码转换,包括:
  - 添加 move、compare、binary operation 等指令发射函数
  - 实现条件分支和跳转指令生成
  - 支持算术、逻辑、移位等基本操作
  - 添加调用和返回指令处理
  - 实现栈分配和寄存器分配功能
- 完善 ir2mcode 模块,将 MIR 指令转换为机器码
- 更新 ir2sccf 模块,集成机器码生成功能
- 添加 mcode 库的架构支持和内存管理功能
- 修复 PE 文件生成中的空指针检查问题
This commit is contained in:
zzy
2026-05-05 15:59:31 +08:00
parent 676f3ec82c
commit aa8a1ff8ce
14 changed files with 842 additions and 399 deletions

View File

@@ -1,107 +1,327 @@
from pprint import PrettyPrinter
#!/usr/bin/env python3
"""Integration test runner for scc compiler.
Reads test expectations from `expect.toml`, compiles each C source file with scc,
executes the resulting binary, and validates the process return code or stdout.
"""
from __future__ import annotations
import argparse
import logging
import os
import subprocess
from pathlib import Path
import sys
import tempfile
import tomllib
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence
# 配置参数
WORKSPACE = Path(__file__).resolve().parent # 测试工作目录
TEST_DIR = Path(WORKSPACE)
CC_PATH = Path(WORKSPACE / "../../build/dev/scc")
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
WORKSPACE = Path(__file__).resolve().parent
CC_PATH = WORKSPACE / "../../build/dev/scc"
CONFIG_PATH = WORKSPACE / "expect.toml"
DEFAULT_TIMEOUT = 10 # seconds
def run_command(cmd, capture_output=True):
"""执行命令并捕获 stdout"""
try:
result = subprocess.run(
cmd,
cwd=WORKSPACE,
stdout=subprocess.PIPE if capture_output else None,
stderr=subprocess.PIPE,
text=True,
timeout=5, # 增加超时时间以防虚拟机启动慢
)
# 返回 stdout 用于获取返回值,同时检查是否有运行时错误
return result.stdout.strip(), result.stderr.strip(), result.returncode
except subprocess.TimeoutExpired:
return None, "Timeout expired", -1
except Exception as e:
return None, str(e), -1
logger = logging.getLogger("scc-test")
def run_test(test_file, expected):
print(f"\nTesting {test_file}...")
# 使用唯一文件名避免并发冲突
unique_id = str(uuid.uuid4())[:8] # 简短的唯一标识符
exe_filename = f"test_{unique_id}.exe"
exe_path = WORKSPACE / exe_filename
# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------
@dataclass
class TestCase:
"""A single test case defined in expect.toml."""
source: Path
expected: int | str # return code (int) or stdout (str)
test_type: str # "return" or "stdout"
origin_key: str # original TOML key for user-friendly display
# 1. 编译
compile_cmd = [str(CC_PATH), str(test_file), "-o", exe_filename, "--entry-point-symbol", "main"]
# 编译时关注 stderr 和返回码
_, compile_err, compile_ret = run_command(compile_cmd)
@property
def description(self) -> str:
return f"{self.test_type.upper():6s} {self.source}"
if not exe_path.exists() or compile_ret != 0:
print(f" Compilation failed: {compile_err}")
# 确保清理失败的输出文件
if exe_path.exists():
try:
exe_path.unlink()
except:
pass # 忽略清理失败
return False
# 2. 执行虚拟机并获取输出
vm_cmd = [str(exe_path)]
actual_output, vm_err, vm_ret = run_command(vm_cmd)
# ---------------------------------------------------------------------------
# Test runner
# ---------------------------------------------------------------------------
class Runner:
"""Compiles and executes test cases."""
# 如果存在 stderr 且返回码异常(例如负数表示信号终止),则视为运行时错误
if vm_err and vm_ret < 0:
print(f" Runtime error: {vm_err}")
# 清理文件后返回
def __init__(
self,
cc: Path,
workspace: Path,
timeout: float = DEFAULT_TIMEOUT,
keep_temps: bool = False,
) -> None:
self.cc = cc.resolve()
self.workspace = workspace.resolve()
self.timeout = timeout
self.keep_temps = keep_temps
self._temp_dir: tempfile.TemporaryDirectory | None = None
@property
def temp_dir(self) -> Path:
if self._temp_dir is None:
self._temp_dir = tempfile.TemporaryDirectory(
prefix="scc-test-", dir=self.workspace
)
return Path(self._temp_dir.name)
def cleanup(self) -> None:
if self._temp_dir is not None:
self._temp_dir.cleanup()
self._temp_dir = None
def _unique_exe_path(self) -> Path:
"""Generate a unique executable path inside the temporary directory."""
return self.temp_dir / f"test_{uuid.uuid4().hex[:8]}.exe"
def compile(self, source: Path, output: Path) -> tuple[bool, str]:
"""Compile *source* into *output*. Returns (success, stderr)."""
cmd = [
str(self.cc),
str(source),
"-o",
str(output),
"--entry-point-symbol", "main",
]
try:
exe_path.unlink()
except:
pass # 忽略清理失败
return False
proc = subprocess.run(
cmd,
cwd=self.workspace,
capture_output=True,
text=True,
timeout=self.timeout,
)
except subprocess.TimeoutExpired:
return False, "Compilation timed out"
except OSError as exc:
return False, f"Failed to invoke compiler: {exc}"
# 3. 获取返回值 (修改进程返回值而非 stdout)
actual = vm_ret
# 4. 清理输出文件
try:
exe_path.unlink()
except:
pass # 忽略清理失败
if proc.returncode != 0 or not output.exists():
return False, proc.stderr.strip() or "(no error message)"
return True, proc.stderr.strip()
# 5. 验证结果
# 注意toml 中读取的 expected 可能是整数actual 也是整数,直接比较
if actual == expected:
print(f" PASSED {test_file}")
return True
else:
print(f" FAILED: Expected '{expected}', got '{actual}'")
return False
def run_exe(self, exe: Path) -> tuple[int, str, str]:
"""Execute a binary. Returns (returncode, stdout, stderr)."""
# Make sure the file is executable (Unix)
exe.chmod(exe.stat().st_mode | 0o111)
try:
proc = subprocess.run(
[str(exe)],
cwd=self.workspace,
capture_output=True,
text=True,
timeout=self.timeout,
)
except subprocess.TimeoutExpired:
return -1, "", "Execution timed out"
except OSError as exc:
return -1, "", f"Failed to run executable: {exc}"
def main():
passed = 0
total = 0
config = {}
config_path = WORKSPACE / "expect.toml"
if not config_path.exists():
print(f"Config file not found: {config_path}")
return proc.returncode, proc.stdout, proc.stderr.strip()
def run_one(self, test: TestCase) -> bool:
"""Run a single test case. Returns True if passed."""
logger.info("Testing %s", test.source)
# 1. Compile
exe_path = self._unique_exe_path()
ok, stderr = self.compile(test.source, exe_path)
if not ok:
logger.error(" Compilation FAILED: %s", stderr)
return False
# 2. Execute
returncode, stdout, run_err = self.run_exe(exe_path)
# Runtime error detection: negative return code (signal) or timeout
if run_err and returncode < 0:
logger.error(" Runtime error: %s", run_err)
self._remove(exe_path)
return False
# 3. Validate
if test.test_type == "return":
actual = returncode
else:
actual = stdout
passed = actual == test.expected
if passed:
logger.info(" PASSED")
else:
logger.error(
" FAILED: expected %r, got %r", test.expected, actual
)
# 4. Cleanup
self._remove(exe_path)
return passed
def _remove(self, path: Path) -> None:
"""Remove *path* unless keep_temps is set."""
if self.keep_temps:
return
try:
path.unlink(missing_ok=True)
except OSError:
pass
def run_all(self, tests: Sequence[TestCase]) -> tuple[int, int]:
"""Run a sequence of tests. Returns (passed, total)."""
passed = 0
for idx, test in enumerate(tests, start=1):
logger.info("(%d/%d) %s", idx, len(tests), test.description)
if self.run_one(test):
passed += 1
return passed, len(tests)
# ---------------------------------------------------------------------------
# Configuration loading
# ---------------------------------------------------------------------------
def load_config(config_path: Path) -> list[TestCase]:
"""Parse `expect.toml` and build list of TestCase objects."""
if not config_path.is_file():
logger.error("Config file not found: %s", config_path)
sys.exit(1)
with config_path.open("rb") as fh:
config = tomllib.load(fh)
cases: list[TestCase] = []
for test_file, expected in config.get("return_val_cases", {}).items():
cases.append(
TestCase(
source=WORKSPACE / test_file,
expected=expected,
test_type="return",
origin_key=test_file,
)
)
for test_file, expected in config.get("stdout_val_cases", {}).items():
cases.append(
TestCase(
source=WORKSPACE / test_file,
expected=expected,
test_type="stdout",
origin_key=test_file,
)
)
return cases
# ---------------------------------------------------------------------------
# CLI helpers
# ---------------------------------------------------------------------------
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Integration test runner for scc",
)
parser.add_argument(
"tests",
nargs="*",
metavar="SOURCE",
help="Specific test source files to run (as listed in expect.toml). "
"If none given, all tests are executed.",
)
parser.add_argument(
"--list",
action="store_true",
help="List all available test cases from expect.toml and exit.",
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable debug-level logging.",
)
parser.add_argument(
"--keep-temps",
action="store_true",
help="Keep temporary executables (useful for debugging).",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
help=f"Timeout in seconds for compilation/execution (default: {DEFAULT_TIMEOUT}).",
)
parser.add_argument(
"--cc",
type=Path,
default=CC_PATH,
help=f"Path to scc compiler (default: {CC_PATH}).",
)
return parser
def select_tests(all_tests: list[TestCase], requested: list[str]) -> list[TestCase]:
"""Filter *all_tests* by user-provided source paths."""
if not requested:
return all_tests
selected: list[TestCase] = []
for name in requested:
matched = [
t for t in all_tests
if t.origin_key == name or str(t.source) == name
or t.source.name == name
]
if not matched:
logger.warning("No test case matches '%s'", name)
continue
selected.extend(matched)
return selected
# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = build_arg_parser()
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(message)s",
)
# Load configuration
all_tests = load_config(CONFIG_PATH)
if args.list:
print("Available test cases:")
for i, test in enumerate(all_tests, start=1):
print(f" {i:3d}: {test.description}")
return
with open(config_path, "rb") as f:
config = tomllib.load(f)
PrettyPrinter().pprint(config)
# Filter tests
tests = select_tests(all_tests, args.tests)
if not tests:
logger.warning("No tests to run.")
return
for test_file, expected in config.get("return_val_cases", {}).items():
total += 1
if run_test(TEST_DIR / test_file, expected):
passed += 1
runner = Runner(
cc=args.cc,
workspace=WORKSPACE,
timeout=args.timeout,
keep_temps=args.keep_temps,
)
passed, total = runner.run_all(tests)
logger.info("=" * 40)
logger.info("Summary: %d/%d passed", passed, total)
if passed != total:
sys.exit(1)
runner.cleanup()
print(f"\nTest Summary: {passed}/{total} passed")
if __name__ == "__main__":
main()
main()