Compare commits

..

10 Commits

28 changed files with 7038 additions and 15282 deletions

View File

@@ -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. - **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 <int>` 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. - **Priority-based redefinition** Use `priority <int>` 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: - **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 <condition> 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. - `if ... end` and `if ... else ... end`. To express additional branches, place `if` on the same line as the preceding `else` (e.g., `else <condition> 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 <condition> do <body> end`; the conditional block lives between `while` and `do` and re-runs every iteration. - `while <condition> do <body> 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. - `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`. - `&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). - **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`. - 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). - 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`. - 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. - 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.
@@ -74,6 +78,7 @@ This document reflects the implementation that ships in this repository today (`
## 8. Standard Library Overview (`stdlib/`) ## 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`). - **`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. - **`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`).

613
examples/snake.sl Normal file
View File

@@ -0,0 +1,613 @@
# Terminal Snake (classic real-time: WASD steer, q quit)
import stdlib.sl
import arr.sl
import linux.sl
macro WIDTH 0 20 ;
macro HEIGHT 0 12 ;
macro CELLS 0 WIDTH HEIGHT * ;
macro CH_W 0 119 ;
macro CH_A 0 97 ;
macro CH_S 0 115 ;
macro CH_D 0 100 ;
macro CH_Q 0 113 ;
macro CH_w 0 87 ;
macro CH_a 0 65 ;
macro CH_s 0 83 ;
macro CH_d 0 68 ;
macro CH_q 0 81 ;
macro FRAME_DELAY_NS 0 350000000 ;
macro TCGETS 0 21505 ;
macro TCSETS 0 21506 ;
macro LFLAG_OFF 0 12 ;
macro ECHO 0 8 ;
macro ICANON 0 2 ;
# state layout (qwords)
macro ST_DIR 0 0 ;
macro ST_LEN 0 8 ;
macro ST_FOOD_X 0 16 ;
macro ST_FOOD_Y 0 24 ;
macro ST_GAME_OVER 0 32 ;
macro ST_QUIT 0 40 ;
macro ST_WIN 0 48 ;
# direction constants
macro DIR_RIGHT 0 0 ;
macro DIR_DOWN 0 1 ;
macro DIR_LEFT 0 2 ;
macro DIR_UP 0 3 ;
#xy_idx [*, x | y] -> [* | idx]
word xy_idx
WIDTH * +
end
#board_get [*, board, x | y] -> [* | value]
word board_get
xy_idx
1 - arr_get
end
#board_set [*, board, x, y | value] -> [*]
word board_set
>r
xy_idx
r> swap 1 - arr_set
end
#state_dir@ [* | state] -> [* | dir]
word state_dir@
ST_DIR + @
end
#state_dir! [*, state | dir] -> [*]
word state_dir!
swap ST_DIR + swap !
end
#state_len@ [* | state] -> [* | len]
word state_len@
ST_LEN + @
end
#state_len! [*, state | len] -> [*]
word state_len!
swap ST_LEN + swap !
end
#state_food_x@ [* | state] -> [* | x]
word state_food_x@
ST_FOOD_X + @
end
#state_food_x! [*, state | x] -> [*]
word state_food_x!
swap ST_FOOD_X + swap !
end
#state_food_y@ [* | state] -> [* | y]
word state_food_y@
ST_FOOD_Y + @
end
#state_food_y! [*, state | y] -> [*]
word state_food_y!
swap ST_FOOD_Y + swap !
end
#state_game_over@ [* | state] -> [* | flag]
word state_game_over@
ST_GAME_OVER + @
end
#state_game_over! [*, state | flag] -> [*]
word state_game_over!
swap ST_GAME_OVER + swap !
end
#state_quit@ [* | state] -> [* | flag]
word state_quit@
ST_QUIT + @
end
#state_quit! [*, state | flag] -> [*]
word state_quit!
swap ST_QUIT + swap !
end
#state_win@ [* | state] -> [* | flag]
word state_win@
ST_WIN + @
end
#state_win! [*, state | flag] -> [*]
word state_win!
swap ST_WIN + swap !
end
#term_enter [*] -> [*]
word term_enter
# Enter alternate screen: ESC[?1049h
27 putc 91 putc 63 putc 49 putc 48 putc 52 putc 57 putc 104 putc
# Hide cursor: ESC[?25l
27 putc 91 putc 63 putc 50 putc 53 putc 108 putc
end
#term_raw_on [*, orig | work] -> [*]
:asm term_raw_on {
; stack: orig (NOS), work (TOS)
mov r14, [r12] ; work
mov r15, [r12 + 8] ; orig
add r12, 16
; ioctl(0, TCGETS, orig)
mov rax, 16
mov rdi, 0
mov rsi, 21505
mov rdx, r15
syscall
; copy 64 bytes orig -> work
mov rcx, 8
mov rsi, r15
mov rdi, r14
.copy_loop:
mov rbx, [rsi]
mov [rdi], rbx
add rsi, 8
add rdi, 8
loop .copy_loop
; clear ECHO | ICANON in c_lflag (offset 12)
mov eax, [r14 + 12]
and eax, 0xFFFFFFF5
mov [r14 + 12], eax
; c_cc[VTIME]=0 (offset 17+5), c_cc[VMIN]=0 (offset 17+6)
mov byte [r14 + 22], 0
mov byte [r14 + 23], 0
; ioctl(0, TCSETS, work)
mov rax, 16
mov rdi, 0
mov rsi, 21506
mov rdx, r14
syscall
}
;
#stdin_nonblock_on [* | old_flags_ptr] -> [*]
:asm stdin_nonblock_on {
mov r14, [r12]
add r12, 8
; old_flags = fcntl(0, F_GETFL, 0)
mov rax, 72
mov rdi, 0
mov rsi, 3
xor rdx, rdx
syscall
mov [r14], rax
; fcntl(0, F_SETFL, old_flags | O_NONBLOCK)
mov rbx, rax
or rbx, 2048
mov rax, 72
mov rdi, 0
mov rsi, 4
mov rdx, rbx
syscall
}
;
#stdin_nonblock_off [* | old_flags_ptr] -> [*]
:asm stdin_nonblock_off {
mov r14, [r12]
add r12, 8
mov rax, 72
mov rdi, 0
mov rsi, 4
mov rdx, [r14]
syscall
}
;
#sleep_tick [* | ts_ptr] -> [*]
:asm sleep_tick {
mov r14, [r12]
add r12, 8
; nanosleep(ts_ptr, NULL)
mov rax, 35
mov rdi, r14
xor rsi, rsi
syscall
}
;
#wait [* | ts_ptr] -> [*]
word wait
sleep_tick
end
#term_raw_off [* | orig] -> [*]
:asm term_raw_off {
mov r14, [r12]
add r12, 8
mov rax, 16
mov rdi, 0
mov rsi, 21506
mov rdx, r14
syscall
}
;
#term_leave [*] -> [*]
word term_leave
# Show cursor: ESC[?25h
27 putc 91 putc 63 putc 50 putc 53 putc 104 putc
# Leave alternate screen: ESC[?1049l
27 putc 91 putc 63 putc 49 putc 48 putc 52 putc 57 putc 108 putc
end
#clear_screen_home [*] -> [*]
word clear_screen_home
# Clear full screen: ESC[2J
27 putc 91 putc 50 putc 74 putc
# Move cursor home: ESC[H
27 putc 91 putc 72 putc
end
#clear_board [* | board] -> [*]
word clear_board
0
while dup CELLS < do
over over 8 * + 0 !
1 +
end
drop
drop
end
#init_state [* | state] -> [*]
word init_state
dup DIR_RIGHT state_dir!
dup 3 state_len!
dup 0 state_food_x!
dup 0 state_food_y!
dup 0 state_game_over!
dup 0 state_quit!
dup 0 state_win!
drop
end
#init_snake [*, board, xs | ys] -> [*]
word init_snake
with b xs ys in
WIDTH 2 /
HEIGHT 2 /
with cx cy in
xs 0 cx swap 1 - arr_set
ys 0 cy swap 1 - arr_set
b cx cy 1 board_set
xs 1 cx 1 - swap 1 - arr_set
ys 1 cy swap 1 - arr_set
b cx 1 - cy 1 board_set
xs 2 cx 2 - swap 1 - arr_set
ys 2 cy swap 1 - arr_set
b cx 2 - cy 1 board_set
end
end
end
#spawn_food [*, board | state] -> [*]
word spawn_food
with b s in
rand syscall.getpid + CELLS %
0
0
with start tried found in
while tried CELLS < do
start tried + CELLS %
dup b swap 1 - arr_get 0 == if
dup WIDTH % s swap state_food_x!
dup WIDTH / s swap state_food_y!
drop
1 found !
CELLS tried !
else
drop
tried 1 + tried !
end
end
found 0 == if
s 1 state_win!
end
end
end
end
#draw_game [*, board, xs, ys | state] -> [*]
word draw_game
with b xs ys s in
"Snake (WASD to steer, q to quit)" puts
"Score: " puts
s state_len@ 3 - puti
10 putc
xs drop
ys drop
0
while dup HEIGHT < do
0
while dup WIDTH < do
over s state_food_y@ == if
dup s state_food_x@ == if
42 putc
else
over WIDTH * over +
b swap 1 - arr_get
if 111 putc else 46 putc end
end
else
over WIDTH * over +
b swap 1 - arr_get
if 111 putc else 46 putc end
end
1 +
end
drop
10 putc
1 +
end
drop
s state_game_over@ if
"Game over!" puts
end
s state_win@ if
"You win!" puts
end
end
end
#read_input [*, input_buf | state] -> [*]
word read_input
with ibuf s in
FD_STDIN ibuf 8 syscall.read
dup 0 <= if
drop
else
drop
ibuf c@
dup CH_Q == if
drop
s 1 state_quit!
else dup CH_q == if
drop
s 1 state_quit!
else dup CH_W == if
drop
s state_dir@ DIR_DOWN != if
s DIR_UP state_dir!
end
else dup CH_w == if
drop
s state_dir@ DIR_DOWN != if
s DIR_UP state_dir!
end
else dup CH_S == if
drop
s state_dir@ DIR_UP != if
s DIR_DOWN state_dir!
end
else dup CH_s == if
drop
s state_dir@ DIR_UP != if
s DIR_DOWN state_dir!
end
else dup CH_A == if
drop
s state_dir@ DIR_RIGHT != if
s DIR_LEFT state_dir!
end
else dup CH_a == if
drop
s state_dir@ DIR_RIGHT != if
s DIR_LEFT state_dir!
end
else dup CH_D == if
drop
s state_dir@ DIR_LEFT != if
s DIR_RIGHT state_dir!
end
else dup CH_d == if
drop
s state_dir@ DIR_LEFT != if
s DIR_RIGHT state_dir!
end
else
drop
end
end
end
end
#step_game [*, board, xs, ys | state] -> [*]
word step_game
with b xs ys s in
xs 0 1 - arr_get
ys 0 1 - arr_get
with hx hy in
hx
hy
# Compute next head from direction.
s state_dir@ DIR_RIGHT == if
drop
hx 1 +
hy
else s state_dir@ DIR_DOWN == if
drop
hx
hy 1 +
else s state_dir@ DIR_LEFT == if
drop
hx 1 -
hy
else
drop
hx
hy 1 -
end
with nx ny in
# dead flag from wall collision
0
nx 0 < if drop 1 end
nx WIDTH >= if drop 1 end
ny 0 < if drop 1 end
ny HEIGHT >= if drop 1 end
with dead in
dead if
s 1 state_game_over!
else
# grow flag
0
nx s state_food_x@ == if
ny s state_food_y@ == if
drop 1
end
end
with grow in
# when not growing, remove tail before collision check
grow 0 == if
s state_len@ 1 -
with ti in
xs ti 1 - arr_get
ys ti 1 - arr_get
with tx ty in
b tx ty 0 board_set
end
end
end
# self collision
b nx ny board_get if
s 1 state_game_over!
else
# shift body
s state_len@
grow if
# start at len for growth
else
1 -
end
while dup 0 > do
dup >r
xs r@ xs r@ 2 - arr_get swap 1 - arr_set
ys r@ ys r@ 2 - arr_get swap 1 - arr_set
rdrop
1 -
end
drop
# write new head
xs 0 nx swap 1 - arr_set
ys 0 ny swap 1 - arr_set
b nx ny 1 board_set
grow if
s state_len@ 1 + s swap state_len!
b s spawn_food
end
end
end
end
end
end
end
end
end
word main
CELLS 8 * alloc
CELLS 8 * alloc
CELLS 8 * alloc
56 alloc
8 alloc
64 alloc
64 alloc
8 alloc
16 alloc
with board xs ys state input term_orig term_work fd_flags sleep_ts in
board clear_board
state init_state
board xs ys init_snake
board state spawn_food
sleep_ts 0 !
sleep_ts 8 + FRAME_DELAY_NS !
term_orig term_work term_raw_on
fd_flags stdin_nonblock_on
term_enter
1
while dup do
drop
clear_screen_home
board xs ys state draw_game
state state_game_over@ if
0
else state state_win@ if
0
else state state_quit@ if
0
else
input state read_input
state state_quit@ if
0
else
board xs ys state step_game
sleep_ts wait
1
end
end
end
drop
clear_screen_home
board xs ys state draw_game
fd_flags stdin_nonblock_off
term_orig term_raw_off
term_leave
sleep_ts 16 free
fd_flags 8 free
term_work 64 free
term_orig 64 free
input 8 free
state 56 free
ys CELLS 8 * free
xs CELLS 8 * free
board CELLS 8 * free
end
0
end

