diff --git a/SPEC.md b/SPEC.md index cd3be18..a53de48 100644 --- a/SPEC.md +++ b/SPEC.md @@ -60,6 +60,7 @@ This document reflects the implementation that ships in this repository today (` - 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. + - Assertions: `static_assert` is a compile-time-only primitive that pops a condition and raises `ParseError("static assertion failed at ::")` when the value is zero/false. - **Text macros** – `macro` is an immediate word implemented in Python; it prevents nesting by tracking active recordings and registers expansion tokens with `$n` substitution. - **Python bridges** – `:py name { ... } ;` executes once during parsing. The body may define `macro(ctx: MacroContext)` (with helpers such as `next_token`, `emit_literal`, `inject_tokens`, `new_label`, and direct `parser` access) and/or `intrinsic(builder: FunctionEmitter)` to emit assembly directly. The `fn` DSL (`libs/fn.sl`) and other syntax layers are ordinary `:py` blocks. @@ -76,10 +77,10 @@ This document reflects the implementation that ships in this repository today (` - **`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`). -- **`arr.sl`** – Dynamically sized qword arrays with `arr_new`, `arr_len`, `arr_cap`, `arr_data`, `arr_push`, `arr_pop`, `arr_reserve`, `arr_free`. +- **`arr.sl`** – Dynamically sized qword arrays with `arr_new`, `arr_len`, `arr_cap`, `arr_data`, `arr_push`, `arr_pop`, `arr_reserve`, `arr_free`; built-in static-array sorting via `arr_sort`/`arr_sorted`; and dynamic-array sorting via `dyn_arr_sort`/`dyn_arr_sorted`. - **`float.sl`** – SSE-based double-precision arithmetic (`f+`, `f-`, `f*`, `f/`, `fneg`, comparisons, `int>float`, `float>int`, `fput`, `fputln`). - **`linux.sl`** – Auto-generated syscall macros (one constant block per entry in `syscall_64.tbl`) plus the `syscallN` helpers implemented purely in assembly so the file can be used in isolation. -- **`debug.sl`** – Diagnostics such as `dump`, `rdump`, and `int3`. +- **`debug.sl`** – Diagnostics and checks such as `dump`, `rdump`, `int3`, runtime `assert` (prints `assertion failed` and exits with code 1), `assert_msg` (message + condition; exits with message when false), `abort` (prints `abort` and exits with code 1), and `abort_msg` (prints caller-provided message and exits with code 1). - **`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 diff --git a/main.py b/main.py index 0c89cb1..5d1881d 100644 --- a/main.py +++ b/main.py @@ -1433,6 +1433,7 @@ class CompileTimeVM: self._dl_handles: List[Any] = [] # ctypes.CDLL handles self._dl_func_cache: Dict[str, Any] = {} # name → ctypes callable self._ct_libs: List[str] = [] # library names from -l flags + self.current_location: Optional[SourceLocation] = None def reset(self) -> None: self.stack.clear() @@ -1443,6 +1444,7 @@ class CompileTimeVM: self._list_capture_stack.clear() self.r12 = 0 self.r13 = 0 + self.current_location = None def invoke(self, word: Word, *, runtime_mode: bool = False, libs: Optional[List[str]] = None) -> None: self.reset() @@ -2194,8 +2196,14 @@ class CompileTimeVM: if defn._begin_pairs is None: defn._begin_pairs = self._begin_pairs(defn.body) self._resolve_words_in_body(defn) - if self.runtime_mode and defn._merged_runs is None: - defn._merged_runs = self._find_mergeable_runs(defn) + if self.runtime_mode: + # Merged JIT runs are a performance optimization, but have shown + # intermittent instability on some environments. Keep them opt-in. + if os.environ.get("L2_CT_MERGED_JIT", "0") == "1": + if defn._merged_runs is None: + defn._merged_runs = self._find_mergeable_runs(defn) + else: + defn._merged_runs = {} return defn._label_positions, defn._for_pairs, defn._begin_pairs def _find_mergeable_runs(self, defn: Definition) -> Dict[int, Tuple[int, str]]: @@ -2387,9 +2395,11 @@ class CompileTimeVM: n_nodes = len(nodes) ip = 0 + prev_location = self.current_location try: while ip < n_nodes: node = nodes[ip] + self.current_location = node.loc kind = node.op if kind == "word": @@ -2627,6 +2637,7 @@ class CompileTimeVM: raise ParseError(f"unsupported compile-time op {node!r}") finally: + self.current_location = prev_location self.loop_stack = prev_loop_stack def _label_positions(self, nodes: Sequence[Op]) -> Dict[str, int]: @@ -4579,6 +4590,23 @@ def _ct_parse_error(vm: CompileTimeVM) -> None: raise ParseError(message) +def _ct_static_assert(vm: CompileTimeVM) -> None: + condition = vm._resolve_handle(vm.pop()) + if isinstance(condition, bool): + ok = condition + elif isinstance(condition, int): + ok = condition != 0 + else: + raise ParseError( + f"static_assert expects integer/boolean condition, got {type(condition).__name__}" + ) + if not ok: + loc = vm.current_location + if loc is not None: + raise ParseError(f"static assertion failed at {loc.path}:{loc.line}:{loc.column}") + raise ParseError("static assertion failed") + + def _ct_lexer_new(vm: CompileTimeVM) -> None: separators = vm.pop_str() vm.push(SplitLexer(vm.parser, separators)) @@ -4902,6 +4930,7 @@ def _register_compile_time_primitives(dictionary: Dictionary) -> None: word_use_l2.immediate = True register("emit-definition", _ct_emit_definition, compile_only=True) register("parse-error", _ct_parse_error, compile_only=True) + register("static_assert", _ct_static_assert, compile_only=True) register("lexer-new", _ct_lexer_new, compile_only=True) register("lexer-pop", _ct_lexer_pop, compile_only=True) diff --git a/stdlib/arr.sl b/stdlib/arr.sl index b47af22..670fba1 100644 --- a/stdlib/arr.sl +++ b/stdlib/arr.sl @@ -127,3 +127,112 @@ end word arr_set arr_data swap 8 * + swap ! end + +#dyn_arr_clone [* | dyn_arr] -> [* | dyn_arr_copy] +word dyn_arr_clone + dup arr_len + dup arr_new + + dup arr_data + 3 pick arr_data + 3 pick + arr_copy_elements + + dup >r + swap ! + drop + r> +end + +#arr_item_ptr [*, i | arr] -> [* | ptr] +word arr_item_ptr + swap 8 * swap 8 + + +end + +#arr_get [*, i | arr] -> [* | x] +# Get element from built-in static array +word arr_get_static + arr_item_ptr @ +end + +#arr_set [*, x, i | arr] -> [*] +# Set element in built-in static array +word arr_set_static + arr_item_ptr swap ! +end + +#arr_sort [* | arr] -> [* | arr] +# Sort built-in static array in-place in ascending order +word arr_sort + dup >r + dup arr_to_dyn + dyn_arr_sort + dup arr_data + r@ 8 + + swap + r@ @ + arr_copy_elements + arr_free + rdrop +end + +#dyn_arr_sort [* | dyn_arr] -> [* | dyn_arr] +:asm dyn_arr_sort { + mov rbx, [r12] ; arr + mov rcx, [rbx] ; len + cmp rcx, 1 + jle .done + + dec rcx ; outer = len - 1 +.outer: + xor rdx, rdx ; j = 0 + +.inner: + cmp rdx, rcx + jge .next_outer + + mov r8, [rbx + 16] ; data ptr + lea r9, [r8 + rdx*8] ; &data[j] + mov r10, [r9] ; a = data[j] + mov r11, [r9 + 8] ; b = data[j+1] + cmp r10, r11 + jle .no_swap + + mov [r9], r11 + mov [r9 + 8], r10 + +.no_swap: + inc rdx + jmp .inner + +.next_outer: + dec rcx + jnz .outer + +.done: + ret +} +; + +#arr_clone [* | arr] -> [* | arr_copy] +# Clone built-in static array (len header + payload) +word arr_clone + dup @ 1 + + dup 8 * alloc + dup >r + rot rot + arr_copy_elements + r> +end + +#arr_sorted [* | arr] -> [* | arr_sorted] +word arr_sorted + arr_clone + arr_sort +end + +#dyn_arr_sorted [* | dyn_arr] -> [* | dyn_arr_sorted] +word dyn_arr_sorted + dyn_arr_clone + dyn_arr_sort +end diff --git a/stdlib/debug.sl b/stdlib/debug.sl index 917633e..db3e914 100644 --- a/stdlib/debug.sl +++ b/stdlib/debug.sl @@ -70,3 +70,32 @@ end } ; +#abort [*] -> [*] +word abort + "abort" eputs + 1 exit +end + +#abort_msg [* | msg] -> [*] +word abort_msg + eputs + 1 exit +end + +#assert [* | cond] -> [*] +word assert + if + else + "assertion failed" abort_msg + end +end + +#assert_msg [*, msg | cond] -> [*] +word assert_msg + if + 2drop + else + abort_msg + end +end + diff --git a/tests/arr_dynamic.expected b/tests/arr_dynamic.expected index e3b1791..f5c2cba 100644 --- a/tests/arr_dynamic.expected +++ b/tests/arr_dynamic.expected @@ -21,4 +21,19 @@ 6 7 8 -9 \ No newline at end of file +9 +1 +2 +3 +3 +1 +2 +1 +2 +3 +4 +6 +9 +4 +9 +6 diff --git a/tests/arr_dynamic.sl b/tests/arr_dynamic.sl index ea0b59c..721c4a7 100644 --- a/tests/arr_dynamic.sl +++ b/tests/arr_dynamic.sl @@ -56,4 +56,44 @@ word main # free list allocation: bytes = (len + 1) * 8 dup @ 1 + 8 * free + + # dyn_arr_sorted (copy) should not mutate source + 5 arr_new + 3 swap arr_push + 1 swap arr_push + 2 swap arr_push + + dup dyn_arr_sorted + dup 0 swap arr_get puti cr + dup 1 swap arr_get puti cr + dup 2 swap arr_get puti cr + arr_free + + dup 0 swap arr_get puti cr + dup 1 swap arr_get puti cr + dup 2 swap arr_get puti cr + + # dyn_arr_sort (alias) sorts in place + dyn_arr_sort + dup 0 swap arr_get puti cr + dup 1 swap arr_get puti cr + dup 2 swap arr_get puti cr + arr_free + + # dyn_arr_sorted (alias) returns a sorted copy + 5 arr_new + 4 swap arr_push + 9 swap arr_push + 6 swap arr_push + + dup dyn_arr_sorted + dup 0 swap arr_get puti cr + dup 1 swap arr_get puti cr + dup 2 swap arr_get puti cr + arr_free + + dup 0 swap arr_get puti cr + dup 1 swap arr_get puti cr + dup 2 swap arr_get puti cr + arr_free end diff --git a/tests/arr_static_sort.expected b/tests/arr_static_sort.expected new file mode 100644 index 0000000..889105e --- /dev/null +++ b/tests/arr_static_sort.expected @@ -0,0 +1,10 @@ +1 +2 +3 +4 +5 +7 +9 +9 +5 +7 diff --git a/tests/arr_static_sort.sl b/tests/arr_static_sort.sl new file mode 100644 index 0000000..f7261c6 --- /dev/null +++ b/tests/arr_static_sort.sl @@ -0,0 +1,29 @@ +import ../stdlib/stdlib.sl +import ../stdlib/io.sl +import ../stdlib/arr.sl + +word free_static + dup @ 1 + 8 * free +end + +word main + [ 4 1 3 2 ] dup arr_sort + dup 0 swap arr_get_static puti cr + dup 1 swap arr_get_static puti cr + dup 2 swap arr_get_static puti cr + dup 3 swap arr_get_static puti cr + free_static + + [ 9 5 7 ] dup arr_sorted + dup 0 swap arr_get_static puti cr + dup 1 swap arr_get_static puti cr + dup 2 swap arr_get_static puti cr + + swap + dup 0 swap arr_get_static puti cr + dup 1 swap arr_get_static puti cr + dup 2 swap arr_get_static puti cr + + free_static + free_static +end diff --git a/tests/assert_msg_fail.expected b/tests/assert_msg_fail.expected new file mode 100644 index 0000000..e69de29 diff --git a/tests/assert_msg_fail.meta.json b/tests/assert_msg_fail.meta.json new file mode 100644 index 0000000..ada3abb --- /dev/null +++ b/tests/assert_msg_fail.meta.json @@ -0,0 +1,3 @@ +{ + "expected_exit": 1 +} diff --git a/tests/assert_msg_fail.sl b/tests/assert_msg_fail.sl new file mode 100644 index 0000000..e3fb5ca --- /dev/null +++ b/tests/assert_msg_fail.sl @@ -0,0 +1,6 @@ +import stdlib/debug.sl + +word main + "boom msg" 0 assert_msg + 0 +end diff --git a/tests/assert_msg_fail.stderr b/tests/assert_msg_fail.stderr new file mode 100644 index 0000000..c7f6e1d --- /dev/null +++ b/tests/assert_msg_fail.stderr @@ -0,0 +1 @@ +boom msg diff --git a/tests/debug_assert.expected b/tests/debug_assert.expected new file mode 100644 index 0000000..f351add --- /dev/null +++ b/tests/debug_assert.expected @@ -0,0 +1,2 @@ +debug assert ok +assert_msg ok diff --git a/tests/debug_assert.sl b/tests/debug_assert.sl new file mode 100644 index 0000000..4f5814d --- /dev/null +++ b/tests/debug_assert.sl @@ -0,0 +1,10 @@ +import stdlib/debug.sl +import stdlib/io.sl + +word main + 1 assert + 2 2 == assert + "should not print" 1 assert_msg + "debug assert ok" puts + "assert_msg ok" puts +end diff --git a/tests/static_assert.expected b/tests/static_assert.expected new file mode 100644 index 0000000..8c0d5d5 --- /dev/null +++ b/tests/static_assert.expected @@ -0,0 +1 @@ +static assert ok diff --git a/tests/static_assert.sl b/tests/static_assert.sl new file mode 100644 index 0000000..39deb57 --- /dev/null +++ b/tests/static_assert.sl @@ -0,0 +1,12 @@ +import stdlib/debug.sl +import stdlib/io.sl + +word ct_checks + 1 static_assert + 2 3 < static_assert +end +compile-time ct_checks + +word main + "static assert ok" puts +end