← Commit history

Language gaps found while building ash-json, on both ashvm and kiln

lennoxrose committed on Oct 8, 2026 at 15:59 UTC

517c220parent8c09e29

163 files changed+4017−527

Booleans (yes/no), insertion-ordered maps, one number format, string
comparison, chr/ord, sort, pop/insert/slice/delete(array, i), contains/
starts_with/ends_with/repeat, rename_file/delete_file/make_dir/list_dir,
exit(code), builtins as values, forward declarations, import aliases,
module-level variables in imported files, short-circuit and/or, a raise
that takes any value with e.message, identical error wording, and fixes
for stale attempt handlers (VM recovery bug; yield/stop/next now pop the
handler). Function limit raised to 1024.

kiln: push grows arrays geometrically; Windows file operations. Adds
Windows test scripts that run the PE builds under Wine or an emulator.

CHANGELOG.mdmodified

+53−0
@@ -1,3 +1,56 @@
1+## 2026-10-08 (item 22 reversed: no JSON in the language)
2+A built-in `json_*` library compiled into both engines was added and then removed again: the goal is general tools, not code for one module, and it needed name-mention detection and a special lookup rule in the compilers. The 'JSON in the stdlib?' item is answered by ash-json itself: its 0.3.0 codec is plain Ash built on the new tools (real booleans, insertion-ordered maps, `chr`/`ord`, forward declarations, `contains`/`repeat`, `rename_file`), with no compiler support. It parses and prints `\uXXXX` (surrogate pairs too), `\b`, `\f` and reports `json: <what> at line L, column C`. Its suite passes on both engines.
3+
4+## 2026-10-08 (the rest of the language gaps ash-json found)
5+Second batch; the numbers in parentheses are the entries of ash-json's list of language gaps. Every change is on both `ashvm` and `kiln` with a test in each `tests/cases/` tree, and the regression diffs below were checked against a baseline built from the previous commit (the intended ones: booleans printing as `yes`/`no`, insertion-ordered maps, the new number format, unified error wording). As before, there is deliberately no `remove` builtin (ash-json defines its own `remove` functions, and a builtin name would shadow them); `delete(array, i)` covers it.
6+
7+**Booleans (4):** new value type. `yes`/`no`, comparisons, `not`, `and`/`or`, `has`, `file_exists`, `contains`/`starts_with`/`ends_with`, `rename_file`/`delete_file`/`make_dir` and `write_file`/`append_file` produce it. Prints `yes`/`no`, `type()` is `"boolean"`, `str(yes)` is `"yes"`, `yes == 1` is false (arithmetic and truthiness still treat it as 1/0). `ashvm`: `VM_BOOL`, `vm_values_equal()` (also gives arrays/maps/functions identity equality, as kiln already did). `kiln`: `TAG_BOOL`. Conditions on both engines now accept booleans, numbers and `none` and raise `invalid operand type in condition` for a string/array/map/function (kiln used to treat a map as truthy; `kiln` has one `codegen_pop_and_test_truthy` instead of three copies). `filter` accepts a boolean-returning predicate.
8+
9+**Insertion-ordered maps (3):** `ashvm`'s open-addressing table became a dense entry list plus an index (deleting no longer leaves probe-chain holes, which also fixed lookups after a delete); `kiln`'s `delete` shifts later entries down instead of swapping in the last one. `keys`/`values`/printing agree on both.
10+
11+**Number format (8):** one format for both engines. `ashvm` used `%lld`/`%g` (6 significant digits), `kiln` printed 6 fraction digits and produced garbage past int64. Now: integers plainly below 1e15, otherwise up to 6 fraction digits with trailing zeros dropped, and scientific (`1e+21`, `1.5e-07`) at or above 1e15 / below 1e-6; `nan`/`inf`/`-inf`. `kiln`: `codegen/C/runtime/number_format.c`, shared by `say` and `str` (it replaces two copies and also fixes a carry bug at `x.9999999`); `ashvm`: `vm_format_number`. 6 fraction digits is not shortest-round-trip; that stays a documented limit.
12+
13+**`var.field` and `e.message` (9):** `x.name` on a local variable is `x["name"]`; indexing a string with a string yields the string itself, so `handle (e) { say e.message; }` works for built-in errors (strings) and raised maps alike on both engines.
14+
15+**Error wording and structured errors (9):** caught messages are identical on both engines and carry no prefix or detail (`index out of bounds`, `key not found`, `invalid operand type in '+'`, `could not open file`, ...); `kiln` adds `error: ` itself when an error is uncaught. `raise` takes any value (`kiln`'s raise routine now carries the value's tag), so `raise {"kind": "parse", "line": 3}` is caught as a map. Built-in errors remain strings (making every caught error a map would break `"x: " + e`); `ashvm` also gained string indexing `s[i]` to match `kiln`.
16+
17+**Sort (6):** `sort(array)` / `sort(array, cmp)`, in place, stable, returns the array; `cmp(a, b) > 0` means `a` goes after `b`. `ashvm`: merge sort; `kiln`: insertion sort (O(n^2)).
18+
19+**Files (17):** `rename_file`, `delete_file`, `make_dir`, `list_dir` (names without `.`/`..`, directory order). Windows `kiln` targets get real `MoveFileA`/`DeleteFileA`/`CreateDirectoryA` imports, and `list_dir` walks `FindFirstFileA`/`FindNextFileA`/`FindClose` (shared with the Linux path through `emit_add_entry`). The Windows code is verified by running it: `kiln/tests/scripts/windows_emulated_check.sh` executes each test case's PE under the Unicorn emulator with a Python stand-in for kernel32 (real files underneath), and all 63 runnable cases match their Linux builds, `fs_ops` included. The same cases also match under Wine 11.19 (`windows_wine_check.sh`), including `C:\\` paths for the file operations, which removes the dependence on my own kernel32 stand-in. Neither is real Windows.
20+
21+**Strings (18, 19):** `contains`, `starts_with`, `ends_with`, `repeat`. Complexity, as requested: strings carry their length now on `ashvm` (`len`, `substring`, `s[i]` no longer `strlen`), and are length-prefixed on `kiln`, so `len`/`s[i]` are O(1) and `substring` O(piece). `s += x` copies the whole string (quadratic over a loop, and nothing is freed); `join(parts, "")` is the linear builder. `kiln`'s `push` grew arrays by 4 slots per reallocation (quadratic memory against its 16 MB bump heap, so a 200k-element array crashed); it doubles now.
22+
23+**Builtins as values (11):** `type(len)`, `map(xs, str)`, `local f = len;` compile a one-line wrapper lambda by re-lexing a synthesized `forge (a0, ...) { yield name(a0, ...); }`. Variadic builtins (`sort`) wrap with their minimum arity.
24+
25+**Forward declarations (13):** a pre-pass over each file reserves every named `forge name(params)` (with its arity), so calls can precede the definition and functions can call each other. `ashvm`: `compiler/C/prescan.c`; `kiln`: calls to a not-yet-defined function emit a placeholder that is patched when the body is compiled (`function_add_call_fixup`/`function_resolve_fixups`). A reserved name that is never defined is `undefined function`. A local variable now shadows a function of the same name on `ashvm` too (kiln already did).
26+
27+**Imports (14, 15, 16):** `@import <file.ash> as name;` chooses the namespace. Transitive imports are now documented (README) and covered by `imports_transitive_test`. Every top-level `local` of an imported file is a module-level variable: its expression runs once where the `@import` is, the file's functions read it and update it (`NAME = ...;`, `NAME += ...;`, `NAME++;`, `NAME[i] = ...;`), and `file.NAME` reads it from outside. `ashvm`: `OP_GET_GLOBAL`/`OP_SET_GLOBAL` (256 slots); `kiln`: a 128-slot table in the globals region of every binary (+2 KB). It replaces the old "literal number/string constant only" rule (such constants were read-only).
28+
29+**Smaller (7, 10, 12, 20, 21):** exponent literals and `num("1.5e2")` already landed in the first batch; `exit(code)` ends the program with a status (an uncaught error exits 1), which is what a test runner needs; the `kiln` output that "silently stopped" after `attempt { delete(array, 0); ... }` came from `delete` treating an array as a map and is gone now that `delete(array, i)` exists (`attempt_array_delete` covers it).
30+
31+**Not done:** nothing left from that list.
32+
33+## 2026-10-08 (language gaps found by ash-json: function limit, `attempt` unwinding, short-circuit, string order, `chr`/`ord`, array mutation, more)
34+First batch from the ash-json 0.2.0 needs list (numbers are the entries of ash-json's list of language gaps). Everything below behaves the same on `ashvm` and `kiln` and has a test in both `tests/cases/` trees.
35+
36+**Function limit (0):** `MAX_VM_FUNCS` / `MAX_KILN_FUNCS` 64 -> 1024, so `@import <ash-json>;` (87 functions) loads.
37+
38+**Stale `attempt` handlers (9b):** two separate causes. `ashvm`: after a `longjmp`, `do_TRY_PUSH`'s recovery branch read its plain local `h`, which still pointed at a later, already-popped nested handler, so a caught error restored the wrong frame/stack. It now re-derives the handler from `try_depth`. Both engines: leaving an `attempt` block early with `yield`, `stop` or `next` skipped the handler pop, so the handler stayed active for the rest of the program. The compilers now count open `attempt` blocks per function (`attempt_enter`/`attempt_leave` in each `loop_stack.c`, which also remembers the count at loop entry) and emit the handler pops before those jumps/returns. No VM cost; kiln had this bug too (found by the new test).
39+
40+**`and` / `or` short-circuit on `ashvm` (10a):** compiled to `OP_JUMP_IF_FALSE` plus the old `OP_AND`/`OP_OR` against a constant (to keep the result 0/1); no new opcode.
41+
42+**String `<` `>` `<=` `>=` (1):** bytewise on both engines. `ashvm`: one `ORDER` macro used by the four ops and the fused `OP_CMP_JUMP`. `kiln`: `codegen_string_order()` leaves cmp-style flags that `push_bool` consumes like `ucomisd`'s. Mixed string/number is still a type error on `ashvm`; kiln keeps its "assume number" limit there.
43+
44+**`chr(n)` / `ord(s)` (2):** byte <-> one-byte string. `chr` takes 1..255 (`ashvm` strings are C strings, so no 0), `ord` of `""` is an error.
45+
46+**Arrays (5):** `delete(array, i)` removes at an index (returns the array, like `delete(map, k)`), plus new `pop(array)`, `insert(array, i, v)` (`i` may equal `len`) and `slice(array, from, to)` (`[from, to)`, strict bounds). kiln: `codegen/C/collections/array_mutate.c`; `insert` validates first, then reuses `push()` for growth. Not added as `remove`: ash-json defines functions called `remove`, and a builtin name shadows user functions.
47+
48+**Numbers (7, 12):** `ashvm` lexes exponent literals (`1e3`, `1.5e-3`) like kiln already did; kiln's `num()` now parses `e`/`E` exponents (`num("-1.5e2")` was `-1.5`).
49+
50+**Type errors (10, 10b):** kiln `"x" + 1` / `1 + "x"` and `str("text")` now raise (`invalid operand type in '+'`, `str() expects a number`) instead of returning garbage.
51+
52+**Still open at this point:** everything not listed above; the rest of the list is in the next entry up.
53+
154 ## 2026-10-07 (three more parity bugs, found verifying the JSON module before handing it off)
255 Before writing `mission.md` (a hand-off doc for shipping the first real `forgepack` module — a JSON codec in pure Ash), actually ran the module's code through both engines rather than just writing it and hoping. Found three more real bugs this way, on top of `type()` above — the module itself was the test case the existing regression suites never exercised (real, non-trivial, deeply-nested-and-recursive user code, not hand-picked feature probes).
356

README.mdmodified

+23−7
@@ -88,20 +88,36 @@ optimizing native compiler.
8888
8989 ## Language features
9090
91-- Numbers (real doubles), strings (with escape sequences), arrays, hash maps, `none`
92-- `yes`/`no` boolean literals (plain sugar for `1`/`0` - truthiness is number-based throughout)
93-- Functions, recursion, closures with by-value capture
94-- First-class functions - pass them around, store them in variables, call them indirectly
95-- `map`, `filter`, `reduce`, `type` (runtime type introspection: `"number"/"string"/"array"/"map"/"function"/"none"`), and 20+ other built-ins (string ops, file I/O, math)
96-- `attempt` / `handle` error handling, plus `raise expr;` for user-thrown errors with a runtime string message
97-- `each (x in array)` and `during` loops, with `stop` / `next` for early exit and skip-to-next-iteration
91+- Numbers (real doubles, with `1e3`-style literals), strings (with escape sequences), arrays, hash maps, `none`
92+- A real boolean type: `yes` / `no`, and everything that answers a question (`==`, `<`, `not`, `and`, `or`, `has`, `contains`, `file_exists`, ...) produces one. They print as `yes` / `no`, `type(yes)` is `"boolean"`, and `yes == 1` is false. Conditions accept booleans, numbers and `none`; a string, array, map or function in a condition is an error
93+- `and` / `or` short-circuit; strings compare bytewise with `< <= > >=`
94+- Maps keep insertion order (`keys`, `values`, printing); `delete` closes the gap
95+- Functions, recursion, closures with by-value capture, and forward declarations: a function can be called before its `forge` appears, so mutual recursion just works
96+- First-class functions - pass them around, store them in variables, call them indirectly. Builtins are values too: `map(xs, str)`, `local f = len;`
97+- Builtins:
98+ - core: `len type str num exit sqrt abs floor chr ord`
99+ - collections: `push pop insert slice delete sort keys values has map filter reduce` (`delete(array, i)` removes an index, `sort(array)` / `sort(array, cmp)` sorts in place and is stable; `cmp(a, b)` returns a number, `> 0` meaning `a` goes after `b`)
100+ - strings: `split join substring indexOf contains starts_with ends_with replace repeat upper lower trim`
101+ - files: `read_file write_file append_file file_exists rename_file delete_file make_dir list_dir`
102+- One number-to-text format on both engines: integers print plainly up to 1e15, other values with up to 6 fraction digits, and anything beyond that (or below 1e-6) in scientific form (`1e+21`, `1.5e-07`)
103+- `attempt` / `handle` error handling. Built-in errors raise a string with the same wording on both engines (`index out of bounds`, `key not found`, `invalid operand type in '+'`, ...); `raise expr;` raises any value, so a map gives a structured error. `e.message` (more generally `var.field`, same as `var["field"]`) reads a field of a raised map, and on a plain string error it is the string itself, so `e.message` works for every error
104+- `each (x in array)` and `during` loops, with `stop` / `next` for early exit and skip-to-next-iteration (leaving an `attempt` block that way pops its handler)
98105 - Compound assignment (`+= -= *= /=`) and `++` / `--`
99106 - Ternary `cond ? a : b`
100107 - Bitwise operators: `& | ^ ~ << >>`
108+- Imports: `@import <./file.ash>;` makes the file's functions callable as `file.name(...)`; `@import <./file.ash> as alias;` picks the namespace. Imports are transitive (a file imported by a file you import is callable by its own namespace too) and each file is loaded once. A top-level `local` in an imported file is a module-level variable: its expression runs once at import, and the file's functions can read and update it (`NAME = ...;`, `NAME += ...;`, `NAME[i] = ...;`); outside, `file.NAME` reads it
109+- `exit(code)` ends the program with that status; an uncaught error exits with status 1
101110 - Colorized, Rust-style diagnostics for both compile-time and runtime
102111 errors: file:line:column, a source snippet, a caret, and a contextual hint
103112 - An interactive REPL with persistent variables across lines
104113
114+### Performance notes
115+
116+- `len(s)`, `substring(s, i, j)` and `s[i]` are O(1) / O(length of the piece) on both engines (strings carry their length).
117+- `s += x` copies `s`, so building a long string one piece at a time is quadratic in time and, since nothing is freed, in memory (and `kiln` has a fixed 16 MB heap). Collect the pieces in an array and `join(parts, "")` once - that is linear.
118+- `push` grows arrays geometrically (amortized O(1)); `sort` is a stable merge sort on `ashvm` and a stable insertion sort on `kiln`.
119+- `kiln`'s Windows output is checked without a Windows machine: `kiln/tests/scripts/windows_emulated_check.sh` runs every test case as a PE under the Unicorn x86-64 emulator (kernel32 faked over the real filesystem) and compares it with the Linux build. All of them, including the Windows file operations (`list_dir` via `FindFirstFileA`/`FindNextFileA`), match. `kiln/tests/scripts/windows_wine_check.sh` does the same under Wine (an independent `kernel32`), also with all cases matching. Neither is Windows itself.
120+
105121 ## Getting started
106122
107123 ```bash

ashvm/Makefilemodified

+2−0
@@ -23,6 +23,8 @@ ASHVM_SRC = \
2323 src/compiler/C/stmt.c \
2424 src/compiler/C/stmt_control.c \
2525 src/compiler/C/stmt_import.c \
26+ src/compiler/C/module_state.c \
27+ src/compiler/C/prescan.c \
2628 src/builtins/C/builtins.c \
2729 src/builtins/C/collection.c \
2830 src/builtins/C/core.c \

ashvm/src/builtins/C/builtins.cmodified

+25−2
@@ -8,7 +8,24 @@ static const char *builtin_names[] = {
88 "map", "filter", "reduce", "keys", "values", "has", "delete",
99 "split", "join", "substring", "indexOf", "replace", "upper", "lower", "trim",
1010 "read_file", "write_file", "append_file", "file_exists",
11- "type"
11+ "type", "chr", "ord",
12+ "pop", "insert", "slice", "sort",
13+ "rename_file", "delete_file", "make_dir", "list_dir",
14+ "contains", "starts_with", "ends_with", "repeat",
15+ "exit"
16+};
17+// Argument count each builtin is wrapped with when used as a value
18+// (`map(xs, str)`); same order as builtin_names.
19+static const int builtin_arity[] = {
20+ 1, 2, 0, 1, 1, 1, 1, 1,
21+ 2, 2, 3, 1, 1, 2, 2,
22+ 2, 2, 3, 2, 3, 1, 1, 1,
23+ 1, 2, 2, 1,
24+ 1, 1, 1,
25+ 1, 3, 3, 1,
26+ 2, 1, 1, 1,
27+ 2, 2, 2, 2,
28+ 1
1229 };
1330 #define NUM_BUILTINS (int)(sizeof(builtin_names) / sizeof(builtin_names[0]))
1431
@@ -19,11 +36,17 @@ int vm_builtin_lookup(const char *name, int len) {
1936 return -1;
2037 }
2138
39+int vm_builtin_arity(int id) { return builtin_arity[id]; }
40+
2241 VMValue vm_call_builtin(int id, VMValue *args, int argc) {
2342 if (id >= 0 && id <= 7) return vm_call_builtin_core(id, args, argc);
2443 if (id >= 8 && id <= 14) return vm_call_builtin_collection(id, args, argc);
2544 if (id >= 15 && id <= 22) return vm_call_builtin_string(id, args, argc);
2645 if (id >= 23 && id <= 26) return vm_call_builtin_file(id, args, argc);
27- if (id == 27) return vm_call_builtin_core(id, args, argc); // type
46+ if (id >= 27 && id <= 29) return vm_call_builtin_core(id, args, argc); // type, chr, ord
47+ if (id == 42) return vm_call_builtin_core(id, args, argc); // exit
48+ if (id >= 38 && id <= 41) return vm_call_builtin_string(id, args, argc); // contains, starts_with, ends_with, repeat
49+ if (id >= 34 && id <= 37) return vm_call_builtin_file(id, args, argc); // rename_file, delete_file, make_dir, list_dir
50+ if (id >= 30 && id <= 33) return vm_call_builtin_collection(id, args, argc); // pop, insert, slice, sort
2851 vm_runtime_error("unknown builtin id: %d\n", id);
2952 }

ashvm/src/builtins/C/collection.cmodified

+75−6
@@ -21,7 +21,7 @@ VMValue vm_call_builtin_collection(int id, VMValue *args, int argc) {
2121 for (int i = 0; i < args[0].array->count; i++) {
2222 VMValue call_args[1] = { args[0].array->items[i] };
2323 VMValue keep = vm_call_callable_sync(args[1], call_args, 1);
24- if (keep.type == VM_NUM && keep.number != 0) vm_array_push(result, args[0].array->items[i]);
24+ if ((keep.type == VM_NUM || keep.type == VM_BOOL) && keep.number != 0) vm_array_push(result, args[0].array->items[i]);
2525 }
2626 return vm_array_val(result);
2727 }
@@ -37,7 +37,7 @@ VMValue vm_call_builtin_collection(int id, VMValue *args, int argc) {
3737 case 11: { // keys
3838 if (argc != 1 || args[0].type != VM_MAP) { vm_runtime_error("keys() expects a map\n"); }
3939 VMArray *result = vm_array_new();
40- for (int i = 0; i < args[0].map->capacity; i++) {
40+ for (int i = 0; i < args[0].map->size; i++) {
4141 if (args[0].map->entries[i].used) {
4242 char *k = args[0].map->entries[i].key;
4343 char *copy = malloc(strlen(k) + 1); strcpy(copy, k);
@@ -49,20 +49,89 @@ VMValue vm_call_builtin_collection(int id, VMValue *args, int argc) {
4949 case 12: { // values
5050 if (argc != 1 || args[0].type != VM_MAP) { vm_runtime_error("values() expects a map\n"); }
5151 VMArray *result = vm_array_new();
52- for (int i = 0; i < args[0].map->capacity; i++) {
52+ for (int i = 0; i < args[0].map->size; i++) {
5353 if (args[0].map->entries[i].used) vm_array_push(result, args[0].map->entries[i].value);
5454 }
5555 return vm_array_val(result);
5656 }
5757 case 13: { // has
5858 if (argc != 2 || args[0].type != VM_MAP || args[1].type != VM_STR) { vm_runtime_error("has(map, key) expected\n"); }
59- return vm_num(vm_map_has(args[0].map, args[1].str) ? 1 : 0);
59+ return vm_bool(vm_map_has(args[0].map, args[1].str));
6060 }
61- case 14: { // delete
62- if (argc != 2 || args[0].type != VM_MAP || args[1].type != VM_STR) { vm_runtime_error("delete(map, key) expected\n"); }
61+ case 14: { // delete -- a map key, or an array index (later items shift left)
62+ if (argc == 2 && args[0].type == VM_ARRAY && args[1].type == VM_NUM) {
63+ VMArray *a = args[0].array;
64+ int i = (int)args[1].number;
65+ if (i < 0 || i >= a->count) { vm_runtime_error("index out of bounds"); }
66+ memmove(&a->items[i], &a->items[i + 1], sizeof(VMValue) * (size_t)(a->count - i - 1));
67+ a->count--;
68+ return args[0];
69+ }
70+ if (argc != 2 || args[0].type != VM_MAP || args[1].type != VM_STR) { vm_runtime_error("delete(map, key) or delete(array, index) expected\n"); }
6371 vm_map_delete(args[0].map, args[1].str);
6472 return args[0];
6573 }
74+ case 30: { // pop -- removes and returns the last item
75+ if (argc != 1 || args[0].type != VM_ARRAY) { vm_runtime_error("pop(array) expected\n"); }
76+ if (args[0].array->count == 0) { vm_runtime_error("pop() on an empty array\n"); }
77+ return args[0].array->items[--args[0].array->count];
78+ }
79+ case 31: { // insert -- insert(array, index, value); index may equal len
80+ if (argc != 3 || args[0].type != VM_ARRAY || args[1].type != VM_NUM) { vm_runtime_error("insert(array, index, value) expected\n"); }
81+ VMArray *a = args[0].array;
82+ int i = (int)args[1].number;
83+ if (i < 0 || i > a->count) { vm_runtime_error("index out of bounds"); }
84+ vm_array_push(a, args[2]); // grows; the new slot is then shifted into place
85+ memmove(&a->items[i + 1], &a->items[i], sizeof(VMValue) * (size_t)(a->count - 1 - i));
86+ a->items[i] = args[2];
87+ return args[0];
88+ }
89+ case 32: { // slice -- slice(array, from, to) is a new array of items [from, to)
90+ if (argc != 3 || args[0].type != VM_ARRAY || args[1].type != VM_NUM || args[2].type != VM_NUM) { vm_runtime_error("slice(array, from, to) expected\n"); }
91+ int from = (int)args[1].number, to = (int)args[2].number;
92+ if (from < 0 || to > args[0].array->count || from > to) { vm_runtime_error("slice bounds out of range"); }
93+ VMArray *result = vm_array_new();
94+ for (int i = from; i < to; i++) vm_array_push(result, args[0].array->items[i]);
95+ return vm_array_val(result);
96+ }
97+ case 33: { // sort -- in place, stable merge sort; sort(array) or sort(array, cmp)
98+ if ((argc != 1 && argc != 2) || args[0].type != VM_ARRAY) { vm_runtime_error("sort(array) or sort(array, cmp) expected\n"); }
99+ int has_cmp = (argc == 2);
100+ if (has_cmp && args[1].type != VM_FUNCTION && args[1].type != VM_CLOSURE) { vm_runtime_error("sort(array, cmp) expected\n"); }
101+ VMArray *a = args[0].array;
102+ int n = a->count;
103+ if (n < 2) return args[0];
104+ VMValue *src = a->items;
105+ VMValue *tmp = malloc(sizeof(VMValue) * (size_t)n);
106+ for (int width = 1; width < n; width *= 2) {
107+ for (int lo = 0; lo < n; lo += 2 * width) {
108+ int mid = lo + width < n ? lo + width : n;
109+ int hi = lo + 2 * width < n ? lo + 2 * width : n;
110+ int l = lo, r = mid, o = lo;
111+ while (l < mid && r < hi) {
112+ // take the left item unless it must come after the right one (keeps it stable)
113+ int right_first;
114+ if (has_cmp) {
115+ VMValue call_args[2] = { src[l], src[r] };
116+ VMValue res = vm_call_callable_sync(args[1], call_args, 2);
117+ if (res.type != VM_NUM) { free(tmp); vm_runtime_error("sort() comparator must return a number\n"); }
118+ right_first = res.number > 0;
119+ } else if (src[l].type == VM_NUM && src[r].type == VM_NUM) {
120+ right_first = src[l].number > src[r].number;
121+ } else if (src[l].type == VM_STR && src[r].type == VM_STR) {
122+ right_first = strcmp(src[l].str, src[r].str) > 0;
123+ } else { free(tmp); vm_runtime_error("sort() cannot compare these values\n"); }
124+ tmp[o++] = right_first ? src[r++] : src[l++];
125+ }
126+ while (l < mid) tmp[o++] = src[l++];
127+ while (r < hi) tmp[o++] = src[r++];
128+ }
129+ VMValue *swap = src; src = tmp; tmp = swap;
130+ }
131+ if (src != a->items) memcpy(a->items, src, sizeof(VMValue) * (size_t)n);
132+ free(src == a->items ? tmp : src);
133+ return args[0];
134+ }
66135 }
67136 vm_runtime_error("unknown builtin id: %d\n", id);
68137 }

ashvm/src/builtins/C/core.cmodified

+24−3
@@ -10,7 +10,7 @@ VMValue vm_call_builtin_core(int id, VMValue *args, int argc) {
1010 switch (id) {
1111 case 0: { // len
1212 if (argc != 1) { vm_runtime_error("len() expects 1 argument\n"); }
13- if (args[0].type == VM_STR) return vm_num((int64_t)strlen(args[0].str));
13+ if (args[0].type == VM_STR) return vm_num(args[0].number);
1414 if (args[0].type == VM_ARRAY) return vm_num(args[0].array->count);
1515 if (args[0].type == VM_MAP) return vm_num(args[0].map->count);
1616 vm_runtime_error("len() expects a string, array, or map\n");
@@ -29,10 +29,13 @@ VMValue vm_call_builtin_core(int id, VMValue *args, int argc) {
2929 return vm_str(copy);
3030 }
3131 case 3: { // str
32+ if (argc == 1 && args[0].type == VM_BOOL) {
33+ char *copy = malloc(4); strcpy(copy, args[0].number != 0 ? "yes" : "no");
34+ return vm_str(copy);
35+ }
3236 if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("str() expects a number\n"); }
3337 char buf[64];
34- if (args[0].number == (long long)args[0].number) snprintf(buf, sizeof(buf), "%lld", (long long)args[0].number);
35- else snprintf(buf, sizeof(buf), "%g", args[0].number);
38+ vm_format_number(args[0].number, buf, sizeof(buf));
3639 char *copy = malloc(strlen(buf) + 1); strcpy(copy, buf);
3740 return vm_str(copy);
3841 }
@@ -62,6 +65,7 @@ VMValue vm_call_builtin_core(int id, VMValue *args, int argc) {
6265 const char *name;
6366 switch (args[0].type) {
6467 case VM_NUM: name = "number"; break;
68+ case VM_BOOL: name = "boolean"; break;
6569 case VM_STR: name = "string"; break;
6670 case VM_ARRAY: name = "array"; break;
6771 case VM_MAP: name = "map"; break;
@@ -72,6 +76,23 @@ VMValue vm_call_builtin_core(int id, VMValue *args, int argc) {
7276 char *copy = malloc(strlen(name) + 1); strcpy(copy, name);
7377 return vm_str(copy);
7478 }
79+ case 42: { // exit -- exit(code) ends the program with that status
80+ if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("exit(code) expected\n"); }
81+ fflush(stdout);
82+ exit((int)args[0].number);
83+ }
84+ case 28: { // chr -- byte value to a one-byte string (strings are C strings: no 0)
85+ if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("chr() expects a number\n"); }
86+ double n = args[0].number;
87+ if (n < 1 || n > 255) { vm_runtime_error("chr() expects a number from 1 to 255\n"); }
88+ char *s = malloc(2); s[0] = (char)(unsigned char)n; s[1] = '\0';
89+ return vm_str(s);
90+ }
91+ case 29: { // ord -- first byte of a string, 0..255
92+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("ord() expects a string\n"); }
93+ if (args[0].str[0] == '\0') { vm_runtime_error("ord() expects a non-empty string\n"); }
94+ return vm_num((unsigned char)args[0].str[0]);
95+ }
7596 }
7697 vm_runtime_error("unknown builtin id: %d\n", id);
7798 }

ashvm/src/builtins/C/file.cmodified

+35−7
@@ -1,6 +1,8 @@
11 #include <stdio.h>
22 #include <stdlib.h>
33 #include <string.h>
4+#include <dirent.h>
5+#include <sys/stat.h>
46 #include "builtins/H/internal.h"
57 #include "vm/H/vm.h"
68
@@ -9,30 +11,56 @@ VMValue vm_call_builtin_file(int id, VMValue *args, int argc) {
911 case 23: { // read_file
1012 if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("read_file(path) expected\n"); }
1113 FILE *f = fopen(args[0].str, "rb");
12- if (!f) { vm_runtime_error("could not open file: %s\n", args[0].str); }
14+ if (!f) { vm_runtime_error("could not open file"); }
1315 fseek(f, 0, SEEK_END); long size = ftell(f); rewind(f);
1416 char *buf = malloc(size + 1); size_t n = fread(buf, 1, size, f); buf[n] = '\0'; fclose(f);
1517 return vm_str(buf);
1618 }
1719 case 24: { // write_file
1820 if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("write_file(path, content) expected\n"); }
1921 FILE *f = fopen(args[0].str, "wb");
20- if (!f) { vm_runtime_error("could not write file: %s\n", args[0].str); }
22+ if (!f) { vm_runtime_error("could not open file"); }
2123 fwrite(args[1].str, 1, strlen(args[1].str), f); fclose(f);
22- return vm_num(1);
24+ return vm_bool(1);
2325 }
2426 case 25: { // append_file
2527 if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("append_file(path, content) expected\n"); }
2628 FILE *f = fopen(args[0].str, "ab");
27- if (!f) { vm_runtime_error("could not open file for append: %s\n", args[0].str); }
29+ if (!f) { vm_runtime_error("could not open file"); }
2830 fwrite(args[1].str, 1, strlen(args[1].str), f); fclose(f);
29- return vm_num(1);
31+ return vm_bool(1);
3032 }
3133 case 26: { // file_exists
3234 if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("file_exists(path) expected\n"); }
3335 FILE *f = fopen(args[0].str, "rb");
34- if (f) { fclose(f); return vm_num(1); }
35- return vm_num(0);
36+ if (f) { fclose(f); return vm_bool(1); }
37+ return vm_bool(0);
38+ }
39+ case 34: { // rename_file(from, to) -> 1 on success, 0 on failure
40+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("rename_file(from, to) expected\n"); }
41+ return vm_bool(rename(args[0].str, args[1].str) == 0);
42+ }
43+ case 35: { // delete_file(path)
44+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("delete_file(path) expected\n"); }
45+ return vm_bool(remove(args[0].str) == 0);
46+ }
47+ case 36: { // make_dir(path)
48+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("make_dir(path) expected\n"); }
49+ return vm_bool(mkdir(args[0].str, 0755) == 0);
50+ }
51+ case 37: { // list_dir(path) -> names, no "." / "..", in directory order
52+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("list_dir(path) expected\n"); }
53+ DIR *d = opendir(args[0].str);
54+ if (!d) { vm_runtime_error("cannot open directory"); }
55+ VMArray *result = vm_array_new();
56+ struct dirent *e;
57+ while ((e = readdir(d)) != NULL) {
58+ if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
59+ char *copy = malloc(strlen(e->d_name) + 1); strcpy(copy, e->d_name);
60+ vm_array_push(result, vm_str(copy));
61+ }
62+ closedir(d);
63+ return vm_array_val(result);
3664 }
3765 }
3866 vm_runtime_error("unknown builtin id: %d\n", id);

ashvm/src/builtins/C/string.cmodified

+24−2
@@ -44,13 +44,13 @@ VMValue vm_call_builtin_string(int id, VMValue *args, int argc) {
4444 }
4545 case 17: { // substring
4646 if (argc != 3 || args[0].type != VM_STR) { vm_runtime_error("substring(str, start, end) expected\n"); }
47- int slen = (int)strlen(args[0].str);
47+ int slen = (int)args[0].number; // cached length
4848 int start = (int)args[1].number; int end = (int)args[2].number;
4949 if (start < 0) start = 0;
5050 if (end > slen) end = slen;
5151 if (start > end) start = end;
5252 char *buf = malloc(end - start + 1); memcpy(buf, args[0].str + start, end - start); buf[end - start] = '\0';
53- return vm_str(buf);
53+ return vm_str_n(buf, (size_t)(end - start));
5454 }
5555 case 18: { // indexOf
5656 if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("indexOf(str, search) expected\n"); }
@@ -76,6 +76,28 @@ VMValue vm_call_builtin_string(int id, VMValue *args, int argc) {
7676 memcpy(buf + blen, cur, remaining); blen += remaining; buf[blen] = '\0';
7777 return vm_str(buf);
7878 }
79+ case 38: { // contains
80+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("contains(str, search) expected\n"); }
81+ return vm_bool(strstr(args[0].str, args[1].str) != NULL);
82+ }
83+ case 39: { // starts_with
84+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("starts_with(str, prefix) expected\n"); }
85+ return vm_bool(strncmp(args[0].str, args[1].str, strlen(args[1].str)) == 0);
86+ }
87+ case 40: { // ends_with
88+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("ends_with(str, suffix) expected\n"); }
89+ size_t sl = strlen(args[0].str), xl = strlen(args[1].str);
90+ return vm_bool(xl <= sl && strcmp(args[0].str + (sl - xl), args[1].str) == 0);
91+ }
92+ case 41: { // repeat
93+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_NUM) { vm_runtime_error("repeat(str, count) expected\n"); }
94+ if (args[1].number < 0) { vm_runtime_error("repeat() count must not be negative\n"); }
95+ size_t n = (size_t)args[1].number, sl = strlen(args[0].str);
96+ char *buf = malloc(sl * n + 1);
97+ for (size_t k = 0; k < n; k++) memcpy(buf + k * sl, args[0].str, sl);
98+ buf[sl * n] = '\0';
99+ return vm_str(buf);
100+ }
79101 case 20: { // upper
80102 if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("upper() expects a string\n"); }
81103 char *copy = malloc(strlen(args[0].str) + 1); strcpy(copy, args[0].str);

ashvm/src/builtins/H/builtins.hmodified

+3−0
@@ -5,4 +5,7 @@
55 int vm_builtin_lookup(const char *name, int len);
66 VMValue vm_call_builtin(int id, VMValue *args, int argc);
77
8+// Number of arguments the builtin is wrapped with when used as a value.
9+int vm_builtin_arity(int id);
10+
811 #endif

ashvm/src/compiler/C/calls.cmodified

+35−6
@@ -8,8 +8,9 @@
88
99 void emit_call(Token id) {
1010 int builtin_id = vm_builtin_lookup(id.start, id.length);
11- int fn_idx = (builtin_id == -1) ? find_function_in_context(id.start, id.length) : -1;
12- int local_slot = (builtin_id == -1 && fn_idx == -1) ? resolve_local(id.start, id.length) : -1;
11+ // A local variable shadows a function of the same name (as on kiln).
12+ int local_slot = (builtin_id == -1) ? resolve_local(id.start, id.length) : -1;
13+ int fn_idx = (builtin_id == -1 && local_slot == -1) ? find_function_in_context(id.start, id.length) : -1;
1314
1415 int argc = 0;
1516 if (current.type != TOKEN_RPAREN) {
@@ -95,6 +96,7 @@ void lambda_literal(void) {
9596 memcpy(vm_functions[fn_idx].name, namebuf, namelen);
9697 vm_functions[fn_idx].name[namelen] = '\0';
9798 vm_functions[fn_idx].arity = 0;
99+ vm_functions[fn_idx].defined = 1;
98100 chunk_init(&vm_functions[fn_idx].chunk);
99101
100102 char param_names[8][64];
@@ -124,9 +126,9 @@ void lambda_literal(void) {
124126 char outer_locals[MAX_VM_LOCALS][64];
125127 int outer_local_count = local_count;
126128 memcpy(outer_locals, local_names, sizeof(local_names));
127- int outer_loop_depth;
128- loop_depth_save(&outer_loop_depth);
129- loop_depth_reset();
129+ LoopScope outer_loop_scope;
130+ loop_scope_save(&outer_loop_scope);
131+ loop_scope_reset();
130132
131133 chunk = &vm_functions[fn_idx].chunk;
132134 local_count = 0;
@@ -140,7 +142,7 @@ void lambda_literal(void) {
140142 chunk = outer_chunk;
141143 local_count = outer_local_count;
142144 memcpy(local_names, outer_locals, sizeof(local_names));
143- loop_depth_restore(outer_loop_depth);
145+ loop_scope_restore(outer_loop_scope);
144146
145147 emit_op(OP_MAKE_CLOSURE);
146148 emit((uint8_t)fn_idx);
@@ -202,3 +204,30 @@ int try_fuse_condition(TokenType terminator) {
202204 emit(0xff); emit(0xff);
203205 return patch_loc;
204206 }
207+
208+// A builtin name used as a value (`map(xs, str)`, `local f = len;`) becomes a
209+// small wrapper lambda `forge (a0, ...) { yield name(a0, ...); }`, compiled by
210+// pointing the lexer at a synthesized snippet. `current` is the token after the
211+// name; everything is restored afterwards. Returns 0 if `id` isn't a builtin.
212+int emit_builtin_value(Token id) {
213+ int builtin_id = vm_builtin_lookup(id.start, id.length);
214+ if (builtin_id == -1) return 0;
215+ int arity = vm_builtin_arity(builtin_id);
216+
217+ char *snippet = malloc(64 + 2 * (size_t)id.length + 12 * (size_t)arity); // kept: tokens point into it
218+ int n = sprintf(snippet, "forge (");
219+ for (int i = 0; i < arity; i++) n += sprintf(snippet + n, "%s__%d", i ? ", " : "", i);
220+ n += sprintf(snippet + n, ") { yield %.*s(", id.length, id.start);
221+ for (int i = 0; i < arity; i++) n += sprintf(snippet + n, "%s__%d", i ? ", " : "", i);
222+ sprintf(snippet + n, "); }");
223+
224+ Token saved_current = current, saved_previous = previous;
225+ LexerState saved_lexer = lexer_save_state();
226+ lexer_init(snippet);
227+ advance_token(); // current = 'forge'
228+ lambda_literal();
229+ lexer_restore_state(saved_lexer);
230+ current = saved_current;
231+ previous = saved_previous;
232+ return 1;
233+}

ashvm/src/compiler/C/expr.cmodified

+48−4
@@ -21,9 +21,9 @@ static void primary(void) {
2121 return;
2222 }
2323 if (current.type == TOKEN_YES || current.type == TOKEN_NO) {
24- double v = current.type == TOKEN_YES ? 1.0 : 0.0;
24+ int v = current.type == TOKEN_YES;
2525 advance_token();
26- emit_constant(vm_num(v));
26+ emit_constant(vm_bool(v));
2727 return;
2828 }
2929 if (current.type == TOKEN_NONE) {
@@ -68,6 +68,20 @@ static void primary(void) {
6868 Token id = current;
6969 advance_token();
7070
71+ // `var.field` on a variable (not a namespace) is var["field"]; on an error
72+ // string it is the string itself, so `e.message` works for built-in errors
73+ // and for raised maps alike.
74+ if (current.type == TOKEN_DOT && resolve_local(id.start, id.length) != -1) {
75+ emit2(OP_GET_LOCAL, (uint8_t)resolve_local(id.start, id.length));
76+ while (current.type == TOKEN_DOT) {
77+ advance_token();
78+ expect(TOKEN_IDENTIFIER, "expected field name after '.'");
79+ emit_constant(vm_str(vm_copy_string_escaped(previous.start, previous.length)));
80+ emit_op(OP_INDEX_GET);
81+ }
82+ return;
83+ }
84+
7185 if (current.type == TOKEN_DOT) {
7286 advance_token();
7387 expect(TOKEN_IDENTIFIER, "expected name after '.'");
@@ -85,6 +99,8 @@ static void primary(void) {
8599
86100 VMConstant *k = resolve_constant(combined, combined_len);
87101 if (k != NULL) { codegen_constant_value(k); return; }
102+ int state = module_state_resolve_qualified(combined, combined_len);
103+ if (state != -1) { emit2(OP_GET_GLOBAL, (uint8_t)state); return; }
88104
89105 diagnostics_report("error", "undefined namespaced function or constant", id.start, id.length);
90106 exit(1);
@@ -112,6 +128,10 @@ static void primary(void) {
112128 if (k != NULL) { codegen_constant_value(k); return; }
113129 }
114130
131+ int state = module_state_resolve(id.start, id.length);
132+ if (state != -1) { emit2(OP_GET_GLOBAL, (uint8_t)state); return; }
133+ if (emit_builtin_value(id)) return;
134+
115135 char msg[128];
116136 snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
117137 diagnostics_report("error", msg, id.start, id.length);
@@ -208,13 +228,37 @@ static void bitwise(void) {
208228 }
209229 }
210230
231+// `and` / `or` short-circuit: the right operand only runs when the left
232+// doesn't already decide the result. Built from the same OP_JUMP_IF_FALSE
233+// the ternary uses; the right operand is normalized to 0/1 by OP_AND/OP_OR
234+// against a constant, so the result is the same 0/1 the old eager ops gave.
211235 static void logical_and(void) {
212236 bitwise();
213- while (current.type == TOKEN_AND) { advance_token(); bitwise(); emit_op(OP_AND); }
237+ while (current.type == TOKEN_AND) {
238+ advance_token();
239+ int lhs_false = emit_jump(OP_JUMP_IF_FALSE);
240+ bitwise();
241+ emit_constant(vm_num(1));
242+ emit_op(OP_AND);
243+ int end = emit_jump(OP_JUMP);
244+ patch_jump(lhs_false);
245+ emit_constant(vm_bool(0));
246+ patch_jump(end);
247+ }
214248 }
215249 static void logical_or(void) {
216250 logical_and();
217- while (current.type == TOKEN_OR) { advance_token(); logical_and(); emit_op(OP_OR); }
251+ while (current.type == TOKEN_OR) {
252+ advance_token();
253+ int try_rhs = emit_jump(OP_JUMP_IF_FALSE);
254+ emit_constant(vm_bool(1));
255+ int end = emit_jump(OP_JUMP);
256+ patch_jump(try_rhs);
257+ logical_and();
258+ emit_constant(vm_num(0));
259+ emit_op(OP_OR);
260+ patch_jump(end);
261+ }
218262 }
219263
220264 // a ? b : c -- lowest precedence, sits above logical_or. Right-associative

ashvm/src/compiler/C/loop_stack.cmodified

+20−3
@@ -11,10 +11,12 @@ typedef struct {
1111 int stop_count;
1212 int next_jumps[MAX_LOOP_JUMPS];
1313 int next_count;
14+ int attempt_base; // attempt_depth when the loop began
1415 } LoopContext;
1516
1617 static LoopContext loop_stack[MAX_LOOP_NESTING];
1718 static int loop_depth = 0;
19+static int attempt_depth = 0; // attempt blocks currently open in this function body
1820
1921 void loop_push(void) {
2022 if (loop_depth >= MAX_LOOP_NESTING) {
@@ -23,9 +25,24 @@ void loop_push(void) {
2325 }
2426 loop_stack[loop_depth].stop_count = 0;
2527 loop_stack[loop_depth].next_count = 0;
28+ loop_stack[loop_depth].attempt_base = attempt_depth;
2629 loop_depth++;
2730 }
2831
32+void attempt_enter(void) { attempt_depth++; }
33+void attempt_leave(void) { attempt_depth--; }
34+
35+static void emit_try_pops(int count) {
36+ for (int i = 0; i < count; i++) emit_op(OP_TRY_POP);
37+}
38+
39+void emit_attempt_unwind_all(void) { emit_try_pops(attempt_depth); }
40+
41+// stop/next leave every attempt block opened since the innermost loop began.
42+void emit_attempt_unwind_to_loop(void) {
43+ if (loop_depth > 0) emit_try_pops(attempt_depth - loop_stack[loop_depth - 1].attempt_base);
44+}
45+
2946 void loop_record_stop(Token keyword, int patch_offset) {
3047 if (loop_depth == 0) {
3148 diagnostics_report("error", "'stop' outside a loop", keyword.start, keyword.length);
@@ -63,6 +80,6 @@ void loop_pop_and_patch_stops(void) {
6380 loop_depth--;
6481 }
6582
66-void loop_depth_save(int *out) { *out = loop_depth; }
67-void loop_depth_reset(void) { loop_depth = 0; }
68-void loop_depth_restore(int saved) { loop_depth = saved; }
83+void loop_scope_save(LoopScope *out) { out->loops = loop_depth; out->attempts = attempt_depth; }
84+void loop_scope_reset(void) { loop_depth = 0; attempt_depth = 0; }
85+void loop_scope_restore(LoopScope saved) { loop_depth = saved.loops; attempt_depth = saved.attempts; }

ashvm/src/compiler/C/module_state.cadded

+49−0
@@ -0,0 +1,49 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "compiler/H/module_state.h"
5+#include "compiler/H/internal.h"
6+#include "vm/H/state.h"
7+#include "diagnostics/H/diagnostics.h"
8+
9+static char names[MAX_VM_GLOBALS][96];
10+static int count = 0;
11+
12+int module_state_resolve_qualified(const char *name, int len) {
13+ for (int i = 0; i < count; i++) {
14+ if ((int)strlen(names[i]) == len && strncmp(names[i], name, (size_t)len) == 0) return i;
15+ }
16+ return -1;
17+}
18+
19+int module_state_resolve(const char *name, int len) {
20+ const char *ns; int ns_len;
21+ get_import_namespace(&ns, &ns_len);
22+ if (ns_len == 0) return -1;
23+ char buf[128];
24+ int n = snprintf(buf, sizeof(buf), "%.*s.%.*s", ns_len, ns, len, name);
25+ if (n < 0 || n >= (int)sizeof(buf)) return -1;
26+ return module_state_resolve_qualified(buf, n);
27+}
28+
29+int module_state_declare(const char *name, int len) {
30+ const char *ns; int ns_len;
31+ get_import_namespace(&ns, &ns_len);
32+ char buf[128];
33+ int n = snprintf(buf, sizeof(buf), "%.*s.%.*s", ns_len, ns, len, name);
34+ if (n < 0 || n >= (int)sizeof(names[0])) {
35+ diagnostics_report("error", "module-level name too long (including namespace prefix)", current.start, current.length);
36+ exit(1);
37+ }
38+ if (module_state_resolve_qualified(buf, n) != -1) {
39+ diagnostics_report("error", "module-level name already declared", current.start, current.length);
40+ exit(1);
41+ }
42+ if (count >= MAX_VM_GLOBALS) {
43+ diagnostics_report("error", "too many module-level variables", current.start, current.length);
44+ exit(1);
45+ }
46+ memcpy(names[count], buf, (size_t)n);
47+ names[count][n] = '\0';
48+ return count++;
49+}

ashvm/src/compiler/C/prescan.cadded

+48−0
@@ -0,0 +1,48 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include "compiler/H/internal.h"
4+#include "compiler/H/compiler.h"
5+#include "diagnostics/H/diagnostics.h"
6+
7+// Forward declarations. Before a file is compiled, scan its tokens for named
8+// `forge name(a, b)` declarations and reserve each name with its arity, so an
9+// earlier function (or top-level code) can call a later one, and two functions
10+// can call each other. A declaration still has to appear somewhere in the
11+// program; check_all_functions_defined() reports a name that was reserved but
12+// never defined. Lambdas (`forge (x) { ... }`) have no name and are skipped.
13+void prescan_functions(const char *source) {
14+ LexerState saved = lexer_save_state();
15+ lexer_init(source);
16+ Token t = lexer_next_token();
17+ while (t.type != TOKEN_EOF) {
18+ if (t.type == TOKEN_FORGE) {
19+ Token name = lexer_next_token();
20+ if (name.type != TOKEN_IDENTIFIER) { t = name; continue; }
21+ Token paren = lexer_next_token();
22+ if (paren.type != TOKEN_LPAREN) { t = paren; continue; }
23+ int arity = 0;
24+ t = lexer_next_token();
25+ if (t.type != TOKEN_RPAREN) {
26+ arity = 1;
27+ while (t.type != TOKEN_RPAREN && t.type != TOKEN_EOF) {
28+ if (t.type == TOKEN_COMMA) arity++;
29+ t = lexer_next_token();
30+ }
31+ }
32+ predeclare_function_in_context(name.start, name.length, arity);
33+ }
34+ if (t.type != TOKEN_EOF) t = lexer_next_token();
35+ }
36+ lexer_restore_state(saved);
37+}
38+
39+void check_all_functions_defined(void) {
40+ for (int i = 0; i < vm_function_count; i++) {
41+ if (!vm_functions[i].defined) {
42+ char msg[128];
43+ snprintf(msg, sizeof(msg), "undefined function: %s", vm_functions[i].name);
44+ diagnostics_report("error", msg, current.start, current.length);
45+ exit(1);
46+ }
47+ }
48+}

ashvm/src/compiler/C/state.cmodified

+29−0
@@ -73,14 +73,43 @@ int declare_function_in_context(const char *name, int len) {
7373 final_name = buf;
7474 final_len = combined_len;
7575 }
76+ // A prescan (prescan.c) may already have reserved this name so earlier code
77+ // could call it; the real declaration fills that entry in.
78+ int existing = find_function(final_name, final_len);
79+ if (existing != -1 && !vm_functions[existing].defined) {
80+ vm_functions[existing].defined = 1;
81+ return existing;
82+ }
7683 if (vm_function_count >= MAX_VM_FUNCS) { diagnostics_report("error", "too many functions", current.start, current.length); exit(1); }
7784 if (final_len >= (int)sizeof(vm_functions[0].name)) { diagnostics_report("error", "function name too long", current.start, current.length); exit(1); }
7885 int fn_idx = vm_function_count++;
7986 memcpy(vm_functions[fn_idx].name, final_name, (size_t)final_len);
8087 vm_functions[fn_idx].name[final_len] = '\0';
88+ vm_functions[fn_idx].defined = 1;
8189 return fn_idx;
8290 }
8391
92+// Reserves `name` (under the current import namespace) with a known arity, not
93+// yet defined, so calls that appear before the `forge` still compile.
94+void predeclare_function_in_context(const char *name, int len, int arity) {
95+ char buf[128];
96+ const char *final_name = name;
97+ int final_len = len;
98+ if (g_import_ns_len > 0) {
99+ int combined_len = build_namespaced_name(buf, sizeof(buf), name, len);
100+ if (combined_len < 0) return; // the real declaration reports it
101+ final_name = buf;
102+ final_len = combined_len;
103+ }
104+ if (find_function(final_name, final_len) != -1) return;
105+ if (vm_function_count >= MAX_VM_FUNCS || final_len >= (int)sizeof(vm_functions[0].name)) return;
106+ int fn_idx = vm_function_count++;
107+ memcpy(vm_functions[fn_idx].name, final_name, (size_t)final_len);
108+ vm_functions[fn_idx].name[final_len] = '\0';
109+ vm_functions[fn_idx].arity = arity;
110+ vm_functions[fn_idx].defined = 0;
111+}
112+
84113 int find_function_in_context(const char *name, int len) {
85114 if (g_import_ns_len == 0) return find_function(name, len);
86115 char buf[128];

ashvm/src/compiler/C/stmt.cmodified

+38−7
@@ -38,7 +38,19 @@ static void local_statement(void) {
3838 // inside one of that file's own functions" except this check -- it
3939 // only makes sense at block_depth == 0, same as the sibling guard in
4040 // statement() above (which already gets this right).
41- if (block_depth == 0 && try_declare_import_constant(name, len)) return;
41+ if (block_depth == 0) {
42+ const char *ns; int ns_len;
43+ get_import_namespace(&ns, &ns_len);
44+ if (ns_len > 0) {
45+ // A top-level `local` of an imported file is a module-level variable.
46+ int state_slot = module_state_declare(name, len);
47+ expression();
48+ expect(TOKEN_SEMICOLON, "expected ';' after statement");
49+ emit2(OP_SET_GLOBAL, (uint8_t)state_slot);
50+ emit_op(OP_POP);
51+ return;
52+ }
53+ }
4254
4355 int slot = resolve_local(name, len);
4456
@@ -96,9 +108,13 @@ static void local_statement(void) {
96108 }
97109 }
98110
111+// A variable a statement can read and write is either a local slot (>= 0) or a
112+// module-level variable of the current imported file, encoded as -2 - index.
99113 static int resolve_local_or_error(Token id) {
100114 int slot = resolve_local(id.start, id.length);
101115 if (slot == -1) {
116+ int state = module_state_resolve(id.start, id.length);
117+ if (state != -1) return -2 - state;
102118 char msg[128];
103119 snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
104120 diagnostics_report("error", msg, id.start, id.length);
@@ -113,9 +129,18 @@ static int resolve_local_or_error(Token id) {
113129 // scope limit as `-`/`*`/`/` themselves. Always the general (GET_LOCAL,
114130 // expression(), op, SET_LOCAL) path, not the OP_ACC_LOCAL fusion -- the
115131 // RHS here is a full expression, not just a number-or-local operand.
132+static void emit_ref_get(int ref) {
133+ if (ref >= 0) emit2(OP_GET_LOCAL, (uint8_t)ref);
134+ else emit2(OP_GET_GLOBAL, (uint8_t)(-2 - ref));
135+}
136+static void emit_ref_set(int ref) {
137+ if (ref >= 0) emit2(OP_SET_LOCAL, (uint8_t)ref);
138+ else emit2(OP_SET_GLOBAL, (uint8_t)(-2 - ref));
139+}
140+
116141 static void compound_assignment_statement(int slot, TokenType op) {
117142 advance_token(); // consume the += / -= / *= / /=
118- emit2(OP_GET_LOCAL, (uint8_t)slot);
143+ emit_ref_get(slot);
119144 expression();
120145 expect(TOKEN_SEMICOLON, "expected ';' after assignment");
121146 switch (op) {
@@ -124,7 +149,7 @@ static void compound_assignment_statement(int slot, TokenType op) {
124149 case TOKEN_STAR_EQUAL: emit_op(OP_MUL); break;
125150 default: emit_op(OP_DIV); break; // TOKEN_SLASH_EQUAL
126151 }
127- emit2(OP_SET_LOCAL, (uint8_t)slot);
152+ emit_ref_set(slot);
128153 emit_op(OP_POP);
129154 }
130155
@@ -134,10 +159,10 @@ static void compound_assignment_statement(int slot, TokenType op) {
134159 static void incdec_statement(int slot, TokenType op) {
135160 advance_token(); // consume ++ or --
136161 expect(TOKEN_SEMICOLON, "expected ';' after statement");
137- emit2(OP_GET_LOCAL, (uint8_t)slot);
162+ emit_ref_get(slot);
138163 emit_constant(vm_num(1));
139164 emit_op(op == TOKEN_PLUS_PLUS ? OP_ADD : OP_SUB);
140- emit2(OP_SET_LOCAL, (uint8_t)slot);
165+ emit_ref_set(slot);
141166 emit_op(OP_POP);
142167 }
143168
@@ -175,7 +200,7 @@ static void identifier_statement(void) {
175200 }
176201 if (current.type == TOKEN_LBRACKET) {
177202 int slot = resolve_local_or_error(id);
178- emit2(OP_GET_LOCAL, (uint8_t)slot);
203+ emit_ref_get(slot);
179204 advance_token();
180205 expression();
181206 expect(TOKEN_RBRACKET, "expected ']'");
@@ -210,7 +235,7 @@ static void identifier_statement(void) {
210235 advance_token();
211236 expression();
212237 expect(TOKEN_SEMICOLON, "expected ';' after assignment");
213- emit2(OP_SET_LOCAL, (uint8_t)slot);
238+ emit_ref_set(slot);
214239 emit_op(OP_POP);
215240 return;
216241 }
@@ -226,6 +251,7 @@ static void stop_statement(void) {
226251 Token keyword = current;
227252 advance_token();
228253 expect(TOKEN_SEMICOLON, "expected ';' after 'stop'");
254+ emit_attempt_unwind_to_loop();
229255 loop_record_stop(keyword, emit_jump(OP_JUMP));
230256 }
231257
@@ -237,6 +263,7 @@ static void next_statement(void) {
237263 Token keyword = current;
238264 advance_token();
239265 expect(TOKEN_SEMICOLON, "expected ';' after 'next'");
266+ emit_attempt_unwind_to_loop();
240267 loop_record_next(keyword, emit_jump(OP_JUMP));
241268 }
242269
@@ -276,6 +303,7 @@ void statement(void) {
276303 if (current.type != TOKEN_SEMICOLON) expression();
277304 else emit_constant(vm_num(0));
278305 expect(TOKEN_SEMICOLON, "expected ';' after yield");
306+ emit_attempt_unwind_all();
279307 emit_op(OP_RETURN);
280308 return;
281309 }
@@ -300,10 +328,12 @@ Chunk *compile(const char *source, const char *source_path) {
300328 local_count = 0;
301329 chunk_init(&main_chunk);
302330 chunk = &main_chunk;
331+ prescan_functions(source);
303332 lexer_init(source);
304333 advance_token();
305334 imports_init(source_path);
306335 while (current.type != TOKEN_EOF) statement();
336+ check_all_functions_defined();
307337 return &main_chunk;
308338 }
309339
@@ -312,6 +342,7 @@ Chunk *compile(const char *source, const char *source_path) {
312342 Chunk *compile_repl_line(const char *source, Chunk *target_chunk, int *out_start_offset) {
313343 chunk = target_chunk;
314344 *out_start_offset = chunk->count;
345+ prescan_functions(source);
315346 lexer_init(source);
316347 advance_token();
317348 while (current.type != TOKEN_EOF) statement();

ashvm/src/compiler/C/stmt_control.cmodified

+6−4
@@ -41,9 +41,9 @@ void compile_forge_decl(void) {
4141 char outer_locals[MAX_VM_LOCALS][64];
4242 int outer_local_count = local_count;
4343 memcpy(outer_locals, local_names, sizeof(local_names));
44- int outer_loop_depth;
45- loop_depth_save(&outer_loop_depth);
46- loop_depth_reset();
44+ LoopScope outer_loop_scope;
45+ loop_scope_save(&outer_loop_scope);
46+ loop_scope_reset();
4747
4848 chunk = &vm_functions[fn_idx].chunk;
4949 local_count = 0;
@@ -56,7 +56,7 @@ void compile_forge_decl(void) {
5656 chunk = outer_chunk;
5757 local_count = outer_local_count;
5858 memcpy(local_names, outer_locals, sizeof(local_names));
59- loop_depth_restore(outer_loop_depth);
59+ loop_scope_restore(outer_loop_scope);
6060 }
6161
6262 // Conditions may be parenthesized or not (`given (x < n) {` and
@@ -123,7 +123,9 @@ void compile_attempt_stmt(void) {
123123
124124 int attempt_jump = emit_jump(OP_TRY_PUSH);
125125
126+ attempt_enter();
126127 block();
128+ attempt_leave();
127129
128130 emit_op(OP_TRY_POP);
129131 int skip_handle = emit_jump(OP_JUMP);

ashvm/src/compiler/C/stmt_import.cmodified

+15−0
@@ -130,6 +130,19 @@ void compile_import_stmt(void) {
130130 fail(message);
131131 }
132132
133+ // Optional `as name`: the namespace this file's functions are called through,
134+ // instead of the one derived from its file name.
135+ char alias[64];
136+ int has_alias = 0;
137+ if (current.type == TOKEN_IDENTIFIER && current.length == 2 && strncmp(current.start, "as", 2) == 0) {
138+ advance_token();
139+ expect(TOKEN_IDENTIFIER, "expected a namespace name after 'as'");
140+ if (previous.length >= (int)sizeof(alias)) fail("namespace name too long");
141+ memcpy(alias, previous.start, (size_t)previous.length);
142+ alias[previous.length] = '\0';
143+ has_alias = 1;
144+ }
145+
133146 expect(TOKEN_SEMICOLON, "expected ';' after import path");
134147
135148 // Cycle check MUST come before the dedup check: a file already in
@@ -147,6 +160,7 @@ void compile_import_stmt(void) {
147160
148161 char ns[64];
149162 import_derive_namespace(canonical, ns, sizeof(ns));
163+ if (has_alias) { memcpy(ns, alias, strlen(alias) + 1); }
150164
151165 ImportedFile *collision = registry_find_by_namespace(ns);
152166 if (collision != NULL) {
@@ -199,6 +213,7 @@ void compile_import_stmt(void) {
199213 import_path_dirname(canonical, current_file_dir, sizeof(current_file_dir));
200214 set_import_namespace(entry->namespace_name, (int)strlen(entry->namespace_name));
201215
216+ prescan_functions(imported_source);
202217 lexer_init(imported_source);
203218 advance_token();
204219 parse_imported_file_body();

ashvm/src/compiler/D/calls.dmodified

+3−2
@@ -1,15 +1,16 @@
11 src/compiler/O/calls.o: src/compiler/C/calls.c src/compiler/H/internal.h \
22 src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \
33 src/value/H/value.h src/lexer/H/lexer.h src/compiler/H/constants.h \
4- src/builtins/H/builtins.h src/diagnostics/H/diagnostics.h \
5- src/compiler/H/loop_stack.h
4+ src/compiler/H/module_state.h src/builtins/H/builtins.h \
5+ src/diagnostics/H/diagnostics.h src/compiler/H/loop_stack.h
66 src/compiler/H/internal.h:
77 src/compiler/H/state.h:
88 src/compiler/H/compiler.h:
99 src/vm/H/chunk.h:
1010 src/value/H/value.h:
1111 src/lexer/H/lexer.h:
1212 src/compiler/H/constants.h:
13+src/compiler/H/module_state.h:
1314 src/builtins/H/builtins.h:
1415 src/diagnostics/H/diagnostics.h:
1516 src/compiler/H/loop_stack.h:

ashvm/src/compiler/D/constants.dmodified

+3−1
@@ -1,12 +1,14 @@
11 src/compiler/O/constants.o: src/compiler/C/constants.c \
22 src/compiler/H/constants.h src/value/H/value.h src/compiler/H/internal.h \
33 src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \
4- src/lexer/H/lexer.h src/diagnostics/H/diagnostics.h
4+ src/lexer/H/lexer.h src/compiler/H/module_state.h \
5+ src/diagnostics/H/diagnostics.h
56 src/compiler/H/constants.h:
67 src/value/H/value.h:
78 src/compiler/H/internal.h:
89 src/compiler/H/state.h:
910 src/compiler/H/compiler.h:
1011 src/vm/H/chunk.h:
1112 src/lexer/H/lexer.h:
13+src/compiler/H/module_state.h:
1214 src/diagnostics/H/diagnostics.h:

ashvm/src/compiler/D/expr.dmodified

+2−1
@@ -1,12 +1,13 @@
11 src/compiler/O/expr.o: src/compiler/C/expr.c src/compiler/H/internal.h \
22 src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \
33 src/value/H/value.h src/lexer/H/lexer.h src/compiler/H/constants.h \
4- src/diagnostics/H/diagnostics.h
4+ src/compiler/H/module_state.h src/diagnostics/H/diagnostics.h
55 src/compiler/H/internal.h:
66 src/compiler/H/state.h:
77 src/compiler/H/compiler.h:
88 src/vm/H/chunk.h:
99 src/value/H/value.h:
1010 src/lexer/H/lexer.h:
1111 src/compiler/H/constants.h:
12+src/compiler/H/module_state.h:
1213 src/diagnostics/H/diagnostics.h:

ashvm/src/compiler/D/module_state.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/compiler/O/module_state.o: src/compiler/C/module_state.c \
2+ src/compiler/H/module_state.h src/compiler/H/internal.h \
3+ src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \
4+ src/value/H/value.h src/lexer/H/lexer.h src/compiler/H/constants.h \
5+ src/vm/H/state.h src/diagnostics/H/diagnostics.h
6+src/compiler/H/module_state.h:
7+src/compiler/H/internal.h:
8+src/compiler/H/state.h:
9+src/compiler/H/compiler.h:
10+src/vm/H/chunk.h:
11+src/value/H/value.h:
12+src/lexer/H/lexer.h:
13+src/compiler/H/constants.h:
14+src/vm/H/state.h:
15+src/diagnostics/H/diagnostics.h:

ashvm/src/compiler/D/prescan.dadded

+14−0
@@ -0,0 +1,14 @@
1+src/compiler/O/prescan.o: src/compiler/C/prescan.c \
2+ src/compiler/H/internal.h src/compiler/H/state.h \
3+ src/compiler/H/compiler.h src/vm/H/chunk.h src/value/H/value.h \
4+ src/lexer/H/lexer.h src/compiler/H/constants.h \
5+ src/compiler/H/module_state.h src/diagnostics/H/diagnostics.h
6+src/compiler/H/internal.h:
7+src/compiler/H/state.h:
8+src/compiler/H/compiler.h:
9+src/vm/H/chunk.h:
10+src/value/H/value.h:
11+src/lexer/H/lexer.h:
12+src/compiler/H/constants.h:
13+src/compiler/H/module_state.h:
14+src/diagnostics/H/diagnostics.h:

ashvm/src/compiler/D/stmt.dmodified

+3−1
@@ -1,13 +1,15 @@
11 src/compiler/O/stmt.o: src/compiler/C/stmt.c src/compiler/H/internal.h \
22 src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \
33 src/value/H/value.h src/lexer/H/lexer.h src/compiler/H/constants.h \
4- src/diagnostics/H/diagnostics.h src/compiler/H/loop_stack.h
4+ src/compiler/H/module_state.h src/diagnostics/H/diagnostics.h \
5+ src/compiler/H/loop_stack.h
56 src/compiler/H/internal.h:
67 src/compiler/H/state.h:
78 src/compiler/H/compiler.h:
89 src/vm/H/chunk.h:
910 src/value/H/value.h:
1011 src/lexer/H/lexer.h:
1112 src/compiler/H/constants.h:
13+src/compiler/H/module_state.h:
1214 src/diagnostics/H/diagnostics.h:
1315 src/compiler/H/loop_stack.h:

ashvm/src/compiler/D/stmt_control.dmodified

+3−2
@@ -2,15 +2,16 @@ src/compiler/O/stmt_control.o: src/compiler/C/stmt_control.c \
22 src/compiler/H/internal.h src/compiler/H/state.h \
33 src/compiler/H/compiler.h src/vm/H/chunk.h src/value/H/value.h \
44 src/lexer/H/lexer.h src/compiler/H/constants.h \
5- src/diagnostics/H/diagnostics.h src/builtins/H/builtins.h \
6- src/compiler/H/loop_stack.h
5+ src/compiler/H/module_state.h src/diagnostics/H/diagnostics.h \
6+ src/builtins/H/builtins.h src/compiler/H/loop_stack.h
77 src/compiler/H/internal.h:
88 src/compiler/H/state.h:
99 src/compiler/H/compiler.h:
1010 src/vm/H/chunk.h:
1111 src/value/H/value.h:
1212 src/lexer/H/lexer.h:
1313 src/compiler/H/constants.h:
14+src/compiler/H/module_state.h:
1415 src/diagnostics/H/diagnostics.h:
1516 src/builtins/H/builtins.h:
1617 src/compiler/H/loop_stack.h:

ashvm/src/compiler/D/stmt_import.dmodified

+3−1
@@ -2,13 +2,15 @@ src/compiler/O/stmt_import.o: src/compiler/C/stmt_import.c \
22 src/compiler/H/internal.h src/compiler/H/state.h \
33 src/compiler/H/compiler.h src/vm/H/chunk.h src/value/H/value.h \
44 src/lexer/H/lexer.h src/compiler/H/constants.h \
5- src/compiler/H/import_paths.h src/diagnostics/H/diagnostics.h
5+ src/compiler/H/module_state.h src/compiler/H/import_paths.h \
6+ src/diagnostics/H/diagnostics.h
67 src/compiler/H/internal.h:
78 src/compiler/H/state.h:
89 src/compiler/H/compiler.h:
910 src/vm/H/chunk.h:
1011 src/value/H/value.h:
1112 src/lexer/H/lexer.h:
1213 src/compiler/H/constants.h:
14+src/compiler/H/module_state.h:
1315 src/compiler/H/import_paths.h:
1416 src/diagnostics/H/diagnostics.h:

ashvm/src/compiler/H/compiler.hmodified

+2−1
@@ -2,12 +2,13 @@
22 #define ASH_VM_COMPILER_H
33 #include "vm/H/chunk.h"
44
5-#define MAX_VM_FUNCS 64
5+#define MAX_VM_FUNCS 1024
66 #define MAX_VM_LOCALS 128
77
88 typedef struct {
99 char name[64];
1010 int arity;
11+ int defined; // 0 while only forward-declared by the prescan (see compiler/C/prescan.c)
1112 Chunk chunk;
1213 } VMFunction;
1314

ashvm/src/compiler/H/internal.hmodified

+8−0
@@ -2,15 +2,23 @@
22 #define ASH_VM_COMPILER_INTERNAL_H
33 #include "compiler/H/state.h"
44 #include "compiler/H/constants.h"
5+#include "compiler/H/module_state.h"
56
67 // Cross-file forward declarations between compiler_expr.c and
78 // compiler_stmt.c -- not part of the public API (compiler.h).
89
910 // compiler_expr.c
1011 void expression(void);
1112
13+// compiler_prescan.c -- forward declarations: finds every `forge name(...)` in a
14+// source text up front so calls can appear before the definition, and checks at
15+// the end that every reserved name did get defined.
16+void prescan_functions(const char *source);
17+void check_all_functions_defined(void);
18+
1219 // compiler_calls.c
1320 void emit_call(Token id);
21+int emit_builtin_value(Token id); // builtin name used as a value -> wrapper lambda; 0 if not a builtin
1422 void emit_call_named(const char *name, int len);
1523 void lambda_literal(void);
1624 // Tries to compile `identifier CMP (number|identifier)` directly into a

ashvm/src/compiler/H/loop_stack.hmodified

+15−3
@@ -43,8 +43,20 @@ void loop_pop_and_patch_stops(void);
4343 // reach past it into an ENCLOSING loop it happens to be textually written
4444 // inside -- jumps can't cross Chunks anyway, so this is what turns that
4545 // into a clean compile error instead of nonsense bytecode.
46-void loop_depth_save(int *out);
47-void loop_depth_reset(void);
48-void loop_depth_restore(int saved);
46+typedef struct { int loops; int attempts; } LoopScope;
47+void loop_scope_save(LoopScope *out);
48+void loop_scope_reset(void);
49+void loop_scope_restore(LoopScope saved);
50+
51+// An attempt block pushes a runtime handler (OP_TRY_PUSH) that only its own
52+// normal fallthrough pops, so any other way out of the block -- yield, stop,
53+// next -- must pop it explicitly or the handler is left active and a later
54+// raise jumps into it. attempt_enter/leave bracket the attempt block (not the
55+// handle block, whose handler is already gone); the emit_* calls put one
56+// OP_TRY_POP per open handler right before the exit jump/return.
57+void attempt_enter(void);
58+void attempt_leave(void);
59+void emit_attempt_unwind_all(void); // yield: leave every open attempt
60+void emit_attempt_unwind_to_loop(void); // stop/next: leave those inside the loop
4961
5062 #endif

ashvm/src/compiler/H/module_state.hadded

+19−0
@@ -0,0 +1,19 @@
1+#ifndef ASH_VM_COMPILER_MODULE_STATE_H
2+#define ASH_VM_COMPILER_MODULE_STATE_H
3+
4+// Module-level state: every top-level `local NAME = <expression>;` of an
5+// imported file. The expression runs once, where the `@import` appears (so
6+// before the importing file's own code), and the value lives in a numbered
7+// global slot instead of a frame, so every function of that file -- and
8+// `ns.NAME` from the importer -- can read it, and `NAME = ...;`, `NAME += ...;`,
9+// `NAME[i] = ...;` update it. Names are qualified with the file's namespace
10+// ("ns.NAME"), like imported functions.
11+
12+// Reserves a slot for NAME in the current import namespace; returns its index.
13+int module_state_declare(const char *name, int len);
14+// A bare NAME inside an imported file (resolved in that file's namespace), or a
15+// fully qualified "ns.NAME". Return the slot index or -1.
16+int module_state_resolve(const char *name, int len);
17+int module_state_resolve_qualified(const char *name, int len);
18+
19+#endif

ashvm/src/compiler/H/state.hmodified

+1−0
@@ -40,6 +40,7 @@ void get_import_namespace(const char **out_ns, int *out_len);
4040 // imports.c-equivalent code and compile_forge_decl share it). Exits via
4141 // diagnostics_report + exit(1) on table-full or name-too-long, matching
4242 // every other error path in this file.
43+void predeclare_function_in_context(const char *name, int len, int arity);
4344 int declare_function_in_context(const char *name, int len);
4445
4546 // While a namespace is set: resolves ONLY "ns.NAME" (no bare fallback).

ashvm/src/diagnostics/C/diagnostics.cmodified

+1−1
@@ -67,7 +67,7 @@ static const char *lookup_hint(const char *message) {
6767 if (has_prefix(message, "index out of bounds")) return "check the array's length before indexing";
6868 if (has_prefix(message, "key not found")) return "use `has(map, key)` to check before accessing";
6969 if (has_prefix(message, "could not open file")) return "check the file path is correct";
70- if (has_prefix(message, "type error")) return "check the value's type before using it this way";
70+ if (has_prefix(message, "invalid operand type")) return "check the value's type before using it this way";
7171 if (has_prefix(message, "could not resolve")) return "check the import path is correct and the file exists";
7272 if (has_prefix(message, "circular import")) return "break the cycle by removing one of the imports in the chain shown above";
7373 if (has_prefix(message, "namespace")) return "give one of the colliding files/libraries a different name, or move one out of the way";

ashvm/src/lexer/C/lexer.cmodified

+8−0
@@ -76,6 +76,14 @@ static Token number(void) {
7676 advance(); // consume '.'
7777 while (isdigit(peek())) advance();
7878 }
79+ if (peek() == 'e' || peek() == 'E') {
80+ char after = peek_next();
81+ if (isdigit(after) || after == '+' || after == '-') {
82+ advance(); // consume 'e'/'E'
83+ if (peek() == '+' || peek() == '-') advance();
84+ while (isdigit(peek())) advance();
85+ }
86+ }
7987 return make_token(TOKEN_NUMBER);
8088 }
8189

ashvm/src/value/C/hashmap.cmodified

+72−40
@@ -2,77 +2,109 @@
22 #include <string.h>
33 #include "value/H/hashmap.h"
44
5+#define INDEX_EMPTY (-1)
6+#define INDEX_DELETED (-2)
7+
58 static unsigned long hash_string(const char *s) {
69 unsigned long h = 5381;
710 int c;
811 while ((c = *s++)) h = ((h << 5) + h) + (unsigned long)c;
912 return h;
1013 }
1114
15+static int *new_index(int capacity) {
16+ int *index = malloc(sizeof(int) * (size_t)capacity);
17+ for (int i = 0; i < capacity; i++) index[i] = INDEX_EMPTY;
18+ return index;
19+}
20+
1221 VMMap *vm_map_new(void) {
1322 VMMap *m = malloc(sizeof(VMMap));
1423 m->capacity = 8;
24+ m->size = 0;
1525 m->count = 0;
16- m->entries = calloc(m->capacity, sizeof(VMMapEntry));
26+ m->entries = calloc((size_t)m->capacity, sizeof(VMMapEntry));
27+ m->index_capacity = 16;
28+ m->index = new_index(m->index_capacity);
1729 return m;
1830 }
1931
20-static void vm_map_resize(VMMap *m) {
21- int old_capacity = m->capacity;
22- VMMapEntry *old_entries = m->entries;
23- m->capacity *= 2;
24- m->entries = calloc(m->capacity, sizeof(VMMapEntry));
25- m->count = 0;
26- for (int i = 0; i < old_capacity; i++) {
27- if (old_entries[i].used) vm_map_set(m, old_entries[i].key, old_entries[i].value);
32+// Position in `entries` of `key`, or -1.
33+static int find_entry(VMMap *m, const char *key) {
34+ unsigned long h = hash_string(key) % (unsigned long)m->index_capacity;
35+ for (int probes = 0; probes < m->index_capacity; probes++) {
36+ int pos = m->index[h];
37+ if (pos == INDEX_EMPTY) return -1;
38+ if (pos >= 0 && strcmp(m->entries[pos].key, key) == 0) return pos;
39+ h = (h + 1) % (unsigned long)m->index_capacity;
2840 }
29- free(old_entries);
41+ return -1;
42+}
43+
44+static void index_insert(VMMap *m, int pos) {
45+ unsigned long h = hash_string(m->entries[pos].key) % (unsigned long)m->index_capacity;
46+ while (m->index[h] >= 0) h = (h + 1) % (unsigned long)m->index_capacity;
47+ m->index[h] = pos;
48+}
49+
50+// Called when `entries` is full: drop dead slots (keeping order), grow if the
51+// live entries still fill more than half, and rebuild the index.
52+static void compact_and_grow(VMMap *m) {
53+ int live = 0;
54+ for (int i = 0; i < m->size; i++) {
55+ if (m->entries[i].used) m->entries[live++] = m->entries[i];
56+ }
57+ m->size = live;
58+ if (live * 2 >= m->capacity) {
59+ m->capacity *= 2;
60+ m->entries = realloc(m->entries, sizeof(VMMapEntry) * (size_t)m->capacity);
61+ }
62+ for (int i = m->size; i < m->capacity; i++) m->entries[i].used = 0;
63+ free(m->index);
64+ m->index_capacity = m->capacity * 2;
65+ m->index = new_index(m->index_capacity);
66+ for (int i = 0; i < m->size; i++) index_insert(m, i);
3067 }
3168
3269 void vm_map_set(VMMap *m, char *key, VMValue value) {
33- if ((double)(m->count + 1) / m->capacity > 0.7) vm_map_resize(m);
34- unsigned long h = hash_string(key) % (unsigned long)m->capacity;
35- while (m->entries[h].used) {
36- if (strcmp(m->entries[h].key, key) == 0) {
37- m->entries[h].value = value;
38- free(key);
39- return;
40- }
41- h = (h + 1) % (unsigned long)m->capacity;
70+ int pos = find_entry(m, key);
71+ if (pos >= 0) {
72+ m->entries[pos].value = value;
73+ free(key);
74+ return;
4275 }
43- m->entries[h].key = key;
44- m->entries[h].value = value;
45- m->entries[h].used = 1;
76+ if (m->size >= m->capacity) compact_and_grow(m);
77+ pos = m->size++;
78+ m->entries[pos].key = key;
79+ m->entries[pos].value = value;
80+ m->entries[pos].used = 1;
4681 m->count++;
82+ index_insert(m, pos);
4783 }
4884
4985 int vm_map_get(VMMap *m, const char *key, VMValue *out) {
50- unsigned long h = hash_string(key) % (unsigned long)m->capacity;
51- unsigned long start = h;
52- while (m->entries[h].used) {
53- if (strcmp(m->entries[h].key, key) == 0) { *out = m->entries[h].value; return 1; }
54- h = (h + 1) % (unsigned long)m->capacity;
55- if (h == start) break;
56- }
57- return 0;
86+ int pos = find_entry(m, key);
87+ if (pos < 0) return 0;
88+ *out = m->entries[pos].value;
89+ return 1;
5890 }
5991
6092 int vm_map_has(VMMap *m, const char *key) {
61- VMValue tmp;
62- return vm_map_get(m, key, &tmp);
93+ return find_entry(m, key) >= 0;
6394 }
6495
6596 void vm_map_delete(VMMap *m, const char *key) {
66- unsigned long h = hash_string(key) % (unsigned long)m->capacity;
67- unsigned long start = h;
68- while (m->entries[h].used) {
69- if (strcmp(m->entries[h].key, key) == 0) {
70- free(m->entries[h].key);
71- m->entries[h].used = 0;
97+ unsigned long h = hash_string(key) % (unsigned long)m->index_capacity;
98+ for (int probes = 0; probes < m->index_capacity; probes++) {
99+ int pos = m->index[h];
100+ if (pos == INDEX_EMPTY) return;
101+ if (pos >= 0 && strcmp(m->entries[pos].key, key) == 0) {
102+ free(m->entries[pos].key);
103+ m->entries[pos].used = 0;
104+ m->index[h] = INDEX_DELETED;
72105 m->count--;
73106 return;
74107 }
75- h = (h + 1) % (unsigned long)m->capacity;
76- if (h == start) break;
108+ h = (h + 1) % (unsigned long)m->index_capacity;
77109 }
78110 }

ashvm/src/value/C/value.cmodified

+67−10
@@ -1,3 +1,4 @@
1+#include <math.h>
12 #include <stdio.h>
23 #include <stdlib.h>
34 #include <string.h>
@@ -7,11 +8,33 @@
78 VMValue vm_num(double n) {
89 VMValue v; v.type = VM_NUM; v.number = n; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
910 }
11+VMValue vm_bool(int b) {
12+ VMValue v; v.type = VM_BOOL; v.number = b ? 1 : 0; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
13+}
14+int vm_values_equal(VMValue a, VMValue b) {
15+ if (a.type != b.type) return 0;
16+ switch (a.type) {
17+ case VM_STR: return strcmp(a.str, b.str) == 0;
18+ case VM_NUM: case VM_BOOL: case VM_FUNCTION: return a.number == b.number;
19+ case VM_NIL: return 1;
20+ case VM_ARRAY: return a.array == b.array;
21+ case VM_MAP: return a.map == b.map;
22+ case VM_CLOSURE: return a.closure == b.closure;
23+ }
24+ return 0;
25+}
1026 VMValue vm_nil(void) {
1127 VMValue v; v.type = VM_NIL; v.number = 0; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
1228 }
29+// A string value caches its byte length in `number`, so len(), substring() and
30+// s[i] are O(1) instead of an strlen each time. vm_str measures once;
31+// vm_str_n is for callers that already know the length.
32+VMValue vm_str_n(char *s, size_t len) {
33+ VMValue v; v.type = VM_STR; v.number = (double)len; v.str = s;
34+ v.array = NULL; v.map = NULL; v.closure = NULL; return v;
35+}
1336 VMValue vm_str(char *s) {
14- VMValue v; v.type = VM_STR; v.number = 0; v.str = s; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
37+ VMValue v; v.type = VM_STR; v.number = (double)strlen(s); v.str = s; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
1538 }
1639 VMValue vm_array_val(VMArray *a) {
1740 VMValue v; v.type = VM_ARRAY; v.number = 0; v.str = NULL; v.array = a; v.map = NULL; v.closure = NULL; return v;
@@ -51,14 +74,13 @@ char *vm_copy_string_escaped(const char *start, int length) {
5174 return buf;
5275 }
5376
54-char *vm_concat_strings(const char *a, const char *b) {
55- int la = (int)strlen(a);
56- int lb = (int)strlen(b);
77+VMValue vm_concat_strings(VMValue a, VMValue b) {
78+ size_t la = (size_t)a.number, lb = (size_t)b.number;
5779 char *buf = malloc(la + lb + 1);
58- memcpy(buf, a, la);
59- memcpy(buf + la, b, lb);
80+ memcpy(buf, a.str, la);
81+ memcpy(buf + la, b.str, lb);
6082 buf[la + lb] = '\0';
61- return buf;
83+ return vm_str_n(buf, la + lb);
6284 }
6385
6486 VMArray *vm_array_new(void) {
@@ -77,10 +99,45 @@ void vm_array_push(VMArray *arr, VMValue v) {
7799 arr->items[arr->count++] = v;
78100 }
79101
102+// Drops trailing zeros (and a bare '.') from the fraction in the mantissa
103+// ending at `end` (exclusive); returns the new end.
104+static char *trim_fraction(char *start, char *end) {
105+ if (!memchr(start, '.', (size_t)(end - start))) return end;
106+ while (end > start && end[-1] == '0') end--;
107+ if (end > start && end[-1] == '.') end--;
108+ return end;
109+}
110+
111+// Ash's one number-to-text format (kiln's number_format_emit matches it):
112+// nan/inf/-inf; plain decimal with at most 6 fraction digits (trailing zeros
113+// dropped) for 0 and 1e-6 <= |x| < 1e15; otherwise scientific with a mantissa
114+// in [1,10) ("1e+21", "1.5e-07").
115+void vm_format_number(double x, char *buf, size_t size) {
116+ if (isnan(x)) { snprintf(buf, size, "nan"); return; }
117+ if (isinf(x)) { snprintf(buf, size, x < 0 ? "-inf" : "inf"); return; }
118+ double a = fabs(x);
119+ if (a == 0 || (a >= 1e-6 && a < 1e15)) {
120+ snprintf(buf, size, "%.6f", x);
121+ char *end = trim_fraction(buf, buf + strlen(buf));
122+ *end = '\0';
123+ if (strcmp(buf, "-0") == 0) strcpy(buf, "0");
124+ return;
125+ }
126+ snprintf(buf, size, "%.6e", x);
127+ char *e = strchr(buf, 'e');
128+ char exponent[16];
129+ snprintf(exponent, sizeof(exponent), "%s", e);
130+ char *end = trim_fraction(buf, e);
131+ memmove(end, exponent, strlen(exponent) + 1);
132+}
133+
80134 void vm_print_value(VMValue v) {
81135 if (v.type == VM_NUM) {
82- if (v.number == (long long)v.number) printf("%lld", (long long)v.number);
83- else printf("%g", v.number);
136+ char buf[64];
137+ vm_format_number(v.number, buf, sizeof(buf));
138+ printf("%s", buf);
139+ } else if (v.type == VM_BOOL) {
140+ printf("%s", v.number != 0 ? "yes" : "no");
84141 } else if (v.type == VM_STR) {
85142 printf("%s", v.str);
86143 } else if (v.type == VM_NIL) {
@@ -101,7 +158,7 @@ void vm_print_value(VMValue v) {
101158 } else if (v.type == VM_MAP) {
102159 printf("{");
103160 int first = 1;
104- for (int i = 0; i < v.map->capacity; i++) {
161+ for (int i = 0; i < v.map->size; i++) {
105162 if (v.map->entries[i].used) {
106163 if (!first) printf(", ");
107164 printf("\"%s\": ", v.map->entries[i].key);

ashvm/src/value/H/hashmap.hmodified

+10−2
@@ -2,6 +2,11 @@
22 #define ASH_VM_HASHMAP_H
33 #include "value/H/value.h"
44
5+// Insertion-ordered map: `entries` is a dense list in the order keys were first
6+// set; `index` is an open-addressing table of positions into it, used only for
7+// lookup. Deleting leaves a dead slot (used == 0) so the order of the others
8+// is untouched; dead slots are squeezed out when the list fills up. Iterate
9+// `entries[0 .. size)` and skip entries with used == 0.
510 typedef struct {
611 char *key;
712 VMValue value;
@@ -10,8 +15,11 @@ typedef struct {
1015
1116 struct VMMap {
1217 VMMapEntry *entries;
13- int count;
14- int capacity;
18+ int size; // slots filled in `entries`, live or dead
19+ int count; // live entries
20+ int capacity; // allocated slots in `entries`
21+ int *index; // index_capacity slots: -1 empty, -2 deleted, else a position in entries
22+ int index_capacity;
1523 };
1624
1725 VMMap *vm_map_new(void);

ashvm/src/value/H/value.hmodified

+8−2
@@ -2,7 +2,7 @@
22 #define ASH_VM_VALUE_H
33 #include <stdint.h>
44
5-typedef enum { VM_NUM, VM_STR, VM_ARRAY, VM_MAP, VM_FUNCTION, VM_CLOSURE, VM_NIL } VMType;
5+typedef enum { VM_NUM, VM_STR, VM_ARRAY, VM_MAP, VM_FUNCTION, VM_CLOSURE, VM_NIL, VM_BOOL } VMType;
66
77 typedef struct VMArray {
88 struct VMValue *items;
@@ -28,15 +28,21 @@ typedef struct VMValue {
2828 } VMValue;
2929
3030 VMValue vm_num(double n);
31+
32+// Number -> text in Ash's single format (see value.c).
33+void vm_format_number(double x, char *buf, size_t size);
3134 VMValue vm_nil(void);
35+VMValue vm_bool(int b); // yes / no: number holds 1 / 0
36+int vm_values_equal(VMValue a, VMValue b); // == semantics: by value for strings, numbers, booleans, none; by identity otherwise
3237 VMValue vm_str(char *s);
38+VMValue vm_str_n(char *s, size_t len); // len = strlen(s), already known
3339 VMValue vm_array_val(VMArray *a);
3440 VMValue vm_map_val(VMMap *m);
3541 VMValue vm_function_val(int function_index);
3642 VMValue vm_closure_val(VMClosureObj *c);
3743
3844 char *vm_copy_string_escaped(const char *start, int length);
39-char *vm_concat_strings(const char *a, const char *b);
45+VMValue vm_concat_strings(VMValue a, VMValue b); // both VM_STR
4046
4147 VMArray *vm_array_new(void);
4248 void vm_array_push(VMArray *arr, VMValue v);

ashvm/src/vm/C/dispatch.cmodified

+44−45
@@ -16,6 +16,12 @@
1616 // that makes this VM fast in the first place. Everything that CAN live
1717 // outside it (error handling, the public call/run API, shared state) does,
1818 // in vm_errors.c / vm_api.c / vm_state.c.
19+// Ordering comparison: numbers numerically, strings bytewise (strcmp);
20+// anything else (or a mix) is the usual type error.
21+#define ORDER(a, b, op) \
22+ (((a).type == VM_STR && (b).type == VM_STR) ? strcmp((a).str, (b).str) op 0 \
23+ : require_num((a), "comparison") op require_num((b), "comparison"))
24+
1925 void vm_execute(int stop_at_frame_count) {
2026 CallFrame *volatile frame = &frames[frame_count - 1];
2127
@@ -34,7 +40,8 @@ void vm_execute(int stop_at_frame_count) {
3440 &&do_TRY_PUSH, &&do_TRY_POP,
3541 &&do_RAISE,
3642 &&do_BAND, &&do_BOR, &&do_BXOR, &&do_BNOT, &&do_SHL, &&do_SHR,
37- &&do_TRUNC_LOCALS
43+ &&do_TRUNC_LOCALS,
44+ &&do_GET_GLOBAL, &&do_SET_GLOBAL
3845 };
3946
4047 #define DISPATCH() \
@@ -52,7 +59,7 @@ void vm_execute(int stop_at_frame_count) {
5259 do_ADD: {
5360 VMValue b = *--stack_top;
5461 VMValue *a = stack_top - 1;
55- if (a->type == VM_STR && b.type == VM_STR) *a = vm_str(vm_concat_strings(a->str, b.str));
62+ if (a->type == VM_STR && b.type == VM_STR) *a = vm_concat_strings(*a, b);
5663 else *a = vm_num(require_num(*a, "'+'") + require_num(b, "'+'"));
5764 DISPATCH();
5865 }
@@ -61,33 +68,15 @@ void vm_execute(int stop_at_frame_count) {
6168 do_DIV: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "'/'") / require_num(b, "'/'")); DISPATCH(); }
6269 do_MOD: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(fmod(require_num(*a, "'%'"), require_num(b, "'%'"))); DISPATCH(); }
6370 do_NEG: { stack_top[-1] = vm_num(-require_num(stack_top[-1], "unary '-'")); DISPATCH(); }
64- do_NOT: { stack_top[-1] = vm_num(require_num(stack_top[-1], "'!'") == 0 ? 1 : 0); DISPATCH(); }
65- do_EQ: {
66- VMValue b = *--stack_top; VMValue *a = stack_top - 1;
67- int eq;
68- if (a->type == VM_STR && b.type == VM_STR) eq = strcmp(a->str, b.str) == 0;
69- else if (a->type == VM_NUM && b.type == VM_NUM) eq = a->number == b.number;
70- else if (a->type == VM_NIL && b.type == VM_NIL) eq = 1;
71- else eq = 0;
72- *a = vm_num(eq);
73- DISPATCH();
74- }
75- do_NEQ: {
76- VMValue b = *--stack_top; VMValue *a = stack_top - 1;
77- int eq;
78- if (a->type == VM_STR && b.type == VM_STR) eq = strcmp(a->str, b.str) == 0;
79- else if (a->type == VM_NUM && b.type == VM_NUM) eq = a->number == b.number;
80- else if (a->type == VM_NIL && b.type == VM_NIL) eq = 1;
81- else eq = 0;
82- *a = vm_num(!eq);
83- DISPATCH();
84- }
85- do_LT: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") < require_num(b, "comparison")); DISPATCH(); }
86- do_LE: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") <= require_num(b, "comparison")); DISPATCH(); }
87- do_GT: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") > require_num(b, "comparison")); DISPATCH(); }
88- do_GE: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") >= require_num(b, "comparison")); DISPATCH(); }
89- do_AND: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((truthy(*a, "'&&'") && truthy(b, "'&&'")) ? 1 : 0); DISPATCH(); }
90- do_OR: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((truthy(*a, "'||'") || truthy(b, "'||'")) ? 1 : 0); DISPATCH(); }
71+ do_NOT: { stack_top[-1] = vm_bool(!truthy(stack_top[-1], "'!'")); DISPATCH(); }
72+ do_EQ: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(vm_values_equal(*a, b)); DISPATCH(); }
73+ do_NEQ: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(!vm_values_equal(*a, b)); DISPATCH(); }
74+ do_LT: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(ORDER(*a, b, <)); DISPATCH(); }
75+ do_LE: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(ORDER(*a, b, <=)); DISPATCH(); }
76+ do_GT: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(ORDER(*a, b, >)); DISPATCH(); }
77+ do_GE: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(ORDER(*a, b, >=)); DISPATCH(); }
78+ do_AND: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(truthy(*a, "'&&'") && truthy(b, "'&&'")); DISPATCH(); }
79+ do_OR: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_bool(truthy(*a, "'||'") || truthy(b, "'||'")); DISPATCH(); }
9180 do_PRINT: { VMValue v = *--stack_top; vm_print_value(v); printf("\n"); DISPATCH(); }
9281 do_POP: { --stack_top; DISPATCH(); }
9382 do_GET_LOCAL: { uint8_t slot = *frame->ip++; *stack_top++ = frame->slots[slot]; DISPATCH(); }
@@ -165,7 +154,7 @@ void vm_execute(int stop_at_frame_count) {
165154 int op = flags & 0x03;
166155 VMValue *a = &frame->slots[slot_a];
167156 if (op == 0) {
168- if (a->type == VM_STR && b.type == VM_STR) *a = vm_str(vm_concat_strings(a->str, b.str));
157+ if (a->type == VM_STR && b.type == VM_STR) *a = vm_concat_strings(*a, b);
169158 else *a = vm_num(require_num(*a, "'+'") + require_num(b, "'+'"));
170159 } else if (op == 1) {
171160 *a = vm_num(require_num(*a, "'-'") - require_num(b, "'-'"));
@@ -185,20 +174,14 @@ void vm_execute(int stop_at_frame_count) {
185174 int cmp_code = flags & 0x0F;
186175 int taken;
187176 if (cmp_code == 4 || cmp_code == 5) {
188- int eq;
189- if (a.type == VM_STR && b.type == VM_STR) eq = strcmp(a.str, b.str) == 0;
190- else if (a.type == VM_NUM && b.type == VM_NUM) eq = a.number == b.number;
191- else if (a.type == VM_NIL && b.type == VM_NIL) eq = 1;
192- else eq = 0;
177+ int eq = vm_values_equal(a, b);
193178 taken = (cmp_code == 4) ? eq : !eq;
194179 } else {
195- double av = require_num(a, "comparison");
196- double bv = require_num(b, "comparison");
197180 switch (cmp_code) {
198- case 0: taken = av < bv; break;
199- case 1: taken = av <= bv; break;
200- case 2: taken = av > bv; break;
201- default: taken = av >= bv; break;
181+ case 0: taken = ORDER(a, b, <); break;
182+ case 1: taken = ORDER(a, b, <=); break;
183+ case 2: taken = ORDER(a, b, >); break;
184+ default: taken = ORDER(a, b, >=); break;
202185 }
203186 }
204187 if (!taken) frame->ip += off;
@@ -217,13 +200,20 @@ void vm_execute(int stop_at_frame_count) {
217200 VMValue arr = *--stack_top;
218201 if (arr.type == VM_ARRAY) {
219202 int64_t i = (int64_t)require_num(idx, "array index");
220- if (i < 0 || i >= arr.array->count) vm_runtime_error("index out of bounds: %lld", (long long)i);
203+ if (i < 0 || i >= arr.array->count) vm_runtime_error("index out of bounds");
221204 *stack_top++ = arr.array->items[i];
222205 } else if (arr.type == VM_MAP) {
223206 if (idx.type != VM_STR) vm_runtime_error("map keys must be strings");
224207 VMValue out;
225- if (!vm_map_get(arr.map, idx.str, &out)) vm_runtime_error("key not found: %s", idx.str);
208+ if (!vm_map_get(arr.map, idx.str, &out)) vm_runtime_error("key not found");
226209 *stack_top++ = out;
210+ } else if (arr.type == VM_STR && idx.type == VM_STR) {
211+ *stack_top++ = arr; // string[string]: an error value's `.message` is the string itself
212+ } else if (arr.type == VM_STR) {
213+ int64_t i = (int64_t)require_num(idx, "string index");
214+ if (i < 0 || i >= (int64_t)arr.number) vm_runtime_error("index out of bounds");
215+ char *one = malloc(2); one[0] = arr.str[i]; one[1] = '\0';
216+ *stack_top++ = vm_str(one);
227217 } else {
228218 type_error("indexing");
229219 }
@@ -235,12 +225,14 @@ void vm_execute(int stop_at_frame_count) {
235225 VMValue arr = *--stack_top;
236226 if (arr.type == VM_ARRAY) {
237227 int64_t i = (int64_t)require_num(idx, "array index");
238- if (i < 0 || i >= arr.array->count) vm_runtime_error("index out of bounds: %lld", (long long)i);
228+ if (i < 0 || i >= arr.array->count) vm_runtime_error("index out of bounds");
239229 arr.array->items[i] = value;
240230 } else if (arr.type == VM_MAP) {
241231 if (idx.type != VM_STR) vm_runtime_error("map keys must be strings");
242232 char *key_copy = malloc(strlen(idx.str) + 1); strcpy(key_copy, idx.str);
243233 vm_map_set(arr.map, key_copy, value);
234+ } else if (arr.type == VM_STR) {
235+ vm_runtime_error("cannot assign to a string index");
244236 } else {
245237 type_error("index-assign");
246238 }
@@ -283,7 +275,10 @@ void vm_execute(int stop_at_frame_count) {
283275 if (setjmp(h->jmp) == 0) {
284276 DISPATCH();
285277 } else {
286- try_depth--;
278+ // `h` is a plain local reused by every TRY_PUSH, so after a longjmp it
279+ // may point at a later (now stale) handler; the one that fired is
280+ // always the top of the handler stack.
281+ h = &try_handlers[--try_depth];
287282 frame_count = h->saved_frame_count;
288283 stack_top = h->saved_stack_top;
289284 frame = h->target_frame;
@@ -325,5 +320,9 @@ void vm_execute(int stop_at_frame_count) {
325320 // static per chunk, "what the stack should look like once this block
326321 // is done" is just "n slots past frame->slots" -- always correct
327322 // regardless of the path taken through the block this time.
323+ // Module-level variables (see compiler/C/module_state.c). SET_GLOBAL keeps the
324+ // value on the stack, like SET_LOCAL.
325+ do_GET_GLOBAL: { uint8_t idx = *frame->ip++; *stack_top++ = vm_globals[idx]; DISPATCH(); }
326+ do_SET_GLOBAL: { uint8_t idx = *frame->ip++; vm_globals[idx] = stack_top[-1]; DISPATCH(); }
328327 do_TRUNC_LOCALS: { uint8_t n = *frame->ip++; stack_top = frame->slots + n; DISPATCH(); }
329328 }

ashvm/src/vm/C/errors.cmodified

+10−9
@@ -1,6 +1,7 @@
11 #include <stdio.h>
22 #include <stdlib.h>
33 #include <stdarg.h>
4+#include <string.h>
45 #include "vm/H/vm.h"
56 #include "vm/H/state.h"
67 #include "diagnostics/H/diagnostics.h"
@@ -15,6 +16,8 @@ void vm_runtime_error(const char *fmt, ...) {
1516 va_start(args, fmt);
1617 vsnprintf(error_message, sizeof(error_message), fmt, args);
1718 va_end(args);
19+ size_t n = strlen(error_message); // messages are caught as values: no trailing newline
20+ while (n > 0 && error_message[n - 1] == '\n') error_message[--n] = '\0';
1821
1922 if (try_depth > 0) {
2023 longjmp(try_handlers[try_depth - 1].jmp, 1);
@@ -29,37 +32,35 @@ void vm_runtime_error(const char *fmt, ...) {
2932 }
3033 }
3134
32-// `raise expr;` -- expr must already be a VM_STR (checked here, not at
33-// compile time, since the raised value is a general expression that could
34-// be anything at runtime). Mirrors vm_runtime_error's three-way dispatch
35+// `raise expr;` -- any value (a string is what built-in errors raise; a map
36+// gives a structured error). Mirrors vm_runtime_error's three-way dispatch
3537 // (attempt handler / REPL recovery / hard exit) exactly, but hands back
3638 // the exact raised VMValue via `raised_value` instead of funneling it
3739 // through the fixed-size error_message buffer -- see state.h.
3840 void vm_raise_value(VMValue msg) {
39- if (msg.type != VM_STR) type_error("'raise'");
40-
4141 raised_value = msg;
42+ const char *text = msg.type == VM_STR ? msg.str : "uncaught non-string value";
4243 has_raised_value = 1;
4344
4445 if (try_depth > 0) {
4546 longjmp(try_handlers[try_depth - 1].jmp, 1);
4647 } else if (repl_recovery_jmp != NULL) {
47- diagnostics_report("error", msg.str, current_pos, current_poslen);
48+ diagnostics_report("error", text, current_pos, current_poslen);
4849 frame_count = repl_saved_frame_count;
4950 stack_top = repl_saved_stack_top;
5051 longjmp(*repl_recovery_jmp, 1);
5152 } else {
52- diagnostics_report("error", msg.str, current_pos, current_poslen);
53+ diagnostics_report("error", text, current_pos, current_poslen);
5354 exit(1);
5455 }
5556 }
5657
5758 void type_error(const char *context) {
58- vm_runtime_error("type error: invalid operand type in %s", context);
59+ vm_runtime_error("invalid operand type in %s", context);
5960 }
6061
6162 double require_num(VMValue v, const char *context) {
62- if (v.type != VM_NUM) type_error(context);
63+ if (v.type != VM_NUM && v.type != VM_BOOL) type_error(context);
6364 return v.number;
6465 }
6566

ashvm/src/vm/C/state.cmodified

+1−0
@@ -2,6 +2,7 @@
22 #include "vm/H/state.h"
33
44 VMValue stack[STACK_MAX];
5+VMValue vm_globals[MAX_VM_GLOBALS];
56 VMValue *stack_top;
67 CallFrame frames[FRAMES_MAX];
78 int frame_count;

ashvm/src/vm/H/chunk.hmodified

+2−1
@@ -25,7 +25,8 @@ typedef enum {
2525 OP_TRY_POP,
2626 OP_RAISE,
2727 OP_BAND, OP_BOR, OP_BXOR, OP_BNOT, OP_SHL, OP_SHR,
28- OP_TRUNC_LOCALS
28+ OP_TRUNC_LOCALS,
29+ OP_GET_GLOBAL, OP_SET_GLOBAL
2930 } OpCode;
3031
3132 typedef struct {

ashvm/src/vm/H/state.hmodified

+2−0
@@ -26,6 +26,8 @@ typedef struct {
2626 uint8_t *target_ip;
2727 } TryHandler;
2828
29+#define MAX_VM_GLOBALS 256
30+extern VMValue vm_globals[MAX_VM_GLOBALS]; // module-level variables of imported files
2931 extern VMValue stack[STACK_MAX];
3032 extern VMValue *stack_top;
3133 extern CallFrame frames[FRAMES_MAX];

ashvm/tests/cases/basics/booleans.ashadded

+35−0
@@ -0,0 +1,35 @@
1+say yes;
2+say no;
3+say 1 < 2;
4+say 2 < 1;
5+say not yes;
6+say not 0;
7+say 1 == 1;
8+say "a" == "a";
9+say "a" == "b";
10+say 1 and 1;
11+say 0 or 1;
12+say yes == 1;
13+say yes == yes;
14+say type(yes);
15+say type(1 < 2);
16+say [yes, no, 3 > 4];
17+say {"ok": yes};
18+say str(no) + str(yes);
19+say has({"a": 1}, "a");
20+say has({"a": 1}, "b");
21+say yes + 1;
22+say file_exists("/nonexistent");
23+say contains("abc", "b");
24+local flag = 5 > 3;
25+given flag { say "flag set"; }
26+given not flag { say "bad"; } otherwise { say "ok"; }
27+say flag and not no;
28+say none == none;
29+say not none;
30+attempt { given {"a": 1} { say "bad"; } } handle (e) { say "map condition"; }
31+attempt { given "x" { say "bad"; } } handle (e) { say "string condition"; }
32+local n = 0;
33+during n < 3 { n++; }
34+say n;
35+say yes ? "t" : "f";

ashvm/tests/cases/basics/exponent_literals.ashadded

+6−0
@@ -0,0 +1,6 @@
1+say 1e3;
2+say 1.5e-3;
3+say 2E2 + 1;
4+say num("-1.5e2");
5+say num("1e3") + 1;
6+say num("2.5E-1");

ashvm/tests/cases/basics/number_format.ashadded

+26−0
@@ -0,0 +1,26 @@
1+say 0;
2+say 3;
3+say -42;
4+say 0.5;
5+say -0.25;
6+say 3.14159265;
7+say 0.1 + 0.2;
8+say 123456789012;
9+say 999999999999999;
10+say 1e15;
11+say 1e21;
12+say -1e21;
13+say 1.5e-7;
14+say 1e-7;
15+say 0.000001;
16+say 0.0000005;
17+say 123456.789;
18+say 1e100;
19+say 1.2345678e30;
20+say 9.9999996;
21+say 0.9999999;
22+say 2.5e-300;
23+say 1 / 0;
24+say -1 / 0;
25+say str(1e21) + "|" + str(1.5) + "|" + str(-7);
26+say [1.5, 1e21, 2];

ashvm/tests/cases/collections/array_mutation.ashadded

+16−0
@@ -0,0 +1,16 @@
1+local a = [1, 2, 3, 4];
2+delete(a, 1);
3+say a;
4+say pop(a);
5+say a;
6+insert(a, 0, 9);
7+insert(a, len(a), 7);
8+insert(a, 1, 8);
9+say a;
10+say slice(a, 1, 3);
11+say slice(a, 0, 0);
12+attempt { pop([]); } handle (e) { say "empty"; }
13+attempt { delete(a, 10); } handle (e) { say "oob"; }
14+local m = {"k": 1};
15+delete(m, "k");
16+say len(m);

ashvm/tests/cases/collections/map_order.ashadded

+30−0
@@ -0,0 +1,30 @@
1+local m = {};
2+m["b"] = 1; m["zeta"] = 2; m["alpha"] = 3; m["a"] = 4; m["mid"] = 5;
3+say keys(m);
4+say values(m);
5+delete(m, "alpha");
6+say keys(m);
7+m["alpha"] = 9;
8+m["b"] = 100;
9+say keys(m);
10+say m;
11+delete(m, "b");
12+delete(m, "zzz");
13+say keys(m);
14+say len(m);
15+say has(m, "b");
16+say has(m, "mid");
17+local big = {};
18+local i = 0;
19+during i < 100 { big[str(i)] = i; i++; }
20+i = 0;
21+during i < 100 { given i % 2 == 0 { delete(big, str(i)); } i++; }
22+say len(big);
23+say big["51"];
24+say keys(big)[0];
25+say keys(big)[49];
26+i = 0;
27+during i < 100 { given i % 2 == 0 { big[str(i)] = i; } i++; }
28+say len(big);
29+say keys(big)[50];
30+say big["98"];

ashvm/tests/cases/collections/sort.ashadded

+20−0
@@ -0,0 +1,20 @@
1+local a = [5, 2, 9, 1, 5, 6, -3, 0];
2+sort(a);
3+say a;
4+local s = ["pear", "apple", "fig", "banana", "apple", ""];
5+say sort(s);
6+local m = [{"k": 2, "t": "a"}, {"k": 1, "t": "b"}, {"k": 2, "t": "c"}, {"k": 1, "t": "d"}];
7+sort(m, forge (x, y) { yield x["k"] - y["k"]; });
8+say map(m, forge (x) { yield x["t"]; });
9+say sort([3, 1, 2], forge (x, y) { yield y - x; });
10+say sort([]);
11+say sort([1]);
12+local big = [];
13+local i = 0;
14+during i < 200 { push(big, (i * 37) % 101); i++; }
15+sort(big);
16+local ok = yes;
17+i = 1;
18+during i < len(big) { given big[i - 1] > big[i] { ok = no; } i++; }
19+say ok;
20+say len(big);

ashvm/tests/cases/control_flow/short_circuit.ashadded

+11−0
@@ -0,0 +1,11 @@
1+local a = [1, 2];
2+given 5 < len(a) and a[5] == 1 { say "bad"; } otherwise { say "ok and"; }
3+given 0 < len(a) or a[5] == 1 { say "ok or"; }
4+say 1 and 2;
5+say 0 or 3;
6+say 0 and 1;
7+say 1 or 0;
8+say 1 and 0 or 1;
9+local n = 0;
10+during n < 5 and n != 3 { n++; }
11+say n;

ashvm/tests/cases/errors/attempt_array_delete.ashadded

+10−0
@@ -0,0 +1,10 @@
1+local a = [1, 2, 3];
2+attempt { delete(a, 0); say "deleted"; say a; push(a, 9); delete(a, 99); say "unreachable"; } handle (e) { say "caught: " + e; }
3+say a;
4+attempt { local m = {"x": 1}; delete(m, "x"); say m; say m["x"]; } handle (e) { say "caught: " + e; }
5+local i = 0;
6+during i < 3 {
7+ attempt { delete(a, 10); } handle (e) { say "again: " + e; }
8+ i++;
9+}
10+say "end";

ashvm/tests/cases/errors/attempt_stale_handler.ashadded

+21−0
@@ -0,0 +1,21 @@
1+forge f() {
2+ local i = 0;
3+ during i < 3 {
4+ attempt { local x = 1; } handle (e) { say "inner"; }
5+ i++;
6+ }
7+ raise "boom";
8+}
9+attempt { f(); } handle (e) { say "outer: " + e; }
10+forge g() {
11+ attempt { yield 5; } handle (e) { say "no"; }
12+}
13+forge h() { local v = g(); raise "boom2"; }
14+attempt { h(); } handle (e) { say "outer: " + e; }
15+local i = 0;
16+during i < 3 {
17+ i++;
18+ attempt { next; } handle (e) { say "no"; }
19+}
20+attempt { raise "second"; } handle (e) { say "caught " + e; }
21+say "done";

ashvm/tests/cases/errors/error_message_field.ashadded

+5−0
@@ -0,0 +1,5 @@
1+attempt { local a = [1]; say a[3]; } handle (e) { say e.message; say "x: " + e; }
2+attempt { raise {"message": "custom", "line": 3}; } handle (e) { say e.message; say e.line; }
3+attempt { raise "plain"; } handle (e) { say e.message == e; }
4+local m = {"inner": {"deep": 7}};
5+say m.inner.deep;

ashvm/tests/cases/errors/error_messages.ashadded

+23−0
@@ -0,0 +1,23 @@
1+local a = [1, 2];
2+attempt { say a[5]; } handle (e) { say e; }
3+attempt { say {"k": 1}["zz"]; } handle (e) { say e; }
4+attempt { say "abc"[9]; } handle (e) { say e; }
5+attempt { say "x" + 1; } handle (e) { say e; }
6+attempt { say 1 + "x"; } handle (e) { say e; }
7+attempt { say str("t"); } handle (e) { say e; }
8+attempt { pop([]); } handle (e) { say e; }
9+attempt { delete([1], 3); } handle (e) { say e; }
10+attempt { insert([1], 3, 0); } handle (e) { say e; }
11+attempt { slice([1, 2], 1, 5); } handle (e) { say e; }
12+attempt { chr(0); } handle (e) { say e; }
13+attempt { ord(""); } handle (e) { say e; }
14+attempt { repeat("a", -2); } handle (e) { say e; }
15+attempt { read_file("/nonexistent/file"); } handle (e) { say e; }
16+attempt { list_dir("/nonexistent"); } handle (e) { say e; }
17+attempt { given [1] { say "bad"; } } handle (e) { say e; }
18+attempt { raise "custom"; } handle (e) { say e; }
19+attempt { raise {"kind": "parse", "line": 3, "col": 7}; } handle (e) { say e["kind"] + ":" + str(e["line"]) + ":" + str(e["col"]); }
20+attempt { raise [1, 2]; } handle (e) { say e; }
21+attempt { raise 42; } handle (e) { say e + 1; }
22+forge f() { raise {"m": "inner"}; }
23+attempt { f(); } handle (e) { say e["m"]; }

ashvm/tests/cases/errors/type_errors.ashadded

+6−0
@@ -0,0 +1,6 @@
1+attempt { say "x" + 1; } handle (e) { say "caught plus"; }
2+attempt { say 1 + "x"; } handle (e) { say "caught plus2"; }
3+attempt { say str("text"); } handle (e) { say "caught str"; }
4+say "a" + "b";
5+say 1 + 2;
6+say str(5);

ashvm/tests/cases/functions/builtins_as_values.ashadded

+9−0
@@ -0,0 +1,9 @@
1+say type(len);
2+say map([1, 22, 333], str);
3+say map(["a", "bb"], len);
4+say map(["ab", "cd"], upper);
5+say filter([1, 2, 3, 4], forge (x) { yield x > 2; });
6+local f = len;
7+say f("hello");
8+say reduce([1, 2, 3], forge (a, b) { yield a + b; }, 0);
9+say map([4, 9], sqrt);

ashvm/tests/cases/functions/forward_declarations.ashadded

+20−0
@@ -0,0 +1,20 @@
1+say is_even(10);
2+say is_odd(7);
3+say is_even(7);
4+forge is_even(n) {
5+ given n == 0 { yield yes; }
6+ yield is_odd(n - 1);
7+}
8+forge is_odd(n) {
9+ given n == 0 { yield no; }
10+ yield is_even(n - 1);
11+}
12+say twice(3);
13+forge twice(x) { yield helper(x) * 2; }
14+forge helper(x) { yield x + 1; }
15+local f = helper;
16+say f(5);
17+say map([1, 2], twice);
18+forge shadow(a) { yield a; }
19+local shadow = forge (a) { yield a * 100; };
20+say shadow(2);

ashvm/tests/cases/imports/fixtures/imports_state_lib.ashadded

+15−0
@@ -0,0 +1,15 @@
1+local DIGITS = "0123456789abcdef";
2+local TABLE = [10, 20, 30];
3+local COUNT = 0;
4+local CONFIG = {"name": "stateful", "level": 2};
5+local DOUBLE = 21 * 2;
6+local ENABLED = yes;
7+
8+forge bump() { COUNT += 1; yield COUNT; }
9+forge hexdigit(n) { yield substring(DIGITS, n, n + 1); }
10+forge table_at(i) { yield TABLE[i]; }
11+forge add_item(v) { push(TABLE, v); yield len(TABLE); }
12+forge set_level(l) { CONFIG["level"] = l; }
13+forge level() { yield CONFIG["level"]; }
14+forge reset() { COUNT = 0; }
15+forge bump2() { COUNT++; yield COUNT; }

ashvm/tests/cases/imports/fixtures/imports_transitive_mid.ashadded

+3−0
@@ -0,0 +1,3 @@
1+@import <./imports_basic_lib.ash>;
2+
3+forge mid_triple(x) { yield imports_basic_lib.add(x, imports_basic_lib.add(x, x)); }

ashvm/tests/cases/imports/imports_alias_test.ashadded

+6−0
@@ -0,0 +1,6 @@
1+@import <./fixtures/imports_basic_lib.ash> as lib;
2+@import <./fixtures/imports_const_lib.ash> as k;
3+
4+say lib.add(3, 4);
5+say lib.helper(10);
6+say k.GREETING;

ashvm/tests/cases/imports/imports_state_test.ashadded

+17−0
@@ -0,0 +1,17 @@
1+@import <./fixtures/imports_state_lib.ash>;
2+
3+say imports_state_lib.bump();
4+say imports_state_lib.bump();
5+say imports_state_lib.hexdigit(11);
6+say imports_state_lib.table_at(1);
7+say imports_state_lib.add_item(40);
8+say imports_state_lib.add_item(50);
9+say imports_state_lib.TABLE;
10+imports_state_lib.set_level(9);
11+say imports_state_lib.level();
12+say imports_state_lib.CONFIG["name"];
13+say imports_state_lib.DOUBLE;
14+say imports_state_lib.ENABLED;
15+imports_state_lib.reset();
16+say imports_state_lib.bump2();
17+say imports_state_lib.COUNT;

ashvm/tests/cases/imports/imports_transitive_test.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./fixtures/imports_transitive_mid.ash>;
2+
3+say imports_transitive_mid.mid_triple(4);
4+say imports_basic_lib.add(1, 2);
5+say imports_basic_lib.helper(10);

ashvm/tests/cases/io/fs_ops.ashadded

+15−0
@@ -0,0 +1,15 @@
1+local d = "/tmp/ash_fs_test_dir";
2+delete_file(d + "/b.txt"); delete_file(d + "/a.txt"); delete_file(d + "/c.txt");
3+make_dir(d);
4+say file_exists(d + "/a.txt");
5+write_file(d + "/a.txt", "hello");
6+write_file(d + "/b.txt", "world");
7+say rename_file(d + "/b.txt", d + "/c.txt");
8+say rename_file(d + "/nope.txt", d + "/x.txt");
9+say sort(list_dir(d));
10+say read_file(d + "/c.txt");
11+say delete_file(d + "/a.txt");
12+say delete_file(d + "/a.txt");
13+say delete_file(d + "/c.txt");
14+say list_dir(d);
15+attempt { list_dir(d + "/missing"); } handle (e) { say "no dir"; }

ashvm/tests/cases/strings/affix_ops.ashadded

+16−0
@@ -0,0 +1,16 @@
1+say contains("hello world", "o w");
2+say contains("hello", "xyz");
3+say contains("hello", "");
4+say starts_with("hello", "he");
5+say starts_with("hello", "hello!");
6+say starts_with("hello", "");
7+say starts_with("hello", "el");
8+say ends_with("hello", "llo");
9+say ends_with("hello", "hell");
10+say ends_with("hello", "");
11+say ends_with("lo", "hello");
12+say repeat("ab", 3);
13+say repeat("x", 0);
14+say repeat("", 5) + "|";
15+say len(repeat("abc", 1000));
16+attempt { repeat("a", -1); } handle (e) { say "neg"; }

ashvm/tests/cases/strings/chr_ord.ashadded

+7−0
@@ -0,0 +1,7 @@
1+say ord("A");
2+say chr(97) + chr(98);
3+say ord(chr(200));
4+say chr(ord("z") - 1);
5+local s = "hello";
6+local i = 0;
7+during i < len(s) { say ord(substring(s, i, i + 1)); i++; }

ashvm/tests/cases/strings/string_compare.ashadded

+9−0
@@ -0,0 +1,9 @@
1+say "a" < "b";
2+say "b" < "a";
3+say "abc" <= "abc";
4+say "abd" > "abc";
5+say "" < "a";
6+say "Z" < "a";
7+say 1 < 2;
8+local s = "m";
9+given s >= "k" { say "fused ok"; }

kiln/Makefilemodified

+8−0
@@ -11,6 +11,7 @@ KILN_SRC = \
1111 src/parser/C/core/parser.c \
1212 src/parser/C/declarations/constants.c \
1313 src/parser/C/declarations/functions.c \
14+ src/parser/C/declarations/module_state.c \
1415 src/parser/C/declarations/vars.c \
1516 src/parser/C/imports/import_paths.c \
1617 src/parser/C/imports/imports.c \
@@ -19,13 +20,16 @@ KILN_SRC = \
1920 src/parser/C/statements/parser_control.c \
2021 src/parser/C/statements/attempt_handle.c \
2122 src/codegen/C/builtins/builtins.c \
23+ src/codegen/C/builtins/exit_builtin.c \
2224 src/codegen/C/builtins/filter_builtin.c \
2325 src/codegen/C/builtins/map_builtin.c \
2426 src/codegen/C/builtins/type_builtin.c \
2527 src/codegen/C/builtins/math_builtins.c \
2628 src/codegen/C/builtins/num_builtin.c \
2729 src/codegen/C/builtins/reduce_builtin.c \
30+ src/codegen/C/builtins/sort_builtin.c \
2831 src/codegen/C/builtins/str_builtin.c \
32+ src/codegen/C/collections/array_mutate.c \
2933 src/codegen/C/collections/arrays.c \
3034 src/codegen/C/collections/keys_values.c \
3135 src/codegen/C/collections/maps.c \
@@ -43,6 +47,7 @@ KILN_SRC = \
4347 src/codegen/C/io/argv_builtin.c \
4448 src/codegen/C/io/argv_builtin_windows.c \
4549 src/codegen/C/io/file_path.c \
50+ src/codegen/C/io/fs_builtins.c \
4651 src/codegen/C/io/input_builtin.c \
4752 src/codegen/C/io/read_file_builtin.c \
4853 src/codegen/C/io/write_file_builtins.c \
@@ -52,8 +57,11 @@ KILN_SRC = \
5257 src/codegen/C/platform/win_file_call.c \
5358 src/codegen/C/runtime/errors.c \
5459 src/codegen/C/runtime/heap.c \
60+ src/codegen/C/runtime/number_format.c \
5561 src/codegen/C/runtime/print_int.c \
62+ src/codegen/C/strings/affix_builtins.c \
5663 src/codegen/C/strings/case_builtins.c \
64+ src/codegen/C/strings/char_builtins.c \
5765 src/codegen/C/strings/indexof_builtin.c \
5866 src/codegen/C/strings/join_builtin.c \
5967 src/codegen/C/strings/replace_builtin.c \

kiln/src/codegen/C/builtins/builtins.cmodified

+83−0
@@ -12,6 +12,7 @@
1212 #include "codegen/H/io/input_builtin.h"
1313 #include "codegen/H/io/argv_builtin.h"
1414 #include "codegen/H/builtins/type_builtin.h"
15+#include "codegen/H/builtins/exit_builtin.h"
1516 #include "parser/H/core/parser.h"
1617
1718 int codegen_try_builtin_call(const char *name, int len) {
@@ -39,6 +40,22 @@ int codegen_try_builtin_call(const char *name, int len) {
3940 codegen_builtin_append_file();
4041 return 1;
4142 }
43+ if (len == 11 && strncmp(name, "rename_file", 11) == 0) {
44+ codegen_builtin_rename_file();
45+ return 1;
46+ }
47+ if (len == 11 && strncmp(name, "delete_file", 11) == 0) {
48+ codegen_builtin_delete_file();
49+ return 1;
50+ }
51+ if (len == 8 && strncmp(name, "make_dir", 8) == 0) {
52+ codegen_builtin_make_dir();
53+ return 1;
54+ }
55+ if (len == 8 && strncmp(name, "list_dir", 8) == 0) {
56+ codegen_builtin_list_dir();
57+ return 1;
58+ }
4259 if (len == 11 && strncmp(name, "file_exists", 11) == 0) {
4360 codegen_expression();
4461 expect(TOKEN_RPAREN, "expected ')' after argument");
@@ -95,6 +112,15 @@ int codegen_try_builtin_call(const char *name, int len) {
95112 codegen_builtin_values();
96113 return 1;
97114 }
115+ if (len == 8 && strncmp(name, "contains", 8) == 0) { codegen_builtin_contains(); return 1; }
116+ if (len == 11 && strncmp(name, "starts_with", 11) == 0) { codegen_builtin_starts_with(); return 1; }
117+ if (len == 9 && strncmp(name, "ends_with", 9) == 0) { codegen_builtin_ends_with(); return 1; }
118+ if (len == 6 && strncmp(name, "repeat", 6) == 0) { codegen_builtin_repeat(); return 1; }
119+ if (len == 4 && strncmp(name, "exit", 4) == 0) { codegen_builtin_exit(); return 1; }
120+ if (len == 4 && strncmp(name, "sort", 4) == 0) {
121+ codegen_builtin_sort();
122+ return 1;
123+ }
98124 if (len == 3 && strncmp(name, "map", 3) == 0) {
99125 codegen_builtin_map();
100126 return 1;
@@ -167,6 +193,44 @@ int codegen_try_builtin_call(const char *name, int len) {
167193 codegen_builtin_delete();
168194 return 1;
169195 }
196+ if (len == 3 && strncmp(name, "pop", 3) == 0) {
197+ codegen_expression();
198+ expect(TOKEN_RPAREN, "expected ')' after argument");
199+ codegen_builtin_pop();
200+ return 1;
201+ }
202+ if (len == 6 && strncmp(name, "insert", 6) == 0) {
203+ codegen_expression();
204+ expect(TOKEN_COMMA, "expected ',' after array argument");
205+ codegen_expression();
206+ expect(TOKEN_COMMA, "expected ',' after index argument");
207+ codegen_expression();
208+ expect(TOKEN_RPAREN, "expected ')' after arguments");
209+ codegen_builtin_insert();
210+ return 1;
211+ }
212+ if (len == 5 && strncmp(name, "slice", 5) == 0) {
213+ codegen_expression();
214+ expect(TOKEN_COMMA, "expected ',' after array argument");
215+ codegen_expression();
216+ expect(TOKEN_COMMA, "expected ',' after start argument");
217+ codegen_expression();
218+ expect(TOKEN_RPAREN, "expected ')' after arguments");
219+ codegen_builtin_slice();
220+ return 1;
221+ }
222+ if (len == 3 && strncmp(name, "chr", 3) == 0) {
223+ codegen_expression();
224+ expect(TOKEN_RPAREN, "expected ')' after argument");
225+ codegen_builtin_chr();
226+ return 1;
227+ }
228+ if (len == 3 && strncmp(name, "ord", 3) == 0) {
229+ codegen_expression();
230+ expect(TOKEN_RPAREN, "expected ')' after argument");
231+ codegen_builtin_ord();
232+ return 1;
233+ }
170234 if (len == 4 && strncmp(name, "type", 4) == 0) {
171235 codegen_expression();
172236 expect(TOKEN_RPAREN, "expected ')' after argument");
@@ -175,3 +239,22 @@ int codegen_try_builtin_call(const char *name, int len) {
175239 }
176240 return 0;
177241 }
242+
243+// Argument count each builtin is wrapped with when used as a value
244+// (`map(xs, str)`); -1 if `name` is not a builtin. Same set and arities as
245+// ashvm's builtin table.
246+int codegen_builtin_arity(const char *name, int len) {
247+ static const struct { const char *name; int arity; } table[] = {
248+ {"len", 1}, {"push", 2}, {"input", 0}, {"str", 1}, {"num", 1}, {"sqrt", 1}, {"abs", 1}, {"floor", 1},
249+ {"map", 2}, {"filter", 2}, {"reduce", 3}, {"keys", 1}, {"values", 1}, {"has", 2}, {"delete", 2},
250+ {"split", 2}, {"join", 2}, {"substring", 3}, {"indexOf", 2}, {"replace", 3}, {"upper", 1}, {"lower", 1}, {"trim", 1},
251+ {"read_file", 1}, {"write_file", 2}, {"append_file", 2}, {"file_exists", 1},
252+ {"type", 1}, {"chr", 1}, {"ord", 1}, {"pop", 1}, {"insert", 3}, {"slice", 3}, {"sort", 1},
253+ {"rename_file", 2}, {"delete_file", 1}, {"make_dir", 1}, {"list_dir", 1},
254+ {"contains", 2}, {"starts_with", 2}, {"ends_with", 2}, {"repeat", 2}, {"exit", 1}, {"argv", 0},
255+ };
256+ for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++) {
257+ if ((int)strlen(table[i].name) == len && strncmp(table[i].name, name, (size_t)len) == 0) return table[i].arity;
258+ }
259+ return -1;
260+}

kiln/src/codegen/C/builtins/exit_builtin.cadded

+30−0
@@ -0,0 +1,30 @@
1+#include "codegen/H/builtins/exit_builtin.h"
2+#include "codegen/H/expressions/expr.h"
3+#include "codegen/H/emit/emit.h"
4+#include "codegen/H/emit/emit_sse.h"
5+#include "codegen/H/platform/win_call.h"
6+#include "app/H/target.h"
7+#include "parser/H/core/parser.h"
8+
9+// exit(code): ends the process with that status. Output is written
10+// unbuffered, so there is nothing to flush first.
11+void codegen_builtin_exit(void) {
12+ codegen_expression();
13+ expect(TOKEN_RPAREN, "expected ')' after argument");
14+ emit_pop_reg(code, REG_RAX); // payload
15+ emit_pop_reg(code, REG_RBX); // tag (ignored)
16+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
17+ emit_cvttsd2si(code, REG_RDI, XMM0);
18+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
19+ win_call_begin(code, 0);
20+ win_call_arg_reg(code, 0, REG_RDI);
21+ win_call_import(code, PE_IMPORT_EXIT_PROCESS);
22+ } else {
23+ emit_mov_reg_imm64(code, REG_RAX, 60); // syscall: exit
24+ emit_syscall(code);
25+ }
26+ // never returns; keep the stack-machine model balanced for the caller
27+ emit_mov_reg_imm64(code, REG_RBX, 5 /* TAG_NIL */);
28+ emit_push_reg(code, REG_RBX);
29+ emit_push_reg(code, REG_RBX);
30+}

kiln/src/codegen/C/builtins/num_builtin.cmodified

+67−0
@@ -117,6 +117,73 @@ void codegen_builtin_num(void) {
117117 emit_patch_jump(code, not_dot);
118118 emit_patch_jump(code, no_frac_check);
119119
120+ // ---- optional exponent: e/E, optional sign, digits ----
121+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
122+ int exp_end = emit_jcc_rel32(code, COND_GE);
123+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
124+ emit_cmp_reg_imm32(code, REG_RCX, 'e');
125+ int is_e = emit_jcc_rel32(code, COND_E);
126+ emit_cmp_reg_imm32(code, REG_RCX, 'E');
127+ int exp_none = emit_jcc_rel32(code, COND_NE);
128+ emit_patch_jump(code, is_e);
129+ emit_add_reg_imm8(code, REG_RSI, 1);
130+
131+ load_double_const(XMM2, 10.0); // (clobbers RAX, so before the flag)
132+ emit_mov_reg_imm64(code, REG_RAX, 0); // exponent-negative flag
133+ emit_pxor_xmm_xmm(code, XMM3); // exponent magnitude, as a double
134+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
135+ int exp_sign_skip = emit_jcc_rel32(code, COND_GE);
136+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
137+ emit_cmp_reg_imm32(code, REG_RCX, '-');
138+ int exp_not_minus = emit_jcc_rel32(code, COND_NE);
139+ emit_mov_reg_imm64(code, REG_RAX, 1);
140+ emit_add_reg_imm8(code, REG_RSI, 1);
141+ int exp_sign_done = emit_jmp_rel32(code);
142+ emit_patch_jump(code, exp_not_minus);
143+ emit_cmp_reg_imm32(code, REG_RCX, '+');
144+ int exp_not_plus = emit_jcc_rel32(code, COND_NE);
145+ emit_add_reg_imm8(code, REG_RSI, 1);
146+ emit_patch_jump(code, exp_not_plus);
147+ emit_patch_jump(code, exp_sign_done);
148+ emit_patch_jump(code, exp_sign_skip);
149+
150+ int exp_loop = code->count;
151+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
152+ int exp_digits_end = emit_jcc_rel32(code, COND_GE);
153+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
154+ emit_cmp_reg_imm32(code, REG_RCX, '0');
155+ int exp_lt = emit_jcc_rel32(code, COND_LT);
156+ emit_cmp_reg_imm32(code, REG_RCX, '9');
157+ int exp_gt = emit_jcc_rel32(code, COND_GT);
158+ emit_mulsd(code, XMM3, XMM2);
159+ emit_sub_reg_imm8(code, REG_RCX, '0');
160+ emit_cvtsi2sd(code, XMM1, REG_RCX);
161+ emit_addsd(code, XMM3, XMM1);
162+ emit_add_reg_imm8(code, REG_RSI, 1);
163+ emit_jmp_back(code, exp_loop);
164+ emit_patch_jump(code, exp_lt);
165+ emit_patch_jump(code, exp_gt);
166+ emit_patch_jump(code, exp_digits_end);
167+
168+ // Scale by 10^exponent, one multiply (or divide, for a negative
169+ // exponent) at a time -- exact for the common small cases.
170+ emit_cvttsd2si(code, REG_RDX, XMM3);
171+ int scale_loop = code->count;
172+ emit_cmp_reg_imm32(code, REG_RDX, 0);
173+ int scale_done = emit_jcc_rel32(code, COND_LE);
174+ emit_cmp_reg_imm32(code, REG_RAX, 0);
175+ int scale_down = emit_jcc_rel32(code, COND_NE);
176+ emit_mulsd(code, XMM0, XMM2);
177+ int scale_next = emit_jmp_rel32(code);
178+ emit_patch_jump(code, scale_down);
179+ emit_divsd(code, XMM0, XMM2);
180+ emit_patch_jump(code, scale_next);
181+ emit_dec_reg(code, REG_RDX);
182+ emit_jmp_back(code, scale_loop);
183+ emit_patch_jump(code, scale_done);
184+ emit_patch_jump(code, exp_none);
185+ emit_patch_jump(code, exp_end);
186+
120187 // ---- apply sign ----
121188 emit_cmp_reg_imm32(code, REG_RBX, 0);
122189 int no_negate = emit_jcc_rel32(code, COND_E);

kiln/src/codegen/C/builtins/sort_builtin.cadded

+164−0
@@ -0,0 +1,164 @@
1+#include "codegen/H/builtins/higher_order.h"
2+#include "codegen/H/expressions/expr.h"
3+#include "codegen/H/functions/closures.h"
4+#include "codegen/H/strings/strings.h"
5+#include "codegen/H/emit/emit.h"
6+#include "codegen/H/emit/emit_sse.h"
7+#include "codegen/H/emit/value.h"
8+#include "parser/H/core/parser.h"
9+#include "parser/H/declarations/vars.h"
10+
11+// sort(array) / sort(array, cmp): in-place, stable insertion sort; pushes the
12+// array back (like push()). Without cmp, numbers sort numerically and strings
13+// bytewise (a pair of strings -> string order, anything else -> numeric). With
14+// cmp, an element moves left while cmp(previous, key) is a number > 0.
15+//
16+// Scratch layout (see vars.h's higher_order_scratch_offset):
17+// [0]=cmp_tag [8]=cmp_payload [16]=data_ptr [24]=count [32]=i [40]=j
18+// [48]=key_tag [56]=key_payload [64]=array_object
19+void codegen_builtin_sort(void) {
20+ codegen_expression(); // array
21+ int has_cmp = 0;
22+ if (current.type == TOKEN_COMMA) {
23+ advance_token();
24+ codegen_expression(); // cmp
25+ has_cmp = 1;
26+ }
27+ expect(TOKEN_RPAREN, "expected ')' after arguments");
28+
29+ int32_t base = higher_order_scratch_offset();
30+
31+ if (has_cmp) {
32+ emit_pop_reg(code, REG_RAX);
33+ emit_pop_reg(code, REG_RBX);
34+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RBX);
35+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX);
36+ }
37+ emit_pop_reg(code, REG_RSI); // array object
38+ emit_pop_reg(code, REG_RCX); // tag (ignored)
39+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RSI);
40+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
41+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16);
42+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
43+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RDX);
44+ emit_mov_reg_imm64(code, REG_RCX, 1);
45+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RCX); // i = 1
46+
47+ int outer = code->count;
48+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32);
49+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
50+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
51+ int all_done = emit_jcc_rel32(code, COND_GE);
52+
53+ // key = data[i]; j = i
54+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);
55+ emit_mov_reg_imm64(code, REG_RAX, 16);
56+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
57+ emit_add_reg_reg(code, REG_RSI, REG_RCX);
58+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 0);
59+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
60+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RBX);
61+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX);
62+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32);
63+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX);
64+
65+ int inner = code->count;
66+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
67+ emit_cmp_reg_imm32(code, REG_RCX, 0);
68+ int place = emit_jcc_rel32(code, COND_LE);
69+
70+ // RSI = &data[j - 1]
71+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);
72+ emit_sub_reg_imm8(code, REG_RCX, 1);
73+ emit_mov_reg_imm64(code, REG_RAX, 16);
74+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
75+ emit_add_reg_reg(code, REG_RSI, REG_RCX);
76+
77+ int not_greater;
78+ if (has_cmp) {
79+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 0);
80+ emit_push_reg(code, REG_RAX);
81+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8);
82+ emit_push_reg(code, REG_RAX);
83+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0);
84+ emit_push_reg(code, REG_RAX);
85+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
86+ emit_push_reg(code, REG_RAX);
87+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 48);
88+ emit_push_reg(code, REG_RAX);
89+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 56);
90+ emit_push_reg(code, REG_RAX);
91+ codegen_call_closure_value(2);
92+ emit_pop_reg(code, REG_RAX); // result payload
93+ emit_pop_reg(code, REG_RBX); // result tag (assumed NUMBER)
94+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
95+ emit_pxor_xmm_xmm(code, XMM1);
96+ emit_ucomisd(code, XMM0, XMM1);
97+ not_greater = emit_jcc_rel32(code, COND_BE);
98+ // the call clobbered RSI: recompute &data[j - 1]
99+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
100+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);
101+ emit_sub_reg_imm8(code, REG_RCX, 1);
102+ emit_mov_reg_imm64(code, REG_RAX, 16);
103+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
104+ emit_add_reg_reg(code, REG_RSI, REG_RCX);
105+ } else {
106+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 0); // previous tag
107+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // previous payload
108+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 48); // key tag
109+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 56); // key payload
110+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
111+ int a_num = emit_jcc_rel32(code, COND_NE);
112+ emit_cmp_reg_imm32(code, REG_RDX, TAG_STRING);
113+ int b_num = emit_jcc_rel32(code, COND_NE);
114+ codegen_string_order();
115+ int flags_ready = emit_jmp_rel32(code);
116+ emit_patch_jump(code, a_num);
117+ emit_patch_jump(code, b_num);
118+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
119+ emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
120+ emit_ucomisd(code, XMM0, XMM1);
121+ emit_patch_jump(code, flags_ready);
122+ not_greater = emit_jcc_rel32(code, COND_BE);
123+ // string_order clobbered RSI: recompute &data[j - 1]
124+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
125+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);
126+ emit_sub_reg_imm8(code, REG_RCX, 1);
127+ emit_mov_reg_imm64(code, REG_RAX, 16);
128+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
129+ emit_add_reg_reg(code, REG_RSI, REG_RCX);
130+ }
131+
132+ // data[j] = data[j - 1]; j--
133+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0);
134+ emit_store_mem_disp32(code, REG_RSI, 16, REG_RAX);
135+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
136+ emit_store_mem_disp32(code, REG_RSI, 24, REG_RAX);
137+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
138+ emit_sub_reg_imm8(code, REG_RCX, 1);
139+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX);
140+ emit_jmp_back(code, inner);
141+
142+ // data[j] = key; i++
143+ emit_patch_jump(code, place);
144+ emit_patch_jump(code, not_greater);
145+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
146+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);
147+ emit_mov_reg_imm64(code, REG_RAX, 16);
148+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
149+ emit_add_reg_reg(code, REG_RSI, REG_RCX);
150+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 48);
151+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 56);
152+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RBX);
153+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
154+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32);
155+ emit_add_reg_imm8(code, REG_RCX, 1);
156+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RCX);
157+ emit_jmp_back(code, outer);
158+
159+ emit_patch_jump(code, all_done);
160+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 64);
161+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
162+ emit_push_reg(code, REG_RBX);
163+ emit_push_reg(code, REG_RAX);
164+}

kiln/src/codegen/C/builtins/str_builtin.cmodified

+24−88
@@ -4,98 +4,32 @@
44 #include "codegen/H/emit/emit_sse.h"
55 #include "codegen/H/emit/value.h"
66 #include "codegen/H/runtime/heap.h"
7+#include "codegen/H/runtime/errors.h"
8+#include "codegen/H/strings/strings.h"
9+#include "codegen/H/runtime/number_format.h"
710 #include "codegen/H/emit/bytes.h"
811 #include "parser/H/core/parser.h"
912
10-static void load_double_const(XReg dst, double v) {
11- uint64_t bits;
12- memcpy(&bits, &v, sizeof(bits));
13- emit_mov_reg_imm64(code, REG_RAX, bits);
14- emit_movq_xmm_from_reg(code, dst, REG_RAX);
15-}
16-
17-// Scratch layout while building digits, same shape as print_int.c:
18-// [rsp, rsp+32) integer digits (+ optional '-'), built backward
19-// rsp+32 '.' (only if there's a fraction)
20-// [rsp+33, rsp+39) up to 6 fraction digits, built forward
13+// str(number): formats via number_format_emit (see its header for the format)
14+// into a stack scratch region, then copies the text into a fresh string.
2115 void codegen_builtin_str(void) {
2216 emit_pop_reg(code, REG_RAX); // payload
23- emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
24- emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
25- emit_sub_reg_imm8(code, REG_RSP, 40);
26-
27- // ---- sign ----
28- emit_pxor_xmm_xmm(code, XMM2);
29- emit_ucomisd(code, XMM0, XMM2);
30- emit_mov_reg_imm64(code, REG_RBX, 0);
31- int skip_negate = emit_jcc_rel32(code, COND_AE);
32- emit_pxor_xmm_xmm(code, XMM2);
33- emit_subsd(code, XMM2, XMM0);
34- emit_movsd_xmm_xmm(code, XMM0, XMM2);
35- emit_mov_reg_imm64(code, REG_RBX, 1);
36- emit_patch_jump(code, skip_negate);
37-
38- // ---- split into integer part (RAX) and fractional remainder (XMM0) ----
39- emit_cvttsd2si(code, REG_RAX, XMM0);
40- emit_cvtsi2sd(code, XMM1, REG_RAX);
41- emit_subsd(code, XMM0, XMM1);
42-
43- // ---- integer digits, backward ----
44- emit_mov_reg_reg(code, REG_RCX, REG_RSP);
45- emit_add_reg_imm8(code, REG_RCX, 32);
46- int loop_start = code->count;
47- emit_dec_reg(code, REG_RCX);
48- emit_cqo(code);
49- emit_mov_reg_imm64(code, REG_RSI, 10);
50- emit_idiv_reg(code, REG_RSI);
51- emit_add_reg_imm8(code, REG_RDX, '0');
52- emit_store_byte_reg(code, REG_RCX, REG_RDX);
53- emit_cmp_reg_imm32(code, REG_RAX, 0);
54- emit_jcc_back(code, COND_NE, loop_start);
55-
56- // ---- sign prepend ----
57- emit_cmp_reg_imm32(code, REG_RBX, 0);
58- int skip_sign = emit_jcc_rel32(code, COND_E);
59- emit_dec_reg(code, REG_RCX);
60- emit_store_byte_imm(code, REG_RCX, '-');
61- emit_patch_jump(code, skip_sign);
62-
63- // ---- fraction digits, forward, round-half-up first (see
64- // print_int.c's comment for why) ----
65- load_double_const(XMM1, 0.0000005);
66- emit_addsd(code, XMM0, XMM1);
67- emit_mov_reg_reg(code, REG_RSI, REG_RSP);
68- emit_add_reg_imm8(code, REG_RSI, 33);
69- load_double_const(XMM3, 10.0);
70- emit_mov_reg_imm64(code, REG_RBX, 0); // frac_len
71- for (int i = 0; i < 6; i++) {
72- emit_mulsd(code, XMM0, XMM3);
73- emit_cvttsd2si(code, REG_RAX, XMM0);
74- emit_cvtsi2sd(code, XMM1, REG_RAX);
75- emit_subsd(code, XMM0, XMM1);
76- emit_cmp_reg_imm32(code, REG_RAX, 0);
77- int skip_mark = emit_jcc_rel32(code, COND_E);
78- emit_mov_reg_imm64(code, REG_RBX, (uint64_t)(i + 1));
79- emit_patch_jump(code, skip_mark);
80- emit_add_reg_imm8(code, REG_RAX, '0');
81- emit_store_byte_reg(code, REG_RSI, REG_RAX);
82- emit_add_reg_imm8(code, REG_RSI, 1);
83- }
84-
85- // ---- decide '.' placement, compute the content's end pointer (no
86- // trailing newline -- this is a string value, not a print) ----
87- emit_mov_reg_reg(code, REG_RDX, REG_RSP);
88- emit_add_reg_imm8(code, REG_RDX, 32);
89- emit_cmp_reg_imm32(code, REG_RBX, 0);
90- int whole_number = emit_jcc_rel32(code, COND_E);
91- emit_store_byte_imm(code, REG_RDX, '.');
92- emit_mov_reg_reg(code, REG_RDX, REG_RSP);
93- emit_add_reg_imm8(code, REG_RDX, 33);
94- emit_add_reg_reg(code, REG_RDX, REG_RBX);
95- int after_dot = emit_jmp_rel32(code);
96- emit_patch_jump(code, whole_number);
97- emit_patch_jump(code, after_dot);
98-
17+ emit_pop_reg(code, REG_RBX); // tag
18+ emit_cmp_reg_imm32(code, REG_RBX, TAG_NUMBER);
19+ int is_number = emit_jcc_rel32(code, COND_E);
20+ emit_cmp_reg_imm32(code, REG_RBX, TAG_BOOL);
21+ int is_bool = emit_jcc_rel32(code, COND_E);
22+ errors_emit_die(code, "runtime error: str() expects a number");
23+ emit_patch_jump(code, is_bool);
24+ emit_cmp_reg_imm32(code, REG_RAX, 0); // payload bits: 0.0 is all zero
25+ int bool_no = emit_jcc_rel32(code, COND_E);
26+ codegen_string_literal_bytes("yes", 3);
27+ int bool_done_yes = emit_jmp_rel32(code);
28+ emit_patch_jump(code, bool_no);
29+ codegen_string_literal_bytes("no", 2);
30+ int bool_done = emit_jmp_rel32(code);
31+ emit_patch_jump(code, is_number);
32+ number_format_emit(code); // RCX = text start, RDX = text end (on the stack)
9933 emit_sub_reg_reg(code, REG_RDX, REG_RCX); // RDX = content length
10034
10135 // ---- heap-allocate a length-prefixed block and copy the digits in ----
@@ -110,12 +44,14 @@ void codegen_builtin_str(void) {
11044 emit_mov_reg_reg(code, REG_RBX, REG_RCX); // src = digit buffer start
11145 bytes_emit_copy(code); // clobbers RAX,RBX,RCX,RDX,RDI; leaves RSI = block addr
11246
113- emit_add_reg_imm8(code, REG_RSP, 40); // release the digit scratch
47+ emit_add_reg_imm8(code, REG_RSP, NUMBER_FORMAT_SCRATCH); // release the digit scratch
11448
11549 emit_mov_reg_reg(code, REG_RAX, REG_RSI);
11650 emit_add_reg_imm8(code, REG_RAX, 8); // payload = block+8
11751
11852 emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
11953 emit_push_reg(code, REG_RBX);
12054 emit_push_reg(code, REG_RAX);
55+ emit_patch_jump(code, bool_done_yes);
56+ emit_patch_jump(code, bool_done);
12157 }

kiln/src/codegen/C/builtins/type_builtin.cmodified

+8−2
@@ -4,7 +4,7 @@
44 #include "codegen/H/emit/value.h"
55 #include "parser/H/core/parser.h"
66
7-// Pops (tag, payload), pushes one of six string literals depending on
7+// Pops (tag, payload), pushes one of seven string literals depending on
88 // the tag -- TAG_FUNCTION covers both a plain named function and a
99 // closure, matching value.h's documented "capture_count=0 is just a
1010 // zero-capture closure" shape (kiln has no separate closure tag the way
@@ -16,7 +16,7 @@ void codegen_builtin_type(void) {
1616 emit_pop_reg(code, REG_RAX); // payload (unused -- only the tag matters)
1717 emit_pop_reg(code, REG_RBX); // tag
1818
19- int jumps[5];
19+ int jumps[6];
2020 int n = 0;
2121
2222 emit_cmp_reg_imm32(code, REG_RBX, TAG_NUMBER);
@@ -25,6 +25,12 @@ void codegen_builtin_type(void) {
2525 jumps[n++] = emit_jmp_rel32(code);
2626 emit_patch_jump(code, not_number);
2727
28+ emit_cmp_reg_imm32(code, REG_RBX, TAG_BOOL);
29+ int not_bool = emit_jcc_rel32(code, COND_NE);
30+ codegen_string_literal_bytes("boolean", 7);
31+ jumps[n++] = emit_jmp_rel32(code);
32+ emit_patch_jump(code, not_bool);
33+
2834 emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
2935 int not_string = emit_jcc_rel32(code, COND_NE);
3036 codegen_string_literal_bytes("string", 6);

kiln/src/codegen/C/collections/array_mutate.cadded

+218−0
@@ -0,0 +1,218 @@
1+#include "codegen/H/collections/arrays.h"
2+#include "codegen/H/emit/emit_internal.h"
3+#include "codegen/H/emit/emit_sse.h"
4+#include "codegen/H/emit/value.h"
5+#include "codegen/H/runtime/heap.h"
6+#include "codegen/H/runtime/errors.h"
7+#include "parser/H/core/parser.h"
8+
9+// Array element = 16 bytes (tag, payload); object = [capacity][count][data_ptr].
10+// See codegen/H/emit/value.h.
11+
12+// RAX = index (a double's bits) -> RCX = integer index.
13+static void index_to_int(void) {
14+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
15+ emit_cvttsd2si(code, REG_RCX, XMM0);
16+}
17+
18+// Dies unless 0 <= RCX < count (or <= count when `allow_end`); RSI = array object.
19+static void check_index(int allow_end, const char *msg) {
20+ emit_cmp_reg_imm32(code, REG_RCX, 0);
21+ int neg = emit_jcc_rel32(code, COND_LT);
22+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count
23+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
24+ int big = emit_jcc_rel32(code, allow_end ? COND_GT : COND_GE);
25+ int ok = emit_jmp_rel32(code);
26+ emit_patch_jump(code, neg);
27+ emit_patch_jump(code, big);
28+ errors_emit_die(code, msg);
29+ emit_patch_jump(code, ok);
30+}
31+
32+// Called by codegen_builtin_delete when its first argument is an array.
33+// In: RDI = index payload, RSI = array object.
34+void codegen_array_delete_index(void) {
35+ emit_mov_reg_reg(code, REG_RAX, REG_RDI);
36+ index_to_int();
37+ check_index(0, "index out of bounds");
38+
39+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 8); // count
40+ emit_sub_reg_reg(code, REG_RBX, REG_RCX);
41+ emit_sub_reg_imm8(code, REG_RBX, 1); // items to shift left
42+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // data_ptr
43+ emit_mov_reg_imm64(code, REG_RAX, 16);
44+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
45+ emit_add_reg_reg(code, REG_RDI, REG_RCX); // &data[index]
46+
47+ int loop = code->count;
48+ emit_cmp_reg_imm32(code, REG_RBX, 0);
49+ int done = emit_jcc_rel32(code, COND_E);
50+ emit_load_mem_disp32(code, REG_RAX, REG_RDI, 16); emit_store_mem_disp32(code, REG_RDI, 0, REG_RAX);
51+ emit_load_mem_disp32(code, REG_RAX, REG_RDI, 24); emit_store_mem_disp32(code, REG_RDI, 8, REG_RAX);
52+ emit_add_reg_imm8(code, REG_RDI, 16);
53+ emit_dec_reg(code, REG_RBX);
54+ emit_jmp_back(code, loop);
55+ emit_patch_jump(code, done);
56+
57+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
58+ emit_sub_reg_imm8(code, REG_RAX, 1);
59+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX); // count--
60+
61+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
62+ emit_push_reg(code, REG_RBX);
63+ emit_push_reg(code, REG_RSI); // delete() returns the container, like for maps
64+}
65+
66+void codegen_builtin_pop(void) {
67+ emit_pop_reg(code, REG_RSI); // array object
68+ emit_pop_reg(code, REG_RBX); // tag (ignored)
69+
70+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
71+ emit_cmp_reg_imm32(code, REG_RAX, 0);
72+ int nonempty = emit_jcc_rel32(code, COND_NE);
73+ errors_emit_die(code, "runtime error: pop() on an empty array");
74+ emit_patch_jump(code, nonempty);
75+
76+ emit_sub_reg_imm8(code, REG_RAX, 1);
77+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX); // count--
78+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr
79+ emit_mov_reg_imm64(code, REG_RCX, 16);
80+ emit_imul_reg_reg(code, REG_RAX, REG_RCX);
81+ emit_add_reg_reg(code, REG_RAX, REG_RDX); // &data[old count - 1]
82+ emit_load_mem_disp32(code, REG_RBX, REG_RAX, 0);
83+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 8);
84+ emit_push_reg(code, REG_RBX);
85+ emit_push_reg(code, REG_RAX);
86+}
87+
88+// insert(array, index, value): validates the index, appends via push() (so
89+// growth is shared), then shifts the tail right and drops the value in.
90+void codegen_builtin_insert(void) {
91+ emit_pop_reg(code, REG_RAX); // value payload
92+ emit_pop_reg(code, REG_RBX); // value tag
93+ emit_pop_reg(code, REG_RCX); // index payload
94+ emit_pop_reg(code, REG_RDX); // index tag (ignored)
95+ emit_pop_reg(code, REG_RSI); // array object
96+ emit_pop_reg(code, REG_RDI); // array tag
97+
98+ emit_push_reg(code, REG_RAX); // keep value payload across the index check
99+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
100+ index_to_int();
101+ check_index(1, "index out of bounds");
102+ emit_pop_reg(code, REG_RAX);
103+
104+ emit_push_reg(code, REG_RBX); // saved: value tag
105+ emit_push_reg(code, REG_RAX); // value payload
106+ emit_push_reg(code, REG_RCX); // index
107+ emit_push_reg(code, REG_RDI); // push() arguments: array, then value
108+ emit_push_reg(code, REG_RSI);
109+ emit_push_reg(code, REG_RBX);
110+ emit_push_reg(code, REG_RAX);
111+ codegen_builtin_push();
112+ emit_pop_reg(code, REG_RSI); // array object (push() returns it)
113+ emit_pop_reg(code, REG_RDI); // tag
114+ emit_pop_reg(code, REG_RCX); // index
115+
116+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 8); // new count
117+ emit_sub_reg_imm8(code, REG_RBX, 1);
118+ emit_mov_reg_reg(code, REG_RDX, REG_RBX); // last index
119+ emit_sub_reg_reg(code, REG_RBX, REG_RCX); // items to shift right
120+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // data_ptr
121+ emit_mov_reg_imm64(code, REG_RAX, 16);
122+ emit_imul_reg_reg(code, REG_RDX, REG_RAX);
123+ emit_add_reg_reg(code, REG_RDI, REG_RDX); // &data[last]
124+
125+ int loop = code->count;
126+ emit_cmp_reg_imm32(code, REG_RBX, 0);
127+ int done = emit_jcc_rel32(code, COND_E);
128+ emit_load_mem_disp32(code, REG_RAX, REG_RDI, -16); emit_store_mem_disp32(code, REG_RDI, 0, REG_RAX);
129+ emit_load_mem_disp32(code, REG_RAX, REG_RDI, -8); emit_store_mem_disp32(code, REG_RDI, 8, REG_RAX);
130+ emit_sub_reg_imm8(code, REG_RDI, 16);
131+ emit_dec_reg(code, REG_RBX);
132+ emit_jmp_back(code, loop);
133+ emit_patch_jump(code, done);
134+
135+ emit_pop_reg(code, REG_RAX); // value payload
136+ emit_pop_reg(code, REG_RBX); // value tag
137+ emit_store_mem_disp32(code, REG_RDI, 0, REG_RBX);
138+ emit_store_mem_disp32(code, REG_RDI, 8, REG_RAX);
139+
140+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
141+ emit_push_reg(code, REG_RBX);
142+ emit_push_reg(code, REG_RSI);
143+}
144+
145+// slice(array, from, to): a new array of items [from, to).
146+void codegen_builtin_slice(void) {
147+ emit_pop_reg(code, REG_RAX); // to payload
148+ emit_pop_reg(code, REG_RBX);
149+ emit_pop_reg(code, REG_RCX); // from payload
150+ emit_pop_reg(code, REG_RBX);
151+ emit_pop_reg(code, REG_RSI); // array object
152+ emit_pop_reg(code, REG_RBX);
153+
154+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
155+ emit_cvttsd2si(code, REG_RDX, XMM0); // to
156+ emit_movq_xmm_from_reg(code, XMM0, REG_RCX);
157+ emit_cvttsd2si(code, REG_RCX, XMM0); // from
158+
159+ emit_cmp_reg_imm32(code, REG_RCX, 0);
160+ int bad1 = emit_jcc_rel32(code, COND_LT);
161+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
162+ emit_cmp_reg_reg(code, REG_RDX, REG_RAX);
163+ int bad2 = emit_jcc_rel32(code, COND_GT);
164+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
165+ int bad3 = emit_jcc_rel32(code, COND_GT);
166+ int ok = emit_jmp_rel32(code);
167+ emit_patch_jump(code, bad1);
168+ emit_patch_jump(code, bad2);
169+ emit_patch_jump(code, bad3);
170+ errors_emit_die(code, "runtime error: slice bounds out of range");
171+ emit_patch_jump(code, ok);
172+
173+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // n
174+ emit_push_reg(code, REG_RSI);
175+ emit_push_reg(code, REG_RCX); // from
176+ emit_push_reg(code, REG_RDX); // n
177+
178+ emit_mov_reg_reg(code, REG_RDI, REG_RDX);
179+ emit_mov_reg_imm64(code, REG_RAX, 16);
180+ emit_imul_reg_reg(code, REG_RDI, REG_RAX);
181+ emit_add_reg_imm8(code, REG_RDI, 16); // room for one spare slot (n may be 0)
182+ heap_emit_alloc(code); // RAX = data block
183+ emit_push_reg(code, REG_RAX);
184+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
185+ heap_emit_alloc(code); // RAX = array object
186+
187+ emit_pop_reg(code, REG_RBX); // data block
188+ emit_pop_reg(code, REG_RCX); // n
189+ emit_pop_reg(code, REG_RDX); // from
190+ emit_pop_reg(code, REG_RSI); // source array
191+
192+ emit_mov_reg_reg(code, REG_RDI, REG_RCX);
193+ emit_add_reg_imm8(code, REG_RDI, 1);
194+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RDI); // capacity = n + 1
195+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count = n
196+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RBX); // data_ptr
197+
198+ emit_load_mem_disp32(code, REG_RSI, REG_RSI, 16); // source data_ptr
199+ emit_mov_reg_imm64(code, REG_RDI, 16);
200+ emit_imul_reg_reg(code, REG_RDX, REG_RDI);
201+ emit_add_reg_reg(code, REG_RSI, REG_RDX); // &source[from]
202+ emit_mov_reg_reg(code, REG_RDI, REG_RBX); // dest cursor
203+
204+ int loop = code->count;
205+ emit_cmp_reg_imm32(code, REG_RCX, 0);
206+ int done = emit_jcc_rel32(code, COND_E);
207+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 0); emit_store_mem_disp32(code, REG_RDI, 0, REG_RDX);
208+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); emit_store_mem_disp32(code, REG_RDI, 8, REG_RDX);
209+ emit_add_reg_imm8(code, REG_RSI, 16);
210+ emit_add_reg_imm8(code, REG_RDI, 16);
211+ emit_dec_reg(code, REG_RCX);
212+ emit_jmp_back(code, loop);
213+ emit_patch_jump(code, done);
214+
215+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
216+ emit_push_reg(code, REG_RBX);
217+ emit_push_reg(code, REG_RAX);
218+}

kiln/src/codegen/C/collections/arrays.cmodified

+10−4
@@ -72,7 +72,7 @@ void codegen_array_index_read(void) {
7272 emit_pop_reg(code, REG_RSI); // array object address
7373 emit_pop_reg(code, REG_RBX); // array tag (assumed ARRAY)
7474
75- bounds_check("runtime error: array index out of bounds"); // RCX=index, RDX=data_ptr
75+ bounds_check("index out of bounds"); // RCX=index, RDX=data_ptr
7676
7777 // element address = data_ptr + RCX*16
7878 emit_mov_reg_imm64(code, REG_RAX, 16);
@@ -98,7 +98,7 @@ void codegen_array_index_store(void) {
9898 emit_push_reg(code, REG_RAX); // value payload
9999 emit_mov_reg_reg(code, REG_RAX, REG_RCX); // index payload -> RAX, where bounds_check expects it
100100
101- bounds_check("runtime error: array index out of bounds"); // RCX=index, RDX=data_ptr
101+ bounds_check("index out of bounds"); // RCX=index, RDX=data_ptr
102102
103103 emit_mov_reg_imm64(code, REG_RAX, 16);
104104 emit_imul_reg_reg(code, REG_RCX, REG_RAX);
@@ -129,8 +129,14 @@ void codegen_builtin_push(void) {
129129 emit_cmp_reg_reg(code, REG_RDX, REG_RAX);
130130 int has_room = emit_jcc_rel32(code, COND_LT);
131131
132- // --- grow: new data block sized capacity+4, copy the old elements over ---
133- emit_add_reg_imm8(code, REG_RAX, 4); // RAX = new_capacity
132+ // --- grow: new data block of double the capacity (at least 4 slots), copy the
133+ // old elements over. Doubling keeps total allocation linear in the final
134+ // size; the bump allocator never frees the old block. ---
135+ emit_add_reg_reg(code, REG_RAX, REG_RAX); // RAX = new_capacity = 2 * capacity
136+ emit_cmp_reg_imm32(code, REG_RAX, 4);
137+ int big_enough = emit_jcc_rel32(code, COND_GE);
138+ emit_mov_reg_imm64(code, REG_RAX, 4);
139+ emit_patch_jump(code, big_enough);
134140 emit_mov_reg_reg(code, REG_RDI, REG_RAX);
135141 emit_mov_reg_imm64(code, REG_RBX, 16);
136142 emit_imul_reg_reg(code, REG_RDI, REG_RBX); // RDI = new data block size

kiln/src/codegen/C/collections/maps_mutate.cmodified

+22−8
@@ -1,4 +1,5 @@
11 #include "codegen/H/collections/maps.h"
2+#include "codegen/H/collections/arrays.h"
23 #include "codegen/H/collections/maps_internal.h"
34 #include "codegen/H/emit/emit_internal.h"
45 #include "codegen/H/emit/value.h"
@@ -9,7 +10,7 @@
910 static void push_number_local(double v) {
1011 uint64_t bits;
1112 __builtin_memcpy(&bits, &v, sizeof(bits));
12- emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
13+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
1314 emit_push_reg(code, REG_RBX);
1415 emit_mov_reg_imm64(code, REG_RAX, bits);
1516 emit_push_reg(code, REG_RAX);
@@ -128,15 +129,20 @@ void codegen_builtin_delete(void) {
128129 emit_pop_reg(code, REG_RDI); // key payload
129130 emit_pop_reg(code, REG_RBX); // key tag (ignored)
130131 emit_pop_reg(code, REG_RSI); // map object
131- emit_pop_reg(code, REG_RBX); // map tag (ignored)
132+ emit_pop_reg(code, REG_RBX); // map tag
133+
134+ emit_cmp_reg_imm32(code, REG_RBX, TAG_ARRAY);
135+ int is_map = emit_jcc_rel32(code, COND_NE);
136+ codegen_array_delete_index(); // delete(array, index)
137+ int array_done = emit_jmp_rel32(code);
138+ emit_patch_jump(code, is_map);
132139
133140 emit_sub_reg_imm8(code, REG_RSP, 8);
134141 emit_mov_reg_reg(code, REG_RCX, REG_RSP);
135142 emit_store_mem_disp32(code, REG_RCX, 0, REG_RSI);
136143
137144 int not_found = find_entry_index();
138- // found: RBX = entry addr to remove -- overwrite it with the last
139- // entry's data (order doesn't matter for a map), then count--
145+ // found: RBX = entry addr to remove -- close the gap, then count--
140146 emit_mov_reg_reg(code, REG_RCX, REG_RSP);
141147 emit_load_mem_disp32(code, REG_RSI, REG_RCX, 0);
142148 emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
@@ -147,10 +153,17 @@ void codegen_builtin_delete(void) {
147153 emit_imul_reg_reg(code, REG_RCX, REG_RAX);
148154 emit_add_reg_reg(code, REG_RCX, REG_RDX); // RCX = last entry's address
149155
150- emit_load_mem_disp32(code, REG_RAX, REG_RCX, 0); emit_store_mem_disp32(code, REG_RBX, 0, REG_RAX);
151- emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8); emit_store_mem_disp32(code, REG_RBX, 8, REG_RAX);
152- emit_load_mem_disp32(code, REG_RAX, REG_RCX, 16); emit_store_mem_disp32(code, REG_RBX, 16, REG_RAX);
153- emit_load_mem_disp32(code, REG_RAX, REG_RCX, 24); emit_store_mem_disp32(code, REG_RBX, 24, REG_RAX);
156+ // shift every later entry down one slot, so the survivors keep their order
157+ int shift = code->count;
158+ emit_cmp_reg_reg(code, REG_RBX, REG_RCX);
159+ int shifted = emit_jcc_rel32(code, COND_GE);
160+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 32); emit_store_mem_disp32(code, REG_RBX, 0, REG_RAX);
161+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 40); emit_store_mem_disp32(code, REG_RBX, 8, REG_RAX);
162+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 48); emit_store_mem_disp32(code, REG_RBX, 16, REG_RAX);
163+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 56); emit_store_mem_disp32(code, REG_RBX, 24, REG_RAX);
164+ emit_add_reg_imm8(code, REG_RBX, MAP_ENTRY_SIZE);
165+ emit_jmp_back(code, shift);
166+ emit_patch_jump(code, shifted);
154167
155168 emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
156169 emit_sub_reg_imm8(code, REG_RAX, 1);
@@ -173,4 +186,5 @@ void codegen_builtin_delete(void) {
173186 emit_push_reg(code, REG_RSI);
174187
175188 emit_patch_jump(code, done);
189+ emit_patch_jump(code, array_done);
176190 }

kiln/src/codegen/C/emit/emit_mem.cmodified

+7−0
@@ -85,6 +85,13 @@ void emit_call_back(CodeBuf *buf, int target_offset) {
8585 emit_i32(buf, rel);
8686 }
8787
88+int emit_call_rel32(CodeBuf *buf) {
89+ emit_byte(buf, 0xE8);
90+ int patch_offset = buf->count;
91+ emit_i32(buf, 0);
92+ return patch_offset;
93+}
94+
8895 void emit_ret(CodeBuf *buf) {
8996 emit_byte(buf, 0xC3);
9097 }

kiln/src/codegen/C/emit/layout.cmodified

+6−0
@@ -37,6 +37,12 @@ uint64_t kiln_try_depth_addr(void) {
3737 return KILN_TRY_DEPTH_ADDR;
3838 }
3939
40+uint64_t kiln_modstate_addr(void) {
41+ if (kiln_get_target() == KILN_TARGET_WINDOWS) return KILN_PE_MODSTATE_ADDR;
42+ if (is_linux_shared()) return KILN_DYN_MODSTATE_ADDR;
43+ return KILN_MODSTATE_ADDR;
44+}
45+
4046 uint64_t kiln_try_handlers_addr(void) {
4147 if (kiln_get_target() == KILN_TARGET_WINDOWS) return KILN_PE_TRY_HANDLERS_ADDR;
4248 if (is_linux_shared()) return KILN_DYN_GLOBALS_ADDR + 8 + KILN_TRY_DEPTH_SIZE;

kiln/src/codegen/C/expressions/expr.cmodified

+89−17
@@ -11,8 +11,10 @@
1111 #include "codegen/H/collections/maps.h"
1212 #include "codegen/H/builtins/builtins.h"
1313 #include "codegen/H/functions/closures.h"
14+#include "codegen/H/runtime/errors.h"
1415 #include "parser/H/core/parser.h"
1516 #include "parser/H/declarations/vars.h"
17+#include "parser/H/declarations/module_state.h"
1618 #include "parser/H/declarations/functions.h"
1719 #include "parser/H/declarations/constants.h"
1820
@@ -37,6 +39,33 @@
3739 // milestone, not silently pretended-away here).
3840
3941 static void power(void);
42+static void emit_index_read(void);
43+
44+// A builtin name used as a value (`map(xs, str)`, `local f = len;`) becomes a
45+// small wrapper lambda `forge (a0, ...) { yield name(a0, ...); }`, compiled by
46+// pointing the lexer at a synthesized snippet. `current` is the token after
47+// the name; everything is restored afterwards. Returns 0 if `id` isn't a builtin.
48+static int codegen_builtin_value(Token id) {
49+ int arity = codegen_builtin_arity(id.start, id.length);
50+ if (arity < 0) return 0;
51+
52+ char *snippet = malloc(64 + 2 * (size_t)id.length + 12 * (size_t)arity); // kept: tokens point into it
53+ int n = sprintf(snippet, "forge (");
54+ for (int i = 0; i < arity; i++) n += sprintf(snippet + n, "%s__%d", i ? ", " : "", i);
55+ n += sprintf(snippet + n, ") { yield %.*s(", id.length, id.start);
56+ for (int i = 0; i < arity; i++) n += sprintf(snippet + n, "%s__%d", i ? ", " : "", i);
57+ sprintf(snippet + n, "); }");
58+
59+ Token saved_current = current, saved_previous = previous;
60+ LexerState saved_lexer = lexer_save_state();
61+ lexer_init(snippet);
62+ advance_token(); // current = 'forge'
63+ codegen_lambda_expr();
64+ lexer_restore_state(saved_lexer);
65+ current = saved_current;
66+ previous = saved_previous;
67+ return 1;
68+}
4069
4170 static void primary(void) {
4271 if (current.type == TOKEN_NUMBER) {
@@ -77,7 +106,7 @@ static void primary(void) {
77106 advance_token();
78107 uint64_t bits;
79108 memcpy(&bits, &v, sizeof(bits));
80- emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
109+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
81110 emit_push_reg(code, REG_RBX);
82111 emit_mov_reg_imm64(code, REG_RAX, bits);
83112 emit_push_reg(code, REG_RAX);
@@ -95,6 +124,24 @@ static void primary(void) {
95124 Token id = current;
96125 advance_token();
97126
127+ // `var.field` on a variable (not a namespace) is var["field"]; on an
128+ // error string it is the string itself, so `e.message` works for built-in
129+ // errors and for raised maps alike.
130+ if (current.type == TOKEN_DOT && resolve_var(id.start, id.length) != -1) {
131+ int slot = resolve_var(id.start, id.length);
132+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(slot));
133+ emit_push_reg(code, REG_RBX);
134+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
135+ emit_push_reg(code, REG_RAX);
136+ while (current.type == TOKEN_DOT) {
137+ advance_token();
138+ expect(TOKEN_IDENTIFIER, "expected field name after '.'");
139+ codegen_string_literal_bytes(previous.start, previous.length);
140+ emit_index_read();
141+ }
142+ return;
143+ }
144+
98145 if (current.type == TOKEN_DOT) {
99146 advance_token();
100147 expect(TOKEN_IDENTIFIER, "expected name after '.'");
@@ -112,6 +159,8 @@ static void primary(void) {
112159
113160 KilnConstant *k = resolve_constant(combined, combined_len);
114161 if (k != NULL) { codegen_constant_value(k); return; }
162+ int state = module_state_resolve_qualified(combined, combined_len);
163+ if (state != -1) { module_state_emit_load(state); return; }
115164
116165 parse_error("undefined namespaced function or constant");
117166 }
@@ -154,6 +203,10 @@ static void primary(void) {
154203 KilnConstant *k = resolve_constant(combined, combined_len);
155204 if (k != NULL) { codegen_constant_value(k); return; }
156205 }
206+ int state = module_state_resolve(id.start, id.length);
207+ if (state != -1) { module_state_emit_load(state); return; }
208+ if (codegen_builtin_value(id)) return;
209+
157210 char msg[128];
158211 snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
159212 parse_error(msg);
@@ -181,13 +234,10 @@ static void primary(void) {
181234 // the same order, and dispatches. A while loop (not a single check) so
182235 // chains like `arr[0][1]` work, matching ashvm/src/compiler/expr.c's
183236 // postfix() shape.
184-static void postfix(void) {
185- primary();
186- while (current.type == TOKEN_LBRACKET) {
187- advance_token();
188- codegen_expression();
189- expect(TOKEN_RBRACKET, "expected ']' after index");
190-
237+// Both the base and the index are already pushed (base deeper). Pops both and
238+// pushes base[index] for an array, map or string. A string indexed by a string
239+// is an error value's `.message`: it yields the string itself.
240+static void emit_index_read(void) {
191241 emit_pop_reg(code, REG_RAX); // index payload
192242 emit_pop_reg(code, REG_RBX); // index tag
193243 emit_pop_reg(code, REG_RCX); // base payload
@@ -206,9 +256,24 @@ static void postfix(void) {
206256 codegen_map_index_read();
207257 int done2 = emit_jmp_rel32(code);
208258 emit_patch_jump(code, is_string);
259+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING); // string[string]: the value itself
260+ int by_number = emit_jcc_rel32(code, COND_NE);
261+ emit_add_reg_imm8(code, REG_RSP, 16);
262+ int done3 = emit_jmp_rel32(code);
263+ emit_patch_jump(code, by_number);
209264 codegen_string_index_read();
210265 emit_patch_jump(code, done);
211266 emit_patch_jump(code, done2);
267+ emit_patch_jump(code, done3);
268+}
269+
270+static void postfix(void) {
271+ primary();
272+ while (current.type == TOKEN_LBRACKET) {
273+ advance_token();
274+ codegen_expression();
275+ expect(TOKEN_RBRACKET, "expected ']' after index");
276+ emit_index_read();
212277 }
213278 }
214279
@@ -222,26 +287,22 @@ static void unary(void) {
222287 if (current.type == TOKEN_BANG) {
223288 advance_token();
224289 unary();
225- emit_pop_reg(code, REG_RAX); // payload
226- emit_pop_reg(code, REG_RBX); // tag (ignored)
227- emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
228- emit_pxor_xmm_xmm(code, XMM1);
229- emit_ucomisd(code, XMM0, XMM1);
290+ codegen_pop_and_test_truthy();
230291 int is_zero = emit_jcc_rel32(code, COND_E);
231- emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
292+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
232293 emit_push_reg(code, REG_RBX);
233294 emit_pxor_xmm_xmm(code, XMM0);
234295 emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
235- emit_push_reg(code, REG_RAX); // nonzero operand -> 0.0
296+ emit_push_reg(code, REG_RAX); // truthy operand -> no
236297 int done = emit_jmp_rel32(code);
237298 emit_patch_jump(code, is_zero);
238- emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
299+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
239300 emit_push_reg(code, REG_RBX);
240301 double one = 1.0;
241302 uint64_t one_bits;
242303 memcpy(&one_bits, &one, sizeof(one_bits));
243304 emit_mov_reg_imm64(code, REG_RAX, one_bits);
244- emit_push_reg(code, REG_RAX); // zero operand -> 1.0
305+ emit_push_reg(code, REG_RAX); // falsy operand -> yes
245306 emit_patch_jump(code, done);
246307 return;
247308 }
@@ -400,6 +461,17 @@ void codegen_apply_plus(void) {
400461 int done = emit_jmp_rel32(code);
401462 emit_patch_jump(code, not_string);
402463 emit_patch_jump(code, not_string2);
464+ // A string on exactly one side is a type error (as on ashvm), not a
465+ // numeric add of a heap pointer.
466+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
467+ int a_is_string = emit_jcc_rel32(code, COND_E);
468+ emit_cmp_reg_imm32(code, REG_RDX, TAG_STRING);
469+ int b_is_string = emit_jcc_rel32(code, COND_E);
470+ int numeric = emit_jmp_rel32(code);
471+ emit_patch_jump(code, a_is_string);
472+ emit_patch_jump(code, b_is_string);
473+ errors_emit_die(code, "runtime error: invalid operand type in '+'");
474+ emit_patch_jump(code, numeric);
403475 emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
404476 emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
405477 emit_addsd(code, XMM0, XMM1);

kiln/src/codegen/C/expressions/expr_bool.cmodified

+30−15
@@ -5,6 +5,7 @@
55 #include "codegen/H/emit/emit_sse.h"
66 #include "codegen/H/emit/value.h"
77 #include "codegen/H/strings/strings.h"
8+#include "codegen/H/runtime/errors.h"
89 #include "parser/H/core/parser.h"
910
1011 // Precedence climbing continued from expr.c: comparison -> logical_and ->
@@ -14,7 +15,7 @@
1415
1516 // Pushes a literal double 0.0 or 1.0, tagged NUMBER.
1617 static void push_number_literal(double v) {
17- emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
18+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
1819 emit_push_reg(code, REG_RBX);
1920 if (v == 0.0) {
2021 emit_pxor_xmm_xmm(code, XMM0);
@@ -43,11 +44,19 @@ static void push_bool(Cond cc_if_true) {
4344 }
4445
4546 // Pops the top-of-stack value and sets flags as if its payload were
46-// compared against 0.0 (ucomisd), for truthiness tests -- assumes
47-// NUMBER, matching ashvm's own truthy(), which requires a number too.
48-static void pop_and_test_truthy(void) {
49- emit_pop_reg(code, REG_RAX); // payload
50- emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
47+// compared against 0.0 (ucomisd), for truthiness tests: COND_E afterward
48+// means falsy. Numbers, booleans and none are testable (their payload is
49+// 0.0 when falsy); strings, arrays, maps and functions are a type error, as
50+// on ashvm.
51+void codegen_pop_and_test_truthy(void) {
52+ emit_pop_reg(code, REG_RAX); // payload
53+ emit_pop_reg(code, REG_RBX); // tag
54+ emit_mov_reg_reg(code, REG_RCX, REG_RBX);
55+ emit_sub_reg_imm8(code, REG_RCX, 1);
56+ emit_cmp_reg_imm32(code, REG_RCX, 4); // unsigned: tags 1..4 (STRING..FUNCTION) fall below 4
57+ int testable = emit_jcc_rel32(code, COND_AE);
58+ errors_emit_die(code, "runtime error: invalid operand type in condition");
59+ emit_patch_jump(code, testable);
5160 emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
5261 emit_pxor_xmm_xmm(code, XMM1);
5362 emit_ucomisd(code, XMM0, XMM1);
@@ -90,9 +99,19 @@ static void comparison(void) {
9099 emit_patch_jump(code, done);
91100 emit_patch_jump(code, done2);
92101 } else {
102+ // Two strings order bytewise; anything else is compared as numbers.
103+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
104+ int a_not_string = emit_jcc_rel32(code, COND_NE);
105+ emit_cmp_reg_imm32(code, REG_RDX, TAG_STRING);
106+ int b_not_string = emit_jcc_rel32(code, COND_NE);
107+ codegen_string_order();
108+ int flags_ready = emit_jmp_rel32(code);
109+ emit_patch_jump(code, a_not_string);
110+ emit_patch_jump(code, b_not_string);
93111 emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
94112 emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
95113 emit_ucomisd(code, XMM0, XMM1);
114+ emit_patch_jump(code, flags_ready);
96115 switch (op) {
97116 case TOKEN_LESS: push_bool(COND_B); break;
98117 case TOKEN_LESS_EQUAL: push_bool(COND_BE); break;
@@ -151,10 +170,10 @@ static void logical_and(void) {
151170 bitwise();
152171 while (current.type == TOKEN_AND) {
153172 advance_token();
154- pop_and_test_truthy();
173+ codegen_pop_and_test_truthy();
155174 int to_false = emit_jcc_rel32(code, COND_E);
156175 bitwise();
157- pop_and_test_truthy();
176+ codegen_pop_and_test_truthy();
158177 int to_false2 = emit_jcc_rel32(code, COND_E);
159178 push_number_literal(1.0);
160179 int to_end = emit_jmp_rel32(code);
@@ -171,10 +190,10 @@ static void logical_or(void) {
171190 logical_and();
172191 while (current.type == TOKEN_OR) {
173192 advance_token();
174- pop_and_test_truthy();
193+ codegen_pop_and_test_truthy();
175194 int to_true = emit_jcc_rel32(code, COND_NE);
176195 logical_and();
177- pop_and_test_truthy();
196+ codegen_pop_and_test_truthy();
178197 int to_true2 = emit_jcc_rel32(code, COND_NE);
179198 push_number_literal(0.0);
180199 int to_end = emit_jmp_rel32(code);
@@ -194,11 +213,7 @@ void codegen_expression(void) {
194213 logical_or();
195214 if (current.type == TOKEN_QUESTION) {
196215 advance_token();
197- emit_pop_reg(code, REG_RAX); // condition payload
198- emit_pop_reg(code, REG_RBX); // condition tag (assumed NUMBER)
199- emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
200- emit_pxor_xmm_xmm(code, XMM1);
201- emit_ucomisd(code, XMM0, XMM1);
216+ codegen_pop_and_test_truthy();
202217 int else_jump = emit_jcc_rel32(code, COND_E);
203218
204219 codegen_expression(); // true branch

kiln/src/codegen/C/expressions/expr_call.cmodified

+4−2
@@ -39,7 +39,8 @@ void codegen_call(Token id) {
3939 snprintf(msg, sizeof(msg), "function %.*s expected %d args, got %d", id.length, id.start, fn->arity, argc);
4040 parse_error(msg);
4141 }
42- emit_call_back(code, fn->code_offset);
42+ if (fn->defined) emit_call_back(code, fn->code_offset);
43+ else function_add_call_fixup(fn, emit_call_rel32(code));
4344 if (argc > 0) emit_add_reg_imm32(code, REG_RSP, 16 * argc); // caller cleans up pushed args (each now 16 bytes)
4445 emit_push_reg(code, REG_RBX); // return convention: RBX=tag, RAX=payload
4546 emit_push_reg(code, REG_RAX);
@@ -67,7 +68,8 @@ void codegen_call_named(const char *name, int len) {
6768 snprintf(msg, sizeof(msg), "function %.*s expected %d args, got %d", len, name, fn->arity, argc);
6869 parse_error(msg);
6970 }
70- emit_call_back(code, fn->code_offset);
71+ if (fn->defined) emit_call_back(code, fn->code_offset);
72+ else function_add_call_fixup(fn, emit_call_rel32(code));
7173 if (argc > 0) emit_add_reg_imm32(code, REG_RSP, 16 * argc);
7274 emit_push_reg(code, REG_RBX);
7375 emit_push_reg(code, REG_RAX);

kiln/src/codegen/C/functions/closures.cmodified

+5−2
@@ -36,8 +36,11 @@ void closures_push_value(void) {
3636
3737 void codegen_named_function_value(KilnFunction *fn) {
3838 closures_alloc_object(0);
39- uint64_t addr = kiln_code_base() + (uint64_t)fn->code_offset;
40- emit_mov_reg_imm64(code, REG_RCX, addr);
39+ if (fn->defined) {
40+ emit_mov_reg_imm64(code, REG_RCX, kiln_code_base() + (uint64_t)fn->code_offset);
41+ } else {
42+ function_add_abs_fixup(fn, emit_mov_reg_imm64_patchable(code, REG_RCX)); // filled in when the body is compiled
43+ }
4144 emit_store_mem_disp32(code, REG_RBX, 0, REG_RCX);
4245 closures_push_value();
4346 }

kiln/src/codegen/C/functions/lambda.cmodified

+4−4
@@ -46,9 +46,9 @@ void codegen_lambda_expr(void) {
4646 int outer_each_depth;
4747 each_depth_save(&outer_each_depth);
4848 each_depth_reset();
49- int outer_loop_depth;
50- loop_depth_save(&outer_loop_depth);
51- loop_depth_reset();
49+ LoopScope outer_loop_depth;
50+ loop_scope_save(&outer_loop_depth);
51+ loop_scope_reset();
5252 for (int i = 0; i < capture_count; i++) {
5353 declare_var(outer_vars.names[i], (int)strlen(outer_vars.names[i]));
5454 }
@@ -103,7 +103,7 @@ void codegen_lambda_expr(void) {
103103
104104 vars_restore(&outer_vars);
105105 each_depth_restore(outer_each_depth);
106- loop_depth_restore(outer_loop_depth);
106+ loop_scope_restore(outer_loop_depth);
107107 emit_patch_jump(code, skip_jump);
108108
109109 uint64_t addr = kiln_code_base() + (uint64_t)lambda_code_offset;

kiln/src/codegen/C/io/fs_builtins.cadded

+299−0
@@ -0,0 +1,299 @@
1+#include <stdint.h>
2+#include "codegen/H/io/file_builtins.h"
3+#include "codegen/H/io/file_path.h"
4+#include "codegen/H/expressions/expr.h"
5+#include "codegen/H/emit/emit_sse.h"
6+#include "codegen/H/emit/value.h"
7+#include "codegen/H/emit/bytes.h"
8+#include "codegen/H/collections/arrays.h"
9+#include "codegen/H/platform/platform_file.h"
10+#include "codegen/H/runtime/errors.h"
11+#include "codegen/H/runtime/heap.h"
12+#include "codegen/H/strings/string_alloc.h"
13+#include "app/H/target.h"
14+#include "codegen/H/platform/win_call.h"
15+#include "parser/H/core/parser.h"
16+#include "parser/H/declarations/vars.h"
17+
18+// rename_file / delete_file / make_dir / list_dir. The first three return 1 on
19+// success and 0 on failure (like write_file); list_dir raises when the
20+// directory cannot be opened.
21+
22+// RAX = status (0 = success) -> pushes the NUMBER 1.0 / 0.0.
23+static void push_status_as_number(void) {
24+ emit_cmp_reg_imm32(code, REG_RAX, 0);
25+ int failed = emit_jcc_rel32(code, COND_NE);
26+ emit_mov_reg_imm64(code, REG_RAX, 0x3FF0000000000000ull); // 1.0
27+ int done = emit_jmp_rel32(code);
28+ emit_patch_jump(code, failed);
29+ emit_mov_reg_imm64(code, REG_RAX, 0); // 0.0
30+ emit_patch_jump(code, done);
31+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
32+ emit_push_reg(code, REG_RBX);
33+ emit_push_reg(code, REG_RAX);
34+}
35+
36+// Pops a string and leaves its null-terminated copy's address in RAX.
37+static void pop_path_nullterm(void) {
38+ emit_pop_reg(code, REG_RSI);
39+ emit_pop_reg(code, REG_RBX); // tag (ignored)
40+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8);
41+ filepath_emit_nullterm(code);
42+}
43+
44+void codegen_builtin_rename_file(void) {
45+ codegen_expression(); // from
46+ expect(TOKEN_COMMA, "expected ',' after first path");
47+ codegen_expression(); // to
48+ expect(TOKEN_RPAREN, "expected ')' after arguments");
49+ int32_t base = higher_order_scratch_offset();
50+ pop_path_nullterm();
51+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX); // to
52+ pop_path_nullterm();
53+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
54+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 24);
55+ platform_emit_rename(code);
56+ push_status_as_number();
57+}
58+
59+void codegen_builtin_delete_file(void) {
60+ codegen_expression();
61+ expect(TOKEN_RPAREN, "expected ')' after argument");
62+ pop_path_nullterm();
63+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
64+ platform_emit_delete(code);
65+ push_status_as_number();
66+}
67+
68+void codegen_builtin_make_dir(void) {
69+ codegen_expression();
70+ expect(TOKEN_RPAREN, "expected ')' after argument");
71+ pop_path_nullterm();
72+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
73+ platform_emit_mkdir(code);
74+ push_status_as_number();
75+}
76+
77+// list_dir(path): array of entry names (no "." / ".."), in directory order.
78+// Linux walks getdents64 records; Windows walks FindFirstFileA / FindNextFileA.
79+//
80+// Scratch: [24]=fd/find handle [32]=dirent / WIN32_FIND_DATA buffer
81+// [40]=bytes read [48]=offset [56]=result array object
82+// [64]=pattern buffer [72]=path length (Windows)
83+
84+// An empty result array in [base + 56].
85+static void emit_new_result_array(int32_t base) {
86+ emit_mov_reg_imm64(code, REG_RDI, 16);
87+ heap_emit_alloc(code);
88+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // (temp) data block
89+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
90+ heap_emit_alloc(code);
91+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32);
92+ emit_mov_reg_imm64(code, REG_RCX, 0);
93+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
94+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX);
95+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
96+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX);
97+}
98+
99+// RSI = a NUL-terminated entry name. Appends a copy to the result array unless
100+// it is "." or "..". Clobbers every register.
101+static void emit_add_entry(int32_t base) {
102+ emit_load_byte_reg(code, REG_RAX, REG_RSI);
103+ emit_cmp_reg_imm32(code, REG_RAX, '.');
104+ int has_name = emit_jcc_rel32(code, COND_NE);
105+ emit_add_reg_imm8(code, REG_RSI, 1);
106+ emit_load_byte_reg(code, REG_RAX, REG_RSI);
107+ emit_sub_reg_imm8(code, REG_RSI, 1);
108+ emit_cmp_reg_imm32(code, REG_RAX, 0);
109+ int skip_dot = emit_jcc_rel32(code, COND_E);
110+ emit_cmp_reg_imm32(code, REG_RAX, '.');
111+ int has_name2 = emit_jcc_rel32(code, COND_NE);
112+ emit_add_reg_imm8(code, REG_RSI, 2);
113+ emit_load_byte_reg(code, REG_RAX, REG_RSI);
114+ emit_sub_reg_imm8(code, REG_RSI, 2);
115+ emit_cmp_reg_imm32(code, REG_RAX, 0);
116+ int skip_dotdot = emit_jcc_rel32(code, COND_E);
117+ emit_patch_jump(code, has_name);
118+ emit_patch_jump(code, has_name2);
119+
120+ // RDX = strlen(name)
121+ emit_mov_reg_imm64(code, REG_RDX, 0);
122+ int len_loop = code->count;
123+ emit_mov_reg_reg(code, REG_RCX, REG_RSI);
124+ emit_add_reg_reg(code, REG_RCX, REG_RDX);
125+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
126+ emit_cmp_reg_imm32(code, REG_RAX, 0);
127+ int len_done = emit_jcc_rel32(code, COND_E);
128+ emit_add_reg_imm8(code, REG_RDX, 1);
129+ emit_jmp_back(code, len_loop);
130+ emit_patch_jump(code, len_done);
131+
132+ // new string = copy of the name
133+ emit_mov_reg_reg(code, REG_RBX, REG_RSI); // src (survives string_alloc, as in upper())
134+ string_alloc_emit_prefixed(code); // RAX = block, RDX = length
135+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
136+ emit_add_reg_imm8(code, REG_RDI, 8);
137+ emit_push_reg(code, REG_RAX);
138+ bytes_emit_copy(code); // RDI=dest, RBX=src, RDX=count
139+ emit_pop_reg(code, REG_RAX);
140+ emit_add_reg_imm8(code, REG_RAX, 8); // string payload
141+
142+ // push(result, name)
143+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
144+ emit_mov_reg_reg(code, REG_RCX, REG_RAX);
145+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 56);
146+ emit_mov_reg_imm64(code, REG_RDX, TAG_ARRAY);
147+ emit_push_reg(code, REG_RDX);
148+ emit_push_reg(code, REG_RSI);
149+ emit_push_reg(code, REG_RBX);
150+ emit_push_reg(code, REG_RCX);
151+ codegen_builtin_push();
152+ emit_pop_reg(code, REG_RAX); // discard push()'s result
153+ emit_pop_reg(code, REG_RAX);
154+
155+ emit_patch_jump(code, skip_dot);
156+ emit_patch_jump(code, skip_dotdot);
157+}
158+
159+static void emit_push_result_array(int32_t base) {
160+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 56);
161+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
162+ emit_push_reg(code, REG_RBX);
163+ emit_push_reg(code, REG_RAX);
164+}
165+
166+#define WIN32_FIND_DATA_SIZE 320
167+#define WIN32_FIND_DATA_NAME_OFFSET 44
168+
169+// Windows: pattern = path + "\\*", then FindFirstFileA / FindNextFileA / FindClose.
170+static void emit_list_dir_windows(int32_t base) {
171+ emit_pop_reg(code, REG_RSI); // path payload
172+ emit_pop_reg(code, REG_RBX);
173+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8);
174+ emit_store_mem_disp32(code, REG_RBP, base + 72, REG_RDX);
175+ emit_store_mem_disp32(code, REG_RBP, base + 80, REG_RSI);
176+ emit_mov_reg_reg(code, REG_RDI, REG_RDX);
177+ emit_add_reg_imm8(code, REG_RDI, 3); // "\\", "*", NUL
178+ heap_emit_alloc(code);
179+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RAX);
180+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
181+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 80);
182+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 72);
183+ bytes_emit_copy(code);
184+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 64);
185+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 72);
186+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
187+ emit_store_byte_imm(code, REG_RAX, '\\');
188+ emit_add_reg_imm8(code, REG_RAX, 1);
189+ emit_store_byte_imm(code, REG_RAX, '*');
190+ emit_add_reg_imm8(code, REG_RAX, 1);
191+ emit_store_byte_imm(code, REG_RAX, 0);
192+
193+ emit_new_result_array(base);
194+ emit_mov_reg_imm64(code, REG_RDI, WIN32_FIND_DATA_SIZE);
195+ heap_emit_alloc(code);
196+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX);
197+
198+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 64);
199+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
200+ win_call_begin(code, 0);
201+ win_call_arg_reg(code, 0, REG_RSI);
202+ win_call_arg_reg(code, 1, REG_RDI);
203+ win_call_import(code, PE_IMPORT_FIND_FIRST_FILE_A);
204+ win_call_end(code);
205+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
206+ emit_cmp_reg_imm32(code, REG_RAX, -1); // INVALID_HANDLE_VALUE
207+ int opened = emit_jcc_rel32(code, COND_NE);
208+ errors_emit_die(code, "runtime error: cannot open directory");
209+ emit_patch_jump(code, opened);
210+
211+ int next_entry = code->count;
212+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32);
213+ emit_add_reg_imm8(code, REG_RSI, WIN32_FIND_DATA_NAME_OFFSET);
214+ emit_add_entry(base);
215+
216+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 24);
217+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 32);
218+ win_call_begin(code, 0);
219+ win_call_arg_reg(code, 0, REG_RSI);
220+ win_call_arg_reg(code, 1, REG_RDI);
221+ win_call_import(code, PE_IMPORT_FIND_NEXT_FILE_A);
222+ win_call_end(code);
223+ emit_cmp_reg_imm32(code, REG_RAX, 0);
224+ emit_jcc_back(code, COND_NE, next_entry);
225+
226+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 24);
227+ win_call_begin(code, 0);
228+ win_call_arg_reg(code, 0, REG_RSI);
229+ win_call_import(code, PE_IMPORT_FIND_CLOSE);
230+ win_call_end(code);
231+ emit_push_result_array(base);
232+}
233+
234+void codegen_builtin_list_dir(void) {
235+ codegen_expression();
236+ expect(TOKEN_RPAREN, "expected ')' after argument");
237+ int32_t base = higher_order_scratch_offset();
238+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
239+ emit_list_dir_windows(base);
240+ return;
241+ }
242+ pop_path_nullterm();
243+
244+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
245+ emit_mov_reg_imm64(code, REG_RSI, 0x10000); // O_RDONLY | O_DIRECTORY
246+ emit_mov_reg_imm64(code, REG_RDX, 0);
247+ emit_mov_reg_imm64(code, REG_RAX, 2); // syscall: open
248+ emit_syscall(code);
249+ emit_cmp_reg_imm32(code, REG_RAX, 0);
250+ int opened = emit_jcc_rel32(code, COND_GE);
251+ errors_emit_die(code, "runtime error: cannot open directory");
252+ emit_patch_jump(code, opened);
253+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
254+
255+ emit_new_result_array(base);
256+
257+ emit_mov_reg_imm64(code, REG_RDI, 4096);
258+ heap_emit_alloc(code);
259+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // dirent buffer
260+
261+ int fill = code->count;
262+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 24);
263+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32);
264+ emit_mov_reg_imm64(code, REG_RDX, 4096);
265+ emit_mov_reg_imm64(code, REG_RAX, 217); // syscall: getdents64
266+ emit_syscall(code);
267+ emit_cmp_reg_imm32(code, REG_RAX, 0);
268+ int eof = emit_jcc_rel32(code, COND_LE);
269+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX);
270+ emit_mov_reg_imm64(code, REG_RCX, 0);
271+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RCX);
272+
273+ int next_entry = code->count;
274+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 48);
275+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40);
276+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
277+ int buffer_done = emit_jcc_rel32(code, COND_GE);
278+
279+ // RSI = entry; advance the stored offset by d_reclen (u16 at +16) up front
280+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32);
281+ emit_add_reg_reg(code, REG_RSI, REG_RCX);
282+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 16);
283+ emit_mov_reg_imm64(code, REG_RDX, 0xFFFF);
284+ emit_and_reg_reg(code, REG_RAX, REG_RDX);
285+ emit_add_reg_reg(code, REG_RCX, REG_RAX);
286+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RCX);
287+ emit_add_reg_imm8(code, REG_RSI, 19); // d_name
288+
289+ emit_add_entry(base);
290+ emit_jmp_back(code, next_entry);
291+
292+ emit_patch_jump(code, buffer_done);
293+ emit_jmp_back(code, fill);
294+
295+ emit_patch_jump(code, eof);
296+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 24);
297+ platform_emit_close(code);
298+ emit_push_result_array(base);
299+}

kiln/src/codegen/C/io/write_file_builtins.cmodified

+1−1
@@ -9,7 +9,7 @@
99 #include "parser/H/declarations/vars.h"
1010
1111 static void push_number(double v) {
12- emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
12+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
1313 emit_push_reg(code, REG_RBX);
1414 if (v == 0.0) {
1515 emit_pxor_xmm_xmm(code, XMM0);

kiln/src/codegen/C/platform/platform_file.cmodified

+53−0
@@ -108,3 +108,56 @@ void platform_emit_file_size(CodeBuf *code) {
108108 emit_syscall(code); // rewind (syscall preserves RDI, the fd survives for the caller's next use)
109109 emit_pop_reg(code, REG_RAX);
110110 }
111+
112+// Windows BOOL (nonzero = success) -> the Linux-style "0 = success" contract.
113+static void bool_to_status(CodeBuf *code) {
114+ emit_cmp_reg_imm32(code, REG_RAX, 0);
115+ int failed = emit_jcc_rel32(code, COND_E);
116+ emit_mov_reg_imm64(code, REG_RAX, 0);
117+ int done = emit_jmp_rel32(code);
118+ emit_patch_jump(code, failed);
119+ emit_mov_reg_imm64(code, REG_RAX, 1);
120+ emit_patch_jump(code, done);
121+}
122+
123+void platform_emit_rename(CodeBuf *code) {
124+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
125+ win_call_begin(code, 0);
126+ win_call_arg_reg(code, 0, REG_RDI);
127+ win_call_arg_reg(code, 1, REG_RSI);
128+ win_call_import(code, PE_IMPORT_MOVE_FILE_A);
129+ win_call_end(code);
130+ bool_to_status(code);
131+ return;
132+ }
133+ emit_mov_reg_imm64(code, REG_RAX, 82); // syscall: rename
134+ emit_syscall(code);
135+}
136+
137+void platform_emit_delete(CodeBuf *code) {
138+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
139+ win_call_begin(code, 0);
140+ win_call_arg_reg(code, 0, REG_RDI);
141+ win_call_import(code, PE_IMPORT_DELETE_FILE_A);
142+ win_call_end(code);
143+ bool_to_status(code);
144+ return;
145+ }
146+ emit_mov_reg_imm64(code, REG_RAX, 87); // syscall: unlink
147+ emit_syscall(code);
148+}
149+
150+void platform_emit_mkdir(CodeBuf *code) {
151+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
152+ win_call_begin(code, 0);
153+ win_call_arg_reg(code, 0, REG_RDI);
154+ win_call_arg_imm64(code, 1, 0); // lpSecurityAttributes = NULL
155+ win_call_import(code, PE_IMPORT_CREATE_DIRECTORY_A);
156+ win_call_end(code);
157+ bool_to_status(code);
158+ return;
159+ }
160+ emit_mov_reg_imm64(code, REG_RSI, 493); // mode 0755
161+ emit_mov_reg_imm64(code, REG_RAX, 83); // syscall: mkdir
162+ emit_syscall(code);
163+}

kiln/src/codegen/C/runtime/errors.cmodified

+40−2
@@ -11,6 +11,22 @@
1111
1212 static int raise_routine_offset = -1;
1313
14+// Writes `text` (<= 31 bytes) to stderr via a stack buffer. Clobbers
15+// RAX/RCX/RDX/RSI (and RBX/RDI on Windows), like platform_emit_write_stderr.
16+static void write_stderr_text(CodeBuf *code, const char *text) {
17+ int n = (int)strlen(text);
18+ emit_sub_reg_imm8(code, REG_RSP, 32);
19+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
20+ for (int i = 0; i < n; i++) {
21+ emit_store_byte_imm(code, REG_RCX, (uint8_t)text[i]);
22+ emit_add_reg_imm8(code, REG_RCX, 1);
23+ }
24+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
25+ emit_mov_reg_imm64(code, REG_RDX, (uint64_t)n);
26+ platform_emit_write_stderr(code);
27+ emit_add_reg_imm8(code, REG_RSP, 32);
28+}
29+
1430 static int is_shared(void) {
1531 return kiln_get_link_mode() == KILN_LINK_SHARED && !kiln_is_compiling_so();
1632 }
@@ -36,7 +52,8 @@ static void embed_message(CodeBuf *code, const char *msg, uint64_t *out_addr, in
3652 *out_len = len;
3753 }
3854
39-// Raise routine -- inputs RSI=message addr, RDX=message len. If an `attempt`
55+// Raise routine -- inputs RBX=value tag, RSI=value payload, RDX=message len
56+// (only meaningful when the tag is STRING). If an `attempt`
4057 // is active (kiln_try_depth_addr() > 0), pops the innermost handler,
4158 // restores its saved RSP/RBP (kiln's hand-rolled longjmp -- no libc
4259 // setjmp in a freestanding binary), stores the message as a TAG_STRING
@@ -47,6 +64,7 @@ static void emit_raise_routine(CodeBuf *code) {
4764 int skip = emit_jmp_rel32(code);
4865 raise_routine_offset = code->count;
4966
67+ emit_push_reg(code, REG_RBX); // the raised value's tag, kept until the handler's slot is known
5068 emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());
5169 emit_load_mem_disp32(code, REG_RAX, REG_RAX, 0);
5270 emit_cmp_reg_imm32(code, REG_RAX, 0);
@@ -66,12 +84,12 @@ static void emit_raise_routine(CodeBuf *code) {
6684 emit_load_mem_disp32(code, REG_RDX, REG_RAX, 16); // handle target (clobbers RDX -- fine, msg len only needed on the fatal branch we didn't take)
6785 emit_load_mem_disp32(code, REG_RBX, REG_RAX, 24); // error variable's rbp-relative tag offset
6886
87+ emit_pop_reg(code, REG_RAX); // the raised value's tag
6988 emit_mov_reg_reg(code, REG_RSP, REG_RCX);
7089 emit_mov_reg_reg(code, REG_RBP, REG_RDI);
7190
7291 emit_mov_reg_reg(code, REG_RCX, REG_RBP);
7392 emit_add_reg_reg(code, REG_RCX, REG_RBX); // RCX = error variable's address
74- emit_mov_reg_imm64(code, REG_RAX, TAG_STRING);
7593 emit_store_mem_disp32(code, REG_RCX, 0, REG_RAX); // tag
7694 emit_store_mem_disp32(code, REG_RCX, 8, REG_RSI); // payload = message address
7795
@@ -81,7 +99,21 @@ static void emit_raise_routine(CodeBuf *code) {
8199 // RSI/RDX still hold this call's original message addr/len -- nothing
82100 // between the raise routine's entry and here touches them on the path
83101 // that skips straight past the attempt-active handling.
102+ emit_pop_reg(code, REG_RAX); // tag
103+ emit_push_reg(code, REG_RSI);
104+ emit_push_reg(code, REG_RDX);
105+ emit_push_reg(code, REG_RAX);
106+ write_stderr_text(code, "error: ");
107+ emit_pop_reg(code, REG_RAX);
108+ emit_pop_reg(code, REG_RDX);
109+ emit_pop_reg(code, REG_RSI);
110+ emit_cmp_reg_imm32(code, REG_RAX, TAG_STRING);
111+ int is_text = emit_jcc_rel32(code, COND_E);
112+ write_stderr_text(code, "uncaught non-string value");
113+ int text_done = emit_jmp_rel32(code);
114+ emit_patch_jump(code, is_text);
84115 platform_emit_write_stderr(code);
116+ emit_patch_jump(code, text_done);
85117 emit_sub_reg_imm8(code, REG_RSP, 8);
86118 emit_mov_reg_reg(code, REG_RCX, REG_RSP);
87119 emit_store_byte_imm(code, REG_RCX, '\n');
@@ -121,8 +153,14 @@ static void emit_die(CodeBuf *code) {
121153 }
122154
123155 void errors_emit_die(CodeBuf *code, const char *msg) {
156+ // Call sites spell messages "runtime error: ..."; the caught value is just
157+ // the text after that (identical to ashvm's), and the fatal path adds its own
158+ // "error: " prefix.
159+ static const char prefix[] = "runtime error: ";
160+ if (strncmp(msg, prefix, sizeof(prefix) - 1) == 0) msg += sizeof(prefix) - 1;
124161 uint64_t addr; int len;
125162 embed_message(code, msg, &addr, &len);
163+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
126164 emit_mov_reg_imm64(code, REG_RSI, addr);
127165 emit_mov_reg_imm64(code, REG_RDX, (uint64_t)len);
128166 emit_die(code);

kiln/src/codegen/C/runtime/number_format.cadded

+222−0
@@ -0,0 +1,222 @@
1+#include <string.h>
2+#include "codegen/H/runtime/number_format.h"
3+#include "codegen/H/emit/emit_internal.h"
4+#include "codegen/H/emit/emit_sse.h"
5+
6+static void load_double_const(CodeBuf *code, XReg dst, double v) {
7+ uint64_t bits;
8+ memcpy(&bits, &v, sizeof(bits));
9+ emit_mov_reg_imm64(code, REG_RAX, bits);
10+ emit_movq_xmm_from_reg(code, dst, REG_RAX);
11+}
12+
13+// Writes `text` at [RCX...] advancing RCX, for the NaN/inf literals.
14+static void store_text(CodeBuf *code, const char *text) {
15+ for (const char *p = text; *p; p++) {
16+ emit_store_byte_imm(code, REG_RCX, (uint8_t)*p);
17+ emit_add_reg_imm8(code, REG_RCX, 1);
18+ }
19+}
20+
21+// Scratch layout (relative to RSP after the sub; kiln's emitter has no
22+// [rsp+disp] addressing, so everything is reached through pointer registers):
23+// [0, 32) sign + integer digits, built backward from 32
24+// 32 '.'
25+// [33, 39) up to 6 fraction digits, built forward
26+// 39... exponent text ('e', sign, digits) in scientific mode (<= 6 bytes)
27+// Register roles: RBX = sign, then frac_len; RDI = the decimal exponent in
28+// scientific mode, or NF_PLAIN in plain mode.
29+#define NF_PLAIN 0x7FFFFFFF
30+void number_format_emit(CodeBuf *code) {
31+ emit_sub_reg_imm8(code, REG_RSP, NUMBER_FORMAT_SCRATCH);
32+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
33+
34+ // ---- NaN / infinity: exponent bits all ones ----
35+ emit_mov_reg_imm64(code, REG_RCX, 0x7FF0000000000000ull);
36+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
37+ emit_and_reg_reg(code, REG_RDX, REG_RCX);
38+ emit_cmp_reg_reg(code, REG_RDX, REG_RCX);
39+ int finite = emit_jcc_rel32(code, COND_NE);
40+
41+ emit_mov_reg_imm64(code, REG_RCX, 0x000FFFFFFFFFFFFFull);
42+ emit_and_reg_reg(code, REG_RCX, REG_RAX);
43+ emit_cmp_reg_imm32(code, REG_RCX, 0);
44+ int is_inf = emit_jcc_rel32(code, COND_E);
45+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
46+ store_text(code, "nan");
47+ int special_done_nan = emit_jmp_rel32(code);
48+
49+ emit_patch_jump(code, is_inf);
50+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
51+ emit_pxor_xmm_xmm(code, XMM2);
52+ emit_ucomisd(code, XMM0, XMM2);
53+ int inf_pos = emit_jcc_rel32(code, COND_AE);
54+ store_text(code, "-");
55+ emit_patch_jump(code, inf_pos);
56+ store_text(code, "inf");
57+ emit_patch_jump(code, special_done_nan);
58+ emit_mov_reg_reg(code, REG_RDX, REG_RCX); // end
59+ emit_mov_reg_reg(code, REG_RCX, REG_RSP); // start
60+ int special_done = emit_jmp_rel32(code);
61+
62+ emit_patch_jump(code, finite);
63+
64+ // ---- sign: RBX = 1 if negative, XMM0 = |x| ----
65+ emit_pxor_xmm_xmm(code, XMM2);
66+ emit_ucomisd(code, XMM0, XMM2);
67+ emit_mov_reg_imm64(code, REG_RBX, 0);
68+ int skip_negate = emit_jcc_rel32(code, COND_AE);
69+ emit_subsd(code, XMM2, XMM0);
70+ emit_movsd_xmm_xmm(code, XMM0, XMM2);
71+ emit_mov_reg_imm64(code, REG_RBX, 1);
72+ emit_patch_jump(code, skip_negate);
73+
74+ // ---- plain (0 or 1e-6 <= x < 1e15) vs scientific ----
75+ emit_mov_reg_imm64(code, REG_RDI, NF_PLAIN);
76+ emit_pxor_xmm_xmm(code, XMM2);
77+ emit_ucomisd(code, XMM0, XMM2);
78+ int plain = emit_jcc_rel32(code, COND_E); // zero
79+ load_double_const(code, XMM1, 1e15);
80+ emit_ucomisd(code, XMM0, XMM1);
81+ int sci = emit_jcc_rel32(code, COND_AE);
82+ load_double_const(code, XMM1, 1e-6);
83+ emit_ucomisd(code, XMM0, XMM1);
84+ int sci_small = emit_jcc_rel32(code, COND_B);
85+
86+ emit_patch_jump(code, plain);
87+ // plain: nudge for round-half-up (carry reaches the integer part), then split
88+ load_double_const(code, XMM1, 0.0000005);
89+ emit_addsd(code, XMM0, XMM1);
90+ emit_cvttsd2si(code, REG_RAX, XMM0);
91+ emit_cvtsi2sd(code, XMM1, REG_RAX);
92+ emit_subsd(code, XMM0, XMM1);
93+ int to_digits = emit_jmp_rel32(code);
94+
95+ // scientific: normalize XMM0 into [1,10), counting the exponent in RDI
96+ emit_patch_jump(code, sci);
97+ emit_patch_jump(code, sci_small);
98+ emit_mov_reg_imm64(code, REG_RDI, 0);
99+ load_double_const(code, XMM3, 10.0);
100+ load_double_const(code, XMM1, 1.0);
101+ emit_ucomisd(code, XMM0, XMM1);
102+ int too_big = emit_jcc_rel32(code, COND_AE);
103+ int small_loop = code->count;
104+ emit_ucomisd(code, XMM0, XMM1);
105+ int normalized_small = emit_jcc_rel32(code, COND_AE);
106+ emit_mulsd(code, XMM0, XMM3);
107+ emit_sub_reg_imm8(code, REG_RDI, 1);
108+ emit_jmp_back(code, small_loop);
109+ emit_patch_jump(code, too_big);
110+ int big_loop = code->count;
111+ emit_ucomisd(code, XMM0, XMM3);
112+ int normalized_big = emit_jcc_rel32(code, COND_B);
113+ emit_divsd(code, XMM0, XMM3);
114+ emit_add_reg_imm8(code, REG_RDI, 1);
115+ emit_jmp_back(code, big_loop);
116+ emit_patch_jump(code, normalized_small);
117+ emit_patch_jump(code, normalized_big);
118+ // round the mantissa; 9.9999996 carries to 10 -> 1 and exponent + 1
119+ load_double_const(code, XMM2, 0.0000005);
120+ emit_addsd(code, XMM0, XMM2);
121+ emit_ucomisd(code, XMM0, XMM3);
122+ int no_carry = emit_jcc_rel32(code, COND_B);
123+ emit_movsd_xmm_xmm(code, XMM0, XMM1); // 1.0
124+ emit_add_reg_imm8(code, REG_RDI, 1);
125+ emit_patch_jump(code, no_carry);
126+ emit_cvttsd2si(code, REG_RAX, XMM0);
127+ emit_cvtsi2sd(code, XMM1, REG_RAX);
128+ emit_subsd(code, XMM0, XMM1);
129+
130+ // ---- integer digits (RAX), backward ----
131+ emit_patch_jump(code, to_digits);
132+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
133+ emit_add_reg_imm8(code, REG_RCX, 32);
134+ int loop_start = code->count;
135+ emit_dec_reg(code, REG_RCX);
136+ emit_cqo(code);
137+ emit_mov_reg_imm64(code, REG_RSI, 10);
138+ emit_idiv_reg(code, REG_RSI);
139+ emit_add_reg_imm8(code, REG_RDX, '0');
140+ emit_store_byte_reg(code, REG_RCX, REG_RDX);
141+ emit_cmp_reg_imm32(code, REG_RAX, 0);
142+ emit_jcc_back(code, COND_NE, loop_start);
143+
144+ emit_cmp_reg_imm32(code, REG_RBX, 0);
145+ int skip_sign = emit_jcc_rel32(code, COND_E);
146+ emit_dec_reg(code, REG_RCX);
147+ emit_store_byte_imm(code, REG_RCX, '-');
148+ emit_patch_jump(code, skip_sign);
149+
150+ // ---- fraction digits, forward; RBX = how many are significant ----
151+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
152+ emit_add_reg_imm8(code, REG_RSI, 33);
153+ load_double_const(code, XMM3, 10.0);
154+ emit_mov_reg_imm64(code, REG_RBX, 0);
155+ for (int i = 0; i < 6; i++) {
156+ emit_mulsd(code, XMM0, XMM3);
157+ emit_cvttsd2si(code, REG_RAX, XMM0);
158+ emit_cvtsi2sd(code, XMM1, REG_RAX);
159+ emit_subsd(code, XMM0, XMM1);
160+ emit_cmp_reg_imm32(code, REG_RAX, 0);
161+ int skip_mark = emit_jcc_rel32(code, COND_E);
162+ emit_mov_reg_imm64(code, REG_RBX, (uint64_t)(i + 1));
163+ emit_patch_jump(code, skip_mark);
164+ emit_add_reg_imm8(code, REG_RAX, '0');
165+ emit_store_byte_reg(code, REG_RSI, REG_RAX);
166+ emit_add_reg_imm8(code, REG_RSI, 1);
167+ }
168+
169+ // ---- '.' placement: RSI = end of text ----
170+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
171+ emit_add_reg_imm8(code, REG_RSI, 32);
172+ emit_cmp_reg_imm32(code, REG_RBX, 0);
173+ int whole = emit_jcc_rel32(code, COND_E);
174+ emit_mov_reg_reg(code, REG_RDX, REG_RSP);
175+ emit_add_reg_imm8(code, REG_RDX, 32);
176+ emit_store_byte_imm(code, REG_RDX, '.');
177+ emit_add_reg_imm8(code, REG_RSI, 1);
178+ emit_add_reg_reg(code, REG_RSI, REG_RBX);
179+ emit_patch_jump(code, whole);
180+
181+ // ---- exponent suffix (scientific mode only) ----
182+ emit_cmp_reg_imm32(code, REG_RDI, NF_PLAIN);
183+ int no_exp = emit_jcc_rel32(code, COND_E);
184+ emit_store_byte_imm(code, REG_RSI, 'e');
185+ emit_add_reg_imm8(code, REG_RSI, 1);
186+ emit_mov_reg_reg(code, REG_RAX, REG_RDI);
187+ emit_cmp_reg_imm32(code, REG_RAX, 0);
188+ int exp_neg = emit_jcc_rel32(code, COND_LT);
189+ emit_store_byte_imm(code, REG_RSI, '+');
190+ int sign_done = emit_jmp_rel32(code);
191+ emit_patch_jump(code, exp_neg);
192+ emit_store_byte_imm(code, REG_RSI, '-');
193+ emit_mov_reg_imm64(code, REG_RDX, 0);
194+ emit_sub_reg_reg(code, REG_RDX, REG_RAX);
195+ emit_mov_reg_reg(code, REG_RAX, REG_RDX); // |e|
196+ emit_patch_jump(code, sign_done);
197+ emit_add_reg_imm8(code, REG_RSI, 1);
198+ // hundreds digit only when |e| >= 100
199+ emit_cmp_reg_imm32(code, REG_RAX, 100);
200+ int two_digits = emit_jcc_rel32(code, COND_LT);
201+ emit_cqo(code);
202+ emit_mov_reg_imm64(code, REG_RBX, 100);
203+ emit_idiv_reg(code, REG_RBX); // RAX = hundreds, RDX = rest
204+ emit_add_reg_imm8(code, REG_RAX, '0');
205+ emit_store_byte_reg(code, REG_RSI, REG_RAX);
206+ emit_add_reg_imm8(code, REG_RSI, 1);
207+ emit_mov_reg_reg(code, REG_RAX, REG_RDX);
208+ emit_patch_jump(code, two_digits);
209+ emit_cqo(code);
210+ emit_mov_reg_imm64(code, REG_RBX, 10);
211+ emit_idiv_reg(code, REG_RBX); // RAX = tens, RDX = ones
212+ emit_add_reg_imm8(code, REG_RAX, '0');
213+ emit_store_byte_reg(code, REG_RSI, REG_RAX);
214+ emit_add_reg_imm8(code, REG_RSI, 1);
215+ emit_add_reg_imm8(code, REG_RDX, '0');
216+ emit_store_byte_reg(code, REG_RSI, REG_RDX);
217+ emit_add_reg_imm8(code, REG_RSI, 1);
218+ emit_patch_jump(code, no_exp);
219+
220+ emit_mov_reg_reg(code, REG_RDX, REG_RSI); // end of text (RCX = start)
221+ emit_patch_jump(code, special_done);
222+}

kiln/src/codegen/C/runtime/print_int.cmodified

+23−91
@@ -1,5 +1,6 @@
11 #include <string.h>
22 #include "codegen/H/runtime/print_int.h"
3+#include "codegen/H/runtime/number_format.h"
34 #include "codegen/H/emit/emit_internal.h"
45 #include "codegen/H/emit/emit_sse.h"
56 #include "codegen/H/emit/value.h"
@@ -9,17 +10,6 @@
910 #include "elf/H/elf_dynamic_call.h"
1011 #include "app/H/target.h"
1112
12-// Loads a compile-time-known double literal into an xmm register, via a
13-// GP register holding its raw IEEE-754 bit pattern -- same technique
14-// codegen/C/expressions/expr.c uses for user-written number literals (there's no "mov
15-// xmm, imm64" instruction).
16-static void load_double_const(CodeBuf *code, XReg dst, double v) {
17- uint64_t bits;
18- memcpy(&bits, &v, sizeof(bits));
19- emit_mov_reg_imm64(code, REG_RAX, bits);
20- emit_movq_xmm_from_reg(code, dst, REG_RAX);
21-}
22-
2313 // Embeds a fixed byte sequence directly in the code stream (jumped over
2414 // at runtime, never executed) and writes it to stdout -- used for the
2515 // small fixed strings ("none", "<function>") that don't need per-call
@@ -90,87 +80,12 @@ static void emit_number_routine(CodeBuf *code) {
9080 int routine_skip = emit_jmp_rel32(code);
9181 number_routine_offset = code->count;
9282
93- emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
94- emit_sub_reg_imm8(code, REG_RSP, 40);
95-
96- // ---- sign ----
97- emit_pxor_xmm_xmm(code, XMM2); // XMM2 = +0.0, used as the comparison zero
98- emit_ucomisd(code, XMM0, XMM2);
99- emit_mov_reg_imm64(code, REG_RBX, 0);
100- int skip_negate = emit_jcc_rel32(code, COND_AE); // XMM0 >= 0 -> not negative
101- emit_pxor_xmm_xmm(code, XMM2);
102- emit_subsd(code, XMM2, XMM0); // XMM2 = 0.0 - XMM0 = -XMM0
103- emit_movsd_xmm_xmm(code, XMM0, XMM2);
104- emit_mov_reg_imm64(code, REG_RBX, 1);
105- emit_patch_jump(code, skip_negate);
106-
107- // ---- split into integer part (RAX) and fractional remainder (XMM0, now in [0,1)) ----
108- emit_cvttsd2si(code, REG_RAX, XMM0);
109- emit_cvtsi2sd(code, XMM1, REG_RAX);
110- emit_subsd(code, XMM0, XMM1);
111-
112- // ---- integer digits, backward ----
113- emit_mov_reg_reg(code, REG_RCX, REG_RSP);
114- emit_add_reg_imm8(code, REG_RCX, 32);
115- int loop_start = code->count;
116- emit_dec_reg(code, REG_RCX);
117- emit_cqo(code);
118- emit_mov_reg_imm64(code, REG_RSI, 10);
119- emit_idiv_reg(code, REG_RSI);
120- emit_add_reg_imm8(code, REG_RDX, '0');
121- emit_store_byte_reg(code, REG_RCX, REG_RDX);
122- emit_cmp_reg_imm32(code, REG_RAX, 0);
123- emit_jcc_back(code, COND_NE, loop_start);
124-
125- // ---- sign prepend ----
126- emit_cmp_reg_imm32(code, REG_RBX, 0);
127- int skip_sign = emit_jcc_rel32(code, COND_E);
128- emit_dec_reg(code, REG_RCX);
129- emit_store_byte_imm(code, REG_RCX, '-');
130- emit_patch_jump(code, skip_sign);
131-
132- // ---- fraction digits, forward, 6 unrolled steps -- see print_int.c's
133- // history for why the 0.0000005 round-half-up nudge is needed ----
134- load_double_const(code, XMM1, 0.0000005);
135- emit_addsd(code, XMM0, XMM1);
136- emit_mov_reg_reg(code, REG_RSI, REG_RSP);
137- emit_add_reg_imm8(code, REG_RSI, 33);
138- load_double_const(code, XMM3, 10.0);
139- emit_mov_reg_imm64(code, REG_RBX, 0); // repurposed: frac_len (sign already applied above)
140- for (int i = 0; i < 6; i++) {
141- emit_mulsd(code, XMM0, XMM3);
142- emit_cvttsd2si(code, REG_RAX, XMM0); // this step's digit, 0-9
143- emit_cvtsi2sd(code, XMM1, REG_RAX);
144- emit_subsd(code, XMM0, XMM1); // remainder for the next digit
145- emit_cmp_reg_imm32(code, REG_RAX, 0);
146- int skip_mark = emit_jcc_rel32(code, COND_E);
147- emit_mov_reg_imm64(code, REG_RBX, (uint64_t)(i + 1)); // "kept up through here"
148- emit_patch_jump(code, skip_mark);
149- emit_add_reg_imm8(code, REG_RAX, '0');
150- emit_store_byte_reg(code, REG_RSI, REG_RAX);
151- emit_add_reg_imm8(code, REG_RSI, 1);
152- }
153-
154- // ---- decide '.' placement, then a single write(2) -- no newline ----
155- emit_mov_reg_reg(code, REG_RDX, REG_RSP);
156- emit_add_reg_imm8(code, REG_RDX, 32);
157- emit_cmp_reg_imm32(code, REG_RBX, 0);
158- int whole_number = emit_jcc_rel32(code, COND_E);
159- emit_store_byte_imm(code, REG_RDX, '.');
160- emit_mov_reg_reg(code, REG_RDX, REG_RSP);
161- emit_add_reg_imm8(code, REG_RDX, 33);
162- emit_add_reg_reg(code, REG_RDX, REG_RBX); // + frac_len
163- int after_dot = emit_jmp_rel32(code);
164- emit_patch_jump(code, whole_number);
165- // RDX already points one past the last int digit -- the correct
166- // end-of-content position when there's no fraction.
167- emit_patch_jump(code, after_dot);
168-
169- emit_mov_reg_reg(code, REG_RSI, REG_RCX); // buf start
170- emit_sub_reg_reg(code, REG_RDX, REG_RCX); // length = content_end - buf_start
83+ number_format_emit(code); // RCX = text start, RDX = text end (on the stack)
84+ emit_mov_reg_reg(code, REG_RSI, REG_RCX);
85+ emit_sub_reg_reg(code, REG_RDX, REG_RCX);
17186 platform_emit_write_stdout(code);
17287
173- emit_add_reg_imm8(code, REG_RSP, 40);
88+ emit_add_reg_imm8(code, REG_RSP, NUMBER_FORMAT_SCRATCH);
17489 emit_ret(code);
17590 emit_patch_jump(code, routine_skip);
17691 }
@@ -211,7 +126,7 @@ static void emit_print_recursive_routine(CodeBuf *code) {
211126 emit_mov_reg_reg(code, REG_RBP, REG_RSP);
212127 emit_sub_reg_imm8(code, REG_RSP, 32); // [RBP-8/-16/-24] loop state, [RBP-32] map's current entry addr
213128
214- int to_epilogue[5];
129+ int to_epilogue[8];
215130 int n_epilogue = 0;
216131
217132 emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
@@ -251,6 +166,23 @@ static void emit_print_recursive_routine(CodeBuf *code) {
251166 }
252167 emit_patch_jump(code, not_number);
253168
169+ emit_cmp_reg_imm32(code, REG_RBX, TAG_BOOL);
170+ int not_bool = emit_jcc_rel32(code, COND_NE);
171+ {
172+ static int yes_offset = -1;
173+ static int no_offset = -1;
174+ if (yes_offset == -1) yes_offset = emit_literal(code, "yes", 3);
175+ if (no_offset == -1) no_offset = emit_literal(code, "no", 2);
176+ emit_cmp_reg_imm32(code, REG_RAX, 0); // payload bits: 0.0 is all zero
177+ int is_no = emit_jcc_rel32(code, COND_E);
178+ write_literal(code, yes_offset, 3);
179+ to_epilogue[n_epilogue++] = emit_jmp_rel32(code);
180+ emit_patch_jump(code, is_no);
181+ write_literal(code, no_offset, 2);
182+ to_epilogue[n_epilogue++] = emit_jmp_rel32(code);
183+ }
184+ emit_patch_jump(code, not_bool);
185+
254186 emit_cmp_reg_imm32(code, REG_RBX, TAG_ARRAY);
255187 int not_array = emit_jcc_rel32(code, COND_NE);
256188 {

kiln/src/codegen/C/strings/affix_builtins.cadded

+134−0
@@ -0,0 +1,134 @@
1+#include "codegen/H/strings/string_builtins.h"
2+#include "codegen/H/strings/string_alloc.h"
3+#include "codegen/H/expressions/expr.h"
4+#include "codegen/H/emit/emit.h"
5+#include "codegen/H/emit/emit_sse.h"
6+#include "codegen/H/emit/bytes.h"
7+#include "codegen/H/emit/value.h"
8+#include "codegen/H/runtime/errors.h"
9+#include "parser/H/core/parser.h"
10+#include "parser/H/declarations/vars.h"
11+
12+static void push_number(double v) {
13+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
14+ emit_push_reg(code, REG_RBX);
15+ uint64_t bits;
16+ __builtin_memcpy(&bits, &v, sizeof(bits));
17+ emit_mov_reg_imm64(code, REG_RAX, bits);
18+ emit_push_reg(code, REG_RAX);
19+}
20+
21+// contains(s, search) = indexOf(s, search) >= 0
22+void codegen_builtin_contains(void) {
23+ codegen_builtin_indexof();
24+ emit_pop_reg(code, REG_RAX);
25+ emit_pop_reg(code, REG_RBX);
26+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
27+ emit_pxor_xmm_xmm(code, XMM1);
28+ emit_ucomisd(code, XMM0, XMM1);
29+ int found = emit_jcc_rel32(code, COND_AE);
30+ push_number(0.0);
31+ int done = emit_jmp_rel32(code);
32+ emit_patch_jump(code, found);
33+ push_number(1.0);
34+ emit_patch_jump(code, done);
35+}
36+
37+// starts_with(s, prefix) / ends_with(s, suffix): compare `affix_len` bytes at
38+// the start (or at s_len - affix_len) of s.
39+static void affix(int at_end) {
40+ codegen_expression(); // s
41+ expect(TOKEN_COMMA, "expected ',' after string argument");
42+ codegen_expression(); // affix
43+ expect(TOKEN_RPAREN, "expected ')' after arguments");
44+
45+ emit_pop_reg(code, REG_RCX); // affix payload
46+ emit_pop_reg(code, REG_RBX);
47+ emit_pop_reg(code, REG_RSI); // s payload
48+ emit_pop_reg(code, REG_RBX);
49+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, -8); // affix_len
50+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, -8); // s_len
51+ emit_cmp_reg_reg(code, REG_RDX, REG_RDI);
52+ int too_long = emit_jcc_rel32(code, COND_GT);
53+ if (at_end) {
54+ emit_sub_reg_reg(code, REG_RDI, REG_RDX);
55+ emit_add_reg_reg(code, REG_RSI, REG_RDI);
56+ }
57+ int loop = code->count;
58+ emit_cmp_reg_imm32(code, REG_RDX, 0);
59+ int matched = emit_jcc_rel32(code, COND_E);
60+ emit_load_byte_reg(code, REG_RAX, REG_RSI);
61+ emit_load_byte_reg(code, REG_RBX, REG_RCX);
62+ emit_cmp_reg_reg(code, REG_RAX, REG_RBX);
63+ int mismatch = emit_jcc_rel32(code, COND_NE);
64+ emit_add_reg_imm8(code, REG_RSI, 1);
65+ emit_add_reg_imm8(code, REG_RCX, 1);
66+ emit_dec_reg(code, REG_RDX);
67+ emit_jmp_back(code, loop);
68+
69+ emit_patch_jump(code, too_long);
70+ emit_patch_jump(code, mismatch);
71+ push_number(0.0);
72+ int done = emit_jmp_rel32(code);
73+ emit_patch_jump(code, matched);
74+ push_number(1.0);
75+ emit_patch_jump(code, done);
76+}
77+
78+void codegen_builtin_starts_with(void) { affix(0); }
79+void codegen_builtin_ends_with(void) { affix(1); }
80+
81+// repeat(s, n): s concatenated n times.
82+// Scratch: [0]=s [8]=len [16]=remaining [24]=dest cursor [32]=block
83+void codegen_builtin_repeat(void) {
84+ codegen_expression(); // s
85+ expect(TOKEN_COMMA, "expected ',' after string argument");
86+ codegen_expression(); // n
87+ expect(TOKEN_RPAREN, "expected ')' after arguments");
88+
89+ int32_t base = higher_order_scratch_offset();
90+ emit_pop_reg(code, REG_RAX);
91+ emit_pop_reg(code, REG_RBX);
92+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
93+ emit_cvttsd2si(code, REG_RCX, XMM0); // n
94+ emit_pop_reg(code, REG_RSI);
95+ emit_pop_reg(code, REG_RBX);
96+
97+ emit_cmp_reg_imm32(code, REG_RCX, 0);
98+ int n_ok = emit_jcc_rel32(code, COND_GE);
99+ errors_emit_die(code, "runtime error: repeat() count must not be negative");
100+ emit_patch_jump(code, n_ok);
101+
102+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // len
103+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RSI);
104+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX);
105+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RCX);
106+ emit_imul_reg_reg(code, REG_RDX, REG_RCX); // total length
107+ string_alloc_emit_prefixed(code); // RAX = block, RDX = total
108+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX);
109+ emit_add_reg_imm8(code, REG_RAX, 8);
110+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
111+
112+ int loop = code->count;
113+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 16);
114+ emit_cmp_reg_imm32(code, REG_RCX, 0);
115+ int finished = emit_jcc_rel32(code, COND_E);
116+ emit_sub_reg_imm8(code, REG_RCX, 1);
117+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RCX);
118+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 24);
119+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0);
120+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8);
121+ bytes_emit_copy(code);
122+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 24);
123+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8);
124+ emit_add_reg_reg(code, REG_RDI, REG_RDX);
125+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RDI);
126+ emit_jmp_back(code, loop);
127+ emit_patch_jump(code, finished);
128+
129+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 32);
130+ emit_add_reg_imm8(code, REG_RAX, 8);
131+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
132+ emit_push_reg(code, REG_RBX);
133+ emit_push_reg(code, REG_RAX);
134+}

kiln/src/codegen/C/strings/char_builtins.cadded

+54−0
@@ -0,0 +1,54 @@
1+#include "codegen/H/strings/string_builtins.h"
2+#include "codegen/H/strings/string_alloc.h"
3+#include "codegen/H/runtime/errors.h"
4+#include "codegen/H/emit/emit_internal.h"
5+#include "codegen/H/emit/emit_sse.h"
6+#include "codegen/H/emit/value.h"
7+#include "parser/H/core/parser.h"
8+
9+// chr(n): a one-byte string holding byte n (1..255). Same allocation shape as
10+// codegen_string_index_read's one-char result.
11+void codegen_builtin_chr(void) {
12+ emit_pop_reg(code, REG_RAX); // payload
13+ emit_pop_reg(code, REG_RBX); // tag (ignored, assumed NUMBER)
14+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
15+ emit_cvttsd2si(code, REG_RBX, XMM0); // code (RBX survives string_alloc_emit_prefixed)
16+
17+ emit_cmp_reg_imm32(code, REG_RBX, 1);
18+ int too_small = emit_jcc_rel32(code, COND_LT);
19+ emit_cmp_reg_imm32(code, REG_RBX, 255);
20+ int too_big = emit_jcc_rel32(code, COND_GT);
21+ int ok = emit_jmp_rel32(code);
22+ emit_patch_jump(code, too_small);
23+ emit_patch_jump(code, too_big);
24+ errors_emit_die(code, "runtime error: chr() expects a number from 1 to 255");
25+ emit_patch_jump(code, ok);
26+
27+ emit_mov_reg_imm64(code, REG_RDX, 1);
28+ string_alloc_emit_prefixed(code); // RAX = new 1-byte block, length prefix written
29+ emit_add_reg_imm8(code, REG_RAX, 8);
30+ emit_store_byte_reg(code, REG_RAX, REG_RBX);
31+
32+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
33+ emit_push_reg(code, REG_RBX);
34+ emit_push_reg(code, REG_RAX);
35+}
36+
37+// ord(s): the first byte of s as a number (0..255).
38+void codegen_builtin_ord(void) {
39+ emit_pop_reg(code, REG_RSI); // string payload
40+ emit_pop_reg(code, REG_RBX); // tag (ignored, assumed STRING)
41+
42+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length
43+ emit_cmp_reg_imm32(code, REG_RDX, 0);
44+ int nonempty = emit_jcc_rel32(code, COND_NE);
45+ errors_emit_die(code, "runtime error: ord() expects a non-empty string");
46+ emit_patch_jump(code, nonempty);
47+
48+ emit_load_byte_reg(code, REG_RAX, REG_RSI);
49+ emit_cvtsi2sd(code, XMM0, REG_RAX);
50+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
51+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
52+ emit_push_reg(code, REG_RBX);
53+ emit_push_reg(code, REG_RAX);
54+}

kiln/src/codegen/C/strings/strings.cmodified

+28−2
@@ -12,7 +12,7 @@
1212 static void push_number_literal(double v) {
1313 uint64_t bits;
1414 __builtin_memcpy(&bits, &v, sizeof(bits));
15- emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
15+ emit_mov_reg_imm64(code, REG_RBX, TAG_BOOL);
1616 emit_push_reg(code, REG_RBX);
1717 emit_mov_reg_imm64(code, REG_RAX, bits);
1818 emit_push_reg(code, REG_RAX);
@@ -141,6 +141,32 @@ void codegen_string_compare(int invert) {
141141 emit_patch_jump(code, skip_true);
142142 }
143143
144+void codegen_string_order(void) {
145+ emit_load_mem_disp32(code, REG_RBX, REG_RAX, -8); // bytes left in a
146+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, -8); // bytes left in b
147+
148+ int loop_start = code->count;
149+ emit_cmp_reg_imm32(code, REG_RBX, 0);
150+ int a_done = emit_jcc_rel32(code, COND_E);
151+ emit_cmp_reg_imm32(code, REG_RDX, 0);
152+ int b_done = emit_jcc_rel32(code, COND_E);
153+ emit_load_byte_reg(code, REG_RSI, REG_RAX);
154+ emit_load_byte_reg(code, REG_RDI, REG_RCX);
155+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
156+ int differ = emit_jcc_rel32(code, COND_NE); // flags from the first differing byte
157+ emit_add_reg_imm8(code, REG_RAX, 1);
158+ emit_add_reg_imm8(code, REG_RCX, 1);
159+ emit_dec_reg(code, REG_RBX);
160+ emit_dec_reg(code, REG_RDX);
161+ emit_jmp_back(code, loop_start);
162+
163+ // One side ran out: the one with bytes left is greater (both out: equal).
164+ emit_patch_jump(code, a_done);
165+ emit_patch_jump(code, b_done);
166+ emit_cmp_reg_reg(code, REG_RBX, REG_RDX);
167+ emit_patch_jump(code, differ);
168+}
169+
144170 void codegen_string_index_read(void) {
145171 emit_pop_reg(code, REG_RAX); // index payload
146172 emit_pop_reg(code, REG_RBX); // index tag (ignored, assumed NUMBER)
@@ -158,7 +184,7 @@ void codegen_string_index_read(void) {
158184 int ok = emit_jmp_rel32(code);
159185 emit_patch_jump(code, neg);
160186 emit_patch_jump(code, toobig);
161- errors_emit_die(code, "runtime error: string index out of bounds");
187+ errors_emit_die(code, "index out of bounds");
162188 emit_patch_jump(code, ok);
163189
164190 emit_add_reg_reg(code, REG_RSI, REG_RCX); // char address

kiln/src/codegen/D/builtins/builtins.dmodified

+3−1
@@ -7,7 +7,8 @@ src/codegen/O/builtins/builtins.o: src/codegen/C/builtins/builtins.c \
77 src/codegen/H/collections/keys_values.h \
88 src/codegen/H/strings/string_builtins.h src/codegen/H/io/file_builtins.h \
99 src/codegen/H/io/input_builtin.h src/codegen/H/io/argv_builtin.h \
10- src/codegen/H/builtins/type_builtin.h src/parser/H/core/parser.h \
10+ src/codegen/H/builtins/type_builtin.h \
11+ src/codegen/H/builtins/exit_builtin.h src/parser/H/core/parser.h \
1112 src/codegen/H/emit/emit.h
1213 src/codegen/H/builtins/builtins.h:
1314 src/codegen/H/expressions/expr.h:
@@ -23,5 +24,6 @@ src/codegen/H/io/file_builtins.h:
2324 src/codegen/H/io/input_builtin.h:
2425 src/codegen/H/io/argv_builtin.h:
2526 src/codegen/H/builtins/type_builtin.h:
27+src/codegen/H/builtins/exit_builtin.h:
2628 src/parser/H/core/parser.h:
2729 src/codegen/H/emit/emit.h:

kiln/src/codegen/D/builtins/exit_builtin.dadded

+22−0
@@ -0,0 +1,22 @@
1+src/codegen/O/builtins/exit_builtin.o: \
2+ src/codegen/C/builtins/exit_builtin.c \
3+ src/codegen/H/builtins/exit_builtin.h src/codegen/H/expressions/expr.h \
4+ src/lexer/H/lexer.h src/codegen/H/emit/emit.h \
5+ src/codegen/H/emit/emit_sse.h src/codegen/H/platform/win_call.h \
6+ src/pe/H/pe_writer.h src/codegen/H/emit/runtime_layout.h \
7+ src/codegen/H/emit/runtime_import_enum.h \
8+ src/codegen/H/emit/runtime_exports.def src/pe/H/pe_imports.def \
9+ src/app/H/target.h src/parser/H/core/parser.h
10+src/codegen/H/builtins/exit_builtin.h:
11+src/codegen/H/expressions/expr.h:
12+src/lexer/H/lexer.h:
13+src/codegen/H/emit/emit.h:
14+src/codegen/H/emit/emit_sse.h:
15+src/codegen/H/platform/win_call.h:
16+src/pe/H/pe_writer.h:
17+src/codegen/H/emit/runtime_layout.h:
18+src/codegen/H/emit/runtime_import_enum.h:
19+src/codegen/H/emit/runtime_exports.def:
20+src/pe/H/pe_imports.def:
21+src/app/H/target.h:
22+src/parser/H/core/parser.h:

kiln/src/codegen/D/builtins/sort_builtin.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/codegen/O/builtins/sort_builtin.o: \
2+ src/codegen/C/builtins/sort_builtin.c \
3+ src/codegen/H/builtins/higher_order.h src/codegen/H/expressions/expr.h \
4+ src/lexer/H/lexer.h src/codegen/H/functions/closures.h \
5+ src/parser/H/declarations/functions.h src/codegen/H/strings/strings.h \
6+ src/codegen/H/emit/emit.h src/codegen/H/emit/emit_sse.h \
7+ src/codegen/H/emit/value.h src/parser/H/core/parser.h \
8+ src/parser/H/declarations/vars.h
9+src/codegen/H/builtins/higher_order.h:
10+src/codegen/H/expressions/expr.h:
11+src/lexer/H/lexer.h:
12+src/codegen/H/functions/closures.h:
13+src/parser/H/declarations/functions.h:
14+src/codegen/H/strings/strings.h:
15+src/codegen/H/emit/emit.h:
16+src/codegen/H/emit/emit_sse.h:
17+src/codegen/H/emit/value.h:
18+src/parser/H/core/parser.h:
19+src/parser/H/declarations/vars.h:

kiln/src/codegen/D/builtins/str_builtin.dmodified

+7−2
@@ -2,13 +2,18 @@ src/codegen/O/builtins/str_builtin.o: \
22 src/codegen/C/builtins/str_builtin.c \
33 src/codegen/H/builtins/convert_builtins.h src/codegen/H/emit/emit.h \
44 src/codegen/H/emit/emit_sse.h src/codegen/H/emit/value.h \
5- src/codegen/H/runtime/heap.h src/codegen/H/emit/bytes.h \
6- src/parser/H/core/parser.h src/lexer/H/lexer.h
5+ src/codegen/H/runtime/heap.h src/codegen/H/runtime/errors.h \
6+ src/codegen/H/strings/strings.h src/codegen/H/runtime/number_format.h \
7+ src/codegen/H/emit/bytes.h src/parser/H/core/parser.h \
8+ src/lexer/H/lexer.h
79 src/codegen/H/builtins/convert_builtins.h:
810 src/codegen/H/emit/emit.h:
911 src/codegen/H/emit/emit_sse.h:
1012 src/codegen/H/emit/value.h:
1113 src/codegen/H/runtime/heap.h:
14+src/codegen/H/runtime/errors.h:
15+src/codegen/H/strings/strings.h:
16+src/codegen/H/runtime/number_format.h:
1217 src/codegen/H/emit/bytes.h:
1318 src/parser/H/core/parser.h:
1419 src/lexer/H/lexer.h:

kiln/src/codegen/D/collections/array_mutate.dadded

+16−0
@@ -0,0 +1,16 @@
1+src/codegen/O/collections/array_mutate.o: \
2+ src/codegen/C/collections/array_mutate.c \
3+ src/codegen/H/collections/arrays.h src/codegen/H/emit/emit_internal.h \
4+ src/codegen/H/emit/emit.h src/codegen/H/emit/emit_sse.h \
5+ src/codegen/H/emit/value.h src/codegen/H/runtime/heap.h \
6+ src/codegen/H/runtime/errors.h src/parser/H/core/parser.h \
7+ src/lexer/H/lexer.h
8+src/codegen/H/collections/arrays.h:
9+src/codegen/H/emit/emit_internal.h:
10+src/codegen/H/emit/emit.h:
11+src/codegen/H/emit/emit_sse.h:
12+src/codegen/H/emit/value.h:
13+src/codegen/H/runtime/heap.h:
14+src/codegen/H/runtime/errors.h:
15+src/parser/H/core/parser.h:
16+src/lexer/H/lexer.h:

kiln/src/codegen/D/collections/maps_mutate.dmodified

+2−0
@@ -1,11 +1,13 @@
11 src/codegen/O/collections/maps_mutate.o: \
22 src/codegen/C/collections/maps_mutate.c src/codegen/H/collections/maps.h \
3+ src/codegen/H/collections/arrays.h \
34 src/codegen/H/collections/maps_internal.h \
45 src/codegen/H/emit/emit_internal.h src/codegen/H/emit/emit.h \
56 src/codegen/H/emit/value.h src/codegen/H/runtime/heap.h \
67 src/codegen/H/emit/bytes.h src/parser/H/core/parser.h \
78 src/lexer/H/lexer.h
89 src/codegen/H/collections/maps.h:
10+src/codegen/H/collections/arrays.h:
911 src/codegen/H/collections/maps_internal.h:
1012 src/codegen/H/emit/emit_internal.h:
1113 src/codegen/H/emit/emit.h:

kiln/src/codegen/D/expressions/expr.dmodified

+5−1
@@ -5,7 +5,9 @@ src/codegen/O/expressions/expr.o: src/codegen/C/expressions/expr.c \
55 src/codegen/H/strings/strings.h src/codegen/H/collections/arrays.h \
66 src/codegen/H/collections/maps.h src/codegen/H/builtins/builtins.h \
77 src/codegen/H/functions/closures.h src/parser/H/declarations/functions.h \
8- src/parser/H/core/parser.h src/parser/H/declarations/vars.h \
8+ src/codegen/H/runtime/errors.h src/parser/H/core/parser.h \
9+ src/parser/H/declarations/vars.h \
10+ src/parser/H/declarations/module_state.h \
911 src/parser/H/declarations/constants.h
1012 src/codegen/H/expressions/expr.h:
1113 src/lexer/H/lexer.h:
@@ -19,6 +21,8 @@ src/codegen/H/collections/maps.h:
1921 src/codegen/H/builtins/builtins.h:
2022 src/codegen/H/functions/closures.h:
2123 src/parser/H/declarations/functions.h:
24+src/codegen/H/runtime/errors.h:
2225 src/parser/H/core/parser.h:
2326 src/parser/H/declarations/vars.h:
27+src/parser/H/declarations/module_state.h:
2428 src/parser/H/declarations/constants.h:

kiln/src/codegen/D/expressions/expr_bool.dmodified

+2−1
@@ -3,12 +3,13 @@ src/codegen/O/expressions/expr_bool.o: \
33 src/lexer/H/lexer.h src/codegen/H/expressions/expr_internal.h \
44 src/codegen/H/emit/emit.h src/codegen/H/emit/emit_sse.h \
55 src/codegen/H/emit/value.h src/codegen/H/strings/strings.h \
6- src/parser/H/core/parser.h
6+ src/codegen/H/runtime/errors.h src/parser/H/core/parser.h
77 src/codegen/H/expressions/expr.h:
88 src/lexer/H/lexer.h:
99 src/codegen/H/expressions/expr_internal.h:
1010 src/codegen/H/emit/emit.h:
1111 src/codegen/H/emit/emit_sse.h:
1212 src/codegen/H/emit/value.h:
1313 src/codegen/H/strings/strings.h:
14+src/codegen/H/runtime/errors.h:
1415 src/parser/H/core/parser.h:

kiln/src/codegen/D/io/fs_builtins.dadded

+35−0
@@ -0,0 +1,35 @@
1+src/codegen/O/io/fs_builtins.o: src/codegen/C/io/fs_builtins.c \
2+ src/codegen/H/io/file_builtins.h src/codegen/H/io/file_path.h \
3+ src/codegen/H/emit/emit.h src/codegen/H/expressions/expr.h \
4+ src/lexer/H/lexer.h src/codegen/H/emit/emit_sse.h \
5+ src/codegen/H/emit/value.h src/codegen/H/emit/bytes.h \
6+ src/codegen/H/collections/arrays.h \
7+ src/codegen/H/platform/platform_file.h src/codegen/H/runtime/errors.h \
8+ src/codegen/H/runtime/heap.h src/codegen/H/strings/string_alloc.h \
9+ src/app/H/target.h src/codegen/H/platform/win_call.h \
10+ src/pe/H/pe_writer.h src/codegen/H/emit/runtime_layout.h \
11+ src/codegen/H/emit/runtime_import_enum.h \
12+ src/codegen/H/emit/runtime_exports.def src/pe/H/pe_imports.def \
13+ src/parser/H/core/parser.h src/parser/H/declarations/vars.h
14+src/codegen/H/io/file_builtins.h:
15+src/codegen/H/io/file_path.h:
16+src/codegen/H/emit/emit.h:
17+src/codegen/H/expressions/expr.h:
18+src/lexer/H/lexer.h:
19+src/codegen/H/emit/emit_sse.h:
20+src/codegen/H/emit/value.h:
21+src/codegen/H/emit/bytes.h:
22+src/codegen/H/collections/arrays.h:
23+src/codegen/H/platform/platform_file.h:
24+src/codegen/H/runtime/errors.h:
25+src/codegen/H/runtime/heap.h:
26+src/codegen/H/strings/string_alloc.h:
27+src/app/H/target.h:
28+src/codegen/H/platform/win_call.h:
29+src/pe/H/pe_writer.h:
30+src/codegen/H/emit/runtime_layout.h:
31+src/codegen/H/emit/runtime_import_enum.h:
32+src/codegen/H/emit/runtime_exports.def:
33+src/pe/H/pe_imports.def:
34+src/parser/H/core/parser.h:
35+src/parser/H/declarations/vars.h:

kiln/src/codegen/D/runtime/number_format.dadded

+8−0
@@ -0,0 +1,8 @@
1+src/codegen/O/runtime/number_format.o: \
2+ src/codegen/C/runtime/number_format.c \
3+ src/codegen/H/runtime/number_format.h src/codegen/H/emit/emit.h \
4+ src/codegen/H/emit/emit_internal.h src/codegen/H/emit/emit_sse.h
5+src/codegen/H/runtime/number_format.h:
6+src/codegen/H/emit/emit.h:
7+src/codegen/H/emit/emit_internal.h:
8+src/codegen/H/emit/emit_sse.h:

kiln/src/codegen/D/runtime/print_int.dmodified

+5−4
@@ -1,15 +1,16 @@
11 src/codegen/O/runtime/print_int.o: src/codegen/C/runtime/print_int.c \
22 src/codegen/H/runtime/print_int.h src/codegen/H/emit/emit.h \
3- src/codegen/H/emit/emit_internal.h src/codegen/H/emit/emit_sse.h \
4- src/codegen/H/emit/value.h src/codegen/H/emit/layout.h \
5- src/codegen/H/platform/win_call.h src/pe/H/pe_writer.h \
6- src/codegen/H/emit/runtime_layout.h \
3+ src/codegen/H/runtime/number_format.h src/codegen/H/emit/emit_internal.h \
4+ src/codegen/H/emit/emit_sse.h src/codegen/H/emit/value.h \
5+ src/codegen/H/emit/layout.h src/codegen/H/platform/win_call.h \
6+ src/pe/H/pe_writer.h src/codegen/H/emit/runtime_layout.h \
77 src/codegen/H/emit/runtime_import_enum.h \
88 src/codegen/H/emit/runtime_exports.def src/pe/H/pe_imports.def \
99 src/codegen/H/platform/platform_console.h src/elf/H/elf_dynamic_call.h \
1010 src/elf/H/elf_dynamic.h src/elf/H/elf_writer.h src/app/H/target.h
1111 src/codegen/H/runtime/print_int.h:
1212 src/codegen/H/emit/emit.h:
13+src/codegen/H/runtime/number_format.h:
1314 src/codegen/H/emit/emit_internal.h:
1415 src/codegen/H/emit/emit_sse.h:
1516 src/codegen/H/emit/value.h:

kiln/src/codegen/D/strings/affix_builtins.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/codegen/O/strings/affix_builtins.o: \
2+ src/codegen/C/strings/affix_builtins.c \
3+ src/codegen/H/strings/string_builtins.h \
4+ src/codegen/H/strings/string_alloc.h src/codegen/H/emit/emit.h \
5+ src/codegen/H/expressions/expr.h src/lexer/H/lexer.h \
6+ src/codegen/H/emit/emit_sse.h src/codegen/H/emit/bytes.h \
7+ src/codegen/H/emit/value.h src/codegen/H/runtime/errors.h \
8+ src/parser/H/core/parser.h src/parser/H/declarations/vars.h
9+src/codegen/H/strings/string_builtins.h:
10+src/codegen/H/strings/string_alloc.h:
11+src/codegen/H/emit/emit.h:
12+src/codegen/H/expressions/expr.h:
13+src/lexer/H/lexer.h:
14+src/codegen/H/emit/emit_sse.h:
15+src/codegen/H/emit/bytes.h:
16+src/codegen/H/emit/value.h:
17+src/codegen/H/runtime/errors.h:
18+src/parser/H/core/parser.h:
19+src/parser/H/declarations/vars.h:

kiln/src/codegen/D/strings/char_builtins.dadded

+16−0
@@ -0,0 +1,16 @@
1+src/codegen/O/strings/char_builtins.o: \
2+ src/codegen/C/strings/char_builtins.c \
3+ src/codegen/H/strings/string_builtins.h \
4+ src/codegen/H/strings/string_alloc.h src/codegen/H/emit/emit.h \
5+ src/codegen/H/runtime/errors.h src/codegen/H/emit/emit_internal.h \
6+ src/codegen/H/emit/emit_sse.h src/codegen/H/emit/value.h \
7+ src/parser/H/core/parser.h src/lexer/H/lexer.h
8+src/codegen/H/strings/string_builtins.h:
9+src/codegen/H/strings/string_alloc.h:
10+src/codegen/H/emit/emit.h:
11+src/codegen/H/runtime/errors.h:
12+src/codegen/H/emit/emit_internal.h:
13+src/codegen/H/emit/emit_sse.h:
14+src/codegen/H/emit/value.h:
15+src/parser/H/core/parser.h:
16+src/lexer/H/lexer.h:

kiln/src/codegen/H/builtins/builtins.hmodified

+4−0
@@ -8,4 +8,8 @@
88 // caller falls through to a user-defined function lookup.
99 int codegen_try_builtin_call(const char *name, int len);
1010
11+// Argument count a builtin is wrapped with when used as a value, or -1 if
12+// `name` is not a builtin.
13+int codegen_builtin_arity(const char *name, int len);
14+
1115 #endif

kiln/src/codegen/H/builtins/exit_builtin.hadded

+8−0
@@ -0,0 +1,8 @@
1+#ifndef KILN_EXIT_BUILTIN_H
2+#define KILN_EXIT_BUILTIN_H
3+
4+// exit(code): ends the process with that exit status (self-parses its argument,
5+// starting right after the already-consumed '(').
6+void codegen_builtin_exit(void);
7+
8+#endif

kiln/src/codegen/H/builtins/higher_order.hmodified

+4−0
@@ -16,4 +16,8 @@ void codegen_builtin_filter(void);
1616 // reduce(array, fn, initial): pushes fn(...fn(fn(initial, e0), e1)..., eN).
1717 void codegen_builtin_reduce(void);
1818
19+// sort(array) / sort(array, cmp): in-place stable sort, pushes the array.
20+// cmp(a, b) returns a number; > 0 means a goes after b.
21+void codegen_builtin_sort(void);
22+
1923 #endif

kiln/src/codegen/H/collections/arrays.hmodified

+8−0
@@ -28,4 +28,12 @@ void codegen_builtin_push(void);
2828 // len(array): pushes the element count as a NUMBER.
2929 void codegen_builtin_len(void);
3030
31+// pop/insert/slice and delete(array, i): see C/collections/array_mutate.c.
32+// Arguments already pushed left-to-right, like push()/len().
33+void codegen_builtin_pop(void);
34+void codegen_builtin_insert(void);
35+void codegen_builtin_slice(void);
36+// delete() on an array: RDI = index payload, RSI = array object; pushes the array.
37+void codegen_array_delete_index(void);
38+
3139 #endif

kiln/src/codegen/H/emit/emit.hmodified

+3−0
@@ -99,6 +99,9 @@ void emit_jcc_back(CodeBuf *buf, Cond cc, int target_offset);
9999 // in the source, same single-pass limitation ashvm's compiler has -- so
100100 // the target's code offset is always already known, no patching needed).
101101 void emit_call_back(CodeBuf *buf, int target_offset);
102+// Forward call: emits `call rel32` with a placeholder and returns its patch
103+// offset (rel32 = target - (patch_offset + 4)).
104+int emit_call_rel32(CodeBuf *buf);
102105 void emit_ret(CodeBuf *buf);
103106
104107 // CALL r/m64 (opcode FF /2) -- indirect call through a register holding

kiln/src/codegen/H/emit/layout.hmodified

+1−0
@@ -10,6 +10,7 @@
1010 // elf/H/elf_writer.h vs pe/H/pe_writer.h), those hardcodes would each need
1111 // their own target check -- so instead every call site goes through one
1212 // of these, and the target check lives here exactly once.
13+uint64_t kiln_modstate_addr(void); // start of the module-state slot table (16 bytes per slot)
1314 uint64_t kiln_code_base(void); // add a code offset to get an absolute address
1415 uint64_t kiln_heap_ptr_addr(void);
1516 uint64_t kiln_try_depth_addr(void);

kiln/src/codegen/H/emit/runtime_layout.hmodified

+8−0
@@ -11,4 +11,12 @@
1111 #define KILN_TRY_HANDLERS_SIZE (MAX_KILN_TRY_DEPTH * KILN_TRY_HANDLER_SIZE)
1212 #define KILN_TRY_DEPTH_SIZE 8
1313
14+
15+// Module-level state: top-level `local`s of an imported file whose value is
16+// computed at import time live in a fixed table of (tag, payload) slots in the
17+// globals region (see parser/C/declarations/module_state.c), because a called
18+// function's own frame can't reach the importing frame.
19+#define KILN_MODSTATE_SLOTS 128
20+#define KILN_MODSTATE_SIZE (16 * KILN_MODSTATE_SLOTS)
21+
1422 #endif

kiln/src/codegen/H/emit/value.hmodified

+7−6
@@ -31,14 +31,15 @@
3131 // M5's `==`/`!=`, see codegen/C/expressions/expr_bool.c -- nil==nil compares equal
3232 // tags then falls into the numeric path, 0.0==0.0; nil compared against
3333 // any other tag is caught by the tags-differ branch before ever
34-// touching the payload). `true`/`false` are NOT a separate tag --
35-// they're just named literals for TAG_NUMBER 1.0/0.0 (missing.md's
36-// "boolean type" note is intentionally only half-addressed this way:
37-// the whole codebase already treats "truthy" as "NUMBER != 0", so a
38-// distinct boolean tag would mean touching that logic everywhere
39-// instead of adding one clean new case).
34+// touching the payload).
4035 #define TAG_NIL 5
4136
37+// A real boolean: `yes` / `no`, and what comparisons, `!`, `and`/`or`, has() and
38+// the *_exists / contains-style builtins produce. The payload is 1.0 / 0.0, so
39+// truthiness and arithmetic (which only look at the payload) keep working;
40+// `yes == 1` is false because the tags differ. Prints as yes / no.
41+#define TAG_BOOL 6
42+
4243 // An array's payload is the address of a small, fixed-size, STABLE
4344 // "array object" (see codegen/H/strings/strings.h's comment on why absolute
4445 // addresses are safe: kiln is always a fixed, non-PIE executable):

kiln/src/codegen/H/expressions/expr.hmodified

+4−0
@@ -33,4 +33,8 @@ void codegen_call_named(const char *name, int len);
3333 // the exact same logic instead of a second, drifting copy of it.
3434 void codegen_apply_plus(void);
3535
36+// Pops a value and sets flags for a condition: COND_E afterward means falsy.
37+// Errors on strings, arrays, maps and functions.
38+void codegen_pop_and_test_truthy(void);
39+
3640 #endif

kiln/src/codegen/H/io/file_builtins.hmodified

+7−0
@@ -9,4 +9,11 @@ void codegen_builtin_write_file(void);
99 void codegen_builtin_append_file(void);
1010 void codegen_builtin_file_exists(void);
1111
12+// rename_file(from, to) / delete_file(path) / make_dir(path): 1 on success, 0 on failure.
13+// list_dir(path): array of entry names. Implemented in C/io/fs_builtins.c.
14+void codegen_builtin_rename_file(void);
15+void codegen_builtin_delete_file(void);
16+void codegen_builtin_make_dir(void);
17+void codegen_builtin_list_dir(void);
18+
1219 #endif

kiln/src/codegen/H/platform/platform_file.hmodified

+6−0
@@ -21,4 +21,10 @@ void platform_emit_write_bytes(CodeBuf *code); // in: RDI=fd/handle, RSI=buf
2121 // limit already documented elsewhere for file I/O).
2222 void platform_emit_file_size(CodeBuf *code); // in: RDI=fd/handle -> out: RAX=size
2323
24+// Filesystem operations. All three return RAX = 0 on success, nonzero on failure,
25+// on both targets.
26+void platform_emit_rename(CodeBuf *code); // in: RDI=nullterm from, RSI=nullterm to
27+void platform_emit_delete(CodeBuf *code); // in: RDI=nullterm path
28+void platform_emit_mkdir(CodeBuf *code); // in: RDI=nullterm path
29+
2430 #endif

kiln/src/codegen/H/runtime/errors.hmodified

+4−4
@@ -31,10 +31,10 @@ void errors_emit_die(CodeBuf *code, const char *msg);
3131 // the adjacent code segment).
3232 void errors_emit_fatal(CodeBuf *code, const char *msg);
3333
34-// Like errors_emit_die, but for a RUNTIME string value (parser.c's
35-// `raise expr;`) instead of a compile-time-constant C string -- the
36-// caller must already have RSI=message payload and RDX=message length
37-// loaded (a raise expression's already-popped (tag,payload), payload is
34+// Like errors_emit_die, but for a RUNTIME value (parser.c's `raise expr;`,
35+// any type) instead of a compile-time-constant C string -- the caller must
36+// already have RBX=tag and RSI=payload loaded, plus RDX=length when the tag is
37+// STRING (a raise expression's already-popped (tag,payload), payload is
3838 // already a valid length-prefixed TAG_STRING pointer, so there's nothing
3939 // to embed, just the same jump to the raise routine errors_emit_die uses.
4040 void errors_emit_die_dynamic(CodeBuf *code);

kiln/src/codegen/H/runtime/number_format.hadded

+21−0
@@ -0,0 +1,21 @@
1+#ifndef KILN_NUMBER_FORMAT_H
2+#define KILN_NUMBER_FORMAT_H
3+#include "codegen/H/emit/emit.h"
4+
5+// Ash's one number-to-text format (ashvm's vm_format_number matches it):
6+// - NaN / inf / -inf -> "nan" / "inf" / "-inf"
7+// - |x| < 1e15 and |x| >= 1e-6 (or 0): plain decimal, at most 6 fraction
8+// digits (rounded), trailing zeros dropped, so 3.0 -> "3", 0.5 -> "0.5"
9+// - otherwise scientific: mantissa as above in [1,10), then 'e', a sign and
10+// at least two exponent digits ("1e+21", "1.5e-07")
11+//
12+// In: RAX = the double's raw bits.
13+// Out: RCX = first byte, RDX = one past the last byte of the text, which lives
14+// in a 64-byte scratch region on the stack. RSP has been lowered by
15+// NUMBER_FORMAT_SCRATCH; the caller consumes the text, then does
16+// `add rsp, NUMBER_FORMAT_SCRATCH`.
17+// Clobbers RAX, RBX, RCX, RDX, RSI, RDI, XMM0-XMM3.
18+#define NUMBER_FORMAT_SCRATCH 64
19+void number_format_emit(CodeBuf *code);
20+
21+#endif

kiln/src/codegen/H/strings/string_builtins.hmodified

+11−0
@@ -7,6 +7,10 @@ void codegen_builtin_upper(void);
77 void codegen_builtin_lower(void);
88 void codegen_builtin_trim(void);
99
10+// chr(n) / ord(s): byte <-> one-byte string (argument already pushed).
11+void codegen_builtin_chr(void);
12+void codegen_builtin_ord(void);
13+
1014 // substring/indexOf/split/join/replace self-parse their (multiple)
1115 // arguments starting right after the already-consumed '(', same
1216 // convention as codegen/H/builtins/higher_order.h's map/filter/reduce.
@@ -16,4 +20,11 @@ void codegen_builtin_split(void);
1620 void codegen_builtin_join(void);
1721 void codegen_builtin_replace(void);
1822
23+// contains/starts_with/ends_with (-> 1/0) and repeat(s, n); self-parse like
24+// indexOf. Implemented in C/strings/affix_builtins.c.
25+void codegen_builtin_contains(void);
26+void codegen_builtin_starts_with(void);
27+void codegen_builtin_ends_with(void);
28+void codegen_builtin_repeat(void);
29+
1930 #endif

kiln/src/codegen/H/strings/strings.hmodified

+6−0
@@ -28,6 +28,12 @@ void codegen_string_concat(void);
2828 // byte-different (invert=1, i.e. `!=`).
2929 void codegen_string_compare(int invert);
3030
31+// Lexicographic (bytewise) order of two strings, a_payload=RAX, b_payload=RCX.
32+// Pushes nothing: leaves the flags as if a cmp of a against b had run, so the
33+// caller can branch with the same unsigned conditions ucomisd uses (COND_B/BE/
34+// A/AE). Clobbers RAX, RCX, RBX, RDX, RSI, RDI.
35+void codegen_string_order(void);
36+
3137 // Assumes index already pushed (top) and string already pushed (below
3238 // it), both (tag, payload) pairs -- postfix()'s `s[i]` read, the
3339 // TAG_STRING case (previously missing entirely -- see missing.md #1:

kiln/src/elf/H/elf_dynamic.hmodified

+2−1
@@ -61,7 +61,8 @@
6161 #define KILN_DYN_RELA_SIZE (KILN_DYN_RELAENT_SIZE * (unsigned)RUNTIME_IMPORT_COUNT_)
6262
6363 #define KILN_DYN_GLOBALS_ADDR (KILN_DYN_RELA_ADDR + KILN_DYN_RELA_SIZE)
64-#define KILN_DYN_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE)
64+#define KILN_DYN_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE + KILN_MODSTATE_SIZE)
65+#define KILN_DYN_MODSTATE_ADDR (KILN_DYN_GLOBALS_ADDR + 8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE)
6566 #define KILN_DYN_CODE_START_OFFSET (KILN_DYN_GLOBALS_ADDR + KILN_DYN_GLOBALS_SIZE - KILN_LOAD_BASE)
6667
6768 // Absolute VA of `which`'s GOT slot -- loaded via a compile-time-known

kiln/src/elf/H/elf_writer.hmodified

+2−1
@@ -34,11 +34,12 @@
3434 // inside it (see codegen/C/io/argv_builtin.c).
3535 #define KILN_ARGV_SIZE 8
3636
37-#define KILN_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE) // heap_ptr, try_depth, handler stack, argv
37+#define KILN_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE + KILN_MODSTATE_SIZE) // heap_ptr, try_depth, handler stack, argv, module state
3838 #define KILN_GLOBALS_ADDR (KILN_LOAD_BASE + KILN_EHDR_SIZE + KILN_PHDR_SIZE)
3939 #define KILN_TRY_DEPTH_ADDR (KILN_GLOBALS_ADDR + 8)
4040 #define KILN_TRY_HANDLERS_ADDR (KILN_TRY_DEPTH_ADDR + KILN_TRY_DEPTH_SIZE)
4141 #define KILN_ARGV_ADDR (KILN_TRY_HANDLERS_ADDR + KILN_TRY_HANDLERS_SIZE)
42+#define KILN_MODSTATE_ADDR (KILN_ARGV_ADDR + KILN_ARGV_SIZE)
4243 #define KILN_CODE_START_OFFSET (KILN_EHDR_SIZE + KILN_PHDR_SIZE + KILN_GLOBALS_SIZE)
4344
4445 // Writes `machine_code` as a minimal, standalone, directly-executable

kiln/src/parser/C/core/parser.cmodified

+67−59
@@ -4,6 +4,7 @@
44 #include "parser/H/core/parser.h"
55 #include "parser/H/core/parser_internal.h"
66 #include "parser/H/declarations/vars.h"
7+#include "parser/H/declarations/module_state.h"
78 #include "parser/H/statements/loop_stack.h"
89 #include "parser/H/declarations/functions.h"
910 #include "parser/H/imports/imports.h"
@@ -77,26 +78,14 @@ static void local_statement(void) {
7778 // that file's own functions -- the sibling guard further down in
7879 // statement() already gets this right, this one didn't.
7980 if (ns_len > 0 && block_depth == 0) {
80- char combined[96];
81- int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s", ns_len, ns, len, name);
82- if (current.type == TOKEN_NUMBER) {
83- char clean[128];
84- int clean_len = 0;
85- for (int i = 0; i < current.length; i++) {
86- if (current.start[i] == '_') continue;
87- clean[clean_len++] = current.start[i];
88- }
89- clean[clean_len] = '\0';
90- double v = strtod(clean, NULL);
91- advance_token();
92- declare_number_constant(combined, combined_len, v);
93- } else if (current.type == TOKEN_STRING) {
94- declare_string_constant(combined, combined_len, current.start, current.length);
95- advance_token();
96- } else {
97- parse_error("imported top-level 'local' must be a literal number or string constant");
98- }
81+ // A top-level `local` of an imported file is a module-level variable: the
82+ // expression runs once, at the point of the `@import`, and the value lives
83+ // in a global slot every function of that file (and `ns.NAME`) can read,
84+ // and `NAME = ...;` can update (see parser/H/declarations/module_state.h).
85+ int state_slot = module_state_declare(name, len);
86+ codegen_expression();
9987 expect(TOKEN_SEMICOLON, "expected ';' after statement");
88+ module_state_emit_store(state_slot);
10089 return;
10190 }
10291
@@ -111,20 +100,46 @@ static void local_statement(void) {
111100 emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
112101 }
113102
103+// A variable that statements can read and write: either a frame slot of the
104+// current function, or (inside an imported file) a module-level variable.
105+typedef struct { int global; int index; } VarRef;
106+
107+static VarRef resolve_ref_or_error(Token id) {
108+ int slot = resolve_var(id.start, id.length);
109+ if (slot != -1) return (VarRef){ 0, slot };
110+ int state = module_state_resolve(id.start, id.length);
111+ if (state != -1) return (VarRef){ 1, state };
112+ char msg[128];
113+ snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
114+ parse_error(msg);
115+ return (VarRef){ 0, -1 };
116+}
117+
118+static void ref_push(VarRef r) {
119+ if (r.global) { module_state_emit_load(r.index); return; }
120+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(r.index));
121+ emit_push_reg(code, REG_RBX);
122+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(r.index));
123+ emit_push_reg(code, REG_RAX);
124+}
125+
126+// Stores RBX (tag) / RAX (payload) into the variable.
127+static void ref_store_regs(VarRef r) {
128+ if (r.global) {
129+ emit_push_reg(code, REG_RBX);
130+ emit_push_reg(code, REG_RAX);
131+ module_state_emit_store(r.index);
132+ return;
133+ }
134+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(r.index), REG_RBX);
135+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(r.index), REG_RAX);
136+}
137+
114138 // IDENT[index] = expr ; -- writes through an existing array OR map
115139 // variable (which one is only known at runtime -- see expr.c's postfix()
116140 // for the read-side version of this same dispatch).
117141 static void index_assignment_statement(Token id) {
118- int slot = resolve_var(id.start, id.length);
119- if (slot == -1) {
120- char msg[128];
121- snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
122- parse_error(msg);
123- }
124- emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(slot));
125- emit_push_reg(code, REG_RBX);
126- emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
127- emit_push_reg(code, REG_RAX);
142+ ref_push(resolve_ref_or_error(id));
128143
129144 advance_token(); // consume '['
130145 codegen_expression();
@@ -171,28 +186,15 @@ static void call_statement(Token id) {
171186 emit_pop_reg(code, REG_RBX);
172187 }
173188
174-static int resolve_var_or_error(Token id) {
175- int slot = resolve_var(id.start, id.length);
176- if (slot == -1) {
177- char msg[128];
178- snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
179- parse_error(msg);
180- }
181- return slot;
182-}
183-
184189 // x += e ; / x -= e ; / x *= e ; / x /= e ; -- sugar for x = x <op> e.
185190 // `+=` reuses codegen_apply_plus() (the exact same tag-aware string-concat-
186191 // or-numeric-add logic `+` itself uses); `-=`/`*=`/`/=` are NUMBER-only,
187192 // same scope limit as `-`/`*`/`/` themselves.
188193 static void compound_assignment_statement(Token id, TokenType op) {
189- int slot = resolve_var_or_error(id);
194+ VarRef ref = resolve_ref_or_error(id);
190195 advance_token(); // consume the += / -= / *= / /=
191196
192- emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(slot));
193- emit_push_reg(code, REG_RBX);
194- emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
195- emit_push_reg(code, REG_RAX);
197+ ref_push(ref);
196198
197199 codegen_expression();
198200 expect(TOKEN_SEMICOLON, "expected ';' after assignment");
@@ -218,19 +220,20 @@ static void compound_assignment_statement(Token id, TokenType op) {
218220
219221 emit_pop_reg(code, REG_RAX); // result payload
220222 emit_pop_reg(code, REG_RBX); // result tag
221- emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
222- emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
223+ ref_store_regs(ref);
223224 }
224225
225226 // x++ ; / x-- ; -- sugar for x = x + 1 / x = x - 1. Statement-only (not
226227 // a pre/post-increment EXPRESSION with a value of its own -- that needs
227228 // primary()/postfix() changes this project's scope doesn't call for).
228229 static void incdec_statement(Token id, TokenType op) {
229- int slot = resolve_var_or_error(id);
230+ VarRef ref = resolve_ref_or_error(id);
230231 advance_token(); // consume ++ or --
231232 expect(TOKEN_SEMICOLON, "expected ';' after statement");
232233
233- emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
234+ ref_push(ref);
235+ emit_pop_reg(code, REG_RAX); // payload
236+ emit_pop_reg(code, REG_RBX); // tag (ignored)
234237 emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
235238 double one = 1.0;
236239 uint64_t one_bits;
@@ -241,8 +244,7 @@ static void incdec_statement(Token id, TokenType op) {
241244 else emit_subsd(code, XMM0, XMM1);
242245 emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
243246 emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
244- emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
245- emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
247+ ref_store_regs(ref);
246248 }
247249
248250 // IDENT = expr ; -- `name` must already be declared via `let`.
@@ -284,11 +286,10 @@ static void assignment_statement(void) {
284286 codegen_expression();
285287 expect(TOKEN_SEMICOLON, "expected ';' after assignment");
286288
287- int slot = resolve_var_or_error(id);
289+ VarRef ref = resolve_ref_or_error(id);
288290 emit_pop_reg(code, REG_RAX); // payload
289291 emit_pop_reg(code, REG_RBX); // tag
290- emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
291- emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
292+ ref_store_regs(ref);
292293 }
293294
294295 // yield [expr] ; -- bare `yield;` returns 0, matching ashc/ashvm.
@@ -306,14 +307,14 @@ static void yield_statement(void) {
306307 emit_mov_reg_imm64(code, REG_RAX, 0); // 0.0's bits are all zero
307308 emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
308309 }
310+ emit_attempt_unwind_all(); // leave open attempts; only touches RCX/RDX, so the result in RAX/RBX survives
309311 emit_mov_reg_reg(code, REG_RSP, REG_RBP);
310312 emit_pop_reg(code, REG_RBP);
311313 emit_ret(code);
312314 }
313315
314-// raise expr ; -- expr must be a STRING (matches attempt/handle's own
315-// convention: every caught error, built-in or user-raised, is a plain
316-// string). Reuses codegen/C/runtime/errors.c's raise routine directly --
316+// raise expr ; -- expr may be any value: a string (what every built-in
317+// runtime error raises) or, say, a map for structured errors. Reuses codegen/C/runtime/errors.c's raise routine directly --
317318 // the exact same handle/unwind mechanism M9's built-in errors (array
318319 // bounds, missing map key) already go through, just with a runtime
319320 // message instead of a compile-time-constant one. (The runtime "raise
@@ -324,9 +325,12 @@ static void raise_statement(void) {
324325 codegen_expression();
325326 expect(TOKEN_SEMICOLON, "expected ';' after 'raise'");
326327
327- emit_pop_reg(code, REG_RSI); // message payload (assumed STRING)
328- emit_pop_reg(code, REG_RBX); // message tag (ignored)
329- emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length
328+ emit_pop_reg(code, REG_RSI); // raised payload
329+ emit_pop_reg(code, REG_RBX); // raised tag
330+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
331+ int not_text = emit_jcc_rel32(code, COND_NE);
332+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length (strings only)
333+ emit_patch_jump(code, not_text);
330334 errors_emit_die_dynamic(code);
331335 }
332336
@@ -337,6 +341,7 @@ static void raise_statement(void) {
337341 static void stop_statement(void) {
338342 advance_token();
339343 expect(TOKEN_SEMICOLON, "expected ';' after 'stop'");
344+ emit_attempt_unwind_to_loop();
340345 loop_record_stop(emit_jmp_rel32(code));
341346 }
342347
@@ -347,6 +352,7 @@ static void stop_statement(void) {
347352 static void next_statement(void) {
348353 advance_token();
349354 expect(TOKEN_SEMICOLON, "expected ';' after 'next'");
355+ emit_attempt_unwind_to_loop();
350356 loop_record_next(emit_jmp_rel32(code));
351357 }
352358
@@ -394,6 +400,7 @@ void statement(void) {
394400 void compile_program(const char *source, const char *source_path, CodeBuf *out) {
395401 code = out;
396402 code_init(code);
403+ prescan_functions(source);
397404 lexer_init(source);
398405 advance_token();
399406 imports_init(source_path);
@@ -429,5 +436,6 @@ void compile_program(const char *source, const char *source_path, CodeBuf *out)
429436 errors_emit_startup(code);
430437
431438 while (current.type != TOKEN_EOF) statement();
439+ check_all_functions_defined();
432440 codegen_exit0(code);
433441 }

kiln/src/parser/C/declarations/functions.cmodified

+100−2
@@ -2,6 +2,9 @@
22 #include <stdio.h>
33 #include "parser/H/declarations/functions.h"
44 #include "parser/H/core/parser.h"
5+#include "lexer/H/lexer.h"
6+#include "codegen/H/emit/emit.h"
7+#include "codegen/H/emit/layout.h"
58
69 static KilnFunction functions[MAX_KILN_FUNCS];
710 static int function_count = 0;
@@ -25,6 +28,7 @@ KilnFunction *declare_function(const char *name, int len) {
2528 fn->name[len] = '\0';
2629 fn->code_offset = 0;
2730 fn->arity = 0;
31+ fn->defined = 1;
2832 return fn;
2933 }
3034
@@ -49,12 +53,106 @@ static int build_namespaced_name(char *buf, int buf_size, const char *name, int
4953 return g_import_ns_len + 1 + len;
5054 }
5155
56+// A prescan (prescan_functions below) may already have reserved the name so
57+// earlier code could call it; the real declaration then fills that entry in.
58+static KilnFunction *declare_or_define(const char *name, int len) {
59+ KilnFunction *existing = resolve_function(name, len);
60+ if (existing != NULL && !existing->defined) {
61+ existing->defined = 1;
62+ return existing;
63+ }
64+ return declare_function(name, len);
65+}
66+
5267 KilnFunction *declare_function_in_context(const char *name, int len) {
53- if (g_import_ns_len == 0) return declare_function(name, len);
68+ if (g_import_ns_len == 0) return declare_or_define(name, len);
5469 char buf[128];
5570 int combined_len = build_namespaced_name(buf, sizeof(buf), name, len);
5671 if (combined_len < 0) parse_error("function name too long (including namespace prefix)");
57- return declare_function(buf, combined_len);
72+ return declare_or_define(buf, combined_len);
73+}
74+
75+void predeclare_function_in_context(const char *name, int len, int arity) {
76+ const char *final_name = name;
77+ int final_len = len;
78+ char buf[128];
79+ if (g_import_ns_len > 0) {
80+ final_len = build_namespaced_name(buf, sizeof(buf), name, len);
81+ if (final_len < 0) return; // the real declaration reports it
82+ final_name = buf;
83+ }
84+ if (resolve_function(final_name, final_len) != NULL) return;
85+ if (function_count >= MAX_KILN_FUNCS || final_len >= (int)sizeof(functions[0].name)) return;
86+ KilnFunction *fn = &functions[function_count++];
87+ memcpy(fn->name, final_name, (size_t)final_len);
88+ fn->name[final_len] = '\0';
89+ fn->code_offset = 0;
90+ fn->arity = arity;
91+ fn->defined = 0;
92+}
93+
94+void prescan_functions(const char *source) {
95+ LexerState saved = lexer_save_state();
96+ lexer_init(source);
97+ Token t = lexer_next_token();
98+ while (t.type != TOKEN_EOF) {
99+ if (t.type == TOKEN_FORGE) {
100+ Token name = lexer_next_token();
101+ if (name.type != TOKEN_IDENTIFIER) { t = name; continue; }
102+ Token paren = lexer_next_token();
103+ if (paren.type != TOKEN_LPAREN) { t = paren; continue; }
104+ int arity = 0;
105+ t = lexer_next_token();
106+ if (t.type != TOKEN_RPAREN) {
107+ arity = 1;
108+ while (t.type != TOKEN_RPAREN && t.type != TOKEN_EOF) {
109+ if (t.type == TOKEN_COMMA) arity++;
110+ t = lexer_next_token();
111+ }
112+ }
113+ predeclare_function_in_context(name.start, name.length, arity);
114+ }
115+ if (t.type != TOKEN_EOF) t = lexer_next_token();
116+ }
117+ lexer_restore_state(saved);
118+}
119+
120+#define MAX_KILN_FIXUPS 4096
121+static struct { KilnFunction *fn; int patch_offset; int absolute; } fixups[MAX_KILN_FIXUPS];
122+static int fixup_count = 0;
123+
124+static void add_fixup(KilnFunction *fn, int patch_offset, int absolute) {
125+ if (fixup_count >= MAX_KILN_FIXUPS) parse_error("too many calls to functions defined later");
126+ fixups[fixup_count].fn = fn;
127+ fixups[fixup_count].patch_offset = patch_offset;
128+ fixups[fixup_count].absolute = absolute;
129+ fixup_count++;
130+}
131+void function_add_call_fixup(KilnFunction *fn, int patch_offset) { add_fixup(fn, patch_offset, 0); }
132+void function_add_abs_fixup(KilnFunction *fn, int patch_offset) { add_fixup(fn, patch_offset, 1); }
133+
134+void function_resolve_fixups(KilnFunction *fn) {
135+ int kept = 0;
136+ for (int i = 0; i < fixup_count; i++) {
137+ if (fixups[i].fn != fn) { fixups[kept++] = fixups[i]; continue; }
138+ if (fixups[i].absolute) {
139+ emit_patch_imm64(code, fixups[i].patch_offset, kiln_code_base() + (uint64_t)fn->code_offset);
140+ } else {
141+ int32_t rel = fn->code_offset - (fixups[i].patch_offset + 4);
142+ memcpy(code->code + fixups[i].patch_offset, &rel, 4);
143+ }
144+ }
145+ fixup_count = kept;
146+}
147+
148+void check_all_functions_defined(void) {
149+ for (int i = 0; i < function_count; i++) {
150+ if (!functions[i].defined) {
151+ char msg[128];
152+ snprintf(msg, sizeof(msg), "undefined function: %s", functions[i].name);
153+ parse_error(msg);
154+ }
155+ }
58156 }
59157
60158 KilnFunction *resolve_function_in_context(const char *name, int len) {

kiln/src/parser/C/declarations/module_state.cadded

+58−0
@@ -0,0 +1,58 @@
1+#include <stdio.h>
2+#include <string.h>
3+#include "parser/H/declarations/module_state.h"
4+#include "parser/H/declarations/functions.h"
5+#include "parser/H/declarations/constants.h"
6+#include "parser/H/core/parser.h"
7+#include "codegen/H/emit/emit.h"
8+#include "codegen/H/emit/layout.h"
9+#include "codegen/H/emit/runtime_layout.h"
10+
11+static char names[KILN_MODSTATE_SLOTS][96];
12+static int count = 0;
13+
14+int module_state_resolve_qualified(const char *name, int len) {
15+ for (int i = 0; i < count; i++) {
16+ if ((int)strlen(names[i]) == len && strncmp(names[i], name, (size_t)len) == 0) return i;
17+ }
18+ return -1;
19+}
20+
21+int module_state_resolve(const char *name, int len) {
22+ const char *ns; int ns_len;
23+ get_import_namespace(&ns, &ns_len);
24+ if (ns_len == 0) return -1;
25+ char buf[128];
26+ int n = snprintf(buf, sizeof(buf), "%.*s.%.*s", ns_len, ns, len, name);
27+ if (n < 0 || n >= (int)sizeof(buf)) return -1;
28+ return module_state_resolve_qualified(buf, n);
29+}
30+
31+int module_state_declare(const char *name, int len) {
32+ const char *ns; int ns_len;
33+ get_import_namespace(&ns, &ns_len);
34+ char buf[128];
35+ int n = snprintf(buf, sizeof(buf), "%.*s.%.*s", ns_len, ns, len, name);
36+ if (n < 0 || n >= (int)sizeof(names[0])) parse_error("module-level name too long (including namespace prefix)");
37+ if (module_state_resolve_qualified(buf, n) != -1 || resolve_constant(buf, n) != NULL) parse_error("module-level name already declared");
38+ if (count >= KILN_MODSTATE_SLOTS) parse_error("too many module-level variables");
39+ memcpy(names[count], buf, (size_t)n);
40+ names[count][n] = '\0';
41+ return count++;
42+}
43+
44+void module_state_emit_load(int index) {
45+ emit_mov_reg_imm64(code, REG_RSI, kiln_modstate_addr() + 16u * (unsigned)index);
46+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 0);
47+ emit_push_reg(code, REG_RBX);
48+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
49+ emit_push_reg(code, REG_RAX);
50+}
51+
52+void module_state_emit_store(int index) {
53+ emit_pop_reg(code, REG_RAX); // payload
54+ emit_pop_reg(code, REG_RBX); // tag
55+ emit_mov_reg_imm64(code, REG_RSI, kiln_modstate_addr() + 16u * (unsigned)index);
56+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RBX);
57+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
58+}

kiln/src/parser/C/imports/imports.cmodified

+15−0
@@ -129,6 +129,19 @@ void import_statement(void) {
129129 parse_error(message);
130130 }
131131
132+ // Optional `as name`: the namespace this file's functions are called through,
133+ // instead of the one derived from its file name.
134+ char alias[64];
135+ int has_alias = 0;
136+ if (current.type == TOKEN_IDENTIFIER && current.length == 2 && strncmp(current.start, "as", 2) == 0) {
137+ advance_token();
138+ expect(TOKEN_IDENTIFIER, "expected a namespace name after 'as'");
139+ if (previous.length >= (int)sizeof(alias)) parse_error("namespace name too long");
140+ memcpy(alias, previous.start, (size_t)previous.length);
141+ alias[previous.length] = '\0';
142+ has_alias = 1;
143+ }
144+
132145 expect(TOKEN_SEMICOLON, "expected ';' after import path");
133146
134147 // Cycle check MUST come before the dedup check: a file already in
@@ -146,6 +159,7 @@ void import_statement(void) {
146159
147160 char ns[64];
148161 import_derive_namespace(canonical, ns, sizeof(ns));
162+ if (has_alias) { memcpy(ns, alias, strlen(alias) + 1); }
149163
150164 ImportedFile *collision = registry_find_by_namespace(ns);
151165 if (collision != NULL) {
@@ -198,6 +212,7 @@ void import_statement(void) {
198212 import_path_dirname(canonical, current_file_dir, sizeof(current_file_dir));
199213 set_import_namespace(entry->namespace_name, (int)strlen(entry->namespace_name));
200214
215+ prescan_functions(imported_source);
201216 lexer_init(imported_source);
202217 advance_token();
203218 parse_imported_file_body();

kiln/src/parser/C/statements/attempt_handle.cmodified

+11−4
@@ -1,6 +1,7 @@
11 #include "parser/H/core/parser.h"
22 #include "parser/H/core/parser_internal.h"
33 #include "parser/H/declarations/vars.h"
4+#include "parser/H/statements/loop_stack.h"
45 #include "codegen/H/emit/emit.h"
56 #include "codegen/H/runtime/errors.h"
67 #include "codegen/H/emit/layout.h"
@@ -23,6 +24,13 @@
2324 // is actually seen. attempt/handle is inline (like given/during), not a
2425 // new call frame, so this works the same way given/during's own bodies
2526 // share the enclosing function's variable table.
27+void attempt_emit_handler_pop(void) {
28+ emit_mov_reg_imm64(code, REG_RDX, kiln_try_depth_addr());
29+ emit_load_mem_disp32(code, REG_RCX, REG_RDX, 0);
30+ emit_dec_reg(code, REG_RCX);
31+ emit_store_mem_disp32(code, REG_RDX, 0, REG_RCX);
32+}
33+
2634 void attempt_statement(void) {
2735 VarBlockScope saved_scope = vars_scope_begin();
2836 advance_token(); // consume 'attempt'
@@ -58,15 +66,14 @@ void attempt_statement(void) {
5866 errors_emit_fatal(code, "runtime error: too many nested try blocks");
5967 emit_patch_jump(code, skip_overflow_die);
6068
69+ attempt_enter();
6170 block(); // compiles A
71+ attempt_leave();
6272
6373 // Normal completion (no error was raised): pop the handler -- past
6474 // this point, an error inside the enclosing scope should NOT be
6575 // caught by this attempt anymore.
66- emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());
67- emit_load_mem_disp32(code, REG_RCX, REG_RAX, 0);
68- emit_dec_reg(code, REG_RCX);
69- emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
76+ attempt_emit_handler_pop();
7077
7178 int skip_handle = emit_jmp_rel32(code);
7279 int handle_code_offset = code->count;

kiln/src/parser/C/statements/loop_stack.cmodified

+20−3
@@ -9,15 +9,18 @@ typedef struct {
99 int stop_count;
1010 int next_jumps[MAX_LOOP_JUMPS];
1111 int next_count;
12+ int attempt_base; // attempt_depth when the loop began
1213 } LoopContext;
1314
1415 static LoopContext loop_stack[MAX_LOOP_NESTING];
1516 static int loop_depth = 0;
17+static int attempt_depth = 0; // attempt blocks currently open in this function body
1618
1719 void loop_push(void) {
1820 if (loop_depth >= MAX_LOOP_NESTING) parse_error("too many nested loops");
1921 loop_stack[loop_depth].stop_count = 0;
2022 loop_stack[loop_depth].next_count = 0;
23+ loop_stack[loop_depth].attempt_base = attempt_depth;
2124 loop_depth++;
2225 }
2326
@@ -46,6 +49,20 @@ void loop_pop_and_patch_stops(void) {
4649 loop_depth--;
4750 }
4851
49-void loop_depth_save(int *out) { *out = loop_depth; }
50-void loop_depth_reset(void) { loop_depth = 0; }
51-void loop_depth_restore(int saved) { loop_depth = saved; }
52+void loop_scope_save(LoopScope *out) { out->loops = loop_depth; out->attempts = attempt_depth; }
53+void loop_scope_reset(void) { loop_depth = 0; attempt_depth = 0; }
54+void loop_scope_restore(LoopScope saved) { loop_depth = saved.loops; attempt_depth = saved.attempts; }
55+
56+void attempt_enter(void) { attempt_depth++; }
57+void attempt_leave(void) { attempt_depth--; }
58+
59+static void emit_handler_pops(int count) {
60+ for (int i = 0; i < count; i++) attempt_emit_handler_pop();
61+}
62+
63+void emit_attempt_unwind_all(void) { emit_handler_pops(attempt_depth); }
64+
65+// stop/next leave every attempt block opened since the innermost loop began.
66+void emit_attempt_unwind_to_loop(void) {
67+ if (loop_depth > 0) emit_handler_pops(attempt_depth - loop_stack[loop_depth - 1].attempt_base);
68+}

kiln/src/parser/C/statements/parser_control.cmodified

+7−19
@@ -9,19 +9,6 @@
99 #include "codegen/H/emit/emit_sse.h"
1010 #include "codegen/H/emit/value.h"
1111
12-// Truthiness test for given/during conditions: pop the value (tag
13-// discarded, assumed NUMBER -- matches ashvm's own truthy(), which
14-// requires a number too) and compare its payload against 0.0 via ucomisd
15-// (not GP cmp -- see emit_sse.h). COND_E afterward means "was zero, i.e.
16-// falsy".
17-static void pop_and_test_truthy(void) {
18- emit_pop_reg(code, REG_RAX); // payload
19- emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
20- emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
21- emit_pxor_xmm_xmm(code, XMM1); // XMM1 = +0.0
22- emit_ucomisd(code, XMM0, XMM1);
23-}
24-
2512 // Conditions may be parenthesized or not (`given (x < n) {` and
2613 // `given x < n {` both work) -- unlike ashvm, there's no superinstruction
2714 // fusion here to keep firing either way, so this is just a plain optional
@@ -42,7 +29,7 @@ void given_statement(void) {
4229 int parenthesized = consume_optional_lparen();
4330 codegen_expression();
4431 if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");
45- pop_and_test_truthy();
32+ codegen_pop_and_test_truthy();
4633 int otherwise_jump = emit_jcc_rel32(code, COND_E);
4734
4835 block();
@@ -63,7 +50,7 @@ void during_statement(void) {
6350 int parenthesized = consume_optional_lparen();
6451 codegen_expression();
6552 if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");
66- pop_and_test_truthy();
53+ codegen_pop_and_test_truthy();
6754 int exit_jump = emit_jcc_rel32(code, COND_E);
6855
6956 loop_push();
@@ -127,16 +114,17 @@ void forge_statement(void) {
127114
128115 int skip_jump = emit_jmp_rel32(code);
129116 fn->code_offset = code->count;
117+ function_resolve_fixups(fn);
130118
131119 VarScope outer_vars;
132120 vars_save(&outer_vars);
133121 vars_clear();
134122 int outer_each_depth;
135123 each_depth_save(&outer_each_depth);
136124 each_depth_reset();
137- int outer_loop_depth;
138- loop_depth_save(&outer_loop_depth);
139- loop_depth_reset();
125+ LoopScope outer_loop_depth;
126+ loop_scope_save(&outer_loop_depth);
127+ loop_scope_reset();
140128
141129 emit_push_reg(code, REG_RBP);
142130 emit_mov_reg_reg(code, REG_RBP, REG_RSP);
@@ -173,6 +161,6 @@ void forge_statement(void) {
173161
174162 vars_restore(&outer_vars);
175163 each_depth_restore(outer_each_depth);
176- loop_depth_restore(outer_loop_depth);
164+ loop_scope_restore(outer_loop_depth);
177165 emit_patch_jump(code, skip_jump);
178166 }

kiln/src/parser/D/core/parser.dmodified

+2−0
@@ -1,6 +1,7 @@
11 src/parser/O/core/parser.o: src/parser/C/core/parser.c \
22 src/parser/H/core/parser.h src/lexer/H/lexer.h src/codegen/H/emit/emit.h \
33 src/parser/H/core/parser_internal.h src/parser/H/declarations/vars.h \
4+ src/parser/H/declarations/module_state.h \
45 src/parser/H/statements/loop_stack.h \
56 src/parser/H/declarations/functions.h src/parser/H/imports/imports.h \
67 src/parser/H/declarations/constants.h src/codegen/H/expressions/expr.h \
@@ -16,6 +17,7 @@ src/lexer/H/lexer.h:
1617 src/codegen/H/emit/emit.h:
1718 src/parser/H/core/parser_internal.h:
1819 src/parser/H/declarations/vars.h:
20+src/parser/H/declarations/module_state.h:
1921 src/parser/H/statements/loop_stack.h:
2022 src/parser/H/declarations/functions.h:
2123 src/parser/H/imports/imports.h:

kiln/src/parser/D/declarations/functions.dmodified

+3−1
@@ -1,8 +1,10 @@
11 src/parser/O/declarations/functions.o: \
22 src/parser/C/declarations/functions.c \
33 src/parser/H/declarations/functions.h src/parser/H/core/parser.h \
4- src/lexer/H/lexer.h src/codegen/H/emit/emit.h
4+ src/lexer/H/lexer.h src/codegen/H/emit/emit.h \
5+ src/codegen/H/emit/layout.h
56 src/parser/H/declarations/functions.h:
67 src/parser/H/core/parser.h:
78 src/lexer/H/lexer.h:
89 src/codegen/H/emit/emit.h:
10+src/codegen/H/emit/layout.h:

kiln/src/parser/D/declarations/module_state.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/parser/O/declarations/module_state.o: \
2+ src/parser/C/declarations/module_state.c \
3+ src/parser/H/declarations/module_state.h \
4+ src/parser/H/declarations/functions.h \
5+ src/parser/H/declarations/constants.h src/parser/H/core/parser.h \
6+ src/lexer/H/lexer.h src/codegen/H/emit/emit.h \
7+ src/codegen/H/emit/layout.h src/codegen/H/emit/runtime_layout.h
8+src/parser/H/declarations/module_state.h:
9+src/parser/H/declarations/functions.h:
10+src/parser/H/declarations/constants.h:
11+src/parser/H/core/parser.h:
12+src/lexer/H/lexer.h:
13+src/codegen/H/emit/emit.h:
14+src/codegen/H/emit/layout.h:
15+src/codegen/H/emit/runtime_layout.h:

kiln/src/parser/D/statements/attempt_handle.dmodified

+3−2
@@ -2,13 +2,14 @@ src/parser/O/statements/attempt_handle.o: \
22 src/parser/C/statements/attempt_handle.c src/parser/H/core/parser.h \
33 src/lexer/H/lexer.h src/codegen/H/emit/emit.h \
44 src/parser/H/core/parser_internal.h src/parser/H/declarations/vars.h \
5- src/codegen/H/runtime/errors.h src/codegen/H/emit/layout.h \
6- src/codegen/H/emit/runtime_layout.h
5+ src/parser/H/statements/loop_stack.h src/codegen/H/runtime/errors.h \
6+ src/codegen/H/emit/layout.h src/codegen/H/emit/runtime_layout.h
77 src/parser/H/core/parser.h:
88 src/lexer/H/lexer.h:
99 src/codegen/H/emit/emit.h:
1010 src/parser/H/core/parser_internal.h:
1111 src/parser/H/declarations/vars.h:
12+src/parser/H/statements/loop_stack.h:
1213 src/codegen/H/runtime/errors.h:
1314 src/codegen/H/emit/layout.h:
1415 src/codegen/H/emit/runtime_layout.h:

kiln/src/parser/H/declarations/functions.hmodified

+17−1
@@ -13,15 +13,31 @@
1313 // programs room to import a handful of libraries without hitting the cap
1414 // immediately; still a hard ceiling, noted as a tuning knob in the design
1515 // spec, not solved generally here.
16-#define MAX_KILN_FUNCS 64
16+#define MAX_KILN_FUNCS 1024
1717 #define MAX_KILN_PARAMS 8
1818
1919 typedef struct {
2020 char name[64];
2121 int code_offset;
2222 int arity;
23+ int defined; // 0 while only forward-declared by the prescan; code_offset is valid once 1
2324 } KilnFunction;
2425
26+// --- Forward declarations ---
27+// Before a file is compiled, prescan_functions() reserves every named
28+// `forge name(...)` in it (with its arity), so calls may appear before the
29+// definition and functions may call each other. A call to a not-yet-defined
30+// function emits a placeholder that function_add_call_fixup /
31+// function_add_abs_fixup record; forge_statement resolves them the moment the
32+// body's start is known (function_resolve_fixups). check_all_functions_defined()
33+// reports a reserved name that never got defined.
34+void prescan_functions(const char *source);
35+void predeclare_function_in_context(const char *name, int len, int arity);
36+void function_add_call_fixup(KilnFunction *fn, int patch_offset); // rel32 of a `call`
37+void function_add_abs_fixup(KilnFunction *fn, int patch_offset); // imm64 holding the absolute address
38+void function_resolve_fixups(KilnFunction *fn);
39+void check_all_functions_defined(void);
40+
2541 // Returns a pointer to the function's entry, or NULL if not declared.
2642 KilnFunction *resolve_function(const char *name, int len);
2743

kiln/src/parser/H/declarations/module_state.hadded

+25−0
@@ -0,0 +1,25 @@
1+#ifndef KILN_MODULE_STATE_H
2+#define KILN_MODULE_STATE_H
3+
4+// Module-level state: a top-level `local NAME = <expression>;` in an imported
5+// file whose value is not a plain number/string literal. The expression runs
6+// once, where the `@import` appears (so before the importing file's own code),
7+// and the value lives in a numbered slot in the globals region instead of a
8+// frame, so every function of that file -- and `ns.NAME` from the importer --
9+// can read it, and `NAME = ...;`, `NAME += ...;`, `NAME[i] = ...;` update it.
10+// Slots are named under the file's namespace ("ns.NAME"), like imported
11+// functions and constants.
12+
13+// Reserves a slot for NAME in the current import namespace; returns its index.
14+int module_state_declare(const char *name, int len);
15+
16+// A bare NAME inside an imported file (resolved in that file's namespace), or
17+// a fully qualified "ns.NAME". Return the slot index or -1.
18+int module_state_resolve(const char *name, int len);
19+int module_state_resolve_qualified(const char *name, int len);
20+
21+// Push the slot's (tag, payload) / pop a (tag, payload) into it.
22+void module_state_emit_load(int index);
23+void module_state_emit_store(int index);
24+
25+#endif

kiln/src/parser/H/statements/loop_stack.hmodified

+17−3
@@ -40,8 +40,22 @@ void loop_pop_and_patch_stops(void);
4040 // each_depth_save/reset/restore): a stop/next inside a function or
4141 // lambda body must never reach past that body into an ENCLOSING loop it
4242 // happens to be textually written inside.
43-void loop_depth_save(int *out);
44-void loop_depth_reset(void);
45-void loop_depth_restore(int saved);
43+typedef struct { int loops; int attempts; } LoopScope;
44+void loop_scope_save(LoopScope *out);
45+void loop_scope_reset(void);
46+void loop_scope_restore(LoopScope saved);
47+
48+// An attempt block pushes a runtime handler that only its own normal
49+// fallthrough pops, so any other way out of the block -- yield, stop, next --
50+// must pop it explicitly or a later raise jumps into the stale handler.
51+// attempt_enter/leave bracket the attempt block (not the handle block); the
52+// emit_* calls put one handler pop per open attempt right before the exit.
53+void attempt_enter(void);
54+void attempt_leave(void);
55+void emit_attempt_unwind_all(void); // yield: leave every open attempt
56+void emit_attempt_unwind_to_loop(void); // stop/next: leave those inside the loop
57+
58+// Emits the code that pops the innermost runtime handler (attempt_handle.c).
59+void attempt_emit_handler_pop(void);
4660
4761 #endif

kiln/src/pe/H/pe_imports.defmodified

+6−0
@@ -14,3 +14,9 @@ PE_IMPORT(GET_FILE_SIZE, "GetFileSize")
1414 PE_IMPORT(VIRTUAL_ALLOC, "VirtualAlloc")
1515 PE_IMPORT(GET_COMMAND_LINE_A, "GetCommandLineA")
1616 PE_IMPORT(GET_STD_HANDLE, "GetStdHandle")
17+PE_IMPORT(MOVE_FILE_A, "MoveFileA")
18+PE_IMPORT(DELETE_FILE_A, "DeleteFileA")
19+PE_IMPORT(CREATE_DIRECTORY_A, "CreateDirectoryA")
20+PE_IMPORT(FIND_FIRST_FILE_A, "FindFirstFileA")
21+PE_IMPORT(FIND_NEXT_FILE_A, "FindNextFileA")
22+PE_IMPORT(FIND_CLOSE, "FindClose")

kiln/src/pe/H/pe_writer.hmodified

+2−1
@@ -34,7 +34,8 @@
3434 // No argv slot: GetCommandLineA() can be called at any point, unlike
3535 // Linux's argv() which has to snapshot the kernel's original RSP before
3636 // anything else touches it (see codegen/C/io/argv_builtin.c).
37-#define KILN_PE_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE)
37+#define KILN_PE_MODSTATE_ADDR (KILN_PE_TRY_HANDLERS_ADDR + KILN_TRY_HANDLERS_SIZE)
38+#define KILN_PE_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_MODSTATE_SIZE)
3839
3940 // Every kernel32.dll function kiln's Windows codegen calls -- single list
4041 // in pe_imports.def, shared by this enum and pe_writer.c's actual table

kiln/tests/cases/basics/booleans.ashadded

+35−0
@@ -0,0 +1,35 @@
1+say yes;
2+say no;
3+say 1 < 2;
4+say 2 < 1;
5+say not yes;
6+say not 0;
7+say 1 == 1;
8+say "a" == "a";
9+say "a" == "b";
10+say 1 and 1;
11+say 0 or 1;
12+say yes == 1;
13+say yes == yes;
14+say type(yes);
15+say type(1 < 2);
16+say [yes, no, 3 > 4];
17+say {"ok": yes};
18+say str(no) + str(yes);
19+say has({"a": 1}, "a");
20+say has({"a": 1}, "b");
21+say yes + 1;
22+say file_exists("/nonexistent");
23+say contains("abc", "b");
24+local flag = 5 > 3;
25+given flag { say "flag set"; }
26+given not flag { say "bad"; } otherwise { say "ok"; }
27+say flag and not no;
28+say none == none;
29+say not none;
30+attempt { given {"a": 1} { say "bad"; } } handle (e) { say "map condition"; }
31+attempt { given "x" { say "bad"; } } handle (e) { say "string condition"; }
32+local n = 0;
33+during n < 3 { n++; }
34+say n;
35+say yes ? "t" : "f";

kiln/tests/cases/basics/exponent_literals.ashadded

+6−0
@@ -0,0 +1,6 @@
1+say 1e3;
2+say 1.5e-3;
3+say 2E2 + 1;
4+say num("-1.5e2");
5+say num("1e3") + 1;
6+say num("2.5E-1");

kiln/tests/cases/basics/number_format.ashadded

+26−0
@@ -0,0 +1,26 @@
1+say 0;
2+say 3;
3+say -42;
4+say 0.5;
5+say -0.25;
6+say 3.14159265;
7+say 0.1 + 0.2;
8+say 123456789012;
9+say 999999999999999;
10+say 1e15;
11+say 1e21;
12+say -1e21;
13+say 1.5e-7;
14+say 1e-7;
15+say 0.000001;
16+say 0.0000005;
17+say 123456.789;
18+say 1e100;
19+say 1.2345678e30;
20+say 9.9999996;
21+say 0.9999999;
22+say 2.5e-300;
23+say 1 / 0;
24+say -1 / 0;
25+say str(1e21) + "|" + str(1.5) + "|" + str(-7);
26+say [1.5, 1e21, 2];

kiln/tests/cases/collections/array_mutation.ashadded

+16−0
@@ -0,0 +1,16 @@
1+local a = [1, 2, 3, 4];
2+delete(a, 1);
3+say a;
4+say pop(a);
5+say a;
6+insert(a, 0, 9);
7+insert(a, len(a), 7);
8+insert(a, 1, 8);
9+say a;
10+say slice(a, 1, 3);
11+say slice(a, 0, 0);
12+attempt { pop([]); } handle (e) { say "empty"; }
13+attempt { delete(a, 10); } handle (e) { say "oob"; }
14+local m = {"k": 1};
15+delete(m, "k");
16+say len(m);

kiln/tests/cases/collections/map_order.ashadded

+30−0
@@ -0,0 +1,30 @@
1+local m = {};
2+m["b"] = 1; m["zeta"] = 2; m["alpha"] = 3; m["a"] = 4; m["mid"] = 5;
3+say keys(m);
4+say values(m);
5+delete(m, "alpha");
6+say keys(m);
7+m["alpha"] = 9;
8+m["b"] = 100;
9+say keys(m);
10+say m;
11+delete(m, "b");
12+delete(m, "zzz");
13+say keys(m);
14+say len(m);
15+say has(m, "b");
16+say has(m, "mid");
17+local big = {};
18+local i = 0;
19+during i < 100 { big[str(i)] = i; i++; }
20+i = 0;
21+during i < 100 { given i % 2 == 0 { delete(big, str(i)); } i++; }
22+say len(big);
23+say big["51"];
24+say keys(big)[0];
25+say keys(big)[49];
26+i = 0;
27+during i < 100 { given i % 2 == 0 { big[str(i)] = i; } i++; }
28+say len(big);
29+say keys(big)[50];
30+say big["98"];

kiln/tests/cases/collections/sort.ashadded

+20−0
@@ -0,0 +1,20 @@
1+local a = [5, 2, 9, 1, 5, 6, -3, 0];
2+sort(a);
3+say a;
4+local s = ["pear", "apple", "fig", "banana", "apple", ""];
5+say sort(s);
6+local m = [{"k": 2, "t": "a"}, {"k": 1, "t": "b"}, {"k": 2, "t": "c"}, {"k": 1, "t": "d"}];
7+sort(m, forge (x, y) { yield x["k"] - y["k"]; });
8+say map(m, forge (x) { yield x["t"]; });
9+say sort([3, 1, 2], forge (x, y) { yield y - x; });
10+say sort([]);
11+say sort([1]);
12+local big = [];
13+local i = 0;
14+during i < 200 { push(big, (i * 37) % 101); i++; }
15+sort(big);
16+local ok = yes;
17+i = 1;
18+during i < len(big) { given big[i - 1] > big[i] { ok = no; } i++; }
19+say ok;
20+say len(big);

kiln/tests/cases/control_flow/short_circuit.ashadded

+11−0
@@ -0,0 +1,11 @@
1+local a = [1, 2];
2+given 5 < len(a) and a[5] == 1 { say "bad"; } otherwise { say "ok and"; }
3+given 0 < len(a) or a[5] == 1 { say "ok or"; }
4+say 1 and 2;
5+say 0 or 3;
6+say 0 and 1;
7+say 1 or 0;
8+say 1 and 0 or 1;
9+local n = 0;
10+during n < 5 and n != 3 { n++; }
11+say n;

kiln/tests/cases/errors/attempt_array_delete.ashadded

+10−0
@@ -0,0 +1,10 @@
1+local a = [1, 2, 3];
2+attempt { delete(a, 0); say "deleted"; say a; push(a, 9); delete(a, 99); say "unreachable"; } handle (e) { say "caught: " + e; }
3+say a;
4+attempt { local m = {"x": 1}; delete(m, "x"); say m; say m["x"]; } handle (e) { say "caught: " + e; }
5+local i = 0;
6+during i < 3 {
7+ attempt { delete(a, 10); } handle (e) { say "again: " + e; }
8+ i++;
9+}
10+say "end";

kiln/tests/cases/errors/attempt_stale_handler.ashadded

+21−0
@@ -0,0 +1,21 @@
1+forge f() {
2+ local i = 0;
3+ during i < 3 {
4+ attempt { local x = 1; } handle (e) { say "inner"; }
5+ i++;
6+ }
7+ raise "boom";
8+}
9+attempt { f(); } handle (e) { say "outer: " + e; }
10+forge g() {
11+ attempt { yield 5; } handle (e) { say "no"; }
12+}
13+forge h() { local v = g(); raise "boom2"; }
14+attempt { h(); } handle (e) { say "outer: " + e; }
15+local i = 0;
16+during i < 3 {
17+ i++;
18+ attempt { next; } handle (e) { say "no"; }
19+}
20+attempt { raise "second"; } handle (e) { say "caught " + e; }
21+say "done";

kiln/tests/cases/errors/error_message_field.ashadded

+5−0
@@ -0,0 +1,5 @@
1+attempt { local a = [1]; say a[3]; } handle (e) { say e.message; say "x: " + e; }
2+attempt { raise {"message": "custom", "line": 3}; } handle (e) { say e.message; say e.line; }
3+attempt { raise "plain"; } handle (e) { say e.message == e; }
4+local m = {"inner": {"deep": 7}};
5+say m.inner.deep;

kiln/tests/cases/errors/error_messages.ashadded

+23−0
@@ -0,0 +1,23 @@
1+local a = [1, 2];
2+attempt { say a[5]; } handle (e) { say e; }
3+attempt { say {"k": 1}["zz"]; } handle (e) { say e; }
4+attempt { say "abc"[9]; } handle (e) { say e; }
5+attempt { say "x" + 1; } handle (e) { say e; }
6+attempt { say 1 + "x"; } handle (e) { say e; }
7+attempt { say str("t"); } handle (e) { say e; }
8+attempt { pop([]); } handle (e) { say e; }
9+attempt { delete([1], 3); } handle (e) { say e; }
10+attempt { insert([1], 3, 0); } handle (e) { say e; }
11+attempt { slice([1, 2], 1, 5); } handle (e) { say e; }
12+attempt { chr(0); } handle (e) { say e; }
13+attempt { ord(""); } handle (e) { say e; }
14+attempt { repeat("a", -2); } handle (e) { say e; }
15+attempt { read_file("/nonexistent/file"); } handle (e) { say e; }
16+attempt { list_dir("/nonexistent"); } handle (e) { say e; }
17+attempt { given [1] { say "bad"; } } handle (e) { say e; }
18+attempt { raise "custom"; } handle (e) { say e; }
19+attempt { raise {"kind": "parse", "line": 3, "col": 7}; } handle (e) { say e["kind"] + ":" + str(e["line"]) + ":" + str(e["col"]); }
20+attempt { raise [1, 2]; } handle (e) { say e; }
21+attempt { raise 42; } handle (e) { say e + 1; }
22+forge f() { raise {"m": "inner"}; }
23+attempt { f(); } handle (e) { say e["m"]; }

kiln/tests/cases/errors/type_errors.ashadded

+6−0
@@ -0,0 +1,6 @@
1+attempt { say "x" + 1; } handle (e) { say "caught plus"; }
2+attempt { say 1 + "x"; } handle (e) { say "caught plus2"; }
3+attempt { say str("text"); } handle (e) { say "caught str"; }
4+say "a" + "b";
5+say 1 + 2;
6+say str(5);

kiln/tests/cases/functions/builtins_as_values.ashadded

+9−0
@@ -0,0 +1,9 @@
1+say type(len);
2+say map([1, 22, 333], str);
3+say map(["a", "bb"], len);
4+say map(["ab", "cd"], upper);
5+say filter([1, 2, 3, 4], forge (x) { yield x > 2; });
6+local f = len;
7+say f("hello");
8+say reduce([1, 2, 3], forge (a, b) { yield a + b; }, 0);
9+say map([4, 9], sqrt);

kiln/tests/cases/functions/forward_declarations.ashadded

+20−0
@@ -0,0 +1,20 @@
1+say is_even(10);
2+say is_odd(7);
3+say is_even(7);
4+forge is_even(n) {
5+ given n == 0 { yield yes; }
6+ yield is_odd(n - 1);
7+}
8+forge is_odd(n) {
9+ given n == 0 { yield no; }
10+ yield is_even(n - 1);
11+}
12+say twice(3);
13+forge twice(x) { yield helper(x) * 2; }
14+forge helper(x) { yield x + 1; }
15+local f = helper;
16+say f(5);
17+say map([1, 2], twice);
18+forge shadow(a) { yield a; }
19+local shadow = forge (a) { yield a * 100; };
20+say shadow(2);

kiln/tests/cases/imports/fixtures/imports_state_lib.ashadded

+15−0
@@ -0,0 +1,15 @@
1+local DIGITS = "0123456789abcdef";
2+local TABLE = [10, 20, 30];
3+local COUNT = 0;
4+local CONFIG = {"name": "stateful", "level": 2};
5+local DOUBLE = 21 * 2;
6+local ENABLED = yes;
7+
8+forge bump() { COUNT += 1; yield COUNT; }
9+forge hexdigit(n) { yield substring(DIGITS, n, n + 1); }
10+forge table_at(i) { yield TABLE[i]; }
11+forge add_item(v) { push(TABLE, v); yield len(TABLE); }
12+forge set_level(l) { CONFIG["level"] = l; }
13+forge level() { yield CONFIG["level"]; }
14+forge reset() { COUNT = 0; }
15+forge bump2() { COUNT++; yield COUNT; }

kiln/tests/cases/imports/fixtures/imports_transitive_mid.ashadded

+3−0
@@ -0,0 +1,3 @@
1+@import <./imports_basic_lib.ash>;
2+
3+forge mid_triple(x) { yield imports_basic_lib.add(x, imports_basic_lib.add(x, x)); }

kiln/tests/cases/imports/imports_alias_test.ashadded

+6−0
@@ -0,0 +1,6 @@
1+@import <./fixtures/imports_basic_lib.ash> as lib;
2+@import <./fixtures/imports_const_lib.ash> as k;
3+
4+say lib.add(3, 4);
5+say lib.helper(10);
6+say k.GREETING;

kiln/tests/cases/imports/imports_state_test.ashadded

+17−0
@@ -0,0 +1,17 @@
1+@import <./fixtures/imports_state_lib.ash>;
2+
3+say imports_state_lib.bump();
4+say imports_state_lib.bump();
5+say imports_state_lib.hexdigit(11);
6+say imports_state_lib.table_at(1);
7+say imports_state_lib.add_item(40);
8+say imports_state_lib.add_item(50);
9+say imports_state_lib.TABLE;
10+imports_state_lib.set_level(9);
11+say imports_state_lib.level();
12+say imports_state_lib.CONFIG["name"];
13+say imports_state_lib.DOUBLE;
14+say imports_state_lib.ENABLED;
15+imports_state_lib.reset();
16+say imports_state_lib.bump2();
17+say imports_state_lib.COUNT;

kiln/tests/cases/imports/imports_transitive_test.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./fixtures/imports_transitive_mid.ash>;
2+
3+say imports_transitive_mid.mid_triple(4);
4+say imports_basic_lib.add(1, 2);
5+say imports_basic_lib.helper(10);

kiln/tests/cases/io/fs_ops.ashadded

+15−0
@@ -0,0 +1,15 @@
1+local d = "/tmp/ash_fs_test_dir";
2+delete_file(d + "/b.txt"); delete_file(d + "/a.txt"); delete_file(d + "/c.txt");
3+make_dir(d);
4+say file_exists(d + "/a.txt");
5+write_file(d + "/a.txt", "hello");
6+write_file(d + "/b.txt", "world");
7+say rename_file(d + "/b.txt", d + "/c.txt");
8+say rename_file(d + "/nope.txt", d + "/x.txt");
9+say sort(list_dir(d));
10+say read_file(d + "/c.txt");
11+say delete_file(d + "/a.txt");
12+say delete_file(d + "/a.txt");
13+say delete_file(d + "/c.txt");
14+say list_dir(d);
15+attempt { list_dir(d + "/missing"); } handle (e) { say "no dir"; }

kiln/tests/cases/strings/affix_ops.ashadded

+16−0
@@ -0,0 +1,16 @@
1+say contains("hello world", "o w");
2+say contains("hello", "xyz");
3+say contains("hello", "");
4+say starts_with("hello", "he");
5+say starts_with("hello", "hello!");
6+say starts_with("hello", "");
7+say starts_with("hello", "el");
8+say ends_with("hello", "llo");
9+say ends_with("hello", "hell");
10+say ends_with("hello", "");
11+say ends_with("lo", "hello");
12+say repeat("ab", 3);
13+say repeat("x", 0);
14+say repeat("", 5) + "|";
15+say len(repeat("abc", 1000));
16+attempt { repeat("a", -1); } handle (e) { say "neg"; }

kiln/tests/cases/strings/chr_ord.ashadded

+7−0
@@ -0,0 +1,7 @@
1+say ord("A");
2+say chr(97) + chr(98);
3+say ord(chr(200));
4+say chr(ord("z") - 1);
5+local s = "hello";
6+local i = 0;
7+during i < len(s) { say ord(substring(s, i, i + 1)); i++; }

kiln/tests/cases/strings/string_compare.ashadded

+9−0
@@ -0,0 +1,9 @@
1+say "a" < "b";
2+say "b" < "a";
3+say "abc" <= "abc";
4+say "abd" > "abc";
5+say "" < "a";
6+say "Z" < "a";
7+say 1 < 2;
8+local s = "m";
9+given s >= "k" { say "fused ok"; }

kiln/tests/scripts/run_pe_emulated.pyadded

+163−0
@@ -0,0 +1,163 @@
1+"""Runs a kiln-produced PE32+ console program under the Unicorn x86-64 emulator.
2+kernel32 is faked in Python against the real filesystem; the program's own code
3+(including its Windows file-operation paths) is executed for real."""
4+import sys, os, struct, pefile
5+from unicorn import *
6+from unicorn.x86_const import *
7+
8+path = sys.argv[1]
9+pe = pefile.PE(path)
10+base = pe.OPTIONAL_HEADER.ImageBase
11+mu = Uc(UC_ARCH_X86, UC_MODE_64)
12+
13+def align(n, a=0x1000): return (n + a - 1) // a * a
14+
15+image_size = align(pe.OPTIONAL_HEADER.SizeOfImage)
16+mu.mem_map(base, image_size + 0x1000)
17+data = open(path, 'rb').read()
18+mu.mem_write(base, data[:pe.OPTIONAL_HEADER.SizeOfHeaders])
19+for s in pe.sections:
20+ mu.mem_write(base + s.VirtualAddress, s.get_data())
21+
22+STUBS = 0x7ff000000000
23+mu.mem_map(STUBS, 0x10000)
24+mu.mem_write(STUBS, b'\xc3' * 0x10000)
25+stub_names = {}
26+for i, entry in enumerate(pe.DIRECTORY_ENTRY_IMPORT):
27+ for imp in entry.imports:
28+ addr = STUBS + 16 * len(stub_names)
29+ stub_names[addr] = imp.name.decode()
30+ mu.mem_write(imp.address - base + base if imp.address >= base else base + imp.address, struct.pack('<Q', addr))
31+
32+STACK = 0x7fff00000000
33+mu.mem_map(STACK, 0x400000)
34+HEAPS = [0x100000000]
35+mu.reg_write(UC_X86_REG_RSP, STACK + 0x400000 - 0x1000)
36+
37+handles = {}
38+next_handle = [0x100]
39+out = sys.stdout.buffer
40+cmdline = base + image_size
41+mu.mem_write(cmdline, b'prog.exe\0')
42+
43+def cstr(addr):
44+ b = bytearray()
45+ while True:
46+ c = mu.mem_read(addr, 1)[0]
47+ if c == 0: return bytes(b).decode()
48+ b.append(c)
49+ addr += 1
50+
51+def arg(n):
52+ regs = [UC_X86_REG_RCX, UC_X86_REG_RDX, UC_X86_REG_R8, UC_X86_REG_R9]
53+ if n < 4: return mu.reg_read(regs[n])
54+ rsp = mu.reg_read(UC_X86_REG_RSP)
55+ return struct.unpack('<Q', mu.mem_read(rsp + 8 + 8 * n, 8))[0] # return addr, then shadow(32) = args 1-4 slots
56+
57+def stack_arg(n):
58+ rsp = mu.reg_read(UC_X86_REG_RSP)
59+ return struct.unpack('<Q', mu.mem_read(rsp + 8 + 8 * n, 8))[0]
60+
61+def ret(v): mu.reg_write(UC_X86_REG_RAX, v & 0xFFFFFFFFFFFFFFFF)
62+INVALID = 0xFFFFFFFFFFFFFFFF
63+calls = []
64+
65+def alloc(size):
66+ start = HEAPS[0]
67+ size = align(size, 0x1000)
68+ mu.mem_map(start, size)
69+ HEAPS[0] += size
70+ return start
71+
72+def new_handle(obj):
73+ h = next_handle[0]; next_handle[0] += 4
74+ handles[h] = obj
75+ return h
76+
77+def to_posix(p): return p.replace('\\', '/')
78+
79+def hook(uc, address, size, user):
80+ if not (STUBS <= address < STUBS + 0x10000): return
81+ name = stub_names.get(address)
82+ if name is None: return
83+ calls.append(name)
84+ if name == 'ExitProcess':
85+ sys.stdout.flush(); out.flush()
86+ os._exit(arg(0) & 0xFF)
87+ elif name == 'GetStdHandle': ret({0xFFFFFFF6: 1, 0xFFFFFFF5: 2, 0xFFFFFFF4: 3}.get(arg(0) & 0xFFFFFFFF, INVALID))
88+ elif name == 'WriteFile':
89+ h, buf, n, written = arg(0), arg(1), arg(2), arg(3)
90+ d = bytes(uc.mem_read(buf, n))
91+ if h == 2 or h == 3: sys.stderr.buffer.write(d); sys.stderr.flush()
92+ elif h == 1 or h == 0x1: out.write(d); out.flush()
93+ else:
94+ f = handles.get(h)
95+ if f is None: ret(0); return
96+ f['file'].write(d)
97+ if written: uc.mem_write(written, struct.pack('<I', n))
98+ ret(1)
99+ elif name == 'ReadFile':
100+ h, buf, n, got = arg(0), arg(1), arg(2), arg(3)
101+ f = handles.get(h)
102+ d = f['file'].read(n) if f else b''
103+ uc.mem_write(buf, d)
104+ if got: uc.mem_write(got, struct.pack('<I', len(d)))
105+ ret(1 if f else 0)
106+ elif name == 'CreateFileA':
107+ p, access, share, sa, disp = to_posix(cstr(arg(0))), arg(1), arg(2), arg(3), stack_arg(4)
108+ try:
109+ if disp == 3: f = open(p, 'rb') # OPEN_EXISTING
110+ elif disp == 2: f = open(p, 'wb') # CREATE_ALWAYS
111+ elif disp == 4: f = open(p, 'ab') # OPEN_ALWAYS
112+ else: ret(INVALID); return
113+ ret(new_handle({'file': f}))
114+ except OSError:
115+ ret(INVALID)
116+ elif name == 'CloseHandle':
117+ f = handles.pop(arg(0), None)
118+ if f and 'file' in f: f['file'].close()
119+ ret(1)
120+ elif name == 'GetFileSize':
121+ f = handles.get(arg(0)); ret(os.fstat(f['file'].fileno()).st_size if f else INVALID)
122+ elif name == 'VirtualAlloc': ret(alloc(arg(1)))
123+ elif name == 'GetCommandLineA': ret(cmdline)
124+ elif name == 'MoveFileA':
125+ try: os.rename(to_posix(cstr(arg(0))), to_posix(cstr(arg(1)))); ret(1)
126+ except OSError: ret(0)
127+ elif name == 'DeleteFileA':
128+ try: os.remove(to_posix(cstr(arg(0)))); ret(1)
129+ except OSError: ret(0)
130+ elif name == 'CreateDirectoryA':
131+ try: os.mkdir(to_posix(cstr(arg(0)))); ret(1)
132+ except OSError: ret(0)
133+ elif name == 'FindFirstFileA':
134+ pattern = to_posix(cstr(arg(0)))
135+ assert pattern.endswith('/*'), pattern
136+ try: names = ['.', '..'] + sorted(os.listdir(pattern[:-2]))
137+ except OSError: ret(INVALID); return
138+ h = new_handle({'names': names, 'i': 0})
139+ fill_find(uc, arg(1), h); ret(h)
140+ elif name == 'FindNextFileA':
141+ ret(1 if fill_find(uc, arg(1), arg(0)) else 0)
142+ elif name == 'FindClose':
143+ handles.pop(arg(0), None); ret(1)
144+ else:
145+ raise RuntimeError('unimplemented kernel32 call: ' + name)
146+
147+def fill_find(uc, buf, h):
148+ st = handles[h]
149+ if st['i'] >= len(st['names']): return False
150+ nm = st['names'][st['i']].encode(); st['i'] += 1
151+ uc.mem_write(buf, b'\0' * 320)
152+ uc.mem_write(buf + 44, nm + b'\0')
153+ return True
154+
155+mu.hook_add(UC_HOOK_CODE, hook, begin=STUBS, end=STUBS + 0x10000)
156+entry = base + pe.OPTIONAL_HEADER.AddressOfEntryPoint
157+try:
158+ mu.emu_start(entry, 0, count=2_000_000_000)
159+except UcError as e:
160+ sys.stdout.flush(); out.flush()
161+ print('EMULATION ERROR:', e, 'at rip', hex(mu.reg_read(UC_X86_REG_RIP)), file=sys.stderr)
162+ sys.exit(99)
163+sys.stdout.flush(); out.flush()

kiln/tests/scripts/windows_emulated_check.shadded

+29−0
@@ -0,0 +1,29 @@
1+#!/usr/bin/env bash
2+# kiln/tests/scripts/windows_emulated_check.sh
3+# No Windows machine needed: builds every kiln test case as a Windows PE (--target=windows),
4+# runs it under the Unicorn x86-64 emulator (run_pe_emulated.py, which fakes kernel32 on top of the
5+# real filesystem), and compares its output with the same program built for Linux.
6+# Needs python3 with `unicorn` and `pefile`; a venv is created in /tmp/kiln_winemu on first use.
7+set -uo pipefail
8+cd "$(dirname "$0")/../.." # -> kiln/
9+KILN="$PWD/../bin/kiln"
10+SCRIPTS="$PWD/tests/scripts"
11+VENV=/tmp/kiln_winemu
12+if [ ! -x "$VENV/bin/python" ]; then
13+ python3 -m venv "$VENV" && "$VENV/bin/pip" install -q unicorn pefile || { echo "could not set up the emulator venv"; exit 1; }
14+fi
15+make -s || { echo "build failed"; exit 1; }
16+OUT=/tmp/kiln_winemu_out; mkdir -p "$OUT"
17+pass=0; fail=0
18+for f in tests/cases/*/*.ash; do
19+ n=$(basename "$f" .ash); d=$(dirname "$f")
20+ case $n in argv|input*|*circular*|*collision*) continue;; esac # need real arguments / stdin / expected failure
21+ rm -rf /tmp/ash_fs_test_dir
22+ (cd "$d" && "$KILN" "$n.ash" -o "$OUT/lin_$n" >/dev/null 2>&1 && "$KILN" "$n.ash" -o "$OUT/win_$n.exe" --target=windows >/dev/null 2>&1) || { echo "COMPILE-FAIL: $n"; fail=$((fail + 1)); continue; }
23+ (cd "$d" && timeout 120 "$OUT/lin_$n" < /dev/null > "$OUT/lin_$n.out" 2>&1)
24+ rm -rf /tmp/ash_fs_test_dir
25+ (cd "$d" && timeout 600 "$VENV/bin/python" "$SCRIPTS/run_pe_emulated.py" "$OUT/win_$n.exe" < /dev/null > "$OUT/win_$n.out" 2>&1)
26+ if cmp -s "$OUT/lin_$n.out" "$OUT/win_$n.out"; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "DIFF: $n"; fi
27+done
28+echo "windows (emulated) vs linux: same $pass, different $fail"
29+[ "$fail" -eq 0 ]

kiln/tests/scripts/windows_wine_check.shadded

+29−0
@@ -0,0 +1,29 @@
1+#!/usr/bin/env bash
2+# kiln/tests/scripts/windows_wine_check.sh
3+# Builds every kiln test case as a Windows PE (--target=windows), runs it under Wine -- whose
4+# kernel32 is an independent reimplementation of the Windows API -- and compares its output with
5+# the same program built for Linux.
6+# WINE=/path/to/wine kiln/tests/scripts/windows_wine_check.sh
7+# Without WINE it uses `wine` from PATH. A portable build works without root, e.g.
8+# https://github.com/Kron4ek/Wine-Builds (unpack the amd64 tarball, point WINE at bin/wine).
9+set -uo pipefail
10+cd "$(dirname "$0")/../.." # -> kiln/
11+KILN="$PWD/../bin/kiln"
12+WINE="${WINE:-wine}"
13+command -v "$WINE" >/dev/null 2>&1 || [ -x "$WINE" ] || { echo "wine not found (set WINE=/path/to/wine)"; exit 1; }
14+export WINEPREFIX="${WINEPREFIX:-$HOME/.cache/ash_wine_prefix}" WINEDEBUG=-all WINEDLLOVERRIDES="mscoree,mshtml=" DISPLAY=
15+make -s || { echo "build failed"; exit 1; }
16+OUT=/tmp/kiln_wine_out; mkdir -p "$OUT"
17+pass=0; fail=0
18+for f in tests/cases/*/*.ash; do
19+ n=$(basename "$f" .ash); d=$(dirname "$f")
20+ case $n in argv|input*|*circular*|*collision*) continue;; esac # need real arguments / stdin / expected failure
21+ rm -rf /tmp/ash_fs_test_dir
22+ (cd "$d" && "$KILN" "$n.ash" -o "$OUT/lin_$n" >/dev/null 2>&1 && "$KILN" "$n.ash" -o "$OUT/win_$n.exe" --target=windows >/dev/null 2>&1) || { echo "COMPILE-FAIL: $n"; fail=$((fail + 1)); continue; }
23+ (cd "$d" && timeout 120 "$OUT/lin_$n" < /dev/null > "$OUT/lin_$n.out" 2>&1)
24+ rm -rf /tmp/ash_fs_test_dir
25+ (cd "$d" && timeout 300 "$WINE" "$OUT/win_$n.exe" < /dev/null > "$OUT/win_$n.out" 2>/dev/null)
26+ if cmp -s "$OUT/lin_$n.out" "$OUT/win_$n.out"; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "DIFF: $n"; fi
27+done
28+echo "windows (wine) vs linux: same $pass, different $fail"
29+[ "$fail" -eq 0 ]