diff --git a/SPEC.md b/SPEC.md index 93dedbf..a0f9bad 100644 --- a/SPEC.md +++ b/SPEC.md @@ -43,6 +43,7 @@ This document reflects the implementation that ships in this repository today (` - `while do end`; the conditional block lives between `while` and `do` and re-runs every iteration. - `n for ... end`; the loop count is popped, stored on the return stack, and decremented each pass. The compile-time word `i` exposes the loop index inside macros. - `label name` / `goto name` perform local jumps within a definition. + - `&name` pushes a pointer to word `name` (its callable code label). This is intended for indirect control flow; `&name jmp` performs a tail jump to that word and is compatible with `--ct-run-main`. - **Text macros** – `macro name [param_count] ... ;` records raw tokens until `;`. `$0`, `$1`, ... expand to positional arguments. Macro definitions cannot nest (attempting to start another `macro` while recording raises a parse error). - **Struct builder** – `struct Foo ... end` emits `.size`, `.field.size`, `.field.offset`, `.field@`, and `.field!` helpers. Layout is tightly packed with no implicit padding. - **With-blocks** – `with a b in ... end` rewrites occurrences of `a`/`b` into accesses against hidden global cells (`__with_a`). On entry the block pops the named values and stores them in those cells; reads compile to `@`, writes to `!`. Because the cells live in `.data`, the slots persist across calls and are not re-entrant. diff --git a/main.py b/main.py index 6eee6f2..ca78306 100644 --- a/main.py +++ b/main.py @@ -742,6 +742,13 @@ class Parser: if self._try_literal(token): return + if token.lexeme.startswith("&"): + target_name = token.lexeme[1:] + if not target_name: + raise ParseError(f"missing word name after '&' at {token.line}:{token.column}") + self._append_op(Op(op="word_ptr", data=target_name)) + return + word = self.dictionary.lookup(token.lexeme) if word and word.immediate: if word.macro: @@ -1174,6 +1181,10 @@ class _CTVMJump(Exception): self.target_ip = target_ip +class _CTVMReturn(Exception): + """Raised to return from the current word frame in _execute_nodes.""" + + class _CTVMExit(Exception): """Raised by the ``exit`` intrinsic to stop compile-time execution.""" @@ -1708,7 +1719,7 @@ class CompileTimeVM: self._execute_nodes(definition.body, _defn=definition) except CompileTimeError: raise - except (_CTVMJump, _CTVMExit): + except (_CTVMJump, _CTVMExit, _CTVMReturn): raise except ParseError as exc: raise CompileTimeError(f"{exc}\ncompile-time stack: {' -> '.join(self.call_stack)}") from None @@ -2341,6 +2352,9 @@ class CompileTimeVM: self.call_stack.pop() ip = jmp.target_ip continue + except _CTVMReturn: + self.call_stack.pop() + return finally: if self.call_stack and self.call_stack[-1] == word.name: self.call_stack.pop() @@ -2365,6 +2379,8 @@ class CompileTimeVM: except _CTVMJump as jmp: ip = jmp.target_ip continue + except _CTVMReturn: + return ip += 1 continue @@ -2406,6 +2422,19 @@ class CompileTimeVM: except _CTVMJump as jmp: ip = jmp.target_ip continue + except _CTVMReturn: + return + ip += 1 + continue + + if kind == "word_ptr": + target_name = str(node.data) + target_word = _dict_lookup(target_name) + if target_word is None: + raise ParseError( + f"unknown word '{target_name}' referenced by pointer during compile-time execution" + ) + _push(self._handles.store(target_word)) ip += 1 continue @@ -3002,7 +3031,7 @@ class Assembler: refs: Set[str] = set() if isinstance(definition, Definition): for node in definition.body: - if node.op == "word": + if node.op in {"word", "word_ptr"}: refs.add(str(node.data)) edges[definition.name] = refs @@ -3261,6 +3290,10 @@ class Assembler: self._emit_wordref(str(data), builder) return + if kind == "word_ptr": + self._emit_wordptr(str(data), builder) + return + if kind == "branch_zero": self._emit_branch_zero(str(data), builder) return @@ -3438,6 +3471,16 @@ class Assembler: else: builder.emit(f" call {sanitize_label(name)}") + def _emit_wordptr(self, name: str, builder: FunctionEmitter) -> None: + word = self.dictionary.lookup(name) + if word is None: + suffix = f" while emitting '{self._emit_stack[-1]}'" if self._emit_stack else "" + raise CompileError(f"unknown word '{name}'{suffix}") + if getattr(word, "is_extern", False): + builder.push_label(name) + return + builder.push_label(sanitize_label(name)) + def _emit_branch_zero(self, target: str, builder: FunctionEmitter) -> None: builder.pop_to("rax") builder.emit(" test rax, rax") @@ -4279,8 +4322,18 @@ def _rt_exit(vm: CompileTimeVM) -> None: def _rt_jmp(vm: CompileTimeVM) -> None: - target_ip = vm.pop_int() - raise _CTVMJump(target_ip) + target = vm.pop() + resolved = vm._resolve_handle(target) + if isinstance(resolved, Word): + vm._call_word(resolved) + raise _CTVMReturn() + if isinstance(resolved, bool): + raise _CTVMJump(int(resolved)) + if not isinstance(resolved, int): + raise ParseError( + f"jmp expects an address or word pointer, got {type(resolved).__name__}: {resolved!r}" + ) + raise _CTVMJump(resolved) def _rt_syscall(vm: CompileTimeVM) -> None: diff --git a/tests/word_ptr_jmp.expected b/tests/word_ptr_jmp.expected new file mode 100644 index 0000000..069291f --- /dev/null +++ b/tests/word_ptr_jmp.expected @@ -0,0 +1 @@ +via word ptr diff --git a/tests/word_ptr_jmp.sl b/tests/word_ptr_jmp.sl new file mode 100644 index 0000000..ca908ad --- /dev/null +++ b/tests/word_ptr_jmp.sl @@ -0,0 +1,11 @@ +import stdlib/stdlib.sl +import stdlib/io.sl + +word target + "via word ptr\n" puts +end + +word main + &target + jmp +end