Compare commits
21 Commits
ecf90feab9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
052f9191c3 | ||
|
|
a74c4b8c41 | ||
|
|
d639c63fd3 | ||
|
|
ab613e644a | ||
|
|
75b01b9635 | ||
|
|
b263e7d0de | ||
|
|
bc3a894737 | ||
|
|
e8271895fb | ||
|
|
0477e4c10d | ||
|
|
9fbba4eefb | ||
| 44a49d17d5 | |||
|
|
2055aa3b1f | ||
|
|
5dd361e563 | ||
|
|
e2159bbca2 | ||
|
|
f4d688cac1 | ||
|
|
4508433206 | ||
|
|
cda34e61bd | ||
|
|
78bf6c132f | ||
|
|
fd030be086 | ||
|
|
2193e0bf3c | ||
|
|
fd115f31dc |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sl linguist-language=l2
|
||||||
19
SPEC.md
19
SPEC.md
@@ -4,13 +4,13 @@ This document reflects the implementation that ships in this repository today (`
|
|||||||
|
|
||||||
## 1. Scope and Principles
|
## 1. Scope and Principles
|
||||||
- **Stack-based core** – All user code manipulates a 64-bit data stack plus a separate return stack. Every definition is a “word.”
|
- **Stack-based core** – All user code manipulates a 64-bit data stack plus a separate return stack. Every definition is a “word.”
|
||||||
- **Ahead-of-time native output** – `main.py` always emits NASM-compatible x86-64 assembly, assembles it with `nasm -f elf64`, and links it with `ld`/`ld.lld` into an ELF64 executable. There is no JIT; the REPL repeatedly rebuilds and executes small binaries.
|
- **Ahead-of-time native output** – `main.py` emits NASM-compatible x86-64 assembly, assembles it with `nasm -f elf64`, and links it with `ld`/`ld.lld` into an ELF64 executable. There is JIT for the compile time execution and the REPL uses it as well.
|
||||||
- **Meta-programmable front-end** – Parsing, macro expansion, and syntax sugar live in user space via immediate words, text macros, compile-time intrinsics, and `:py` blocks. Users can reshape syntax without touching the Python host.
|
- **Meta-programmable front-end** – Parsing, macro expansion, and syntax sugar live in user space via immediate words, text macros, compile-time intrinsics, and `:py` blocks. Users can reshape syntax without touching the Python host.
|
||||||
- **Unsafe by design** – Memory, syscalls, inline assembly, and FFI expose raw machine power. The standard library is intentionally thin and policy-free.
|
- **Unsafe by design** – Memory, syscalls, inline assembly, and FFI expose raw machine power. The standard library is intentionally thin and policy-free.
|
||||||
|
|
||||||
## 2. Toolchain and Repository Layout
|
## 2. Toolchain and Repository Layout
|
||||||
- **Driver (`main.py`)** – Supports `python main.py source.sl -o a.out`, `--emit-asm`, `--run`, `--dbg`, `--repl`, `--temp-dir`, `--clean`, `--dump-cfg[=path]`, repeated `-I/--include` paths, and repeated `-l` linker flags (either `-lfoo` or `-l libc.so.6`). Unknown `-l` flags are collected and forwarded to the linker. Pass `--ct-run-main` to run the program's `main` word on the compile-time VM before NASM/ld run, which surfaces discrepancies between compile-time and runtime semantics. Pass `--no-artifact` to stop after compilation/assembly emission without building an output file, or use `--script` as shorthand for `--no-artifact --ct-run-main`. Pass `--docs` to open a searchable TUI that scans stack-effect comments and nearby docs from `.sl` files (`--docs-query` sets initial filter and `--docs-root` adds scan roots). `--no-folding` disables constant folding and `--no-peephole` disables peephole rewrites (for example `swap drop` → `nip`, `dup drop` removed, `swap over` → `tuck`, `nip drop` → `2drop`, `x 0 +` removed, `x 1 *` removed, `x -1 *` → `neg`, and `not not` removed).
|
- **Driver (`main.py`)** – Supports `python main.py source.sl -o a.out`, `--emit-asm`, `--run`, `--dbg`, `--repl`, `--temp-dir`, `--clean`, `--dump-cfg[=path]`, repeated `-I/--include` paths, and repeated `-l` linker flags (either `-lfoo` or `-l libc.so.6`). Unknown `-l` flags are collected and forwarded to the linker. Pass `--ct-run-main` to run the program's `main` word on the compile-time VM before NASM/ld run, which surfaces discrepancies between compile-time and runtime semantics. Pass `--no-artifact` to stop after compilation/assembly emission without building an output file, or use `--script` as shorthand for `--no-artifact --ct-run-main`. Pass `--docs` to open a searchable TUI that scans stack-effect comments and nearby docs from `.sl` files (`--docs-query` sets initial filter and `--docs-root` adds scan roots). `--no-folding` disables constant folding and `--no-peephole` disables peephole rewrites (for example `swap drop` → `nip`, `dup drop` removed, `swap over` → `tuck`, `nip drop` → `2drop`, `x 0 +` removed, `x 1 *` removed, `x -1 *` → `neg`, and `not not` removed).
|
||||||
- **REPL** – `--repl` launches a stateful session with commands such as `:help`, `:reset`, `:load`, `:call <word>`, `:edit`, and `:show`. The REPL still emits/links entire programs for each run; it simply manages the session source for you.
|
- **REPL** – `--repl` launches a stateful session with commands such as `:help`, `:reset`, `:load`, `:call <word>`, `:edit`, and `:show`.
|
||||||
- **Imports** – `import relative/or/absolute/path.sl` inserts the referenced file textually. Resolution order: (1) absolute path, (2) relative to the importing file, (3) each include path (defaults: project root and `./stdlib`). Each file is included at most once per compilation unit. Import lines leave blank placeholders so error spans stay meaningful.
|
- **Imports** – `import relative/or/absolute/path.sl` inserts the referenced file textually. Resolution order: (1) absolute path, (2) relative to the importing file, (3) each include path (defaults: project root and `./stdlib`). Each file is included at most once per compilation unit. Import lines leave blank placeholders so error spans stay meaningful.
|
||||||
- **Workspace** – `stdlib/` holds library modules, `tests/` contains executable samples with `.expected` outputs, `extra_tests/` houses standalone integration demos, and `libs/` collects opt-in extensions such as `libs/fn.sl` and `libs/nob.sl`.
|
- **Workspace** – `stdlib/` holds library modules, `tests/` contains executable samples with `.expected` outputs, `extra_tests/` houses standalone integration demos, and `libs/` collects opt-in extensions such as `libs/fn.sl` and `libs/nob.sl`.
|
||||||
|
|
||||||
@@ -18,19 +18,19 @@ This document reflects the implementation that ships in this repository today (`
|
|||||||
- **Reader** – Whitespace-delimited; `#` starts a line comment. String literals honor `\"`, `\\`, `\n`, `\r`, `\t`, and `\0`. Numbers default to signed 64-bit integers via `int(token, 0)` (so `0x`, `0o`, `0b` all work). Tokens containing `.` or `e` parse as floats.
|
- **Reader** – Whitespace-delimited; `#` starts a line comment. String literals honor `\"`, `\\`, `\n`, `\r`, `\t`, and `\0`. Numbers default to signed 64-bit integers via `int(token, 0)` (so `0x`, `0o`, `0b` all work). Tokens containing `.` or `e` parse as floats.
|
||||||
- **Identifiers** – `[A-Za-z_][A-Za-z0-9_]*`. Everything else is treated as punctuation or literal.
|
- **Identifiers** – `[A-Za-z_][A-Za-z0-9_]*`. Everything else is treated as punctuation or literal.
|
||||||
- **String representation** – At runtime each literal pushes `(addr len)` with the length on top. The assembler stores literals in `section .data` with a trailing `NULL` for convenience.
|
- **String representation** – At runtime each literal pushes `(addr len)` with the length on top. The assembler stores literals in `section .data` with a trailing `NULL` for convenience.
|
||||||
- **Lists** – `[` begins a list literal, `]` ends it. The compiler captures the intervening stack segment into a freshly `mmap`'d buffer that stores `(len followed by qword items)`, drops the captured values, and pushes the buffer address. Users must `munmap` the buffer when done.
|
- **Lists** – `[` begins a list literal, `]` ends it. The compiler captures the intervening stack segment into a freshly `mmap`'d buffer that stores `(len followed by qword items)`, drops the captured values, and pushes the buffer address. Users must `munmap` the buffer when done. When elems are known at compile time then the list is folded and put in .bss so it doesn't need to be freed then, you can disable this optimization via a flag --no-static-list-folding.
|
||||||
- **Token customization** – Immediate words can call `add-token` or `add-token-chars` to teach the reader about new multi-character tokens. `libs/fn.sl` uses this in combination with token hooks to recognize `foo(1, 2)` syntax.
|
- **Token customization** – Immediate words can call `add-token` or `add-token-chars` to teach the reader about new multi-character tokens. `libs/fn.sl` uses this in combination with token hooks to recognize `foo(1, 2)` syntax.
|
||||||
|
|
||||||
### Stack-effect comments
|
### Stack-effect comments
|
||||||
- **Location and prefix** – Public words in `stdlib/` (and most user code should) document its stack effect with a line comment directly above the definition: `#word_name …`.
|
- **Location and prefix** – Public words in `stdlib/` (and most user code should) document its stack effect with a line comment directly above the definition: `#word_name …`.
|
||||||
- **Before/after form** – Use `[before] -> [after]`, where each side is a comma-separated list. Items sitting to the left of `|` are deeper in the stack; the segment to the right of `|` runs all the way to the current top-of-stack. Omit the `|` only when a side is empty (`[*]`).
|
- **Before/after form** – Use `[before] -> [after]`, where each side is a comma-separated list. Items sitting to the left of `|` are deeper in the stack and on the right is the top most element. Omit the `|` only when a side is empty (`[*]`).
|
||||||
- **Tail sentinel** – `*` represents the untouched rest of the stack. By convention it is always the first entry on each side so readers can quickly see which values are consumed/produced.
|
- **Tail sentinel** – `*` represents the untouched rest of the stack. By convention it is always the first entry on each side so readers can quickly see which values are consumed/produced.
|
||||||
- **Alternatives** – Separate multiple outcomes with `||`. Each branch repeats the `[before] -> [after]` structure (e.g., `#read_file [*, path | len] -> [*, addr | len] || [*, tag | neg_errno]`).
|
- **Alternatives** – Separate multiple outcomes with `||`. Each branch repeats the `[before] -> [after]` structure (e.g., `#read_file [*, path | len] -> [*, addr | len] || [*, tag | neg_errno]`).
|
||||||
- **Examples** – `#dup [* | x] -> [*, x | x]` means a word consumes the top value `x` and returns two copies with the newest copy at TOS; `#arr_pop [* | arr] -> [*, arr | x]` states that the array pointer remains just below the popped element. This notation keeps stack order resonably easy to read and grep.
|
- **Examples** – `#dup [* | x] -> [*, x | x]` means a word consumes the top value `x` and returns two copies with the newest copy at TOS; `#arr_pop [* | arr] -> [*, arr | x]` states that the array pointer remains just below the popped element. This notation keeps stack order resonably easy to read and grep.
|
||||||
|
|
||||||
## 4. Runtime Model
|
## 4. Runtime Model
|
||||||
- **Stacks** – `r12` holds the data stack pointer, `r13` the return stack pointer. Both live in `.bss` buffers sized by `DSTK_BYTES`/`RSTK_BYTES` (default 64 KiB each). `stdlib/core.sl` implements all standard stack shuffles, arithmetic, comparisons, boolean ops, `@`/`!`, `c@`/`c!`, and return-stack transfers (`>r`, `r>`, `rdrop`, `rpick`).
|
- **Stacks** – `r12` holds the data stack pointer, `r13` the return stack pointer. Both live in `.bss` buffers sized by `DSTK_BYTES`/`RSTK_BYTES` (default 64 KiB each). `stdlib/core.sl` implements all standard stack shuffles, arithmetic, comparisons, boolean ops, `@`/`!`, `c@`/`c!`, and return-stack transfers (`>r`, `r>`, `rdrop`, `rpick`).
|
||||||
- **Calling convention** – Words call each other using the System V ABI. `extern` words marshal arguments into registers before `call symbol`, then push results back onto the data stack. Integer results come from `rax`; floating results come from `xmm0` and are copied into a qword slot.
|
- **Calling convention** – Calling convention applies only to the extern functions and follows the System V ABI. `extern` words marshal arguments into registers before `call symbol`, then push results back onto the data stack. Integer results come from `rax`; floating results come from `xmm0` and are copied into a qword slot.
|
||||||
- **Memory helpers** – `mem` returns the address of the `persistent` buffer (default 64 bytes). `argc`, `argv`, and `argv@` expose process arguments. `alloc`/`free` wrap `mmap`/`munmap` for general-purpose buffers, while `memcpy` performs byte-wise copies.
|
- **Memory helpers** – `mem` returns the address of the `persistent` buffer (default 64 bytes). `argc`, `argv`, and `argv@` expose process arguments. `alloc`/`free` wrap `mmap`/`munmap` for general-purpose buffers, while `memcpy` performs byte-wise copies.
|
||||||
- **BSS customization** – Compile-time words may call `bss-clear` followed by `bss-append`/`bss-set` to replace the default `.bss` layout (e.g., `tests/bss_override.sl` enlarges `persistent`).
|
- **BSS customization** – Compile-time words may call `bss-clear` followed by `bss-append`/`bss-set` to replace the default `.bss` layout (e.g., `tests/bss_override.sl` enlarges `persistent`).
|
||||||
- **Strings & buffers** – IO helpers consume explicit `(addr len)` pairs only; there is no implicit NULL contract except for stored literals.
|
- **Strings & buffers** – IO helpers consume explicit `(addr len)` pairs only; there is no implicit NULL contract except for stored literals.
|
||||||
@@ -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,8 +78,9 @@ 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`.
|
||||||
- **`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`; built-in static-array sorting via `arr_sort`/`arr_sorted`; and dynamic-array sorting via `dyn_arr_sort`/`dyn_arr_sorted`.
|
- **`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`).
|
||||||
|
|||||||
613
examples/snake.sl
Normal file
613
examples/snake.sl
Normal 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
|
||||||
8
extra_tests/termios_test.expected
Normal file
8
extra_tests/termios_test.expected
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
stdin is a tty?
|
||||||
|
1
|
||||||
|
stdout is a tty?
|
||||||
|
0
|
||||||
|
stderr is a tty?
|
||||||
|
0
|
||||||
|
Invalid fd (999) is a tty?
|
||||||
|
-1
|
||||||
16
extra_tests/termios_test.sl
Normal file
16
extra_tests/termios_test.sl
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import stdlib.sl
|
||||||
|
import termios.sl
|
||||||
|
|
||||||
|
word main
|
||||||
|
"stdin is a tty? " puts
|
||||||
|
0 isatty puti cr
|
||||||
|
|
||||||
|
"stdout is a tty? " puts
|
||||||
|
1 isatty puti cr
|
||||||
|
|
||||||
|
"stderr is a tty? " puts
|
||||||
|
2 isatty puti cr
|
||||||
|
|
||||||
|
"Invalid fd (999) is a tty? " puts
|
||||||
|
999 isatty puti cr
|
||||||
|
end
|
||||||
@@ -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
|
||||||
|
|||||||
629
main.py
629
main.py
@@ -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"
|
||||||
@@ -11117,7 +11542,7 @@ def _run_docs_tui(
|
|||||||
"\n"
|
"\n"
|
||||||
" 5. NASM + LINKER\n"
|
" 5. NASM + LINKER\n"
|
||||||
" The assembly is assembled by NASM into an object\n"
|
" The assembly is assembled by NASM into an object\n"
|
||||||
" file, then linked (via ld or gcc) into the final\n"
|
" file, then linked (via ld or ld.ldd) into the final\n"
|
||||||
" binary.\n"
|
" binary.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"───────────────────────────────────────────────────────────────\n"
|
"───────────────────────────────────────────────────────────────\n"
|
||||||
@@ -11142,7 +11567,7 @@ def _run_docs_tui(
|
|||||||
" The CT VM is a stack-based interpreter that runs during\n"
|
" The CT VM is a stack-based interpreter that runs during\n"
|
||||||
" parsing. It maintains:\n"
|
" parsing. It maintains:\n"
|
||||||
"\n"
|
"\n"
|
||||||
" - A value stack (Python list of ints/strings/lists)\n"
|
" - A value stack\n"
|
||||||
" - A dictionary of CT-callable words\n"
|
" - A dictionary of CT-callable words\n"
|
||||||
" - A return stack for nested calls\n"
|
" - A return stack for nested calls\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -11154,7 +11579,7 @@ def _run_docs_tui(
|
|||||||
"\n"
|
"\n"
|
||||||
" When --ct-run-main is used, the CT VM can also JIT-compile\n"
|
" When --ct-run-main is used, the CT VM can also JIT-compile\n"
|
||||||
" and execute native x86-64 code via the Keystone assembler\n"
|
" and execute native x86-64 code via the Keystone assembler\n"
|
||||||
" engine (for words that need native performance).\n"
|
" engine (for words that need near native performance).\n"
|
||||||
"\n"
|
"\n"
|
||||||
"───────────────────────────────────────────────────────────────\n"
|
"───────────────────────────────────────────────────────────────\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -11216,7 +11641,7 @@ def _run_docs_tui(
|
|||||||
" just numbers. Type safety is your responsibility.\n"
|
" just numbers. Type safety is your responsibility.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" - Macro expansion depth: macros can expand macros,\n"
|
" - Macro expansion depth: macros can expand macros,\n"
|
||||||
" but there's a limit (default 64, configurable via\n"
|
" but there's a limit (default 256, configurable via\n"
|
||||||
" --macro-expansion-limit).\n"
|
" --macro-expansion-limit).\n"
|
||||||
"\n"
|
"\n"
|
||||||
" - :py blocks: Python code embedded in :py { ... }\n"
|
" - :py blocks: Python code embedded in :py { ... }\n"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -326,3 +326,22 @@ word dyn_arr_sorted
|
|||||||
dyn_arr_clone
|
dyn_arr_clone
|
||||||
dyn_arr_sort
|
dyn_arr_sort
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# arr_contains [*, addr | x] -> [* | bool]
|
||||||
|
word arr_contains
|
||||||
|
over @ >r >r 8 + r> r>
|
||||||
|
for
|
||||||
|
2dup swap @ == if 1 nip nip rdrop ret end
|
||||||
|
swap 8 + swap
|
||||||
|
end 0 nip nip
|
||||||
|
end
|
||||||
|
|
||||||
|
# arr_find [*, addr | x] -> [* | bool]
|
||||||
|
word arr_find
|
||||||
|
over @ >r >r 8 + r> r>
|
||||||
|
0 >r
|
||||||
|
for
|
||||||
|
2dup swap @ == if rswap r> nip nip rdrop ret end
|
||||||
|
swap 8 + swap rswap r> 1 + >r rswap
|
||||||
|
end rdrop -1 nip nip
|
||||||
|
end
|
||||||
|
|||||||
137
stdlib/control.sl
Normal file
137
stdlib/control.sl
Normal 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
|
||||||
@@ -167,6 +167,37 @@
|
|||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
||||||
|
#3dup [*, x1, x2 | x3] -> [*, x1, x2, x3, x1, x2 | x3]
|
||||||
|
:asm 3dup {
|
||||||
|
mov rax, [r12] ; c (top)
|
||||||
|
mov rbx, [r12 + 8] ; b
|
||||||
|
mov rcx, [r12 + 16] ; a
|
||||||
|
sub r12, 8 ; make room
|
||||||
|
mov [r12], rcx ; push a
|
||||||
|
sub r12, 8 ; make room
|
||||||
|
mov [r12], rbx ; push b
|
||||||
|
sub r12, 8 ; make room
|
||||||
|
mov [r12], rax ; push c
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
|
#4dup [*, x1, x2, x3 | x4] -> [*, x1, x2, x3, x4, x1, x2, x3 | x4]
|
||||||
|
:asm 4dup {
|
||||||
|
mov rax, [r12] ; d
|
||||||
|
mov rbx, [r12 + 8] ; c
|
||||||
|
mov rcx, [r12 + 16] ; b
|
||||||
|
mov rdx, [r12 + 24] ; a
|
||||||
|
sub r12, 8 ; make room
|
||||||
|
mov [r12], rdx ; push a
|
||||||
|
sub r12, 8 ; make room
|
||||||
|
mov [r12], rcx ; push b
|
||||||
|
sub r12, 8 ; make room
|
||||||
|
mov [r12], rbx ; push c
|
||||||
|
sub r12, 8 ; make room
|
||||||
|
mov [r12], rax ; push d
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
#2drop [*, x1 | x2] -> [*]
|
#2drop [*, x1 | x2] -> [*]
|
||||||
:asm 2drop {
|
:asm 2drop {
|
||||||
add r12, 16 ; remove two items
|
add r12, 16 ; remove two items
|
||||||
@@ -442,6 +473,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
|
||||||
|
|||||||
20280
stdlib/linux.sl
20280
stdlib/linux.sl
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
36
stdlib/termios.sl
Normal file
36
stdlib/termios.sl
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import stdlib.sl
|
||||||
|
|
||||||
|
macro TCGETS 0 0x5401 ;
|
||||||
|
macro ENOTTY 0 25 ;
|
||||||
|
macro EBADF 0 9 ;
|
||||||
|
|
||||||
|
# isatty [* | fd] -> [* | flag]
|
||||||
|
word isatty
|
||||||
|
>r # save fd
|
||||||
|
|
||||||
|
60 alloc # push addr
|
||||||
|
r@ TCGETS over 3 16 syscall # addr result
|
||||||
|
|
||||||
|
# Duplicate result and save it
|
||||||
|
dup >r # push result to return stack
|
||||||
|
|
||||||
|
# Free buffer
|
||||||
|
60 swap free # free(addr, size)
|
||||||
|
|
||||||
|
# Restore result
|
||||||
|
r> # result back to data stack
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
dup ENOTTY neg == if # -ENOTTY (not a tty)
|
||||||
|
drop 0
|
||||||
|
else
|
||||||
|
dup EBADF neg == if # -EBADF (bad fd)
|
||||||
|
drop -1
|
||||||
|
else
|
||||||
|
# Any other value means it's a tty
|
||||||
|
drop 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
rdrop
|
||||||
|
end
|
||||||
180
stdlib/utils.sl
180
stdlib/utils.sl
@@ -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,170 @@ 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]
|
||||||
|
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
|
||||||
|
|
||||||
|
# ltrim [*, addr | len] -> [*, addr, | len]
|
||||||
|
word ltrim
|
||||||
|
dup for
|
||||||
|
over c@ 32 == if
|
||||||
|
swap 1 + swap 1 -
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# rtrim [*, addr | len] -> [*, addr, | len]
|
||||||
|
word rtrim
|
||||||
|
swap tuck swap
|
||||||
|
swap over + 1 - swap
|
||||||
|
dup for
|
||||||
|
over c@ 32 == if
|
||||||
|
swap 1 - swap 1 -
|
||||||
|
end
|
||||||
|
end nip
|
||||||
|
end
|
||||||
|
|
||||||
|
# trim [*, addr | len] -> [*, addr | len]
|
||||||
|
word trim
|
||||||
|
ltrim rtrim
|
||||||
|
end
|
||||||
|
|
||||||
|
# startswith [*, addr, len, addr | len] -> [*, bool]
|
||||||
|
inline word startswith
|
||||||
|
strcmp
|
||||||
|
end
|
||||||
|
|
||||||
|
# endswith [*, addr, len, addr | len] -> [*, bool]
|
||||||
|
word endswith
|
||||||
|
dup 3 pick swap - 4 pick + over 2 pick 4 pick swap strcmp
|
||||||
|
nip nip nip nip
|
||||||
|
end
|
||||||
|
|
||||||
|
# contains [*, addr, len, addr | len] -> [* | bool]
|
||||||
|
word contains
|
||||||
|
2 pick for
|
||||||
|
4dup strcmp 1 == if 1 nip nip nip nip rdrop ret end
|
||||||
|
>r >r >r 1 + r> r> r>
|
||||||
|
end 0 nip nip nip nip
|
||||||
|
end
|
||||||
|
|
||||||
|
# find the first occurence of a string inside another string, returns the index
|
||||||
|
# find [*, addr, len, addr | len] -> [* | index]
|
||||||
|
word find
|
||||||
|
0 >r 2 pick for
|
||||||
|
4dup strcmp 1 == if rswap r> nip nip nip nip rdrop ret end
|
||||||
|
>r >r >r 1 + r> r> r> rswap r> 1 + >r rswap
|
||||||
|
end -1 nip nip nip nip
|
||||||
|
end
|
||||||
|
|
||||||
|
# find the last occurence of a string inside another string, returns the index
|
||||||
|
# rfind [*, addr, len, addr | len] -> [* | index]
|
||||||
|
word rfind
|
||||||
|
>r >r dup >r + 1 - r> r> r>
|
||||||
|
2 pick 1 - >r 2 pick for
|
||||||
|
4dup strcmp 1 == if rswap r> nip nip nip nip rdrop ret end
|
||||||
|
>r >r >r 1 - r> r> r> rswap r> 1 - >r rswap
|
||||||
|
end -1 nip nip nip nip
|
||||||
|
end
|
||||||
|
|||||||
40
test.py
40
test.py
@@ -28,6 +28,7 @@ DEFAULT_EXTRA_TESTS = [
|
|||||||
"extra_tests/c_extern_structs.sl",
|
"extra_tests/c_extern_structs.sl",
|
||||||
"extra_tests/fn_test.sl",
|
"extra_tests/fn_test.sl",
|
||||||
"extra_tests/nob_test.sl",
|
"extra_tests/nob_test.sl",
|
||||||
|
"extra_tests/termios_test.sl",
|
||||||
]
|
]
|
||||||
|
|
||||||
COLORS = {
|
COLORS = {
|
||||||
@@ -224,6 +225,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 +326,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 +394,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 +640,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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
5
tests/custom_control_structures.expected
Normal file
5
tests/custom_control_structures.expected
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
11
|
||||||
|
22
|
||||||
|
33
|
||||||
|
5
|
||||||
|
3
|
||||||
36
tests/custom_control_structures.sl
Normal file
36
tests/custom_control_structures.sl
Normal 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
|
||||||
1
tests/i_non_compile_time.compile.expected
Normal file
1
tests/i_non_compile_time.compile.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[error] word 'i' is compile-time only and cannot be used at runtime while emitting 'main'
|
||||||
4
tests/i_non_compile_time.meta.json
Normal file
4
tests/i_non_compile_time.meta.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"expect_compile_error": true,
|
||||||
|
"description": "'i' is compile-time only and rejected in runtime code"
|
||||||
|
}
|
||||||
8
tests/i_non_compile_time.sl
Normal file
8
tests/i_non_compile_time.sl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import stdlib/stdlib.sl
|
||||||
|
|
||||||
|
word main
|
||||||
|
0
|
||||||
|
3 for
|
||||||
|
i puti cr
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
1
tests/ret_test.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
g
|
||||||
11
tests/ret_test.sl
Normal file
11
tests/ret_test.sl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import stdlib.sl
|
||||||
|
|
||||||
|
word g
|
||||||
|
"g" puts
|
||||||
|
ret
|
||||||
|
"g" puts
|
||||||
|
end
|
||||||
|
|
||||||
|
word main
|
||||||
|
g
|
||||||
|
end
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
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
|
||||||
|
|f |
|
||||||
|
| f|
|
||||||
|
|f|
|
||||||
|
|||||||
20
tests/str.sl
20
tests/str.sl
@@ -1,15 +1,29 @@
|
|||||||
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
|
||||||
|
|
||||||
|
" f " 2dup 2dup
|
||||||
|
124 putc ltrim write_buf 124 putc cr
|
||||||
|
124 putc rtrim write_buf 124 putc cr
|
||||||
|
124 putc trim write_buf 124 putc cr
|
||||||
end
|
end
|
||||||
|
|||||||
4
tests/syscall_write.asm.forbid
Normal file
4
tests/syscall_write.asm.forbid
Normal 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_
|
||||||
@@ -6,7 +6,6 @@ word main
|
|||||||
1
|
1
|
||||||
"hello"
|
"hello"
|
||||||
syscall.write
|
syscall.write
|
||||||
syscall
|
|
||||||
#drop
|
#drop
|
||||||
|
|
||||||
1
|
1
|
||||||
|
|||||||
4
tests/with_else_if_shorthand.expected
Normal file
4
tests/with_else_if_shorthand.expected
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
neg
|
||||||
|
zero
|
||||||
|
small
|
||||||
|
big
|
||||||
23
tests/with_else_if_shorthand.sl
Normal file
23
tests/with_else_if_shorthand.sl
Normal 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
220
tools/gen_linux_sl.py
Normal 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()
|
||||||
Reference in New Issue
Block a user