This commit is contained in:
igor
2026-02-18 16:05:48 +01:00
parent cd1df4b18d
commit f3de51f5d8
16 changed files with 302 additions and 5 deletions

View File

@@ -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. - 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. - 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. - 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 <path>:<line>:<column>")` 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. - **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. - **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. - **`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. - **`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`). - **`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`). - **`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. - **`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`. - **`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 ## 9. Testing and Usage Patterns

31
main.py
View File

@@ -1433,6 +1433,7 @@ class CompileTimeVM:
self._dl_handles: List[Any] = [] # ctypes.CDLL handles self._dl_handles: List[Any] = [] # ctypes.CDLL handles
self._dl_func_cache: Dict[str, Any] = {} # name → ctypes callable self._dl_func_cache: Dict[str, Any] = {} # name → ctypes callable
self._ct_libs: List[str] = [] # library names from -l flags self._ct_libs: List[str] = [] # library names from -l flags
self.current_location: Optional[SourceLocation] = None
def reset(self) -> None: def reset(self) -> None:
self.stack.clear() self.stack.clear()
@@ -1443,6 +1444,7 @@ class CompileTimeVM:
self._list_capture_stack.clear() self._list_capture_stack.clear()
self.r12 = 0 self.r12 = 0
self.r13 = 0 self.r13 = 0
self.current_location = None
def invoke(self, word: Word, *, runtime_mode: bool = False, libs: Optional[List[str]] = None) -> None: def invoke(self, word: Word, *, runtime_mode: bool = False, libs: Optional[List[str]] = None) -> None:
self.reset() self.reset()
@@ -2194,8 +2196,14 @@ class CompileTimeVM:
if defn._begin_pairs is None: if defn._begin_pairs is None:
defn._begin_pairs = self._begin_pairs(defn.body) defn._begin_pairs = self._begin_pairs(defn.body)
self._resolve_words_in_body(defn) self._resolve_words_in_body(defn)
if self.runtime_mode and defn._merged_runs is None: 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) defn._merged_runs = self._find_mergeable_runs(defn)
else:
defn._merged_runs = {}
return defn._label_positions, defn._for_pairs, defn._begin_pairs return defn._label_positions, defn._for_pairs, defn._begin_pairs
def _find_mergeable_runs(self, defn: Definition) -> Dict[int, Tuple[int, str]]: def _find_mergeable_runs(self, defn: Definition) -> Dict[int, Tuple[int, str]]:
@@ -2387,9 +2395,11 @@ class CompileTimeVM:
n_nodes = len(nodes) n_nodes = len(nodes)
ip = 0 ip = 0
prev_location = self.current_location
try: try:
while ip < n_nodes: while ip < n_nodes:
node = nodes[ip] node = nodes[ip]
self.current_location = node.loc
kind = node.op kind = node.op
if kind == "word": if kind == "word":
@@ -2627,6 +2637,7 @@ class CompileTimeVM:
raise ParseError(f"unsupported compile-time op {node!r}") raise ParseError(f"unsupported compile-time op {node!r}")
finally: finally:
self.current_location = prev_location
self.loop_stack = prev_loop_stack self.loop_stack = prev_loop_stack
def _label_positions(self, nodes: Sequence[Op]) -> Dict[str, int]: def _label_positions(self, nodes: Sequence[Op]) -> Dict[str, int]:
@@ -4579,6 +4590,23 @@ def _ct_parse_error(vm: CompileTimeVM) -> None:
raise ParseError(message) 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: def _ct_lexer_new(vm: CompileTimeVM) -> None:
separators = vm.pop_str() separators = vm.pop_str()
vm.push(SplitLexer(vm.parser, separators)) vm.push(SplitLexer(vm.parser, separators))
@@ -4902,6 +4930,7 @@ def _register_compile_time_primitives(dictionary: Dictionary) -> None:
word_use_l2.immediate = True word_use_l2.immediate = True
register("emit-definition", _ct_emit_definition, compile_only=True) register("emit-definition", _ct_emit_definition, compile_only=True)
register("parse-error", _ct_parse_error, 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-new", _ct_lexer_new, compile_only=True)
register("lexer-pop", _ct_lexer_pop, compile_only=True) register("lexer-pop", _ct_lexer_pop, compile_only=True)

View File

@@ -127,3 +127,112 @@ end
word arr_set word arr_set
arr_data swap 8 * + swap ! arr_data swap 8 * + swap !
end 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

View File

@@ -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

View File

@@ -22,3 +22,18 @@
7 7
8 8
9 9
1
2
3
3
1
2
1
2
3
4
6
9
4
9
6

View File

@@ -56,4 +56,44 @@ word main
# free list allocation: bytes = (len + 1) * 8 # free list allocation: bytes = (len + 1) * 8
dup @ 1 + 8 * free 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 end

View File

@@ -0,0 +1,10 @@
1
2
3
4
5
7
9
9
5
7

29
tests/arr_static_sort.sl Normal file
View File

@@ -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

View File

View File

@@ -0,0 +1,3 @@
{
"expected_exit": 1
}

6
tests/assert_msg_fail.sl Normal file
View File

@@ -0,0 +1,6 @@
import stdlib/debug.sl
word main
"boom msg" 0 assert_msg
0
end

View File

@@ -0,0 +1 @@
boom msg

View File

@@ -0,0 +1,2 @@
debug assert ok
assert_msg ok

10
tests/debug_assert.sl Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
static assert ok

12
tests/static_assert.sl Normal file
View File

@@ -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