216 files changed+4596−1682
.gitignoremodified
+6−1@@ -2,6 +2,10 @@22
*.o33
/bin/44
5
+# forgepack-installed dependencies (see ideas/ash_modules.md) -- fetched,6
+# never hand-edited, same spirit as node_modules/7
+@ash-modules/8
+59
# Compiled benchmark binaries (keep the .cpp sources, ignore the built outputs)610
ashvm/benchmarks/fib_cpp711
ashvm/benchmarks/loop_cpp@@ -24,4 +28,5 @@ skills-lock.json2428
# Private Notes2529
goals.md2630
rundown.md27
-missing.md\ No newline at end of file31
+missing.md32
+test.ash\ No newline at end of fileCHANGELOG.mdmodified
+150−0@@ -1,3 +1,153 @@1
+## 2026-10-07 (three more parity bugs, found verifying the JSON module before handing it off)2
+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).3
+4
+**Bare reassignment (`x = expr;`, no `local`) didn't exist on `ashvm` at all.** A known, previously-*documented*-but-never-fixed gap from an earlier session (flagged when asked "are both engines exactly the same" — the honest answer was no, citing this exact example) — writing a parser that reassigns a position variable throughout (`i = json_skip_ws(s, i);`) finally made it a hard blocker instead of a noted gap. `identifier_statement()` (`compiler/C/stmt.c`) handled a call, an index-assign, compound-assignment, and `++`/`--` on an already-declared variable, but fell through to "unexpected identifier statement" for a plain `=`. `kiln` already supported this (`assignment_statement()` always reassigns an existing slot). Fixed by adding the missing case: `OP_SET_LOCAL` after a full `expression()`, same as `local_statement()`'s own "already declared" path.5
+6
+**A namespaced call used as its own statement didn't parse on either engine.** `json.save(path, value);` (the entire point of a `save()` function — call it for the side effect, don't use its result) failed on both: `ashvm` with "unexpected identifier statement", `kiln` with "expected '=' after variable name". `module.func(...)` as part of an *expression* (`local x = json.load(...);`) already worked (`expr.c`'s `primary()` has a `TOKEN_DOT` case) — the gap was specifically the bare-statement entry points (`identifier_statement()` on `ashvm`, `assignment_statement()` on `kiln`), which checked for a plain call, an index-assign, etc., but never checked for `TOKEN_DOT` before falling through. Fixed by mirroring the exact dotted-call logic from each engine's own expression parser, with a trailing `OP_POP`/pop-pop to discard the unused return value, matching how a bare non-namespaced call statement already does.7
+8
+**Import compiler rejected any `local` inside an imported function's own loop.** `try_declare_import_constant()` (the mechanism that lets a top-level `local NAME = <literal>;` in an imported file become a namespaced compile-time constant, e.g. `mathlib.PI`) was called unconditionally for *every* `local` statement anywhere in an imported file's compiled body — including deep inside one of that file's own functions, since the "currently compiling an imported file" flag is set once for the whole file, not scoped by block depth. Every `local` inside `json_skip_ws`'s loop (`local c = substring(s, i, i + 1);`) got misdiagnosed as a failed attempt at a top-level namespaced constant ("imported top-level 'local' must be a literal number or string constant") even though it's three blocks deep, nowhere near the file's top level. The sibling guard in `statement()` already correctly checks `block_depth == 0` for the analogous "only forge/local allowed at top level" restriction — this specific call site just never got the same check. One-line fix, mirrored on both engines.9
+10
+All three fixes verified together: the full JSON module (parse/stringify/load/save, nested arrays-of-objects-of-arrays, malformed-input error handling via `attempt`/`handle`) now runs byte-for-byte identical on both engines, including a fourth bug that was in the *module's own code*, not the engines — `json_parse_string`'s loop had no end-of-input check (`during yes` with only "found a closing quote" as the exit condition), so an unterminated string in malformed input scanned forever: OOM-killed on `ashvm`, segfaulted on `kiln` (its bump allocator has no bounds check once the arena's exhausted). Fixed in the module, not the engines — bounded the loop on `i < len(s)` and `raise` on falling through.11
+12
+## 2026-10-07 (`type()` builtin, both engines)13
+A genuine gap found while designing the first real `forgepack` module (a JSON library): there was no way for pure Ash code to ask a value what it *is*. Every existing builtin either requires a specific type already (`keys()`/`has()` error on a non-map, `upper()` on a non-string, ...) or works the same way regardless of type (`len()`). A general `json.stringify(value)` has to decide "format this as an array vs. a map vs. a string vs. a number" from an arbitrary value with no static type — there's no way to do that without some form of runtime introspection.14
+15
+Added `type(value)` → `"number"/"string"/"array"/"map"/"function"/"none"` to both engines. `ashvm`: builtin id 27 (`builtins/C/core.c`, `builtins/C/builtins.c`), a straight switch on `VMValue.type`. `kiln`: a new module (`codegen/C/builtins/type_builtin.c`) — pop the value's tag, branch, and push one of six pre-embedded string literals via the already-existing `codegen_string_literal_bytes()` (the exact same length-prefix-embedding helper regular string literals use, so the result is a completely ordinary string value, not a special case). `ashvm`'s own print path distinguishes `"<function>"` vs. `"<closure>"` (see the array/map-printing fix further down this file), but `kiln` has no separate closure tag at all to match that with — `type()` collapses both to `"function"` on `ashvm` too, so the two engines agree on every possible result, rather than exposing a distinction only one side can make.16
+17
+## 2026-10-07 (`forgepack` — a package manager, GitHub-only for now)18
+New third project, `forgepack/`, following the same `C/H/D/O` + `-MMD` conventions as `ashvm`/`kiln`. Design doc first (`ideas/ash_modules.md`, modeled loosely on npm) before any code, per the usual rule for a new subsystem this size. Scope deliberately narrowed during the build: no registry/index yet (the domain for that is reserved, but a static index is a later phase, not now), no `path:`/plain-version dependencies yet either — `github:user/repo[@ref]` is the only dependency kind forgepack can actually install without a server, so that's the only one it accepts right now rather than recording something it can't resolve.19
+20
+**Commands:** `forgepack init` (writes `ash.pkg`), `add github:user/repo[@ref] [--as <name>]` (records the dependency and installs it), `install` (populates `@ash-modules/` from `ash.pkg` for a fresh checkout), `list`. `ash.pkg` is a plain key-value text file (`name:`/`version:`/`[dependencies]`), not JSON — no need to pull in a JSON parser for a package manager in a project whose whole premise is "no external dependencies." Dependencies land flat in `@ash-modules/<name>/`, gitignored like `node_modules` — `forgepack/src/C/git_fetch.c` shells out to the system `git` binary (validates `user`/`repo`/`ref` against a strict alnum/`-`/`_`/`.` charset *before* ever building a shell command string, since a dependency spec is attacker-controlled the moment it comes from someone else's published `ash.pkg`).21
+22
+**Wired into `@import` on both engines, not just the new tool.** A bare `@import <name>;` already resolved to `ash.libs/<name>/<name>.ash` (`import_paths.c`, identical in `ashvm` and `kiln`); it now falls back to `@ash-modules/<name>/<name>.ash` when `ash.libs/` doesn't have it, so a `forgepack`-installed dependency is actually `@import`-able, not just downloaded. `ash.libs/` still wins first — a project can always shadow an installed package name with its own local copy. No new import syntax, no change to the relative-path (`./...`) branch at all.23
+24
+**CLI output uses real Unicode symbols (✓/✗/→/•, not emoji) and ANSI color** (`forgepack/src/C/ui.c`), auto-disabled when stdout/stderr isn't a TTY or `NO_COLOR` is set — confirmed piped output (`| cat -v`) carries the plain symbol with zero escape codes, only an interactive terminal gets color.25
+26
+## 2026-10-07 (four cross-engine parity bugs, found by a side-by-side audit)27
+Ran the same `.ash` source files through both engines and diffed stdout byte-for-byte, which the previous entry's feature-parity work made possible for the first time (no point diffing output that couldn't even be compared before). Found four real bugs — three in `kiln`, one `ashvm`-only — fixed all four, then re-ran the audit to confirm convergence. The only remaining difference across every test file is map iteration order (`keys(m)`/`values(m)`), which is a documented, pre-existing architectural difference (`ashvm`'s real hash table vs. kiln's dense insertion-ordered array, see `codegen/H/emit/value.h`) and not a bug — neither engine's map iteration order was ever a language guarantee.28
+29
+**`kiln`: missing `%` operator.** `TOKEN_PERCENT` was lexed but `term()` in `codegen/C/expressions/expr.c` only handled `*`/`/` — every `kiln` test needing modulo worked around it with `x - floor(x / n) * n`, which is a *different* operation for negative operands (floor-based mod vs. truncate-based `fmod`). Implemented as `a - trunc(a / b) * b` using the SSE2 primitives `floor()`'s codegen already established the pattern for (`emit_cvttsd2si` truncates toward zero directly, no separate correction step needed the way `floor()`'s overshoot-fix does) — matches `ashvm`'s C `fmod()` exactly, including sign: `-7 % 3 == -1`, not `2`. Verified identical output on both engines across positive, negative, and fractional operands.30
+31
+**`kiln`: `say`-ing an array or map directly crashed or printed garbage.** `codegen/C/runtime/print_int.c`'s single print routine only ever had cases for `TAG_STRING` and `TAG_NIL`; anything else (including `TAG_ARRAY`/`TAG_MAP`, and, it turns out, `TAG_FUNCTION`) silently fell into the number-formatting path and tried to print a heap pointer as a double. This is why `kiln`'s own test suite never caught it: every test that uses `map()`/`filter()`/`keys()`/etc. immediately indexes into the result rather than printing it directly. Fixed with a new recursive printer (`emit_print_recursive_routine`, self-`call`s for nested arrays/maps, quotes string elements the way `ashvm`'s `vm_print_value()` does, falls back to `<function>` for anything else) plus a `number_routine` extracted out of the old inline digit-building code so both the recursive printer and the top-level routine can share it without duplicating ~90 lines of SSE2 asm. Wrote and threw away three register-clobbering bugs getting this right — hand-rolled x86-64 means `write_one_char()` (every bracket, comma, quote) clobbers RAX/RCX/RDX/RSI as a side effect of its own `write(2)`/`WriteFile` call, so anything that needs to survive across one has to live in memory (this routine's own `push rbp`-anchored frame, `[RBP-8/-16/-24/-32]`, *not* `[RSP+disp]`, which this codegen layer can't even encode — `RSP` as a base needs a SIB byte `modrm_mem_disp32()` doesn't emit), never in a register held "across" the call. Found via `gdb` on the raw statically-linked ELF (no section headers, no symbols — breakpoints by address, registers read at the exact instruction before/after a suspect load) after the first version printed `[1, ]` for `[1, 2]` instead of `[1, 2]`: traced to the loop's "index + 1, maybe print a comma, then store the new index" sequence holding the incremented index in `RCX` across the two `write_one_char(',')`/`write_one_char(' ')` calls that immediately clobber it.32
+33
+**`kiln`: `len()` on a string read garbage.** `codegen_builtin_len()` in `codegen/C/collections/arrays.c` unconditionally read offset `+8` as "the count" — correct for `TAG_ARRAY`/`TAG_MAP` (they share the same `[capacity][count][data/entries_ptr]` object shape), but a string's length lives at offset `-8` from its payload (the length prefix that precedes the raw bytes), not `+8`. `len("hello")` was reading whatever garbage byte happened to sit 8 bytes into the string's own content. Fixed by branching on `TAG_STRING` first.34
+35
+**`ashvm`: a fresh `local` declared inside a re-executed block (a `during`/`each` body, or a `given`/`otherwise` branch nested inside one) leaked a stack slot every pass instead of being replaced.** `ashvm` has no explicit "store a local" instruction — declaring one just means "whatever's on top of the stack now has this name," at a compile-time-fixed slot index (`frame->slots[n]`). `each`'s own three setup slots (array/index/var) already got cleaned up once, after the whole loop exits — but nothing cleaned up locals the *body* itself declared, every single iteration, and `given`/`during` didn't clean up after themselves at all. First iteration of `local x = i * 10; say x;` inside a `during` loop pushes `x` correctly; every iteration after that pushes *another* copy on top without popping the old one, while the compiled `OP_GET_LOCAL` for `x` keeps reading the same fixed slot index — the first iteration's value, frozen forever. `kiln` doesn't have this bug (confirmed directly, contradicting an assumption in the previous entry that this was some shared/inherited limitation): its locals are fixed `rbp`-relative memory addresses, not stack positions, so re-declaring one just overwrites the same address.36
+37
+Fixed with a new VM instruction, `OP_TRUNC_LOCALS n` (`stack_top = frame->slots + n`) — an *absolute* reset, not a fixed number of `OP_POP`s, because a block can exit early (`next`, or one branch of a nested `given`) having declared fewer locals than its "normal" path does, so only the known target depth is ever safe to assume. `compile_given_stmt`/`compile_during_stmt`/`compile_each_stmt` each save `local_count` before their re-executed block, then call a shared `truncate_block_locals()` right where `next` already lands (before the per-iteration advance, so both the normal fallthrough and an early `next` converge on the same cleanup) — compiled to nothing extra when a block doesn't declare any locals of its own, so the common case (most loop bodies) costs zero bytes and zero instructions beyond what was already there. Verified against `fib.ash`/`loop.ash` (unaffected — this machine's own baseline for both, checked against the pre-session commit with a throwaway `git worktree`, already sits well above `AGENTS.md`'s reference numbers, which are from different hardware) and against deliberately adversarial cases (nested `given`/`next` inside `during`, nested `each` inside `each`) diffed byte-for-byte against `kiln`.38
+39
+## 2026-10-07 (bring `ashvm` to the same feature level as `kiln`)40
+The reverse direction of the earlier kiln-parity work: `kiln` had six capabilities `ashvm` didn't (`stop`/`next` loop control, `raise` with a runtime string message, compound assignment, `++`/`--`, a ternary, and bitwise operators), plus `yes`/`no`/`none` literals from the same-day rename work. All ported, staying entirely within `ashvm`'s own execution model (interpreted bytecode VM, not compiled to native code like kiln) — this is language-feature parity, not architecture parity.41
+42
+**`none` (nil) — the one real new concept, not just a port.** `ashvm`'s value representation had no null type at all (`VMType` was `{VM_NUM, VM_STR, VM_ARRAY, VM_MAP, VM_FUNCTION, VM_CLOSURE}`). Added `VM_NIL` and `vm_nil()` to `value/H/value.h`+`value/C/value.c`, then threaded it through every place a value's type is inspected: `truthy()` in `vm/C/errors.c` now treats `none` as always-falsy without erroring (matching kiln's "nil's payload is always exactly 0" rule) instead of the default "non-number errors" path every other truthiness check still takes; `do_EQ`/`do_NEQ`/`do_CMP_JUMP`'s equality branch in `vm/C/dispatch.c` gained a `VM_NIL == VM_NIL` case (including the fused-comparison path, which can reach it since `x == y` where both are locals holding `none` at runtime is a fusable `identifier CMP identifier` shape even though `x == none` itself never fuses — `none` is a keyword, not a number or identifier); `vm_print_value()` prints `none`. No existing builtin needed a change — `len()`, `str()`, etc. already reject unexpected types generically, so `none` just became one more value their existing type checks correctly refuse.43
+44
+**`stop`/`next`** — a new `compiler/C/loop_stack.c`+`H/loop_stack.h` module (mirrors kiln's `parser/C/statements/loop_stack.c`), forward-jump bookkeeping per nesting level, no new VM instructions (reuses the existing `OP_JUMP`). Wired into `compile_during_stmt`/`compile_each_stmt` (`loop_push()` before the body, `loop_patch_nexts()` right before the per-iteration advance step, `loop_pop_and_patch_stops()` after) and `compile_forge_decl`/`lambda_literal` (save/reset/restore the loop-nesting depth around a function/lambda body, so `stop`/`next` inside one can't reach an enclosing loop it's textually written inside — ashvm's functions get their own `Chunk`, so a cross-chunk jump offset would be nonsense bytecode anyway; this turns that into a clean compile error instead). One genuine ordering subtlety for `each`: `stop` jumps and the loop's own condition-false exit both have to land at the *exact* same bytecode position (right before the each-loop's three cleanup `OP_POP`s, not after) so both paths fall through into the same cleanup — tested explicitly with nested `each` loops to confirm `stop` only breaks the innermost one and the stack stays balanced either way.45
+46
+**`raise`** — needed a new opcode (`OP_RAISE`) and a new VM entry point (`vm_raise_value()` in `vm/C/errors.c`), since every existing runtime error goes through a fixed-size `char error_message[512]` buffer (fine for compile-time format strings, not for an arbitrary-length runtime string value). Added `raised_value`/`has_raised_value` globals (`vm/H/state.h`) that `vm_raise_value()` sets right before unwinding and `do_TRY_PUSH`'s recovery branch checks first, falling back to the `error_message` copy path for every other kind of error — so a caught `raise` hands back the exact raised string, not a truncated-at-512-bytes copy.47
+48
+**Compound assignment / `++`/`--`** — deliberately *not* using the existing `OP_ACC_LOCAL` fused op (that only accepts a number-or-local operand, not a general expression — `x += f() + 1;` needs the full `GET_LOCAL, expression(), <op>, SET_LOCAL` path). `+=` reuses `OP_ADD` directly (same tag-aware string-concat-or-numeric-add logic `+` itself has); `-= *= /=` are number-only, same as `- * /` themselves. Unlike `ashvm`'s existing `local x = expr;` (which doubles as declare-or-reassign), these require the variable to already exist — `resolve_local_or_error()` added for the clean "undefined variable" diagnostic.49
+50
+**Ternary and bitwise** — ternary sits at the bottom of `expr.c`'s precedence chain (above `logical_or`, matching kiln's placement), same jump-patch shape `given` already uses. Bitwise is one flat precedence level between `comparison` and `logical_and` (not real C precedence — a deliberate simplification kiln made first, carried over for consistency). Bitwise genuinely needed six new opcodes (`OP_BAND/BOR/BXOR/BNOT/SHL/SHR`) — real VM additions, but zero per-existing-instruction cost: computed-goto dispatch is a jump through a pointer array indexed by opcode byte, so adding table entries doesn't slow down dispatching to any *other* opcode. Confirmed by benchmarks below, not just asserted.51
+52
+**A real, separate bug found and fixed along the way, not part of the language work:** `ashvm`'s Makefile had no `-MMD` header-dependency tracking (`AGENTS.md` explicitly documented this as a known fact: "`ashvm` has no `D/` tree"). Adding six new tokens to `lexer.h` shifted every subsequent `TokenType` enum value by 6 — `lexer.c` got recompiled (it was edited directly), but `stmt_import.c` (which only includes `lexer.h` transitively and wasn't itself touched) didn't, so it kept checking `TOKEN_LESS` against its *old* numeric value while the lexer now emitted the *new* one. Every `@import` in the regression suite broke with a confusing "expected '<' after '@import'" pointing straight at a literal `<` character. Root-caused by writing a tiny standalone program linked against just the lexer object file to print raw token type numbers, confirming the lexer was correct and the mismatch was elsewhere — then recognizing the "its Makefile makes no dependency files" line in `AGENTS.md` as the actual cause. Fixed by porting kiln's exact `-MMD -MP` Makefile pattern to `ashvm` (kiln's own Makefile comment already named this exact failure mode as the reason it has the pattern at all) — new `D/` trees added (seven subsystems, matching the existing `O/` trees), `AGENTS.md` and `README.md` updated since both explicitly documented "ashvm has no D/" as fact. Verified the fix actually works, not just that the symptom went away: touching `lexer.h` with nothing else changed now correctly recompiles all 10 dependent files, confirmed by counting recompiled files before and after.53
+54
+Added regression tests: `tests/cases/basics/literals_test.ash` (`yes`/`no`/`none`, truthiness, equality, containers), `tests/cases/basics/operators_test.ash` (compound assignment, `++`/`--`, ternary, bitwise), `tests/cases/control_flow/loop_control_test.ash` (`stop`/`next`, including nested loops), `tests/cases/errors/raise_test.ash` (`raise`/`attempt`/`handle` with dynamic messages). `basics/` is a new feature folder — one of `AGENTS.md`'s own suggested names, previously unused in `ashvm`.55
+56
+Verified: zero-warning clean rebuild, full regression suite byte-identical to a re-captured baseline, all five benchmarks within normal noise of their pre-this-round timings (fib ~0.148s, loop ~1.10s, countdown ~1.06s, loop_varbound ~1.10s, hashmap ~0.059s — no regression, as expected since none of the new opcodes touch any existing benchmark's hot path), and a hand-written smoke test exercising every new construct together, including nested `each` loops with `stop`/`next` and `none` stored in arrays/maps. `README.md`'s example and feature list updated; `AGENTS.md` corrected for the `D/` tree addition.57
+58
+## 2026-10-07 (rename kiln's own six keywords: `throw`->`raise`, `break`->`stop`, `continue`->`next`, `true`->`yes`, `false`->`no`, `nil`->`none`)59
+These six have no `ashvm` equivalent at all (`ashvm` doesn't have exception-throwing, loop control, or boolean/nil literals), so this isn't parity work — just carrying the same "Ash's own plainer words, not borrowed syntax" idea kiln already got from the parity round into kiln's own extra vocabulary. Same mechanics as every rename so far: `TOKEN_THROW`/`TOKEN_BREAK`/`TOKEN_CONTINUE`/`TOKEN_TRUE`/`TOKEN_FALSE`/`TOKEN_NIL` became `TOKEN_RAISE`/`TOKEN_STOP`/`TOKEN_NEXT`/`TOKEN_YES`/`TOKEN_NO`/`TOKEN_NONE`, `throw_statement`/`break_statement`/`continue_statement` became `raise_statement`/`stop_statement`/`next_statement`.60
+61
+Went further than previous rounds on internal consistency: `loop_stack.c`/`.h`'s `loop_record_break`/`loop_record_continue`/`loop_patch_continues`/`loop_pop_and_patch_breaks` (and their `break_count`/`continue_count` struct fields) became `loop_record_stop`/`loop_record_next`/`loop_patch_nexts`/`loop_pop_and_patch_stops`/`stop_count`/`next_count`. Unlike `local_count` (a genuinely generic "local variable" term, kept through the `let`->`local` rename), these names directly mirrored the old keyword 1:1 with nothing more generic backing them, so renaming them was the more consistent call. Also fixed two real user-facing error message strings in `loop_stack.c` that a plain token-reference grep had missed in earlier rounds (`"'break' outside a loop"` etc. — these only showed up because this round's sweep specifically grepped for the bare words, not just `TOKEN_*` references) and a stale `` `throw expr;` `` example in a comment in `errors.h`.62
+63
+**One real behavior fix, not just a rename:** the runtime's own printed representation of a nil-tagged value (`codegen/C/runtime/print_int.c`, hand-emitting the raw bytes `'n','i','l','\n'` plus a hardcoded length-4 write syscall) still said "nil" even after `nil` became `none` — unlike the `vm/C/dispatch.c` "too many nested try blocks" precedent from the `ashvm` renames, this one IS genuinely user-facing (`say none;` printing `nil` back would be actively confusing, not just an internal inconsistency), so it was updated to emit `"none\n"` with the write length corrected to 5. Caught by actually running `say none;` after the rename and noticing the mismatch, not by code review alone.64
+65
+While sweeping for leftover references, also found and fixed several **stale references from the *previous* `try`->`attempt`/`catch`->`handle` round** that earlier grepping (focused on `TOKEN_TRY`/`TOKEN_CATCH` specifically) had missed: comments in `elf/H/elf_writer.h`, `codegen/H/emit/runtime_layout.h`, `codegen/H/emit/emit.h`, `parser/H/declarations/vars.h`, and `parser/C/core/parser.c` still said "try"/"catch" where they meant "attempt"/"handle". A reminder that grepping for the token constant alone isn't enough — bare-word greps catch what token-reference greps miss, and are worth doing as a final pass after any rename.66
+67
+Verified: zero-warning rebuild, full regression suite re-baselined (one legitimate diff from the nil->none print fix, everything else byte-identical) and now clean, a hand-written smoke test exercising `stop`/`next` inside a `during` loop together with `yes`/`no`/`none`/`and`/`not`/`attempt`/`handle`/`raise`, migrated all affected test files (with the same care as before for string literals that happened to contain the old keyword words as their actual printed content — `ternary.ash`'s `"a is true"`/`"a is false"` and `true_false_nil.ash`'s `"falsy nil"` were preserved exactly, not touched by the rename).68
+69
+## 2026-10-07 (bring `kiln` to the same syntax as `ashvm`)70
+`kiln` has its own, completely separate lexer and parser (it compiles straight to native ELF/PE, no bytecode VM), so none of today's `ashvm` syntax work reached it for free. This brings it up to parity: all ten keyword renames (`let`->`local`, `fn`->`forge`, `try`->`attempt`, `print`->`say`, `if`->`given`, `else`->`otherwise`, `while`->`during`, `return`->`yield`, `catch`->`handle`, `for`->`each`), optional parens on `given`/`during` conditions, and `&&`/`||`/`!` -> `and`/`or`/`not`. Kiln-only features with no `ashvm` equivalent (`break`/`continue`/`throw`, compound assignment `+= -= *= /=`, `++`/`--`, the `?:` ternary, bitwise `& | ^ ~ << >>`, `true`/`false`/`nil` literals) are untouched — porting syntax parity doesn't mean deleting kiln's own extra capabilities, and none of them collide with the renames (bitwise `&`/`|` keep their own token types, distinct from the `&&`/`||` that became `and`/`or`).71
+72
+**The `each (x in array)` loop already existed in kiln**, fully working, with its own dedicated per-nesting-depth stack slots and proper block scoping (`vars_scope_begin`/`vars_scope_end`) — architecturally sounder than `ashvm`'s version even needed to be. It just needed the `for`->`each` rename (plus making its own parens optional, for consistency with `given`/`during`) and renaming everything that referenced the old name: the file itself (`for_loop.c`/`.h` -> `each_loop.c`/`.h`, updated in the Makefile), `for_statement`->`each_statement`, `for_depth_save/reset/restore`->`each_depth_*`, `MAX_KILN_FOR_DEPTH`/`KILN_FOR_LEVEL_SIZE`/`KILN_FOR_STACK_SIZE`->`*_EACH_*`, `for_level_offset`->`each_level_offset`. Likewise `try_catch.c` -> `attempt_handle.c`, `try_statement`->`attempt_statement`, and the internal jump-bookkeeping variables renamed for consistency (`skip_catch`->`skip_handle`, etc.) — mirroring the pattern already used for `ashvm`'s own `try_jump`->`attempt_jump` rename earlier today. `if_statement`/`while_statement`/`fn_statement` similarly became `given_statement`/`during_statement`/`forge_statement`. VM/opcode-equivalent runtime mechanism names were left alone, same principle as `ashvm`: `kiln_try_depth_addr()`, `kiln_try_handlers_addr()`, `MAX_KILN_TRY_DEPTH` (the try/catch handler stack is a genuine runtime data structure, unlike each's compile-time-known nesting, so it still needs runtime tracking regardless of what the surface keyword is called) are unrenamed, and the runtime-emitted fatal message "too many nested try blocks" was deliberately left saying "try" — matching `ashvm`'s own equivalent deliberate inconsistency in `vm/C/dispatch.c`, for the same "same status as ashvm" reason.73
+74
+Migrated all 49 of kiln's test `.ash` files (`tests/cases/**`, including 11 nested import fixtures the top-level glob doesn't directly run but the import tests exercise indirectly). Caught and fixed a real mistake from the blind word-boundary `sed`: three string literals that happened to contain the words "print"/"else" as their actual printed content (demonstrating which branch ran) got their content silently rewritten by the naive rename pass (`sed` has no concept of "inside a string literal") — caught by a regression diff for one of them, and by explicitly grepping every string literal for the new keyword words for the other two, which the diff hadn't caught (they sat in branches that never execute, so the corruption never surfaced in any program's actual output). All three restored to their original string content.75
+76
+Verified properly this time: unlike letting the working tree drift, a true pre-rename baseline was captured by `git stash`-ing every uncommitted change (including today's earlier `ashvm` work, repo-wide — `git stash` isn't directory-scoped), rebuilding the untouched original `kiln`, running its regression suite, then `git stash pop`-ing everything back before migrating. Final result: zero-warning rebuild, full regression suite byte-identical to that true baseline, and a hand-written smoke test exercising every renamed construct together (nested nesting between `given`/`during`/`each`/`attempt`/`handle`, `and`/`or`/`not`, optional parens both ways) compiled and ran correctly.77
+78
+**One real mistake from the `git stash` round-trip:** `deep_nesting_demo.ash` (the untracked demo file made earlier today) didn't survive — not because `git stash` touches untracked files (it doesn't, by default), but it was gone by the time of the `stash pop` regardless, cause not fully root-caused. Recreated from the conversation's own record of its contents, verified to produce byte-identical output to before.79
+80
+## 2026-10-07 (add the `each (x in array)` loop)81
+`in` turned out to be a second dead keyword sitting right next to `for` (lexed, never consumed by any parse rule) — so, having just renamed the first one, this built the actual feature `README.md`'s language-features list had been advertising as `for (x in array)` all along: `each (x in array) { ... }` (parens optional, same as `given`/`during`).82
+83
+**Implementation:** `compile_each_stmt()` in `stmt_control.c` hand-emits the exact bytecode shape a counting `during`-loop would produce — `len()` via the existing builtin-call opcode, `OP_INDEX_GET`, and the existing `OP_ACC_LOCAL` fused op for the index bump — rather than synthesizing source text and reparsing it. No new VM instructions, so the performance rule that held for the whole renaming phase holds for this too, even though it's a genuinely new feature, not a rename. Array-only, matching `map()`/`filter()`/`reduce()`'s own existing scope; iterate a map via `each (k in keys(m))`.84
+85
+**A real bug, caught before shipping:** the first version restored the compiler's `local_count` after the loop without emitting the matching runtime `OP_POP`s, so the three setup slots (array, index, loop variable) stayed physically on the VM stack. A second `each` right after a first one would then compile its slots to the same reused indices while the runtime stack was still three deeper — every `GET_LOCAL`/`SET_LOCAL` read the wrong position, and the loop silently ran zero iterations. A single isolated `each` looked completely correct; only testing two in a row caught it. Fixed with three explicit `OP_POP`s, the same fix shape `compile_attempt_stmt()` already uses for its own one handle-variable slot.86
+87
+**A pre-existing limitation this surfaced, not introduced here:** declaring a genuinely new `local` inside *any* loop body (`given`, `during`, or `each`) corrupts output from the second iteration on, since none of them truncate the runtime stack per iteration. Confirmed with a plain `during` loop first. Not fixed — flagged in `plans.md`'s open questions, since every loop in this codebase already uses the safe workaround (declare outside, reassign inside).88
+89
+Added a regression test, `ashvm/tests/cases/control_flow/each_test.ash` (new feature folder — `control_flow` is one of `AGENTS.md`'s own suggested names, previously unused): array iteration parenthesized and not, a map via `keys()`, an accumulator, an empty array, and nested `each` using the correct accumulate-outside idiom. Verified: zero-warning rebuild, full regression suite (including the new test) byte-identical to a re-captured baseline, all five benchmarks within normal noise (nothing in any hot path changed, since no benchmark uses `each`), `deep_nesting_demo.ash` unchanged, `kiln` untouched. `README.md`'s example and feature list updated — the feature list's `each (x in array)` line is no longer a doc/reality mismatch.90
+91
+## 2026-10-07 (rename `for` to `each`)92
+Tenth and last keyword rename of the day, and the cheapest one: `for` is now `each`. Unlike every other rename today, this one changes nothing observable — `for`/`TOKEN_FOR` (now `each`/`TOKEN_EACH`) is a dead keyword. It's lexed, but no parser rule in `stmt.c`/`stmt_control.c` ever consumes it; there is no for-loop statement in Ash today, despite `README.md`'s feature list claiming `for (x in array)` (now corrected to read `each (x in array)`, still just as unimplemented as before — a pre-existing doc/reality mismatch flagged back when this was first noticed, not something fixed today). Renamed anyway, purely for consistency: it costs one lexer keyword-length check and touches no parser logic, so there was no reason to leave the one remaining C-flavored keyword sitting in an otherwise fully-renamed set.93
+94
+No `.ash` file in the repo uses `for` (confirmed by grep before touching anything), so there was nothing to migrate except the one README mention. Verified: zero-warning rebuild, full regression suite byte-identical to baseline (expected — nothing that actually runs changed).95
+96
+All ten renames from today, for the record: `let`->`local`, `fn`->`forge`, `try`->`attempt`, `print`->`say`, `if`->`given`, `else`->`otherwise`, `while`->`during`, `return`->`yield`, `catch`->`handle`, `for`->`each`. The only Ash keyword never touched today is `import` (the `@import` sigil form) — and `in`, which like `for`/`each` is lexed but still unused by any parser rule.97
+98
+## 2026-10-07 (rename `while` to `during`, `return` to `yield`, `catch` to `handle`)99
+Three more keyword renames in quick succession, same mechanics as every rename today: `while`->`during`, `return`->`yield`, `catch`->`handle`. `TOKEN_WHILE`/`compile_while_stmt()` became `TOKEN_DURING`/`compile_during_stmt()`; `TOKEN_RETURN` became `TOKEN_YIELD` (the `OP_RETURN` opcode it still emits is unrenamed — opcode names are independent of surface keyword, same reasoning as every other opcode left alone); `TOKEN_CATCH` became `TOKEN_HANDLE`, with the compiler's internal `skip_catch` jump-target variable renamed to `skip_handle` for consistency with the earlier `try_jump`/`else_jump` renames.100
+101
+`emit_loop`/`loop_start` (used by the during-loop's backward jump) were deliberately left unrenamed — "loop" is the general mechanism's name, independent of whichever keyword (`while` or now `during`) introduces one, same reasoning that kept `fn_idx` and `local_count` alone through earlier renames.102
+103
+Migrated every existing `.ash` file through all three renames at once: `tests/cases/**` (including nested `fixtures/`/`ash.libs/`), `benchmarks/*.ash`, `README.md` (code example and the "Language features"/performance-note mentions), and `deep_nesting_demo.ash`. Verified the usual way: zero-warning rebuild, full regression suite byte-identical to baseline, all five benchmarks within a few percent of their pre-rename timings, demo file's output unchanged.104
+105
+Nine keywords renamed today, running tally: `let`->`local`, `fn`->`forge`, `try`->`attempt`, `print`->`say`, `if`->`given`, `else`->`otherwise`, `while`->`during`, `return`->`yield`, `catch`->`handle`.106
+107
+## 2026-10-07 (rename `else` to `otherwise`)108
+Sixth keyword rename of the day: `else` is now `otherwise` — `given x < n { ... } otherwise { ... }`. `TOKEN_ELSE` became `TOKEN_OTHERWISE`; the compiler's internal `else_jump` variable (the bytecode jump target patched to the otherwise-branch) was renamed to `otherwise_jump` for consistency with the earlier `try_jump`->`attempt_jump` rename. The C-language `else` keywords sprinkled through the compiler's own implementation (e.g. `if (...) { ... } else { ... }` in `stmt.c`/`stmt_control.c`) are untouched, obviously — those are C, not Ash, and renaming Ash's `else` has nothing to do with them.109
+110
+Migrated every existing `.ash` file again: `tests/cases/**` (including nested `fixtures/`/`ash.libs/`), `benchmarks/*.ash`, and `deep_nesting_demo.ash` (which uses `otherwise` extensively — five branches). `README.md`'s code example doesn't have an else-branch, so nothing there needed changing this time. Verified the same way as every rename this round: zero-warning rebuild, full regression suite byte-identical to baseline, all five benchmarks within a few percent of their pre-rename timings, demo file's output unchanged.111
+112
+Six keywords renamed today, all told: `let`->`local`, `fn`->`forge`, `try`->`attempt`, `print`->`say`, `if`->`given`, `else`->`otherwise`. Unchanged: `catch`, `while`, `return`.113
+114
+## 2026-10-07 (rename `if` to `given`)115
+Fifth keyword rename of the day, same shape as the previous four: `if` is now `given` (`else` is unchanged — `given x < n { ... } else { ... }`). `TOKEN_IF`/`compile_if_stmt()` became `TOKEN_GIVEN`/`compile_given_stmt()`. `try_fuse_condition()` wasn't touched by this rename at all — it already took its terminator token as a parameter (from the earlier optional-parens work) and has no idea which keyword called it, so renaming the caller is a pure caller-side change.116
+117
+Migrated every existing `.ash` file again: `tests/cases/**` (including nested `fixtures/`/`ash.libs/`), `benchmarks/*.ash`, `README.md` (code example only this time — the "Language features" list doesn't mention `if` by name), and `deep_nesting_demo.ash`. Verified the same way as every rename this round: zero-warning rebuild, full regression suite byte-identical to baseline, all five benchmarks within a few percent of their pre-rename timings, demo file's output unchanged.118
+119
+With this, five of Ash's keywords now read differently from where the day started: `let`->`local`, `fn`->`forge`, `try`->`attempt`, `print`->`say`, `if`->`given`. `catch`, `else`, `while`, and `return` are unchanged.120
+121
+## 2026-10-07 (rename `try` to `attempt`, `print` to `say`)122
+Two more keyword renames, same shape as the previous two: `try` is now `attempt` (`catch` is unchanged — `attempt { ... } catch (e) { ... }`), and `print` is now `say`. `TOKEN_TRY`/`compile_try_stmt()` became `TOKEN_ATTEMPT`/`compile_attempt_stmt()` (its local `try_jump` variable renamed to `attempt_jump` too, and its comments describing "the try block" now say "the attempt block"); `TOKEN_PRINT` became `TOKEN_SAY`. No behavior change — same grammar, same bytecode (`OP_PRINT`, `OP_TRY_PUSH`/`OP_TRY_POP` in the VM are unrenamed, since they're opcode names independent of what keyword triggers them, same as `fn_idx` was left alone during the `fn`->`forge` rename).123
+124
+One deliberate loose end, left alone on purpose rather than silently fixed: `vm/C/dispatch.c`'s runtime error string "too many nested try blocks" still says "try". This phase's performance rules are explicit that work stays confined to the lexer and compiler — `vm/` doesn't get touched even for a one-line string, to avoid setting a precedent of "just this once." Noted in `plans.md`.125
+126
+Migrated again, same files as every rename this round: `tests/cases/**` (including nested `fixtures/`/`ash.libs/`), `benchmarks/*.ash`, `README.md` (both the code example and the "Language features" bullet list), and `deep_nesting_demo.ash`. Verified the same way: zero-warning rebuild, full regression suite byte-identical to baseline, all five benchmarks within a few percent of their pre-rename timings, demo file's output unchanged.127
+128
+## 2026-10-07 (rename `fn` to `forge`)129
+Second rename follow-up: `fn` is now spelled `forge` — Ash's own word, not borrowed from Lua or anywhere else (chosen over the more generic `function`/`func`/`def` specifically to fit the fire/embers theme the project is named after). Same shape as the `let`->`local` rename: `TOKEN_FN`/`compile_fn_decl()` became `TOKEN_FORGE`/`compile_forge_decl()`, every user-facing message updated, no behavior change (`forge` still declares both named functions and anonymous lambdas, exactly as `fn` did). Internal `fn_idx`/`find_function`-style identifiers were left alone — they mean "function index"/"find function", a naming coincidence unrelated to which keyword spells the declaration.130
+131
+`fn` was removed outright (no alias kept), so every existing `.ash` file needed migrating again: `tests/cases/**` (including nested `fixtures/`/`ash.libs/`), `benchmarks/fib.ash` and `fib20.ash` (the only benchmarks that declare a function), `README.md`'s example, and `deep_nesting_demo.ash` (a throwaway example file at the repo root, not part of the test suite, made earlier in the day to look at how deeply nested logic reads under the new syntax).132
+133
+Verified the same way as the previous rename: zero-warning rebuild, full regression suite byte-identical to baseline, all five benchmarks within a few percent of their pre-rename timings (no codegen change, just one more lexer keyword-length check), and the demo file's output unchanged.134
+135
+## 2026-10-07 (rename `let` to `local`)136
+Small follow-up to the Lua-alike syntax entry below: `let` is now spelled `local`, matching Lua's own keyword. Purely a lexer change plus renaming the internal `TOKEN_LET`/`let_statement()` to `TOKEN_LOCAL`/`local_statement()` so the code doesn't say one thing while the language says another — behavior is unchanged, including `local` still doing double duty for both declaring a new variable and reassigning an existing one (every reassignment still repeats the keyword, e.g. `local i = i + 1;`; that's a bigger, separate change, flagged but not done here — see `plans.md`'s open questions).137
+138
+Unlike the optional-parens/`and`/`or`/`not` change above, `let` was removed outright rather than kept as an alias, so every existing `.ash` file needed migrating: all of `ashvm/tests/cases/**` (including the nested `fixtures/` and `ash.libs/` directories, which a first pass over `tests/cases/*/*.ash` missed since they're one level deeper — caught by re-grepping with `find`), `ashvm/benchmarks/*.ash`, and `README.md`'s example. All mechanical word-boundary renames (`let` only ever appeared as the keyword, never as a substring of an identifier).139
+140
+Verified: zero-warning rebuild, full regression suite byte-identical to a re-captured baseline (one diagnostic's expected text changed: the source line and hint now read `local`, re-baselined since that's the intended new output), and all five benchmarks within a few percent of their pre-rename timings — expected, since the rename adds one keyword-length check to the lexer and changes no codegen at all.141
+142
+## 2026-10-07 (Lua-alike syntax: optional condition parens, and/or/not)143
+First slice of the Lua-flavored rework from `plans.md` (goals 1-3): `if`/`while` conditions no longer require parentheses, and the symbolic logical operators become word keywords, the way Lua spells them: `&&` -> `and`, `||` -> `or`, unary `!` -> `not` (`!=` stays symbolic). Blocks stay `{ }`-delimited and `;` stays a required statement terminator — both were tried as `then`/`do`/`end` keyword blocks and optional/no-op semicolons first, then reverted: the ask was Lua-*alike*, not a literal copy of Lua's own block grammar or its semicolon quirks, and Ash didn't need to inherit either.144
+145
+Confined entirely to `ashvm/src/lexer` and the if/while-condition parsing path in `ashvm/src/compiler` (`stmt_control.c`, `calls.c`'s `try_fuse_condition`) — the VM, bytecode format and every other statement kind are untouched. `try_fuse_condition()` now takes the token that must terminate a fused comparison (`)` when parenthesized, `{` when not), so the hand-fused `OP_CMP_JUMP` superinstruction still fires either way (`while i < n { ... }` and `while (i < n) { ... }` compile identically). `!` only changes meaning when it stood alone; `!=` is unaffected since the lexer still checks for a following `=` first.146
+147
+No existing `.ash` file needed migrating: nothing in `tests/cases/**` or `benchmarks/*.ash` used unparenthesized conditions or the old logical operators, so every one of them produces byte-identical bytecode to before. Verified: zero-warning rebuild, `ashvm/tests/scripts/run_regression.sh check` byte-identical to a pre-change baseline, and all five benchmarks within measurement noise of their pre-change timings (see `plans.md`'s performance table).148
+149
+**Deferred, not done here:** `kiln` keeps its own old syntax (separate lexer/parser, not blocked on this); the VS Code syntax extension (`ash-syntax`, separate repo) still needs `and`/`or`/`not` added to its keyword highlighting.150
+1151
## 2026-10-07 (back on Ash + source and test restructure)2152
First day back on this project after a break (the last entry here is from 2026-07-21), and it's good to finally be able to pick Ash up again. Before building anything new, the repo got a full cleanup so it's a place I actually want to work in: nothing in the language itself changed, only where files live.3153
README.mdmodified
+65−27@@ -11,34 +11,58 @@1111
Ash is a small programming language built from scratch in C - lexer,1212
compiler, and a bytecode VM, all hand-written with no external dependencies.1313
It has closures, hash maps, first-class functions, real floating-point14
-numbers, `try`/`catch`, and a colorized compiler that points straight at14
+numbers, `attempt`/`handle`, and a colorized compiler that points straight at1515
your mistakes.1616
1717
```ash18
-fn make_adder(x) {19
- return fn(y) { return x + y; };18
+forge make_adder(x) {19
+ yield forge(y) { yield x + y; };2020
}2121
22
-let add5 = make_adder(5);23
-print add5(3); // 822
+local add5 = make_adder(5);23
+say add5(3); // 82424
25
-let nums = [1, 2, 3, 4, 5];26
-let evens = filter(nums, fn(n) { return n % 2 == 0; });27
-print evens; // [2, 4]25
+local nums = [1, 2, 3, 4, 5];26
+local evens = filter(nums, forge(n) { yield n % 2 == 0; });27
+say evens; // [2, 4]2828
29
-try {30
- let m = {"name": "Ash"};31
- print m["missing"];32
-} catch (e) {33
- print "caught: " + e;29
+attempt {30
+ local m = {"name": "Ash"};31
+ say m["missing"];32
+} handle (e) {33
+ say "caught: " + e;3434
}35
+36
+// conditions can drop the parens, and logical operators read as words37
+given add5(3) > 7 and not (evens[0] == 1) {38
+ say "Lua-alike, not a Lua copy";39
+}40
+41
+each n in evens {42
+ say n;43
+}44
+45
+// stop/next, compound assignment, ternary, bitwise ops, and/none46
+local total = 0;47
+local i = 0;48
+during i < 10 {49
+ i += 1;50
+ given i == 3 { next; }51
+ given i == 7 { stop; }52
+ total += i;53
+}54
+say total > 0 ? "positive" : "zero or less"; // ternary55
+say (5 & 3) | (1 << 2); // bitwise: and, or, shift56
+57
+local maybe = none;58
+say maybe == none ? "nothing there" : "something there";3559
```3660
3761
## Engine3862
3963
Ash compiles to real bytecode and runs it on a stack-based VM (`ashvm`) with4064
computed-goto instruction dispatch and a handful of hand-fused41
-"superinstructions" for common patterns like `x = x + y` and `while (i < n)`65
+"superinstructions" for common patterns like `x = x + y` and `during (i < n)`4266
that collapse several bytecode ops into one. Ash used to ship a second,4367
slower tree-walk interpreter (`ashc`) as the place new features were4468
prototyped first - it has since been retired now that the VM has full@@ -64,12 +88,16 @@ optimizing native compiler.6488
6589
## Language features6690
67
-- Numbers (real doubles), strings (with escape sequences), arrays, hash maps91
+- 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)6893
- Functions, recursion, closures with by-value capture6994
- First-class functions - pass them around, store them in variables, call them indirectly70
-- `map`, `filter`, `reduce`, and 20+ other built-ins (string ops, file I/O, math)71
-- `try` / `catch` error handling72
-- `for (x in array)` and `while` loops95
+- `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 message97
+- `each (x in array)` and `during` loops, with `stop` / `next` for early exit and skip-to-next-iteration98
+- Compound assignment (`+= -= *= /=`) and `++` / `--`99
+- Ternary `cond ? a : b`100
+- Bitwise operators: `& | ^ ~ << >>`73101
- Colorized, Rust-style diagnostics for both compile-time and runtime74102
errors: file:line:column, a source snippet, a caret, and a contextual hint75103
- An interactive REPL with persistent variables across lines@@ -83,6 +111,10 @@ cd ashvm && make # builds ../bin/ashvm83111
../bin/ashvm # start the REPL84112
```85113
114
+`kiln` (compiles `.ash` straight to a native ELF/PE binary) and `forgepack`115
+(the package manager) build the same way, each from its own directory:116
+`cd kiln && make` / `cd forgepack && make`.117
+86118
## Project structure87119
ashvm/ the engine - everything below is self-contained inside here88120
src/@@ -94,19 +126,25 @@ call/lambda emission, statement compilation94126
builtins/ len, map, filter, reduce, string/file ops95127
diagnostics/ shared Rust-style error reporting (compile-time and runtime)96128
97
-Every subsystem keeps sources in `C/`, headers in `H/`, and build output in a98
-mirrored `O/` tree (kiln also has `D/` for dependency files), e.g.99
-`src/vm/C/dispatch.c` includes `src/vm/H/vm.h` and builds to `src/vm/O/dispatch.o`.129
+Every subsystem keeps sources in `C/`, headers in `H/`, build output in a130
+mirrored `O/` tree, and generated dependency files in `D/` (so editing a131
+shared header correctly triggers a rebuild of everything that includes it),132
+e.g. `src/vm/C/dispatch.c` includes `src/vm/H/vm.h` and builds to133
+`src/vm/O/dispatch.o` with `src/vm/D/dispatch.d` tracking the header.100134
app/ main.c entry point (file mode + REPL)101135
benchmarks/ .ash and .cpp benchmark programs, plus the x86-64 JIT experiment102136
tests/ cases/<feature>/*.ash programs covering each language feature; scripts/ holds the regression runners103
-kiln/ a standalone .ash compiler (Windows + Linux). Not started yet.104137
105
-## Editor support138
+kiln/ a second, independent implementation: compiles .ash straight to a139
+native ELF/PE binary (no VM, no libc), same language, full feature parity140
+with ashvm, cross-validated against it directly (see CHANGELOG.md).106141
107
-A VS Code extension with syntax highlighting is at https://github.com/lennoxrose/ash-syntax108
-A VS Code extension with the Icon pack is at https://github.com/lennoxrose/ash-icons/releases/tag/v1.2.12vsix142
+forgepack/ package manager -- `forgepack init/add/install/list`, dependencies143
+fetched from GitHub into a project-local @ash-modules/ folder (same idea144
+as node_modules). `@import <name>;` already works with it. No registry145
+yet -- see ideas/ash_modules.md for the full design and what's next.109146
110
-## Future of Ash147
+## Editor support111148
112
-Read the plans.md for further plans about Ash\ No newline at end of file149
+A VS Code extension with syntax highlighting is at https://github.com/lennoxrose/ash-syntax150
+A VS Code extension with the Icon pack is at https://github.com/lennoxrose/ash-icons/releases/tag/v1.2.12vsix\ No newline at end of fileashvm/Makefilemodified
+18−2@@ -18,6 +18,7 @@ ASHVM_SRC = \1818
src/compiler/C/constants.c \1919
src/compiler/C/expr.c \2020
src/compiler/C/import_paths.c \21
+ src/compiler/C/loop_stack.c \2122
src/compiler/C/state.c \2223
src/compiler/C/stmt.c \2324
src/compiler/C/stmt_control.c \@@ -31,13 +32,25 @@ ASHVM_SRC = \3132
3233
# Objects and dependency files live in trees parallel to each C tree:3334
# src/<subsystem>/C/<feature>/x.c -> src/<subsystem>/O/<feature>/x.o35
+# src/<subsystem>/D/<feature>/x.d3436
obj_of = $(patsubst %.c,%.o,$(subst /C/,/O/,$(1)))37
+dep_of = $(patsubst %.c,%.d,$(subst /C/,/D/,$(1)))3538
ASHVM_OBJ = $(foreach s,$(ASHVM_SRC),$(call obj_of,$(s)))39
+ASHVM_DEP = $(foreach s,$(ASHVM_SRC),$(call dep_of,$(s)))3640
41
+# -MMD -MP: emit a per-object .d file listing the headers it includes, so42
+# editing a shared header (e.g. lexer.h, value.h, chunk.h) correctly43
+# triggers a rebuild of every .o that includes it, not just files whose44
+# own .c changed. Without this, make's only dependency is "the .c is newer45
+# than the .o" -- a header-only edit silently produces a binary built from46
+# mismatched token/opcode numbering across files, with no compile error,47
+# until something fails at runtime for reasons the source doesn't explain48
+# (this bit for real during the kiln-parity work this mirrors kiln's own49
+# Makefile comment for -- see kiln/Makefile).3750
define compile_rule3851
$(call obj_of,$(1)): $(1)39
- @mkdir -p $$(dir $$@)40
- $$(CC) $$(CFLAGS) -c $$< -o $$@52
+ @mkdir -p $$(dir $$@) $$(dir $(call dep_of,$(1)))53
+ $$(CC) $$(CFLAGS) -MMD -MP -MF $(call dep_of,$(1)) -MT $$@ -c $$< -o $$@4154
endef4255
$(foreach s,$(ASHVM_SRC),$(eval $(call compile_rule,$(s))))4356
@@ -49,8 +62,11 @@ $(BIN): $(ASHVM_OBJ)4962
mkdir -p ../bin5063
$(CC) $(CFLAGS) -o $(BIN) $(ASHVM_OBJ) $(LDFLAGS)5164
65
+-include $(ASHVM_DEP)66
+5267
clean:5368
find src -name '*.o' -delete69
+ find src -name '*.d' -delete5470
rm -f $(BIN)5571
5672
.PHONY: all cleanashvm/benchmarks/countdown.ashmodified
+6−6@@ -1,7 +1,7 @@1
-let i = 20000000;2
-let count = 0;3
-while (i > 0) {4
- let count = count + 1;5
- let i = i - 1;1
+local i = 20000000;2
+local count = 0;3
+during (i > 0) {4
+ local count = count + 1;5
+ local i = i - 1;66
}7
-print count;7
+say count;ashvm/benchmarks/fib.ashmodified
+5−5@@ -1,7 +1,7 @@1
-fn fib(n) {2
- if (n < 2) {3
- return n;1
+forge fib(n) {2
+ given (n < 2) {3
+ yield n;44
}5
- return fib(n - 1) + fib(n - 2);5
+ yield fib(n - 1) + fib(n - 2);66
}7
-print fib(30);7
+say fib(30);ashvm/benchmarks/fib20.ashmodified
+1−1@@ -1 +1 @@1
-fn fib(n) { if (n < 2) { return n; } return fib(n-1)+fib(n-2); } print fib(20);1
+forge fib(n) { given (n < 2) { yield n; } yield fib(n-1)+fib(n-2); } say fib(20);ashvm/benchmarks/hashmap.ashmodified
+10−10@@ -1,13 +1,13 @@1
-let m = {};2
-let i = 0;3
-while (i < 50000) {1
+local m = {};2
+local i = 0;3
+during (i < 50000) {44
m[str(i)] = i * 2;5
- let i = i + 1;5
+ local i = i + 1;66
}7
-let j = 0;8
-let total = 0;9
-while (j < 50000) {10
- let total = total + m[str(j)];11
- let j = j + 1;7
+local j = 0;8
+local total = 0;9
+during (j < 50000) {10
+ local total = total + m[str(j)];11
+ local j = j + 1;1212
}13
-print total;13
+say total;ashvm/benchmarks/loop.ashmodified
+6−6@@ -1,7 +1,7 @@1
-let sum = 0;2
-let i = 0;3
-while (i < 20000000) {4
- let sum = sum + i;5
- let i = i + 1;1
+local sum = 0;2
+local i = 0;3
+during (i < 20000000) {4
+ local sum = sum + i;5
+ local i = i + 1;66
}7
-print sum;7
+say sum;ashvm/benchmarks/loop_varbound.ashmodified
+7−7@@ -1,8 +1,8 @@1
-let n = 20000000;2
-let sum = 0;3
-let i = 0;4
-while (i < n) {5
- let sum = sum + i;6
- let i = i + 1;1
+local n = 20000000;2
+local sum = 0;3
+local i = 0;4
+during (i < n) {5
+ local sum = sum + i;6
+ local i = i + 1;77
}8
-print sum;8
+say sum;ashvm/src/app/D/main.dadded
+8−0@@ -0,0 +1,8 @@1
+src/app/O/main.o: src/app/C/main.c src/compiler/H/compiler.h \2
+ src/vm/H/chunk.h src/value/H/value.h src/vm/H/vm.h \3
+ src/diagnostics/H/diagnostics.h4
+src/compiler/H/compiler.h:5
+src/vm/H/chunk.h:6
+src/value/H/value.h:7
+src/vm/H/vm.h:8
+src/diagnostics/H/diagnostics.h:ashvm/src/builtins/C/builtins.cmodified
+3−1@@ -7,7 +7,8 @@ static const char *builtin_names[] = {77
"len", "push", "input", "str", "num", "sqrt", "abs", "floor",88
"map", "filter", "reduce", "keys", "values", "has", "delete",99
"split", "join", "substring", "indexOf", "replace", "upper", "lower", "trim",10
- "read_file", "write_file", "append_file", "file_exists"10
+ "read_file", "write_file", "append_file", "file_exists",11
+ "type"1112
};1213
#define NUM_BUILTINS (int)(sizeof(builtin_names) / sizeof(builtin_names[0]))1314
@@ -23,5 +24,6 @@ VMValue vm_call_builtin(int id, VMValue *args, int argc) {2324
if (id >= 8 && id <= 14) return vm_call_builtin_collection(id, args, argc);2425
if (id >= 15 && id <= 22) return vm_call_builtin_string(id, args, argc);2526
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); // type2628
vm_runtime_error("unknown builtin id: %d\n", id);2729
}ashvm/src/builtins/C/core.cmodified
+20−0@@ -52,6 +52,26 @@ VMValue vm_call_builtin_core(int id, VMValue *args, int argc) {5252
if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("floor() expects a number\n"); }5353
return vm_num(floor(args[0].number));5454
}55
+ case 27: { // type -- introspection, needed for anything that has to56
+ // branch on a value's own type at runtime (json's57
+ // stringify is the motivating case: it can't decide58
+ // "format this as an array vs. a map vs. a string"59
+ // from pure Ash without some way to ask a value what60
+ // it is).61
+ if (argc != 1) { vm_runtime_error("type() expects 1 argument\n"); }62
+ const char *name;63
+ switch (args[0].type) {64
+ case VM_NUM: name = "number"; break;65
+ case VM_STR: name = "string"; break;66
+ case VM_ARRAY: name = "array"; break;67
+ case VM_MAP: name = "map"; break;68
+ case VM_FUNCTION: case VM_CLOSURE: name = "function"; break;69
+ case VM_NIL: name = "none"; break;70
+ default: name = "unknown"; break;71
+ }72
+ char *copy = malloc(strlen(name) + 1); strcpy(copy, name);73
+ return vm_str(copy);74
+ }5575
}5676
vm_runtime_error("unknown builtin id: %d\n", id);5777
}ashvm/src/builtins/D/builtins.dadded
+8−0@@ -0,0 +1,8 @@1
+src/builtins/O/builtins.o: src/builtins/C/builtins.c \2
+ src/builtins/H/builtins.h src/value/H/value.h src/builtins/H/internal.h \3
+ src/vm/H/vm.h src/vm/H/chunk.h4
+src/builtins/H/builtins.h:5
+src/value/H/value.h:6
+src/builtins/H/internal.h:7
+src/vm/H/vm.h:8
+src/vm/H/chunk.h:ashvm/src/builtins/D/collection.dadded
+8−0@@ -0,0 +1,8 @@1
+src/builtins/O/collection.o: src/builtins/C/collection.c \2
+ src/builtins/H/internal.h src/value/H/value.h src/vm/H/vm.h \3
+ src/vm/H/chunk.h src/value/H/hashmap.h4
+src/builtins/H/internal.h:5
+src/value/H/value.h:6
+src/vm/H/vm.h:7
+src/vm/H/chunk.h:8
+src/value/H/hashmap.h:ashvm/src/builtins/D/core.dadded
+7−0@@ -0,0 +1,7 @@1
+src/builtins/O/core.o: src/builtins/C/core.c src/builtins/H/internal.h \2
+ src/value/H/value.h src/vm/H/vm.h src/vm/H/chunk.h src/value/H/hashmap.h3
+src/builtins/H/internal.h:4
+src/value/H/value.h:5
+src/vm/H/vm.h:6
+src/vm/H/chunk.h:7
+src/value/H/hashmap.h:ashvm/src/builtins/D/file.dadded
+6−0@@ -0,0 +1,6 @@1
+src/builtins/O/file.o: src/builtins/C/file.c src/builtins/H/internal.h \2
+ src/value/H/value.h src/vm/H/vm.h src/vm/H/chunk.h3
+src/builtins/H/internal.h:4
+src/value/H/value.h:5
+src/vm/H/vm.h:6
+src/vm/H/chunk.h:ashvm/src/builtins/D/string.dadded
+7−0@@ -0,0 +1,7 @@1
+src/builtins/O/string.o: src/builtins/C/string.c \2
+ src/builtins/H/internal.h src/value/H/value.h src/vm/H/vm.h \3
+ src/vm/H/chunk.h4
+src/builtins/H/internal.h:5
+src/value/H/value.h:6
+src/vm/H/vm.h:7
+src/vm/H/chunk.h:ashvm/src/compiler/C/calls.cmodified
+14−9@@ -4,6 +4,7 @@44
#include "compiler/H/internal.h"55
#include "builtins/H/builtins.h"66
#include "diagnostics/H/diagnostics.h"7
+#include "compiler/H/loop_stack.h"78
89
void emit_call(Token id) {910
int builtin_id = vm_builtin_lookup(id.start, id.length);@@ -75,15 +76,15 @@ void emit_call_named(const char *name, int len) {7576
emit((uint8_t)argc);7677
}7778
78
-// fn(params) { body } as an expression. Captures the ENTIRE enclosing scope's79
-// locals by value at creation time (same rule as ashc's closures used to80
-// have). The captured variables get their own reserved local slots in the81
-// lambda's own chunk, placed right after its parameters, so the compiled82
-// body can reference them via the normal resolve_local() mechanism just83
-// like any other local.79
+// forge(params) { body } as an expression. Captures the ENTIRE enclosing80
+// scope's locals by value at creation time (same rule as ashc's closures81
+// used to have). The captured variables get their own reserved local slots82
+// in the lambda's own chunk, placed right after its parameters, so the83
+// compiled body can reference them via the normal resolve_local()84
+// mechanism just like any other local.8485
void lambda_literal(void) {8586
advance_token();86
- expect(TOKEN_LPAREN, "expected '(' after 'fn'");87
+ expect(TOKEN_LPAREN, "expected '(' after 'forge'");8788
8889
char namebuf[32];8990
int namelen = snprintf(namebuf, sizeof(namebuf), "__lambda_%d", lambda_counter++);@@ -123,6 +124,9 @@ void lambda_literal(void) {123124
char outer_locals[MAX_VM_LOCALS][64];124125
int outer_local_count = local_count;125126
memcpy(outer_locals, local_names, sizeof(local_names));127
+ int outer_loop_depth;128
+ loop_depth_save(&outer_loop_depth);129
+ loop_depth_reset();126130
127131
chunk = &vm_functions[fn_idx].chunk;128132
local_count = 0;@@ -136,6 +140,7 @@ void lambda_literal(void) {136140
chunk = outer_chunk;137141
local_count = outer_local_count;138142
memcpy(local_names, outer_locals, sizeof(local_names));143
+ loop_depth_restore(outer_loop_depth);139144
140145
emit_op(OP_MAKE_CLOSURE);141146
emit((uint8_t)fn_idx);@@ -147,7 +152,7 @@ void lambda_literal(void) {147152
// offset to patch_jump() once the branch target is known, or -1 if the148153
// condition didn't match the fusable shape (caller falls back to a plain149154
// expression() + OP_JUMP_IF_FALSE).150
-int try_fuse_condition(void) {155
+int try_fuse_condition(TokenType terminator) {151156
if (current.type != TOKEN_IDENTIFIER) return -1;152157
Token save = current;153158
Token id = current;@@ -185,7 +190,7 @@ int try_fuse_condition(void) {185190
lexer_init(save.start + save.length); current = save; return -1;186191
}187192
188
- if (current.type != TOKEN_RPAREN) {193
+ if (current.type != terminator) {189194
lexer_init(save.start + save.length); current = save; return -1;190195
}191196
ashvm/src/compiler/C/constants.cmodified
+1−1@@ -45,7 +45,7 @@ int try_declare_import_constant(const char *name, int len) {4545
value = vm_str(vm_copy_string_escaped(current.start, current.length));4646
advance_token();4747
} else {48
- diagnostics_report("error", "imported top-level 'let' must be a literal number or string constant", current.start, current.length);48
+ diagnostics_report("error", "imported top-level 'local' must be a literal number or string constant", current.start, current.length);4949
exit(1);5050
}5151
declare_constant(combined, combined_len, value);ashvm/src/compiler/C/expr.cmodified
+59−4@@ -7,7 +7,7 @@77
// comparison -> additive -> term -> unary -> postfix -> primary.88
99
static void primary(void) {10
- if (current.type == TOKEN_FN) { lambda_literal(); return; }10
+ if (current.type == TOKEN_FORGE) { lambda_literal(); return; }1111
if (current.type == TOKEN_NUMBER) {1212
double v = strtod(current.start, NULL);1313
advance_token();@@ -20,6 +20,17 @@ static void primary(void) {2020
emit_constant(vm_str(s));2121
return;2222
}23
+ if (current.type == TOKEN_YES || current.type == TOKEN_NO) {24
+ double v = current.type == TOKEN_YES ? 1.0 : 0.0;25
+ advance_token();26
+ emit_constant(vm_num(v));27
+ return;28
+ }29
+ if (current.type == TOKEN_NONE) {30
+ advance_token();31
+ emit_constant(vm_nil());32
+ return;33
+ }2334
if (current.type == TOKEN_LBRACKET) {2435
advance_token();2536
int count = 0;@@ -129,6 +140,7 @@ static void postfix(void) {129140
static void unary(void) {130141
if (current.type == TOKEN_BANG) { advance_token(); unary(); emit_op(OP_NOT); return; }131142
if (current.type == TOKEN_MINUS) { advance_token(); unary(); emit_op(OP_NEG); return; }143
+ if (current.type == TOKEN_TILDE) { advance_token(); unary(); emit_op(OP_BNOT); return; }132144
postfix();133145
}134146
@@ -172,12 +184,55 @@ static void comparison(void) {172184
}173185
}174186
175
-static void logical_and(void) {187
+// Bitwise operators (& | ^ << >>), one flat precedence level (left to188
+// right) sitting between comparison and logical_and -- not real C189
+// precedence (which splits shift/bitand/bitxor/bitor into four separate190
+// levels), a deliberate simplification matching kiln's own choice here.191
+// Operands are truncated to int64 in the VM (do_BAND etc. in192
+// vm/C/dispatch.c), same "assume NUMBER" scope limit as -, *, /, and the193
+// comparison operators.194
+static void bitwise(void) {176195
comparison();177
- while (current.type == TOKEN_AND) { advance_token(); comparison(); emit_op(OP_AND); }196
+ while (current.type == TOKEN_AMP || current.type == TOKEN_PIPE ||197
+ current.type == TOKEN_CARET || current.type == TOKEN_SHL || current.type == TOKEN_SHR) {198
+ TokenType op = current.type;199
+ advance_token();200
+ comparison();201
+ switch (op) {202
+ case TOKEN_AMP: emit_op(OP_BAND); break;203
+ case TOKEN_PIPE: emit_op(OP_BOR); break;204
+ case TOKEN_CARET: emit_op(OP_BXOR); break;205
+ case TOKEN_SHL: emit_op(OP_SHL); break;206
+ default: emit_op(OP_SHR); break;207
+ }208
+ }209
+}210
+211
+static void logical_and(void) {212
+ bitwise();213
+ while (current.type == TOKEN_AND) { advance_token(); bitwise(); emit_op(OP_AND); }178214
}179215
static void logical_or(void) {180216
logical_and();181217
while (current.type == TOKEN_OR) { advance_token(); logical_and(); emit_op(OP_OR); }182218
}183
-void expression(void) { logical_or(); }219
+220
+// a ? b : c -- lowest precedence, sits above logical_or. Right-associative221
+// via the recursive expression() calls for both branches (so222
+// `a ? b : c ? d : e` parses as `a ? b : (c ? d : e)`), same jump-patch223
+// shape as given/during's own truthy-test-then-jump pattern, just as an224
+// expression instead of a statement. OP_JUMP_IF_FALSE pops the condition225
+// itself, matching how compile_given_stmt already uses it.226
+void expression(void) {227
+ logical_or();228
+ if (current.type == TOKEN_QUESTION) {229
+ advance_token();230
+ int else_jump = emit_jump(OP_JUMP_IF_FALSE);231
+ expression(); // true branch232
+ int end_jump = emit_jump(OP_JUMP);233
+ expect(TOKEN_COLON, "expected ':' in ternary expression");234
+ patch_jump(else_jump);235
+ expression(); // false branch236
+ patch_jump(end_jump);237
+ }238
+}ashvm/src/compiler/C/import_paths.cmodified
+6−0@@ -50,8 +50,14 @@ int import_resolve_path(const char *raw_path, int raw_len,5050
return 0;5151
}5252
}53
+ // ash.libs/ (hand-written local libraries) wins first; a package54
+ // installed by forgepack (see ideas/ash_modules.md) only gets55
+ // tried if there's no local library shadowing the same name.5356
snprintf(candidate, sizeof(candidate), "%s/ash.libs/%.*s/%.*s.ash",5457
project_root_dir, raw_len, raw_path, raw_len, raw_path);58
+ if (realpath(candidate, canonical_out) != NULL) return 1;59
+ snprintf(candidate, sizeof(candidate), "%s/@ash-modules/%.*s/%.*s.ash",60
+ project_root_dir, raw_len, raw_path, raw_len, raw_path);5561
}5662
5763
if (realpath(candidate, canonical_out) == NULL) {ashvm/src/compiler/C/loop_stack.cadded
+68−0@@ -0,0 +1,68 @@1
+#include <stdlib.h>2
+#include "compiler/H/loop_stack.h"3
+#include "compiler/H/state.h"4
+#include "diagnostics/H/diagnostics.h"5
+6
+#define MAX_LOOP_NESTING 167
+#define MAX_LOOP_JUMPS 648
+9
+typedef struct {10
+ int stop_jumps[MAX_LOOP_JUMPS];11
+ int stop_count;12
+ int next_jumps[MAX_LOOP_JUMPS];13
+ int next_count;14
+} LoopContext;15
+16
+static LoopContext loop_stack[MAX_LOOP_NESTING];17
+static int loop_depth = 0;18
+19
+void loop_push(void) {20
+ if (loop_depth >= MAX_LOOP_NESTING) {21
+ diagnostics_report("error", "too many nested loops", current.start, current.length);22
+ exit(1);23
+ }24
+ loop_stack[loop_depth].stop_count = 0;25
+ loop_stack[loop_depth].next_count = 0;26
+ loop_depth++;27
+}28
+29
+void loop_record_stop(Token keyword, int patch_offset) {30
+ if (loop_depth == 0) {31
+ diagnostics_report("error", "'stop' outside a loop", keyword.start, keyword.length);32
+ exit(1);33
+ }34
+ LoopContext *ctx = &loop_stack[loop_depth - 1];35
+ if (ctx->stop_count >= MAX_LOOP_JUMPS) {36
+ diagnostics_report("error", "too many stop statements in one loop", keyword.start, keyword.length);37
+ exit(1);38
+ }39
+ ctx->stop_jumps[ctx->stop_count++] = patch_offset;40
+}41
+42
+void loop_record_next(Token keyword, int patch_offset) {43
+ if (loop_depth == 0) {44
+ diagnostics_report("error", "'next' outside a loop", keyword.start, keyword.length);45
+ exit(1);46
+ }47
+ LoopContext *ctx = &loop_stack[loop_depth - 1];48
+ if (ctx->next_count >= MAX_LOOP_JUMPS) {49
+ diagnostics_report("error", "too many next statements in one loop", keyword.start, keyword.length);50
+ exit(1);51
+ }52
+ ctx->next_jumps[ctx->next_count++] = patch_offset;53
+}54
+55
+void loop_patch_nexts(void) {56
+ LoopContext *ctx = &loop_stack[loop_depth - 1];57
+ for (int i = 0; i < ctx->next_count; i++) patch_jump(ctx->next_jumps[i]);58
+}59
+60
+void loop_pop_and_patch_stops(void) {61
+ LoopContext *ctx = &loop_stack[loop_depth - 1];62
+ for (int i = 0; i < ctx->stop_count; i++) patch_jump(ctx->stop_jumps[i]);63
+ loop_depth--;64
+}65
+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; }ashvm/src/compiler/C/stmt.cmodified
+161−27@@ -3,13 +3,14 @@33
#include <string.h>44
#include "compiler/H/internal.h"55
#include "diagnostics/H/diagnostics.h"6
+#include "compiler/H/loop_stack.h"67
78
// statement() is shared between top-level parsing (compile()'s own loop,89
// and stmt_import.c's parse_imported_file_body()) and here, inside a9
-// fn/if/while/try body -- block_depth distinguishes the two so the "only10
-// fn/let at an imported file's top level" guard in statement() only11
-// fires for genuinely top-level statements, never for an ordinary12
-// return/if/etc. nested inside an imported function's own body.10
+// forge/given/during/attempt body -- block_depth distinguishes the two so11
+// the "only forge/local at an imported file's top level" guard in12
+// statement() only fires for genuinely top-level statements, never for an13
+// ordinary yield/given/etc. nested inside an imported function's own body.1314
static int block_depth = 0;1415
1516
void block(void) {@@ -20,17 +21,24 @@ void block(void) {2021
expect(TOKEN_RBRACE, "expected '}'");2122
}2223
23
-// let name = value; -- also tries to fuse `let x = x OP (number|local);`24
+// local name = value; -- also tries to fuse `local x = x OP (number|local);`2425
// into a single OP_ACC_LOCAL instead of a full expression compile, when the2526
// shape matches exactly (see vm/compiler.c's history / rundown.md).26
-static void let_statement(void) {27
+static void local_statement(void) {2728
advance_token();28
- expect(TOKEN_IDENTIFIER, "expected variable name after 'let'");29
+ expect(TOKEN_IDENTIFIER, "expected variable name after 'local'");2930
const char *name = previous.start;3031
int len = previous.length;3132
expect(TOKEN_EQUAL, "expected '=' after variable name");3233
33
- if (try_declare_import_constant(name, len)) return;34
+ // Bug (found while writing the first real forgepack module, a JSON35
+ // codec -- any imported function with a `local` inside a loop failed36
+ // to compile): try_declare_import_constant() has no way to tell "a37
+ // top-level local in an imported file" from "an ordinary local deep38
+ // inside one of that file's own functions" except this check -- it39
+ // only makes sense at block_depth == 0, same as the sibling guard in40
+ // statement() above (which already gets this right).41
+ if (block_depth == 0 && try_declare_import_constant(name, len)) return;3442
3543
int slot = resolve_local(name, len);3644
@@ -88,11 +96,76 @@ static void let_statement(void) {8896
}8997
}9098
91
-// Either a bare call (`foo();`) or an index assignment (`foo[i] = value;`) --92
-// the two statement shapes that start with a bare identifier.99
+static int resolve_local_or_error(Token id) {100
+ int slot = resolve_local(id.start, id.length);101
+ if (slot == -1) {102
+ char msg[128];103
+ snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);104
+ diagnostics_report("error", msg, id.start, id.length);105
+ exit(1);106
+ }107
+ return slot;108
+}109
+110
+// x += e; / x -= e; / x *= e; / x /= e; -- sugar for x = x <op> e.111
+// `+=` reuses OP_ADD directly (the exact same tag-aware string-concat-or-112
+// numeric-add logic `+` itself uses); `-=`/`*=`/`/=` are NUMBER-only, same113
+// scope limit as `-`/`*`/`/` themselves. Always the general (GET_LOCAL,114
+// expression(), op, SET_LOCAL) path, not the OP_ACC_LOCAL fusion -- the115
+// RHS here is a full expression, not just a number-or-local operand.116
+static void compound_assignment_statement(int slot, TokenType op) {117
+ advance_token(); // consume the += / -= / *= / /=118
+ emit2(OP_GET_LOCAL, (uint8_t)slot);119
+ expression();120
+ expect(TOKEN_SEMICOLON, "expected ';' after assignment");121
+ switch (op) {122
+ case TOKEN_PLUS_EQUAL: emit_op(OP_ADD); break;123
+ case TOKEN_MINUS_EQUAL: emit_op(OP_SUB); break;124
+ case TOKEN_STAR_EQUAL: emit_op(OP_MUL); break;125
+ default: emit_op(OP_DIV); break; // TOKEN_SLASH_EQUAL126
+ }127
+ emit2(OP_SET_LOCAL, (uint8_t)slot);128
+ emit_op(OP_POP);129
+}130
+131
+// x++; / x--; -- sugar for x = x + 1 / x = x - 1. Statement-only (not a132
+// pre/post-increment EXPRESSION with a value of its own), matching kiln's133
+// own incdec_statement exactly.134
+static void incdec_statement(int slot, TokenType op) {135
+ advance_token(); // consume ++ or --136
+ expect(TOKEN_SEMICOLON, "expected ';' after statement");137
+ emit2(OP_GET_LOCAL, (uint8_t)slot);138
+ emit_constant(vm_num(1));139
+ emit_op(op == TOKEN_PLUS_PLUS ? OP_ADD : OP_SUB);140
+ emit2(OP_SET_LOCAL, (uint8_t)slot);141
+ emit_op(OP_POP);142
+}143
+144
+// A bare call (`foo();`), an index assignment (`foo[i] = value;`), or a145
+// compound-assignment/increment-decrement on an already-declared local --146
+// the statement shapes that start with a bare identifier.93147
static void identifier_statement(void) {94148
Token id = current;95149
advance_token();150
+ // A namespaced call used as its OWN statement (`json.save(...);`,151
+ // discarding the result) rather than nested in an expression152
+ // (`local x = json.load(...);`, where expr.c's primary() already153
+ // handles this same `ident.member(...)` shape) -- found missing154
+ // while writing the first real forgepack module, whose whole point155
+ // (`json.save(path, value);`) is exactly this shape.156
+ if (current.type == TOKEN_DOT) {157
+ advance_token();158
+ expect(TOKEN_IDENTIFIER, "expected name after '.'");159
+ Token member = previous;160
+ char combined[128];161
+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s",162
+ id.length, id.start, member.length, member.start);163
+ expect(TOKEN_LPAREN, "expected '(' after namespaced function name");164
+ emit_call_named(combined, combined_len);165
+ expect(TOKEN_SEMICOLON, "expected ';' after call");166
+ emit_op(OP_POP);167
+ return;168
+ }96169
if (current.type == TOKEN_LPAREN) {97170
advance_token();98171
emit_call(id);@@ -101,13 +174,7 @@ static void identifier_statement(void) {101174
return;102175
}103176
if (current.type == TOKEN_LBRACKET) {104
- int slot = resolve_local(id.start, id.length);105
- if (slot == -1) {106
- char msg[128];107
- snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);108
- diagnostics_report("error", msg, id.start, id.length);109
- exit(1);110
- }177
+ int slot = resolve_local_or_error(id);111178
emit2(OP_GET_LOCAL, (uint8_t)slot);112179
advance_token();113180
expression();@@ -118,41 +185,108 @@ static void identifier_statement(void) {118185
emit_op(OP_INDEX_SET);119186
return;120187
}188
+ if (current.type == TOKEN_PLUS_EQUAL || current.type == TOKEN_MINUS_EQUAL ||189
+ current.type == TOKEN_STAR_EQUAL || current.type == TOKEN_SLASH_EQUAL) {190
+ compound_assignment_statement(resolve_local_or_error(id), current.type);191
+ return;192
+ }193
+ if (current.type == TOKEN_PLUS_PLUS || current.type == TOKEN_MINUS_MINUS) {194
+ incdec_statement(resolve_local_or_error(id), current.type);195
+ return;196
+ }197
+ // Bare reassignment (`x = expr;`, no `local`) on an already-declared198
+ // variable -- found missing while writing the first real forgepack199
+ // module (a JSON codec, which reassigns a loop position variable200
+ // throughout). kiln already supports this (its local_statement()201
+ // reassigns an existing slot in place rather than requiring `local`202
+ // again); ashvm didn't, which is exactly the kind of "both engines203
+ // run the exact same code" gap this project's cross-engine audits204
+ // exist to catch. Plain SET_LOCAL, same as local_statement()'s own205
+ // `slot != -1` path -- the RHS here is a full expression, so this206
+ // doesn't try the OP_ACC_LOCAL fusion local_statement() attempts207
+ // first (that fusion only fires for a number-or-local RHS operand).208
+ if (current.type == TOKEN_EQUAL) {209
+ int slot = resolve_local_or_error(id);210
+ advance_token();211
+ expression();212
+ expect(TOKEN_SEMICOLON, "expected ';' after assignment");213
+ emit2(OP_SET_LOCAL, (uint8_t)slot);214
+ emit_op(OP_POP);215
+ return;216
+ }121217
diagnostics_report("error", "unexpected identifier statement", current.start, current.length);122218
exit(1);123219
}124220
221
+// stop; -- jumps past the innermost loop; the jump is recorded, not222
+// resolved here, since the innermost loop's own codegen223
+// (compile_during_stmt/compile_each_stmt in stmt_control.c) hasn't emitted224
+// the "after the loop" landing point yet.225
+static void stop_statement(void) {226
+ Token keyword = current;227
+ advance_token();228
+ expect(TOKEN_SEMICOLON, "expected ';' after 'stop'");229
+ loop_record_stop(keyword, emit_jump(OP_JUMP));230
+}231
+232
+// next; -- jumps to the innermost loop's per-iteration advance step (an233
+// each-loop's index increment, or straight back to the condition for a234
+// during loop) -- also recorded, not resolved here, for the same reason235
+// as stop above.236
+static void next_statement(void) {237
+ Token keyword = current;238
+ advance_token();239
+ expect(TOKEN_SEMICOLON, "expected ';' after 'next'");240
+ loop_record_next(keyword, emit_jump(OP_JUMP));241
+}242
+243
+// raise expr; -- expr must evaluate to a string at runtime (checked by244
+// the VM's do_RAISE, not here, since the raised value is a general245
+// expression that could be anything). Hands off to vm_raise_value, which246
+// performs the exact same attempt/handle unwind every other runtime error247
+// already goes through (vm/C/errors.c).248
+static void raise_statement(void) {249
+ advance_token();250
+ expression();251
+ expect(TOKEN_SEMICOLON, "expected ';' after 'raise'");252
+ emit_op(OP_RAISE);253
+}254
+125255
void statement(void) {126256
if (current.type == TOKEN_IMPORT) { advance_token(); compile_import_stmt(); return; }127257
imports_close_block();128258
129259
const char *ns; int ns_len;130260
get_import_namespace(&ns, &ns_len);131
- if (ns_len > 0 && block_depth == 0 && current.type != TOKEN_FN && current.type != TOKEN_LET) {132
- diagnostics_report("error", "only 'fn' and constant 'let' declarations are allowed at an imported file's top level", current.start, current.length);261
+ if (ns_len > 0 && block_depth == 0 && current.type != TOKEN_FORGE && current.type != TOKEN_LOCAL) {262
+ diagnostics_report("error", "only 'forge' and constant 'local' declarations are allowed at an imported file's top level", current.start, current.length);133263
exit(1);134264
}135265
136
- if (current.type == TOKEN_PRINT) {266
+ if (current.type == TOKEN_SAY) {137267
advance_token();138268
expression();139269
expect(TOKEN_SEMICOLON, "expected ';' after statement");140270
emit_op(OP_PRINT);141271
return;142272
}143
- if (current.type == TOKEN_LET) { let_statement(); return; }144
- if (current.type == TOKEN_RETURN) {273
+ if (current.type == TOKEN_LOCAL) { local_statement(); return; }274
+ if (current.type == TOKEN_YIELD) {145275
advance_token();146276
if (current.type != TOKEN_SEMICOLON) expression();147277
else emit_constant(vm_num(0));148
- expect(TOKEN_SEMICOLON, "expected ';' after return");278
+ expect(TOKEN_SEMICOLON, "expected ';' after yield");149279
emit_op(OP_RETURN);150280
return;151281
}152
- if (current.type == TOKEN_FN) { advance_token(); compile_fn_decl(); return; }153
- if (current.type == TOKEN_IF) { advance_token(); compile_if_stmt(); return; }154
- if (current.type == TOKEN_TRY) { advance_token(); compile_try_stmt(); return; }155
- if (current.type == TOKEN_WHILE) { advance_token(); compile_while_stmt(); return; }282
+ if (current.type == TOKEN_FORGE) { advance_token(); compile_forge_decl(); return; }283
+ if (current.type == TOKEN_GIVEN) { advance_token(); compile_given_stmt(); return; }284
+ if (current.type == TOKEN_ATTEMPT) { advance_token(); compile_attempt_stmt(); return; }285
+ if (current.type == TOKEN_DURING) { advance_token(); compile_during_stmt(); return; }286
+ if (current.type == TOKEN_EACH) { advance_token(); compile_each_stmt(); return; }287
+ if (current.type == TOKEN_STOP) { stop_statement(); return; }288
+ if (current.type == TOKEN_NEXT) { next_statement(); return; }289
+ if (current.type == TOKEN_RAISE) { raise_statement(); return; }156290
if (current.type == TOKEN_IDENTIFIER) { identifier_statement(); return; }157291
158292
diagnostics_report("error", "unexpected token", current.start, current.length);ashvm/src/compiler/C/stmt_control.cmodified
+173−31@@ -2,12 +2,14 @@22
#include <string.h>33
#include "compiler/H/internal.h"44
#include "diagnostics/H/diagnostics.h"5
+#include "builtins/H/builtins.h"6
+#include "compiler/H/loop_stack.h"57
68
// Each function here assumes statement() already consumed the keyword token7
-// (TOKEN_FN / TOKEN_IF / TOKEN_TRY / TOKEN_WHILE) that identified it.9
+// (TOKEN_FORGE / TOKEN_GIVEN / TOKEN_ATTEMPT / TOKEN_DURING) that identified it.810
9
-void compile_fn_decl(void) {10
- expect(TOKEN_IDENTIFIER, "expected function name after 'fn'");11
+void compile_forge_decl(void) {12
+ expect(TOKEN_IDENTIFIER, "expected function name after 'forge'");1113
const char *name = previous.start;1214
int name_len = previous.length;1315
expect(TOKEN_LPAREN, "expected '(' after function name");@@ -39,6 +41,9 @@ void compile_fn_decl(void) {3941
char outer_locals[MAX_VM_LOCALS][64];4042
int outer_local_count = local_count;4143
memcpy(outer_locals, local_names, sizeof(local_names));44
+ int outer_loop_depth;45
+ loop_depth_save(&outer_loop_depth);46
+ loop_depth_reset();4247
4348
chunk = &vm_functions[fn_idx].chunk;4449
local_count = 0;@@ -51,79 +56,216 @@ void compile_fn_decl(void) {5156
chunk = outer_chunk;5257
local_count = outer_local_count;5358
memcpy(local_names, outer_locals, sizeof(local_names));59
+ loop_depth_restore(outer_loop_depth);5460
}5561
56
-void compile_if_stmt(void) {57
- expect(TOKEN_LPAREN, "expected '(' after 'if'");58
- int patch_loc = try_fuse_condition();59
- int else_jump;62
+// Conditions may be parenthesized or not (`given (x < n) {` and63
+// `given x < n {` both work) -- try_fuse_condition() needs to know which,64
+// since the fused shape's lookahead has to stop at whatever actually65
+// follows the comparison: `)`, or straight on to the block's opening `{`.66
+static int consume_optional_lparen(void) {67
+ if (current.type != TOKEN_LPAREN) return 0;68
+ advance_token();69
+ return 1;70
+}71
+72
+// After compiling a block that may be re-entered (a loop body) or whose73
+// sibling branch may take a different path (given/otherwise), resets the74
+// runtime stack back to exactly saved_local_count slots if the block75
+// declared any locals of its own, and restores compile-time bookkeeping76
+// to match. This is an ABSOLUTE reset (OP_TRUNC_LOCALS sets77
+// stack_top = frame->slots + n), not a fixed number of OP_POPs -- a jump78
+// that exits the block early (`next`, or one branch of a nested given)79
+// may have declared fewer locals than the block's "straight-line" path80
+// does, so only the known target depth is safe to assume. Without this,81
+// a fresh `local` declared inside a during/each body (or inside a given82
+// nested in one) would push one more stack slot every time the block83
+// re-runs, permanently stuck at slot `saved_local_count` from the first84
+// pass -- found via a cross-engine audit against kiln, which doesn't85
+// have this bug (its locals are fixed rbp-relative memory, not stack86
+// positions, so re-declaring one just overwrites the same address).87
+static void truncate_block_locals(int saved_local_count) {88
+ if (local_count != saved_local_count) {89
+ emit2(OP_TRUNC_LOCALS, (uint8_t)saved_local_count);90
+ }91
+ local_count = saved_local_count;92
+}93
+94
+void compile_given_stmt(void) {95
+ int parenthesized = consume_optional_lparen();96
+ int patch_loc = try_fuse_condition(parenthesized ? TOKEN_RPAREN : TOKEN_LBRACE);97
+ int otherwise_jump;6098
if (patch_loc != -1) {61
- expect(TOKEN_RPAREN, "expected ')' after condition");62
- else_jump = patch_loc;99
+ if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");100
+ otherwise_jump = patch_loc;63101
} else {64102
expression();65
- expect(TOKEN_RPAREN, "expected ')' after condition");66
- else_jump = emit_jump(OP_JUMP_IF_FALSE);103
+ if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");104
+ otherwise_jump = emit_jump(OP_JUMP_IF_FALSE);67105
}106
+ int saved_local_count = local_count;68107
block();69
- if (current.type == TOKEN_ELSE) {108
+ truncate_block_locals(saved_local_count);109
+ if (current.type == TOKEN_OTHERWISE) {70110
int end_jump = emit_jump(OP_JUMP);71
- patch_jump(else_jump);111
+ patch_jump(otherwise_jump);72112
advance_token();73113
block();114
+ truncate_block_locals(saved_local_count);74115
patch_jump(end_jump);75116
} else {76
- patch_jump(else_jump);117
+ patch_jump(otherwise_jump);77118
}78119
}79120
80
-void compile_try_stmt(void) {121
+void compile_attempt_stmt(void) {81122
int saved_local_count = local_count;82123
83
- int try_jump = emit_jump(OP_TRY_PUSH);124
+ int attempt_jump = emit_jump(OP_TRY_PUSH);84125
85126
block();86127
87128
emit_op(OP_TRY_POP);88
- int skip_catch = emit_jump(OP_JUMP);129
+ int skip_handle = emit_jump(OP_JUMP);89130
90
- patch_jump(try_jump);131
+ patch_jump(attempt_jump);91132
92
- expect(TOKEN_CATCH, "expected 'catch' after try block");93
- expect(TOKEN_LPAREN, "expected '(' after 'catch'");133
+ expect(TOKEN_HANDLE, "expected 'handle' after attempt block");134
+ expect(TOKEN_LPAREN, "expected '(' after 'handle'");94135
expect(TOKEN_IDENTIFIER, "expected error variable name");95136
// 'e' must land at EXACTLY saved_local_count, not wherever local_count96
- // ended up after compiling the try block -- since the try block may have97
- // declared its own locals before erroring, but the runtime error-value98
- // push always lands at the try-entry stack position, regardless of what99
- // the try block attempted (all of that gets discarded on the error path).137
+ // ended up after compiling the attempt block -- since the attempt block138
+ // may have declared its own locals before erroring, but the runtime139
+ // error-value push always lands at the attempt-entry stack position,140
+ // regardless of what the attempt block attempted (all of that gets141
+ // discarded on the error path).100142
memcpy(local_names[saved_local_count], previous.start, previous.length);101143
local_names[saved_local_count][previous.length] = '\0';102144
local_count = saved_local_count + 1;103
- expect(TOKEN_RPAREN, "expected ')' after catch variable");145
+ expect(TOKEN_RPAREN, "expected ')' after handle variable");104146
105147
block();106148
emit_op(OP_POP);107149
108
- patch_jump(skip_catch);150
+ patch_jump(skip_handle);109151
110152
local_count = saved_local_count;111153
}112154
113
-void compile_while_stmt(void) {114
- expect(TOKEN_LPAREN, "expected '(' after 'while'");155
+void compile_during_stmt(void) {115156
int loop_start = chunk->count;116
- int patch_loc = try_fuse_condition();157
+ int parenthesized = consume_optional_lparen();158
+ int patch_loc = try_fuse_condition(parenthesized ? TOKEN_RPAREN : TOKEN_LBRACE);117159
int exit_jump;118160
if (patch_loc != -1) {119
- expect(TOKEN_RPAREN, "expected ')' after condition");161
+ if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");120162
exit_jump = patch_loc;121163
} else {122164
expression();123
- expect(TOKEN_RPAREN, "expected ')' after condition");165
+ if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");124166
exit_jump = emit_jump(OP_JUMP_IF_FALSE);125167
}168
+ int saved_local_count = local_count;169
+ loop_push();170
+ block();171
+ loop_patch_nexts(); // next lands here, right before the loop-back172
+ truncate_block_locals(saved_local_count);173
+ emit_loop(loop_start);174
+ patch_jump(exit_jump);175
+ loop_pop_and_patch_stops(); // stop lands here too -- same position as exit_jump176
+}177
+178
+// each (x in arr) { body } -- iterates an array by index, compiling down179
+// to exactly the bytecode a hand-written counting during-loop would produce180
+// (len(), OP_INDEX_GET, and OP_ACC_LOCAL for the index bump, wired together181
+// directly instead of going through expression()/statement() parsing of182
+// synthesized source text). Arrays only, matching map()/filter()/reduce()'s183
+// own array-only scope -- iterate a map via `each (k in keys(m))`.184
+//185
+// The three slots below (the array, the index, and the loop variable) are186
+// declared ONCE, before loop_start, and only ever updated via187
+// OP_SET_LOCAL/OP_ACC_LOCAL inside the repeated region -- never re-declared188
+// per iteration -- so the stack-slot layout stays stable across however189
+// many times the loop body actually runs (compiling a fresh declare_local()190
+// call into code that runs every iteration would desync the compile-time191
+// slot index from the runtime stack position after the first iteration).192
+// local_count is restored afterward (like compile_attempt_stmt does for its193
+// handle-variable) so repeated or nested `each` loops can't run the194
+// 128-slot MAX_VM_LOCALS table dry. The body's OWN locals (declared fresh195
+// inside `block()`, unlike these three setup slots) are handled separately196
+// by truncate_block_locals() every iteration -- see its comment.197
+void compile_each_stmt(void) {198
+ int saved_local_count = local_count;199
+ int parenthesized = consume_optional_lparen();200
+201
+ expect(TOKEN_IDENTIFIER, "expected loop variable name after 'each'");202
+ const char *var_name = previous.start;203
+ int var_len = previous.length;204
+ expect(TOKEN_IN, "expected 'in' after loop variable name");205
+206
+ expression(); // the collection expression; its value becomes the next local slot207
+ int arr_slot = declare_local("__each_arr", 10);208
+209
+ emit_constant(vm_num(0));210
+ int idx_slot = declare_local("__each_idx", 10);211
+212
+ emit_constant(vm_num(0)); // placeholder, overwritten before the body ever reads it213
+ int var_slot = declare_local(var_name, var_len);214
+215
+ if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after 'each' header");216
+217
+ int len_id = vm_builtin_lookup("len", 3);218
+219
+ int loop_start = chunk->count;220
+ emit2(OP_GET_LOCAL, (uint8_t)idx_slot);221
+ emit2(OP_GET_LOCAL, (uint8_t)arr_slot);222
+ emit_op(OP_CALL_BUILTIN);223
+ emit((uint8_t)len_id);224
+ emit((uint8_t)1);225
+ emit_op(OP_LT);226
+ int exit_jump = emit_jump(OP_JUMP_IF_FALSE);227
+228
+ emit2(OP_GET_LOCAL, (uint8_t)arr_slot);229
+ emit2(OP_GET_LOCAL, (uint8_t)idx_slot);230
+ emit_op(OP_INDEX_GET);231
+ emit2(OP_SET_LOCAL, (uint8_t)var_slot);232
+ emit_op(OP_POP);233
+234
+ int body_local_count = local_count; // past arr/idx/var, before the body's own locals235
+ loop_push();126236
block();237
+ loop_patch_nexts(); // next lands here, right before the index bump238
+ truncate_block_locals(body_local_count);239
+240
+ emit_op(OP_ACC_LOCAL);241
+ emit((uint8_t)idx_slot);242
+ emit((uint8_t)0x80); // op 0 ('+'), operand is the constant below243
+ emit((uint8_t)chunk_add_constant(chunk, vm_num(1)));244
+127245
emit_loop(loop_start);128246
patch_jump(exit_jump);247
+ // stop must land at this EXACT bytecode position too (not after the248
+ // cleanup pops below) -- patch_jump() backfills an offset into249
+ // already-written bytes without advancing chunk->count, so this call250
+ // doesn't move "the current position" at all. Patching stop jumps to251
+ // land here means execution falls through into the same cleanup pops252
+ // below regardless of whether the loop ended via its own condition253
+ // going false or via an explicit `stop`.254
+ loop_pop_and_patch_stops();255
+256
+ // local_count rolling back is compile-time bookkeeping only -- it does257
+ // NOT shrink the runtime stack, so the three slots pushed during setup258
+ // (arr, idx, var) are still physically sitting there when the loop259
+ // exits. Without these, a second `each` after this one would compile260
+ // its own slots to the same (now compile-time-reused) indices while261
+ // the runtime stack is still three deeper than that, desyncing every262
+ // GET_LOCAL/SET_LOCAL in it from where its values actually live.263
+ // compile_attempt_stmt's `emit_op(OP_POP);` after its handle-block264
+ // pops exactly this same kind of leftover for its one handle-variable265
+ // slot; this is the three-slot version of the same fix.266
+ emit_op(OP_POP);267
+ emit_op(OP_POP);268
+ emit_op(OP_POP);269
+270
+ local_count = saved_local_count;129271
}ashvm/src/compiler/D/calls.dadded
+15−0@@ -0,0 +1,15 @@1
+src/compiler/O/calls.o: src/compiler/C/calls.c src/compiler/H/internal.h \2
+ src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \3
+ 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.h6
+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/builtins/H/builtins.h:14
+src/diagnostics/H/diagnostics.h:15
+src/compiler/H/loop_stack.h:ashvm/src/compiler/D/constants.dadded
+12−0@@ -0,0 +1,12 @@1
+src/compiler/O/constants.o: src/compiler/C/constants.c \2
+ src/compiler/H/constants.h src/value/H/value.h src/compiler/H/internal.h \3
+ 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.h5
+src/compiler/H/constants.h:6
+src/value/H/value.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/lexer/H/lexer.h:12
+src/diagnostics/H/diagnostics.h:ashvm/src/compiler/D/expr.dadded
+12−0@@ -0,0 +1,12 @@1
+src/compiler/O/expr.o: src/compiler/C/expr.c src/compiler/H/internal.h \2
+ src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \3
+ src/value/H/value.h src/lexer/H/lexer.h src/compiler/H/constants.h \4
+ src/diagnostics/H/diagnostics.h5
+src/compiler/H/internal.h:6
+src/compiler/H/state.h:7
+src/compiler/H/compiler.h:8
+src/vm/H/chunk.h:9
+src/value/H/value.h:10
+src/lexer/H/lexer.h:11
+src/compiler/H/constants.h:12
+src/diagnostics/H/diagnostics.h:ashvm/src/compiler/D/import_paths.dadded
+3−0@@ -0,0 +1,3 @@1
+src/compiler/O/import_paths.o: src/compiler/C/import_paths.c \2
+ src/compiler/H/import_paths.h3
+src/compiler/H/import_paths.h:ashvm/src/compiler/D/loop_stack.dadded
+11−0@@ -0,0 +1,11 @@1
+src/compiler/O/loop_stack.o: src/compiler/C/loop_stack.c \2
+ src/compiler/H/loop_stack.h src/lexer/H/lexer.h src/compiler/H/state.h \3
+ src/compiler/H/compiler.h src/vm/H/chunk.h src/value/H/value.h \4
+ src/diagnostics/H/diagnostics.h5
+src/compiler/H/loop_stack.h:6
+src/lexer/H/lexer.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/diagnostics/H/diagnostics.h:ashvm/src/compiler/D/state.dadded
+9−0@@ -0,0 +1,9 @@1
+src/compiler/O/state.o: src/compiler/C/state.c src/compiler/H/state.h \2
+ src/compiler/H/compiler.h src/vm/H/chunk.h src/value/H/value.h \3
+ src/lexer/H/lexer.h src/diagnostics/H/diagnostics.h4
+src/compiler/H/state.h:5
+src/compiler/H/compiler.h:6
+src/vm/H/chunk.h:7
+src/value/H/value.h:8
+src/lexer/H/lexer.h:9
+src/diagnostics/H/diagnostics.h:ashvm/src/compiler/D/stmt.dadded
+13−0@@ -0,0 +1,13 @@1
+src/compiler/O/stmt.o: src/compiler/C/stmt.c src/compiler/H/internal.h \2
+ src/compiler/H/state.h src/compiler/H/compiler.h src/vm/H/chunk.h \3
+ 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.h5
+src/compiler/H/internal.h:6
+src/compiler/H/state.h:7
+src/compiler/H/compiler.h:8
+src/vm/H/chunk.h:9
+src/value/H/value.h:10
+src/lexer/H/lexer.h:11
+src/compiler/H/constants.h:12
+src/diagnostics/H/diagnostics.h:13
+src/compiler/H/loop_stack.h:ashvm/src/compiler/D/stmt_control.dadded
+16−0@@ -0,0 +1,16 @@1
+src/compiler/O/stmt_control.o: src/compiler/C/stmt_control.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/diagnostics/H/diagnostics.h src/builtins/H/builtins.h \6
+ src/compiler/H/loop_stack.h7
+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/diagnostics/H/diagnostics.h:15
+src/builtins/H/builtins.h:16
+src/compiler/H/loop_stack.h:ashvm/src/compiler/D/stmt_import.dadded
+14−0@@ -0,0 +1,14 @@1
+src/compiler/O/stmt_import.o: src/compiler/C/stmt_import.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/import_paths.h src/diagnostics/H/diagnostics.h6
+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/import_paths.h:14
+src/diagnostics/H/diagnostics.h:ashvm/src/compiler/H/constants.hmodified
+1−1@@ -18,7 +18,7 @@ VMConstant *resolve_constant(const char *name, int len);1818
// would have -- used at every `ns.CONST` use site.1919
void codegen_constant_value(VMConstant *k);2020
21
-// Called at the top of let_statement(), right after the variable name and21
+// Called at the top of local_statement(), right after the variable name and2222
// '=' have been consumed: if an import namespace is currently active,2323
// consumes the RHS (must be a literal number or string), registers it as2424
// a namespaced constant, and consumes the trailing ';'. Returns 1 if itashvm/src/compiler/H/internal.hmodified
+15−10@@ -14,11 +14,15 @@ void emit_call(Token id);1414
void emit_call_named(const char *name, int len);1515
void lambda_literal(void);1616
// Tries to compile `identifier CMP (number|identifier)` directly into a17
-// fused OP_CMP_JUMP, used by if/while conditions. Returns the bytecode18
-// offset to patch_jump() once the branch target is known, or -1 if the19
-// condition didn't match the fusable shape (caller falls back to a plain20
-// expression() + OP_JUMP_IF_FALSE).21
-int try_fuse_condition(void);17
+// fused OP_CMP_JUMP, used by if/while conditions. `terminator` is whatever18
+// token must immediately follow the comparison for the fusion to apply --19
+// TOKEN_RPAREN when the condition was parenthesized, or TOKEN_LBRACE when20
+// it wasn't (conditions may optionally drop the parens, but the block's21
+// opening '{' is always there) -- so fusion keeps firing either way.22
+// Returns the bytecode offset to patch_jump() once the branch target is23
+// known, or -1 if the condition didn't match the fusable shape (caller24
+// falls back to a plain expression() + OP_JUMP_IF_FALSE).25
+int try_fuse_condition(TokenType terminator);2226
2327
// compiler_stmt.c2428
void block(void);@@ -29,13 +33,14 @@ void block(void);2933
// rather than staying static to compiler_stmt.c.3034
void statement(void);3135
32
-// compiler_stmt_control.c -- the four statement kinds involved enough to36
+// compiler_stmt_control.c -- the statement kinds involved enough to3337
// warrant their own function (each assumes its keyword token has already3438
// been consumed by statement()'s dispatch in compiler_stmt.c)35
-void compile_fn_decl(void);36
-void compile_if_stmt(void);37
-void compile_try_stmt(void);38
-void compile_while_stmt(void);39
+void compile_forge_decl(void);40
+void compile_given_stmt(void);41
+void compile_attempt_stmt(void);42
+void compile_during_stmt(void);43
+void compile_each_stmt(void);3944
4045
// compiler_stmt_import.c4146
void compile_import_stmt(void);ashvm/src/compiler/H/loop_stack.hadded
+50−0@@ -0,0 +1,50 @@1
+#ifndef ASH_VM_COMPILER_LOOP_STACK_H2
+#define ASH_VM_COMPILER_LOOP_STACK_H3
+#include "lexer/H/lexer.h"4
+5
+// stop/next need to know which loop they belong to, and loops nest --6
+// they're pure "emit now, patch later" jump bookkeeping (same primitive7
+// OP_JUMP/OP_JUMP_IF_FALSE already use elsewhere in this compiler), just8
+// tracked per-nesting-level at compile time instead of resolved9
+// immediately. Mirrors kiln's own parser/C/statements/loop_stack.c.10
+//11
+// next can't simply backward-jump to a known offset the way a plain loop12
+// back-edge does: `next`'s target (the per-iteration advance step -- an13
+// each-loop's index increment, or straight back to the condition for a14
+// during loop) is only emitted AFTER the body compiles, i.e. after the15
+// next statement itself. So next jumps are forward-patched too, resolved16
+// by the loop's own codegen right after block() returns, before whatever17
+// per-iteration bookkeeping runs next.18
+19
+// Pushes a fresh loop context (call right before compiling the body).20
+void loop_push(void);21
+22
+// Called from stmt.c's stop_statement()/next_statement() -- records a23
+// forward jump's patch offset into the innermost loop's list. `keyword`24
+// anchors the "outside a loop" diagnostic at the stop/next token itself25
+// rather than wherever parsing happens to be next. Exits via26
+// diagnostics_report + exit(1) if called outside any loop.27
+void loop_record_stop(Token keyword, int patch_offset);28
+void loop_record_next(Token keyword, int patch_offset);29
+30
+// Patches every recorded next jump to the CURRENT code position -- call31
+// right after block() returns, before emitting the per-iteration advance32
+// step.33
+void loop_patch_nexts(void);34
+35
+// Patches every recorded stop jump to the CURRENT code position, then36
+// pops the loop context -- call once the loop is fully compiled.37
+void loop_pop_and_patch_stops(void);38
+39
+// Save/reset/restore the loop-nesting depth, for compile_forge_decl and40
+// lambda_literal to wrap around a called body's compilation (same spirit41
+// as local_count's own save-recurse-restore): a function or lambda body42
+// gets its own Chunk at runtime, so a stop/next inside one must never43
+// reach past it into an ENCLOSING loop it happens to be textually written44
+// inside -- jumps can't cross Chunks anyway, so this is what turns that45
+// 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);49
+50
+#endifashvm/src/compiler/H/state.hmodified
+2−2@@ -36,8 +36,8 @@ void get_import_namespace(const char **out_ns, int *out_len);3636
3737
// Declares a NEW vm_functions[] entry under "ns.NAME" if a namespace is3838
// currently set, else under the bare NAME. Returns the new entry's index39
-// (mirrors compile_fn_decl's own prior inline logic, now centralized so40
-// imports.c-equivalent code and compile_fn_decl share it). Exits via39
+// (mirrors compile_forge_decl's own prior inline logic, now centralized so40
+// imports.c-equivalent code and compile_forge_decl share it). Exits via4141
// diagnostics_report + exit(1) on table-full or name-too-long, matching4242
// every other error path in this file.4343
int declare_function_in_context(const char *name, int len);ashvm/src/diagnostics/C/diagnostics.cmodified
+1−1@@ -62,7 +62,7 @@ static int has_prefix(const char *message, const char *prefix) {6262
}6363
6464
static const char *lookup_hint(const char *message) {65
- if (has_prefix(message, "undefined variable")) return "did you forget to declare it with `let`?";65
+ if (has_prefix(message, "undefined variable")) return "did you forget to declare it with `local`?";6666
if (has_prefix(message, "undefined function")) return "check the function name is spelled correctly";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";ashvm/src/diagnostics/D/diagnostics.dadded
+3−0@@ -0,0 +1,3 @@1
+src/diagnostics/O/diagnostics.o: src/diagnostics/C/diagnostics.c \2
+ src/diagnostics/H/diagnostics.h3
+src/diagnostics/H/diagnostics.h:ashvm/src/lexer/C/lexer.cmodified
+46−22@@ -107,17 +107,29 @@ static Token identifier(void) {107107
while (isalnum(peek()) || peek() == '_') advance();108108
109109
int length = (int)(lex_current - lex_start);110
- if (length == 5 && strncmp(lex_start, "print", 5) == 0) return make_token(TOKEN_PRINT);111
- if (length == 3 && strncmp(lex_start, "let", 3) == 0) return make_token(TOKEN_LET);112
- if (length == 2 && strncmp(lex_start, "if", 2) == 0) return make_token(TOKEN_IF);113
- if (length == 4 && strncmp(lex_start, "else", 4) == 0) return make_token(TOKEN_ELSE);114
- if (length == 5 && strncmp(lex_start, "while", 5) == 0) return make_token(TOKEN_WHILE);115
- if (length == 3 && strncmp(lex_start, "for", 3) == 0) return make_token(TOKEN_FOR);110
+ if (length == 3 && strncmp(lex_start, "say", 3) == 0) return make_token(TOKEN_SAY);111
+ if (length == 5 && strncmp(lex_start, "local", 5) == 0) return make_token(TOKEN_LOCAL);112
+ if (length == 5 && strncmp(lex_start, "given", 5) == 0) return make_token(TOKEN_GIVEN);113
+ if (length == 9 && strncmp(lex_start, "otherwise", 9) == 0) return make_token(TOKEN_OTHERWISE);114
+ if (length == 6 && strncmp(lex_start, "during", 6) == 0) return make_token(TOKEN_DURING);115
+ if (length == 4 && strncmp(lex_start, "each", 4) == 0) return make_token(TOKEN_EACH);116116
if (length == 2 && strncmp(lex_start, "in", 2) == 0) return make_token(TOKEN_IN);117
- if (length == 2 && strncmp(lex_start, "fn", 2) == 0) return make_token(TOKEN_FN);118
- if (length == 6 && strncmp(lex_start, "return", 6) == 0) return make_token(TOKEN_RETURN);119
- if (length == 3 && strncmp(lex_start, "try", 3) == 0) return make_token(TOKEN_TRY);120
- if (length == 5 && strncmp(lex_start, "catch", 5) == 0) return make_token(TOKEN_CATCH);117
+ if (length == 5 && strncmp(lex_start, "forge", 5) == 0) return make_token(TOKEN_FORGE);118
+ if (length == 5 && strncmp(lex_start, "yield", 5) == 0) return make_token(TOKEN_YIELD);119
+ if (length == 7 && strncmp(lex_start, "attempt", 7) == 0) return make_token(TOKEN_ATTEMPT);120
+ if (length == 6 && strncmp(lex_start, "handle", 6) == 0) return make_token(TOKEN_HANDLE);121
+ if (length == 5 && strncmp(lex_start, "raise", 5) == 0) return make_token(TOKEN_RAISE);122
+ if (length == 4 && strncmp(lex_start, "stop", 4) == 0) return make_token(TOKEN_STOP);123
+ if (length == 4 && strncmp(lex_start, "next", 4) == 0) return make_token(TOKEN_NEXT);124
+ if (length == 3 && strncmp(lex_start, "yes", 3) == 0) return make_token(TOKEN_YES);125
+ if (length == 2 && strncmp(lex_start, "no", 2) == 0) return make_token(TOKEN_NO);126
+ if (length == 4 && strncmp(lex_start, "none", 4) == 0) return make_token(TOKEN_NONE);127
+ if (length == 3 && strncmp(lex_start, "and", 3) == 0) return make_token(TOKEN_AND);128
+ if (length == 2 && strncmp(lex_start, "or", 2) == 0) return make_token(TOKEN_OR);129
+ // "not" reuses TOKEN_BANG (unary logical negation) -- same meaning as130
+ // the symbol it replaces, so unary()'s existing TOKEN_BANG handling in131
+ // expr.c needs no change at all.132
+ if (length == 3 && strncmp(lex_start, "not", 3) == 0) return make_token(TOKEN_BANG);121133
122134
return make_token(TOKEN_IDENTIFIER);123135
}@@ -145,27 +157,39 @@ Token lexer_next_token(void) {145157
case ';': return make_token(TOKEN_SEMICOLON);146158
case ',': return make_token(TOKEN_COMMA);147159
case ':': return make_token(TOKEN_COLON);148
- case '+': return make_token(TOKEN_PLUS);149
- case '-': return make_token(TOKEN_MINUS);150
- case '*': return make_token(TOKEN_STAR);151
- case '/': return make_token(TOKEN_SLASH);160
+ case '?': return make_token(TOKEN_QUESTION);161
+ case '+':162
+ if (match('+')) return make_token(TOKEN_PLUS_PLUS);163
+ return make_token(match('=') ? TOKEN_PLUS_EQUAL : TOKEN_PLUS);164
+ case '-':165
+ if (match('-')) return make_token(TOKEN_MINUS_MINUS);166
+ return make_token(match('=') ? TOKEN_MINUS_EQUAL : TOKEN_MINUS);167
+ case '*': return make_token(match('=') ? TOKEN_STAR_EQUAL : TOKEN_STAR);168
+ case '/': return make_token(match('=') ? TOKEN_SLASH_EQUAL : TOKEN_SLASH);152169
case '%': return make_token(TOKEN_PERCENT);170
+ case '&': return make_token(TOKEN_AMP);171
+ case '|': return make_token(TOKEN_PIPE);172
+ case '^': return make_token(TOKEN_CARET);173
+ case '~': return make_token(TOKEN_TILDE);153174
case '(': return make_token(TOKEN_LPAREN);154175
case ')': return make_token(TOKEN_RPAREN);155176
case '{': return make_token(TOKEN_LBRACE);156177
case '}': return make_token(TOKEN_RBRACE);157178
case '[': return make_token(TOKEN_LBRACKET);158179
case ']': return make_token(TOKEN_RBRACKET);159180
case '=': return make_token(match('=') ? TOKEN_EQUAL_EQUAL : TOKEN_EQUAL);160
- case '!': return make_token(match('=') ? TOKEN_BANG_EQUAL : TOKEN_BANG);161
- case '<': return make_token(match('=') ? TOKEN_LESS_EQUAL : TOKEN_LESS);162
- case '>': return make_token(match('=') ? TOKEN_GREATER_EQUAL : TOKEN_GREATER);163
- case '&':164
- if (match('&')) return make_token(TOKEN_AND);165
- return error_token("unexpected character");166
- case '|':167
- if (match('|')) return make_token(TOKEN_OR);181
+ // Bare '!' is no longer valid on its own -- logical negation is the182
+ // 'not' keyword now (see identifier()). '!=' (inequality) stays183
+ // symbolic either way.184
+ case '!':185
+ if (match('=')) return make_token(TOKEN_BANG_EQUAL);168186
return error_token("unexpected character");187
+ case '<':188
+ if (match('<')) return make_token(TOKEN_SHL);189
+ return make_token(match('=') ? TOKEN_LESS_EQUAL : TOKEN_LESS);190
+ case '>':191
+ if (match('>')) return make_token(TOKEN_SHR);192
+ return make_token(match('=') ? TOKEN_GREATER_EQUAL : TOKEN_GREATER);169193
}170194
171195
return error_token("unexpected character");ashvm/src/lexer/D/lexer.dadded
+4−0@@ -0,0 +1,4 @@1
+src/lexer/O/lexer.o: src/lexer/C/lexer.c src/lexer/H/lexer.h \2
+ src/lexer/H/lexer_import.h3
+src/lexer/H/lexer.h:4
+src/lexer/H/lexer_import.h:ashvm/src/lexer/D/lexer_import.dadded
+4−0@@ -0,0 +1,4 @@1
+src/lexer/O/lexer_import.o: src/lexer/C/lexer_import.c \2
+ src/lexer/H/lexer_import.h src/lexer/H/lexer.h3
+src/lexer/H/lexer_import.h:4
+src/lexer/H/lexer.h:ashvm/src/lexer/H/lexer.hmodified
+29−10@@ -2,18 +2,24 @@22
#define LEXER_H33
44
typedef enum {5
- TOKEN_PRINT,6
- TOKEN_LET,7
- TOKEN_IF,8
- TOKEN_ELSE,9
- TOKEN_WHILE,10
- TOKEN_FOR,5
+ TOKEN_SAY,6
+ TOKEN_LOCAL,7
+ TOKEN_GIVEN,8
+ TOKEN_OTHERWISE,9
+ TOKEN_DURING,10
+ TOKEN_EACH,1111
TOKEN_IN,1212
TOKEN_IMPORT,13
- TOKEN_FN,14
- TOKEN_RETURN,15
- TOKEN_TRY,16
- TOKEN_CATCH,13
+ TOKEN_FORGE,14
+ TOKEN_YIELD,15
+ TOKEN_ATTEMPT,16
+ TOKEN_HANDLE,17
+ TOKEN_RAISE,18
+ TOKEN_STOP,19
+ TOKEN_NEXT,20
+ TOKEN_YES,21
+ TOKEN_NO,22
+ TOKEN_NONE,1723
TOKEN_IDENTIFIER,1824
TOKEN_STRING,1925
TOKEN_IMPORT_PATH,@@ -41,6 +47,19 @@ typedef enum {4147
TOKEN_COMMA,4248
TOKEN_COLON,4349
TOKEN_DOT,50
+ TOKEN_QUESTION,51
+ TOKEN_AMP,52
+ TOKEN_PIPE,53
+ TOKEN_CARET,54
+ TOKEN_TILDE,55
+ TOKEN_SHL,56
+ TOKEN_SHR,57
+ TOKEN_PLUS_EQUAL,58
+ TOKEN_MINUS_EQUAL,59
+ TOKEN_STAR_EQUAL,60
+ TOKEN_SLASH_EQUAL,61
+ TOKEN_PLUS_PLUS,62
+ TOKEN_MINUS_MINUS,4463
TOKEN_NUMBER,4564
TOKEN_SEMICOLON,4665
TOKEN_EOF,ashvm/src/value/C/value.cmodified
+5−0@@ -7,6 +7,9 @@77
VMValue vm_num(double n) {88
VMValue v; v.type = VM_NUM; v.number = n; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = NULL; return v;99
}10
+VMValue vm_nil(void) {11
+ VMValue v; v.type = VM_NIL; v.number = 0; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = NULL; return v;12
+}1013
VMValue vm_str(char *s) {1114
VMValue v; v.type = VM_STR; v.number = 0; v.str = s; v.array = NULL; v.map = NULL; v.closure = NULL; return v;1215
}@@ -80,6 +83,8 @@ void vm_print_value(VMValue v) {8083
else printf("%g", v.number);8184
} else if (v.type == VM_STR) {8285
printf("%s", v.str);86
+ } else if (v.type == VM_NIL) {87
+ printf("none");8388
} else if (v.type == VM_FUNCTION) {8489
printf("<function>");8590
} else if (v.type == VM_CLOSURE) {ashvm/src/value/D/hashmap.dadded
+4−0@@ -0,0 +1,4 @@1
+src/value/O/hashmap.o: src/value/C/hashmap.c src/value/H/hashmap.h \2
+ src/value/H/value.h3
+src/value/H/hashmap.h:4
+src/value/H/value.h:ashvm/src/value/D/value.dadded
+4−0@@ -0,0 +1,4 @@1
+src/value/O/value.o: src/value/C/value.c src/value/H/value.h \2
+ src/value/H/hashmap.h3
+src/value/H/value.h:4
+src/value/H/hashmap.h:ashvm/src/value/H/value.hmodified
+2−1@@ -2,7 +2,7 @@22
#define ASH_VM_VALUE_H33
#include <stdint.h>44
5
-typedef enum { VM_NUM, VM_STR, VM_ARRAY, VM_MAP, VM_FUNCTION, VM_CLOSURE } VMType;5
+typedef enum { VM_NUM, VM_STR, VM_ARRAY, VM_MAP, VM_FUNCTION, VM_CLOSURE, VM_NIL } VMType;66
77
typedef struct VMArray {88
struct VMValue *items;@@ -28,6 +28,7 @@ typedef struct VMValue {2828
} VMValue;2929
3030
VMValue vm_num(double n);31
+VMValue vm_nil(void);3132
VMValue vm_str(char *s);3233
VMValue vm_array_val(VMArray *a);3334
VMValue vm_map_val(VMMap *m);ashvm/src/vm/C/dispatch.cmodified
+38−4@@ -31,7 +31,10 @@ void vm_execute(int stop_at_frame_count) {3131
&&do_ARRAY, &&do_INDEX_GET, &&do_INDEX_SET,3232
&&do_MAP, &&do_CALL_VALUE, &&do_CALL_BUILTIN,3333
&&do_MAKE_CLOSURE,34
- &&do_TRY_PUSH, &&do_TRY_POP34
+ &&do_TRY_PUSH, &&do_TRY_POP,35
+ &&do_RAISE,36
+ &&do_BAND, &&do_BOR, &&do_BXOR, &&do_BNOT, &&do_SHL, &&do_SHR,37
+ &&do_TRUNC_LOCALS3538
};3639
3740
#define DISPATCH() \@@ -64,6 +67,7 @@ void vm_execute(int stop_at_frame_count) {6467
int eq;6568
if (a->type == VM_STR && b.type == VM_STR) eq = strcmp(a->str, b.str) == 0;6669
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;6771
else eq = 0;6872
*a = vm_num(eq);6973
DISPATCH();@@ -73,6 +77,7 @@ void vm_execute(int stop_at_frame_count) {7377
int eq;7478
if (a->type == VM_STR && b.type == VM_STR) eq = strcmp(a->str, b.str) == 0;7579
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;7681
else eq = 0;7782
*a = vm_num(!eq);7883
DISPATCH();@@ -183,6 +188,7 @@ void vm_execute(int stop_at_frame_count) {183188
int eq;184189
if (a.type == VM_STR && b.type == VM_STR) eq = strcmp(a.str, b.str) == 0;185190
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;186192
else eq = 0;187193
taken = (cmp_code == 4) ? eq : !eq;188194
} else {@@ -282,14 +288,42 @@ void vm_execute(int stop_at_frame_count) {282288
stack_top = h->saved_stack_top;283289
frame = h->target_frame;284290
frame->ip = h->target_ip;285
- char *msg_copy = malloc(strlen(error_message) + 1);286
- strcpy(msg_copy, error_message);287
- *stack_top++ = vm_str(msg_copy);291
+ if (has_raised_value) {292
+ *stack_top++ = raised_value;293
+ has_raised_value = 0;294
+ } else {295
+ char *msg_copy = malloc(strlen(error_message) + 1);296
+ strcpy(msg_copy, error_message);297
+ *stack_top++ = vm_str(msg_copy);298
+ }288299
DISPATCH();289300
}290301
}291302
do_TRY_POP: {292303
try_depth--;293304
DISPATCH();294305
}306
+ do_RAISE: {307
+ VMValue msg = *--stack_top;308
+ vm_raise_value(msg); // noreturn -- longjmps to a handler, or exits309
+ }310
+ // Bitwise ops truncate to int64 (matching kiln's cvttsd2si approach)311
+ // then convert back -- same "assume NUMBER" scope limit as -, *, /,312
+ // and the comparison operators.313
+ do_BAND: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((double)((int64_t)require_num(*a, "'&'") & (int64_t)require_num(b, "'&'"))); DISPATCH(); }314
+ do_BOR: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((double)((int64_t)require_num(*a, "'|'") | (int64_t)require_num(b, "'|'"))); DISPATCH(); }315
+ do_BXOR: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((double)((int64_t)require_num(*a, "'^'") ^ (int64_t)require_num(b, "'^'"))); DISPATCH(); }316
+ do_BNOT: { stack_top[-1] = vm_num((double)(~(int64_t)require_num(stack_top[-1], "'~'"))); DISPATCH(); }317
+ do_SHL: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((double)((int64_t)require_num(*a, "'<<'") << (int64_t)require_num(b, "'<<'"))); DISPATCH(); }318
+ do_SHR: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((double)((int64_t)require_num(*a, "'>>'") >> (int64_t)require_num(b, "'>>'"))); DISPATCH(); }319
+ // Resets the stack to exactly `frame->slots + n` -- an absolute reset,320
+ // not N pops. A re-executed block (loop body, given/otherwise branch)321
+ // may declare a variable number of fresh locals depending on which322
+ // internal path it took (early `next`, nested given, ...), so "pop323
+ // however many were declared on a typical pass" can under- or324
+ // over-pop. Since every local is a stack slot and slot layout is325
+ // static per chunk, "what the stack should look like once this block326
+ // is done" is just "n slots past frame->slots" -- always correct327
+ // regardless of the path taken through the block this time.328
+ do_TRUNC_LOCALS: { uint8_t n = *frame->ip++; stack_top = frame->slots + n; DISPATCH(); }295329
}ashvm/src/vm/C/errors.cmodified
+32−2@@ -7,9 +7,10 @@77
88
// Every genuinely-runtime error (as opposed to a compile-time/structural one,99
// which stays a hard crash since ashvm resolves names at compile time) funnels10
-// through here. If a try block is active, longjmp back to its handler instead11
-// of crashing the whole program.10
+// through here. If an attempt block is active, longjmp back to its handler11
+// instead of crashing the whole program.1212
void vm_runtime_error(const char *fmt, ...) {13
+ has_raised_value = 0; // this error carries a formatted C string, not a raised VMValue1314
va_list args;1415
va_start(args, fmt);1516
vsnprintf(error_message, sizeof(error_message), fmt, args);@@ -28,6 +29,31 @@ void vm_runtime_error(const char *fmt, ...) {2829
}2930
}3031
32
+// `raise expr;` -- expr must already be a VM_STR (checked here, not at33
+// compile time, since the raised value is a general expression that could34
+// be anything at runtime). Mirrors vm_runtime_error's three-way dispatch35
+// (attempt handler / REPL recovery / hard exit) exactly, but hands back36
+// the exact raised VMValue via `raised_value` instead of funneling it37
+// through the fixed-size error_message buffer -- see state.h.38
+void vm_raise_value(VMValue msg) {39
+ if (msg.type != VM_STR) type_error("'raise'");40
+41
+ raised_value = msg;42
+ has_raised_value = 1;43
+44
+ if (try_depth > 0) {45
+ longjmp(try_handlers[try_depth - 1].jmp, 1);46
+ } else if (repl_recovery_jmp != NULL) {47
+ diagnostics_report("error", msg.str, current_pos, current_poslen);48
+ frame_count = repl_saved_frame_count;49
+ stack_top = repl_saved_stack_top;50
+ longjmp(*repl_recovery_jmp, 1);51
+ } else {52
+ diagnostics_report("error", msg.str, current_pos, current_poslen);53
+ exit(1);54
+ }55
+}56
+3157
void type_error(const char *context) {3258
vm_runtime_error("type error: invalid operand type in %s", context);3359
}@@ -38,5 +64,9 @@ double require_num(VMValue v, const char *context) {3864
}3965
4066
int truthy(VMValue v, const char *context) {67
+ // none is always falsy -- checked here rather than erroring like every68
+ // other non-number would, matching kiln's own nil-is-always-falsy rule69
+ // (its payload is always exactly 0).70
+ if (v.type == VM_NIL) return 0;4171
return require_num(v, context) != 0;4272
}ashvm/src/vm/C/state.cmodified
+3−0@@ -10,6 +10,9 @@ TryHandler try_handlers[MAX_TRY_DEPTH];1010
int try_depth = 0;1111
char error_message[512];1212
13
+VMValue raised_value;14
+int has_raised_value = 0;15
+1316
jmp_buf *repl_recovery_jmp = NULL;1417
int repl_saved_frame_count;1518
VMValue *repl_saved_stack_top;ashvm/src/vm/D/api.dadded
+7−0@@ -0,0 +1,7 @@1
+src/vm/O/api.o: src/vm/C/api.c src/vm/H/vm.h src/vm/H/chunk.h \2
+ src/value/H/value.h src/vm/H/state.h src/compiler/H/compiler.h3
+src/vm/H/vm.h:4
+src/vm/H/chunk.h:5
+src/value/H/value.h:6
+src/vm/H/state.h:7
+src/compiler/H/compiler.h:ashvm/src/vm/D/chunk.dadded
+3−0@@ -0,0 +1,3 @@1
+src/vm/O/chunk.o: src/vm/C/chunk.c src/vm/H/chunk.h src/value/H/value.h2
+src/vm/H/chunk.h:3
+src/value/H/value.h:ashvm/src/vm/D/dispatch.dadded
+10−0@@ -0,0 +1,10 @@1
+src/vm/O/dispatch.o: src/vm/C/dispatch.c src/vm/H/vm.h src/vm/H/chunk.h \2
+ src/value/H/value.h src/vm/H/state.h src/compiler/H/compiler.h \3
+ src/builtins/H/builtins.h src/value/H/hashmap.h4
+src/vm/H/vm.h:5
+src/vm/H/chunk.h:6
+src/value/H/value.h:7
+src/vm/H/state.h:8
+src/compiler/H/compiler.h:9
+src/builtins/H/builtins.h:10
+src/value/H/hashmap.h:ashvm/src/vm/D/errors.dadded
+7−0@@ -0,0 +1,7 @@1
+src/vm/O/errors.o: src/vm/C/errors.c src/vm/H/vm.h src/vm/H/chunk.h \2
+ src/value/H/value.h src/vm/H/state.h src/diagnostics/H/diagnostics.h3
+src/vm/H/vm.h:4
+src/vm/H/chunk.h:5
+src/value/H/value.h:6
+src/vm/H/state.h:7
+src/diagnostics/H/diagnostics.h:ashvm/src/vm/D/state.dadded
+5−0@@ -0,0 +1,5 @@1
+src/vm/O/state.o: src/vm/C/state.c src/vm/H/state.h src/vm/H/chunk.h \2
+ src/value/H/value.h3
+src/vm/H/state.h:4
+src/vm/H/chunk.h:5
+src/value/H/value.h:ashvm/src/vm/H/chunk.hmodified
+4−1@@ -22,7 +22,10 @@ typedef enum {2222
OP_CALL_BUILTIN,2323
OP_MAKE_CLOSURE,2424
OP_TRY_PUSH,25
- OP_TRY_POP25
+ OP_TRY_POP,26
+ OP_RAISE,27
+ OP_BAND, OP_BOR, OP_BXOR, OP_BNOT, OP_SHL, OP_SHR,28
+ OP_TRUNC_LOCALS2629
} OpCode;2730
2831
typedef struct {ashvm/src/vm/H/state.hmodified
+8−0@@ -35,6 +35,14 @@ extern TryHandler try_handlers[MAX_TRY_DEPTH];3535
extern int try_depth;3636
extern char error_message[512];3737
38
+// Set by vm_raise_value() right before it longjmps, consumed (and cleared)39
+// by do_TRY_PUSH's recovery branch in dispatch.c -- lets a caught `raise`40
+// hand back the exact VMValue the program raised instead of being forced41
+// through the fixed-size error_message C-string buffer every other runtime42
+// error uses.43
+extern VMValue raised_value;44
+extern int has_raised_value;45
+3846
extern jmp_buf *repl_recovery_jmp;3947
extern int repl_saved_frame_count;4048
extern VMValue *repl_saved_stack_top;ashvm/src/vm/H/vm.hmodified
+6−0@@ -12,5 +12,11 @@ VMValue vm_call_function_sync(int func_idx, VMValue *args, int argc);1212
VMValue vm_call_closure_sync(VMClosureObj *c, VMValue *args, int argc);1313
VMValue vm_call_callable_sync(VMValue callable, VMValue *args, int argc);1414
void vm_runtime_error(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));15
+// Like vm_runtime_error, but for a RUNTIME string value (`raise expr;`)16
+// instead of a compile-time format string -- msg must be VM_STR. Unlike17
+// vm_runtime_error, the caught value is the exact string the program18
+// raised, not a copy funneled through the fixed-size error_message buffer,19
+// so an arbitrarily long raised message survives intact.20
+void vm_raise_value(VMValue msg) __attribute__((noreturn));1521
1622
#endifashvm/tests/cases/basics/bare_reassign_test.ashadded
+10−0@@ -0,0 +1,10 @@1
+forge f(i) {2
+ i = i + 1;3
+ i = i * 2;4
+ yield i;5
+}6
+say f(5);7
+8
+local x = 1;9
+x = x + 10;10
+say x;ashvm/tests/cases/basics/literals_test.ashadded
+29−0@@ -0,0 +1,29 @@1
+say yes;2
+say no;3
+say none;4
+5
+say yes == 1;6
+say no == 0;7
+say none == none;8
+say none == 0;9
+say none == no;10
+11
+given yes {12
+ say "yes is truthy";13
+}14
+given no {15
+ say "unreachable";16
+} otherwise {17
+ say "no is falsy";18
+}19
+local x = none;20
+given x {21
+ say "unreachable";22
+} otherwise {23
+ say "none is falsy";24
+}25
+26
+local arr = [1, none, yes, no];27
+say arr;28
+local m = {"a": none, "b": yes};29
+say m;ashvm/tests/cases/basics/modulo.ashadded
+7−0@@ -0,0 +1,7 @@1
+say 7 % 3;2
+say -7 % 3;3
+say 7 % -3;4
+say -7 % -3;5
+say 7.5 % 2;6
+say 10 % 2 == 0;7
+say 10 % 3 == 1;ashvm/tests/cases/basics/operators_test.ashadded
+31−0@@ -0,0 +1,31 @@1
+local x = 10;2
+x += 5;3
+say x;4
+x -= 3;5
+say x;6
+x *= 2;7
+say x;8
+x /= 4;9
+say x;10
+11
+local s = "hello";12
+s += " world";13
+say s;14
+15
+local i = 0;16
+i++;17
+i++;18
+i--;19
+say i;20
+21
+local n = 7;22
+say n > 5 ? "big" : "small";23
+say n < 5 ? "big" : n < 10 ? "medium" : "huge";24
+25
+say 5 & 3;26
+say 5 | 2;27
+say 5 ^ 1;28
+say ~5;29
+say 1 << 4;30
+say 256 >> 4;31
+say (5 & 3) == 1;ashvm/tests/cases/basics/type_test.ashadded
+8−0@@ -0,0 +1,8 @@1
+say type(5);2
+say type("hi");3
+say type([1, 2]);4
+say type({"a": 1});5
+say type(none);6
+say type(yes);7
+forge f() { yield 1; }8
+say type(f);ashvm/tests/cases/collections/maps_test.ashmodified
+14−14@@ -1,22 +1,22 @@1
-let person = {"name": "Lennox", "age": 20};2
-print person;3
-print person["name"];4
-print person["age"];1
+local person = {"name": "Lennox", "age": 20};2
+say person;3
+say person["name"];4
+say person["age"];55
66
person["age"] = 21;7
-print person["age"];7
+say person["age"];88
99
person["city"] = "Luebeck";10
-print person;10
+say person;1111
12
-print keys(person);13
-print values(person);14
-print has(person, "name");15
-print has(person, "zip");12
+say keys(person);13
+say values(person);14
+say has(person, "name");15
+say has(person, "zip");1616
1717
delete(person, "city");18
-print person;18
+say person;1919
20
-let empty = {};21
-print empty;22
-print len(person);20
+local empty = {};21
+say empty;22
+say len(person);ashvm/tests/cases/collections/vm_arrays_test.ashmodified
+16−16@@ -1,23 +1,23 @@1
-let nums = [1, 2, 3, 4, 5];2
-print nums;3
-print nums[0];4
-print nums[4];1
+local nums = [1, 2, 3, 4, 5];2
+say nums;3
+say nums[0];4
+say nums[4];55
66
nums[0] = 99;7
-print nums;7
+say nums;88
9
-let sum = 0;10
-let i = 0;11
-while (i < 5) {12
- let sum = sum + nums[i];13
- let i = i + 1;9
+local sum = 0;10
+local i = 0;11
+during (i < 5) {12
+ local sum = sum + nums[i];13
+ local i = i + 1;1414
}15
-print sum;15
+say sum;1616
17
-fn first(arr) {18
- return arr[0];17
+forge first(arr) {18
+ yield arr[0];1919
}20
-print first(nums);20
+say first(nums);2121
22
-let names = ["Ash", "Sonnet"];23
-print names[1];22
+local names = ["Ash", "Sonnet"];23
+say names[1];ashvm/tests/cases/control_flow/each_test.ashadded
+45−0@@ -0,0 +1,45 @@1
+local nums = [10, 20, 30, 40, 50];2
+3
+each x in nums {4
+ say x;5
+}6
+7
+each (y in nums) {8
+ say y * 2;9
+}10
+11
+local m = {"a": 1, "b": 2, "c": 3};12
+each k in keys(m) {13
+ say k + " = " + str(m[k]);14
+}15
+16
+local total = 0;17
+each n in nums {18
+ local total = total + n;19
+}20
+say total;21
+22
+local empty = [];23
+local empty_count = 0;24
+each e in empty {25
+ local empty_count = empty_count + 1;26
+}27
+say empty_count;28
+29
+local rows = [[1, 2], [3, 4], [5, 6]];30
+local line = "";31
+each row in rows {32
+ local line = "";33
+ each cell in row {34
+ local line = line + str(cell) + " ";35
+ }36
+ say line;37
+}38
+39
+local outer_total = 0;40
+each a in [1, 2, 3] {41
+ each b in [10, 20] {42
+ local outer_total = outer_total + a * b;43
+ }44
+}45
+say outer_total;ashvm/tests/cases/control_flow/loop_body_locals_test.ashadded
+42−0@@ -0,0 +1,42 @@1
+// A fresh `local` declared inside a re-executed block must not leak across2
+// iterations/branches -- found via a cross-engine audit against kiln.3
+4
+local i = 0;5
+during i < 3 {6
+ local x = i * 10;7
+ say x;8
+ i += 1;9
+}10
+11
+local arr = [1, 2, 3];12
+each n in arr {13
+ local y = n * 10;14
+ say y;15
+}16
+17
+local j = 0;18
+during j < 6 {19
+ given j % 2 == 0 {20
+ local a = j * 100;21
+ say a;22
+ } otherwise {23
+ j += 1;24
+ next;25
+ }26
+ j += 1;27
+}28
+29
+each x in arr {30
+ given x == 2 { next; }31
+ local z = x * 1000;32
+ say z;33
+}34
+35
+each outer in [1, 2] {36
+ local tmp = outer;37
+ each inner in [10, 20] {38
+ local tmp2 = outer + inner;39
+ say tmp2;40
+ }41
+ say tmp;42
+}ashvm/tests/cases/control_flow/loop_control_test.ashadded
+41−0@@ -0,0 +1,41 @@1
+local total = 0;2
+local j = 0;3
+during j < 10 {4
+ j += 1;5
+ given j == 3 {6
+ next;7
+ }8
+ given j == 7 {9
+ stop;10
+ }11
+ total += j;12
+}13
+say total;14
+say j;15
+16
+local sum = 0;17
+each v in [1, 2, 3, 4, 5, 6, 7, 8] {18
+ given v == 2 {19
+ next;20
+ }21
+ given v == 6 {22
+ stop;23
+ }24
+ sum += v;25
+}26
+say sum;27
+28
+// stop only breaks the innermost loop29
+local results = [];30
+each a in [1, 2, 3] {31
+ each b in [10, 20, 30] {32
+ given b == 20 {33
+ next;34
+ }35
+ given a == 3 {36
+ stop;37
+ }38
+ local results = push(results, a * b);39
+ }40
+}41
+say results;ashvm/tests/cases/core/vm_full_test.ashmodified
+30−30@@ -1,39 +1,39 @@1
-let person = {"name": "Ash", "version": 2};2
-print person;3
-print person["name"];1
+local person = {"name": "Ash", "version": 2};2
+say person;3
+say person["name"];44
person["version"] = 3;5
-print person;6
-print keys(person);7
-print values(person);8
-print has(person, "name");5
+say person;6
+say keys(person);7
+say values(person);8
+say has(person, "name");99
delete(person, "version");10
-print person;10
+say person;1111
12
-fn double(x) { return x * 2; }13
-let f = double;14
-print f(21);12
+forge double(x) { yield x * 2; }13
+local f = double;14
+say f(21);1515
16
-let nums = [1, 2, 3, 4, 5];17
-let doubled = map(nums, double);18
-print doubled;16
+local nums = [1, 2, 3, 4, 5];17
+local doubled = map(nums, double);18
+say doubled;1919
20
-fn isEven(x) { return x % 2 == 0; }21
-let evens = filter(nums, isEven);22
-print evens;20
+forge isEven(x) { yield x % 2 == 0; }21
+local evens = filter(nums, isEven);22
+say evens;2323
24
-fn add(a, b) { return a + b; }25
-let total = reduce(nums, add, 0);26
-print total;24
+forge add(a, b) { yield a + b; }25
+local total = reduce(nums, add, 0);26
+say total;2727
28
-print upper("hello");29
-print lower("WORLD");30
-print trim(" spaced ");31
-print split("a,b,c", ",");32
-print join(["x", "y", "z"], "-");33
-print substring("hello world", 0, 5);34
-print indexOf("hello world", "world");35
-print replace("foo bar foo", "foo", "baz");28
+say upper("hello");29
+say lower("WORLD");30
+say trim(" spaced ");31
+say split("a,b,c", ",");32
+say join(["x", "y", "z"], "-");33
+say substring("hello world", 0, 5);34
+say indexOf("hello world", "world");35
+say replace("foo bar foo", "foo", "baz");3636
3737
write_file("/tmp/ashvm_test.txt", "vm file test\n");38
-print file_exists("/tmp/ashvm_test.txt");39
-print read_file("/tmp/ashvm_test.txt");38
+say file_exists("/tmp/ashvm_test.txt");39
+say read_file("/tmp/ashvm_test.txt");ashvm/tests/cases/errors/error_handling_test.ashmodified
+41−41@@ -1,57 +1,57 @@1
-fn risky(n) {2
- if (n > 3) {3
- let undefined_thing = does_not_exist + 1;1
+forge risky(n) {2
+ given (n > 3) {3
+ local undefined_thing = does_not_exist + 1;44
}5
- return n;5
+ yield n;66
}77
8
-let result = 0;9
-try {10
- let result = risky(5);11
- print "should not reach here";12
-} catch (e) {13
- print "caught: " + e;8
+local result = 0;9
+attempt {10
+ local result = risky(5);11
+ say "should not reach here";12
+} handle (e) {13
+ say "caught: " + e;1414
}15
-print "after try 1, result still: " + str(result);15
+say "after attempt 1, result still: " + str(result);1616
17
-let arr = [1, 2, 3];18
-try {19
- print arr[10];20
-} catch (e) {21
- print "caught: " + e;17
+local arr = [1, 2, 3];18
+attempt {19
+ say arr[10];20
+} handle (e) {21
+ say "caught: " + e;2222
}2323
24
-let m = {"a": 1};25
-try {26
- print m["missing_key"];27
-} catch (e) {28
- print "caught: " + e;24
+local m = {"a": 1};25
+attempt {26
+ say m["missing_key"];27
+} handle (e) {28
+ say "caught: " + e;2929
}3030
31
-fn needs_two(a, b) { return a + b; }32
-try {33
- print needs_two(1);34
-} catch (e) {35
- print "caught: " + e;31
+forge needs_two(a, b) { yield a + b; }32
+attempt {33
+ say needs_two(1);34
+} handle (e) {35
+ say "caught: " + e;3636
}3737
38
-try {39
- let content = read_file("/tmp/does_not_exist_xyz_ash.txt");40
-} catch (e) {41
- print "caught: " + e;38
+attempt {39
+ local content = read_file("/tmp/does_not_exist_xyz_ash.txt");40
+} handle (e) {41
+ say "caught: " + e;4242
}4343
44
-try {45
- try {46
- let x = totally_undefined_var;47
- } catch (inner) {48
- print "inner caught: " + inner;44
+attempt {45
+ attempt {46
+ local x = totally_undefined_var;47
+ } handle (inner) {48
+ say "inner caught: " + inner;4949
}50
- print "inner try/catch resolved, continuing outer try";51
-} catch (outer) {52
- print "outer should not see this: " + outer;50
+ say "inner attempt/handle resolved, continuing outer attempt";51
+} handle (outer) {52
+ say "outer should not see this: " + outer;5353
}5454
55
-print "program completed normally";56
-let final_check = 42;57
-print final_check;55
+say "program completed normally";56
+local final_check = 42;57
+say final_check;ashvm/tests/cases/errors/raise_test.ashadded
+22−0@@ -0,0 +1,22 @@1
+attempt {2
+ raise "custom error: " + str(42);3
+} handle (e) {4
+ say "caught: " + e;5
+}6
+say "after";7
+8
+forge validate(x) {9
+ given x < 0 {10
+ raise "negative value: " + str(x);11
+ }12
+ yield x;13
+}14
+15
+attempt {16
+ say validate(5);17
+ say validate(-3);18
+ say "unreachable";19
+} handle (e) {20
+ say "caught: " + e;21
+}22
+say "done";ashvm/tests/cases/errors/vm_trycatch_test.ashmodified
+38−38@@ -1,52 +1,52 @@1
-let arr = [1, 2, 3];2
-try {3
- print arr[10];4
-} catch (e) {5
- print "caught: " + e;1
+local arr = [1, 2, 3];2
+attempt {3
+ say arr[10];4
+} handle (e) {5
+ say "caught: " + e;66
}7
-print "after array test";7
+say "after array test";88
9
-let m = {"a": 1};10
-try {11
- print m["missing"];12
-} catch (e) {13
- print "caught: " + e;9
+local m = {"a": 1};10
+attempt {11
+ say m["missing"];12
+} handle (e) {13
+ say "caught: " + e;1414
}1515
16
-try {17
- let content = read_file("/tmp/nonexistent_ashvm_xyz.txt");18
-} catch (e) {19
- print "caught: " + e;16
+attempt {17
+ local content = read_file("/tmp/nonexistent_ashvm_xyz.txt");18
+} handle (e) {19
+ say "caught: " + e;2020
}2121
22
-fn risky(n) {23
- if (n > 3) {24
- let arr2 = [1, 2];25
- print arr2[99];22
+forge risky(n) {23
+ given (n > 3) {24
+ local arr2 = [1, 2];25
+ say arr2[99];2626
}27
- return n;27
+ yield n;2828
}2929
30
-try {31
- let result = risky(5);32
- print "should not print";33
-} catch (e) {34
- print "caught in function: " + e;30
+attempt {31
+ local result = risky(5);32
+ say "should not say";33
+} handle (e) {34
+ say "caught in function: " + e;3535
}36
-print "still running after function error";36
+say "still running after function error";3737
38
-try {39
- try {40
- let arr3 = [1];41
- print arr3[5];42
- } catch (inner) {43
- print "inner caught: " + inner;38
+attempt {39
+ attempt {40
+ local arr3 = [1];41
+ say arr3[5];42
+ } handle (inner) {43
+ say "inner caught: " + inner;4444
}45
- print "inner resolved";46
-} catch (outer) {47
- print "outer should not fire";45
+ say "inner resolved";46
+} handle (outer) {47
+ say "outer should not fire";4848
}4949
50
-print "program completed";51
-let final = 42;52
-print final;50
+say "program completed";51
+local final = 42;52
+say final;ashvm/tests/cases/functions/closures_test.ashmodified
+16−16@@ -1,21 +1,21 @@1
-fn make_adder(x) {2
- return fn(y) { return x + y; };1
+forge make_adder(x) {2
+ yield forge(y) { yield x + y; };33
}44
5
-let add5 = make_adder(5);6
-let add10 = make_adder(10);7
-print add5(3);8
-print add10(3);5
+local add5 = make_adder(5);6
+local add10 = make_adder(10);7
+say add5(3);8
+say add10(3);99
10
-let nums = [1, 2, 3];11
-let shift100 = make_adder(100);12
-let shifted = map(nums, shift100);13
-print shifted;10
+local nums = [1, 2, 3];11
+local shift100 = make_adder(100);12
+local shifted = map(nums, shift100);13
+say shifted;1414
15
-fn compose(f, g) {16
- return fn(x) { return f(g(x)); };15
+forge compose(f, g) {16
+ yield forge(x) { yield f(g(x)); };1717
}18
-fn double(x) { return x * 2; }19
-fn inc(x) { return x + 1; }20
-let doubleThenInc = compose(inc, double);21
-print doubleThenInc(5);18
+forge double(x) { yield x * 2; }19
+forge inc(x) { yield x + 1; }20
+local doubleThenInc = compose(inc, double);21
+say doubleThenInc(5);ashvm/tests/cases/functions/functional_test.ashmodified
+14−14@@ -1,20 +1,20 @@1
-fn double(x) { return x * 2; }2
-fn is_even(x) { return x % 2 == 0; }1
+forge double(x) { yield x * 2; }2
+forge is_even(x) { yield x % 2 == 0; }33
4
-let nums = [1, 2, 3, 4, 5, 6];4
+local nums = [1, 2, 3, 4, 5, 6];55
6
-let doubled = map(nums, double);7
-print doubled;6
+local doubled = map(nums, double);7
+say doubled;88
9
-let evens = filter(nums, is_even);10
-print evens;9
+local evens = filter(nums, is_even);10
+say evens;1111
12
-let total = reduce(nums, fn(acc, x) { return acc + x; }, 0);13
-print total;12
+local total = reduce(nums, forge(acc, x) { yield acc + x; }, 0);13
+say total;1414
15
-let squareLambda = fn(x) { return x * x; };16
-let squares = map(nums, squareLambda);17
-print squares;15
+local squareLambda = forge(x) { yield x * x; };16
+local squares = map(nums, squareLambda);17
+say squares;1818
19
-let f = double;20
-print f(21);19
+local f = double;20
+say f(21);ashvm/tests/cases/functions/vm_closures_test.ashmodified
+18−18@@ -1,24 +1,24 @@1
-fn make_adder(x) {2
- return fn(y) { return x + y; };1
+forge make_adder(x) {2
+ yield forge(y) { yield x + y; };33
}44
5
-let add5 = make_adder(5);6
-let add10 = make_adder(10);7
-print add5(3);8
-print add10(3);5
+local add5 = make_adder(5);6
+local add10 = make_adder(10);7
+say add5(3);8
+say add10(3);99
10
-let nums = [1, 2, 3];11
-let shift100 = make_adder(100);12
-let shifted = map(nums, shift100);13
-print shifted;10
+local nums = [1, 2, 3];11
+local shift100 = make_adder(100);12
+local shifted = map(nums, shift100);13
+say shifted;1414
15
-fn compose(f, g) {16
- return fn(x) { return f(g(x)); };15
+forge compose(f, g) {16
+ yield forge(x) { yield f(g(x)); };1717
}18
-fn double(x) { return x * 2; }19
-fn inc(x) { return x + 1; }20
-let doubleThenInc = compose(inc, double);21
-print doubleThenInc(5);18
+forge double(x) { yield x * 2; }19
+forge inc(x) { yield x + 1; }20
+local doubleThenInc = compose(inc, double);21
+say doubleThenInc(5);2222
23
-let total = reduce(nums, fn(acc, n) { return acc + n; }, 0);24
-print total;23
+local total = reduce(nums, forge(acc, n) { yield acc + n; }, 0);24
+say total;ashvm/tests/cases/imports/ash.libs/testlib/testlib.ashmodified
+3−3@@ -1,5 +1,5 @@1
-let VERSION = "1.0";1
+local VERSION = "1.0";22
3
-fn square(x) {4
- return x * x;3
+forge square(x) {4
+ yield x * x;55
}ashvm/tests/cases/imports/fixtures/collision_dir_one/shared.ashmodified
+2−2@@ -1,3 +1,3 @@1
-fn fromOne() {2
- return 1;1
+forge fromOne() {2
+ yield 1;33
}ashvm/tests/cases/imports/fixtures/collision_dir_two/shared.ashmodified
+2−2@@ -1,3 +1,3 @@1
-fn fromTwo() {2
- return 2;1
+forge fromTwo() {2
+ yield 2;33
}ashvm/tests/cases/imports/fixtures/imports_basic_lib.ashmodified
+4−4@@ -1,7 +1,7 @@1
-fn helper(x) {2
- return x * 2;1
+forge helper(x) {2
+ yield x * 2;33
}44
5
-fn add(a, b) {6
- return a + b + helper(1);5
+forge add(a, b) {6
+ yield a + b + helper(1);77
}ashvm/tests/cases/imports/fixtures/imports_circular_a.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_circular_b.ash>;22
3
-fn fromA() {4
- return 1;3
+forge fromA() {4
+ yield 1;55
}ashvm/tests/cases/imports/fixtures/imports_circular_b.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_circular_a.ash>;22
3
-fn fromB() {4
- return 2;3
+forge fromB() {4
+ yield 2;55
}ashvm/tests/cases/imports/fixtures/imports_collision_main.ashmodified
+1−1@@ -1,4 +1,4 @@11
@import <./collision_dir_one/shared.ash>;22
@import <./collision_dir_two/shared.ash>;33
4
-print shared.fromOne();4
+say shared.fromOne();ashvm/tests/cases/imports/fixtures/imports_const_lib.ashmodified
+4−4@@ -1,6 +1,6 @@1
-let PI = 3.14159;2
-let GREETING = "hello from lib";1
+local PI = 3.14159;2
+local GREETING = "hello from lib";33
4
-fn circleArea(r) {5
- return PI * r * r;4
+forge circleArea(r) {5
+ yield PI * r * r;66
}ashvm/tests/cases/imports/fixtures/imports_diamond_b.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_diamond_d.ash>;22
3
-fn fromB() {4
- return imports_diamond_d.base() + 1;3
+forge fromB() {4
+ yield imports_diamond_d.base() + 1;55
}ashvm/tests/cases/imports/fixtures/imports_diamond_c.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_diamond_d.ash>;22
3
-fn fromC() {4
- return imports_diamond_d.base() + 2;3
+forge fromC() {4
+ yield imports_diamond_d.base() + 2;55
}ashvm/tests/cases/imports/fixtures/imports_diamond_d.ashmodified
+2−2@@ -1,3 +1,3 @@1
-fn base() {2
- return 100;1
+forge base() {2
+ yield 100;33
}ashvm/tests/cases/imports/fixtures/imports_statement_lib.ashadded
+19−0@@ -0,0 +1,19 @@1
+// Covers two bugs found writing the first real forgepack module (see2
+// CHANGELOG.md): a `local` inside a loop in an imported function used to3
+// be misdiagnosed as a failed top-level namespaced-constant declaration,4
+// and a namespaced call used as its OWN statement (not nested in an5
+// expression) used to fail to parse entirely.6
+forge sum_doubled(arr) {7
+ local total = 0;8
+ local i = 0;9
+ during i < len(arr) {10
+ local doubled = arr[i] * 2;11
+ total += doubled;12
+ i += 1;13
+ }14
+ yield total;15
+}16
+17
+forge announce(label) {18
+ say "announced: " + label;19
+}ashvm/tests/cases/imports/imports_basic_test.ashmodified
+2−2@@ -1,4 +1,4 @@11
@import <./fixtures/imports_basic_lib.ash>;22
3
-print imports_basic_lib.add(3, 4);4
-print imports_basic_lib.helper(10);3
+say imports_basic_lib.add(3, 4);4
+say imports_basic_lib.helper(10);ashvm/tests/cases/imports/imports_const_test.ashmodified
+3−3@@ -1,5 +1,5 @@11
@import <./fixtures/imports_const_lib.ash>;22
3
-print imports_const_lib.PI;4
-print imports_const_lib.GREETING;5
-print imports_const_lib.circleArea(2);3
+say imports_const_lib.PI;4
+say imports_const_lib.GREETING;5
+say imports_const_lib.circleArea(2);ashvm/tests/cases/imports/imports_diamond_test.ashmodified
+3−3@@ -1,6 +1,6 @@11
@import <./fixtures/imports_diamond_b.ash>;22
@import <./fixtures/imports_diamond_c.ash>;33
4
-print imports_diamond_b.fromB();5
-print imports_diamond_c.fromC();6
-print imports_diamond_d.base();4
+say imports_diamond_b.fromB();5
+say imports_diamond_c.fromC();6
+say imports_diamond_d.base();ashvm/tests/cases/imports/imports_lib_test.ashmodified
+2−2@@ -1,4 +1,4 @@11
@import <testlib>;22
3
-print testlib.VERSION;4
-print testlib.square(6);3
+say testlib.VERSION;4
+say testlib.square(6);ashvm/tests/cases/imports/imports_statement_test.ashadded
+4−0@@ -0,0 +1,4 @@1
+@import <./fixtures/imports_statement_lib.ash>;2
+3
+say imports_statement_lib.sum_doubled([1, 2, 3]);4
+imports_statement_lib.announce("done");ashvm/tests/cases/strings/escapes_test.ashmodified
+7−7@@ -1,9 +1,9 @@1
-print "line one\nline two";2
-print "tab\there";3
-print "quote: \"hello\"";4
-print "backslash: \\";1
+say "line one\nline two";2
+say "tab\there";3
+say "quote: \"hello\"";4
+say "backslash: \\";55
write_file("/tmp/ash_escape_test.txt", "Hello from Ash!\n");6
-let content = read_file("/tmp/ash_escape_test.txt");7
-print content;6
+local content = read_file("/tmp/ash_escape_test.txt");7
+say content;88
append_file("/tmp/ash_escape_test.txt", "Second line\n");9
-print read_file("/tmp/ash_escape_test.txt");9
+say read_file("/tmp/ash_escape_test.txt");ashvm/tests/cases/strings/strings_files_test.ashmodified
+16−16@@ -1,19 +1,19 @@1
-let parts = split("a,b,c", ",");2
-print parts;3
-print join(parts, "-");4
-print substring("hello world", 0, 5);5
-print indexOf("hello world", "world");6
-print indexOf("hello world", "xyz");7
-print replace("foo bar foo", "foo", "baz");8
-print upper("hello");9
-print lower("WORLD");10
-print trim(" spaced ");1
+local parts = split("a,b,c", ",");2
+say parts;3
+say join(parts, "-");4
+say substring("hello world", 0, 5);5
+say indexOf("hello world", "world");6
+say indexOf("hello world", "xyz");7
+say replace("foo bar foo", "foo", "baz");8
+say upper("hello");9
+say lower("WORLD");10
+say trim(" spaced ");1111
1212
write_file("/tmp/ash_test.txt", "Hello from Ash!\n");13
-print file_exists("/tmp/ash_test.txt");14
-let content = read_file("/tmp/ash_test.txt");15
-print content;13
+say file_exists("/tmp/ash_test.txt");14
+local content = read_file("/tmp/ash_test.txt");15
+say content;1616
append_file("/tmp/ash_test.txt", "Second line\n");17
-let content2 = read_file("/tmp/ash_test.txt");18
-print content2;19
-print file_exists("/tmp/nonexistent_file_xyz.txt");17
+local content2 = read_file("/tmp/ash_test.txt");18
+say content2;19
+say file_exists("/tmp/nonexistent_file_xyz.txt");ashvm/tests/cases/strings/vm_strings_test.ashmodified
+9−9@@ -1,12 +1,12 @@1
-let name = "Ash";2
-print "Hello, " + name + "!";1
+local name = "Ash";2
+say "Hello, " + name + "!";33
4
-let a = "abc";5
-let b = "abc";6
-print a == b;7
-print a == "xyz";4
+local a = "abc";5
+local b = "abc";6
+say a == b;7
+say a == "xyz";88
9
-fn greet(who) {10
- return "Hi " + who;9
+forge greet(who) {10
+ yield "Hi " + who;1111
}12
-print greet("world");12
+say greet("world");forgepack/Makefileadded
+46−0@@ -0,0 +1,46 @@1
+CC = gcc2
+.DEFAULT_GOAL := all3
+CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc4
+5
+FORGEPACK_SRC = \6
+ src/C/main.c \7
+ src/C/manifest.c \8
+ src/C/fsutil.c \9
+ src/C/git_fetch.c \10
+ src/C/install.c \11
+ src/C/cmd.c \12
+ src/C/ui.c13
+14
+# Objects and dependency files live in trees parallel to C/:15
+# src/C/x.c -> src/O/x.o, src/D/x.d16
+obj_of = $(patsubst %.c,%.o,$(subst /C/,/O/,$(1)))17
+dep_of = $(patsubst %.c,%.d,$(subst /C/,/D/,$(1)))18
+FORGEPACK_OBJ = $(foreach s,$(FORGEPACK_SRC),$(call obj_of,$(s)))19
+FORGEPACK_DEP = $(foreach s,$(FORGEPACK_SRC),$(call dep_of,$(s)))20
+21
+# -MMD -MP: see ashvm/Makefile's and kiln/Makefile's comment -- a shared22
+# header edit must rebuild every .o that includes it, not just the .c23
+# whose own timestamp changed.24
+define compile_rule25
+$(call obj_of,$(1)): $(1)26
+ @mkdir -p $$(dir $$@) $$(dir $(call dep_of,$(1)))27
+ $$(CC) $$(CFLAGS) -MMD -MP -MF $(call dep_of,$(1)) -MT $$@ -c $$< -o $$@28
+endef29
+$(foreach s,$(FORGEPACK_SRC),$(eval $(call compile_rule,$(s))))30
+31
+BIN = ../bin/forgepack32
+33
+all: $(BIN)34
+35
+$(BIN): $(FORGEPACK_OBJ)36
+ mkdir -p ../bin37
+ $(CC) $(CFLAGS) -o $(BIN) $(FORGEPACK_OBJ)38
+39
+-include $(FORGEPACK_DEP)40
+41
+clean:42
+ find src -name '*.o' -delete43
+ find src -name '*.d' -delete44
+ rm -f $(BIN)45
+46
+.PHONY: all cleanforgepack/src/C/cmd.cadded
+137−0@@ -0,0 +1,137 @@1
+#define _XOPEN_SOURCE 700 // exposes PATH_MAX under -std=c11 (see ashvm/kiln's import_paths.c)2
+#include <stdio.h>3
+#include <string.h>4
+#include <unistd.h>5
+#include <limits.h>6
+#include "H/cmd.h"7
+#include "H/manifest.h"8
+#include "H/install.h"9
+#include "H/fsutil.h"10
+#include "H/git_fetch.h"11
+#include "H/ui.h"12
+13
+static void infer_project_name(char *out, int out_size) {14
+ char cwd[PATH_MAX];15
+ if (getcwd(cwd, sizeof(cwd)) == NULL) { snprintf(out, (size_t)out_size, "my-project"); return; }16
+ char *slash = strrchr(cwd, '/');17
+ const char *base = slash ? slash + 1 : cwd;18
+ int len = (int)strlen(base);19
+ if (len >= out_size) len = out_size - 1;20
+ memcpy(out, base, (size_t)len);21
+ out[len] = '\0';22
+}23
+24
+int cmd_init(const char *project_dir, int argc, char **argv) {25
+ (void)argc; (void)argv;26
+ Manifest m;27
+ if (manifest_load(project_dir, &m)) {28
+ ui_err("%s already exists", MANIFEST_FILE);29
+ return 1;30
+ }31
+32
+ char name[MANIFEST_NAME_LEN];33
+ infer_project_name(name, sizeof(name));34
+ manifest_init_default(&m, name);35
+ if (!manifest_save(project_dir, &m)) return 1;36
+37
+ ui_ok("created %s (%s@%s)", MANIFEST_FILE, m.name, m.version);38
+ return 0;39
+}40
+41
+// forgepack add github:user/repo[@ref] [--as <name>]42
+//43
+// GitHub-only for now -- forgepack has no registry/index yet (it will,44
+// see ideas/ash_modules.md's phased plan; ash-project.org is reserved45
+// for that once there's a real flat index worth serving from it), so a46
+// git source is the only dependency kind that's actually installable47
+// without a server.48
+int cmd_add(const char *project_dir, int argc, char **argv) {49
+ if (argc < 1 || !git_fetch_is_github_spec(argv[0])) {50
+ ui_err("usage: forgepack add github:<user>/<repo>[@ref] [--as <name>]");51
+ return 1;52
+ }53
+ const char *source = argv[0];54
+ const char *explicit_name = NULL;55
+ for (int i = 1; i < argc - 1; i++) {56
+ if (strcmp(argv[i], "--as") == 0) explicit_name = argv[i + 1];57
+ }58
+59
+ char name[MANIFEST_NAME_LEN];60
+ if (explicit_name) {61
+ snprintf(name, sizeof(name), "%s", explicit_name);62
+ } else {63
+ git_fetch_infer_name(source, name, sizeof(name));64
+ }65
+66
+ Manifest m;67
+ if (!manifest_load(project_dir, &m)) {68
+ ui_err("no %s here -- run 'forgepack init' first", MANIFEST_FILE);69
+ return 1;70
+ }71
+ if (!manifest_set_dependency(&m, name, source)) {72
+ ui_err("dependency table full (max %d)", MANIFEST_MAX_DEPS);73
+ return 1;74
+ }75
+ if (!manifest_save(project_dir, &m)) return 1;76
+77
+ char ash_modules[1024];78
+ snprintf(ash_modules, sizeof(ash_modules), "%s/@ash-modules", project_dir);79
+ fsutil_mkdir_p(ash_modules);80
+81
+ Dependency *dep = manifest_find_dependency(&m, name);82
+ if (!install_dependency(project_dir, dep)) {83
+ ui_err("failed to install '%s'", name);84
+ return 1;85
+ }86
+ ui_ok("added %s (%s)", name, source);87
+ return 0;88
+}89
+90
+int cmd_install(const char *project_dir, int argc, char **argv) {91
+ (void)argc; (void)argv;92
+ Manifest m;93
+ if (!manifest_load(project_dir, &m)) {94
+ ui_err("no %s here -- run 'forgepack init' first", MANIFEST_FILE);95
+ return 1;96
+ }97
+98
+ char ash_modules[1024];99
+ snprintf(ash_modules, sizeof(ash_modules), "%s/@ash-modules", project_dir);100
+ fsutil_mkdir_p(ash_modules);101
+102
+ if (m.dep_count == 0) {103
+ ui_info("%s has no dependencies", m.name);104
+ return 0;105
+ }106
+107
+ ui_header("%s@%s", m.name, m.version);108
+ int ok = 1;109
+ for (int i = 0; i < m.dep_count; i++) {110
+ if (!install_dependency(project_dir, &m.deps[i])) ok = 0;111
+ }112
+ if (ok) ui_ok("done");113
+ else ui_err("one or more dependencies failed to install");114
+ return ok ? 0 : 1;115
+}116
+117
+int cmd_list(const char *project_dir, int argc, char **argv) {118
+ (void)argc; (void)argv;119
+ Manifest m;120
+ if (!manifest_load(project_dir, &m)) {121
+ ui_err("no %s here -- run 'forgepack init' first", MANIFEST_FILE);122
+ return 1;123
+ }124
+125
+ ui_header("%s@%s", m.name, m.version);126
+ if (m.dep_count == 0) {127
+ ui_bullet("(no dependencies)");128
+ return 0;129
+ }130
+ for (int i = 0; i < m.dep_count; i++) {131
+ char dest[1024];132
+ snprintf(dest, sizeof(dest), "%s/@ash-modules/%s", project_dir, m.deps[i].name);133
+ if (fsutil_dir_exists(dest)) ui_bullet("%s: %s", m.deps[i].name, m.deps[i].value);134
+ else ui_bullet("%s: %s (not installed)", m.deps[i].name, m.deps[i].value);135
+ }136
+ return 0;137
+}forgepack/src/C/fsutil.cadded
+73−0@@ -0,0 +1,73 @@1
+#include <stdio.h>2
+#include <stdlib.h>3
+#include <string.h>4
+#include <sys/stat.h>5
+#include <dirent.h>6
+#include <unistd.h>7
+#include "H/fsutil.h"8
+9
+int fsutil_dir_exists(const char *path) {10
+ struct stat st;11
+ return stat(path, &st) == 0 && S_ISDIR(st.st_mode);12
+}13
+14
+int fsutil_mkdir_p(const char *path) {15
+ char buf[1024];16
+ snprintf(buf, sizeof(buf), "%s", path);17
+ size_t len = strlen(buf);18
+ for (size_t i = 1; i <= len; i++) {19
+ if (buf[i] == '/' || i == len) {20
+ char save = buf[i];21
+ buf[i] = '\0';22
+ if (buf[0] != '\0' && mkdir(buf, 0755) != 0) {23
+ // EEXIST is fine -- anything else means a real failure.24
+ struct stat st;25
+ if (stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) return 0;26
+ }27
+ buf[i] = save;28
+ }29
+ }30
+ return 1;31
+}32
+33
+static int copy_file(const char *src, const char *dest) {34
+ FILE *in = fopen(src, "rb");35
+ if (!in) return 0;36
+ FILE *out = fopen(dest, "wb");37
+ if (!out) { fclose(in); return 0; }38
+ char buf[8192];39
+ size_t n;40
+ while ((n = fread(buf, 1, sizeof(buf), in)) > 0) fwrite(buf, 1, n, out);41
+ fclose(in);42
+ fclose(out);43
+ return 1;44
+}45
+46
+int fsutil_copy_dir(const char *src_dir, const char *dest_dir) {47
+ if (!fsutil_mkdir_p(dest_dir)) return 0;48
+49
+ DIR *d = opendir(src_dir);50
+ if (!d) return 0;51
+52
+ struct dirent *entry;53
+ int ok = 1;54
+ while (ok && (entry = readdir(d)) != NULL) {55
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;56
+ // Never copy a nested @ash-modules/ or .git -- a "path:" dependency57
+ // installs its OWN source, not whatever it happened to have58
+ // installed/cloned locally when you ran `forgepack add`.59
+ if (strcmp(entry->d_name, "@ash-modules") == 0 || strcmp(entry->d_name, ".git") == 0) continue;60
+61
+ char src_path[1024], dest_path[1024];62
+ snprintf(src_path, sizeof(src_path), "%s/%s", src_dir, entry->d_name);63
+ snprintf(dest_path, sizeof(dest_path), "%s/%s", dest_dir, entry->d_name);64
+65
+ if (fsutil_dir_exists(src_path)) {66
+ ok = fsutil_copy_dir(src_path, dest_path);67
+ } else {68
+ ok = copy_file(src_path, dest_path);69
+ }70
+ }71
+ closedir(d);72
+ return ok;73
+}forgepack/src/C/git_fetch.cadded
+83−0@@ -0,0 +1,83 @@1
+#include <stdio.h>2
+#include <stdlib.h>3
+#include <string.h>4
+#include <ctype.h>5
+#include "H/git_fetch.h"6
+7
+int git_fetch_is_github_spec(const char *value) {8
+ return strncmp(value, "github:", 7) == 0;9
+}10
+11
+// user/repo/ref only ever contain characters that are safe to drop12
+// straight into a shell command -- anything else is rejected outright13
+// rather than escaped, since a dependency spec is attacker-controlled14
+// the moment it comes from someone else's published ash.pkg.15
+static int is_safe_component(const char *s, int len) {16
+ if (len <= 0) return 0;17
+ for (int i = 0; i < len; i++) {18
+ char c = s[i];19
+ if (!(isalnum((unsigned char)c) || c == '-' || c == '_' || c == '.')) return 0;20
+ }21
+ return 1;22
+}23
+24
+static int parse_github_spec(const char *spec, char *user, int user_size, char *repo, int repo_size, char *ref, int ref_size) {25
+ const char *rest = spec + 7; // skip "github:"26
+ const char *slash = strchr(rest, '/');27
+ if (!slash) return 0;28
+29
+ int user_len = (int)(slash - rest);30
+ if (!is_safe_component(rest, user_len) || user_len >= user_size) return 0;31
+ memcpy(user, rest, (size_t)user_len);32
+ user[user_len] = '\0';33
+34
+ const char *repo_start = slash + 1;35
+ const char *at = strchr(repo_start, '@');36
+ int repo_len = at ? (int)(at - repo_start) : (int)strlen(repo_start);37
+ if (!is_safe_component(repo_start, repo_len) || repo_len >= repo_size) return 0;38
+ memcpy(repo, repo_start, (size_t)repo_len);39
+ repo[repo_len] = '\0';40
+41
+ if (at) {42
+ int ref_len = (int)strlen(at + 1);43
+ if (!is_safe_component(at + 1, ref_len) || ref_len >= ref_size) return 0;44
+ memcpy(ref, at + 1, (size_t)ref_len);45
+ ref[ref_len] = '\0';46
+ } else {47
+ ref[0] = '\0';48
+ }49
+ return 1;50
+}51
+52
+void git_fetch_infer_name(const char *spec, char *out, int out_size) {53
+ char user[128], repo[128], ref[128];54
+ if (git_fetch_is_github_spec(spec) && parse_github_spec(spec, user, sizeof(user), repo, sizeof(repo), ref, sizeof(ref))) {55
+ snprintf(out, (size_t)out_size, "%s", repo);56
+ } else {57
+ snprintf(out, (size_t)out_size, "%s", spec);58
+ }59
+}60
+61
+int git_fetch_github(const char *spec, const char *dest_dir) {62
+ char user[128], repo[128], ref[128];63
+ if (!parse_github_spec(spec, user, sizeof(user), repo, sizeof(repo), ref, sizeof(ref))) {64
+ fprintf(stderr, "forgepack: malformed github spec '%s' (expected github:user/repo or github:user/repo@ref)\n", spec);65
+ return 0;66
+ }67
+68
+ char cmd[1024];69
+ if (ref[0] != '\0') {70
+ snprintf(cmd, sizeof(cmd), "git clone --quiet --depth 1 --branch %s https://github.com/%s/%s.git %s",71
+ ref, user, repo, dest_dir);72
+ } else {73
+ snprintf(cmd, sizeof(cmd), "git clone --quiet --depth 1 https://github.com/%s/%s.git %s",74
+ user, repo, dest_dir);75
+ }76
+77
+ int rc = system(cmd);78
+ if (rc != 0) {79
+ fprintf(stderr, "forgepack: 'git clone' failed for %s\n", spec);80
+ return 0;81
+ }82
+ return 1;83
+}forgepack/src/C/install.cadded
+30−0@@ -0,0 +1,30 @@1
+#include <stdio.h>2
+#include "H/install.h"3
+#include "H/fsutil.h"4
+#include "H/git_fetch.h"5
+#include "H/ui.h"6
+7
+// GitHub-only for now, deliberately -- no registry/index yet (see8
+// ideas/ash_modules.md's phased plan), so a git source is the only thing9
+// forgepack can actually resolve without a server. path:/plain-version10
+// dependencies are a later addition, not scoped in yet.11
+int install_dependency(const char *project_dir, const Dependency *dep) {12
+ char dest[1024];13
+ snprintf(dest, sizeof(dest), "%s/@ash-modules/%s", project_dir, dep->name);14
+15
+ if (fsutil_dir_exists(dest)) {16
+ ui_bullet("%s already installed", dep->name);17
+ return 1;18
+ }19
+20
+ if (!git_fetch_is_github_spec(dep->value)) {21
+ ui_err("'%s' is not a github: dependency ('%s') -- only github:user/repo[@ref] is supported right now",22
+ dep->name, dep->value);23
+ return 0;24
+ }25
+26
+ ui_info("fetching %s (%s)", dep->name, dep->value);27
+ if (!git_fetch_github(dep->value, dest)) return 0;28
+ ui_ok("%s installed", dep->name);29
+ return 1;30
+}forgepack/src/C/main.cadded
+31−0@@ -0,0 +1,31 @@1
+#include <stdio.h>2
+#include <string.h>3
+#include "H/cmd.h"4
+#include "H/ui.h"5
+6
+static void print_usage(void) {7
+ printf("forgepack -- package manager for Ash (github: sources only, for now)\n\n");8
+ printf("usage:\n");9
+ printf(" forgepack init\n");10
+ printf(" forgepack add github:<user>/<repo>[@ref] [--as <name>]\n");11
+ printf(" forgepack install\n");12
+ printf(" forgepack list\n");13
+}14
+15
+int main(int argc, char **argv) {16
+ if (argc < 2) { print_usage(); return 1; }17
+18
+ const char *command = argv[1];19
+ int rest_argc = argc - 2;20
+ char **rest_argv = argv + 2;21
+ const char *project_dir = ".";22
+23
+ if (strcmp(command, "init") == 0) return cmd_init(project_dir, rest_argc, rest_argv);24
+ if (strcmp(command, "add") == 0) return cmd_add(project_dir, rest_argc, rest_argv);25
+ if (strcmp(command, "install") == 0) return cmd_install(project_dir, rest_argc, rest_argv);26
+ if (strcmp(command, "list") == 0) return cmd_list(project_dir, rest_argc, rest_argv);27
+28
+ ui_err("unknown command '%s'", command);29
+ print_usage();30
+ return 1;31
+}forgepack/src/C/manifest.cadded
+98−0@@ -0,0 +1,98 @@1
+#include <stdio.h>2
+#include <stdlib.h>3
+#include <string.h>4
+#include "H/manifest.h"5
+6
+void manifest_init_default(Manifest *m, const char *project_name) {7
+ memset(m, 0, sizeof(*m));8
+ snprintf(m->name, sizeof(m->name), "%s", project_name);9
+ snprintf(m->version, sizeof(m->version), "0.1.0");10
+}11
+12
+// Trims leading/trailing whitespace in place, returns the trimmed start.13
+static char *trim(char *s) {14
+ while (*s == ' ' || *s == '\t') s++;15
+ size_t len = strlen(s);16
+ while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r' || s[len - 1] == ' ' || s[len - 1] == '\t')) {17
+ s[--len] = '\0';18
+ }19
+ return s;20
+}21
+22
+int manifest_load(const char *dir, Manifest *out) {23
+ char path[1024];24
+ snprintf(path, sizeof(path), "%s/%s", dir, MANIFEST_FILE);25
+ FILE *f = fopen(path, "r");26
+ if (!f) return 0;27
+28
+ memset(out, 0, sizeof(*out));29
+ int in_deps = 0;30
+ char line[512];31
+ while (fgets(line, sizeof(line), f)) {32
+ char *l = trim(line);33
+ if (l[0] == '\0' || l[0] == '#') continue;34
+ if (strcmp(l, "[dependencies]") == 0) { in_deps = 1; continue; }35
+36
+ char *colon = strchr(l, ':');37
+ if (!colon) continue; // ignore malformed lines rather than aborting the whole load38
+ *colon = '\0';39
+ char *key = trim(l);40
+ char *value = trim(colon + 1);41
+42
+ if (!in_deps) {43
+ if (strcmp(key, "name") == 0) snprintf(out->name, sizeof(out->name), "%s", value);44
+ else if (strcmp(key, "version") == 0) snprintf(out->version, sizeof(out->version), "%s", value);45
+ } else {46
+ if (out->dep_count >= MANIFEST_MAX_DEPS) {47
+ fprintf(stderr, "forgepack: too many dependencies (max %d), ignoring '%s'\n", MANIFEST_MAX_DEPS, key);48
+ continue;49
+ }50
+ Dependency *d = &out->deps[out->dep_count++];51
+ snprintf(d->name, sizeof(d->name), "%s", key);52
+ snprintf(d->value, sizeof(d->value), "%s", value);53
+ }54
+ }55
+ fclose(f);56
+ return 1;57
+}58
+59
+int manifest_save(const char *dir, const Manifest *m) {60
+ char path[1024];61
+ snprintf(path, sizeof(path), "%s/%s", dir, MANIFEST_FILE);62
+ FILE *f = fopen(path, "w");63
+ if (!f) {64
+ fprintf(stderr, "forgepack: could not write %s\n", path);65
+ return 0;66
+ }67
+68
+ fprintf(f, "name: %s\n", m->name);69
+ fprintf(f, "version: %s\n", m->version);70
+ if (m->dep_count > 0) {71
+ fprintf(f, "\n[dependencies]\n");72
+ for (int i = 0; i < m->dep_count; i++) {73
+ fprintf(f, "%s: %s\n", m->deps[i].name, m->deps[i].value);74
+ }75
+ }76
+ fclose(f);77
+ return 1;78
+}79
+80
+int manifest_set_dependency(Manifest *m, const char *name, const char *value) {81
+ Dependency *existing = manifest_find_dependency(m, name);82
+ if (existing) {83
+ snprintf(existing->value, sizeof(existing->value), "%s", value);84
+ return 1;85
+ }86
+ if (m->dep_count >= MANIFEST_MAX_DEPS) return 0;87
+ Dependency *d = &m->deps[m->dep_count++];88
+ snprintf(d->name, sizeof(d->name), "%s", name);89
+ snprintf(d->value, sizeof(d->value), "%s", value);90
+ return 1;91
+}92
+93
+Dependency *manifest_find_dependency(Manifest *m, const char *name) {94
+ for (int i = 0; i < m->dep_count; i++) {95
+ if (strcmp(m->deps[i].name, name) == 0) return &m->deps[i];96
+ }97
+ return NULL;98
+}forgepack/src/C/ui.cadded
+67−0@@ -0,0 +1,67 @@1
+#define _POSIX_C_SOURCE 200809L // exposes fileno() under -std=c112
+#include <stdio.h>3
+#include <stdarg.h>4
+#include <stdlib.h>5
+#include <unistd.h>6
+#include "H/ui.h"7
+8
+#define GREEN "\x1b[32m"9
+#define RED "\x1b[31m"10
+#define CYAN "\x1b[36m"11
+#define DIM "\x1b[2m"12
+#define BOLD "\x1b[1m"13
+#define RESET "\x1b[0m"14
+15
+static int color_enabled(FILE *stream) {16
+ static int checked = 0, out_tty = -1, err_tty = -1, no_color = -1;17
+ if (!checked) {18
+ out_tty = isatty(fileno(stdout));19
+ err_tty = isatty(fileno(stderr));20
+ no_color = getenv("NO_COLOR") != NULL;21
+ checked = 1;22
+ }23
+ if (no_color) return 0;24
+ return stream == stdout ? out_tty : err_tty;25
+}26
+27
+static void emit(FILE *stream, const char *color, const char *symbol, const char *fmt, va_list args) {28
+ int c = color_enabled(stream);29
+ if (c) fprintf(stream, "%s%s%s ", color, symbol, RESET);30
+ else fprintf(stream, "%s ", symbol);31
+ vfprintf(stream, fmt, args);32
+ fprintf(stream, "\n");33
+}34
+35
+void ui_ok(const char *fmt, ...) {36
+ va_list args; va_start(args, fmt);37
+ emit(stdout, GREEN, "\xe2\x9c\x93", fmt, args); // U+2713 CHECK MARK38
+ va_end(args);39
+}40
+41
+void ui_err(const char *fmt, ...) {42
+ va_list args; va_start(args, fmt);43
+ emit(stderr, RED, "\xe2\x9c\x97", fmt, args); // U+2717 BALLOT X44
+ va_end(args);45
+}46
+47
+void ui_info(const char *fmt, ...) {48
+ va_list args; va_start(args, fmt);49
+ emit(stdout, CYAN, "\xe2\x86\x92", fmt, args); // U+2192 RIGHTWARDS ARROW50
+ va_end(args);51
+}52
+53
+void ui_bullet(const char *fmt, ...) {54
+ va_list args; va_start(args, fmt);55
+ emit(stdout, DIM, "\xe2\x80\xa2", fmt, args); // U+2022 BULLET56
+ va_end(args);57
+}58
+59
+void ui_header(const char *fmt, ...) {60
+ va_list args; va_start(args, fmt);61
+ int c = color_enabled(stdout);62
+ if (c) printf("%s", BOLD);63
+ vprintf(fmt, args);64
+ if (c) printf("%s", RESET);65
+ printf("\n");66
+ va_end(args);67
+}forgepack/src/D/cmd.dadded
+8−0@@ -0,0 +1,8 @@1
+src/O/cmd.o: src/C/cmd.c src/H/cmd.h src/H/manifest.h src/H/install.h \2
+ src/H/fsutil.h src/H/git_fetch.h src/H/ui.h3
+src/H/cmd.h:4
+src/H/manifest.h:5
+src/H/install.h:6
+src/H/fsutil.h:7
+src/H/git_fetch.h:8
+src/H/ui.h:forgepack/src/D/fsutil.dadded
+2−0@@ -0,0 +1,2 @@1
+src/O/fsutil.o: src/C/fsutil.c src/H/fsutil.h2
+src/H/fsutil.h:forgepack/src/D/git_fetch.dadded
+2−0@@ -0,0 +1,2 @@1
+src/O/git_fetch.o: src/C/git_fetch.c src/H/git_fetch.h2
+src/H/git_fetch.h:forgepack/src/D/install.dadded
+7−0@@ -0,0 +1,7 @@1
+src/O/install.o: src/C/install.c src/H/install.h src/H/manifest.h \2
+ src/H/fsutil.h src/H/git_fetch.h src/H/ui.h3
+src/H/install.h:4
+src/H/manifest.h:5
+src/H/fsutil.h:6
+src/H/git_fetch.h:7
+src/H/ui.h:forgepack/src/D/main.dadded
+3−0@@ -0,0 +1,3 @@1
+src/O/main.o: src/C/main.c src/H/cmd.h src/H/ui.h2
+src/H/cmd.h:3
+src/H/ui.h:forgepack/src/D/manifest.dadded
+2−0@@ -0,0 +1,2 @@1
+src/O/manifest.o: src/C/manifest.c src/H/manifest.h2
+src/H/manifest.h:forgepack/src/D/ui.dadded
+2−0@@ -0,0 +1,2 @@1
+src/O/ui.o: src/C/ui.c src/H/ui.h2
+src/H/ui.h:forgepack/src/H/cmd.hadded
+12−0@@ -0,0 +1,12 @@1
+#ifndef FORGEPACK_CMD_H2
+#define FORGEPACK_CMD_H3
+4
+// Each command takes the project directory ("." almost always, since5
+// forgepack is always run from a project's root) and the remaining argv6
+// past the subcommand name. Returns an exit code.7
+int cmd_init(const char *project_dir, int argc, char **argv);8
+int cmd_add(const char *project_dir, int argc, char **argv);9
+int cmd_install(const char *project_dir, int argc, char **argv);10
+int cmd_list(const char *project_dir, int argc, char **argv);11
+12
+#endifforgepack/src/H/fsutil.hadded
+16−0@@ -0,0 +1,16 @@1
+#ifndef FORGEPACK_FSUTIL_H2
+#define FORGEPACK_FSUTIL_H3
+4
+int fsutil_dir_exists(const char *path);5
+6
+// mkdir -p equivalent -- creates every missing component of `path`.7
+// Returns 1 on success (including "already exists"), 0 on failure.8
+int fsutil_mkdir_p(const char *path);9
+10
+// Recursively copies `src_dir` into `dest_dir` (dest_dir is created).11
+// Used for "path:" dependencies -- deliberately a copy, not a symlink,12
+// so an installed @ash-modules/ entry never changes out from under a13
+// build just because the original source directory did.14
+int fsutil_copy_dir(const char *src_dir, const char *dest_dir);15
+16
+#endifforgepack/src/H/git_fetch.hadded
+19−0@@ -0,0 +1,19 @@1
+#ifndef FORGEPACK_GIT_FETCH_H2
+#define FORGEPACK_GIT_FETCH_H3
+4
+// Recognizes "github:user/repo" and "github:user/repo@ref".5
+int git_fetch_is_github_spec(const char *value);6
+7
+// Clones the given "github:..." spec into dest_dir (shells out to the8
+// system `git` binary -- forgepack has no git implementation of its own,9
+// same as every other package manager that supports a git source).10
+// user/repo/ref are validated against a strict charset before ever11
+// touching a shell command string, so a crafted dependency spec can't12
+// inject anything. Returns 1 on success.13
+int git_fetch_github(const char *spec, const char *dest_dir);14
+15
+// Derives the package name forgepack should use when none was given16
+// explicitly ("add github:user/repo" -> "repo").17
+void git_fetch_infer_name(const char *spec, char *out, int out_size);18
+19
+#endifforgepack/src/H/install.hadded
+15−0@@ -0,0 +1,15 @@1
+#ifndef FORGEPACK_INSTALL_H2
+#define FORGEPACK_INSTALL_H3
+#include "H/manifest.h"4
+5
+// Installs one dependency into <project_dir>/@ash-modules/<dep->name>/ if6
+// it isn't already there. Dispatches on the dependency's value: "github:"7
+// -> git clone, "path:" -> recursive copy, anything else (a plain8
+// version/range) -> prints a note and is skipped, since there's no9
+// registry yet to resolve a bare version against (see10
+// ideas/ash_modules.md's phased plan -- that's phase 3, not phase 1).11
+// Returns 1 if the dependency ended up installed (or already was), 0 on12
+// a real failure.13
+int install_dependency(const char *project_dir, const Dependency *dep);14
+15
+#endifforgepack/src/H/manifest.hadded
+42−0@@ -0,0 +1,42 @@1
+#ifndef FORGEPACK_MANIFEST_H2
+#define FORGEPACK_MANIFEST_H3
+4
+#define MANIFEST_MAX_DEPS 645
+#define MANIFEST_NAME_LEN 1286
+#define MANIFEST_VALUE_LEN 2567
+#define MANIFEST_FILE "ash.pkg"8
+9
+// A dependency's value is a raw spec string. GitHub-only for now --10
+// "github:user/repo" or "github:user/repo@ref" (ref = tag or branch).11
+// No registry yet (see ideas/ash_modules.md), so a plain version/range12
+// or a local "path:" dependency aren't supported yet either.13
+typedef struct {14
+ char name[MANIFEST_NAME_LEN];15
+ char value[MANIFEST_VALUE_LEN];16
+} Dependency;17
+18
+typedef struct {19
+ char name[MANIFEST_NAME_LEN];20
+ char version[32];21
+ Dependency deps[MANIFEST_MAX_DEPS];22
+ int dep_count;23
+} Manifest;24
+25
+void manifest_init_default(Manifest *m, const char *project_name);26
+27
+// Reads ash.pkg from `dir` (a directory path, not the file itself).28
+// Returns 1 on success, 0 if the file doesn't exist or is malformed29
+// (a message is printed to stderr either way).30
+int manifest_load(const char *dir, Manifest *out);31
+32
+// Writes ash.pkg into `dir`, overwriting any existing one.33
+int manifest_save(const char *dir, const Manifest *m);34
+35
+// Adds a new dependency or overwrites an existing one with the same name.36
+// Returns 0 if the table is full.37
+int manifest_set_dependency(Manifest *m, const char *name, const char *value);38
+39
+// Returns NULL if no dependency with that name exists.40
+Dependency *manifest_find_dependency(Manifest *m, const char *name);41
+42
+#endifforgepack/src/H/ui.hadded
+14−0@@ -0,0 +1,14 @@1
+#ifndef FORGEPACK_UI_H2
+#define FORGEPACK_UI_H3
+4
+// Plain Unicode symbols (box-drawing/dingbats block, not emoji -- no5
+// color-font fallback needed, renders in any monospace terminal font) and6
+// ANSI color, auto-disabled when stdout isn't a TTY or NO_COLOR is set7
+// (piping to a file or another program should never see escape codes).8
+void ui_ok(const char *fmt, ...); // green check -- succeeded9
+void ui_err(const char *fmt, ...); // red cross, to stderr -- failed10
+void ui_info(const char *fmt, ...); // cyan arrow -- in progress / notice11
+void ui_bullet(const char *fmt, ...); // dim bullet -- a sub-item (dependency list, etc.)12
+void ui_header(const char *fmt, ...); // bold -- a section/project name header13
+14
+#endifforgepack/src/O/.gitkeepadded
+0−0No content changes. View the file on GitHub
forgepack/tests/scripts/smoke_test.shadded
+39−0@@ -0,0 +1,39 @@1
+#!/usr/bin/env bash2
+# forgepack/tests/scripts/smoke_test.sh -- exercises the CLI end to end3
+# (init/add/install/list) against a throwaway scratch directory. Not a4
+# .ash regression suite like ashvm/kiln have (forgepack doesn't run Ash5
+# programs, it manages their dependencies) -- this is the equivalent6
+# check for this project: does the real CLI, talking to the real7
+# filesystem and the real `git`, do what it says.8
+set -euo pipefail9
+cd "$(dirname "$0")/../.." # -> forgepack/10
+11
+make -s12
+13
+BIN="$(cd ../bin && pwd)/forgepack" # absolute -- the script cd's into a scratch dir below14
+SCRATCH=$(mktemp -d)15
+trap 'rm -rf "$SCRATCH"' EXIT16
+17
+fail() { echo "FAIL: $1"; exit 1; }18
+19
+cd "$SCRATCH"20
+21
+"$BIN" init > /dev/null || fail "init"22
+[ -f ash.pkg ] || fail "ash.pkg not created"23
+grep -q "^name:" ash.pkg || fail "ash.pkg missing name"24
+25
+"$BIN" add github:octocat/Hello-World > /dev/null || fail "add"26
+grep -q "Hello-World: github:octocat/Hello-World" ash.pkg || fail "dependency not recorded"27
+[ -d @ash-modules/Hello-World ] || fail "dependency not installed"28
+29
+rm -rf @ash-modules30
+"$BIN" install > /dev/null || fail "install"31
+[ -d @ash-modules/Hello-World ] || fail "install did not restore the dependency"32
+33
+"$BIN" install > /dev/null || fail "re-install (idempotency)"34
+35
+"$BIN" list | grep -q "Hello-World" || fail "list didn't show the dependency"36
+37
+if "$BIN" bogus-command > /dev/null 2>&1; then fail "unknown command should exit non-zero"; fi38
+39
+echo "forgepack smoke test: OK"ideas/ash_modules.mdadded
+208−0@@ -0,0 +1,208 @@1
+# Ash Modules — a package manager for Ash2
+3
+> **TL;DR:** An npm-like package manager for Ash. Packages get fetched into a project-local `@ash-modules/` folder (same idea as `node_modules`). A manifest file (`ash.pkg`) declares dependencies and versions. `@import <name>;` — which already works for local `ash.libs/` folders — gets extended to also search `@ash-modules/`, so existing code and syntax don't change.4
+5
+---6
+7
+## Table of Contents8
+9
+1. [Goals & Non-Goals](#goals--non-goals)10
+2. [What Already Exists](#what-already-exists)11
+3. [The Manifest: `ash.pkg`](#the-manifest-ashpkg)12
+4. [`@ash-modules/` Layout](#ash-modules-layout)13
+5. [Resolution Order](#resolution-order)14
+6. [Versioning](#versioning)15
+7. [The Registry](#the-registry)16
+8. [CLI: `forgepack`](#cli-forgepack)17
+9. [Publishing a Package](#publishing-a-package)18
+10. [Development Phases](#development-phases)19
+11. [Design Principles](#design-principles)20
+12. [Risks & Open Questions](#risks--open-questions)21
+22
+---23
+24
+## Goals & Non-Goals25
+26
+**Goals**27
+28
+- `@import <name>;` keeps working exactly as it does today — a package is just something else `<name>` can resolve to.29
+- A manifest file per project declares what it depends on and at what version.30
+- Installed dependencies live in a project-local folder (`@ash-modules/`), never globally — two projects can depend on different versions of the same package with zero conflict.31
+- Works identically for `ashvm` and `kiln` — a package is plain `.ash` source, and both engines already share one `@import` syntax.32
+- Lockfile for reproducible installs.33
+34
+**Non-Goals**35
+36
+- A general-purpose build system (Ash programs are single-entry-point scripts; this is dependency resolution, not a `Makefile` replacement).37
+- Native/binary dependencies (a package is Ash source only — no compiled artifacts, no FFI, at least for v1).38
+- A hosted registry on day one (see [The Registry](#the-registry) — start file/git-based, add a real registry later).39
+- Semver solving across a huge transitive graph with conflicting ranges — start with a simple model (see [Versioning](#versioning)) and only grow it if real packages need it.40
+41
+---42
+43
+## What Already Exists44
+45
+Don't design this from scratch — `@import` already has most of the shape a package system needs:46
+47
+```ash48
+@import <./fixtures/helper.ash>; // relative path, must end in .ash49
+@import <testlib>; // bare name, no '.' or '/' allowed50
+```51
+52
+A bare name already resolves (`compiler/C/import_paths.c`, mirrored in `kiln`) to:53
+54
+```text55
+<project_root>/ash.libs/<name>/<name>.ash56
+```57
+58
+That's a real, working, local "library folder" convention — it's just not fetchable. Ash Modules reuses the exact same bare-name syntax and the exact same "no `.` or `/`" rule, and adds a *second* place that name is allowed to resolve to: `@ash-modules/<name>/`. `ash.libs/` keeps working unchanged for hand-written local libraries that never need a version or a registry.59
+60
+---61
+62
+## The Manifest: `ash.pkg`63
+64
+One file per project, at the project root (same level as `ash.libs/`), plain key-value text — not JSON, to match Ash's own "no external dependencies" ethos (no need to pull in a JSON parser just to read a manifest):65
+66
+```text67
+name: my-project68
+version: 0.1.069
+70
+[dependencies]71
+http: 2.1.072
+json: ^1.3.073
+left-pad: 1.0.074
+```75
+76
+- `[dependencies]` lists direct dependencies only — transitive ones are resolved and recorded in the lockfile, not hand-written here.77
+- A package itself has the same file at its own root (so a dependency can declare its *own* dependencies) — a package and a project are the same kind of thing, the only difference is whether anything depends on it.78
+- `name`/`version` double as what gets published to the registry (see below).79
+80
+---81
+82
+## `@ash-modules/` Layout83
+84
+```text85
+my-project/86
+├── ash.pkg87
+├── ash.lock88
+├── main.ash89
+├── ash.libs/ # unchanged — hand-written local libs, never fetched90
+│ └── mathutils/91
+│ └── mathutils.ash92
+└── @ash-modules/ # generated, gitignored, never hand-edited93
+ ├── http/94
+ │ ├── ash.pkg95
+ │ └── http.ash96
+ ├── json/97
+ │ ├── ash.pkg98
+ │ └── json.ash99
+ └── left-pad/100
+ ├── ash.pkg101
+ └── left-pad.ash102
+```103
+104
+- Flat, not nested — same lesson npm learned the hard way (npm 2's deeply nested `node_modules` was replaced by npm 3's flat layout). A dependency's *own* dependencies also land flat in the top-level `@ash-modules/`, deduped by version where possible.105
+- A version conflict (two dependencies need incompatible versions of the same package) gets a single nested fallback: `@ash-modules/<name>/@ash-modules/<name>@<version>/` — resolution always checks the nearest `@ash-modules/` first, same lookup order Node uses for `node_modules`.106
+- `@ash-modules/` is machine-generated and gitignored, like `O/`/`D/` already are for build output — `forgepack install` regenerates it from `ash.lock`.107
+108
+---109
+110
+## Resolution Order111
+112
+`@import <name>;` tries, in order, from the importing file's project root:113
+114
+1. `ash.libs/<name>/<name>.ash` — unchanged, hand-written local libraries win first (a project can always shadow a package name with its own local copy).115
+2. `@ash-modules/<name>/<name>.ash` — the installed package's entry file, named the same as the package itself (matches the existing `ash.libs` convention exactly, so the resolver code barely changes).116
+3. Walk up: if the current directory has no `@ash-modules/<name>/`, check the parent's, same as Node's `node_modules` walk — lets a package's own `@import`s resolve against the *project's* installed copies instead of needing its own nested set for every shared dependency.117
+118
+No new import syntax. The only change is `import_resolve_path()` gaining a second candidate path to try before giving up.119
+120
+---121
+122
+## Versioning123
+124
+Semver (`MAJOR.MINOR.PATCH`), kept deliberately simple for v1:125
+126
+- An exact version (`2.1.0`) always means exactly that version.127
+- A caret range (`^1.3.0`) means "`>=1.3.0`, `<2.0.0`" — the usual meaning, allows patch/minor updates, blocks breaking ones.128
+- No `~`, no `>=<`, no OR-ranges in v1 — if a real package needs more than caret ranges, add it then, not speculatively now.129
+- `ash.lock` pins every resolved package (direct and transitive) to one exact version and a content hash, so `forgepack install` is always reproducible — the manifest describes *intent*, the lockfile describes *what actually got installed*.130
+131
+---132
+133
+## The Registry134
+135
+Start without a hosted server at all — two sources, both file-based:136
+137
+1. **Git.** `forgepack add github:someone/some-package` clones a tag/commit straight into `@ash-modules/`. Zero infrastructure, works day one, same escape hatch cargo/npm/go modules all still support.138
+2. **A flat static index**, once there's more than a handful of packages: a single JSON (or plain-text) file mapping `name -> [versions] -> tarball URL`, served from anywhere static (GitHub Pages, a S3 bucket) — no backend service, no database, no auth to build first. `forgepack publish` is just "upload a tarball + update the index."139
+140
+A real registry with search, download counts, a web UI — all later, once there's something worth searching. Don't build npm's infrastructure for zero packages.141
+142
+---143
+144
+## CLI: `forgepack`145
+146
+```text147
+forgepack init # creates ash.pkg in the current directory148
+forgepack install # reads ash.pkg + ash.lock, populates @ash-modules/149
+forgepack add <name>[@version] # adds to ash.pkg, resolves, installs, updates ash.lock150
+forgepack add github:user/repo # git-sourced dependency151
+forgepack remove <name> # removes from ash.pkg, re-resolves, prunes @ash-modules/152
+forgepack update [<name>] # re-resolves within manifest ranges, rewrites ash.lock153
+forgepack publish # packages the current directory, pushes to the index154
+forgepack list # prints the resolved dependency tree155
+```156
+157
+Sample `list` output:158
+159
+```text160
+my-project@0.1.0161
+├── http@2.1.0162
+│ └── json@1.4.2163
+├── json@1.3.0 (direct — different version than http's own json dependency)164
+└── left-pad@1.0.0165
+```166
+167
+---168
+169
+## Publishing a Package170
+171
+A package is just a project with no `main` entry point expectation — `ash.pkg`'s `name`/`version`, a `<name>.ash` entry file at the root, optionally its own `ash.libs/`/dependencies. `forgepack publish`:172
+173
+1. Validates `ash.pkg` (name/version present, version not already published).174
+2. Tars up the directory (minus `@ash-modules/`, minus anything in a `.ashignore`).175
+3. Uploads the tarball and updates the index (or, for the git-only path, publishing *is* just tagging a commit — nothing to run).176
+177
+---178
+179
+## Development Phases180
+181
+| Phase | Goal | Details |182
+|-------|------|---------|183
+| **1. Manifest + local install** | Prove the shape works | `ash.pkg` parsing, `@ash-modules/` resolution added to `import_resolve_path()`, `forgepack install` reading only git/local-path sources (no index yet). |184
+| **2. Lockfile** | Reproducible installs | `ash.lock` generation, content hashing, `forgepack install` becoming deterministic from the lockfile alone. |185
+| **3. Static index** | Discoverability without a server | Flat JSON index, `forgepack publish`/`add <name>` against it, version resolution against caret ranges. |186
+| **4. Version conflict handling** | Real-world dependency graphs | Nested fallback resolution, `forgepack list` tree view, dedup pass. |187
+| **5. Registry service** *(maybe, later)* | Search, stats, web UI | Only once there's enough real packages to justify it. |188
+189
+---190
+191
+## Design Principles192
+193
+1. **`@import` doesn't change.** A package is just another thing a bare name can resolve to — no new syntax, no special "package import" keyword.194
+2. **Project-local, never global.** No version conflicts between unrelated projects, no `sudo` to install a package, matches how `@ash-modules/` already sits next to `ash.libs/`.195
+3. **The manifest describes intent; the lockfile describes reality.** Never let `install` silently drift from what got reviewed/committed.196
+4. **No hosted infrastructure until it's earned.** Git and a static file are a real registry for a first ecosystem; a server is a later problem.197
+5. **Packages are plain Ash.** No native code, no FFI, no build step inside a package — anything that can `@import` a package can also just read it.198
+199
+---200
+201
+## Risks & Open Questions202
+203
+- **Name squatting / collisions** once there's a shared static index — needs *some* claiming rule before the index is writable by anyone.204
+- **Git dependency pinning:** a tag can move, a branch definitely does. `ash.lock` should probably pin a commit hash for git sources, not just a tag name.205
+- **Circular package dependencies:** `ashvm`'s `@import` already handles circular *file* imports (see `imports_circular_*` tests) — need to confirm that still holds once "file" becomes "a file inside someone else's installed package."206
+- **Namespace clashes with `ash.libs/`:** a project with a local `ash.libs/http/` and a dependency also named `http` — local wins per the resolution order above, but should probably at least warn.207
+- **Removing packages:** `forgepack remove` pruning `@ash-modules/` safely needs to know what's still a transitive dependency of something else first.208
+- **No native deps today — forever?** If a package ever genuinely needs to call into C (e.g. for something `kiln` can't do in pure Ash), there's no story for that yet. Worth deciding explicitly rather than backing into it.ideas/gpu_acceleration.mdadded
+447−0@@ -0,0 +1,447 @@1
+# Ash GPU Acceleration2
+3
+> **TL;DR:** Ash stays a simple language. Kiln stays the native compiler. A **separate project (`ash-gpu`)** handles GPU work and picks CPU, SIMD, or GPU automatically. No GPU? Program still runs on CPU.4
+5
+---6
+7
+## Table of Contents8
+9
+1. [Goals & Non-Goals](#goals--non-goals)10
+2. [Why GPUs](#why-gpus)11
+3. [Architecture](#architecture)12
+4. [Components](#components)13
+5. [Dispatch: When to Use the GPU](#dispatch-when-to-use-the-gpu)14
+6. [Memory & Kernel Fusion](#memory--kernel-fusion)15
+7. [Language Surface](#language-surface)16
+8. [Standard Library Acceleration](#standard-library-acceleration)17
+9. [Tooling](#tooling)18
+10. [Development Phases](#development-phases)19
+11. [Design Principles](#design-principles)20
+12. [Risks & Open Questions](#risks--open-questions)21
+22
+---23
+24
+## Goals & Non-Goals25
+26
+**Goals**27
+28
+- Ordinary Ash code benefits from GPU hardware without the programmer managing kernels.29
+- Works on AMD, NVIDIA, and Intel. No vendor lock-in.30
+- Every program runs without a GPU (CPU fallback).31
+- Core language and compiler stay small.32
+33
+**Non-Goals**34
+35
+- Turning `ashvm` or Kiln into a GPU framework.36
+- Making Ash a "GPU language" with GPU syntax in everyday code.37
+- Moving individual scalar math to the GPU (see [below](#do-not-gpu-every-operation)).38
+- Multi-GPU support in the first versions.39
+40
+---41
+42
+## Why GPUs43
+44
+CPUs (e.g. Ryzen 7 7800X3D) are great at branching, sequential work, and low latency.45
+GPUs (e.g. RX 9070 XT) are built to run thousands of similar operations at once.46
+47
+```text48
+CPU: work 1 → work 2 → work 3 → work 4 → ...49
+50
+GPU: work 1 ─┐51
+ work 2 ─┤52
+ work 3 ─┼──> thousands in parallel53
+ work 4 ─┤54
+ work 5 ─┘55
+```56
+57
+**Good fits:** array/matrix ops, image processing, simulations, signal processing, physics, procedural generation, fractals, particle systems, large sorts/reductions/searches, ML workloads, parallel data processing.58
+59
+**Catch:** launching GPU work and moving data CPU ↔ GPU both cost time. So the system must *decide* whether the GPU is worth it, not assume it.60
+61
+---62
+63
+## Architecture64
+65
+```text66
+ Ash Program67
+ │68
+ ▼69
+ Kiln / Ash IR70
+ │71
+ ▼72
+ Optimization Layer73
+ │74
+ ┌──────────┴──────────┐75
+ ▼ ▼76
+ CPU path GPU path77
+ (native / SIMD) (ash-gpu runtime)78
+ │79
+ ┌────────────┼────────────┐80
+ ▼ ▼ ▼81
+ Vulkan HIP CUDA82
+ (all GPUs) (AMD) (NVIDIA)83
+```84
+85
+**Responsibilities**86
+87
+| Layer | Job |88
+|-------|-----|89
+| **Ash** | Describes *what* the programmer wants |90
+| **Kiln** | Turns it into efficient native code |91
+| **ash-gpu** | Hardware-aware acceleration for heavy compute |92
+93
+Kiln talks to `ash-gpu` through a **small C ABI** and never needs to know backend internals.94
+95
+---96
+97
+## Components98
+99
+Proposed layout:100
+101
+```text102
+ash-gpu/103
+├── include/104
+│ └── ash_gpu.h # public C ABI105
+├── runtime/ # device, memory, dispatch, scheduler (C++)106
+├── backends/ # vulkan, hip, cuda, cpu107
+├── kernels/ # vector, matrix, reduction, sorting, image108
+├── bindings/ash/ # Ash-facing API109
+├── tools/ # info, benchmark, profile, doctor110
+├── benchmarks/111
+└── tests/112
+```113
+114
+### 1. Hardware Detection115
+116
+Detect what's available and report it:117
+118
+```text119
+CPU: AMD Ryzen 7 7800X3D120
+GPU: AMD Radeon RX 9070 XT121
+VRAM: 16 GB122
+Compute: available123
+```124
+125
+Collected info: vendor, model, VRAM, compute units, supported APIs, precision formats, features, driver capabilities. No suitable GPU → everything falls back to CPU.126
+127
+### 2. Backends128
+129
+Never tie Ash itself to one API.130
+131
+| Backend | Notes |132
+|---------|-------|133
+| **Vulkan Compute** | Cross-vendor (AMD/NVIDIA/Intel), mature, explicit control. Best default. |134
+| **HIP** | CUDA-like model, strong on AMD |135
+| **CUDA** | Highly optimized NVIDIA path |136
+| **DirectCompute** | Possible Windows-specific option |137
+| **CPU** | Always-present fallback |138
+139
+### 3. Optimized Kernels140
+141
+```text142
+kernels/143
+├── arithmetic/ vector_add, vector_mul, vector_fma144
+├── vector/ vector_sin, vector_cos145
+├── matrix/ matrix_mul146
+├── reduction/ reduce_sum, reduce_min, reduce_max147
+├── sorting/ sort, prefix_sum148
+├── image/149
+├── signal/150
+└── simulation/151
+```152
+153
+Written in whatever gives the best performance: C++, HIP, CUDA, Vulkan/SPIR-V shaders. The goal is speed, not writing everything in Ash.154
+155
+### 4. Why C++ for the Runtime156
+157
+- RAII for GPU resources158
+- Cleaner device/backend abstractions159
+- Better ecosystem support160
+- Easier async management161
+162
+Kiln stays in C. The boundary is a plain C ABI:163
+164
+```c165
+bool ash_gpu_available(void);166
+void *ash_gpu_alloc(size_t bytes);167
+void ash_gpu_free(void *buf);168
+int ash_gpu_upload(void *dst, const void *src, size_t bytes);169
+int ash_gpu_download(void *dst, const void *src, size_t bytes);170
+int ash_gpu_dispatch(/* kernel, args, size */);171
+```172
+173
+```text174
+Ash → C ABI → C++ runtime → Vulkan / HIP / CUDA / DirectX175
+```176
+177
+### 5. Async Execution178
+179
+GPUs are async by nature. Don't block unless you must:180
+181
+```text182
+CPU ──► launch GPU work183
+ ──► keep doing CPU work184
+ ──► launch more GPU work185
+ ──► sync only when the result is needed186
+```187
+188
+### 6. Multiple GPUs *(future)*189
+190
+Split big workloads across devices, e.g. 100M elements → 60M on GPU 0, 40M on GPU 1. Not an initial requirement.191
+192
+---193
+194
+## Dispatch: When to Use the GPU195
+196
+The runtime picks the best implementation per operation:197
+198
+```text199
+operation200
+ │201
+ ├─ small workload? → CPU202
+ ├─ medium workload? → SIMD CPU203
+ └─ huge workload? → GPU204
+```205
+206
+| Example | Elements | Choice |207
+|---------|----------|--------|208
+| vector multiply | 10 | CPU |209
+| vector multiply | 1,000 | CPU / SIMD |210
+| vector multiply | 128 | CPU SIMD |211
+| vector multiply | 10,000,000+ | GPU |212
+213
+Thresholds depend on: operation type, element count, memory size, GPU characteristics, transfer cost, CPU SIMD capability, and **previous benchmark data**.214
+215
+### Do Not GPU Every Operation216
+217
+```ash218
+// BAD: overhead dwarfs the work219
+let x = gpu 10 * 20;220
+221
+// GOOD: big batch, GPU can shine222
+for i in 0..100000000 {223
+ output[i] = input[i] * 20;224
+}225
+```226
+227
+Optimize **workloads**, not individual instructions.228
+229
+---230
+231
+## Memory & Kernel Fusion232
+233
+### Keep data on the GPU234
+235
+Bad (ping-pong):236
+237
+```text238
+CPU → GPU → CPU → GPU → CPU239
+```240
+241
+Good (upload once, download once):242
+243
+```text244
+CPU ─upload─► GPU memory ─► multiply ─► add ─► sin ─► normalize ─download─► CPU245
+```246
+247
+### Kernel fusion248
+249
+Instead of three kernels:250
+251
+```ash252
+a = b * 2253
+c = a + 10254
+d = sin(c)255
+```256
+257
+generate one:258
+259
+```text260
+d[i] = sin(b[i] * 2 + 10)261
+```262
+263
+Saves: kernel launch overhead, intermediate writes, intermediate reads, synchronization.264
+265
+---266
+267
+## Language Surface268
+269
+**Primary goal: automatic.** Plain Ash code gets optimized.270
+271
+```ash272
+let a = huge_array();273
+let b = huge_array();274
+275
+for i in 0..a.length {276
+ a[i] = a[i] * b[i] + 42;277
+}278
+```279
+280
+The toolchain recognizes this as a highly parallel map.281
+282
+### Vectorized operations283
+284
+High-level ops that are easy to analyze:285
+286
+```ash287
+a = a * 2288
+c = a + b289
+result = sin(values)290
+```291
+292
+Internally: `parallel map → parallel map → parallel map` → fusable into one pipeline.293
+294
+### Explicit control *(advanced users, later)*295
+296
+Syntax TBD. Possibilities:297
+298
+```ash299
+parallel {300
+ for i in 0..values.length {301
+ values[i] = values[i] * 2;302
+ }303
+}304
+305
+gpu {306
+ for i in 0..values.length {307
+ output[i] = input[i] * input[i];308
+ }309
+}310
+```311
+312
+| Mode | Behavior |313
+|------|----------|314
+| Normal Ash | Automatic optimization |315
+| `parallel` / `gpu` blocks | Programmer explicitly requests acceleration |316
+317
+---318
+319
+## Standard Library Acceleration320
+321
+Likely the biggest practical win. Users call normal APIs; the implementation picks CPU / SIMD / GPU.322
+323
+```text324
+array.sort() array.sum() array.min() array.max()325
+array.map() array.filter()326
+matrix.multiply()327
+image.resize() image.blur()328
+```329
+330
+### Example331
+332
+```ash333
+let pixels = image.load("image.png");334
+335
+pixels = pixels.blur();336
+pixels = pixels.color_correct();337
+pixels = pixels.resize(3840, 2160);338
+339
+image.save(pixels, "output.png");340
+```341
+342
+| Step | Runs on |343
+|------|---------|344
+| `image.load` | CPU |345
+| `blur` | GPU |346
+| `color_correct` | GPU |347
+| `resize` | GPU |348
+| `image.save` | CPU |349
+350
+The whole middle pipeline stays resident in GPU memory: one upload, one download.351
+352
+---353
+354
+## Tooling355
+356
+```text357
+ash-gpu info # detected devices, backend, VRAM, status358
+ash-gpu benchmark # measure CPU vs SIMD vs GPU359
+ash-gpu kernels # inspect available kernels360
+ash-gpu doctor # diagnose driver/setup problems361
+ash-gpu compile # compile GPU kernels362
+ash-gpu profile program.ash363
+```364
+365
+Sample `info` output:366
+367
+```text368
+Ash GPU Runtime369
+370
+Backend: Vulkan371
+Device: AMD Radeon RX 9070 XT372
+VRAM: 16 GB373
+Status: Available374
+```375
+376
+Sample `profile` output:377
+378
+```text379
+Ash GPU Profile380
+381
+CPU time: 421 ms382
+GPU time: 83 ms383
+GPU transfer time: 19 ms384
+Kernel launch time: 3 ms385
+GPU utilization: 91%386
+387
+Top kernels:388
+ vector_mul 31 ms389
+ matrix_mul 24 ms390
+ sin_kernel 17 ms391
+ reduction 8 ms392
+```393
+394
+The profiler answers the key question: **is the GPU actually helping?**395
+396
+---397
+398
+## Development Phases399
+400
+| Phase | Goal | Details |401
+|-------|------|---------|402
+| **1. Standalone runtime** | Prove the GPU path works | Detection, one backend, alloc, upload/download, kernel execution, CPU fallback, benchmarks. **Don't touch Ash yet.** |403
+| **2. Primitive library** | Fast building blocks | Vector math, matrix math, reductions, sorting, image processing. Benchmark-driven. |404
+| **3. Ash bindings** | Let users experiment | Small API like `gpu.available()`, `gpu.array(...)`. No compiler changes. |405
+| **4. Kiln integration** | Compiler awareness | Only after the runtime is solid. Kiln emits calls to `ash-gpu` for suitable workloads. |406
+| **5. Automatic acceleration** | The real payoff | Plain `for` loops over large arrays become GPU workloads automatically. |407
+408
+---409
+410
+## Design Principles411
+412
+1. **GPU is optional.** No GPU means "use the best CPU implementation", never "can't run".413
+2. **Never assume the GPU is faster.** Only use it when the workload justifies it.414
+3. **Minimize transfers.** Keep data on the GPU across chained operations.415
+4. **Optimize whole workloads**, not single arithmetic instructions.416
+5. **Keep Ash simple.** GPU complexity lives outside the language.417
+6. **Keep Kiln maintainable.** No giant GPU framework inside the compiler.418
+7. **Benchmark everything.** Measurements over assumptions.419
+420
+---421
+422
+## Risks & Open Questions423
+424
+- **Floating-point consistency:** GPU and CPU results may differ slightly. Define what Ash guarantees.425
+- **Detecting parallel-safe loops:** Aliasing and loop-carried dependencies make auto-parallelization hard. Start with a restricted, provably safe subset.426
+- **Shipping binaries:** A program compiled on a GPU machine runs on a laptop. Needs runtime detection (not compile-time), plus bundled or JIT-compiled kernels.427
+- **Driver/backend variance:** Vulkan compute behavior differs across vendors. Needs a solid test matrix.428
+- **Dispatch accuracy:** Heuristics may misjudge. Benchmark data / calibration at install time could help.429
+- **Explicit syntax:** `parallel {}` vs `gpu {}` vs a library API. Decide later.430
+- **Error handling:** What happens on GPU out-of-memory or device loss mid-run? Should fall back to CPU transparently.431
+432
+---433
+434
+## The Big Idea435
+436
+```text437
+Ash → simple high-level programs438
+Kiln → optimized native execution439
+Ash GPU → hardware-aware acceleration440
+ │441
+ ▼442
+ CPU / SIMD / GPU443
+```444
+445
+Ash doesn't become a GPU language. Kiln doesn't become a GPU compiler framework. A normal Ash program can get dramatically faster on machines with capable GPUs and still run fine on CPU-only machines.446
+447
+GPU acceleration becomes an **implementation detail of the ecosystem**, not something every Ash programmer has to think about.\ No newline at end of filekiln/Makefilemodified
+6−3@@ -14,13 +14,14 @@ KILN_SRC = \1414
src/parser/C/declarations/vars.c \1515
src/parser/C/imports/import_paths.c \1616
src/parser/C/imports/imports.c \17
- src/parser/C/statements/for_loop.c \17
+ src/parser/C/statements/each_loop.c \1818
src/parser/C/statements/loop_stack.c \1919
src/parser/C/statements/parser_control.c \20
- src/parser/C/statements/try_catch.c \20
+ src/parser/C/statements/attempt_handle.c \2121
src/codegen/C/builtins/builtins.c \2222
src/codegen/C/builtins/filter_builtin.c \2323
src/codegen/C/builtins/map_builtin.c \24
+ src/codegen/C/builtins/type_builtin.c \2425
src/codegen/C/builtins/math_builtins.c \2526
src/codegen/C/builtins/num_builtin.c \2627
src/codegen/C/builtins/reduce_builtin.c \@@ -132,4 +133,6 @@ $(KILNRT_DLL): $(BIN)132133
133134
runtime-dll: $(KILNRT_DLL)134135
135
-.PHONY: all clean install-runtime runtime-dll136
+.PHONY: all clean rebuild install-runtime runtime-dll137
+138
+rebuild: clean all\ No newline at end of filekiln/README.mdmodified
+3−3@@ -13,7 +13,7 @@ directly, the same "every line hand-written" rule that built `ashc` and1313
1414
## Status: milestone 1 done (Linux only)1515
16
-`kiln file.ash -o out` compiles `print <expr>;` programs (integer literals,16
+`kiln file.ash -o out` compiles `say <expr>;` programs (integer literals,1717
`+ - * /`, unary `-`, parens, multiple statements) straight to a real,1818
standalone, freestanding ELF64 executable - no libc, raw syscalls only.1919
@@ -25,7 +25,7 @@ cd kiln && make # builds ../bin/kiln2525
2626
## What's deliberately not built yet2727
28
-- Everything beyond numeric `print` statements - variables, functions,28
+- Everything beyond numeric `say` statements - variables, functions,2929
strings, arrays, control flow, closures3030
- Multi-object linking (the whole program is one self-contained object3131
right now; there's no symbol-resolution step yet)@@ -46,7 +46,7 @@ kiln/4646
lexer/ tokenizer (own copy, same shape as ashvm's)4747
parser/C/ H/ D/ O/4848
core/ parser entry, shared parser state49
- statements/ control flow, for loops, try/catch, loop stack49
+ statements/ control flow, each loops, attempt/handle, loop stack5050
declarations/ variables, functions, constants5151
imports/ import resolution5252
codegen/C/ H/ D/ O/ (same feature folders in each tree)kiln/src/codegen/C/builtins/builtins.cmodified
+7−0@@ -11,6 +11,7 @@1111
#include "codegen/H/io/file_builtins.h"1212
#include "codegen/H/io/input_builtin.h"1313
#include "codegen/H/io/argv_builtin.h"14
+#include "codegen/H/builtins/type_builtin.h"1415
#include "parser/H/core/parser.h"1516
1617
int codegen_try_builtin_call(const char *name, int len) {@@ -166,5 +167,11 @@ int codegen_try_builtin_call(const char *name, int len) {166167
codegen_builtin_delete();167168
return 1;168169
}170
+ if (len == 4 && strncmp(name, "type", 4) == 0) {171
+ codegen_expression();172
+ expect(TOKEN_RPAREN, "expected ')' after argument");173
+ codegen_builtin_type();174
+ return 1;175
+ }169176
return 0;170177
}kiln/src/codegen/C/builtins/type_builtin.cadded
+57−0@@ -0,0 +1,57 @@1
+#include "codegen/H/builtins/type_builtin.h"2
+#include "codegen/H/strings/strings.h"3
+#include "codegen/H/emit/emit_internal.h"4
+#include "codegen/H/emit/value.h"5
+#include "parser/H/core/parser.h"6
+7
+// Pops (tag, payload), pushes one of six string literals depending on8
+// the tag -- TAG_FUNCTION covers both a plain named function and a9
+// closure, matching value.h's documented "capture_count=0 is just a10
+// zero-capture closure" shape (kiln has no separate closure tag the way11
+// ashvm's VM_CLOSURE vs. VM_FUNCTION does, so there's only one string to12
+// return either way). The argument itself is already parsed and pushed13
+// by the caller (codegen_try_builtin_call), same convention every other14
+// single-argument builtin here uses (upper/lower/trim/str/num/...).15
+void codegen_builtin_type(void) {16
+ emit_pop_reg(code, REG_RAX); // payload (unused -- only the tag matters)17
+ emit_pop_reg(code, REG_RBX); // tag18
+19
+ int jumps[5];20
+ int n = 0;21
+22
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_NUMBER);23
+ int not_number = emit_jcc_rel32(code, COND_NE);24
+ codegen_string_literal_bytes("number", 6);25
+ jumps[n++] = emit_jmp_rel32(code);26
+ emit_patch_jump(code, not_number);27
+28
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);29
+ int not_string = emit_jcc_rel32(code, COND_NE);30
+ codegen_string_literal_bytes("string", 6);31
+ jumps[n++] = emit_jmp_rel32(code);32
+ emit_patch_jump(code, not_string);33
+34
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_ARRAY);35
+ int not_array = emit_jcc_rel32(code, COND_NE);36
+ codegen_string_literal_bytes("array", 5);37
+ jumps[n++] = emit_jmp_rel32(code);38
+ emit_patch_jump(code, not_array);39
+40
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_MAP);41
+ int not_map = emit_jcc_rel32(code, COND_NE);42
+ codegen_string_literal_bytes("map", 3);43
+ jumps[n++] = emit_jmp_rel32(code);44
+ emit_patch_jump(code, not_map);45
+46
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_FUNCTION);47
+ int not_function = emit_jcc_rel32(code, COND_NE);48
+ codegen_string_literal_bytes("function", 8);49
+ jumps[n++] = emit_jmp_rel32(code);50
+ emit_patch_jump(code, not_function);51
+52
+ // TAG_NIL, or anything else -- "none" is the only tag left in53
+ // practice, so this is both the nil case and the safety net.54
+ codegen_string_literal_bytes("none", 4);55
+56
+ for (int i = 0; i < n; i++) emit_patch_jump(code, jumps[i]);57
+}kiln/src/codegen/C/collections/arrays.cmodified
+16−3@@ -189,10 +189,23 @@ void codegen_builtin_push(void) {189189
emit_push_reg(code, REG_RSI); // push() returns the array itself, matching ashvm190190
}191191
192
+// Bug fix (found by a cross-engine ashvm/kiln parity audit): this used to193
+// unconditionally read offset +8 as "count", which is correct for ARRAY194
+// and MAP (both share the same capacity/count/data-or-entries-ptr object195
+// shape, see emit/value.h) but wrong for STRING -- a string's length lives196
+// at offset -8 from its payload (the length prefix), not +8. Calling197
+// len() on a string used to read whatever garbage happened to sit 8 bytes198
+// into the string's own byte content.192199
void codegen_builtin_len(void) {193
- emit_pop_reg(code, REG_RAX); // payload (array object address)194
- emit_pop_reg(code, REG_RBX); // tag (ignored, assumed ARRAY)195
- emit_load_mem_disp32(code, REG_RAX, REG_RAX, 8); // count200
+ emit_pop_reg(code, REG_RAX); // payload201
+ emit_pop_reg(code, REG_RBX); // tag202
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);203
+ int not_string = emit_jcc_rel32(code, COND_NE);204
+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, -8); // string length prefix205
+ int done = emit_jmp_rel32(code);206
+ emit_patch_jump(code, not_string);207
+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 8); // array/map count208
+ emit_patch_jump(code, done);196209
emit_cvtsi2sd(code, XMM0, REG_RAX);197210
emit_movq_reg_from_xmm(code, REG_RAX, XMM0);198211
emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);kiln/src/codegen/C/expressions/expr.cmodified
+23−8@@ -68,12 +68,12 @@ static void primary(void) {6868
codegen_string_literal();6969
return;7070
}71
- if (current.type == TOKEN_FN) {71
+ if (current.type == TOKEN_FORGE) {7272
codegen_lambda_expr();7373
return;7474
}75
- if (current.type == TOKEN_TRUE || current.type == TOKEN_FALSE) {76
- double v = current.type == TOKEN_TRUE ? 1.0 : 0.0;75
+ if (current.type == TOKEN_YES || current.type == TOKEN_NO) {76
+ double v = current.type == TOKEN_YES ? 1.0 : 0.0;7777
advance_token();7878
uint64_t bits;7979
memcpy(&bits, &v, sizeof(bits));@@ -83,7 +83,7 @@ static void primary(void) {8383
emit_push_reg(code, REG_RAX);8484
return;8585
}86
- if (current.type == TOKEN_NIL) {86
+ if (current.type == TOKEN_NONE) {8787
advance_token();8888
emit_mov_reg_imm64(code, REG_RBX, TAG_NIL);8989
emit_push_reg(code, REG_RBX);@@ -139,7 +139,7 @@ static void primary(void) {139139
}140140
// Not a function either -- if we're compiling inside an imported141141
// file's own namespace context, a bare reference may be that same142
- // file's own top-level constant: let_statement always registers142
+ // file's own top-level constant: local_statement always registers143143
// imported `let`s under their namespaced name (e.g. "lib.PI"),144144
// even for references from within lib.ash itself, so a bare "PI"145145
// used inside lib.ash needs this same "in context" namespacing@@ -353,7 +353,7 @@ static void power(void) {353353
354354
static void term(void) {355355
unary();356
- while (current.type == TOKEN_STAR || current.type == TOKEN_SLASH) {356
+ while (current.type == TOKEN_STAR || current.type == TOKEN_SLASH || current.type == TOKEN_PERCENT) {357357
TokenType op = current.type;358358
advance_token();359359
unary();@@ -363,8 +363,23 @@ static void term(void) {363363
emit_pop_reg(code, REG_RBX); // a tag (assumed NUMBER)364364
emit_movq_xmm_from_reg(code, XMM0, REG_RAX);365365
emit_movq_xmm_from_reg(code, XMM1, REG_RCX);366
- if (op == TOKEN_STAR) emit_mulsd(code, XMM0, XMM1);367
- else emit_divsd(code, XMM0, XMM1);366
+ if (op == TOKEN_STAR) {367
+ emit_mulsd(code, XMM0, XMM1);368
+ } else if (op == TOKEN_SLASH) {369
+ emit_divsd(code, XMM0, XMM1);370
+ } else {371
+ // a % b = a - trunc(a / b) * b (matches ashvm's C fmod(): result372
+ // takes the dividend's sign, truncating toward zero rather than373
+ // flooring -- e.g. -7 % 3 == -1, not 2).374
+ emit_movsd_xmm_xmm(code, XMM2, XMM0); // XMM2 = a375
+ emit_divsd(code, XMM0, XMM1); // XMM0 = a / b376
+ emit_cvttsd2si(code, REG_RAX, XMM0); // truncate toward zero377
+ emit_cvtsi2sd(code, XMM0, REG_RAX); // XMM0 = trunc(a / b)378
+ emit_mulsd(code, XMM0, XMM1); // XMM0 = trunc(a / b) * b379
+ emit_movsd_xmm_xmm(code, XMM1, XMM0); // XMM1 = trunc(a / b) * b380
+ emit_movsd_xmm_xmm(code, XMM0, XMM2); // XMM0 = a381
+ emit_subsd(code, XMM0, XMM1); // XMM0 = a - trunc(a / b) * b382
+ }368383
emit_movq_reg_from_xmm(code, REG_RAX, XMM0);369384
emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);370385
emit_push_reg(code, REG_RBX);kiln/src/codegen/C/functions/lambda.cmodified
+9−9@@ -9,19 +9,19 @@99
#include "parser/H/core/parser_internal.h"1010
#include "parser/H/declarations/vars.h"1111
#include "parser/H/declarations/functions.h"12
-#include "parser/H/statements/for_loop.h"12
+#include "parser/H/statements/each_loop.h"1313
#include "parser/H/statements/loop_stack.h"1414
15
-// `fn (params) { body }` as an expression. The closure-object creation15
+// `forge (params) { body }` as an expression. The closure-object creation1616
// code (which runs in the OUTER frame, snapshotting captures by value)1717
// is emitted first, then a jump-over, then the body -- same shape as18
-// parser_control.c's fn_statement, generalized with a capture region18
+// parser_control.c's forge_statement, generalized with a capture region1919
// ahead of the parameter region. The object's code address isn't known2020
// until after the body compiles, so it's written via a patchable imm642121
// (see emit_mov_reg_imm64_patchable) and patched once the body's end is2222
// reached.2323
void codegen_lambda_expr(void) {24
- advance_token(); // consume 'fn'24
+ advance_token(); // consume 'forge'2525
2626
VarScope outer_vars;2727
vars_save(&outer_vars);@@ -43,17 +43,17 @@ void codegen_lambda_expr(void) {4343
int lambda_code_offset = code->count;4444
4545
vars_clear();46
- int outer_for_depth;47
- for_depth_save(&outer_for_depth);48
- for_depth_reset();46
+ int outer_each_depth;47
+ each_depth_save(&outer_each_depth);48
+ each_depth_reset();4949
int outer_loop_depth;5050
loop_depth_save(&outer_loop_depth);5151
loop_depth_reset();5252
for (int i = 0; i < capture_count; i++) {5353
declare_var(outer_vars.names[i], (int)strlen(outer_vars.names[i]));5454
}5555
56
- expect(TOKEN_LPAREN, "expected '(' after 'fn'");56
+ expect(TOKEN_LPAREN, "expected '(' after 'forge'");5757
char param_names[MAX_KILN_PARAMS][64];5858
int param_lens[MAX_KILN_PARAMS];5959
int argc = 0;@@ -102,7 +102,7 @@ void codegen_lambda_expr(void) {102102
emit_ret(code);103103
104104
vars_restore(&outer_vars);105
- for_depth_restore(outer_for_depth);105
+ each_depth_restore(outer_each_depth);106106
loop_depth_restore(outer_loop_depth);107107
emit_patch_jump(code, skip_jump);108108
kiln/src/codegen/C/runtime/errors.cmodified
+5−5@@ -36,12 +36,12 @@ static void embed_message(CodeBuf *code, const char *msg, uint64_t *out_addr, in3636
*out_len = len;3737
}3838
39
-// Raise routine -- inputs RSI=message addr, RDX=message len. If a `try`39
+// Raise routine -- inputs RSI=message addr, RDX=message len. If an `attempt`4040
// is active (kiln_try_depth_addr() > 0), pops the innermost handler,4141
// restores its saved RSP/RBP (kiln's hand-rolled longjmp -- no libc4242
// setjmp in a freestanding binary), stores the message as a TAG_STRING4343
// value into the handler's recorded error-variable slot, and jumps to44
-// its catch block. Otherwise falls through to an inline fatal path44
+// its handle block. Otherwise falls through to an inline fatal path4545
// (write to stderr, exit 1).4646
static void emit_raise_routine(CodeBuf *code) {4747
int skip = emit_jmp_rel32(code);@@ -63,7 +63,7 @@ static void emit_raise_routine(CodeBuf *code) {6363
6464
emit_load_mem_disp32(code, REG_RCX, REG_RAX, 0); // saved rsp6565
emit_load_mem_disp32(code, REG_RDI, REG_RAX, 8); // saved rbp66
- emit_load_mem_disp32(code, REG_RDX, REG_RAX, 16); // catch target (clobbers RDX -- fine, msg len only needed on the fatal branch we didn't take)66
+ 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)6767
emit_load_mem_disp32(code, REG_RBX, REG_RAX, 24); // error variable's rbp-relative tag offset6868
6969
emit_mov_reg_reg(code, REG_RSP, REG_RCX);@@ -80,7 +80,7 @@ static void emit_raise_routine(CodeBuf *code) {8080
emit_patch_jump(code, go_fatal);8181
// RSI/RDX still hold this call's original message addr/len -- nothing8282
// between the raise routine's entry and here touches them on the path83
- // that skips straight past the try-active handling.83
+ // that skips straight past the attempt-active handling.8484
platform_emit_write_stderr(code);8585
emit_sub_reg_imm8(code, REG_RSP, 8);8686
emit_mov_reg_reg(code, REG_RCX, REG_RSP);@@ -131,7 +131,7 @@ void errors_emit_die(CodeBuf *code, const char *msg) {131131
// The ONE case that must never be "catchable" (exceeding132132
// MAX_KILN_TRY_DEPTH itself -- silently continuing there would mean133133
// pushing a handler past the fixed table, corrupting the adjacent code134
-// segment). Only ever called from one site (parser/C/statements/try_catch.c), so134
+// segment). Only ever called from one site (parser/C/statements/attempt_handle.c), so135135
// there's no deduplication payoff in routing it through the shared136136
// runtime at all -- it just emits its own small inline write+exit137137
// sequence directly, in every link mode, decoupled from wherever RAISEkiln/src/codegen/C/runtime/print_int.cmodified
+357−93@@ -20,91 +20,77 @@ static void load_double_const(CodeBuf *code, XReg dst, double v) {2020
emit_movq_xmm_from_reg(code, dst, REG_RAX);2121
}2222
23
-// Scratch layout, all within one `sub rsp, 40` region:24
-// [rsp, rsp+32) integer digits (+ optional '-'), built BACKWARD --25
-// same technique milestone 1 used, RCX decrements26
-// before each store so no reversal step is needed.27
-// rsp+32 '.' (only meaningful if frac_len > 0)28
-// [rsp+33, rsp+39) up to 6 fraction digits, built FORWARD29
-// dynamic '\n', placed right after whichever of the above30
-// actually got used, so the whole thing writes in ONE31
-// contiguous write(2) call regardless of whether this32
-// number turned out to be whole or fractional.33
-// RBX holds the sign flag through phase 3, then gets reused as frac_len34
-// (how many of the 6 fraction digits are significant, i.e. up to the35
-// last nonzero one) -- safe, sign has already been written to the36
-// buffer by the time phase 4 starts.37
-// Dispatches on the popped value's tag: a STRING just gets written38
-// directly (it's already bytes -- no digit conversion needed at all,39
-// unlike a number); everything below this point is the NUMBER path,40
-// unchanged in shape from milestone 4 except that it now starts from an41
-// already-popped payload (in RAX) instead of popping XMM0 itself.42
-//43
-// Plan B, phase B1: emitted once (jumped over), called via emit_call_back44
-// from codegen_print_top below -- this is the single biggest deduplication45
-// win of B1, since this routine used to be re-emitted in full at EVERY46
-// print statement in the source (dwarfing heap_emit_alloc/bytes_emit_copy,47
-// which are individually smaller). Takes its value in RAX=payload,48
-// RBX=tag (NOT by popping them itself): a real CALL pushes a return49
-// address on top of whatever the caller already pushed, so a `pop` here50
-// would grab the return address instead of the caller's value -- same51
-// register-based-argument reasoning codegen/C/runtime/heap.c's alloc routine52
-// already uses, not something print specifically needed before this53
-// became a real subroutine. Each of its three paths (string/nil/number)54
-// still balances its own RSP usage back to the entry value, which a real55
-// subroutine does need.56
-static int print_routine_offset = -1;57
-58
-static void emit_print_routine(CodeBuf *code) {59
- int routine_skip = emit_jmp_rel32(code);60
- print_routine_offset = code->count;61
- emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);62
- int is_number = emit_jcc_rel32(code, COND_NE);23
+// Embeds a fixed byte sequence directly in the code stream (jumped over24
+// at runtime, never executed) and writes it to stdout -- used for the25
+// small fixed strings ("none", "<function>") that don't need per-call26
+// digit/content computation. Position-(in)dependent addressing matches27
+// the technique the NIL case already used before this file grew a28
+// second caller for it (see the KILN_TARGET_LINUX/so comment below).29
+static int emit_literal(CodeBuf *code, const char *s, int len) {30
+ int skip = emit_jmp_rel32(code);31
+ int data_offset = code->count;32
+ for (int i = 0; i < len; i++) emit_byte(code, (uint8_t)s[i]);33
+ emit_patch_jump(code, skip);34
+ return data_offset;35
+}6336
64
- // ---- STRING path: [payload-8] = length, [payload..] = bytes ----65
- emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length66
- emit_mov_reg_reg(code, REG_RSI, REG_RAX); // buf37
+static void write_literal(CodeBuf *code, int data_offset, int len) {38
+ if (kiln_is_compiling_so() && kiln_get_target() == KILN_TARGET_LINUX) {39
+ emit_lea_rip_back(code, REG_RSI, data_offset);40
+ } else {41
+ uint64_t addr = kiln_code_base() + (uint64_t)data_offset;42
+ emit_mov_reg_imm64(code, REG_RSI, addr);43
+ }44
+ emit_mov_reg_imm64(code, REG_RDX, (uint64_t)len);6745
platform_emit_write_stdout(code);68
- // newline, as its own tiny write (the string's bytes aren't69
- // guaranteed to be followed by one in memory)46
+}47
+48
+// Writes a single literal byte via a throwaway one-byte stack buffer --49
+// the same shape the original NIL/newline code used, generalized into a50
+// helper since the array/map printer below needs it for every bracket,51
+// comma, colon and quote. Clobbers RAX/RCX/RDX/RSI (and RBX/RDI on the52
+// Windows win_call path) like any call to platform_emit_write_stdout --53
+// callers that need something to survive across this keep it in memory54
+// ([RBP-disp], never a register), never in a register.55
+static void write_one_char(CodeBuf *code, char ch) {7056
emit_sub_reg_imm8(code, REG_RSP, 8);7157
emit_mov_reg_reg(code, REG_RCX, REG_RSP);72
- emit_store_byte_imm(code, REG_RCX, '\n');58
+ emit_store_byte_imm(code, REG_RCX, (uint8_t)ch);7359
emit_mov_reg_reg(code, REG_RSI, REG_RCX);7460
emit_mov_reg_imm64(code, REG_RDX, 1);7561
platform_emit_write_stdout(code);7662
emit_add_reg_imm8(code, REG_RSP, 8);77
- int done = emit_jmp_rel32(code);78
- emit_patch_jump(code, is_number);63
+}7964
80
- // ---- NIL path ----81
- emit_cmp_reg_imm32(code, REG_RBX, TAG_NIL);82
- int not_nil = emit_jcc_rel32(code, COND_NE);83
- {84
- int skip = emit_jmp_rel32(code);85
- int data_offset = code->count;86
- emit_byte(code, 'n'); emit_byte(code, 'i'); emit_byte(code, 'l'); emit_byte(code, '\n');87
- emit_patch_jump(code, skip);88
- // Position-independent path: needed only for libkilnrt.so, whose89
- // load address ld.so chooses at runtime. libkilnrt.dll (Windows)90
- // is fixed-base like everything else kiln emits (see91
- // pe/H/pe_dll_writer.h), so kiln_code_base() already resolves92
- // correctly there without RIP-relative addressing -- it now93
- // knows about DLL compilation specifically (see codegen/C/emit/layout.c).94
- if (kiln_is_compiling_so() && kiln_get_target() == KILN_TARGET_LINUX) {95
- emit_lea_rip_back(code, REG_RSI, data_offset);96
- } else {97
- uint64_t addr = kiln_code_base() + (uint64_t)data_offset;98
- emit_mov_reg_imm64(code, REG_RSI, addr);99
- }100
- emit_mov_reg_imm64(code, REG_RDX, 4);101
- platform_emit_write_stdout(code);102
- }103
- int done_nil = emit_jmp_rel32(code);104
- emit_patch_jump(code, not_nil);65
+// ---- number formatting, extracted into its own callable routine ----66
+//67
+// Entry: RAX = a double's raw bit pattern. Writes just the digits (and68
+// sign, and '.' + fraction digits if any) to stdout -- deliberately NO69
+// trailing newline, so both the top-level print routine (which adds70
+// exactly one newline after whichever path it took) and the array/map71
+// printer below (which never wants a newline mid-structure) can share72
+// it. This used to be inlined directly in the top-level routine's NUMBER73
+// path, newline and all; splitting it out is what let the array/map74
+// path below reuse it instead of duplicating ~90 lines of digit-building75
+// asm.76
+//77
+// Scratch layout, all within one `sub rsp, 40` region:78
+// [rsp, rsp+32) integer digits (+ optional '-'), built BACKWARD --79
+// RCX decrements before each store so no reversal80
+// step is needed.81
+// rsp+32 '.' (only meaningful if frac_len > 0)82
+// [rsp+33, rsp+39) up to 6 fraction digits, built FORWARD83
+// RBX holds the sign flag through the integer-digit phase, then gets84
+// reused as frac_len (how many of the 6 fraction digits are significant,85
+// i.e. up to the last nonzero one) -- safe, sign has already been86
+// written to the buffer by the time that phase starts.87
+static int number_routine_offset = -1;10588
106
- // ---- NUMBER path ----107
- emit_movq_xmm_from_reg(code, XMM0, REG_RAX); // payload bits -> double89
+static void emit_number_routine(CodeBuf *code) {90
+ int routine_skip = emit_jmp_rel32(code);91
+ number_routine_offset = code->count;92
+93
+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);10894
emit_sub_reg_imm8(code, REG_RSP, 40);10995
11096
// ---- sign ----@@ -123,7 +109,7 @@ static void emit_print_routine(CodeBuf *code) {123109
emit_cvtsi2sd(code, XMM1, REG_RAX);124110
emit_subsd(code, XMM0, XMM1);125111
126
- // ---- integer digits, backward, same loop shape as milestone 1 ----112
+ // ---- integer digits, backward ----127113
emit_mov_reg_reg(code, REG_RCX, REG_RSP);128114
emit_add_reg_imm8(code, REG_RCX, 32);129115
int loop_start = code->count;@@ -143,16 +129,8 @@ static void emit_print_routine(CodeBuf *code) {143129
emit_store_byte_imm(code, REG_RCX, '-');144130
emit_patch_jump(code, skip_sign);145131
146
- // ---- fraction digits, forward, 6 unrolled steps (a fixed, small,147
- // compile-time-known count -- no runtime loop needed) ----148
- // Round-half-up at the 6th decimal place before extracting digits:149
- // repeated float arithmetic (e.g. adding 0.1 a few times) routinely150
- // lands a hair below the "true" decimal value (0.7999999999999999151
- // instead of 0.8) due to ordinary double rounding, and truncating152
- // that directly would print "0.799999" instead of "0.8". Adding half153
- // a unit-in-the-last-place first is the standard fix; it's too small154
- // to introduce a spurious digit for values that are already exact155
- // (see the loop comment below).132
+ // ---- fraction digits, forward, 6 unrolled steps -- see print_int.c's133
+ // history for why the 0.0000005 round-half-up nudge is needed ----156134
load_double_const(code, XMM1, 0.0000005);157135
emit_addsd(code, XMM0, XMM1);158136
emit_mov_reg_reg(code, REG_RSI, REG_RSP);@@ -173,7 +151,7 @@ static void emit_print_routine(CodeBuf *code) {173151
emit_add_reg_imm8(code, REG_RSI, 1);174152
}175153
176
- // ---- decide '.'/newline placement, then a single write(2) ----154
+ // ---- decide '.' placement, then a single write(2) -- no newline ----177155
emit_mov_reg_reg(code, REG_RDX, REG_RSP);178156
emit_add_reg_imm8(code, REG_RDX, 32);179157
emit_cmp_reg_imm32(code, REG_RBX, 0);@@ -184,23 +162,305 @@ static void emit_print_routine(CodeBuf *code) {184162
emit_add_reg_reg(code, REG_RDX, REG_RBX); // + frac_len185163
int after_dot = emit_jmp_rel32(code);186164
emit_patch_jump(code, whole_number);187
- // RDX already points at rsp+32, the correct newline spot when there's no fraction165
+ // RDX already points one past the last int digit -- the correct166
+ // end-of-content position when there's no fraction.188167
emit_patch_jump(code, after_dot);189
- emit_store_byte_imm(code, REG_RDX, '\n');190168
191169
emit_mov_reg_reg(code, REG_RSI, REG_RCX); // buf start192
- emit_add_reg_imm8(code, REG_RDX, 1);193
- emit_sub_reg_reg(code, REG_RDX, REG_RCX); // length = (newline_pos + 1) - buf_start170
+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // length = content_end - buf_start171
+ platform_emit_write_stdout(code);172
+173
+ emit_add_reg_imm8(code, REG_RSP, 40);174
+ emit_ret(code);175
+ emit_patch_jump(code, routine_skip);176
+}177
+178
+// ---- recursive array/map printer ----179
+//180
+// Entry: RAX = payload, RBX = tag -- same convention as the top-level181
+// print routine, so array elements and map values can be fed straight182
+// into it without re-tagging. Unlike the top-level routine, strings ARE183
+// quoted here (matching ashvm's vm_print_value: an element/value that's184
+// a string always prints with quotes, even though a bare top-level `say185
+// "hi";` doesn't) -- see value.c's vm_print_value for the shape this186
+// mirrors. Never writes a trailing newline; the top-level routine adds187
+// exactly one, once, after the whole structure has printed.188
+//189
+// Own stack frame (own RBP, not the enclosing ash function's -- pushed190
+// and restored like any ordinary call) holds the three things that must191
+// survive across both the write(2) calls AND the recursive self-calls192
+// below, since those clobber every GP register this module uses:193
+// [RBP-8] data_ptr (array) / entries_ptr (map)194
+// [RBP-16] count195
+// [RBP-24] index196
+// [RBP+disp] addressing (not [RSP+disp], which this codegen layer can't197
+// even encode -- RSP as a base needs a SIB byte modrm_mem_disp32()198
+// doesn't emit, see emit.c) survives any balanced call uneventfully: a199
+// call's net effect on RSP is always zero once it returns (push200
+// return-addr, callee's own matched sub/add rsp, ret pops the201
+// return-addr), so RBP itself -- untouched by any callee here, each of202
+// which is careful to push/pop its OWN RBP if it uses one at all -- keeps203
+// addressing the exact same bytes before and after.204
+static int print_recursive_offset = -1;205
+206
+static void emit_print_recursive_routine(CodeBuf *code) {207
+ int routine_skip = emit_jmp_rel32(code);208
+ print_recursive_offset = code->count;209
+210
+ emit_push_reg(code, REG_RBP);211
+ emit_mov_reg_reg(code, REG_RBP, REG_RSP);212
+ emit_sub_reg_imm8(code, REG_RSP, 32); // [RBP-8/-16/-24] loop state, [RBP-32] map's current entry addr213
+214
+ int to_epilogue[5];215
+ int n_epilogue = 0;216
+217
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);218
+ int not_string = emit_jcc_rel32(code, COND_NE);219
+ {220
+ // write_one_char() clobbers RAX/RCX/RDX/RSI (it's its own call to221
+ // platform_emit_write_stdout), so length/buf must be saved via the222
+ // real stack -- NOT held in a register -- across the opening quote.223
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length224
+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // buf225
+ emit_push_reg(code, REG_RSI);226
+ emit_push_reg(code, REG_RDX);227
+ write_one_char(code, '"');228
+ emit_pop_reg(code, REG_RDX);229
+ emit_pop_reg(code, REG_RSI);230
+ platform_emit_write_stdout(code);231
+ write_one_char(code, '"');232
+ to_epilogue[n_epilogue++] = emit_jmp_rel32(code);233
+ }234
+ emit_patch_jump(code, not_string);235
+236
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_NIL);237
+ int not_nil = emit_jcc_rel32(code, COND_NE);238
+ {239
+ static int none_offset = -1;240
+ if (none_offset == -1) none_offset = emit_literal(code, "none", 4);241
+ write_literal(code, none_offset, 4);242
+ to_epilogue[n_epilogue++] = emit_jmp_rel32(code);243
+ }244
+ emit_patch_jump(code, not_nil);245
+246
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_NUMBER);247
+ int not_number = emit_jcc_rel32(code, COND_NE);248
+ {249
+ emit_call_back(code, number_routine_offset); // RAX already = payload bits250
+ to_epilogue[n_epilogue++] = emit_jmp_rel32(code);251
+ }252
+ emit_patch_jump(code, not_number);253
+254
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_ARRAY);255
+ int not_array = emit_jcc_rel32(code, COND_NE);256
+ {257
+ emit_mov_reg_reg(code, REG_RCX, REG_RAX); // array object addr258
+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, 8); // count259
+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 16); // data_ptr260
+ emit_store_mem_disp32(code, REG_RBP, -16, REG_RDX);261
+ emit_store_mem_disp32(code, REG_RBP, -8, REG_RSI);262
+ emit_mov_reg_imm64(code, REG_RDX, 0);263
+ emit_store_mem_disp32(code, REG_RBP, -24, REG_RDX); // index = 0264
+265
+ write_one_char(code, '[');266
+267
+ int loop_top = code->count;268
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -24); // index269
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, -16); // count270
+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);271
+ int loop_end = emit_jcc_rel32(code, COND_GE);272
+273
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, -8); // data_ptr274
+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);275
+ emit_mov_reg_imm64(code, REG_RAX, 16);276
+ emit_imul_reg_reg(code, REG_RDX, REG_RAX); // index * 16277
+ emit_add_reg_reg(code, REG_RDX, REG_RSI); // element addr278
+ emit_load_mem_disp32(code, REG_RBX, REG_RDX, 0); // elem tag279
+ emit_load_mem_disp32(code, REG_RAX, REG_RDX, 8); // elem payload280
+ emit_call_back(code, print_recursive_offset); // recursive -- quotes strings281
+282
+ // index++ is stored BEFORE deciding whether to print a comma --283
+ // not held in a register across write_one_char(','), which284
+ // clobbers RCX (see write_one_char's own comment). The decision285
+ // itself re-reads both values fresh from the frame for the same286
+ // reason: nothing here may assume a register survives a call.287
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -24); // index288
+ emit_add_reg_imm8(code, REG_RCX, 1);289
+ emit_store_mem_disp32(code, REG_RBP, -24, REG_RCX); // index++290
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -24);291
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, -16); // count292
+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);293
+ int skip_comma = emit_jcc_rel32(code, COND_GE);294
+ write_one_char(code, ',');295
+ write_one_char(code, ' ');296
+ emit_patch_jump(code, skip_comma);297
+ emit_jmp_back(code, loop_top);298
+299
+ emit_patch_jump(code, loop_end);300
+ write_one_char(code, ']');301
+ to_epilogue[n_epilogue++] = emit_jmp_rel32(code);302
+ }303
+ emit_patch_jump(code, not_array);304
+305
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_MAP);306
+ int not_map = emit_jcc_rel32(code, COND_NE);307
+ {308
+ emit_mov_reg_reg(code, REG_RCX, REG_RAX); // map object addr309
+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, 8); // count310
+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 16); // entries_ptr311
+ emit_store_mem_disp32(code, REG_RBP, -16, REG_RDX);312
+ emit_store_mem_disp32(code, REG_RBP, -8, REG_RSI);313
+ emit_mov_reg_imm64(code, REG_RDX, 0);314
+ emit_store_mem_disp32(code, REG_RBP, -24, REG_RDX); // index = 0315
+316
+ write_one_char(code, '{');317
+318
+ int loop_top = code->count;319
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -24); // index320
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, -16); // count321
+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);322
+ int loop_end = emit_jcc_rel32(code, COND_GE);323
+324
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, -8); // entries_ptr325
+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);326
+ emit_mov_reg_imm64(code, REG_RAX, 32);327
+ emit_imul_reg_reg(code, REG_RDX, REG_RAX); // index * 32328
+ emit_add_reg_reg(code, REG_RDX, REG_RSI); // entry addr329
+ // Persisted in the frame, not a register -- it has to survive the330
+ // key write, the value's tag/payload load, AND the recursive331
+ // call below, and every one of those clobbers RAX/RCX/RDX/RSI.332
+ emit_store_mem_disp32(code, REG_RBP, -32, REG_RDX);333
+334
+ // key: always a string (value.h) -- quoted, written directly335
+ // rather than through the recursive call (no tag dispatch needed).336
+ write_one_char(code, '"');337
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -32); // entry addr338
+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8); // key payload339
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // key length340
+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // key buf341
+ platform_emit_write_stdout(code);342
+ write_one_char(code, '"');343
+ write_one_char(code, ':');344
+ write_one_char(code, ' ');345
+346
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -32); // entry addr347
+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 16); // value tag348
+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 24); // value payload349
+ emit_call_back(code, print_recursive_offset); // recursive -- quotes strings350
+351
+ // See the array loop's matching comment: store index++ before352
+ // deciding on a comma, then re-read both values fresh.353
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -24); // index354
+ emit_add_reg_imm8(code, REG_RCX, 1);355
+ emit_store_mem_disp32(code, REG_RBP, -24, REG_RCX); // index++356
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, -24);357
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, -16); // count358
+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);359
+ int skip_comma = emit_jcc_rel32(code, COND_GE);360
+ write_one_char(code, ',');361
+ write_one_char(code, ' ');362
+ emit_patch_jump(code, skip_comma);363
+ emit_jmp_back(code, loop_top);364
+365
+ emit_patch_jump(code, loop_end);366
+ write_one_char(code, '}');367
+ to_epilogue[n_epilogue++] = emit_jmp_rel32(code);368
+ }369
+ emit_patch_jump(code, not_map);370
+371
+ // Fallback (TAG_FUNCTION, or anything else): matches ashvm's372
+ // "<function>"/"<closure>" rather than silently misprinting the373
+ // function object's code address as a number.374
+ {375
+ static int function_offset = -1;376
+ if (function_offset == -1) function_offset = emit_literal(code, "<function>", 10);377
+ write_literal(code, function_offset, 10);378
+ // falls straight through to the epilogue below379
+ }380
+381
+ for (int i = 0; i < n_epilogue; i++) emit_patch_jump(code, to_epilogue[i]);382
+ emit_mov_reg_reg(code, REG_RSP, REG_RBP);383
+ emit_pop_reg(code, REG_RBP);384
+ emit_ret(code);385
+ emit_patch_jump(code, routine_skip);386
+}387
+388
+// Dispatches on the popped value's tag: a STRING just gets written389
+// directly (it's already bytes -- no digit conversion needed at all,390
+// unlike a number, and unquoted here -- only array/map elements get391
+// quoted, see emit_print_recursive_routine above); NUMBER goes to392
+// number_routine; everything else (ARRAY, MAP, FUNCTION) delegates to393
+// the recursive printer. Each path ends with exactly one trailing394
+// newline, written once after the value's own content, then control395
+// converges on a single `ret`.396
+//397
+// Plan B, phase B1: emitted once (jumped over), called via emit_call_back398
+// from codegen_print_top below -- this is the single biggest deduplication399
+// win of B1, since this routine used to be re-emitted in full at EVERY400
+// print statement in the source (dwarfing heap_emit_alloc/bytes_emit_copy,401
+// which are individually smaller). Takes its value in RAX=payload,402
+// RBX=tag (NOT by popping them itself): a real CALL pushes a return403
+// address on top of whatever the caller already pushed, so a `pop` here404
+// would grab the return address instead of the caller's value -- same405
+// register-based-argument reasoning codegen/C/runtime/heap.c's alloc routine406
+// already uses, not something print specifically needed before this407
+// became a real subroutine.408
+static int print_routine_offset = -1;409
+410
+static void emit_print_routine(CodeBuf *code) {411
+ int routine_skip = emit_jmp_rel32(code);412
+ print_routine_offset = code->count;413
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);414
+ int is_number = emit_jcc_rel32(code, COND_NE);415
+416
+ // ---- STRING path: [payload-8] = length, [payload..] = bytes ----417
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length418
+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // buf194419
platform_emit_write_stdout(code);420
+ write_one_char(code, '\n');421
+ int done = emit_jmp_rel32(code);422
+ emit_patch_jump(code, is_number);423
+424
+ // ---- NIL path ----425
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_NIL);426
+ int not_nil = emit_jcc_rel32(code, COND_NE);427
+ {428
+ int skip = emit_jmp_rel32(code);429
+ int data_offset = code->count;430
+ emit_byte(code, 'n'); emit_byte(code, 'o'); emit_byte(code, 'n'); emit_byte(code, 'e');431
+ emit_patch_jump(code, skip);432
+ write_literal(code, data_offset, 4);433
+ write_one_char(code, '\n');434
+ }435
+ int done_nil = emit_jmp_rel32(code);436
+ emit_patch_jump(code, not_nil);437
+438
+ // ---- NUMBER path ----439
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_NUMBER);440
+ int not_number = emit_jcc_rel32(code, COND_NE);441
+ {442
+ emit_call_back(code, number_routine_offset); // RAX already = payload bits443
+ write_one_char(code, '\n');444
+ }445
+ int done_number = emit_jmp_rel32(code);446
+ emit_patch_jump(code, not_number);447
+448
+ // ---- everything else (ARRAY, MAP, FUNCTION) -- delegate to the449
+ // recursive printer, which already has a case for each (and a450
+ // "<function>" fallback for anything further afield than that) ----451
+ emit_call_back(code, print_recursive_offset);452
+ write_one_char(code, '\n');195453
196
- emit_add_reg_imm8(code, REG_RSP, 40); // restore stack for the next statement197454
emit_patch_jump(code, done);198455
emit_patch_jump(code, done_nil);456
+ emit_patch_jump(code, done_number);199457
emit_ret(code);200458
emit_patch_jump(code, routine_skip);201459
}202460
203461
void print_emit_top_routine_only(CodeBuf *code) {462
+ emit_number_routine(code);463
+ emit_print_recursive_routine(code);204464
emit_print_routine(code);205465
}206466
@@ -211,7 +471,11 @@ static int is_shared(void) {211471
}212472
213473
void print_emit_startup(CodeBuf *code) {214
- if (!is_shared()) emit_print_routine(code);474
+ if (!is_shared()) {475
+ emit_number_routine(code);476
+ emit_print_recursive_routine(code);477
+ emit_print_routine(code);478
+ }215479
}216480
217481
void codegen_print_top(CodeBuf *code) {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/parser/H/core/parser.h src/codegen/H/emit/emit.h10
+ src/codegen/H/builtins/type_builtin.h src/parser/H/core/parser.h \11
+ src/codegen/H/emit/emit.h1112
src/codegen/H/builtins/builtins.h:1213
src/codegen/H/expressions/expr.h:1314
src/lexer/H/lexer.h:@@ -21,5 +22,6 @@ src/codegen/H/strings/string_builtins.h:2122
src/codegen/H/io/file_builtins.h:2223
src/codegen/H/io/input_builtin.h:2324
src/codegen/H/io/argv_builtin.h:25
+src/codegen/H/builtins/type_builtin.h:2426
src/parser/H/core/parser.h:2527
src/codegen/H/emit/emit.h:kiln/src/codegen/D/builtins/type_builtin.dadded
+13−0@@ -0,0 +1,13 @@1
+src/codegen/O/builtins/type_builtin.o: \2
+ src/codegen/C/builtins/type_builtin.c \3
+ src/codegen/H/builtins/type_builtin.h src/codegen/H/strings/strings.h \4
+ src/codegen/H/emit/emit_internal.h src/codegen/H/emit/emit.h \5
+ src/codegen/H/emit/value.h src/parser/H/core/parser.h \6
+ src/lexer/H/lexer.h7
+src/codegen/H/builtins/type_builtin.h:8
+src/codegen/H/strings/strings.h:9
+src/codegen/H/emit/emit_internal.h:10
+src/codegen/H/emit/emit.h:11
+src/codegen/H/emit/value.h:12
+src/parser/H/core/parser.h:13
+src/lexer/H/lexer.h:kiln/src/codegen/D/functions/lambda.dmodified
+2−2@@ -4,7 +4,7 @@ src/codegen/O/functions/lambda.o: src/codegen/C/functions/lambda.c \44
src/codegen/H/emit/value.h src/codegen/H/expressions/expr.h \55
src/lexer/H/lexer.h src/codegen/H/emit/layout.h \66
src/parser/H/core/parser.h src/parser/H/core/parser_internal.h \7
- src/parser/H/declarations/vars.h src/parser/H/statements/for_loop.h \7
+ src/parser/H/declarations/vars.h src/parser/H/statements/each_loop.h \88
src/parser/H/statements/loop_stack.h99
src/codegen/H/functions/closures.h:1010
src/parser/H/declarations/functions.h:@@ -17,5 +17,5 @@ src/codegen/H/emit/layout.h:1717
src/parser/H/core/parser.h:1818
src/parser/H/core/parser_internal.h:1919
src/parser/H/declarations/vars.h:20
-src/parser/H/statements/for_loop.h:20
+src/parser/H/statements/each_loop.h:2121
src/parser/H/statements/loop_stack.h:kiln/src/codegen/H/builtins/type_builtin.hadded
+11−0@@ -0,0 +1,11 @@1
+#ifndef KILN_TYPE_BUILTIN_H2
+#define KILN_TYPE_BUILTIN_H3
+4
+// type(value) -- returns "number"/"string"/"array"/"map"/"function"/"none".5
+// Needed for anything that has to branch on a value's own type at6
+// runtime (the motivating case: a JSON stringify can't decide "format7
+// this as an array vs. a map vs. a string" from pure Ash without some8
+// way to ask a value what it is -- see ideas/ash_modules.md).9
+void codegen_builtin_type(void);10
+11
+#endifkiln/src/codegen/H/emit/emit.hmodified
+2−2@@ -109,8 +109,8 @@ void emit_call_indirect(CodeBuf *buf, Reg target);109109
110110
// JMP r/m64 (opcode FF /4) -- indirect jump through a register holding an111111
// absolute runtime address, no return address pushed (unlike112
-// emit_call_indirect). Used by try/catch's raise routine to resume at a113
-// catch block once RSP/RBP have already been unwound to the handler's112
+// emit_call_indirect). Used by attempt/handle's raise routine to resume at a113
+// handle block once RSP/RBP have already been unwound to the handler's114114
// saved values -- this is a resumption, not a call, so nothing should be115115
// pushed.116116
void emit_jmp_indirect(CodeBuf *buf, Reg target);kiln/src/codegen/H/emit/runtime_layout.hmodified
+2−2@@ -1,8 +1,8 @@11
#ifndef KILN_RUNTIME_LAYOUT_H22
#define KILN_RUNTIME_LAYOUT_H33
4
-// OS-agnostic sizes for kiln's own hand-rolled try/catch handler stack5
-// (see codegen/C/runtime/errors.c's raise routine, parser/C/statements/try_catch.c) -- the byte4
+// OS-agnostic sizes for kiln's own hand-rolled attempt/handle handler stack5
+// (see codegen/C/runtime/errors.c's raise routine, parser/C/statements/attempt_handle.c) -- the byte66
// layout is a kiln-internal invention, not tied to ELF or PE, so both77
// elf/H/elf_writer.h and pe/H/pe_writer.h include this rather than each88
// re-declaring the same numbers.kiln/src/codegen/H/runtime/errors.hmodified
+6−6@@ -7,7 +7,7 @@77
// Emitted once, at the very start of the generated program (right after88
// codegen/C/runtime/heap.c's heap_emit_startup): a single reusable "raise" routine99
// that every runtime error jumps to (never called -- there's nothing to10
-// return to, control either resumes at a catch block or the process10
+// return to, control either resumes at a handle block or the process1111
// exits).1212
void errors_emit_startup(CodeBuf *code);1313
@@ -18,23 +18,23 @@ int errors_raise_routine_offset(void);1818
1919
// Embeds `msg` (compile-time-known, jumped over so it's never executed2020
// as instructions -- same technique codegen/C/strings/strings.c uses for string21
-// literals) and raises it: if a `try` is currently active (see22
-// parser/C/statements/try_catch.c), unwinds to its `catch` block with `msg` bound as21
+// literals) and raises it: if an `attempt` is currently active (see22
+// parser/C/statements/attempt_handle.c), unwinds to its `handle` block with `msg` bound as2323
// a TAG_STRING value; otherwise writes it to stderr and exits with2424
// status 1, same as before milestone 9.2525
void errors_emit_die(CodeBuf *code, const char *msg);2626
27
-// Like errors_emit_die, but ALWAYS fatal -- bypasses the try/catch check27
+// Like errors_emit_die, but ALWAYS fatal -- bypasses the attempt/handle check2828
// entirely. Used only for the one internal case that must never be2929
// "catchable": exceeding MAX_KILN_TRY_DEPTH itself (silently continuing3030
// there would mean pushing a handler past the fixed table, corrupting3131
// the adjacent code segment).3232
void errors_emit_fatal(CodeBuf *code, const char *msg);3333
3434
// Like errors_emit_die, but for a RUNTIME string value (parser.c's35
-// `throw expr;`) instead of a compile-time-constant C string -- the35
+// `raise expr;`) instead of a compile-time-constant C string -- the3636
// caller must already have RSI=message payload and RDX=message length37
-// loaded (a throw expression's already-popped (tag,payload), payload is37
+// loaded (a raise expression's already-popped (tag,payload), payload is3838
// already a valid length-prefixed TAG_STRING pointer, so there's nothing3939
// to embed, just the same jump to the raise routine errors_emit_die uses.4040
void errors_emit_die_dynamic(CodeBuf *code);kiln/src/diagnostics/C/diagnostics.cmodified
+1−1@@ -62,7 +62,7 @@ static int has_prefix(const char *message, const char *prefix) {6262
}6363
6464
static const char *lookup_hint(const char *message) {65
- if (has_prefix(message, "undefined variable")) return "did you forget to declare it with `let`?";65
+ if (has_prefix(message, "undefined variable")) return "did you forget to declare it with `local`?";6666
if (has_prefix(message, "undefined function")) return "check the function name is spelled correctly";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";kiln/src/elf/H/elf_writer.hmodified
+2−2@@ -15,8 +15,8 @@1515
#define KILN_PHDR_SIZE 561616
1717
// Milestone 9: the globals region also holds kiln's own hand-rolled18
-// setjmp/longjmp equivalent for try/catch (see codegen/C/runtime/errors.c's raise19
-// routine and parser/C/statements/try_catch.c) -- a fixed-depth stack of "handler"18
+// setjmp/longjmp equivalent for attempt/handle (see codegen/C/runtime/errors.c's raise19
+// routine and parser/C/statements/attempt_handle.c) -- a fixed-depth stack of "handler"2020
// records, since this is a freestanding binary with no libc setjmp to2121
// call. Each handler is {saved_rsp, saved_rbp, target_addr,2222
// error_slot_rbp_offset}, 4 x int64. Bounded the same way every otherkiln/src/lexer/C/lexer.cmodified
+36−23@@ -140,23 +140,29 @@ static Token identifier(void) {140140
while (isalnum(peek()) || peek() == '_') advance();141141
142142
int length = (int)(lex_current - lex_start);143
- if (length == 5 && strncmp(lex_start, "print", 5) == 0) return make_token(TOKEN_PRINT);144
- if (length == 3 && strncmp(lex_start, "let", 3) == 0) return make_token(TOKEN_LET);145
- if (length == 2 && strncmp(lex_start, "if", 2) == 0) return make_token(TOKEN_IF);146
- if (length == 4 && strncmp(lex_start, "else", 4) == 0) return make_token(TOKEN_ELSE);147
- if (length == 5 && strncmp(lex_start, "while", 5) == 0) return make_token(TOKEN_WHILE);148
- if (length == 3 && strncmp(lex_start, "for", 3) == 0) return make_token(TOKEN_FOR);143
+ if (length == 3 && strncmp(lex_start, "say", 3) == 0) return make_token(TOKEN_SAY);144
+ if (length == 5 && strncmp(lex_start, "local", 5) == 0) return make_token(TOKEN_LOCAL);145
+ if (length == 5 && strncmp(lex_start, "given", 5) == 0) return make_token(TOKEN_GIVEN);146
+ if (length == 9 && strncmp(lex_start, "otherwise", 9) == 0) return make_token(TOKEN_OTHERWISE);147
+ if (length == 6 && strncmp(lex_start, "during", 6) == 0) return make_token(TOKEN_DURING);148
+ if (length == 4 && strncmp(lex_start, "each", 4) == 0) return make_token(TOKEN_EACH);149149
if (length == 2 && strncmp(lex_start, "in", 2) == 0) return make_token(TOKEN_IN);150
- if (length == 2 && strncmp(lex_start, "fn", 2) == 0) return make_token(TOKEN_FN);151
- if (length == 6 && strncmp(lex_start, "return", 6) == 0) return make_token(TOKEN_RETURN);152
- if (length == 3 && strncmp(lex_start, "try", 3) == 0) return make_token(TOKEN_TRY);153
- if (length == 5 && strncmp(lex_start, "catch", 5) == 0) return make_token(TOKEN_CATCH);154
- if (length == 5 && strncmp(lex_start, "throw", 5) == 0) return make_token(TOKEN_THROW);155
- if (length == 5 && strncmp(lex_start, "break", 5) == 0) return make_token(TOKEN_BREAK);156
- if (length == 8 && strncmp(lex_start, "continue", 8) == 0) return make_token(TOKEN_CONTINUE);157
- if (length == 4 && strncmp(lex_start, "true", 4) == 0) return make_token(TOKEN_TRUE);158
- if (length == 5 && strncmp(lex_start, "false", 5) == 0) return make_token(TOKEN_FALSE);159
- if (length == 3 && strncmp(lex_start, "nil", 3) == 0) return make_token(TOKEN_NIL);150
+ if (length == 5 && strncmp(lex_start, "forge", 5) == 0) return make_token(TOKEN_FORGE);151
+ if (length == 5 && strncmp(lex_start, "yield", 5) == 0) return make_token(TOKEN_YIELD);152
+ if (length == 7 && strncmp(lex_start, "attempt", 7) == 0) return make_token(TOKEN_ATTEMPT);153
+ if (length == 6 && strncmp(lex_start, "handle", 6) == 0) return make_token(TOKEN_HANDLE);154
+ if (length == 5 && strncmp(lex_start, "raise", 5) == 0) return make_token(TOKEN_RAISE);155
+ if (length == 4 && strncmp(lex_start, "stop", 4) == 0) return make_token(TOKEN_STOP);156
+ if (length == 4 && strncmp(lex_start, "next", 4) == 0) return make_token(TOKEN_NEXT);157
+ if (length == 3 && strncmp(lex_start, "yes", 3) == 0) return make_token(TOKEN_YES);158
+ if (length == 2 && strncmp(lex_start, "no", 2) == 0) return make_token(TOKEN_NO);159
+ if (length == 4 && strncmp(lex_start, "none", 4) == 0) return make_token(TOKEN_NONE);160
+ if (length == 3 && strncmp(lex_start, "and", 3) == 0) return make_token(TOKEN_AND);161
+ if (length == 2 && strncmp(lex_start, "or", 2) == 0) return make_token(TOKEN_OR);162
+ // "not" reuses TOKEN_BANG (unary logical negation) -- same meaning as163
+ // the symbol it replaces, so unary()'s existing TOKEN_BANG handling in164
+ // codegen/C/expressions/expr.c needs no change at all.165
+ if (length == 3 && strncmp(lex_start, "not", 3) == 0) return make_token(TOKEN_BANG);160166
161167
return make_token(TOKEN_IDENTIFIER);162168
}@@ -196,19 +202,26 @@ Token lexer_next_token(void) {196202
case '[': return make_token(TOKEN_LBRACKET);197203
case ']': return make_token(TOKEN_RBRACKET);198204
case '=': return make_token(match('=') ? TOKEN_EQUAL_EQUAL : TOKEN_EQUAL);199
- case '!': return make_token(match('=') ? TOKEN_BANG_EQUAL : TOKEN_BANG);205
+ // Bare '!' is no longer valid on its own -- logical negation is the206
+ // 'not' keyword now (see identifier()). '!=' (inequality) stays207
+ // symbolic either way.208
+ case '!':209
+ if (match('=')) return make_token(TOKEN_BANG_EQUAL);210
+ return error_token("unexpected character");200211
case '<':201212
if (match('<')) return make_token(TOKEN_SHL);202213
return make_token(match('=') ? TOKEN_LESS_EQUAL : TOKEN_LESS);203214
case '>':204215
if (match('>')) return make_token(TOKEN_SHR);205216
return make_token(match('=') ? TOKEN_GREATER_EQUAL : TOKEN_GREATER);206
- case '&':207
- if (match('&')) return make_token(TOKEN_AND);208
- return make_token(TOKEN_AMP);209
- case '|':210
- if (match('|')) return make_token(TOKEN_OR);211
- return make_token(TOKEN_PIPE);217
+ // '&'/'|' are always the bitwise operators now -- 'and'/'or' are218
+ // the only way to write the logical versions (see identifier()).219
+ // This also means a literal '&&'/'||' in old-syntax source now220
+ // lexes as two separate AMP/PIPE tokens rather than one fused one,221
+ // consistent with the rest of today's renames replacing their old222
+ // symbol outright rather than keeping it as an alias.223
+ case '&': return make_token(TOKEN_AMP);224
+ case '|': return make_token(TOKEN_PIPE);212225
case '^': return make_token(TOKEN_CARET);213226
case '~': return make_token(TOKEN_TILDE);214227
case '@':kiln/src/lexer/H/lexer.hmodified
+16−16@@ -2,24 +2,24 @@22
#define LEXER_H33
44
typedef enum {5
- TOKEN_PRINT,6
- TOKEN_LET,7
- TOKEN_IF,8
- TOKEN_ELSE,9
- TOKEN_WHILE,10
- TOKEN_FOR,5
+ TOKEN_SAY,6
+ TOKEN_LOCAL,7
+ TOKEN_GIVEN,8
+ TOKEN_OTHERWISE,9
+ TOKEN_DURING,10
+ TOKEN_EACH,1111
TOKEN_IN,1212
TOKEN_IMPORT,13
- TOKEN_FN,14
- TOKEN_RETURN,15
- TOKEN_TRY,16
- TOKEN_CATCH,17
- TOKEN_THROW,18
- TOKEN_BREAK,19
- TOKEN_CONTINUE,20
- TOKEN_TRUE,21
- TOKEN_FALSE,22
- TOKEN_NIL,13
+ TOKEN_FORGE,14
+ TOKEN_YIELD,15
+ TOKEN_ATTEMPT,16
+ TOKEN_HANDLE,17
+ TOKEN_RAISE,18
+ TOKEN_STOP,19
+ TOKEN_NEXT,20
+ TOKEN_YES,21
+ TOKEN_NO,22
+ TOKEN_NONE,2323
TOKEN_IDENTIFIER,2424
TOKEN_STRING,2525
TOKEN_IMPORT_PATH,kiln/src/parser/C/core/parser.cmodified
+90−59@@ -40,10 +40,11 @@ void expect(TokenType type, const char *message) {4040
4141
// statement() is shared between top-level parsing (compile_program's own4242
// loop, and imports.c's parse_imported_file_body()) and here, inside a43
-// fn/if/while/for/try body -- block_depth distinguishes the two so the44
-// "only fn/let at an imported file's top level" guard in statement()45
-// only fires for genuinely top-level statements, never for an ordinary46
-// `return`/`if`/etc. nested inside an imported function's own body.43
+// forge/given/during/each/attempt body -- block_depth distinguishes the44
+// two so the "only forge/local at an imported file's top level" guard in45
+// statement() only fires for genuinely top-level statements, never for an46
+// ordinary `yield`/`given`/etc. nested inside an imported function's own47
+// body.4748
static int block_depth = 0;4849
4950
void block(void) {@@ -56,19 +57,26 @@ void block(void) {5657
vars_scope_end(saved_scope);5758
}5859
59
-// let IDENT = expr ; -- reassigns the existing slot if `name` is already60
-// declared instead of erroring, matching ashvm's own let-statement61
-// behavior exactly (ashvm/src/compiler/stmt.c's let_statement).62
-static void let_statement(void) {60
+// local IDENT = expr ; -- reassigns the existing slot if `name` is already61
+// declared instead of erroring, matching ashvm's own local-statement62
+// behavior exactly (ashvm/src/compiler/stmt.c's local_statement).63
+static void local_statement(void) {6364
advance_token();64
- expect(TOKEN_IDENTIFIER, "expected variable name after 'let'");65
+ expect(TOKEN_IDENTIFIER, "expected variable name after 'local'");6566
const char *name = previous.start;6667
int len = previous.length;6768
expect(TOKEN_EQUAL, "expected '=' after variable name");6869
6970
const char *ns; int ns_len;7071
get_import_namespace(&ns, &ns_len);71
- if (ns_len > 0) {72
+ // Bug (found while writing the first real forgepack module, a JSON73
+ // codec -- any imported function with a `local` inside a loop failed74
+ // to compile, mirrored from the identical ashvm bug): this only75
+ // makes sense at block_depth == 0 (a genuinely top-level `local` in76
+ // the imported file), not for an ordinary local deep inside one of77
+ // that file's own functions -- the sibling guard further down in78
+ // statement() already gets this right, this one didn't.79
+ if (ns_len > 0 && block_depth == 0) {7280
char combined[96];7381
int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s", ns_len, ns, len, name);7482
if (current.type == TOKEN_NUMBER) {@@ -86,7 +94,7 @@ static void let_statement(void) {8694
declare_string_constant(combined, combined_len, current.start, current.length);8795
advance_token();8896
} else {89
- parse_error("imported top-level 'let' must be a literal number or string constant");97
+ parse_error("imported top-level 'local' must be a literal number or string constant");9098
}9199
expect(TOKEN_SEMICOLON, "expected ';' after statement");92100
return;@@ -241,6 +249,26 @@ static void incdec_statement(Token id, TokenType op) {241249
static void assignment_statement(void) {242250
Token id = current;243251
advance_token();252
+ // A namespaced call used as its OWN statement (`json.save(...);`,253
+ // discarding the result) rather than nested in an expression254
+ // (`local x = json.load(...);`, where codegen/C/expressions/expr.c's255
+ // primary() already handles this same `ident.member(...)` shape) --256
+ // mirrors the identical ashvm fix, found missing the same way (the257
+ // first real forgepack module's whole point is exactly this shape).258
+ if (current.type == TOKEN_DOT) {259
+ advance_token();260
+ expect(TOKEN_IDENTIFIER, "expected name after '.'");261
+ Token member = previous;262
+ char combined[128];263
+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s",264
+ id.length, id.start, member.length, member.start);265
+ expect(TOKEN_LPAREN, "expected '(' after namespaced function name");266
+ codegen_call_named(combined, combined_len);267
+ expect(TOKEN_SEMICOLON, "expected ';' after call");268
+ emit_pop_reg(code, REG_RAX); // discard the (unused) return value269
+ emit_pop_reg(code, REG_RBX);270
+ return;271
+ }244272
if (current.type == TOKEN_LBRACKET) { index_assignment_statement(id); return; }245273
if (current.type == TOKEN_LPAREN) { call_statement(id); return; }246274
if (current.type == TOKEN_PLUS_EQUAL || current.type == TOKEN_MINUS_EQUAL ||@@ -263,14 +291,14 @@ static void assignment_statement(void) {263291
emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);264292
}265293
266
-// return [expr] ; -- bare `return;` returns 0, matching ashc/ashvm.294
+// yield [expr] ; -- bare `yield;` returns 0, matching ashc/ashvm.267295
// Return value comes back as RBX=tag, RAX=payload (see codegen/C/expressions/expr.c's268296
// call-site handling).269
-static void return_statement(void) {297
+static void yield_statement(void) {270298
advance_token();271299
if (current.type != TOKEN_SEMICOLON) {272300
codegen_expression();273
- expect(TOKEN_SEMICOLON, "expected ';' after return");301
+ expect(TOKEN_SEMICOLON, "expected ';' after yield");274302
emit_pop_reg(code, REG_RAX); // payload275303
emit_pop_reg(code, REG_RBX); // tag276304
} else {@@ -283,81 +311,84 @@ static void return_statement(void) {283311
emit_ret(code);284312
}285313
286
-// throw expr ; -- expr must be a STRING (matches try/catch's own287
-// convention: every caught error, built-in or user-thrown, is a plain288
-// string). Reuses codegen/C/runtime/errors.c's raise routine directly -- the exact289
-// same catch/unwind mechanism M9's built-in errors (array bounds, missing290
-// map key) already go through, just with a runtime message instead of a291
-// compile-time-constant one.292
-static void throw_statement(void) {314
+// raise expr ; -- expr must be a STRING (matches attempt/handle's own315
+// convention: every caught error, built-in or user-raised, is a plain316
+// string). Reuses codegen/C/runtime/errors.c's raise routine directly --317
+// the exact same handle/unwind mechanism M9's built-in errors (array318
+// bounds, missing map key) already go through, just with a runtime319
+// message instead of a compile-time-constant one. (The runtime "raise320
+// routine" this calls into was already named that before the `throw`321
+// keyword became `raise` -- a coincidental match, not a rename.)322
+static void raise_statement(void) {293323
advance_token();294324
codegen_expression();295
- expect(TOKEN_SEMICOLON, "expected ';' after 'throw'");325
+ expect(TOKEN_SEMICOLON, "expected ';' after 'raise'");296326
297327
emit_pop_reg(code, REG_RSI); // message payload (assumed STRING)298328
emit_pop_reg(code, REG_RBX); // message tag (ignored)299329
emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length300330
errors_emit_die_dynamic(code);301331
}302332
303
-// break ; -- jumps past the innermost loop; the jump is recorded, not333
+// stop ; -- jumps past the innermost loop; the jump is recorded, not304334
// resolved here, since the innermost loop's own codegen (parser_control.c's305
-// while_statement / parser/C/statements/for_loop.c's for_statement) hasn't emitted the335
+// during_statement / parser/C/statements/each_loop.c's each_statement) hasn't emitted the306336
// "after the loop" landing point yet.307
-static void break_statement(void) {337
+static void stop_statement(void) {308338
advance_token();309
- expect(TOKEN_SEMICOLON, "expected ';' after 'break'");310
- loop_record_break(emit_jmp_rel32(code));339
+ expect(TOKEN_SEMICOLON, "expected ';' after 'stop'");340
+ loop_record_stop(emit_jmp_rel32(code));311341
}312342
313
-// continue ; -- jumps to the innermost loop's per-iteration advance314
-// step (a for-loop's index increment, or straight back to the condition315
-// for a while loop) -- also recorded, not resolved here, for the same316
-// reason as break above.317
-static void continue_statement(void) {343
+// next ; -- jumps to the innermost loop's per-iteration advance344
+// step (an each-loop's index increment, or straight back to the condition345
+// for a during loop) -- also recorded, not resolved here, for the same346
+// reason as stop above.347
+static void next_statement(void) {318348
advance_token();319
- expect(TOKEN_SEMICOLON, "expected ';' after 'continue'");320
- loop_record_continue(emit_jmp_rel32(code));349
+ expect(TOKEN_SEMICOLON, "expected ';' after 'next'");350
+ loop_record_next(emit_jmp_rel32(code));321351
}322352
323
-// statement := "print" expr ";"324
-// | "let" IDENT "=" expr ";"353
+// statement := "say" expr ";"354
+// | "local" IDENT "=" expr ";"325355
// | IDENT "=" expr ";"326
-// | "if" "(" expr ")" block ["else" block]327
-// | "while" "(" expr ")" block328
-// | "fn" IDENT "(" params ")" block329
-// | "return" [expr] ";"330
-// (milestone 5's entire statement grammar -- anything else is a parse331
-// error, per the project rule that malformed input always fails cleanly)356
+// | "given" ["("] expr [")"] block ["otherwise" block]357
+// | "during" ["("] expr [")"] block358
+// | "forge" IDENT "(" params ")" block359
+// | "yield" [expr] ";"360
+// (milestone 5's entire statement grammar, since renamed and given optional361
+// condition parens -- anything else is a parse error, per the project rule362
+// that malformed input always fails cleanly)332363
void statement(void) {333364
if (current.type == TOKEN_IMPORT) { import_statement(); return; }334365
imports_close_block();335366
336367
const char *ns; int ns_len;337368
get_import_namespace(&ns, &ns_len);338
- if (ns_len > 0 && block_depth == 0 && current.type != TOKEN_FN && current.type != TOKEN_LET) {339
- parse_error("only 'fn' and constant 'let' declarations are allowed at an imported file's top level");369
+ if (ns_len > 0 && block_depth == 0 && current.type != TOKEN_FORGE && current.type != TOKEN_LOCAL) {370
+ parse_error("only 'forge' and constant 'local' declarations are allowed at an imported file's top level");340371
}341372
342
- if (current.type == TOKEN_PRINT) {373
+ if (current.type == TOKEN_SAY) {343374
advance_token();344375
codegen_expression();345
- expect(TOKEN_SEMICOLON, "expected ';' after print statement");376
+ expect(TOKEN_SEMICOLON, "expected ';' after say statement");346377
codegen_print_top(code);347378
return;348379
}349
- if (current.type == TOKEN_LET) { let_statement(); return; }350
- if (current.type == TOKEN_IF) { if_statement(); return; }351
- if (current.type == TOKEN_WHILE) { while_statement(); return; }352
- if (current.type == TOKEN_FN) { fn_statement(); return; }353
- if (current.type == TOKEN_RETURN) { return_statement(); return; }354
- if (current.type == TOKEN_TRY) { try_statement(); return; }355
- if (current.type == TOKEN_FOR) { for_statement(); return; }356
- if (current.type == TOKEN_BREAK) { break_statement(); return; }357
- if (current.type == TOKEN_CONTINUE) { continue_statement(); return; }358
- if (current.type == TOKEN_THROW) { throw_statement(); return; }380
+ if (current.type == TOKEN_LOCAL) { local_statement(); return; }381
+ if (current.type == TOKEN_GIVEN) { given_statement(); return; }382
+ if (current.type == TOKEN_DURING) { during_statement(); return; }383
+ if (current.type == TOKEN_FORGE) { forge_statement(); return; }384
+ if (current.type == TOKEN_YIELD) { yield_statement(); return; }385
+ if (current.type == TOKEN_ATTEMPT) { attempt_statement(); return; }386
+ if (current.type == TOKEN_EACH) { each_statement(); return; }387
+ if (current.type == TOKEN_STOP) { stop_statement(); return; }388
+ if (current.type == TOKEN_NEXT) { next_statement(); return; }389
+ if (current.type == TOKEN_RAISE) { raise_statement(); return; }359390
if (current.type == TOKEN_IDENTIFIER) { assignment_statement(); return; }360
- parse_error("expected 'print', 'let', 'if', 'while', 'fn', 'return', 'try', 'for', 'break', 'continue', 'throw', '@import', or an assignment");391
+ parse_error("expected 'say', 'local', 'given', 'during', 'forge', 'yield', 'attempt', 'each', 'stop', 'next', 'raise', '@import', or an assignment");361392
}362393
363394
void compile_program(const char *source, const char *source_path, CodeBuf *out) {@@ -384,8 +415,8 @@ void compile_program(const char *source, const char *source_path, CodeBuf *out)384415
// Frame prologue: RBP anchors the top-level program's own variable385416
// slots. RSP drifts during expression push/pop; RBP never does. Each386417
// function call below establishes and tears down its own separate387
- // frame (see parser_control.c's fn_statement/parser.c's388
- // return_statement) without disturbing this one -- the callee always418
+ // frame (see parser_control.c's forge_statement/parser.c's419
+ // yield_statement) without disturbing this one -- the callee always389420
// restores RBP before returning. Milestone 5: each slot is now 16390421
// bytes (tag + payload, see codegen/H/emit/value.h), not 8.391422
emit_mov_reg_reg(code, REG_RBP, REG_RSP);kiln/src/parser/C/declarations/vars.cmodified
+12−12@@ -7,16 +7,16 @@ static int var_count = 0;77
static int scope_start = 0;88
99
// Scans newest-first (most-recently-declared wins), standard shadowing10
-// semantics -- required for try/catch: parser/C/statements/try_catch.c reserves each11
-// catch variable's slot with a synthetic placeholder name and renames it10
+// semantics -- required for attempt/handle: parser/C/statements/attempt_handle.c reserves each11
+// handle variable's slot with a synthetic placeholder name and renames it1212
// to the real name only after the fact (the slot's rbp-offset has to be13
-// known before the try body compiles, before the real name is even14
-// parsed -- see try_catch.c's header comment), so two SEQUENTIAL (not15
-// nested) `catch (e)` blocks end up with two separate table entries both16
-// named "e". A first-match-wins scan would bind the second catch body's17
-// references to the FIRST catch's stale slot; newest-first fixes that18
-// while leaving `let`'s own reuse-if-already-declared behavior unchanged19
-// (there's normally only one live entry for a given `let` name anyway).13
+// known before the attempt body compiles, before the real name is even14
+// parsed -- see attempt_handle.c's header comment), so two SEQUENTIAL (not15
+// nested) `handle (e)` blocks end up with two separate table entries both16
+// named "e". A first-match-wins scan would bind the second handle body's17
+// references to the FIRST handle's stale slot; newest-first fixes that18
+// while leaving `local`'s own reuse-if-already-declared behavior unchanged19
+// (there's normally only one live entry for a given `local` name anyway).2020
int resolve_var(const char *name, int len) {2121
for (int i = var_count - 1; i >= 0; i--) {2222
if ((int)strlen(var_names[i]) == len && strncmp(var_names[i], name, len) == 0) return i;@@ -70,12 +70,12 @@ int32_t call_scratch_offset(void) { return var_slot_tag_offset(MAX_KILN_VARS); }7070
7171
int32_t higher_order_scratch_offset(void) { return call_scratch_offset() - KILN_HIGHER_ORDER_SCRATCH_SIZE; }7272
73
-int32_t for_level_offset(int depth) {74
- return higher_order_scratch_offset() - KILN_FOR_STACK_SIZE + depth * KILN_FOR_LEVEL_SIZE;73
+int32_t each_level_offset(int depth) {74
+ return higher_order_scratch_offset() - KILN_EACH_STACK_SIZE + depth * KILN_EACH_LEVEL_SIZE;7575
}7676
7777
int32_t win_call_scratch_offset(void) {78
- return higher_order_scratch_offset() - KILN_FOR_STACK_SIZE - KILN_WIN_CALL_SCRATCH_SIZE;78
+ return higher_order_scratch_offset() - KILN_EACH_STACK_SIZE - KILN_WIN_CALL_SCRATCH_SIZE;7979
}8080
8181
int32_t win_outparam_offset(void) {kiln/src/parser/C/imports/import_paths.cmodified
+6−0@@ -50,8 +50,14 @@ int import_resolve_path(const char *raw_path, int raw_len,5050
return 0;5151
}5252
}53
+ // ash.libs/ (hand-written local libraries) wins first; a package54
+ // installed by forgepack (see ideas/ash_modules.md) only gets55
+ // tried if there's no local library shadowing the same name.5356
snprintf(candidate, sizeof(candidate), "%s/ash.libs/%.*s/%.*s.ash",5457
project_root_dir, raw_len, raw_path, raw_len, raw_path);58
+ if (realpath(candidate, canonical_out) != NULL) return 1;59
+ snprintf(candidate, sizeof(candidate), "%s/@ash-modules/%.*s/%.*s.ash",60
+ project_root_dir, raw_len, raw_path, raw_len, raw_path);5561
}5662
5763
if (realpath(candidate, canonical_out) == NULL) {kiln/src/parser/C/imports/imports.cmodified
+1−1@@ -177,7 +177,7 @@ void import_statement(void) {177177
resolving_depth++;178178
179179
// --- Save everything the recursive parse of the imported file will180
- // clobber, exactly mirroring vars_save/for_depth_save/loop_depth_save's180
+ // clobber, exactly mirroring vars_save/each_depth_save/loop_depth_save's181181
// existing save-recurse-restore pattern (the C call stack gives182182
// correct nesting for free, same as those). ---183183
DiagnosticsSource saved_diag = diagnostics_save_source();kiln/src/parser/C/statements/each_loop.cmodified
+2−2@@ -70,14 +70,14 @@ void each_statement(void) {7070
7171
loop_push();7272
block();73
- loop_patch_continues(); // continue lands here, right before i++73
+ loop_patch_nexts(); // next lands here, right before i++7474
7575
emit_load_mem_disp32(code, REG_RCX, REG_RBP, level + 16);7676
emit_add_reg_imm8(code, REG_RCX, 1);7777
emit_store_mem_disp32(code, REG_RBP, level + 16, REG_RCX); // i++7878
emit_jmp_back(code, loop_start);7979
emit_patch_jump(code, loop_exit);80
- loop_pop_and_patch_breaks(); // break lands here, after the loop80
+ loop_pop_and_patch_stops(); // stop lands here, after the loop8181
8282
each_nesting_depth--;8383
vars_scope_end(saved_scope);kiln/src/parser/C/statements/for_loop.cremoved
+0−82@@ -1,82 +0,0 @@1
-#include "parser/H/statements/for_loop.h"2
-#include "parser/H/core/parser.h"3
-#include "parser/H/core/parser_internal.h"4
-#include "parser/H/declarations/vars.h"5
-#include "parser/H/statements/loop_stack.h"6
-#include "codegen/H/expressions/expr.h"7
-#include "codegen/H/emit/emit.h"8
-9
-static int for_nesting_depth = 0;10
-11
-void for_depth_save(int *out) { *out = for_nesting_depth; }12
-void for_depth_reset(void) { for_nesting_depth = 0; }13
-void for_depth_restore(int saved) { for_nesting_depth = saved; }14
-15
-// for (x in array) { block }16
-//17
-// The array expression is evaluated once, up front; x is bound to each18
-// element in turn via the same var-slot mechanism `let` uses, so it's a19
-// perfectly ordinary variable inside the body (readable, reassignable,20
-// though reassigning it doesn't affect iteration -- the next iteration21
-// overwrites it again from the array, matching a plain for-each's usual22
-// semantics). data_ptr/count/i live in this nesting level's fixed23
-// RBP-relative slot (see vars.h's for_level_offset) so they survive24
-// whatever the body's own codegen does, including calls.25
-void for_statement(void) {26
- VarBlockScope saved_scope = vars_scope_begin();27
- advance_token(); // consume 'for'28
- expect(TOKEN_LPAREN, "expected '(' after 'for'");29
- expect(TOKEN_IDENTIFIER, "expected loop variable name");30
- const char *var_name = previous.start;31
- int var_len = previous.length;32
- expect(TOKEN_IN, "expected 'in' after loop variable");33
- codegen_expression(); // pushes the array (tag, payload)34
- expect(TOKEN_RPAREN, "expected ')' after loop expression");35
-36
- if (for_nesting_depth >= MAX_KILN_FOR_DEPTH) parse_error("too many nested for loops");37
- int32_t level = for_level_offset(for_nesting_depth);38
- for_nesting_depth++;39
-40
- emit_pop_reg(code, REG_RAX); // array payload (object address)41
- emit_pop_reg(code, REG_RBX); // array tag (ignored, assumed ARRAY)42
- emit_load_mem_disp32(code, REG_RCX, REG_RAX, 8); // count43
- emit_load_mem_disp32(code, REG_RDX, REG_RAX, 16); // data_ptr44
- emit_store_mem_disp32(code, REG_RBP, level + 0, REG_RDX);45
- emit_store_mem_disp32(code, REG_RBP, level + 8, REG_RCX);46
- emit_mov_reg_imm64(code, REG_RAX, 0);47
- emit_store_mem_disp32(code, REG_RBP, level + 16, REG_RAX); // i = 048
-49
- int slot = resolve_var_in_current_scope(var_name, var_len);50
- if (slot == -1) slot = declare_var(var_name, var_len);51
-52
- int loop_start = code->count;53
- emit_load_mem_disp32(code, REG_RCX, REG_RBP, level + 16); // i54
- emit_load_mem_disp32(code, REG_RDX, REG_RBP, level + 8); // count55
- emit_cmp_reg_reg(code, REG_RCX, REG_RDX);56
- int loop_exit = emit_jcc_rel32(code, COND_GE);57
-58
- emit_load_mem_disp32(code, REG_RSI, REG_RBP, level + 0); // data_ptr59
- emit_mov_reg_reg(code, REG_RAX, REG_RCX);60
- emit_mov_reg_imm64(code, REG_RDX, 16);61
- emit_imul_reg_reg(code, REG_RAX, REG_RDX);62
- emit_add_reg_reg(code, REG_RSI, REG_RAX); // element address63
-64
- emit_load_mem_disp32(code, REG_RBX, REG_RSI, 0); // element tag65
- emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload66
- emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);67
- emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);68
-69
- loop_push();70
- block();71
- loop_patch_continues(); // continue lands here, right before i++72
-73
- emit_load_mem_disp32(code, REG_RCX, REG_RBP, level + 16);74
- emit_add_reg_imm8(code, REG_RCX, 1);75
- emit_store_mem_disp32(code, REG_RBP, level + 16, REG_RCX); // i++76
- emit_jmp_back(code, loop_start);77
- emit_patch_jump(code, loop_exit);78
- loop_pop_and_patch_breaks(); // break lands here, after the loop79
-80
- for_nesting_depth--;81
- vars_scope_end(saved_scope);82
-}kiln/src/parser/C/statements/loop_stack.cmodified
+18−18@@ -5,44 +5,44 @@55
#define MAX_LOOP_JUMPS 6466
77
typedef struct {8
- int break_jumps[MAX_LOOP_JUMPS];9
- int break_count;10
- int continue_jumps[MAX_LOOP_JUMPS];11
- int continue_count;8
+ int stop_jumps[MAX_LOOP_JUMPS];9
+ int stop_count;10
+ int next_jumps[MAX_LOOP_JUMPS];11
+ int next_count;1212
} LoopContext;1313
1414
static LoopContext loop_stack[MAX_LOOP_NESTING];1515
static int loop_depth = 0;1616
1717
void loop_push(void) {1818
if (loop_depth >= MAX_LOOP_NESTING) parse_error("too many nested loops");19
- loop_stack[loop_depth].break_count = 0;20
- loop_stack[loop_depth].continue_count = 0;19
+ loop_stack[loop_depth].stop_count = 0;20
+ loop_stack[loop_depth].next_count = 0;2121
loop_depth++;2222
}2323
24
-void loop_record_break(int patch_offset) {25
- if (loop_depth == 0) parse_error("'break' outside a loop");24
+void loop_record_stop(int patch_offset) {25
+ if (loop_depth == 0) parse_error("'stop' outside a loop");2626
LoopContext *ctx = &loop_stack[loop_depth - 1];27
- if (ctx->break_count >= MAX_LOOP_JUMPS) parse_error("too many break statements in one loop");28
- ctx->break_jumps[ctx->break_count++] = patch_offset;27
+ if (ctx->stop_count >= MAX_LOOP_JUMPS) parse_error("too many stop statements in one loop");28
+ ctx->stop_jumps[ctx->stop_count++] = patch_offset;2929
}3030
31
-void loop_record_continue(int patch_offset) {32
- if (loop_depth == 0) parse_error("'continue' outside a loop");31
+void loop_record_next(int patch_offset) {32
+ if (loop_depth == 0) parse_error("'next' outside a loop");3333
LoopContext *ctx = &loop_stack[loop_depth - 1];34
- if (ctx->continue_count >= MAX_LOOP_JUMPS) parse_error("too many continue statements in one loop");35
- ctx->continue_jumps[ctx->continue_count++] = patch_offset;34
+ if (ctx->next_count >= MAX_LOOP_JUMPS) parse_error("too many next statements in one loop");35
+ ctx->next_jumps[ctx->next_count++] = patch_offset;3636
}3737
38
-void loop_patch_continues(void) {38
+void loop_patch_nexts(void) {3939
LoopContext *ctx = &loop_stack[loop_depth - 1];40
- for (int i = 0; i < ctx->continue_count; i++) emit_patch_jump(code, ctx->continue_jumps[i]);40
+ for (int i = 0; i < ctx->next_count; i++) emit_patch_jump(code, ctx->next_jumps[i]);4141
}4242
43
-void loop_pop_and_patch_breaks(void) {43
+void loop_pop_and_patch_stops(void) {4444
LoopContext *ctx = &loop_stack[loop_depth - 1];45
- for (int i = 0; i < ctx->break_count; i++) emit_patch_jump(code, ctx->break_jumps[i]);45
+ for (int i = 0; i < ctx->stop_count; i++) emit_patch_jump(code, ctx->stop_jumps[i]);4646
loop_depth--;4747
}4848
kiln/src/parser/C/statements/parser_control.cmodified
+39−28@@ -3,16 +3,17 @@33
#include "parser/H/core/parser_internal.h"44
#include "parser/H/declarations/vars.h"55
#include "parser/H/declarations/functions.h"6
-#include "parser/H/statements/for_loop.h"6
+#include "parser/H/statements/each_loop.h"77
#include "parser/H/statements/loop_stack.h"88
#include "codegen/H/expressions/expr.h"99
#include "codegen/H/emit/emit_sse.h"1010
#include "codegen/H/emit/value.h"1111
12
-// Truthiness test for if/while conditions: pop the value (tag discarded,13
-// assumed NUMBER -- matches ashvm's own truthy(), which requires a14
-// number too) and compare its payload against 0.0 via ucomisd (not GP15
-// cmp -- see emit_sse.h). COND_E afterward means "was zero, i.e. falsy".12
+// Truthiness test for given/during conditions: pop the value (tag13
+// discarded, assumed NUMBER -- matches ashvm's own truthy(), which14
+// requires a number too) and compare its payload against 0.0 via ucomisd15
+// (not GP cmp -- see emit_sse.h). COND_E afterward means "was zero, i.e.16
+// falsy".1617
static void pop_and_test_truthy(void) {1718
emit_pop_reg(code, REG_RAX); // payload1819
emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)@@ -21,49 +22,59 @@ static void pop_and_test_truthy(void) {2122
emit_ucomisd(code, XMM0, XMM1);2223
}2324
25
+// Conditions may be parenthesized or not (`given (x < n) {` and26
+// `given x < n {` both work) -- unlike ashvm, there's no superinstruction27
+// fusion here to keep firing either way, so this is just a plain optional28
+// token, no caller-side terminator-passing needed.29
+static int consume_optional_lparen(void) {30
+ if (current.type != TOKEN_LPAREN) return 0;31
+ advance_token();32
+ return 1;33
+}34
+2435
// The statement kinds involved enough to warrant their own file (mirrors2536
// ashvm/src/compiler/stmt_control.c's split from stmt.c for the same2637
// reason). Each function assumes statement()'s dispatch in parser.c2738
// already consumed the keyword token that identified it.2839
29
-void if_statement(void) {40
+void given_statement(void) {3041
advance_token();31
- expect(TOKEN_LPAREN, "expected '(' after 'if'");42
+ int parenthesized = consume_optional_lparen();3243
codegen_expression();33
- expect(TOKEN_RPAREN, "expected ')' after condition");44
+ if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");3445
pop_and_test_truthy();35
- int else_jump = emit_jcc_rel32(code, COND_E);46
+ int otherwise_jump = emit_jcc_rel32(code, COND_E);3647
3748
block();38
- if (current.type == TOKEN_ELSE) {49
+ if (current.type == TOKEN_OTHERWISE) {3950
int end_jump = emit_jmp_rel32(code);40
- emit_patch_jump(code, else_jump);51
+ emit_patch_jump(code, otherwise_jump);4152
advance_token();4253
block();4354
emit_patch_jump(code, end_jump);4455
} else {45
- emit_patch_jump(code, else_jump);56
+ emit_patch_jump(code, otherwise_jump);4657
}4758
}4859
49
-void while_statement(void) {60
+void during_statement(void) {5061
advance_token();51
- expect(TOKEN_LPAREN, "expected '(' after 'while'");5262
int loop_start = code->count;63
+ int parenthesized = consume_optional_lparen();5364
codegen_expression();54
- expect(TOKEN_RPAREN, "expected ')' after condition");65
+ if (parenthesized) expect(TOKEN_RPAREN, "expected ')' after condition");5566
pop_and_test_truthy();5667
int exit_jump = emit_jcc_rel32(code, COND_E);5768
5869
loop_push();5970
block();60
- loop_patch_continues(); // continue lands here, right before the loop-back71
+ loop_patch_nexts(); // next lands here, right before the loop-back6172
emit_jmp_back(code, loop_start);6273
emit_patch_jump(code, exit_jump);63
- loop_pop_and_patch_breaks(); // break lands here, after the loop74
+ loop_pop_and_patch_stops(); // stop lands here, after the loop6475
}6576
66
-// fn NAME ( [IDENT (, IDENT)*] ) block77
+// forge NAME ( [IDENT (, IDENT)*] ) block6778
//6879
// Function bodies are compiled inline into the SAME flat CodeBuf as6980
// top-level code (unlike ashvm, which gives each function its own Chunk)@@ -83,15 +94,15 @@ void while_statement(void) {8394
// establishes a fresh frame; the FIRST thing the body does is copy each8495
// argument's tag AND payload off the caller's pushed stack values into8596
// its own local slot, so afterward every variable access -- parameter or86
-// `let` -- goes through the exact same var_slot_*_offset() formulas.97
+// `local` -- goes through the exact same var_slot_*_offset() formulas.8798
// Because the last argument is the last one pushed (closest to the8899
// return address), and the first argument is deepest, parameter i's tag89100
// sits at [rbp + 16 + 16*(argc-1-i)] and its payload right after that90101
// (+16 skips the saved rbp and return address). Return value comes back91
-// as RBX=tag, RAX=payload (see parser.c's return_statement).92
-void fn_statement(void) {102
+// as RBX=tag, RAX=payload (see parser.c's yield_statement).103
+void forge_statement(void) {93104
advance_token();94
- expect(TOKEN_IDENTIFIER, "expected function name after 'fn'");105
+ expect(TOKEN_IDENTIFIER, "expected function name after 'forge'");95106
const char *name = previous.start;96107
int name_len = previous.length;97108
KilnFunction *fn = declare_function_in_context(name, name_len);@@ -120,9 +131,9 @@ void fn_statement(void) {120131
VarScope outer_vars;121132
vars_save(&outer_vars);122133
vars_clear();123
- int outer_for_depth;124
- for_depth_save(&outer_for_depth);125
- for_depth_reset();134
+ int outer_each_depth;135
+ each_depth_save(&outer_each_depth);136
+ each_depth_reset();126137
int outer_loop_depth;127138
loop_depth_save(&outer_loop_depth);128139
loop_depth_reset();@@ -150,9 +161,9 @@ void fn_statement(void) {150161
151162
block();152163
153
- // Implicit `return 0;` if the body falls through without one --164
+ // Implicit `yield 0;` if the body falls through without one --154165
// dead code on any path that already returned explicitly, harmless155
- // (matches ashvm's compile_fn_decl, which does the same166
+ // (matches ashvm's compile_forge_decl, which does the same156167
// unconditionally after compiling the body).157168
emit_mov_reg_imm64(code, REG_RAX, 0); // 0.0's bits are all zero158169
emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);@@ -161,7 +172,7 @@ void fn_statement(void) {161172
emit_ret(code);162173
163174
vars_restore(&outer_vars);164
- for_depth_restore(outer_for_depth);175
+ each_depth_restore(outer_each_depth);165176
loop_depth_restore(outer_loop_depth);166177
emit_patch_jump(code, skip_jump);167178
}kiln/src/parser/C/statements/try_catch.cremoved
+0−86@@ -1,86 +0,0 @@1
-#include "parser/H/core/parser.h"2
-#include "parser/H/core/parser_internal.h"3
-#include "parser/H/declarations/vars.h"4
-#include "codegen/H/emit/emit.h"5
-#include "codegen/H/runtime/errors.h"6
-#include "codegen/H/emit/layout.h"7
-#include "codegen/H/emit/runtime_layout.h"8
-9
-// try { A } catch (e) { B }10
-//11
-// A handler stack living in the fixed globals region (see12
-// elf/H/elf_writer.h) holds, per active try, the (rsp, rbp) to restore and13
-// the (target address, error-variable rbp-offset) to resume at -- kiln's14
-// own setjmp/longjmp equivalent, hand-rolled since this is a freestanding15
-// binary with no libc to call into (see codegen/C/runtime/errors.c's raise routine16
-// for the runtime half of this).17
-//18
-// The catch variable's slot has to be reserved BEFORE compiling A: its19
-// rbp-relative offset must be embedded into the handler struct at20
-// try-entry, which runs before A's body -- and therefore before `e`'s21
-// real name, which only appears after A -- is even parsed. A placeholder22
-// slot is declared up front and retroactively renamed once `catch (e)`23
-// is actually seen. try/catch is inline (like if/while), not a new call24
-// frame, so this works the same way if/while's own bodies share the25
-// enclosing function's variable table.26
-void try_statement(void) {27
- VarBlockScope saved_scope = vars_scope_begin();28
- advance_token(); // consume 'try'29
-30
- int catch_slot = declare_var("$catch", 6);31
-32
- // --- push handler (bounds-checked -- overflowing here would33
- // silently corrupt the code segment right after the handler table) ---34
- emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());35
- emit_load_mem_disp32(code, REG_RAX, REG_RAX, 0);36
- emit_cmp_reg_imm32(code, REG_RAX, MAX_KILN_TRY_DEPTH);37
- int overflow = emit_jcc_rel32(code, COND_GE);38
-39
- emit_mov_reg_imm64(code, REG_RCX, KILN_TRY_HANDLER_SIZE);40
- emit_imul_reg_reg(code, REG_RAX, REG_RCX); // RAX = index * handler size41
- emit_mov_reg_imm64(code, REG_RDI, kiln_try_handlers_addr());42
- emit_add_reg_reg(code, REG_RDI, REG_RAX); // RDI = this handler's slot address43
-44
- emit_store_mem_disp32(code, REG_RDI, 0, REG_RSP);45
- emit_store_mem_disp32(code, REG_RDI, 8, REG_RBP);46
- int patch_offset = emit_mov_reg_imm64_patchable(code, REG_RCX); // catch target, patched once known47
- emit_store_mem_disp32(code, REG_RDI, 16, REG_RCX);48
- emit_mov_reg_imm64(code, REG_RCX, (uint64_t)(int64_t)var_slot_tag_offset(catch_slot));49
- emit_store_mem_disp32(code, REG_RDI, 24, REG_RCX);50
-51
- emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());52
- emit_load_mem_disp32(code, REG_RCX, REG_RAX, 0);53
- emit_add_reg_imm8(code, REG_RCX, 1);54
- emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);55
-56
- int skip_overflow_die = emit_jmp_rel32(code);57
- emit_patch_jump(code, overflow);58
- errors_emit_fatal(code, "runtime error: too many nested try blocks");59
- emit_patch_jump(code, skip_overflow_die);60
-61
- block(); // compiles A62
-63
- // Normal completion (no error was raised): pop the handler -- past64
- // this point, an error inside the enclosing scope should NOT be65
- // caught by this try 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);70
-71
- int skip_catch = emit_jmp_rel32(code);72
- int catch_code_offset = code->count;73
- uint64_t catch_addr = kiln_code_base() + (uint64_t)catch_code_offset;74
- emit_patch_imm64(code, patch_offset, catch_addr);75
-76
- expect(TOKEN_CATCH, "expected 'catch' after try block");77
- expect(TOKEN_LPAREN, "expected '(' after 'catch'");78
- expect(TOKEN_IDENTIFIER, "expected error variable name");79
- rename_var(catch_slot, previous.start, previous.length);80
- expect(TOKEN_RPAREN, "expected ')' after catch variable");81
-82
- block(); // compiles B -- `e` already declared at catch_slot above83
-84
- emit_patch_jump(code, skip_catch);85
- vars_scope_end(saved_scope);86
-}kiln/src/parser/D/statements/try_catch.d → kiln/src/parser/D/statements/attempt_handle.drenamed
+3−2@@ -1,5 +1,6 @@1
-src/parser/O/statements/try_catch.o: src/parser/C/statements/try_catch.c \2
- src/parser/H/core/parser.h src/lexer/H/lexer.h src/codegen/H/emit/emit.h \1
+src/parser/O/statements/attempt_handle.o: \2
+ src/parser/C/statements/attempt_handle.c src/parser/H/core/parser.h \3
+ src/lexer/H/lexer.h src/codegen/H/emit/emit.h \34
src/parser/H/core/parser_internal.h src/parser/H/declarations/vars.h \45
src/codegen/H/runtime/errors.h src/codegen/H/emit/layout.h \56
src/codegen/H/emit/runtime_layout.hkiln/src/parser/D/statements/for_loop.d → kiln/src/parser/D/statements/each_loop.drenamed
+3−3@@ -1,9 +1,9 @@1
-src/parser/O/statements/for_loop.o: src/parser/C/statements/for_loop.c \2
- src/parser/H/statements/for_loop.h src/parser/H/core/parser.h \1
+src/parser/O/statements/each_loop.o: src/parser/C/statements/each_loop.c \2
+ src/parser/H/statements/each_loop.h 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 \55
src/parser/H/statements/loop_stack.h src/codegen/H/expressions/expr.h6
-src/parser/H/statements/for_loop.h:6
+src/parser/H/statements/each_loop.h:77
src/parser/H/core/parser.h:88
src/lexer/H/lexer.h:99
src/codegen/H/emit/emit.h:kiln/src/parser/D/statements/parser_control.dmodified
+5−4@@ -2,16 +2,17 @@ src/parser/O/statements/parser_control.o: \22
src/parser/C/statements/parser_control.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/parser/H/declarations/functions.h src/parser/H/statements/for_loop.h \6
- src/parser/H/statements/loop_stack.h src/codegen/H/expressions/expr.h \7
- src/codegen/H/emit/emit_sse.h src/codegen/H/emit/value.h5
+ src/parser/H/declarations/functions.h \6
+ src/parser/H/statements/each_loop.h src/parser/H/statements/loop_stack.h \7
+ src/codegen/H/expressions/expr.h src/codegen/H/emit/emit_sse.h \8
+ src/codegen/H/emit/value.h89
src/parser/H/core/parser.h:910
src/lexer/H/lexer.h:1011
src/codegen/H/emit/emit.h:1112
src/parser/H/core/parser_internal.h:1213
src/parser/H/declarations/vars.h:1314
src/parser/H/declarations/functions.h:14
-src/parser/H/statements/for_loop.h:15
+src/parser/H/statements/each_loop.h:1516
src/parser/H/statements/loop_stack.h:1617
src/codegen/H/expressions/expr.h:1718
src/codegen/H/emit/emit_sse.h:kiln/src/parser/H/core/parser_internal.hmodified
+6−6@@ -2,15 +2,15 @@22
#define KILN_PARSER_INTERNAL_H33
44
// Cross-file link between parser.c (state, simple statements, the5
-// statement() dispatcher) and parser_control.c (if/while/fn -- the5
+// statement() dispatcher) and parser_control.c (given/during/forge -- the66
// statement kinds involved enough to warrant their own file), not part of77
// the public API (parser.h).88
void block(void);9
-void if_statement(void);10
-void while_statement(void);11
-void fn_statement(void);12
-void try_statement(void);13
-void for_statement(void);9
+void given_statement(void);10
+void during_statement(void);11
+void forge_statement(void);12
+void attempt_statement(void);13
+void each_statement(void);1414
1515
// statement() itself (parser.c's dispatcher) also needs to be callable1616
// from imports.c's parse_imported_file_body(), which drives the samekiln/src/parser/H/declarations/constants.hmodified
+1−1@@ -26,7 +26,7 @@ KilnConstant *declare_number_constant(const char *name, int len, double value);2626
2727
// Same, for a string constant. str_bytes/str_len are the RAW (unescaped)2828
// source token bytes -- constants.c does not own or copy this pointer29
-// beyond storing it; the caller (parser.c's let_statement) must pass29
+// beyond storing it; the caller (parser.c's local_statement) must pass3030
// bytes from a buffer that outlives the whole compile (the imported3131
// file's source buffer, which imports.c already never frees -- see3232
// imports.c's read_import_file comment).kiln/src/parser/H/declarations/functions.hmodified
+2−2@@ -32,7 +32,7 @@ KilnFunction *resolve_function(const char *name, int len);3232
KilnFunction *declare_function(const char *name, int len);3333
3434
// --- Import namespace context (parser/C/imports/imports.c sets/restores this around35
-// each recursively-parsed imported file; parser_control.c's fn_statement35
+// each recursively-parsed imported file; parser_control.c's forge_statement3636
// and codegen/C/expressions/expr.c + expr_call.c's call-site resolution consult it) ---3737
3838
// Sets the namespace prefix every subsequent declare_function_in_context/@@ -45,7 +45,7 @@ void set_import_namespace(const char *ns, int len);4545
void get_import_namespace(const char **out_ns, int *out_len);4646
4747
// Declares NAME under "ns.NAME" if a namespace is currently set, else48
-// under the bare NAME -- the single call site fn_statement() uses,48
+// under the bare NAME -- the single call site forge_statement() uses,4949
// replacing a direct declare_function() call.5050
KilnFunction *declare_function_in_context(const char *name, int len);5151
kiln/src/parser/H/declarations/vars.hmodified
+21−20@@ -16,10 +16,10 @@ int resolve_var(const char *name, int len);1616
int declare_var(const char *name, int len);1717
1818
// Milestone 9: overwrites an already-declared slot's name in place (same19
-// slot index, new name) -- parser/C/statements/try_catch.c needs a catch variable's20
-// slot reserved BEFORE its real name (`catch (e)`) is parsed, since the19
+// slot index, new name) -- parser/C/statements/attempt_handle.c needs a handle variable's20
+// slot reserved BEFORE its real name (`handle (e)`) is parsed, since the2121
// slot's rbp-relative offset must be embedded into the handler struct at22
-// try-entry, ahead of the try block's body.22
+// attempt-entry, ahead of the attempt block's body.2323
void rename_var(int slot, const char *name, int len);2424
2525
// Milestone 5: each variable now holds a (tag, payload) pair (16 bytes,@@ -49,19 +49,20 @@ void vars_restore(const VarScope *saved);4949
5050
// Block scoping: `scope_start` marks the slot index where the innermost5151
// active block began. vars_scope_begin/end bracket a single block() call52
-// (or, for for/try statements whose own loop/catch variable is declared53
-// outside their nested block() call -- see for_loop.c/try_catch.c -- the54
-// whole statement). Same save-an-int-on-the-C-call-stack pattern as55
-// for_depth_save/restore and loop_depth_save/restore above: block() nests56
-// via ordinary recursive descent, so the C call stack gives correct57
-// nesting for free, no explicit stack structure needed.52
+// (or, for each/attempt statements whose own loop/handle variable is53
+// declared outside their nested block() call -- see54
+// each_loop.c/attempt_handle.c -- the whole statement). Same55
+// save-an-int-on-the-C-call-stack pattern as each_depth_save/restore and56
+// loop_depth_save/restore above: block() nests via ordinary recursive57
+// descent, so the C call stack gives correct nesting for free, no explicit58
+// stack structure needed.5859
typedef struct { int count_mark; int prev_scope_start; } VarBlockScope;5960
6061
VarBlockScope vars_scope_begin(void);6162
void vars_scope_end(VarBlockScope saved);6263
6364
// Like resolve_var, but only considers slots >= scope_start (the CURRENT64
-// block's own declarations). let_statement/for_statement use this for65
+// block's own declarations). local_statement/each_statement use this for6566
// their "reuse this slot or declare fresh" decision: a name that only6667
// exists in an ENCLOSING scope must shadow (fresh slot), not mutate.6768
int resolve_var_in_current_scope(const char *name, int len);@@ -91,21 +92,21 @@ int32_t call_scratch_offset(void);9192
#define KILN_HIGHER_ORDER_SCRATCH_SIZE 1289293
int32_t higher_order_scratch_offset(void);9394
94
-// Milestone 11: `for (x in array)` loop state (data_ptr, count, running95
+// Milestone 11: `each (x in array)` loop state (data_ptr, count, running9596
// index) also needs to survive across the loop body's own codegen -- but96
-// unlike try/catch's runtime recursion, for-loop NESTING is fully known97
-// at COMPILE TIME (nested `for` statements are literally nested calls in97
+// unlike attempt/handle's runtime recursion, each-loop NESTING is fully known98
+// at COMPILE TIME (nested `each` statements are literally nested calls in9899
// this single-pass compiler), so each nesting level just gets its own99100
// fixed offset rather than a runtime-tracked stack. Bounded the same way100
-// every other table here is; parser/C/statements/for_loop.c resets its nesting101
+// every other table here is; parser/C/statements/each_loop.c resets its nesting101102
// counter to 0 when entering a function or lambda body (mirroring102103
// vars_save/vars_clear/vars_restore), since that body gets its own RBP103104
// at runtime and can safely reuse the same offsets independently of how104
-// deep the enclosing code's own `for` nesting was.105
-#define MAX_KILN_FOR_DEPTH 8106
-#define KILN_FOR_LEVEL_SIZE 24107
-#define KILN_FOR_STACK_SIZE (MAX_KILN_FOR_DEPTH * KILN_FOR_LEVEL_SIZE)108
-int32_t for_level_offset(int depth);105
+// deep the enclosing code's own `each` nesting was.106
+#define MAX_KILN_EACH_DEPTH 8107
+#define KILN_EACH_LEVEL_SIZE 24108
+#define KILN_EACH_STACK_SIZE (MAX_KILN_EACH_DEPTH * KILN_EACH_LEVEL_SIZE)109
+int32_t each_level_offset(int depth);109110
110111
// Plan A, phase A3: one 8-byte RBP-relative slot for codegen/C/platform/win_call.c to111112
// stash the original RSP across a Windows API call (dynamic 16-byte align@@ -131,7 +132,7 @@ int32_t win_call_scratch_offset(void);131132
int32_t win_outparam_offset(void);132133
133134
#define KILN_FRAME_RESERVE \134
- ((MAX_KILN_VARS + 1) * 16 + KILN_HIGHER_ORDER_SCRATCH_SIZE + KILN_FOR_STACK_SIZE + \135
+ ((MAX_KILN_VARS + 1) * 16 + KILN_HIGHER_ORDER_SCRATCH_SIZE + KILN_EACH_STACK_SIZE + \135136
KILN_WIN_CALL_SCRATCH_SIZE + KILN_WIN_OUTPARAM_SCRATCH_SIZE)136137
137138
#endifkiln/src/parser/H/imports/imports.hmodified
+1−1@@ -9,7 +9,7 @@ void imports_init(const char *entry_file_path);99
1010
// Parses one `@import <path>;` statement. Assumes statement()'s1111
// dispatcher has NOT yet consumed the TOKEN_IMPORT (this function does12
-// that itself, matching let_statement()'s own convention).12
+// that itself, matching local_statement()'s own convention).1313
void import_statement(void);1414
1515
// Called by statement() on every non-import statement dispatch. No-opkiln/src/parser/H/statements/for_loop.hremoved
+0−19@@ -1,19 +0,0 @@1
-#ifndef KILN_FOR_LOOP_H2
-#define KILN_FOR_LOOP_H3
-4
-// for_statement itself is declared in parser_internal.h alongside5
-// if_statement/while_statement/fn_statement/try_statement (the other6
-// statement-compiler cross-file links) -- this header exists only for7
-// the depth counter below, which fn_statement/lambda.c also need.8
-//9
-// The compile-time for-nesting-depth counter's save/reset/restore, for10
-// fn_statement/lambda.c to wrap around a called body's compilation (same11
-// spirit as parser/H/declarations/vars.h's vars_save/vars_clear/vars_restore): a12
-// function or lambda body gets its own RBP at runtime, so its own `for`13
-// loops can safely reuse the same offsets independently of how deep the14
-// enclosing code's nesting was -- see vars.h's for_level_offset comment.15
-void for_depth_save(int *out);16
-void for_depth_reset(void);17
-void for_depth_restore(int saved);18
-19
-#endifkiln/src/parser/H/statements/loop_stack.hmodified
+17−17@@ -1,43 +1,43 @@11
#ifndef KILN_LOOP_STACK_H22
#define KILN_LOOP_STACK_H33
4
-// break/continue need to know which loop they belong to, and loops5
-// nest -- but unlike for_loop.h's runtime scratch state, break/continue4
+// stop/next need to know which loop they belong to, and loops5
+// nest -- but unlike each_loop.h's runtime scratch state, stop/next66
// need NO runtime state at all: they're pure "emit now, patch later"77
// jump bookkeeping (same primitive every other forward jump in this88
// project uses), just tracked per-nesting-level at compile time instead99
// of resolved immediately.1010
//11
-// continue can't simply backward-jump to a known offset the way a plain12
-// loop back-edge does: `continue`'s target (the per-iteration advance13
-// step -- a for-loop's index increment, or nothing for a while loop) is14
-// only emitted AFTER the body compiles, i.e. after the continue15
-// statement itself. So continue jumps are forward-patched too, resolved11
+// next can't simply backward-jump to a known offset the way a plain12
+// loop back-edge does: `next`'s target (the per-iteration advance13
+// step -- an each-loop's index increment, or nothing for a during loop)14
+// is only emitted AFTER the body compiles, i.e. after the next15
+// statement itself. So next jumps are forward-patched too, resolved1616
// by the loop's own codegen right after block() returns, before1717
// whatever per-iteration bookkeeping runs next.1818
1919
// Pushes a fresh loop context (call right before compiling the body).2020
void loop_push(void);2121
22
-// Called from parser.c's break_statement()/continue_statement() --22
+// Called from parser.c's stop_statement()/next_statement() --2323
// records a forward jump's patch offset into the innermost loop's list.2424
// parse_error()s (noreturn) if called outside any loop.25
-void loop_record_break(int patch_offset);26
-void loop_record_continue(int patch_offset);25
+void loop_record_stop(int patch_offset);26
+void loop_record_next(int patch_offset);2727
28
-// Patches every recorded continue jump to the CURRENT code position --28
+// Patches every recorded next jump to the CURRENT code position --2929
// call right after block() returns, before emitting the per-iteration3030
// advance step.31
-void loop_patch_continues(void);31
+void loop_patch_nexts(void);3232
33
-// Patches every recorded break jump to the CURRENT code position, then33
+// Patches every recorded stop jump to the CURRENT code position, then3434
// pops the loop context -- call once the loop is fully compiled.35
-void loop_pop_and_patch_breaks(void);35
+void loop_pop_and_patch_stops(void);3636
37
-// Save/reset/restore the loop-nesting depth, for fn_statement/lambda.c37
+// Save/reset/restore the loop-nesting depth, for forge_statement/lambda.c3838
// to wrap around a called body's compilation (same spirit as39
-// parser/H/declarations/vars.h's vars_save/vars_clear/vars_restore and for_loop.h's40
-// for_depth_save/reset/restore): a break/continue inside a function or39
+// parser/H/declarations/vars.h's vars_save/vars_clear/vars_restore and each_loop.h's40
+// each_depth_save/reset/restore): a stop/next inside a function or4141
// lambda body must never reach past that body into an ENCLOSING loop it4242
// happens to be textually written inside.4343
void loop_depth_save(int *out);kiln/tests/cases/basics/bare_reassign.ashadded
+10−0@@ -0,0 +1,10 @@1
+forge f(i) {2
+ i = i + 1;3
+ i = i * 2;4
+ yield i;5
+}6
+say f(5);7
+8
+local x = 1;9
+x = x + 10;10
+say x;kiln/tests/cases/basics/bitwise.ashmodified
+9−9@@ -1,9 +1,9 @@1
-print 5 & 3;2
-print 5 | 2;3
-print 5 ^ 1;4
-print 1 << 4;5
-print 256 >> 4;6
-print ~0;7
-print ~5;8
-print -8 >> 1;9
-print (5 & 3) == 1;1
+say 5 & 3;2
+say 5 | 2;3
+say 5 ^ 1;4
+say 1 << 4;5
+say 256 >> 4;6
+say ~0;7
+say ~5;8
+say -8 >> 1;9
+say (5 & 3) == 1;kiln/tests/cases/basics/block_comments.ashmodified
+5−5@@ -1,10 +1,10 @@11
/* this is a block comment */2
-print 1;2
+say 1;33
/* multi44
line55
comment */6
-print 2;7
-print /* inline */ 3;8
-let x = 5; /* trailing */ print x;6
+say 2;7
+say /* inline */ 3;8
+local x = 5; /* trailing */ say x;99
// still works10
-print 4;10
+say 4;kiln/tests/cases/basics/compound_assignment.ashmodified
+16−16@@ -1,32 +1,32 @@1
-let x = 5;1
+local x = 5;22
x += 3;3
-print x;3
+say x;44
x -= 2;5
-print x;5
+say x;66
x *= 4;7
-print x;7
+say x;88
x /= 3;9
-print x;9
+say x;1010
11
-let s = "hello";11
+local s = "hello";1212
s += " world";13
-print s;13
+say s;1414
15
-let i = 0;15
+local i = 0;1616
i++;1717
i++;18
-print i;18
+say i;1919
i--;20
-print i;20
+say i;2121
22
-let total = 0;23
-for (n in [1,2,3,4,5]) {22
+local total = 0;23
+each (n in [1,2,3,4,5]) {2424
total += n;2525
}26
-print total;26
+say total;2727
28
-let j = 0;29
-while (j < 5) {28
+local j = 0;29
+during (j < 5) {3030
j++;3131
}32
-print j;32
+say j;kiln/tests/cases/basics/exponent.ashmodified
+9−9@@ -1,9 +1,9 @@1
-print 2 ** 10;2
-print 2 ** 0;3
-print 5 ** 1;4
-print 2 ** -2;5
-print -2 ** 2;6
-print 3 ** 3;7
-print 2 ** 3 ** 2;8
-print 2 ** 3 * 4;9
-print (2 ** 3) * 4;1
+say 2 ** 10;2
+say 2 ** 0;3
+say 5 ** 1;4
+say 2 ** -2;5
+say -2 ** 2;6
+say 3 ** 3;7
+say 2 ** 3 ** 2;8
+say 2 ** 3 * 4;9
+say (2 ** 3) * 4;kiln/tests/cases/basics/floats.ashmodified
+16−16@@ -1,22 +1,22 @@1
-print 10 / 4;2
-print 0.5;3
-print -3.25;4
-print 1.5 + 2.25;5
-print 0.1 + 0.2;1
+say 10 / 4;2
+say 0.5;3
+say -3.25;4
+say 1.5 + 2.25;5
+say 0.1 + 0.2;66
7
-let x = 2.5;8
-print x < 3;9
-print x > 3;7
+local x = 2.5;8
+say x < 3;9
+say x > 3;1010
11
-fn avg(a, b) {12
- return (a + b) / 2;11
+forge avg(a, b) {12
+ yield (a + b) / 2;1313
}14
-print avg(1, 2);14
+say avg(1, 2);1515
16
-fn half(n) {17
- if (n < 1) {18
- return n;16
+forge half(n) {17
+ given (n < 1) {18
+ yield n;1919
}20
- return half(n / 2) + 0.1;20
+ yield half(n / 2) + 0.1;2121
}22
-print half(4);22
+say half(4);kiln/tests/cases/basics/hello.ashmodified
+1−1@@ -1 +1 @@1
-print 1 + 2 * 3;1
+say 1 + 2 * 3;kiln/tests/cases/basics/math_builtins.ashmodified
+8−8@@ -1,8 +1,8 @@1
-print sqrt(16);2
-print sqrt(2);3
-print abs(-5.5);4
-print abs(5.5);5
-print floor(3.7);6
-print floor(-3.7);7
-print floor(3.0);8
-print floor(-3.0);1
+say sqrt(16);2
+say sqrt(2);3
+say abs(-5.5);4
+say abs(5.5);5
+say floor(3.7);6
+say floor(-3.7);7
+say floor(3.0);8
+say floor(-3.0);kiln/tests/cases/basics/modulo.ashadded
+7−0@@ -0,0 +1,7 @@1
+say 7 % 3;2
+say -7 % 3;3
+say 7 % -3;4
+say -7 % -3;5
+say 7.5 % 2;6
+say 10 % 2 == 0;7
+say 10 % 3 == 1;kiln/tests/cases/basics/numeric_literals.ashmodified
+10−10@@ -1,10 +1,10 @@1
-print 0xF;2
-print 0x1A;3
-print 0xFF;4
-print 1e3;5
-print 1.5e2;6
-print 2e-2;7
-print 1_000_000;8
-print 1_000.5;9
-print 0x10 + 1;10
-print 3.14159;1
+say 0xF;2
+say 0x1A;3
+say 0xFF;4
+say 1e3;5
+say 1.5e2;6
+say 2e-2;7
+say 1_000_000;8
+say 1_000.5;9
+say 0x10 + 1;10
+say 3.14159;kiln/tests/cases/basics/ternary.ashmodified
+10−10@@ -1,10 +1,10 @@1
-print true ? "yes" : "no";2
-print false ? "yes" : "no";3
-let x = 5;4
-print x > 3 ? "big" : "small";5
-print x < 3 ? "big" : x < 10 ? "medium" : "huge";6
-let m = { "a": true, "b": false };7
-print m["a"] ? "a is true" : "a is false";8
-fn max(a, b) { return a > b ? a : b; }9
-print max(3, 7);10
-print max(10, 2);1
+say yes ? "yes" : "no";2
+say no ? "yes" : "no";3
+local x = 5;4
+say x > 3 ? "big" : "small";5
+say x < 3 ? "big" : x < 10 ? "medium" : "huge";6
+local m = { "a": yes, "b": no };7
+say m["a"] ? "a is true" : "a is false";8
+forge max(a, b) { yield a > b ? a : b; }9
+say max(3, 7);10
+say max(10, 2);kiln/tests/cases/basics/true_false_nil.ashmodified
+14−14@@ -1,14 +1,14 @@1
-print true;2
-print false;3
-print nil;4
-print true == 1;5
-print false == 0;6
-print nil == nil;7
-print nil == 0;8
-print nil == false;9
-if (true) { print "yes"; }10
-if (false) { print "no"; } else { print "else"; }11
-let x = nil;12
-if (x) { print "truthy"; } else { print "falsy nil"; }13
-let y = true;14
-print y;1
+say yes;2
+say no;3
+say none;4
+say yes == 1;5
+say no == 0;6
+say none == none;7
+say none == 0;8
+say none == no;9
+given (yes) { say "yes"; }10
+given (no) { say "no"; } otherwise { say "else"; }11
+local x = none;12
+given (x) { say "truthy"; } otherwise { say "falsy nil"; }13
+local y = yes;14
+say y;kiln/tests/cases/basics/type.ashadded
+8−0@@ -0,0 +1,8 @@1
+say type(5);2
+say type("hi");3
+say type([1, 2]);4
+say type({"a": 1});5
+say type(none);6
+say type(yes);7
+forge f() { yield 1; }8
+say type(f);kiln/tests/cases/basics/unary_not.ashmodified
+7−7@@ -1,7 +1,7 @@1
-print !false;2
-print !true;3
-print !0;4
-print !5;5
-print !nil;6
-print !!5;7
-if (!false) { print "works"; }1
+say not no;2
+say not yes;3
+say not 0;4
+say not 5;5
+say not none;6
+say not not 5;7
+given (not no) { say "works"; }kiln/tests/cases/collections/arrays.ashmodified
+18−18@@ -1,28 +1,28 @@1
-let a = [1, 2, 3];2
-print a[0];3
-print a[1];4
-print a[2];5
-print len(a);1
+local a = [1, 2, 3];2
+say a[0];3
+say a[1];4
+say a[2];5
+say len(a);66
77
a[0] = 99;8
-print a[0];8
+say a[0];99
10
-let b = ["x", "y", "z"];11
-print b[1];10
+local b = ["x", "y", "z"];11
+say b[1];1212
13
-let c = [1];13
+local c = [1];1414
push(c, 2);1515
push(c, 3);16
-print len(c);17
-print c[0];18
-print c[1];19
-print c[2];16
+say len(c);17
+say c[0];18
+say c[1];19
+say c[2];2020
21
-let d = [];22
-let i = 0;23
-while (i < 10) {21
+local d = [];22
+local i = 0;23
+during (i < 10) {2424
push(d, i);2525
i = i + 1;2626
}27
-print len(d);28
-print d[9];27
+say len(d);28
+say d[9];kiln/tests/cases/collections/keys_values.ashmodified
+14−14@@ -1,15 +1,15 @@1
-let m = {"a": 1, "b": 2, "c": 3};2
-let ks = keys(m);3
-let vs = values(m);4
-print len(ks);5
-print len(vs);6
-print ks[0];7
-print ks[1];8
-print ks[2];9
-print vs[0];10
-print vs[1];11
-print vs[2];1
+local m = {"a": 1, "b": 2, "c": 3};2
+local ks = keys(m);3
+local vs = values(m);4
+say len(ks);5
+say len(vs);6
+say ks[0];7
+say ks[1];8
+say ks[2];9
+say vs[0];10
+say vs[1];11
+say vs[2];1212
13
-let empty_m = {};14
-print len(keys(empty_m));15
-print len(values(empty_m));13
+local empty_m = {};14
+say len(keys(empty_m));15
+say len(values(empty_m));kiln/tests/cases/collections/maps.ashmodified
+13−13@@ -1,21 +1,21 @@1
-let m = {"a": 1, "b": 2};2
-print m["a"];3
-print m["b"];1
+local m = {"a": 1, "b": 2};2
+say m["a"];3
+say m["b"];44
55
m["c"] = 3;6
-print m["c"];6
+say m["c"];77
88
m["a"] = 99;9
-print m["a"];9
+say m["a"];1010
11
-print has(m, "b");12
-print has(m, "z");11
+say has(m, "b");12
+say has(m, "z");1313
1414
delete(m, "b");15
-print has(m, "b");16
-print m["c"];15
+say has(m, "b");16
+say m["c"];1717
18
-let g = {};18
+local g = {};1919
g["a"] = 1;2020
g["b"] = 2;2121
g["c"] = 3;@@ -26,6 +26,6 @@ g["g"] = 7;2626
g["h"] = 8;2727
g["i"] = 9;2828
g["j"] = 10;29
-print g["a"];30
-print g["e"];31
-print g["j"];29
+say g["a"];30
+say g["e"];31
+say g["j"];kiln/tests/cases/control_flow/block_scoping.ashmodified
+33−33@@ -1,41 +1,41 @@1
-let x = 1;2
-if (true) {3
- let x = 2;4
- print x;1
+local x = 1;2
+given (yes) {3
+ local x = 2;4
+ say x;55
}6
-print x;6
+say x;77
8
-let y = 10;9
-while (y < 13) {10
- let z = y * 2;11
- print z;8
+local y = 10;9
+during (y < 13) {10
+ local z = y * 2;11
+ say z;1212
y = y + 1;1313
}1414
15
-// 20 if-blocks, each declaring 4 UNIQUE-named variables (80 total) --15
+// 20 given-blocks, each declaring 4 UNIQUE-named variables (80 total) --1616
// leaking would exceed MAX_KILN_VARS (64) and fail to compile; scoped1717
// slot reuse keeps this well under the cap. Verified during planning:18
-// this exact file (with the shadowing let above already showing the18
+// this exact file (with the shadowing local above already showing the1919
// bug) fails pre-fix with "error: too many variables" at block index 15.20
-let count = 0;21
-if (true) { let a0=1; let b0=2; let c0=3; let d0=4; count = count + 1; }22
-if (true) { let a1=1; let b1=2; let c1=3; let d1=4; count = count + 1; }23
-if (true) { let a2=1; let b2=2; let c2=3; let d2=4; count = count + 1; }24
-if (true) { let a3=1; let b3=2; let c3=3; let d3=4; count = count + 1; }25
-if (true) { let a4=1; let b4=2; let c4=3; let d4=4; count = count + 1; }26
-if (true) { let a5=1; let b5=2; let c5=3; let d5=4; count = count + 1; }27
-if (true) { let a6=1; let b6=2; let c6=3; let d6=4; count = count + 1; }28
-if (true) { let a7=1; let b7=2; let c7=3; let d7=4; count = count + 1; }29
-if (true) { let a8=1; let b8=2; let c8=3; let d8=4; count = count + 1; }30
-if (true) { let a9=1; let b9=2; let c9=3; let d9=4; count = count + 1; }31
-if (true) { let a10=1; let b10=2; let c10=3; let d10=4; count = count + 1; }32
-if (true) { let a11=1; let b11=2; let c11=3; let d11=4; count = count + 1; }33
-if (true) { let a12=1; let b12=2; let c12=3; let d12=4; count = count + 1; }34
-if (true) { let a13=1; let b13=2; let c13=3; let d13=4; count = count + 1; }35
-if (true) { let a14=1; let b14=2; let c14=3; let d14=4; count = count + 1; }36
-if (true) { let a15=1; let b15=2; let c15=3; let d15=4; count = count + 1; }37
-if (true) { let a16=1; let b16=2; let c16=3; let d16=4; count = count + 1; }38
-if (true) { let a17=1; let b17=2; let c17=3; let d17=4; count = count + 1; }39
-if (true) { let a18=1; let b18=2; let c18=3; let d18=4; count = count + 1; }40
-if (true) { let a19=1; let b19=2; let c19=3; let d19=4; count = count + 1; }41
-print count;20
+local count = 0;21
+given (yes) { local a0=1; local b0=2; local c0=3; local d0=4; count = count + 1; }22
+given (yes) { local a1=1; local b1=2; local c1=3; local d1=4; count = count + 1; }23
+given (yes) { local a2=1; local b2=2; local c2=3; local d2=4; count = count + 1; }24
+given (yes) { local a3=1; local b3=2; local c3=3; local d3=4; count = count + 1; }25
+given (yes) { local a4=1; local b4=2; local c4=3; local d4=4; count = count + 1; }26
+given (yes) { local a5=1; local b5=2; local c5=3; local d5=4; count = count + 1; }27
+given (yes) { local a6=1; local b6=2; local c6=3; local d6=4; count = count + 1; }28
+given (yes) { local a7=1; local b7=2; local c7=3; local d7=4; count = count + 1; }29
+given (yes) { local a8=1; local b8=2; local c8=3; local d8=4; count = count + 1; }30
+given (yes) { local a9=1; local b9=2; local c9=3; local d9=4; count = count + 1; }31
+given (yes) { local a10=1; local b10=2; local c10=3; local d10=4; count = count + 1; }32
+given (yes) { local a11=1; local b11=2; local c11=3; local d11=4; count = count + 1; }33
+given (yes) { local a12=1; local b12=2; local c12=3; local d12=4; count = count + 1; }34
+given (yes) { local a13=1; local b13=2; local c13=3; local d13=4; count = count + 1; }35
+given (yes) { local a14=1; local b14=2; local c14=3; local d14=4; count = count + 1; }36
+given (yes) { local a15=1; local b15=2; local c15=3; local d15=4; count = count + 1; }37
+given (yes) { local a16=1; local b16=2; local c16=3; local d16=4; count = count + 1; }38
+given (yes) { local a17=1; local b17=2; local c17=3; local d17=4; count = count + 1; }39
+given (yes) { local a18=1; local b18=2; local c18=3; local d18=4; count = count + 1; }40
+given (yes) { local a19=1; local b19=2; local c19=3; local d19=4; count = count + 1; }41
+say count;kiln/tests/cases/control_flow/block_scoping_closures.ashmodified
+35−35@@ -1,50 +1,50 @@1
-// closure created and returned from inside an if-block, capturing a1
+// closure created and returned from inside an given-block, capturing a22
// block-scoped variable; call it after the block, once its slot has33
// been reused by a later declaration4
-fn make_from_if() {5
- let f = 0;6
- if (true) {7
- let x = 42;8
- f = fn() { return x; };4
+forge make_from_if() {5
+ local f = 0;6
+ given (yes) {7
+ local x = 42;8
+ f = forge() { yield x; };99
}10
- let after = 999; // reuses x's old slot11
- return f;10
+ local after = 999; // reuses x's old slot11
+ yield f;1212
}13
-let cf = make_from_if();14
-print cf();13
+local cf = make_from_if();14
+say cf();1515
1616
// closure capturing a variable that shadows an outer same-named one --1717
// the closure must see the INNER value; the outer variable must be1818
// untouched afterward19
-fn make_shadow() {20
- let x = 1;21
- let f = 0;22
- if (true) {23
- let x = 2;24
- f = fn() { return x; };19
+forge make_shadow() {20
+ local x = 1;21
+ local f = 0;22
+ given (yes) {23
+ local x = 2;24
+ f = forge() { yield x; };2525
}26
- return [x, f()];26
+ yield [x, f()];2727
}28
-let r = make_shadow();29
-print r[0];30
-print r[1];28
+local r = make_shadow();29
+say r[0];30
+say r[1];3131
32
-// closures created inside a while body (compiled once, run 3 times),32
+// closures created inside a during body (compiled once, run 3 times),3333
// each capturing that iteration's own block-scoped value34
-fn make_list() {35
- let fns = [];36
- let i = 0;37
- while (i < 3) {38
- let captured = i;39
- push(fns, fn() { return captured; });34
+forge make_list() {35
+ local fns = [];36
+ local i = 0;37
+ during (i < 3) {38
+ local captured = i;39
+ push(fns, forge() { yield captured; });4040
i = i + 1;4141
}42
- return fns;42
+ yield fns;4343
}44
-let list = make_list();45
-let l0 = list[0];46
-let l1 = list[1];47
-let l2 = list[2];48
-print l0();49
-print l1();50
-print l2();44
+local list = make_list();45
+local l0 = list[0];46
+local l1 = list[1];47
+local l2 = list[2];48
+say l0();49
+say l1();50
+say l2();kiln/tests/cases/control_flow/break_continue.ashmodified
+31−31@@ -1,47 +1,47 @@1
-let i = 0;2
-while (i < 10) {1
+local i = 0;2
+during (i < 10) {33
i = i + 1;4
- if (i == 5) { break; }5
- print i;4
+ given (i == 5) { stop; }5
+ say i;66
}7
-print "---";7
+say "---";88
9
-let j = 0;10
-while (j < 5) {9
+local j = 0;10
+during (j < 5) {1111
j = j + 1;12
- if (j == 3) { continue; }13
- print j;12
+ given (j == 3) { next; }13
+ say j;1414
}15
-print "---";15
+say "---";1616
17
-for (x in [1,2,3,4,5]) {18
- if (x == 3) { break; }19
- print x;17
+each (x in [1,2,3,4,5]) {18
+ given (x == 3) { stop; }19
+ say x;2020
}21
-print "---";21
+say "---";2222
23
-for (x in [1,2,3,4,5]) {24
- if (x == 3) { continue; }25
- print x;23
+each (x in [1,2,3,4,5]) {24
+ given (x == 3) { next; }25
+ say x;2626
}27
-print "---";27
+say "---";2828
29
-// nested loops: break only exits innermost30
-for (i in [1,2]) {31
- for (j in [1,2,3]) {32
- if (j == 2) { break; }33
- print i * 10 + j;29
+// nested loops: stop only exits innermost30
+each (i in [1,2]) {31
+ each (j in [1,2,3]) {32
+ given (j == 2) { stop; }33
+ say i * 10 + j;3434
}3535
}36
-print "---";36
+say "---";3737
38
-// break/continue inside function's own loop, not affecting caller39
-fn find_first_even(arr) {40
- for (x in arr) {41
- if (x - floor(x/2)*2 == 0) {42
- return x;38
+// stop/next inside function's own loop, not affecting caller39
+forge find_first_even(arr) {40
+ each (x in arr) {41
+ given (x - floor(x/2)*2 == 0) {42
+ yield x;4343
}4444
}45
- return -1;45
+ yield -1;4646
}47
-print find_first_even([1, 3, 5, 4, 7]);47
+say find_first_even([1, 3, 5, 4, 7]);kiln/tests/cases/control_flow/control_flow.ashmodified
+13−13@@ -1,20 +1,20 @@1
-let x = 5;1
+local x = 5;22
x = x + 1;3
-print x;3
+say x;44
5
-if (x > 5) {6
- print 1;7
-} else {8
- print 0;5
+given (x > 5) {6
+ say 1;7
+} otherwise {8
+ say 0;99
}1010
11
-let i = 0;12
-while (i < 3) {13
- print i;11
+local i = 0;12
+during (i < 3) {13
+ say i;1414
i = i + 1;1515
}1616
17
-print 1 == 1;18
-print 1 != 1;19
-print (1 < 2) && (3 > 2);20
-print (1 > 2) || (3 < 2);17
+say 1 == 1;18
+say 1 != 1;19
+say (1 < 2) and (3 > 2);20
+say (1 > 2) or (3 < 2);kiln/tests/cases/control_flow/for_loop_scoping.ashmodified
+9−9@@ -1,15 +1,15 @@11
// loop variable no longer leaks: outer x, same name, must be untouched2
-let x = 9;3
-for (x in [1, 2, 3]) {4
- print x;2
+local x = 9;3
+each (x in [1, 2, 3]) {4
+ say x;55
}6
-print x;6
+say x;77
8
-// nested for loops with the same variable name at each level -- each8
+// nested each loops with the same variable name at each level -- each99
// level's binding is independent10
-for (n in [1, 2]) {11
- for (n in [10, 20]) {12
- print n;10
+each (n in [1, 2]) {11
+ each (n in [10, 20]) {12
+ say n;1313
}14
- print n;14
+ say n;1515
}kiln/tests/cases/control_flow/for_loops.ashmodified
+27−27@@ -1,44 +1,44 @@1
-let nums = [10, 20, 30];2
-for (x in nums) {3
- print x;1
+local nums = [10, 20, 30];2
+each (x in nums) {3
+ say x;44
}55
6
-// nested for loops7
-let a = [1, 2];8
-let b = [10, 20];9
-for (i in a) {10
- for (j in b) {11
- print i * j;6
+// nested each loops7
+local a = [1, 2];8
+local b = [10, 20];9
+each (i in a) {10
+ each (j in b) {11
+ say i * j;1212
}1313
}1414
15
-// for loop calling a higher-order builtin in its body16
-fn double(x) { return x * 2; }17
-for (x in nums) {18
- let d = map([x], double);19
- print d[0];15
+// each loop calling a higher-order builtin in its body16
+forge double(x) { yield x * 2; }17
+each (x in nums) {18
+ local d = map([x], double);19
+ say d[0];2020
}2121
2222
// empty array23
-let empty = [];24
-for (x in empty) {25
- print "should not print";23
+local empty = [];24
+each (x in empty) {25
+ say "should not print";2626
}27
-print "after empty loop";27
+say "after empty loop";2828
29
-// for loop inside a function, recursion-safe check30
-fn sum_array(arr) {31
- let total = 0;32
- for (v in arr) {29
+// each loop inside a function, recursion-safe check30
+forge sum_array(arr) {31
+ local total = 0;32
+ each (v in arr) {3333
total = total + v;3434
}35
- return total;35
+ yield total;3636
}37
-print sum_array(nums);38
-print sum_array([1, 2, 3, 4, 5]);37
+say sum_array(nums);38
+say sum_array([1, 2, 3, 4, 5]);3939
4040
// loop variable reused/reassignable, doesn't affect iteration41
-for (x in nums) {41
+each (x in nums) {4242
x = x + 1000;43
- print x;43
+ say x;4444
}kiln/tests/cases/control_flow/loop_body_locals.ashadded
+42−0@@ -0,0 +1,42 @@1
+// A fresh `local` declared inside a re-executed block must not leak across2
+// iterations/branches -- found via a cross-engine audit against kiln.3
+4
+local i = 0;5
+during i < 3 {6
+ local x = i * 10;7
+ say x;8
+ i += 1;9
+}10
+11
+local arr = [1, 2, 3];12
+each n in arr {13
+ local y = n * 10;14
+ say y;15
+}16
+17
+local j = 0;18
+during j < 6 {19
+ given j % 2 == 0 {20
+ local a = j * 100;21
+ say a;22
+ } otherwise {23
+ j += 1;24
+ next;25
+ }26
+ j += 1;27
+}28
+29
+each x in arr {30
+ given x == 2 { next; }31
+ local z = x * 1000;32
+ say z;33
+}34
+35
+each outer in [1, 2] {36
+ local tmp = outer;37
+ each inner in [10, 20] {38
+ local tmp2 = outer + inner;39
+ say tmp2;40
+ }41
+ say tmp;42
+}kiln/tests/cases/errors/throw.ashmodified
+16−16@@ -1,22 +1,22 @@1
-try {2
- throw "custom error!";3
-} catch (e) {4
- print "caught: " + e;1
+attempt {2
+ raise "custom error!";3
+} handle (e) {4
+ say "caught: " + e;55
}6
-print "after";6
+say "after";77
8
-fn validate(x) {9
- if (x < 0) {10
- throw "negative value: " + str(x);8
+forge validate(x) {9
+ given (x < 0) {10
+ raise "negative value: " + str(x);1111
}12
- return x;12
+ yield x;1313
}1414
15
-try {16
- print validate(5);17
- print validate(-3);18
- print "unreachable";19
-} catch (e) {20
- print "caught: " + e;15
+attempt {16
+ say validate(5);17
+ say validate(-3);18
+ say "unreachable";19
+} handle (e) {20
+ say "caught: " + e;2121
}22
-print "done";22
+say "done";kiln/tests/cases/errors/try_catch.ashmodified
+36−36@@ -1,49 +1,49 @@1
-// basic catch of an out-of-bounds array error, execution continues after2
-let a = [1, 2, 3];3
-try {4
- print a[10];5
-} catch (e) {6
- print "caught 1";1
+// basic handle of an out-of-bounds array error, execution continues after2
+local a = [1, 2, 3];3
+attempt {4
+ say a[10];5
+} handle (e) {6
+ say "caught 1";77
}8
-print "after 1";8
+say "after 1";99
10
-// catch of a missing-map-key error11
-let m = {"x": 1};12
-try {13
- print m["missing"];14
-} catch (e) {15
- print "caught 2";10
+// handle of a missing-map-key error11
+local m = {"x": 1};12
+attempt {13
+ say m["missing"];14
+} handle (e) {15
+ say "caught 2";1616
}17
-print "after 2";17
+say "after 2";1818
19
-// no error raised -- the try body's own value still prints, catch is skipped20
-try {21
- print 42;22
-} catch (e) {23
- print "should not print";19
+// no error raised -- the attempt body's own value still prints, handle is skipped20
+attempt {21
+ say 42;22
+} handle (e) {23
+ say "should not print";2424
}2525
26
-// nested try/catch: inner catches its own error, outer never sees it27
-try {28
- try {29
- print a[99];30
- } catch (e) {31
- print "inner caught";26
+// nested attempt/handle: inner catches its own error, outer never sees it27
+attempt {28
+ attempt {29
+ say a[99];30
+ } handle (e) {31
+ say "inner caught";3232
}33
- print "after inner";34
-} catch (e) {35
- print "outer should not run";33
+ say "after inner";34
+} handle (e) {35
+ say "outer should not run";3636
}3737
38
-// error raised inside a function called from within a try -- unwinds38
+// error raised inside a function called from within a attempt -- unwinds3939
// through the call40
-fn boom() {41
- let z = [1];42
- return z[5];40
+forge boom() {41
+ local z = [1];42
+ yield z[5];4343
}44
-try {44
+attempt {4545
boom();46
-} catch (e) {47
- print "caught from function";46
+} handle (e) {47
+ say "caught from function";4848
}49
-print "done";49
+say "done";kiln/tests/cases/errors/try_catch_scoping.ashmodified
+21−21@@ -1,27 +1,27 @@1
-let e = "outer";2
-try {3
- throw "inner";4
-} catch (e) {5
- print e;1
+local e = "outer";2
+attempt {3
+ raise "inner";4
+} handle (e) {5
+ say e;66
}7
-print e;7
+say e;88
9
-try {10
- print "no error";11
-} catch (e) {12
- print "should not run";9
+attempt {10
+ say "no error";11
+} handle (e) {12
+ say "should not run";1313
}14
-print e;14
+say e;1515
16
-// nested try/catch, inner catch variable shadows outer's17
-try {18
- try {19
- throw "deep";20
- } catch (e) {21
- print e;16
+// nested attempt/handle, inner handle variable shadows outer's17
+attempt {18
+ attempt {19
+ raise "deep";20
+ } handle (e) {21
+ say e;2222
}23
- print e;24
-} catch (e) {25
- print "outer should not run";23
+ say e;24
+} handle (e) {25
+ say "outer should not run";2626
}27
-print e;27
+say e;kiln/tests/cases/functions/closures.ashmodified
+19−19@@ -1,31 +1,31 @@11
// M8: lambda assigned to a variable and called2
-let square = fn(x) { return x * x; };3
-print square(5);2
+local square = forge(x) { yield x * x; };3
+say square(5);44
55
// named function referenced as a value and called indirectly6
-fn add(a, b) { return a + b; }7
-let op = add;8
-print op(3, 4);6
+forge add(a, b) { yield a + b; }7
+local op = add;8
+say op(3, 4);99
1010
// genuine capture: adder-generator (partial application / currying)11
-fn make_adder(n) {12
- return fn(x) { return x + n; };11
+forge make_adder(n) {12
+ yield forge(x) { yield x + n; };1313
}14
-let add5 = make_adder(5);15
-let add10 = make_adder(10);16
-print add5(1);17
-print add10(1);18
-print add5(2);14
+local add5 = make_adder(5);15
+local add10 = make_adder(10);16
+say add5(1);17
+say add10(1);18
+say add5(2);1919
2020
// higher-order function: pass a closure into a plain function21
-fn apply_twice(f, x) {22
- return f(f(x));21
+forge apply_twice(f, x) {22
+ yield f(f(x));2323
}24
-print apply_twice(square, 3);24
+say apply_twice(square, 3);2525
2626
// closure capturing multiple outer variables27
-fn make_scaler(mul, offset) {28
- return fn(x) { return x * mul + offset; };27
+forge make_scaler(mul, offset) {28
+ yield forge(x) { yield x * mul + offset; };2929
}30
-let scale = make_scaler(2, 1);31
-print scale(10);30
+local scale = make_scaler(2, 1);31
+say scale(10);kiln/tests/cases/functions/functions.ashmodified
+19−19@@ -1,30 +1,30 @@1
-fn add(a, b) {2
- return a + b;1
+forge add(a, b) {2
+ yield a + b;33
}4
-print add(2, 3);4
+say add(2, 3);55
6
-fn fact(n) {7
- if (n <= 1) {8
- return 1;6
+forge fact(n) {7
+ given (n <= 1) {8
+ yield 1;99
}10
- return n * fact(n - 1);10
+ yield n * fact(n - 1);1111
}12
-print fact(5);12
+say fact(5);1313
14
-fn fib(n) {15
- if (n < 2) {16
- return n;14
+forge fib(n) {15
+ given (n < 2) {16
+ yield n;1717
}18
- return fib(n - 1) + fib(n - 2);18
+ yield fib(n - 1) + fib(n - 2);1919
}20
-print fib(10);20
+say fib(10);2121
22
-fn noop() {22
+forge noop() {2323
}24
-print noop();24
+say noop();2525
26
-fn square(x) {27
- return x * x;26
+forge square(x) {27
+ yield x * x;2828
}29
-let y = 5;30
-print square(y) + square(y + 1);29
+local y = 5;30
+say square(y) + square(y + 1);kiln/tests/cases/functions/higher_order.ashmodified
+21−21@@ -1,26 +1,26 @@1
-let nums = [1, 2, 3, 4, 5];1
+local nums = [1, 2, 3, 4, 5];22
3
-fn double(x) { return x * 2; }4
-let doubled = map(nums, double);5
-print doubled[0];6
-print doubled[1];7
-print doubled[4];8
-print len(doubled);3
+forge double(x) { yield x * 2; }4
+local doubled = map(nums, double);5
+say doubled[0];6
+say doubled[1];7
+say doubled[4];8
+say len(doubled);99
10
-fn is_even(x) { return x - floor(x / 2) * 2 == 0; }11
-let evens = filter(nums, is_even);12
-print len(evens);13
-print evens[0];14
-print evens[1];10
+forge is_even(x) { yield x - floor(x / 2) * 2 == 0; }11
+local evens = filter(nums, is_even);12
+say len(evens);13
+say evens[0];14
+say evens[1];1515
16
-fn add(a, b) { return a + b; }17
-print reduce(nums, add, 0);18
-print reduce(nums, add, 100);16
+forge add(a, b) { yield a + b; }17
+say reduce(nums, add, 0);18
+say reduce(nums, add, 100);1919
20
-let squares = map(nums, fn(x) { return x * x; });21
-print squares[2];22
-print squares[4];20
+local squares = map(nums, forge(x) { yield x * x; });21
+say squares[2];22
+say squares[4];2323
24
-let empty = [];25
-let mapped_empty = map(empty, double);26
-print len(mapped_empty);24
+local empty = [];25
+local mapped_empty = map(empty, double);26
+say len(mapped_empty);kiln/tests/cases/imports/ash.libs/testlib/testlib.ashmodified
+3−3@@ -1,5 +1,5 @@1
-let VERSION = "1.0";1
+local VERSION = "1.0";22
3
-fn square(x) {4
- return x * x;3
+forge square(x) {4
+ yield x * x;55
}kiln/tests/cases/imports/fixtures/collision_dir_one/shared.ashmodified
+2−2@@ -1,3 +1,3 @@1
-fn fromOne() {2
- return 1;1
+forge fromOne() {2
+ yield 1;33
}kiln/tests/cases/imports/fixtures/collision_dir_two/shared.ashmodified
+2−2@@ -1,3 +1,3 @@1
-fn fromTwo() {2
- return 2;1
+forge fromTwo() {2
+ yield 2;33
}kiln/tests/cases/imports/fixtures/imports_basic_lib.ashmodified
+4−4@@ -1,7 +1,7 @@1
-fn helper(x) {2
- return x * 2;1
+forge helper(x) {2
+ yield x * 2;33
}44
5
-fn add(a, b) {6
- return a + b + helper(1);5
+forge add(a, b) {6
+ yield a + b + helper(1);77
}kiln/tests/cases/imports/fixtures/imports_circular_a.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_circular_b.ash>;22
3
-fn fromA() {4
- return 1;3
+forge fromA() {4
+ yield 1;55
}kiln/tests/cases/imports/fixtures/imports_circular_b.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_circular_a.ash>;22
3
-fn fromB() {4
- return 2;3
+forge fromB() {4
+ yield 2;55
}kiln/tests/cases/imports/fixtures/imports_collision_main.ashmodified
+1−1@@ -1,4 +1,4 @@11
@import <./collision_dir_one/shared.ash>;22
@import <./collision_dir_two/shared.ash>;33
4
-print shared.fromOne();4
+say shared.fromOne();kiln/tests/cases/imports/fixtures/imports_const_lib.ashmodified
+4−4@@ -1,6 +1,6 @@1
-let PI = 3.14159;2
-let GREETING = "hello from lib";1
+local PI = 3.14159;2
+local GREETING = "hello from lib";33
4
-fn circleArea(r) {5
- return PI * r * r;4
+forge circleArea(r) {5
+ yield PI * r * r;66
}kiln/tests/cases/imports/fixtures/imports_diamond_b.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_diamond_d.ash>;22
3
-fn fromB() {4
- return imports_diamond_d.base() + 1;3
+forge fromB() {4
+ yield imports_diamond_d.base() + 1;55
}kiln/tests/cases/imports/fixtures/imports_diamond_c.ashmodified
+2−2@@ -1,5 +1,5 @@11
@import <./imports_diamond_d.ash>;22
3
-fn fromC() {4
- return imports_diamond_d.base() + 2;3
+forge fromC() {4
+ yield imports_diamond_d.base() + 2;55
}kiln/tests/cases/imports/fixtures/imports_diamond_d.ashmodified
+2−2@@ -1,3 +1,3 @@1
-fn base() {2
- return 100;1
+forge base() {2
+ yield 100;33
}kiln/tests/cases/imports/fixtures/imports_statement_lib.ashadded
+19−0@@ -0,0 +1,19 @@1
+// Covers two bugs found writing the first real forgepack module (see2
+// CHANGELOG.md): a `local` inside a loop in an imported function used to3
+// be misdiagnosed as a failed top-level namespaced-constant declaration,4
+// and a namespaced call used as its OWN statement (not nested in an5
+// expression) used to fail to parse entirely.6
+forge sum_doubled(arr) {7
+ local total = 0;8
+ local i = 0;9
+ during i < len(arr) {10
+ local doubled = arr[i] * 2;11
+ total += doubled;12
+ i += 1;13
+ }14
+ yield total;15
+}16
+17
+forge announce(label) {18
+ say "announced: " + label;19
+}kiln/tests/cases/imports/imports_basic.ashmodified
+2−2@@ -1,4 +1,4 @@11
@import <./fixtures/imports_basic_lib.ash>;22
3
-print imports_basic_lib.add(3, 4);4
-print imports_basic_lib.helper(10);3
+say imports_basic_lib.add(3, 4);4
+say imports_basic_lib.helper(10);kiln/tests/cases/imports/imports_const.ashmodified
+3−3@@ -1,5 +1,5 @@11
@import <./fixtures/imports_const_lib.ash>;22
3
-print imports_const_lib.PI;4
-print imports_const_lib.GREETING;5
-print imports_const_lib.circleArea(2);3
+say imports_const_lib.PI;4
+say imports_const_lib.GREETING;5
+say imports_const_lib.circleArea(2);kiln/tests/cases/imports/imports_diamond.ashmodified
+3−3@@ -1,6 +1,6 @@11
@import <./fixtures/imports_diamond_b.ash>;22
@import <./fixtures/imports_diamond_c.ash>;33
4
-print imports_diamond_b.fromB();5
-print imports_diamond_c.fromC();6
-print imports_diamond_d.base();4
+say imports_diamond_b.fromB();5
+say imports_diamond_c.fromC();6
+say imports_diamond_d.base();kiln/tests/cases/imports/imports_lib.ashmodified
+2−2@@ -1,4 +1,4 @@11
@import <testlib>;22
3
-print testlib.VERSION;4
-print testlib.square(6);3
+say testlib.VERSION;4
+say testlib.square(6);kiln/tests/cases/imports/imports_statement.ashadded
+4−0@@ -0,0 +1,4 @@1
+@import <./fixtures/imports_statement_lib.ash>;2
+3
+say imports_statement_lib.sum_doubled([1, 2, 3]);4
+imports_statement_lib.announce("done");kiln/tests/cases/io/argv.ashmodified
+5−5@@ -1,6 +1,6 @@1
-let args = argv();2
-print len(args);3
-print args[0];4
-for (a in args) {5
- print a;1
+local args = argv();2
+say len(args);3
+say args[0];4
+each (a in args) {5
+ say a;66
}kiln/tests/cases/io/file_io.ashmodified
+7−7@@ -1,7 +1,7 @@1
-let path = "/tmp/kiln_file_test.txt";2
-print file_exists(path);3
-print write_file(path, "hello, file!\nsecond line");4
-print file_exists(path);5
-print read_file(path);6
-print append_file(path, "\nappended");7
-print read_file(path);1
+local path = "/tmp/kiln_file_test.txt";2
+say file_exists(path);3
+say write_file(path, "hello, file!\nsecond line");4
+say file_exists(path);5
+say read_file(path);6
+say append_file(path, "\nappended");7
+say read_file(path);kiln/tests/cases/io/input_builtin.ashmodified
+4−4@@ -1,4 +1,4 @@1
-let name = input();2
-print "hello, " + name + "!";3
-let x = num(input());4
-print x + 1;1
+local name = input();2
+say "hello, " + name + "!";3
+local x = num(input());4
+say x + 1;kiln/tests/cases/strings/convert_builtins.ashmodified
+10−10@@ -1,10 +1,10 @@1
-print str(42);2
-print str(3.5);3
-print str(-7.25);4
-print str(0);5
-print num("42");6
-print num("3.5");7
-print num("-7.25");8
-print num(" 12.5abc");9
-print num("abc");10
-print num(str(123.456));1
+say str(42);2
+say str(3.5);3
+say str(-7.25);4
+say str(0);5
+say num("42");6
+say num("3.5");7
+say num("-7.25");8
+say num(" 12.5abc");9
+say num("abc");10
+say num(str(123.456));kiln/tests/cases/strings/string_builtins2.ashmodified
+14−14@@ -1,14 +1,14 @@1
-print upper("hello world");2
-print lower("HELLO WORLD");3
-print upper("Already Mixed 123!");4
-print trim(" hi there ");5
-print trim("no spaces");6
-print trim(" ");7
-print substring("hello world", 0, 5);8
-print substring("hello world", 6, 11);9
-print substring("hello world", -3, 100);10
-print substring("hello world", 8, 3);11
-print indexOf("hello world", "world");12
-print indexOf("hello world", "xyz");13
-print indexOf("hello world", "");14
-print indexOf("hello world", "hello world");1
+say upper("hello world");2
+say lower("HELLO WORLD");3
+say upper("Already Mixed 123!");4
+say trim(" hi there ");5
+say trim("no spaces");6
+say trim(" ");7
+say substring("hello world", 0, 5);8
+say substring("hello world", 6, 11);9
+say substring("hello world", -3, 100);10
+say substring("hello world", 8, 3);11
+say indexOf("hello world", "world");12
+say indexOf("hello world", "xyz");13
+say indexOf("hello world", "");14
+say indexOf("hello world", "hello world");kiln/tests/cases/strings/string_builtins3.ashmodified
+22−22@@ -1,27 +1,27 @@1
-let parts = split("a,b,c", ",");2
-print len(parts);3
-print parts[0];4
-print parts[1];5
-print parts[2];1
+local parts = split("a,b,c", ",");2
+say len(parts);3
+say parts[0];4
+say parts[1];5
+say parts[2];66
7
-let chars = split("abc", "");8
-print len(chars);9
-print chars[0];10
-print chars[2];7
+local chars = split("abc", "");8
+say len(chars);9
+say chars[0];10
+say chars[2];1111
12
-let empty1 = split("", ",");13
-print len(empty1);14
-print empty1[0];12
+local empty1 = split("", ",");13
+say len(empty1);14
+say empty1[0];1515
16
-let empty2 = split("", "");17
-print len(empty2);16
+local empty2 = split("", "");17
+say len(empty2);1818
19
-print join(["a", "b", "c"], "-");20
-print join(["x"], ",");21
-print join([], ",");19
+say join(["a", "b", "c"], "-");20
+say join(["x"], ",");21
+say join([], ",");2222
23
-print replace("hello world", "world", "there");24
-print replace("aaa", "a", "bb");25
-print replace("hello", "xyz", "abc");26
-print replace("hello", "", "X");27
-print replace("banana", "a", "o");23
+say replace("hello world", "world", "there");24
+say replace("aaa", "a", "bb");25
+say replace("hello", "xyz", "abc");26
+say replace("hello", "", "X");27
+say replace("banana", "a", "o");kiln/tests/cases/strings/string_index.ashmodified
+12−12@@ -1,17 +1,17 @@1
-let s = "hello";2
-print s[0];3
-print s[4];4
-print s[1] + s[2];1
+local s = "hello";2
+say s[0];3
+say s[4];4
+say s[1] + s[2];55
6
-try {7
- print s[10];8
-} catch (e) {9
- print "caught: " + e;6
+attempt {7
+ say s[10];8
+} handle (e) {9
+ say "caught: " + e;1010
}1111
12
-try {12
+attempt {1313
s[0] = "H";14
-} catch (e) {15
- print "caught: " + e;14
+} handle (e) {15
+ say "caught: " + e;1616
}17
-print "done";17
+say "done";kiln/tests/cases/strings/strings.ashmodified
+25−25@@ -1,37 +1,37 @@1
-print "hello";2
-print "hello, " + "ash!";1
+say "hello";2
+say "hello, " + "ash!";33
4
-let s = "x";5
-print s == "x";6
-print s == "y";7
-print s != "x";4
+local s = "x";5
+say s == "x";6
+say s == "y";7
+say s != "x";88
9
-let a = "foo";10
-let b = "bar";11
-print a + b;9
+local a = "foo";10
+local b = "bar";11
+say a + b;1212
13
-print "line1\nline2";14
-print "quote: \"hi\"";13
+say "line1\nline2";14
+say "quote: \"hi\"";1515
16
-fn greet(name) {17
- return "hello, " + name;16
+forge greet(name) {17
+ yield "hello, " + name;1818
}19
-print greet("world");19
+say greet("world");2020
21
-let x = "a";21
+local x = "a";2222
x = x + "b";2323
x = x + "c";24
-print x;24
+say x;2525
26
-fn repeat(s, n) {27
- if (n <= 0) {28
- return "";26
+forge repeat(s, n) {27
+ given (n <= 0) {28
+ yield "";2929
}30
- return s + repeat(s, n - 1);30
+ yield s + repeat(s, n - 1);3131
}32
-print repeat("ab", 5);32
+say repeat("ab", 5);3333
34
-let n = 5;35
-print n;36
-print s + "test";37
-print n + 1;34
+local n = 5;35
+say n;36
+say s + "test";37
+say n + 1;plans.mdremoved
+0−128@@ -1,128 +0,0 @@1
-# Ash: Plans2
-3
-Status: written 2026-10-07, right after the source/test restructure (see `CHANGELOG.md`).4
-Items marked **(proposed)** are my recommendations, not decisions. Items marked **(open)** still need an answer.5
-6
-## Why7
-8
-Ash is in a good position today: a working lexer, single-pass compiler and bytecode VM with closures, maps, `try`/`catch`, real doubles, imports and good diagnostics. But I don't like the general design anymore. Ash currently looks like C/JavaScript: braces, semicolons, parentheses around every condition. I love Lua, and I want Ash to read like Lua: lightweight, high level, little punctuation noise, with blocks that read as structure.9
-10
-The goal is **a higher-level, Lua-looking language with the same performance**. The look changes. The engine's speed does not.11
-12
-## Goals13
-14
-1. **Lua-like surface syntax.** Blocks are keyword-delimited instead of brace-delimited, and the syntax carries less punctuation overall.15
-2. **Same performance.** The VM must not get slower. See the performance rules below. This is the hard constraint.16
-3. **Same behavior.** Existing language semantics stay unless a change is deliberate and written down.17
-4. **Keep the code organized.** Every change follows `AGENTS.md` (mirrored C/H/D/O trees, small feature folders, tests in `tests/cases/<feature>/`).18
-19
-## Non-goals (for this phase)20
-21
-- No new VM instructions or new runtime features just because the syntax changed. The syntax change is a **frontend** change.22
-- No type system, no classes/structs, no new standard library. If "high level" later means more language features (classes, varargs, default parameters), that is a separate plan.23
-- No JIT or backend work (the x86-64 JIT experiment in `ashvm/benchmarks/native_jit.c` stays an experiment).24
-- `kiln` is not blocked on this (see "kiln" below).25
-26
-## What "Lua-like" means here27
-28
-Lua is not indentation-sensitive. Its blocks are delimited by keywords (`then`/`do`/`end`), and whitespace does not matter. That is what "the same indentation as Lua" means in this plan: **keyword-delimited blocks, written with Lua's usual indentation style**. If I ever want Python-style significant indentation instead, that is a different and harder lexer change (INDENT/DEDENT tokens, awkward with multi-line lambdas and the REPL) and needs a new decision.29
-30
-Sketch of the direction **(proposed, not final)**:31
-32
-```text33
--- today -- Lua-like Ash34
-fn make_adder(x) { fn make_adder(x)35
- return fn(y) { return x + y; }; return fn(y) return x + y end36
-} end37
-38
-if (n % 2 == 0) { if n % 2 == 0 then39
- print "even"; print "even"40
-} else { else41
- print "odd"; print "odd"42
-} end43
-44
-while (i < n) { i = i + 1; } while i < n do i = i + 1 end45
-46
-for (x in nums) { print x; } for x in nums do print x end47
-48
-try { ... } catch (e) { ... } try ... catch e ... end49
-```50
-51
-Likely rules: statements end at a newline or `;` (semicolons optional), conditions need no parentheses, `end` closes every block, comments stay as they are or move to `--` **(open: keep `//` or switch to `--`?)**.52
-53
-## What must stay (do not change)54
-55
-These are the things that make Ash good and fast. The syntax work must leave them alone.56
-57
-**Runtime and performance**58
-- The bytecode `Chunk` format, the stack VM and the computed-goto dispatch loop (`ashvm/src/vm/`).59
-- The hand-fused superinstructions (`x = x + y`, `while (i < n)` and similar). They are why loops are fast.60
-- Value representation (`VMValue` tagged type, real `double` numbers), the hash map, and builtins' behavior.61
-- Closures with **by-value capture**, first-class functions, `map`/`filter`/`reduce`, `try`/`catch`, `for (x in array)`.62
-63
-**Behavior users see**64
-- Rust-style diagnostics: `file:line:column`, a source snippet, a caret and a contextual hint, for both compile-time and runtime errors. New syntax must produce equally good errors. For example, a missing `end` must point at the block that was never closed, not at end-of-file.65
-- Import semantics: `@import <./relative.ash>` and `@import <lib>` (resolved through `ash.libs/<lib>/<lib>.ash`), relative to the **entry file's directory**, with namespaced access and circular/collision errors.66
-- The REPL, including persistent variables across lines.67
-68
-**Project structure**69
-- `AGENTS.md` layout and rules. Split new code into feature folders, mirrored across `C/H/D/O`.70
-- The regression harnesses (`tests/scripts/run_regression.sh`) as the safety net.71
-72
-## Performance rules (non-negotiable)73
-74
-Today's baseline, measured 2026-10-07 on my laptop (median of 5 runs; the laptop is slow, so only **relative** comparisons on the same machine matter):75
-76
-| Benchmark | `ashvm` |77
-|---|---|78
-| `fib(30)` | 0.178s |79
-| sum 0..20M (`loop`) | 1.36s |80
-| `loop_varbound` | 1.37s |81
-| `countdown` | 1.36s |82
-| `hashmap` (50k inserts) | 0.081s |83
-84
-The README's older numbers (0.06s / 0.12s) do not reproduce here. Whether that is machine difference, staleness or a fusion that is not firing is **(open)**; see "Investigate first".85
-86
-Rules:87
-1. **Measure before and after**, on the same machine, with the same method (median of at least 5 runs, `ashvm/benchmarks/*.ash`). A change that makes any benchmark more than ~5% slower does not land.88
-2. **The syntax change must not touch the VM.** Work is limited to the lexer and compiler (`ashvm/src/lexer`, `ashvm/src/compiler`). New syntax compiles to the **same bytecode** as the equivalent old-syntax program. The same bytecode means the same speed.89
-3. **Superinstruction fusion must keep firing.** The compiler's fusion peeks at tokens after `(` in conditions (`try_fuse_condition`). When parentheses become optional, `while i < n do` and `x = x + y` must still match those patterns. This is the single most likely way to lose speed by accident, so test it explicitly.90
-4. **No per-instruction cost.** No extra checks, indirection or bookkeeping in `vm/dispatch.c` for the sake of syntax.91
-5. Benchmarks are converted to the new syntax too, and compared against their old-syntax timings.92
-93
-## How (proposed phases)94
-95
-0. **Investigate first (optional, small).** Find out why `loop` takes ~1.36s and whether the `while (i < n)` fusion is firing. Fixing that first gives the rework a better baseline, and it settles the README numbers.96
-1. **Freeze the safety net.** Capture regression baselines (`run_regression.sh baseline`) and benchmark timings. If there is no bytecode dump, consider adding a small one so "same bytecode" can be checked directly rather than inferred from timings.97
-2. **Lexer.** Add the new keyword tokens (`then`, `do`, `end` or whatever is chosen). Make `;` optional if decided.98
-3. **Compiler.** `block()` is the single choke point for blocks. Rework it to stop at `end`/`else` instead of `}`, then update `if`, `while`, `for`, `try`/`catch`, `fn` and lambda emission (`stmt.c`, `stmt_control.c`, `calls.c`, `expr.c`). Make parentheses around conditions optional without breaking fusion.99
-4. **Diagnostics.** Update messages and hints for the new tokens ("expected `end` to close this `if`"), keeping the Rust-style output.100
-5. **Migrate** all `.ash` files: `tests/cases/**`, `benchmarks/*.ash`, README examples. This is mechanical. Run the regression suite after: output must match except where a diagnostic prints new syntax.101
-6. **Compare** benchmarks against the baseline above. If anything regressed, fix before moving on.102
-7. **Docs/tooling:** update `README.md`, `CHANGELOG.md`, and the VS Code syntax extension (separate repo, `ash-syntax`).103
-104
-Each phase keeps the build and tests green and is verified before the next one starts.105
-106
-## kiln107
-108
-`kiln` (the native `.ash` → ELF/PE compiler) has its **own** lexer and parser, so it does not get the new syntax for free. **(open)** Port it too (about the same amount of frontend work again), or leave kiln on the old syntax for now. If it stays behind, the two will accept different languages until it is ported, so this must be a conscious choice and recorded here.109
-110
-## Old syntax: keep or drop? (open)111
-112
-**Proposed:** replace it outright at the switch. Supporting both doubles lexer/compiler complexity and costs roughly 50% more effort, and the old brace syntax has no reason to survive a rework whose point is to leave it. The cost is that every `.ash` file in the repo (and anyone else's scripts) has to be migrated at once, which the phases above already plan for.113
-114
-## Definition of done115
-116
-- Programs are written in the Lua-like syntax and read like Lua.117
-- Full regression suite passes for the migrated tests (identical output, apart from diagnostics that now print new syntax).118
-- Benchmarks are within ~5% of the baseline (or better, if phase 0 improved it).119
-- Diagnostics for syntax errors are as good as today's.120
-- `AGENTS.md` structure rules still hold; docs, changelog and editor extension are updated.121
-122
-## Open questions123
-124
-1. Keyword set and comment style (`//` vs `--`)?125
-2. Semicolons optional, or removed entirely?126
-3. Old syntax: replace at once (recommended) or support both for a while?127
-4. Port `kiln` now or later?128
-5. Does "high level" also mean new language features (classes, varargs, default parameters, a larger standard library) after the syntax change? If so, that gets its own plan.