View File

@@ -48,7 +48,6 @@ word sh
! !
syscall.fork syscall.fork
syscall
dup 0 < if dup 0 < if
>r >r
1 rpick 1 rpick
@@ -67,11 +66,9 @@ word sh
dup dup
32 + 32 +
syscall.execve syscall.execve
syscall
drop drop
127 127
syscall.exit syscall.exit
syscall
else else
mem mem
40 + 40 +
@@ -79,7 +76,6 @@ word sh
0 0
0 0
syscall.wait4 syscall.wait4
syscall
dup 0 < if dup 0 < if
>r >r
rdrop rdrop

621
main.py
View File

@@ -344,6 +344,7 @@ OP_LIST_BEGIN = 8
OP_LIST_END = 9 OP_LIST_END = 9
OP_LIST_LITERAL = 10 OP_LIST_LITERAL = 10
OP_OTHER = 11 OP_OTHER = 11
OP_RET = 12
_OP_STR_TO_INT = { _OP_STR_TO_INT = {
"word": OP_WORD, "word": OP_WORD,
@@ -357,6 +358,7 @@ _OP_STR_TO_INT = {
"list_begin": OP_LIST_BEGIN, "list_begin": OP_LIST_BEGIN,
"list_end": OP_LIST_END, "list_end": OP_LIST_END,
"list_literal": OP_LIST_LITERAL, "list_literal": OP_LIST_LITERAL,
"ret": OP_RET,
} }
@@ -442,6 +444,7 @@ _PEEPHOLE_CANCEL_PAIRS = frozenset({
("inc", "dec"), ("dec", "inc"), ("inc", "dec"), ("dec", "inc"),
}) })
_PEEPHOLE_SHIFT_OPS = frozenset({"shl", "shr", "sar"}) _PEEPHOLE_SHIFT_OPS = frozenset({"shl", "shr", "sar"})
_DEFAULT_CONTROL_WORDS = frozenset({"if", "else", "for", "while", "do"})
class Op: class Op:
@@ -776,6 +779,9 @@ class Parser:
self.source: str = "" self.source: str = ""
self.macro_recording: Optional[MacroDefinition] = None self.macro_recording: Optional[MacroDefinition] = None
self.control_stack: List[Dict[str, str]] = [] 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.label_counter = 0
self.token_hook: Optional[str] = None self.token_hook: Optional[str] = None
self._last_token: Optional[Token] = None self._last_token: Optional[Token] = None
@@ -929,29 +935,43 @@ class Parser:
return label, hidden_word return label, hidden_word
def _handle_end_control(self) -> None: 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: if not self.control_stack:
raise ParseError("unexpected 'end' without matching block") raise ParseError("unexpected 'end' without matching block")
entry = self.control_stack.pop() entry = self.control_stack.pop()
if not isinstance(entry, dict):
raise ParseError("invalid control frame")
if entry["type"] in ("if", "elif"): close_ops = entry.get("close_ops")
# For if/elif without a trailing else if close_ops is None:
if "false" in entry: return
self._append_op(_make_op("label", entry["false"])) if not isinstance(close_ops, list):
if "end" in entry: raise ParseError("control frame field 'close_ops' must be a list")
self._append_op(_make_op("label", entry["end"]))
elif entry["type"] == "else": for spec in close_ops:
self._append_op(_make_op("label", entry["end"])) op_name: Optional[str] = None
elif entry["type"] == "while": data: Any = None
self._append_op(_make_op("jump", entry["begin"])) if isinstance(spec, dict):
self._append_op(_make_op("label", entry["end"])) candidate = spec.get("op")
elif entry["type"] == "for": if isinstance(candidate, str):
# Emit ForEnd node for loop decrement op_name = candidate
self._append_op(_make_op("for_end", {"loop": entry["loop"], "end": entry["end"]})) if "data" in spec:
elif entry["type"] == "begin": data = spec["data"]
self._append_op(_make_op("jump", entry["begin"])) elif isinstance(spec, (list, tuple)):
self._append_op(_make_op("label", entry["end"])) 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 ------------------------------------------------------------------ # Parsing ------------------------------------------------------------------
def parse(self, tokens: Iterable[Token], source: str) -> Module: def parse(self, tokens: Iterable[Token], source: str) -> Module:
@@ -994,20 +1014,13 @@ class Parser:
_KW_PY = 6 _KW_PY = 6
_KW_EXTERN = 7 _KW_EXTERN = 7
_KW_PRIORITY = 8 _KW_PRIORITY = 8
_KW_IF = 9 _KW_RET = 9
_KW_ELSE = 10
_KW_FOR = 11
_KW_WHILE = 12
_KW_DO = 13
_keyword_dispatch = { _keyword_dispatch = {
"[": _KW_LIST_BEGIN, "]": _KW_LIST_END, "word": _KW_WORD, "[": _KW_LIST_BEGIN, "]": _KW_LIST_END, "word": _KW_WORD,
"end": _KW_END, ":asm": _KW_ASM, ":py": _KW_PY, "end": _KW_END, ":asm": _KW_ASM, ":py": _KW_PY,
"extern": _KW_EXTERN, "priority": _KW_PRIORITY, "extern": _KW_EXTERN, "priority": _KW_PRIORITY, "ret": _KW_RET,
"if": _KW_IF, "else": _KW_ELSE, "for": _KW_FOR,
"while": _KW_WHILE, "do": _KW_DO,
} }
_kw_get = _keyword_dispatch.get _kw_get = _keyword_dispatch.get
_tokens = self.tokens _tokens = self.tokens
try: try:
while self.pos < len(_tokens): while self.pos < len(_tokens):
@@ -1054,16 +1067,10 @@ class Parser:
self._parse_extern(token) self._parse_extern(token)
elif kw == _KW_PRIORITY: elif kw == _KW_PRIORITY:
self._parse_priority_directive(token) self._parse_priority_directive(token)
elif kw == _KW_IF: elif kw == _KW_RET:
self._handle_if_control() self._handle_ret(token)
elif kw == _KW_ELSE: continue
self._handle_else_control() if self._try_handle_builtin_control(token):
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 continue
if self._handle_token(token): if self._handle_token(token):
_tokens = self.tokens _tokens = self.tokens
@@ -1120,6 +1127,161 @@ class Parser:
label = entry["label"] label = entry["label"]
self._append_op(_make_op("list_end", 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 <cond> 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: def _parse_priority_directive(self, token: Token) -> None:
if self._eof(): if self._eof():
raise ParseError(f"priority value missing at {token.line}:{token.column}") raise ParseError(f"priority value missing at {token.line}:{token.column}")
@@ -1139,6 +1301,9 @@ class Parser:
self._pending_priority = None self._pending_priority = None
return value return value
def _handle_ret(self, token: Token) -> None:
self._append_op(_make_op("ret", loc=token))
# Internal helpers --------------------------------------------------------- # Internal helpers ---------------------------------------------------------
def _parse_extern(self, token: Token) -> None: def _parse_extern(self, token: Token) -> None:
@@ -1469,52 +1634,6 @@ class Parser:
handled = self.compile_time_vm.pop() handled = self.compile_time_vm.pop()
return bool(handled) 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: def _try_end_definition(self, token: Token) -> bool:
if len(self.context_stack) <= 1: if len(self.context_stack) <= 1:
return False return False
@@ -3886,6 +4005,7 @@ class CompileTimeVM:
_OP_JUMP = OP_JUMP _OP_JUMP = OP_JUMP
_OP_LABEL = OP_LABEL _OP_LABEL = OP_LABEL
_OP_LIST_BEGIN = OP_LIST_BEGIN _OP_LIST_BEGIN = OP_LIST_BEGIN
_OP_RET = OP_RET
_OP_LIST_END = OP_LIST_END _OP_LIST_END = OP_LIST_END
_OP_LIST_LITERAL = OP_LIST_LITERAL _OP_LIST_LITERAL = OP_LIST_LITERAL
try: try:
@@ -4189,6 +4309,9 @@ class CompileTimeVM:
ip += 1 ip += 1
continue continue
if kind == _OP_RET:
return
self.current_location = _node.loc self.current_location = _node.loc
raise ParseError(f"unsupported compile-time op (opcode={kind})") raise ParseError(f"unsupported compile-time op (opcode={kind})")
finally: finally:
@@ -4313,9 +4436,21 @@ class FunctionEmitter:
escaped = path.replace("\\", "\\\\").replace('"', '\\"') escaped = path.replace("\\", "\\\\").replace('"', '\\"')
self.text.append(f'%line {line}+{increment} "{escaped}"') self.text.append(f'%line {line}+{increment} "{escaped}"')
def set_location(self, loc: Optional[SourceLocation]) -> None: def set_location(self, loc) -> None:
if not self.debug_enabled: if not self.debug_enabled:
return return
# Defensive: if loc is a Token, convert to SourceLocation, did not have a better solution, works for me
if loc is not None and not hasattr(loc, 'path') and hasattr(loc, 'line') and hasattr(loc, 'column'):
# Assume self has a reference to the parser or a location_for_token function
# If not, fallback to generic source path
try:
loc = self.location_for_token(loc)
except Exception:
from pathlib import Path
loc = type('SourceLocation', (), {})()
loc.path = Path('<source>')
loc.line = getattr(loc, 'line', 0)
loc.column = getattr(loc, 'column', 0)
if loc is None: if loc is None:
if self._current_loc is None: if self._current_loc is None:
return return
@@ -4362,6 +4497,9 @@ class FunctionEmitter:
_a(f" mov {register}, [r12]") _a(f" mov {register}, [r12]")
_a(" add r12, 8") _a(" add r12, 8")
def ret(self) -> None:
self.text.append(" ret")
def _int_trunc_div(lhs: int, rhs: int) -> int: def _int_trunc_div(lhs: int, rhs: int) -> int:
if rhs == 0: if rhs == 0:
@@ -6442,6 +6580,10 @@ class Assembler:
builder.emit(" mov [r12], rax") builder.emit(" mov [r12], rax")
return return
if kind == OP_RET:
builder.ret()
return
raise CompileError(f"unsupported op {node!r} while emitting '{self._emit_stack[-1]}'" if self._emit_stack else f"unsupported op {node!r}") raise CompileError(f"unsupported op {node!r} while emitting '{self._emit_stack[-1]}'" if self._emit_stack else f"unsupported op {node!r}")
def _emit_mmap_alloc(self, builder: FunctionEmitter, size: int, target_reg: str = "rax") -> None: def _emit_mmap_alloc(self, builder: FunctionEmitter, size: int, target_reg: str = "rax") -> None:
@@ -6702,7 +6844,8 @@ class Assembler:
suffix = f" while emitting '{self._emit_stack[-1]}'" if self._emit_stack else "" suffix = f" while emitting '{self._emit_stack[-1]}'" if self._emit_stack else ""
raise CompileError(f"unknown word '{name}'{suffix}") raise CompileError(f"unknown word '{name}'{suffix}")
if word.compile_only: 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 getattr(word, "inline", False):
if isinstance(word.definition, Definition): if isinstance(word.definition, Definition):
if word.name in self._inline_stack: if word.name in self._inline_stack:
@@ -6943,18 +7086,29 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]:
raise ParseError("'with' requires at least one variable name") raise ParseError("'with' requires at least one variable name")
body: List[Token] = [] body: List[Token] = []
else_line: Optional[int] = None
depth = 0 depth = 0
while True: while True:
if parser._eof(): if parser._eof():
raise ParseError("unterminated 'with' block (missing 'end')") raise ParseError("unterminated 'with' block (missing 'end')")
tok = parser.next_token() tok = parser.next_token()
if else_line is not None and tok.line != else_line:
else_line = None
if tok.lexeme == "end": if tok.lexeme == "end":
if depth == 0: if depth == 0:
break break
depth -= 1 depth -= 1
body.append(tok) body.append(tok)
continue continue
if tok.lexeme in ("with", "if", "for", "while", "begin", "word"): if tok.lexeme == "if":
# Support shorthand elif form `else <cond> if` inside with-blocks.
# This inline `if` shares the same closing `end` as the preceding
# branch and therefore must not increment nesting depth.
if else_line != tok.line:
depth += 1
elif tok.lexeme == "else":
else_line = tok.line
elif tok.lexeme in parser.block_openers:
depth += 1 depth += 1
body.append(tok) body.append(tok)
@@ -6963,14 +7117,26 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]:
_, helper = parser.allocate_variable(name) _, helper = parser.allocate_variable(name)
helper_for[name] = helper helper_for[name] = helper
emitted: List[str] = [] emitted_tokens: List[Token] = []
def _emit_lex(lex: str, src_tok: Optional[Token] = None) -> None:
base = src_tok or template or Token(lexeme="", line=0, column=0, start=0, end=0)
emitted_tokens.append(
Token(
lexeme=lex,
line=base.line,
column=base.column,
start=base.start,
end=base.end,
)
)
# Initialize variables by storing current stack values into their buffers # Initialize variables by storing current stack values into their buffers
for name in reversed(names): for name in reversed(names):
helper = helper_for[name] helper = helper_for[name]
emitted.append(helper) _emit_lex(helper, template)
emitted.append("swap") _emit_lex("swap", template)
emitted.append("!") _emit_lex("!", template)
i = 0 i = 0
while i < len(body): while i < len(body):
@@ -6980,23 +7146,23 @@ def macro_with(ctx: MacroContext) -> Optional[List[Op]]:
if helper is not None: if helper is not None:
next_tok = body[i + 1] if i + 1 < len(body) else None next_tok = body[i + 1] if i + 1 < len(body) else None
if next_tok is not None and next_tok.lexeme == "!": if next_tok is not None and next_tok.lexeme == "!":
emitted.append(helper) _emit_lex(helper, tok)
emitted.append("swap") _emit_lex("swap", tok)
emitted.append("!") _emit_lex("!", tok)
i += 2 i += 2
continue continue
if next_tok is not None and next_tok.lexeme == "@": if next_tok is not None and next_tok.lexeme == "@":
emitted.append(helper) _emit_lex(helper, tok)
i += 1 i += 1
continue continue
emitted.append(helper) _emit_lex(helper, tok)
emitted.append("@") _emit_lex("@", tok)
i += 1 i += 1
continue continue
emitted.append(tok.lexeme) _emit_lex(tok.lexeme, tok)
i += 1 i += 1
ctx.inject_tokens(emitted, template=template) ctx.inject_token_objects(emitted_tokens)
return None return None
@@ -7276,6 +7442,105 @@ def _ct_loop_index(vm: CompileTimeVM) -> None:
vm.push(idx) 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: def _ct_list_get(vm: CompileTimeVM) -> None:
index = vm.pop_int() index = vm.pop_int()
lst = _ensure_list(vm.pop()) lst = _ensure_list(vm.pop())
@@ -7874,6 +8139,21 @@ def _register_compile_time_primitives(dictionary: Dictionary) -> None:
register("list-extend", _ct_list_extend, compile_only=True) register("list-extend", _ct_list_extend, compile_only=True)
register("list-last", _ct_list_last, compile_only=True) register("list-last", _ct_list_last, compile_only=True)
register("i", _ct_loop_index, 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-clear", _ct_prelude_clear, compile_only=True)
register("prelude-append", _ct_prelude_append, compile_only=True) register("prelude-append", _ct_prelude_append, compile_only=True)
@@ -8427,6 +8707,68 @@ class Compiler:
word.intrinsic = self._emit_syscall_intrinsic word.intrinsic = self._emit_syscall_intrinsic
def _emit_syscall_intrinsic(self, builder: FunctionEmitter) -> None: def _emit_syscall_intrinsic(self, builder: FunctionEmitter) -> None:
def _try_pop_known_syscall_setup() -> Optional[Tuple[int, int]]:
"""Recognize and remove literal setup for known-argc syscalls.
Supported forms right before `syscall`:
1) <argc> <nr>
2) <nr> <argc> ___linux_swap
Returns (argc, nr) when recognized.
"""
# Form 1: ... push argc ; push nr ; syscall
nr = Assembler._pop_preceding_literal(builder)
if nr is not None:
argc = Assembler._pop_preceding_literal(builder)
if argc is not None and 0 <= argc <= 6:
return argc, nr
# rollback if second literal wasn't argc
builder.push_literal(nr)
# Form 2: ... push nr ; push argc ; ___linux_swap ; syscall
text = builder.text
swap_tail = [
"mov rax, [r12]",
"mov rbx, [r12 + 8]",
"mov [r12], rbx",
"mov [r12 + 8], rax",
]
if len(text) >= 4 and [s.strip() for s in text[-4:]] == swap_tail:
del text[-4:]
argc2 = Assembler._pop_preceding_literal(builder)
nr2 = Assembler._pop_preceding_literal(builder)
if argc2 is not None and nr2 is not None and 0 <= argc2 <= 6:
return argc2, nr2
# rollback conservatively if match fails
if nr2 is not None:
builder.push_literal(nr2)
if argc2 is not None:
builder.push_literal(argc2)
text.extend(swap_tail)
return None
known = _try_pop_known_syscall_setup()
if known is not None:
argc, nr = known
builder.push_literal(nr)
builder.pop_to("rax")
if argc >= 6:
builder.pop_to("r9")
if argc >= 5:
builder.pop_to("r8")
if argc >= 4:
builder.pop_to("r10")
if argc >= 3:
builder.pop_to("rdx")
if argc >= 2:
builder.pop_to("rsi")
if argc >= 1:
builder.pop_to("rdi")
builder.emit(" syscall")
builder.push_from("rax")
return
label_id = self._syscall_label_counter label_id = self._syscall_label_counter
self._syscall_label_counter += 1 self._syscall_label_counter += 1
@@ -10164,6 +10506,26 @@ def _run_docs_tui(
" 3 1 syscall # 3 args, nr=1 (write)" " 3 1 syscall # 3 args, nr=1 (write)"
), ),
}, },
{
"name": "ret",
"category": "Control Flow",
"syntax": "ret",
"summary": "Return from a word",
"detail": (
"Returns from a word.\n\n"
"Example:\n"
" word a\n"
" \"g\" puts\n"
" ret\n"
" \"g\" puts\n"
" end\n\n"
" word main\n"
" a\n"
" end\n"
"Output:\n"
" g\n"
),
},
{ {
"name": "exit", "name": "exit",
"category": "System", "category": "System",
@@ -10717,6 +11079,54 @@ def _run_docs_tui(
" word <name> <body...> end\n" " word <name> <body...> end\n"
" into the parser's token stream.\n" " into the parser's token stream.\n"
"\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"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
" § 7 LEXER OBJECTS\n" " § 7 LEXER OBJECTS\n"
@@ -10935,6 +11345,21 @@ def _run_docs_tui(
" add-token Token [* | s] -> [*]\n" " add-token Token [* | s] -> [*]\n"
" add-token-chars Token [* | s] -> [*]\n" " add-token-chars Token [* | s] -> [*]\n"
" emit-definition Token [*, name | body] -> [*]\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" " set-token-hook Hook [* | name] -> [*]\n"
" clear-token-hook Hook [*] -> [*]\n" " clear-token-hook Hook [*] -> [*]\n"
" prelude-clear Assembly [*] -> [*]\n" " prelude-clear Assembly [*] -> [*]\n"

View File

@@ -39,8 +39,8 @@ word arr_cap 8 + @ end
#arr_data [* | arr] -> [* | ptr] #arr_data [* | arr] -> [* | ptr]
word arr_data 16 + @ end word arr_data 16 + @ end
#arr_free [* | arr] -> [*] #dyn_arr_free [* | arr] -> [*]
word arr_free word dyn_arr_free
dup arr_cap 8 * 24 + free dup arr_cap 8 * 24 + free
end end
@@ -81,7 +81,7 @@ word arr_reserve
arr_copy_elements arr_copy_elements
# Free old and return new # Free old and return new
swap arr_free swap dyn_arr_free
nip nip
end end
end end
@@ -116,15 +116,15 @@ word arr_pop
end end
end end
#arr_get [*, arr | i] -> [* | x] #dyn_arr_get [*, arr | i] -> [* | x]
# Get element at index i # Get element at index i
word arr_get word dyn_arr_get
swap arr_data swap 8 * + @ swap arr_data swap 8 * + @
end end
#arr_set [*, arr, x | i] -> [*] #dyn_arr_set [*, arr, x | i] -> [*]
# Set element at index i to x # Set element at index i to x
word arr_set word dyn_arr_set
rot arr_data swap 8 * + swap ! rot arr_data swap 8 * + swap !
end end
@@ -149,21 +149,21 @@ word arr_item_ptr
swap 8 * swap 8 + + swap 8 * swap 8 + +
end end
#arr_get_static [*, arr | i] -> [* | x] #arr_get [*, arr | i] -> [* | x]
# Get element from built-in static array # Get element from built-in static array
word arr_get_static word arr_get
swap arr_item_ptr @ swap arr_item_ptr @
end end
#arr_set_static [*, arr, x | i] -> [*] #arr_set [*, arr, x | i] -> [*]
# Set element in built-in static array # Set element in built-in static array
word arr_set_static word arr_set
rot arr_item_ptr swap ! rot arr_item_ptr swap !
end end
#arr_static_free [* | arr] -> [*] #arr_free [* | arr] -> [*]
# Free built-in static array allocation produced by list literals. # Free built-in static array allocation produced by list literals.
word arr_static_free word arr_free
dup @ 1 + 8 * free dup @ 1 + 8 * free
end end

137
stdlib/control.sl Normal file
View File

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

View File

@@ -442,6 +442,14 @@
} }
; ;
:asm rswap {
mov rax, [r13] ; get top
mov rbx, [r13 + 8] ; get second
mov [r13], rbx ; swap
mov [r13 + 8], rax
}
;
#pick [* | n] -> [* | x] #pick [* | n] -> [* | x]
:asm pick { :asm pick {
mov rcx, [r12] ; get index mov rcx, [r12] ; get index

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,7 @@ word memcpy
r> dup -rot - swap r> dup -rot - swap
end end
#memset [*, value, len | addr] -> [*] #memset [*, addr, len | value] -> [*]
word memset word memset
swap swap
0 swap for 0 swap for
@@ -51,13 +51,29 @@ word memset
2drop drop 2drop drop
end end
#memdump [*, len | addr] -> [* | addr] # memset_bytes [*, addr, len | value] -> [*]
word memset_bytes
swap
0 swap for
-rot swap 2 pick + 2dup swap c! 1 + -rot swap
end
2drop drop
end
#memdump [*, addr | len] -> [* | addr]
word memdump word memdump
for for
dup @ puti cr 8 + dup @ puti cr 8 +
end end
end end
#memdump_bytes [*, addr | len] -> [* | addr]
word memdump_bytes
for
dup c@ puti cr 1 +
end
end
#realloc [*, addr, old_len | new_len] -> [* | new_addr] #realloc [*, addr, old_len | new_len] -> [* | new_addr]
word realloc word realloc
2 pick swap alloc 2 pick swap alloc

View File

@@ -1,7 +1,16 @@
#strcmp [*, addr, len, addr | len] -> [*, addr, len, addr, len | bool] #strcmp [*, addr, len, addr | len] -> [* | bool]
word strcmp word strcmp
3 pick 2 pick @ swap @ == >r nip r> for
2dup c@ swap c@ != if drop drop 0 rdrop ret end
1 + swap 1 +
end
drop drop 1
end
# strdup [*, addr | len] -> [*, addr, len, addr1 | len1]
word strdup
dup alloc 2 pick 2 pick memcpy
end end
#strconcat [*, addr, len, addr | len] -> [*, addr | len] #strconcat [*, addr, len, addr | len] -> [*, addr | len]
@@ -378,3 +387,107 @@ word format
drop # drop counter (0) drop # drop counter (0)
end end
end end
# rotate N elements of the top of the stack
# nrot [*, x1 ... xN - 1 | xN] -> [*, xN, xN - 1 ... x2 | x1]
word nrot
dup 1 + 1 swap for
dup pick swap 2 +
end
1 - 2 / pick
dup for
swap >r rswap
end
1 + for
nip
end
for
rswap r>
end
end
# convert a string to a sequence of ascii codes of its characters and push the codes on to the stack,
# Warning! the sequence is reversed so the ascii code of the last character ends up first on the stack
# toascii [*, addr | LEN] -> [*, x, x1 ... xLEN - 1 | xLEN + 1]
word toascii
0 swap
for
2dup + c@
-rot
1 +
end
2drop
end
# rm_zero_len_str [*, addr0, len0 ... addrN, lenN | N] -> [*, addrX, lenX ... addrY, lenY | Z]
word rm_zero_len_str
dup for
swap dup 0 == if
drop nip 1 -
else
>r rswap swap >r rswap
end
end
dup 2 * for
rswap r> swap
end
end
# emit_strs [*, addr | len] -> [*, addr0, len0 ... addrN | lenN]
# given an addr and len emits pairs (addr, len) of strings in the given memopry region
word emit_strs
0 >r
>r
while r@ 0 > do
dup strlen dup r> swap - >r
over over + rswap r> 1 + >r rswap
while dup c@ 0 == do 1 + r> 1 - >r end
end
drop rdrop
end
# splitby_str [*, addr, len, addr1, len1] -> [*, addr0, len0 ... addrN, lenN | N]
# splits a string by another string and emmits a sequence of the new (addr, len) pairs on to the stack as well as the number of strings the oprtation resulted in.
word splitby_str
2 pick for
3 pick 0 2 pick 4 pick swap
strcmp 1 == if 3 pick over 0 memset_bytes end
>r >r swap 1 + swap r> r>
end
2drop 2dup - >r nip r> swap emit_strs r>
rm_zero_len_str
end
# splitby [*, addr, len, addr1 | len1] -> [*, addr0, len0 ... addrN, lenN | N]
# split a string by another string, delegates to either splitby_char or splitby_str based on the length of the delimiter.
word splitby
dup 1 == if
splitby_char
else
splitby_str
end
end
# splitby_char [*, addr, len, addr1 | len] -> [*, addr1, len1 ... addrN, lenN | N]
# split a string by a given character, the resulting (addr, len) pairs are pushed on to the stack followed by the number of the pushed strings.
word splitby_char
2 pick >r
>r >r 2dup r> r> 2swap 2dup
>r >r toascii 1 rpick nrot r> r>
dup 3 + pick c@
swap for
dup
3 pick == if over 0 c! end
swap 1 + swap >r nip r>
end
2drop 2drop drop
r>
emit_strs
r>
rm_zero_len_str
end

39
test.py
View File

@@ -224,6 +224,7 @@ class TestCase:
expected_stdout: Path expected_stdout: Path
expected_stderr: Path expected_stderr: Path
compile_expected: Path compile_expected: Path
asm_forbid: Path
stdin_path: Path stdin_path: Path
args_path: Path args_path: Path
meta_path: Path meta_path: Path
@@ -324,6 +325,7 @@ class TestRunner:
expected_stdout=source.with_suffix(".expected"), expected_stdout=source.with_suffix(".expected"),
expected_stderr=source.with_suffix(".stderr"), expected_stderr=source.with_suffix(".stderr"),
compile_expected=source.with_suffix(".compile.expected"), compile_expected=source.with_suffix(".compile.expected"),
asm_forbid=source.with_suffix(".asm.forbid"),
stdin_path=source.with_suffix(".stdin"), stdin_path=source.with_suffix(".stdin"),
args_path=source.with_suffix(".args"), args_path=source.with_suffix(".args"),
meta_path=meta_path, meta_path=meta_path,
@@ -391,6 +393,10 @@ class TestRunner:
return CaseResult(case, compile_status, "compile", compile_note, compile_details, duration) return CaseResult(case, compile_status, "compile", compile_note, compile_details, duration)
if compile_status == "updated" and compile_note: if compile_status == "updated" and compile_note:
updated_notes.append(compile_note) updated_notes.append(compile_note)
asm_status, asm_note, asm_details = self._check_asm_forbidden_patterns(case)
if asm_status == "failed":
duration = time.perf_counter() - start
return CaseResult(case, asm_status, "asm", asm_note, asm_details, duration)
if case.config.compile_only: if case.config.compile_only:
duration = time.perf_counter() - start duration = time.perf_counter() - start
if updated_notes: if updated_notes:
@@ -633,6 +639,39 @@ class TestRunner:
parts.append(proc.stderr) parts.append(proc.stderr)
return "".join(parts) return "".join(parts)
def _check_asm_forbidden_patterns(self, case: TestCase) -> Tuple[str, str, Optional[str]]:
"""Fail test if generated asm contains forbidden markers listed in *.asm.forbid."""
if not case.asm_forbid.exists():
return "passed", "", None
asm_path = case.build_dir / f"{case.binary_stub}.asm"
if not asm_path.exists():
return "failed", f"missing generated asm file {asm_path.name}", None
asm_text = asm_path.read_text(encoding="utf-8")
patterns: List[str] = []
for raw in case.asm_forbid.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
patterns.append(line)
hits: List[str] = []
for pattern in patterns:
if pattern.startswith("re:"):
expr = pattern[3:]
if re.search(expr, asm_text, re.MULTILINE):
hits.append(pattern)
continue
if pattern in asm_text:
hits.append(pattern)
if not hits:
return "passed", "", None
detail = "forbidden asm pattern(s) matched:\n" + "\n".join(f"- {p}" for p in hits)
return "failed", "assembly contains forbidden patterns", detail
def _compare_nob_test_stdout( def _compare_nob_test_stdout(
self, self,
case: TestCase, case: TestCase,

View File

@@ -19,22 +19,22 @@ word main
dup arr_len puti cr dup arr_len puti cr
dup arr_cap puti cr dup arr_cap puti cr
# arr_get # dyn_arr_get
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
# arr_set # dyn_arr_set
dup 99 1 arr_set dup 99 1 dyn_arr_set
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
# arr_reserve (with len > 0 so element copy path is exercised) # arr_reserve (with len > 0 so element copy path is exercised)
dup 8 arr_reserve dup 8 arr_reserve
dup arr_cap puti cr dup arr_cap puti cr
dup arr_len puti cr dup arr_len puti cr
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
# arr_pop (including empty pop) # arr_pop (including empty pop)
arr_pop puti cr arr_pop puti cr
@@ -43,16 +43,16 @@ word main
arr_pop puti cr arr_pop puti cr
dup arr_len puti cr dup arr_len puti cr
arr_free dyn_arr_free
# arr_to_dyn (convert std list to dynamic array) # arr_to_dyn (convert std list to dynamic array)
[ 7 8 9 ] dup arr_to_dyn [ 7 8 9 ] dup arr_to_dyn
dup arr_len puti cr dup arr_len puti cr
dup arr_cap puti cr dup arr_cap puti cr
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
arr_free dyn_arr_free
# free list allocation: bytes = (len + 1) * 8 # free list allocation: bytes = (len + 1) * 8
dup @ 1 + 8 * free dup @ 1 + 8 * free
@@ -64,21 +64,21 @@ word main
dup 2 arr_push dup 2 arr_push
dup dyn_arr_sorted dup dyn_arr_sorted
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
arr_free dyn_arr_free
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
# dyn_arr_sort (alias) sorts in place # dyn_arr_sort (alias) sorts in place
dyn_arr_sort dyn_arr_sort
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
arr_free dyn_arr_free
# dyn_arr_sorted (alias) returns a sorted copy # dyn_arr_sorted (alias) returns a sorted copy
5 arr_new 5 arr_new
@@ -87,13 +87,13 @@ word main
dup 6 arr_push dup 6 arr_push
dup dyn_arr_sorted dup dyn_arr_sorted
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
arr_free dyn_arr_free
dup 0 arr_get puti cr dup 0 dyn_arr_get puti cr
dup 1 arr_get puti cr dup 1 dyn_arr_get puti cr
dup 2 arr_get puti cr dup 2 dyn_arr_get puti cr
arr_free dyn_arr_free
end end

View File

@@ -4,22 +4,22 @@ import ../stdlib/arr.sl
word main word main
[ 4 1 3 2 ] dup arr_sort [ 4 1 3 2 ] dup arr_sort
dup 0 arr_get_static puti cr dup 0 arr_get puti cr
dup 1 arr_get_static puti cr dup 1 arr_get puti cr
dup 2 arr_get_static puti cr dup 2 arr_get puti cr
dup 3 arr_get_static puti cr dup 3 arr_get puti cr
arr_static_free arr_free
[ 9 5 7 ] dup arr_sorted [ 9 5 7 ] dup arr_sorted
dup 0 arr_get_static puti cr dup 0 arr_get puti cr
dup 1 arr_get_static puti cr dup 1 arr_get puti cr
dup 2 arr_get_static puti cr dup 2 arr_get puti cr
swap swap
dup 0 arr_get_static puti cr dup 0 arr_get puti cr
dup 1 arr_get_static puti cr dup 1 arr_get puti cr
dup 2 arr_get_static puti cr dup 2 arr_get puti cr
arr_static_free arr_free
arr_static_free arr_free
end end

View File

@@ -0,0 +1,5 @@
11
22
33
5
3

View File

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

View File

@@ -0,0 +1 @@
[error] word 'i' is compile-time only and cannot be used at runtime while emitting 'main'

View File

@@ -0,0 +1,4 @@
{
"expect_compile_error": true,
"description": "'i' is compile-time only and rejected in runtime code"
}

View File

@@ -0,0 +1,8 @@
import stdlib/stdlib.sl
word main
0
3 for
i puti cr
end
end

View File

@@ -5,13 +5,13 @@ import ../stdlib/arr.sl
# Get element from static array, preserving the array pointer # Get element from static array, preserving the array pointer
# [*, arr | i] -> [*, arr | value] # [*, arr | i] -> [*, arr | value]
word aget word aget
over swap arr_get_static over swap arr_get
end end
# Set element in static array, preserving the array pointer # Set element in static array, preserving the array pointer
# [*, arr, value | i] -> [* | arr] # [*, arr, value | i] -> [* | arr]
word aset word aset
rot dup >r -rot arr_set_static r> rot dup >r -rot arr_set r>
end end
# Swap elements at indices i and j in a static array # Swap elements at indices i and j in a static array
@@ -86,7 +86,7 @@ end
word print_arr word print_arr
dup @ 0 dup @ 0
while 2dup > do while 2dup > do
2 pick over arr_get_static puti cr 2 pick over arr_get puti cr
1 + 1 +
end end
2drop drop 2drop drop

1
tests/ret_test.expected Normal file
View File

@@ -0,0 +1 @@
g

11
tests/ret_test.sl Normal file
View File

@@ -0,0 +1,11 @@
import stdlib.sl
word g
"g" puts
ret
"g" puts
end
word main
g
end

View File

@@ -1,4 +1,13 @@
1 1
g 0
g
hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world
hello
hello
hello
hello
o
d he
o wor
d he
o wor
he

View File

@@ -1,15 +1,24 @@
import stdlib.sl import stdlib.sl
word main word main
"g" "g" "ggggggggh" "ggggggggh"
strcmp
puti cr
"ggggggggh" "ggggggggd"
strcmp strcmp
puti cr puti cr
puts
puts
"hello world hello world hello " "world hello world hello world" "hello world hello world hello " "world hello world hello world"
strconcat strconcat
2dup 2dup
puts puts
free free
"hello world hello" "world" splitby
for puts end
"hello world hello world" "world" splitby
for puts end
"hello world hello world hello" "l" splitby
for puts end
end end

View File

@@ -0,0 +1,4 @@
# Ensure known-argc syscall lowering avoids generic dynamic syscall boilerplate.
clamp arg count to [0, 6]
re:syscall_\d+_count_
re:syscall_\d+_skip_

View File

@@ -6,7 +6,6 @@ word main
1 1
"hello" "hello"
syscall.write syscall.write
syscall
#drop #drop
1 1

View File

@@ -0,0 +1,4 @@
neg
zero
small
big

View File

@@ -0,0 +1,23 @@
import stdlib/stdlib.sl
word classify
with n in
n 0 < if
"neg" puts
else n 0 == if
"zero" puts
else n 10 < if
"small" puts
else
"big" puts
end
end
end
word main
-1 classify
0 classify
3 classify
20 classify
0
end

220
tools/gen_linux_sl.py Normal file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""Generate stdlib/linux.sl from syscall_64.tbl metadata."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "syscall_64.tbl"
DST = ROOT / "stdlib" / "linux.sl"
def _sanitize_alias(alias: str) -> str:
name = alias.strip()
if not name:
return ""
if name.startswith("__x64_sys_"):
name = name[len("__x64_sys_") :]
elif name.startswith("sys_"):
name = name[len("sys_") :]
name = re.sub(r"[^A-Za-z0-9_]", "_", name)
name = re.sub(r"_+", "_", name).strip("_")
if not name:
return ""
if name[0].isdigit():
name = "n_" + name
return name
@dataclass(frozen=True)
class SyscallEntry:
argc: int
num: int
aliases: tuple[str, ...]
def _parse_table(path: Path) -> list[SyscallEntry]:
entries: list[SyscallEntry] = []
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
parts = line.split(maxsplit=2)
if len(parts) < 3:
continue
try:
argc = int(parts[0])
num = int(parts[1])
except ValueError:
continue
aliases = tuple(a for a in parts[2].split("/") if a)
if not aliases:
continue
entries.append(SyscallEntry(argc=argc, num=num, aliases=aliases))
return entries
def _emit_header(lines: list[str]) -> None:
lines.extend(
[
"# Autogenerated from syscall_64.tbl",
"# Generated by tools/gen_linux_sl.py",
"# Linux syscall constants + convenience wrappers for L2",
"",
"# File descriptor constants",
"macro FD_STDIN 0 0 ;",
"macro FD_STDOUT 0 1 ;",
"macro FD_STDERR 0 2 ;",
"",
"# Common open(2) flags",
"macro O_RDONLY 0 0 ;",
"macro O_WRONLY 0 1 ;",
"macro O_RDWR 0 2 ;",
"macro O_CREAT 0 64 ;",
"macro O_EXCL 0 128 ;",
"macro O_NOCTTY 0 256 ;",
"macro O_TRUNC 0 512 ;",
"macro O_APPEND 0 1024 ;",
"macro O_NONBLOCK 0 2048 ;",
"macro O_CLOEXEC 0 524288 ;",
"",
"# lseek(2)",
"macro SEEK_SET 0 0 ;",
"macro SEEK_CUR 0 1 ;",
"macro SEEK_END 0 2 ;",
"",
"# mmap(2)",
"macro PROT_NONE 0 0 ;",
"macro PROT_READ 0 1 ;",
"macro PROT_WRITE 0 2 ;",
"macro PROT_EXEC 0 4 ;",
"macro MAP_PRIVATE 0 2 ;",
"macro MAP_ANONYMOUS 0 32 ;",
"macro MAP_SHARED 0 1 ;",
"",
"# Socket constants",
"macro AF_UNIX 0 1 ;",
"macro AF_INET 0 2 ;",
"macro AF_INET6 0 10 ;",
"macro SOCK_STREAM 0 1 ;",
"macro SOCK_DGRAM 0 2 ;",
"macro SOCK_NONBLOCK 0 2048 ;",
"macro SOCK_CLOEXEC 0 524288 ;",
"",
"macro INADDR_ANY 0 0 ;",
"",
"# Generic syscall helpers with explicit argument count",
"# Stack form:",
"# syscall -> <argN> ... <arg0> <argc> <nr> syscall",
"# syscallN -> <argN-1> ... <arg0> <nr> syscallN",
"",
"# swap impl is provided so this can be used without stdlib",
"# ___linux_swap [*, x1 | x2] -> [*, x2 | x1]",
":asm ___linux_swap {",
" mov rax, [r12]",
" mov rbx, [r12 + 8]",
" mov [r12], rbx",
" mov [r12 + 8], rax",
"}",
";",
"",
"macro syscall0 0",
" 0",
" ___linux_swap",
" syscall",
";",
"",
"macro syscall1 0",
" 1",
" ___linux_swap",
" syscall",
";",
"",
"macro syscall2 0",
" 2",
" ___linux_swap",
" syscall",
";",
"",
"macro syscall3 0",
" 3",
" ___linux_swap",
" syscall",
";",
"",
"macro syscall4 0",
" 4",
" ___linux_swap",
" syscall",
";",
"",
"macro syscall5 0",
" 5",
" ___linux_swap",
" syscall",
";",
"",
"macro syscall6 0",
" 6",
" ___linux_swap",
" syscall",
";",
"",
]
)
def _emit_entry(lines: list[str], alias: str, argc: int, num: int) -> None:
safe_argc = max(0, min(argc, 6))
lines.extend(
[
f"macro syscall.{alias} 0",
f" {num}",
f" syscall{safe_argc}",
";",
"",
f"macro syscall.{alias}.num 0",
f" {num}",
";",
"",
f"macro syscall.{alias}.argc 0",
f" {safe_argc}",
";",
"",
]
)
def generate() -> str:
entries = _parse_table(SRC)
lines: list[str] = []
_emit_header(lines)
emitted: set[str] = set()
for entry in sorted(entries, key=lambda e: (e.num, e.aliases[0])):
for alias in entry.aliases:
name = _sanitize_alias(alias)
if not name:
continue
key = f"syscall.{name}"
if key in emitted:
continue
_emit_entry(lines, name, entry.argc, entry.num)
emitted.add(key)
return "\n".join(lines).rstrip() + "\n"
def main() -> None:
output = generate()
DST.parent.mkdir(parents=True, exist_ok=True)
DST.write_text(output, encoding="utf-8")
print(f"wrote {DST}")
if __name__ == "__main__":
main()