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:
@@ -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()
|
||||
Reference in New Issue
Block a user