From 78bf6c132f1acf86b27045654e30562eac2741fa Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 16 Mar 2026 10:30:53 +0100 Subject: [PATCH] added an option to redefine control structures --- SPEC.md | 7 +- main.py | 460 ++++++++++++++++++---- stdlib/control.sl | 137 +++++++ tests/custom_control_structures.expected | 5 + tests/custom_control_structures.sl | 36 ++ tests/i_non_compile_time.compile.expected | 1 + tests/i_non_compile_time.meta.json | 4 + tests/i_non_compile_time.sl | 8 + 8 files changed, 572 insertions(+), 86 deletions(-) create mode 100644 stdlib/control.sl create mode 100644 tests/custom_control_structures.expected create mode 100644 tests/custom_control_structures.sl create mode 100644 tests/i_non_compile_time.compile.expected create mode 100644 tests/i_non_compile_time.meta.json create mode 100644 tests/i_non_compile_time.sl diff --git a/SPEC.md b/SPEC.md index 6dbc70e..3ba9eaf 100644 --- a/SPEC.md +++ b/SPEC.md @@ -40,9 +40,11 @@ This document reflects the implementation that ships in this repository today (` - **Word definitions** – Always `word name ... end`. Redefinitions overwrite the previous entry (a warning prints to stderr). `inline word name ... end` marks the definition for inline expansion; recursive inline calls are rejected. `immediate` and `compile-only` apply to the most recently defined word. - **Priority-based redefinition** – Use `priority ` before `word`, `:asm`, `:py`, or `extern` to control conflicts for the same name. Higher priority wins; lower-priority definitions are ignored. Equal priority keeps last definition (with a redefinition warning). The compiler prints a note indicating which priority was selected. - **Control forms** – Built-in tokens drive code emission: + - Default parser-level implementations for `if`, `else`, `for`, `while`, and `do` are always available. + - Import `stdlib/control.sl` to override these defaults with custom compile-time words; when an override is active, the compiler warns and uses the custom implementation. - `if ... end` and `if ... else ... end`. To express additional branches, place `if` on the same line as the preceding `else` (e.g., `else if ...`); the reader treats that form as an implicit chained clause, so each inline `if` consumes one flag and jumps past later clauses on success. - `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. + - `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 and cannot be used in runtime-emitted words. - `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). @@ -57,6 +59,8 @@ This document reflects the implementation that ships in this repository today (` - Strings/numbers: `string=`, `string-length`, `string-append`, `string>number`, `int>string`. - Lexer utilities: `lexer-new`, `lexer-pop`, `lexer-peek`, `lexer-expect`, `lexer-collect-brace`, `lexer-push-back` (used by `libs/fn.sl` to parse signatures and infix expressions). - Token management: `next-token`, `peek-token`, `inject-tokens`, `token-lexeme`, `token-from-lexeme`. + - Control-frame helpers: `ct-control-frame-new`, `ct-control-get`, `ct-control-set`, `ct-control-push`, `ct-control-pop`, `ct-control-peek`, `ct-control-depth`, `ct-control-add-close-op`, `ct-new-label`, `ct-emit-op`, `ct-last-token-line`. + - Control registration: `ct-register-block-opener`, `ct-unregister-block-opener`, `ct-register-control-override`, `ct-unregister-control-override`. - Reader hooks: `set-token-hook` installs a word that receives each token (pushed as a `Token` object) and must leave a truthy handled flag; `clear-token-hook` disables it. `libs/fn.sl`'s `extend-syntax` demonstrates rewriting `foo(1, 2)` into ordinary word calls. - Prelude/BSS control: `prelude-clear`, `prelude-append`, `prelude-set`, `bss-clear`, `bss-append`, `bss-set` let user code override the `_start` stub or `.bss` layout. - Definition helpers: `emit-definition` injects a `word ... end` definition on the fly (used by the struct macro). `parse-error` raises a custom diagnostic. @@ -74,6 +78,7 @@ This document reflects the implementation that ships in this repository today (` ## 8. Standard Library Overview (`stdlib/`) - **`core.sl`** – Stack shuffles, integer arithmetic, comparisons, boolean ops, memory access, syscall stubs (`mmap`, `munmap`, `exit`), argument helpers (`argc`, `argv`, `argv@`), and pointer helpers (`mem`). +- **`control.sl`** – Optional custom control-structure words (`if`, `else`, `for`, `while`, `do`) that can override parser defaults when imported. - **`mem.sl`** – `alloc`/`free` wrappers around `mmap`/`munmap` plus a byte-wise `memcpy` used by higher-level utilities. - **`io.sl`** – `read_file`, `write_file`, `read_stdin`, `write_buf`, `ewrite_buf`, `putc`, `puti`, `puts`, `eputs`, and a smart `print` that detects `(addr,len)` pairs located inside the default `.data` region. - **`utils.sl`** – String and number helpers (`strcmp`, `strconcat`, `strlen`, `digitsN>num`, `toint`, `count_digits`, `tostr`). diff --git a/main.py b/main.py index 7eb8b2e..7a2c030 100644 --- a/main.py +++ b/main.py @@ -442,6 +442,7 @@ _PEEPHOLE_CANCEL_PAIRS = frozenset({ ("inc", "dec"), ("dec", "inc"), }) _PEEPHOLE_SHIFT_OPS = frozenset({"shl", "shr", "sar"}) +_DEFAULT_CONTROL_WORDS = frozenset({"if", "else", "for", "while", "do"}) class Op: @@ -776,6 +777,9 @@ class Parser: self.source: str = "" self.macro_recording: Optional[MacroDefinition] = None self.control_stack: List[Dict[str, str]] = [] + self.block_openers: Set[str] = {"word", "with", "for", "while", "begin"} + self.control_overrides: Set[str] = set() + self._warned_control_overrides: Set[str] = set() self.label_counter = 0 self.token_hook: Optional[str] = None self._last_token: Optional[Token] = None @@ -929,29 +933,43 @@ class Parser: return label, hidden_word def _handle_end_control(self) -> None: - """Handle unified 'end' for all block types""" + """Close one generic control frame pushed by compile-time words.""" if not self.control_stack: raise ParseError("unexpected 'end' without matching block") entry = self.control_stack.pop() + if not isinstance(entry, dict): + raise ParseError("invalid control frame") - if entry["type"] in ("if", "elif"): - # For if/elif without a trailing else - if "false" in entry: - self._append_op(_make_op("label", entry["false"])) - if "end" in entry: - self._append_op(_make_op("label", entry["end"])) - elif entry["type"] == "else": - self._append_op(_make_op("label", entry["end"])) - elif entry["type"] == "while": - self._append_op(_make_op("jump", entry["begin"])) - self._append_op(_make_op("label", entry["end"])) - elif entry["type"] == "for": - # Emit ForEnd node for loop decrement - self._append_op(_make_op("for_end", {"loop": entry["loop"], "end": entry["end"]})) - elif entry["type"] == "begin": - self._append_op(_make_op("jump", entry["begin"])) - self._append_op(_make_op("label", entry["end"])) + close_ops = entry.get("close_ops") + if close_ops is None: + return + if not isinstance(close_ops, list): + raise ParseError("control frame field 'close_ops' must be a list") + + for spec in close_ops: + op_name: Optional[str] = None + data: Any = None + if isinstance(spec, dict): + candidate = spec.get("op") + if isinstance(candidate, str): + op_name = candidate + if "data" in spec: + data = spec["data"] + elif isinstance(spec, (list, tuple)): + if not spec: + raise ParseError("close_ops contains empty sequence") + if isinstance(spec[0], str): + op_name = spec[0] + data = spec[1] if len(spec) > 1 else None + elif isinstance(spec, str): + op_name = spec + else: + raise ParseError(f"invalid close op descriptor: {spec!r}") + + if not op_name: + raise ParseError(f"close op missing valid 'op' name: {spec!r}") + self._append_op(_make_op(op_name, data)) # Parsing ------------------------------------------------------------------ def parse(self, tokens: Iterable[Token], source: str) -> Module: @@ -994,17 +1012,10 @@ class Parser: _KW_PY = 6 _KW_EXTERN = 7 _KW_PRIORITY = 8 - _KW_IF = 9 - _KW_ELSE = 10 - _KW_FOR = 11 - _KW_WHILE = 12 - _KW_DO = 13 _keyword_dispatch = { "[": _KW_LIST_BEGIN, "]": _KW_LIST_END, "word": _KW_WORD, "end": _KW_END, ":asm": _KW_ASM, ":py": _KW_PY, "extern": _KW_EXTERN, "priority": _KW_PRIORITY, - "if": _KW_IF, "else": _KW_ELSE, "for": _KW_FOR, - "while": _KW_WHILE, "do": _KW_DO, } _kw_get = _keyword_dispatch.get @@ -1054,16 +1065,8 @@ class Parser: self._parse_extern(token) elif kw == _KW_PRIORITY: self._parse_priority_directive(token) - elif kw == _KW_IF: - self._handle_if_control() - elif kw == _KW_ELSE: - self._handle_else_control() - elif kw == _KW_FOR: - self._handle_for_control() - elif kw == _KW_WHILE: - self._handle_while_control() - elif kw == _KW_DO: - self._handle_do_control() + continue + if self._try_handle_builtin_control(token): continue if self._handle_token(token): _tokens = self.tokens @@ -1120,6 +1123,161 @@ class Parser: label = entry["label"] self._append_op(_make_op("list_end", label)) + def _should_use_custom_control(self, lexeme: str) -> bool: + # Fast path: default parser controls unless explicitly overridden. + if lexeme not in self.control_overrides: + return False + word = self.dictionary.lookup(lexeme) + if word is None: + return False + return bool(word.immediate) + + def _warn_control_override(self, token: Token, lexeme: str) -> None: + if lexeme in self._warned_control_overrides: + return + self._warned_control_overrides.add(lexeme) + sys.stderr.write( + f"[warn] default control structure ({lexeme}) has been overridden; using custom implementation\n" + ) + + def _try_handle_builtin_control(self, token: Token) -> bool: + lexeme = token.lexeme + if lexeme not in _DEFAULT_CONTROL_WORDS: + return False + if self._should_use_custom_control(lexeme): + self._warn_control_override(token, lexeme) + return False + if lexeme == "if": + self._handle_builtin_if(token) + return True + if lexeme == "else": + self._handle_builtin_else(token) + return True + if lexeme == "for": + self._handle_builtin_for(token) + return True + if lexeme == "while": + self._handle_builtin_while(token) + return True + if lexeme == "do": + self._handle_builtin_do(token) + return True + return False + + def _handle_builtin_if(self, token: Token) -> None: + # Support shorthand `else if` by sharing the previous else-end label. + if self.control_stack: + top = self.control_stack[-1] + if ( + top.get("type") == "else" + and isinstance(top.get("line"), int) + and top["line"] == token.line + ): + prev_else = self._pop_control(("else",)) + shared_end = prev_else.get("end") + if not isinstance(shared_end, str): + shared_end = self._new_label("if_end") + false_label = self._new_label("if_false") + self._append_op(_make_op("branch_zero", false_label)) + self._push_control( + { + "type": "if", + "false": false_label, + "end": shared_end, + "close_ops": [ + {"op": "label", "data": false_label}, + {"op": "label", "data": shared_end}, + ], + "line": token.line, + "column": token.column, + } + ) + return + + false_label = self._new_label("if_false") + self._append_op(_make_op("branch_zero", false_label)) + self._push_control( + { + "type": "if", + "false": false_label, + "end": None, + "close_ops": [{"op": "label", "data": false_label}], + "line": token.line, + "column": token.column, + } + ) + + def _handle_builtin_else(self, token: Token) -> None: + entry = self._pop_control(("if",)) + false_label = entry.get("false") + if not isinstance(false_label, str): + raise ParseError("invalid if control frame") + end_label = entry.get("end") + if not isinstance(end_label, str): + end_label = self._new_label("if_end") + self._append_op(_make_op("jump", end_label)) + self._append_op(_make_op("label", false_label)) + self._push_control( + { + "type": "else", + "end": end_label, + "close_ops": [{"op": "label", "data": end_label}], + "line": token.line, + "column": token.column, + } + ) + + def _handle_builtin_for(self, token: Token) -> None: + loop_label = self._new_label("for_loop") + end_label = self._new_label("for_end") + frame = {"loop": loop_label, "end": end_label} + self._append_op(_make_op("for_begin", dict(frame))) + self._push_control( + { + "type": "for", + "loop": loop_label, + "end": end_label, + "close_ops": [{"op": "for_end", "data": dict(frame)}], + "line": token.line, + "column": token.column, + } + ) + + def _handle_builtin_while(self, token: Token) -> None: + begin_label = self._new_label("begin") + end_label = self._new_label("end") + self._append_op(_make_op("label", begin_label)) + self._push_control( + { + "type": "while_open", + "begin": begin_label, + "end": end_label, + "line": token.line, + "column": token.column, + } + ) + + def _handle_builtin_do(self, token: Token) -> None: + entry = self._pop_control(("while_open",)) + begin_label = entry.get("begin") + end_label = entry.get("end") + if not isinstance(begin_label, str) or not isinstance(end_label, str): + raise ParseError("invalid while control frame") + self._append_op(_make_op("branch_zero", end_label)) + self._push_control( + { + "type": "while", + "begin": begin_label, + "end": end_label, + "close_ops": [ + {"op": "jump", "data": begin_label}, + {"op": "label", "data": end_label}, + ], + "line": token.line, + "column": token.column, + } + ) + def _parse_priority_directive(self, token: Token) -> None: if self._eof(): raise ParseError(f"priority value missing at {token.line}:{token.column}") @@ -1469,52 +1627,6 @@ class Parser: handled = self.compile_time_vm.pop() return bool(handled) - def _handle_if_control(self) -> None: - token = self._last_token - if ( - self.control_stack - and self.control_stack[-1]["type"] == "else" - and token is not None - and self.control_stack[-1].get("line") == token.line - ): - entry = self.control_stack.pop() - end_label = entry.get("end") - if end_label is None: - end_label = self._new_label("if_end") - false_label = self._new_label("if_false") - self._append_op(_make_op("branch_zero", false_label)) - self._push_control({"type": "elif", "false": false_label, "end": end_label}) - return - false_label = self._new_label("if_false") - self._append_op(_make_op("branch_zero", false_label)) - self._push_control({"type": "if", "false": false_label}) - - def _handle_else_control(self) -> None: - entry = self._pop_control(("if", "elif")) - end_label = entry.get("end") - if end_label is None: - end_label = self._new_label("if_end") - self._append_op(_make_op("jump", end_label)) - self._append_op(_make_op("label", entry["false"])) - self._push_control({"type": "else", "end": end_label}) - - def _handle_for_control(self) -> None: - loop_label = self._new_label("for_loop") - end_label = self._new_label("for_end") - self._append_op(_make_op("for_begin", {"loop": loop_label, "end": end_label})) - self._push_control({"type": "for", "loop": loop_label, "end": end_label}) - - def _handle_while_control(self) -> None: - begin_label = self._new_label("begin") - end_label = self._new_label("end") - self._append_op(_make_op("label", begin_label)) - self._push_control({"type": "begin", "begin": begin_label, "end": end_label}) - - def _handle_do_control(self) -> None: - entry = self._pop_control(("begin",)) - self._append_op(_make_op("branch_zero", entry["end"])) - self._push_control(entry) - def _try_end_definition(self, token: Token) -> bool: if len(self.context_stack) <= 1: return False @@ -6702,7 +6814,8 @@ class Assembler: suffix = f" while emitting '{self._emit_stack[-1]}'" if self._emit_stack else "" raise CompileError(f"unknown word '{name}'{suffix}") if word.compile_only: - return # silently skip compile-time-only words during emission + suffix = f" while emitting '{self._emit_stack[-1]}'" if self._emit_stack else "" + raise CompileError(f"word '{name}' is compile-time only and cannot be used at runtime{suffix}") if getattr(word, "inline", False): if isinstance(word.definition, Definition): if word.name in self._inline_stack: @@ -6957,9 +7070,7 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]: depth -= 1 body.append(tok) continue - if tok.lexeme in ("with", "for", "while", "begin", "word"): - depth += 1 - elif tok.lexeme == "if": + if tok.lexeme == "if": # Support shorthand elif form `else if` inside with-blocks. # This inline `if` shares the same closing `end` as the preceding # branch and therefore must not increment nesting depth. @@ -6967,6 +7078,8 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]: depth += 1 elif tok.lexeme == "else": else_line = tok.line + elif tok.lexeme in parser.block_openers: + depth += 1 body.append(tok) helper_for: Dict[str, str] = {} @@ -7299,6 +7412,105 @@ def _ct_loop_index(vm: CompileTimeVM) -> None: vm.push(idx) +def _ct_control_frame_new(vm: CompileTimeVM) -> None: + type_name = vm.pop_str() + vm.push({"type": type_name}) + + +def _ct_control_get(vm: CompileTimeVM) -> None: + key = vm.pop_str() + frame = vm.pop() + if not isinstance(frame, dict): + raise ParseError("ct-control-get expects a control frame") + vm.push(frame.get(key)) + + +def _ct_control_set(vm: CompileTimeVM) -> None: + value = vm.pop() + key = vm.pop_str() + frame = vm.pop() + if not isinstance(frame, dict): + raise ParseError("ct-control-set expects a control frame") + frame[key] = value + vm.push(frame) + + +def _ct_control_push(vm: CompileTimeVM) -> None: + frame = vm.pop() + if not isinstance(frame, dict): + raise ParseError("ct-control-push expects a control frame") + vm.parser._push_control(dict(frame)) + + +def _ct_control_pop(vm: CompileTimeVM) -> None: + if not vm.parser.control_stack: + raise ParseError("control stack underflow") + vm.push(dict(vm.parser.control_stack.pop())) + + +def _ct_control_peek(vm: CompileTimeVM) -> None: + if not vm.parser.control_stack: + vm.push(None) + return + vm.push(dict(vm.parser.control_stack[-1])) + + +def _ct_control_depth(vm: CompileTimeVM) -> None: + vm.push(len(vm.parser.control_stack)) + + +def _ct_new_label(vm: CompileTimeVM) -> None: + prefix = vm.pop_str() + vm.push(vm.parser._new_label(prefix)) + + +def _ct_emit_op(vm: CompileTimeVM) -> None: + data = vm.pop() + op_name = vm.pop_str() + vm.parser.emit_node(_make_op(op_name, data)) + + +def _ct_control_add_close_op(vm: CompileTimeVM) -> None: + data = vm.pop() + op_name = vm.pop_str() + frame = vm.pop() + if not isinstance(frame, dict): + raise ParseError("ct-control-add-close-op expects a control frame") + close_ops = frame.get("close_ops") + if close_ops is None: + close_ops = [] + elif not isinstance(close_ops, list): + raise ParseError("control frame field 'close_ops' must be a list") + close_ops.append({"op": op_name, "data": data}) + frame["close_ops"] = close_ops + vm.push(frame) + + +def _ct_last_token_line(vm: CompileTimeVM) -> None: + tok = vm.parser._last_token + vm.push(0 if tok is None else tok.line) + + +def _ct_register_block_opener(vm: CompileTimeVM) -> None: + name = vm.pop_str() + vm.parser.block_openers.add(name) + + +def _ct_unregister_block_opener(vm: CompileTimeVM) -> None: + name = vm.pop_str() + vm.parser.block_openers.discard(name) + + +def _ct_register_control_override(vm: CompileTimeVM) -> None: + name = vm.pop_str() + vm.parser.control_overrides.add(name) + + +def _ct_unregister_control_override(vm: CompileTimeVM) -> None: + name = vm.pop_str() + vm.parser.control_overrides.discard(name) + + def _ct_list_get(vm: CompileTimeVM) -> None: index = vm.pop_int() lst = _ensure_list(vm.pop()) @@ -7897,6 +8109,21 @@ def _register_compile_time_primitives(dictionary: Dictionary) -> None: register("list-extend", _ct_list_extend, compile_only=True) register("list-last", _ct_list_last, compile_only=True) register("i", _ct_loop_index, compile_only=True) + register("ct-control-frame-new", _ct_control_frame_new, compile_only=True) + register("ct-control-get", _ct_control_get, compile_only=True) + register("ct-control-set", _ct_control_set, compile_only=True) + register("ct-control-push", _ct_control_push, compile_only=True) + register("ct-control-pop", _ct_control_pop, compile_only=True) + register("ct-control-peek", _ct_control_peek, compile_only=True) + register("ct-control-depth", _ct_control_depth, compile_only=True) + register("ct-control-add-close-op", _ct_control_add_close_op, compile_only=True) + register("ct-new-label", _ct_new_label, compile_only=True) + register("ct-emit-op", _ct_emit_op, compile_only=True) + register("ct-last-token-line", _ct_last_token_line, compile_only=True) + register("ct-register-block-opener", _ct_register_block_opener, compile_only=True) + register("ct-unregister-block-opener", _ct_unregister_block_opener, compile_only=True) + register("ct-register-control-override", _ct_register_control_override, compile_only=True) + register("ct-unregister-control-override", _ct_unregister_control_override, compile_only=True) register("prelude-clear", _ct_prelude_clear, compile_only=True) register("prelude-append", _ct_prelude_append, compile_only=True) @@ -10802,6 +11029,54 @@ def _run_docs_tui( " word end\n" " into the parser's token stream.\n" "\n" + " ── Control-frame helpers (for custom control structures)\n" + "\n" + " ct-control-frame-new [* | type] -> [* | frame]\n" + " Create a control frame map with a `type` field.\n" + "\n" + " ct-control-get [*, frame | key] -> [* | value]\n" + " Read key from a control frame map.\n" + "\n" + " ct-control-set [*, frame, key | value] -> [* | frame]\n" + " Write key/value into a control frame map.\n" + "\n" + " ct-control-push [* | frame] -> [*]\n" + " Push a frame onto the parser control stack.\n" + "\n" + " ct-control-pop [*] -> [* | frame]\n" + " Pop and return the top parser control frame.\n" + "\n" + " ct-control-peek [*] -> [* | frame] || [* | nil]\n" + " Return the top parser control frame without popping.\n" + "\n" + " ct-control-depth [*] -> [* | n]\n" + " Return parser control-stack depth.\n" + "\n" + " ct-control-add-close-op [*, frame, op | data] -> [* | frame]\n" + " Append a close operation descriptor to frame.close_ops.\n" + "\n" + " ct-new-label [* | prefix] -> [* | label]\n" + " Allocate a fresh internal label with the given prefix.\n" + "\n" + " ct-emit-op [*, op | data] -> [*]\n" + " Emit an internal op node directly into the current body.\n" + "\n" + " ct-last-token-line [*] -> [* | line]\n" + " Return line number of the last parser token (or 0).\n" + "\n" + " ct-register-block-opener [* | name] -> [*]\n" + " Mark a word name as a block opener for `with` nesting.\n" + "\n" + " ct-unregister-block-opener [* | name] -> [*]\n" + " Remove a word name from block opener registration.\n" + "\n" + " ct-register-control-override [* | name] -> [*]\n" + " Register a control word override so parser can delegate\n" + " built-in control handling to custom compile-time words.\n" + "\n" + " ct-unregister-control-override [* | name] -> [*]\n" + " Remove a control word override registration.\n" + "\n" "\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" " § 7 LEXER OBJECTS\n" @@ -11020,6 +11295,21 @@ def _run_docs_tui( " add-token Token [* | s] -> [*]\n" " add-token-chars Token [* | s] -> [*]\n" " emit-definition Token [*, name | body] -> [*]\n" + " ct-control-frame-new Control [* | type] -> [* | frame]\n" + " ct-control-get Control [*, frame | key] -> [* | value]\n" + " ct-control-set Control [*, frame, key | value] -> [* | frame]\n" + " ct-control-push Control [* | frame] -> [*]\n" + " ct-control-pop Control [*] -> [* | frame]\n" + " ct-control-peek Control [*] -> [* | frame]\n" + " ct-control-depth Control [*] -> [* | n]\n" + " ct-control-add-close-op Control [*, frame, op | data] -> [* | frame]\n" + " ct-new-label Control [* | prefix] -> [* | label]\n" + " ct-emit-op Control [*, op | data] -> [*]\n" + " ct-last-token-line Control [*] -> [* | line]\n" + " ct-register-block-opener Control [* | name] -> [*]\n" + " ct-unregister-block-opener Control [* | name] -> [*]\n" + " ct-register-control-override Control [* | name] -> [*]\n" + " ct-unregister-control-override Control [* | name] -> [*]\n" " set-token-hook Hook [* | name] -> [*]\n" " clear-token-hook Hook [*] -> [*]\n" " prelude-clear Assembly [*] -> [*]\n" diff --git a/stdlib/control.sl b/stdlib/control.sl new file mode 100644 index 0000000..9eba261 --- /dev/null +++ b/stdlib/control.sl @@ -0,0 +1,137 @@ +# Optional control-structure overrides for L2 parser defaults. +# Import this file when you want custom compile-time implementations of +# if/else/for/while/do instead of the built-in Python parser behavior. + +word ct-if-open + "if_false" ct-new-label + dup "branch_zero" swap ct-emit-op + "if" ct-control-frame-new + swap "false" swap ct-control-set + nil "end" swap ct-control-set + dup "false" ct-control-get "label" swap ct-control-add-close-op + ct-control-push +end +compile-only + +word ct-if-open-with-end + "if_false" ct-new-label + dup "branch_zero" swap ct-emit-op + "if" ct-control-frame-new + swap "false" swap ct-control-set + swap "end" swap ct-control-set + dup "false" ct-control-get "label" swap ct-control-add-close-op + dup "end" ct-control-get "label" swap ct-control-add-close-op + ct-control-push +end +compile-only + +word if-base ct-if-open end +immediate +compile-only + +word if + ct-control-depth 0 > if-base + ct-control-peek + dup "type" ct-control-get "else" string= if-base + dup "line" ct-control-get ct-last-token-line == if-base + drop + ct-control-pop >r + r@ "end" ct-control-get dup nil? if-base + drop "if_end" ct-new-label + end + ct-if-open-with-end + r> drop + exit + end + end + drop + end + ct-if-open +end +immediate +compile-only + +word else + ct-control-pop >r + r@ "end" ct-control-get dup nil? if-base + drop "if_end" ct-new-label + end + dup "jump" swap ct-emit-op + r@ "false" ct-control-get "label" swap ct-emit-op + "else" ct-control-frame-new + swap "end" swap ct-control-set + dup "end" ct-control-get "label" swap ct-control-add-close-op + ct-control-push + r> drop +end +immediate +compile-only + +word for + "for_loop" ct-new-label + "for_end" ct-new-label + map-new + "loop" 3 pick map-set + "end" 2 pick map-set + "for_begin" swap ct-emit-op + "for" ct-control-frame-new + swap "end" swap ct-control-set + swap "loop" swap ct-control-set + dup "end" ct-control-get >r + dup "loop" ct-control-get >r + map-new + "loop" r> map-set + "end" r> map-set + "for_end" swap ct-control-add-close-op + ct-control-push +end +immediate +compile-only + +word while + "begin" ct-new-label + "end" ct-new-label + over "label" swap ct-emit-op + "while_open" ct-control-frame-new + swap "end" swap ct-control-set + swap "begin" swap ct-control-set + ct-control-push +end +immediate +compile-only + +word do + ct-control-pop >r + r@ "end" ct-control-get "branch_zero" swap ct-emit-op + "while" ct-control-frame-new + r@ "begin" ct-control-get "begin" swap ct-control-set + r@ "end" ct-control-get "end" swap ct-control-set + dup "begin" ct-control-get "jump" swap ct-control-add-close-op + dup "end" ct-control-get "label" swap ct-control-add-close-op + r> drop + ct-control-push +end +immediate +compile-only + +word block-opener + next-token token-lexeme ct-register-block-opener +end +immediate +compile-only + +word control-override + next-token token-lexeme ct-register-control-override +end +immediate +compile-only + +block-opener if +block-opener for +block-opener while + +control-override if +control-override else +control-override for +control-override while +control-override do diff --git a/tests/custom_control_structures.expected b/tests/custom_control_structures.expected new file mode 100644 index 0000000..11ab32f --- /dev/null +++ b/tests/custom_control_structures.expected @@ -0,0 +1,5 @@ +11 +22 +33 +5 +3 diff --git a/tests/custom_control_structures.sl b/tests/custom_control_structures.sl new file mode 100644 index 0000000..1667754 --- /dev/null +++ b/tests/custom_control_structures.sl @@ -0,0 +1,36 @@ +import stdlib/stdlib.sl +import stdlib/control.sl + +word main + 1 if + 11 puti cr + else + 99 puti cr + end + + 0 if + 99 puti cr + else + 22 puti cr + end + + 0 if + 500 puti cr + else 1 if + 33 puti cr + else + 44 puti cr + end + + 0 + 5 for + 1 + + end + puti cr + + 0 + while dup 3 < do + 1 + + end + puti cr +end diff --git a/tests/i_non_compile_time.compile.expected b/tests/i_non_compile_time.compile.expected new file mode 100644 index 0000000..9117061 --- /dev/null +++ b/tests/i_non_compile_time.compile.expected @@ -0,0 +1 @@ +[error] word 'i' is compile-time only and cannot be used at runtime while emitting 'main' diff --git a/tests/i_non_compile_time.meta.json b/tests/i_non_compile_time.meta.json new file mode 100644 index 0000000..53cb8f8 --- /dev/null +++ b/tests/i_non_compile_time.meta.json @@ -0,0 +1,4 @@ +{ + "expect_compile_error": true, + "description": "'i' is compile-time only and rejected in runtime code" +} diff --git a/tests/i_non_compile_time.sl b/tests/i_non_compile_time.sl new file mode 100644 index 0000000..1ac1667 --- /dev/null +++ b/tests/i_non_compile_time.sl @@ -0,0 +1,8 @@ +import stdlib/stdlib.sl + +word main + 0 + 3 for + i puti cr + end +end