diff --git a/SPEC.md b/SPEC.md index 67ef932..829e245 100644 --- a/SPEC.md +++ b/SPEC.md @@ -9,7 +9,7 @@ This document reflects the implementation that ships in this repository today (` - **Unsafe by design** – Memory, syscalls, inline assembly, and FFI expose raw machine power. The standard library is intentionally thin and policy-free. ## 2. Toolchain and Repository Layout -- **Driver (`main.py`)** – Supports `python main.py source.sl -o a.out`, `--emit-asm`, `--run`, `--dbg`, `--repl`, `--temp-dir`, `--clean`, repeated `-I/--include` paths, and repeated `-l` linker flags (either `-lfoo` or `-l libc.so.6`). Unknown `-l` flags are collected and forwarded to the linker. +- **Driver (`main.py`)** – Supports `python main.py source.sl -o a.out`, `--emit-asm`, `--run`, `--dbg`, `--repl`, `--temp-dir`, `--clean`, repeated `-I/--include` paths, and repeated `-l` linker flags (either `-lfoo` or `-l libc.so.6`). Unknown `-l` flags are collected and forwarded to the linker. Pass `--ct-run-main` to run the program's `main` word on the compile-time VM before NASM/ld run, which surfaces discrepancies between compile-time and runtime semantics. - **REPL** – `--repl` launches a stateful session with commands such as `:help`, `:reset`, `:load`, `:call `, `:edit`, and `:show`. The REPL still emits/links entire programs for each run; it simply manages the session source for you. - **Imports** – `import relative/or/absolute/path.sl` inserts the referenced file textually. Resolution order: (1) absolute path, (2) relative to the importing file, (3) each include path (defaults: project root and `./stdlib`). Each file is included at most once per compilation unit. Import lines leave blank placeholders so error spans stay meaningful. - **Workspace** – `stdlib/` holds library modules, `tests/` contains executable samples with `.expected` outputs, `extra_tests/` houses standalone integration demos, and `libs/` collects opt-in extensions such as `libs/fn.sl` and `libs/nob.sl`. @@ -81,10 +81,11 @@ This document reflects the implementation that ships in this repository today (` - **`stdlib.sl`** – Convenience aggregator that imports `core`, `mem`, `io`, and `utils` so most programs can simply `import stdlib/stdlib.sl`. ## 9. Testing and Usage Patterns -- **Automated coverage** – `python test.py` compiles every `tests/*.sl`, runs the generated binary, and compares stdout against `.expected`. Optional companions include `.stdin` (piped to the process), `.args` (extra CLI args parsed with `shlex`), `.stderr` (expected stderr), and `.meta.json` (per-test knobs such as `expected_exit`, `expect_compile_error`, or `env`). The `extra_tests/` folder ships with curated demos (`extra_tests/ct_test.sl`, `extra_tests/args.sl`, `extra_tests/c_extern.sl`, `extra_tests/fn_test.sl`, `extra_tests/nob_test.sl`) that run alongside the core suite; pass `--extra path/to/foo.sl` to cover more standalone files. Use `python test.py --list` to see descriptions and `python test.py --update foo` to bless outputs after intentional changes. +- **Automated coverage** – `python test.py` compiles every `tests/*.sl`, runs the generated binary, and compares stdout against `.expected`. Optional companions include `.stdin` (piped to the process), `.args` (extra CLI args parsed with `shlex`), `.stderr` (expected stderr), and `.meta.json` (per-test knobs such as `expected_exit`, `expect_compile_error`, or `env`). The `extra_tests/` folder ships with curated demos (`extra_tests/ct_test.sl`, `extra_tests/args.sl`, `extra_tests/c_extern.sl`, `extra_tests/fn_test.sl`, `extra_tests/nob_test.sl`) that run alongside the core suite; pass `--extra path/to/foo.sl` to cover more standalone files. Use `python test.py --list` to see descriptions and `python test.py --update foo` to bless outputs after intentional changes. Add `--ct-run-main` when invoking the harness to run each test's `main` at compile time as well; capture that stream with `.compile.expected` if you want automated comparisons. - **Common commands** – - `python test.py` (run the whole suite) - `python test.py hello --update` (re-bless a single test) + - `python test.py --ct-run-main hello` (compile/run a single test while also exercising `main` on the compile-time VM) - `python main.py tests/hello.sl -o build/hello && ./build/hello` - `python main.py program.sl --emit-asm --temp-dir build` - `python main.py --repl` diff --git a/main.py b/main.py index 85d1c6b..226822d 100644 --- a/main.py +++ b/main.py @@ -3518,6 +3518,12 @@ class Compiler: source, spans = self._load_with_imports(path.resolve()) return self.compile_source(source, spans=spans, debug=debug, entry_mode=entry_mode) + def run_compile_time_word(self, name: str) -> None: + word = self.dictionary.lookup(name) + if word is None: + raise CompileTimeError(f"word '{name}' not defined; cannot run at compile time") + self.parser.compile_time_vm.invoke(word) + def _resolve_import_target(self, importing_file: Path, target: str) -> Path: raw = Path(target) tried: List[Path] = [] @@ -4120,6 +4126,7 @@ def cli(argv: Sequence[str]) -> int: parser.add_argument("--repl", action="store_true", help="interactive REPL; source file is optional") parser.add_argument("-l", dest="libs", action="append", default=[], help="pass library to linker (e.g. -l m or -l libc.so.6)") parser.add_argument("--no-folding", action="store_true", help="disable constant folding optimization") + parser.add_argument("--ct-run-main", action="store_true", help="execute 'main' via the compile-time VM after parsing") # Parse known and unknown args to allow -l flags anywhere args, unknown = parser.parse_known_args(argv) @@ -4138,6 +4145,9 @@ def cli(argv: Sequence[str]) -> int: artifact_kind = args.artifact folding_enabled = not args.no_folding + if args.ct_run_main and artifact_kind != "exe": + parser.error("--ct-run-main requires --artifact exe") + if artifact_kind != "exe" and (args.run or args.dbg): parser.error("--run/--dbg are only available when --artifact exe is selected") @@ -4177,6 +4187,13 @@ def cli(argv: Sequence[str]) -> int: entry_mode = "program" if artifact_kind == "exe" else "library" emission = compiler.compile_file(args.source, debug=args.debug, entry_mode=entry_mode) + + if args.ct_run_main: + try: + compiler.run_compile_time_word("main") + except CompileTimeError as exc: + print(f"[error] compile-time execution of 'main' failed: {exc}") + return 1 except (ParseError, CompileError, CompileTimeError) as exc: print(f"[error] {exc}") return 1 diff --git a/test.py b/test.py index 7465564..57c0099 100644 --- a/test.py +++ b/test.py @@ -418,6 +418,8 @@ class TestRunner: cmd = [sys.executable, str(self.main_py), str(case.source), "-o", str(case.binary_path)] for lib in case.config.libs: cmd.extend(["-l", lib]) + if self.args.ct_run_main: + cmd.append("--ct-run-main") if self.args.verbose: print(f"\n{format_status('CMD', 'blue')} {quote_cmd(cmd)}") return subprocess.run( @@ -601,6 +603,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: parser.add_argument("--list", action="store_true", help="list tests and exit") parser.add_argument("--update", action="store_true", help="update expectation files with actual output") parser.add_argument("--stop-on-fail", action="store_true", help="stop after the first failure") + parser.add_argument("--ct-run-main", action="store_true", help="execute each test's 'main' via the compile-time VM during compilation") parser.add_argument("-v", "--verbose", action="store_true", help="show compiler/runtime commands") return parser.parse_args(argv)