From 9732f0e74a27268855dc7df2215769ec24faf72f Mon Sep 17 00:00:00 2001 From: igor Date: Wed, 18 Feb 2026 08:48:30 +0100 Subject: [PATCH] added --no-artifact and --script and a small fix to the syscall word --- SPEC.md | 2 +- extra_tests/ct_test.compile.expected | 1 - main.py | 97 +++++++++++++++++++--------- 3 files changed, 68 insertions(+), 32 deletions(-) diff --git a/SPEC.md b/SPEC.md index 829e245..cb5eb87 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. 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. +- **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. Pass `--no-artifact` to stop after compilation/assembly emission without building an output file, or use `--script` as shorthand for `--no-artifact --ct-run-main`. - **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`. diff --git a/extra_tests/ct_test.compile.expected b/extra_tests/ct_test.compile.expected index 2e54142..30a2915 100644 --- a/extra_tests/ct_test.compile.expected +++ b/extra_tests/ct_test.compile.expected @@ -1,3 +1,2 @@ hello world [info] built /ct_test -[warn] redefining word syscall diff --git a/main.py b/main.py index d622190..0fcdc83 100644 --- a/main.py +++ b/main.py @@ -4312,7 +4312,9 @@ def _compile_syscall_stub(vm: CompileTimeVM) -> Any: # Stack protocol (matching _emit_syscall_intrinsic): # TOS: syscall number → rax # TOS-1: arg count → rcx - # then up to 6 args → rdi, rsi, rdx, r10, r8, r9 + # then args on stack as ... arg0 arg1 ... argN (argN is top) + # + lines = [ "_stub_entry:", " push rbx", @@ -4339,43 +4341,57 @@ def _compile_syscall_stub(vm: CompileTimeVM) -> Any: " jle _count_clamped", " mov rcx, 6", "_count_clamped:", - # Save arg count in r14 and syscall num in r15 - " mov r14, rcx", + # Save syscall num in r15 " mov r15, rax", - # Pop args into scratch area on machine stack (up to 6 qwords) - # We pop them into rbx, r8-r11 area, then assign to syscall regs after - # Pop all args onto machine stack (reverse order) - " sub rsp, 48", # 6 * 8 bytes for args - " xor rbx, rbx", # index - "_pop_args:", - " cmp rbx, r14", - " jge _pop_done", - " mov rax, [r12]", - " add r12, 8", - " mov [rsp + rbx*8], rax", - " inc rbx", - " jmp _pop_args", - "_pop_done:", # Check for exit (60) / exit_group (231) " cmp r15, 60", " je _do_exit", " cmp r15, 231", " je _do_exit", - # Assign args to syscall registers from the scratch area - # arg0 → rdi, arg1 → rsi, arg2 → rdx, arg3 → r10, arg4 → r8, arg5 → r9 - " mov rdi, [rsp]", - " mov rsi, [rsp+8]", - " mov rdx, [rsp+16]", - " mov r10, [rsp+24]", - " mov r8, [rsp+32]", - " mov r9, [rsp+40]", + # Clear syscall arg registers + " xor rdi, rdi", + " xor rsi, rsi", + " xor rdx, rdx", + " xor r10, r10", + " xor r8, r8", + " xor r9, r9", + # Pop args in the same order as _emit_syscall_intrinsic + " cmp rcx, 6", + " jl _skip_r9", + " mov r9, [r12]", + " add r12, 8", + "_skip_r9:", + " cmp rcx, 5", + " jl _skip_r8", + " mov r8, [r12]", + " add r12, 8", + "_skip_r8:", + " cmp rcx, 4", + " jl _skip_r10", + " mov r10, [r12]", + " add r12, 8", + "_skip_r10:", + " cmp rcx, 3", + " jl _skip_rdx", + " mov rdx, [r12]", + " add r12, 8", + "_skip_rdx:", + " cmp rcx, 2", + " jl _skip_rsi", + " mov rsi, [r12]", + " add r12, 8", + "_skip_rsi:", + " cmp rcx, 1", + " jl _skip_rdi", + " mov rdi, [r12]", + " add r12, 8", + "_skip_rdi:", " mov rax, r15", # syscall number " syscall", # Push result " sub r12, 8", " mov [r12], rax", # Normal return: flag=0 - " add rsp, 48", " mov rax, [rsp]", # output-struct pointer " mov qword [rax], r12", " mov qword [rax+8], r13", @@ -4384,8 +4400,12 @@ def _compile_syscall_stub(vm: CompileTimeVM) -> Any: " jmp _stub_epilogue", # Exit path: don't actually call syscall, just report it "_do_exit:", - " mov rbx, [rsp]", # arg0 = exit code - " add rsp, 48", + " xor rbx, rbx", + " cmp rcx, 1", + " jl _exit_code_ready", + " mov rbx, [r12]", # arg0 = exit code (for exit/exit_group) + " add r12, 8", + "_exit_code_ready:", " mov rax, [rsp]", # output-struct pointer " mov qword [rax], r12", " mov qword [rax+8], r13", @@ -4711,8 +4731,8 @@ class Compiler: word = self.dictionary.lookup("syscall") if word is None: word = Word(name="syscall") + self.dictionary.register(word) word.intrinsic = self._emit_syscall_intrinsic - self.dictionary.register(word) def _emit_syscall_intrinsic(self, builder: FunctionEmitter) -> None: label_id = self._syscall_label_counter @@ -5283,6 +5303,12 @@ def cli(argv: Sequence[str]) -> int: 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") + parser.add_argument("--no-artifact", action="store_true", help="compile source but skip producing final output artifact") + parser.add_argument( + "--script", + action="store_true", + help="shortcut for --no-artifact --ct-run-main", + ) # Parse known and unknown args to allow -l flags anywhere args, unknown = parser.parse_known_args(argv) @@ -5298,6 +5324,10 @@ def cli(argv: Sequence[str]) -> int: else: i += 1 + if args.script: + args.no_artifact = True + args.ct_run_main = True + artifact_kind = args.artifact folding_enabled = not args.no_folding @@ -5307,6 +5337,9 @@ def cli(argv: Sequence[str]) -> int: if artifact_kind != "exe" and (args.run or args.dbg): parser.error("--run/--dbg are only available when --artifact exe is selected") + if args.no_artifact and (args.run or args.dbg): + parser.error("--run/--dbg are not available with --no-artifact") + if args.clean: try: if args.temp_dir.exists(): @@ -5322,7 +5355,7 @@ def cli(argv: Sequence[str]) -> int: if args.source is None and not args.repl: parser.error("the following arguments are required: source") - if not args.repl and args.output is None: + if not args.repl and args.output is None and not args.no_artifact: stem = args.source.stem default_outputs = { "exe": Path("a.out"), @@ -5366,6 +5399,10 @@ def cli(argv: Sequence[str]) -> int: print(f"[info] wrote {asm_path}") return 0 + if args.no_artifact: + print("[info] skipped artifact generation (--no-artifact)") + return 0 + run_nasm(asm_path, obj_path, debug=args.debug) if args.output.parent and not args.output.parent.exists(): args.output.parent.mkdir(parents=True, exist_ok=True)