← Commit history

docs: remove old Ash syntax and ashfront references; document ashvm/kiln CLIs and REPL

lennoxrose committed on Oct 18, 2026 at 00:45 UTC

d4c0a3cparent43406a1

7 files changed+111−204

CHANGELOG.mdmodified

+9−0
@@ -1,3 +1,12 @@
1+## 2026-10-18 (frontend consolidation: hearth/front is the only frontend)
2+- **`ashvm` and `kiln` no longer have lexers, parsers or import systems of their own for Ash source.** Both link `libashfront.a` and call `jdriver_compile_file()` (`front/src/jdriver`): lex, parse, type-check, lower. The result is the internal IR, passed in memory to the `ashvm` compiler / `kiln` code generator. Deleted: `ashvm/src/lexer`, `kiln/src/lexer`, `@import` handling, import-path resolution, import aliases, circular/collision checks (`ashvm/src/compiler/*/stmt_import.*`, `*/import_paths.*`, `kiln/src/parser/*/imports`).
3+- **One IR lexer** (`front/src/ir`) replaces the two old lexers. The IR is one unit of top-level declarations whose `start` function the consumer calls; it has no `@import` and no user entry point. Old-syntax source is rejected by the front end with a message naming the replacement (D3).
4+- **`ashfront` is gone** (source, build target, `bin/ashfront`, docs). Its modes moved into the real tools: `ashvm run [--wrap-int] <file.ash>`, `ashvm check <file.ash>`, `kiln <file.ash> -o <out> [... --wrap-int]`. `--lower` and `--parse` were dropped (`ashvm check` parses and type-checks).
5+- **The `ashvm` REPL is Java-like**: imports, types, static methods and statements persist; a bare expression is printed. Each line runs in a child process on a replay of the accepted history (`front/src/jrepl`), so errors cannot corrupt the session. The old REPL and its VM recovery hooks (`vm_run_from`, `vm_set_repl_recovery`, `vm_repl_checkpoint`) were removed.
6+- **Performance**: `x = x OP (number|local);` now fuses into `OP_ACC_LOCAL` like `local x = x OP ...` did, and the lowering stops parenthesising assignment right-hand sides. On the same Java-like source this is faster than before (loop 2342 -> 1577 ms, countdown 2235 -> 1513, loop_varbound 2371 -> 1553; fib 252 -> 248, hashmap 299 -> 291). Migrated benchmarks `ashvm/benchmarks/*.ash` now match or beat the hand-written old-syntax numbers except `hashmap` (291 ms vs 116 ms for raw VM maps: Java `HashMap` is an equals/hashCode library over them, unchanged by this work) and a ~4 ms start-up for loading the reachable stdlib.
7+- **Tests**: the 144 old-syntax cases under `ashvm/tests/cases` and `kiln/tests/cases` were deleted (they exercised the old language and its builtins directly: `forge`/`attempt`/`raise` semantics, builtin error wording, `@import` resolution, GC stress on old programs); the behaviours that still exist are covered by `front/tests/cases/run` and `examples/`. `GcChurn` and `HeapGrowth` were migrated. `run_regression.sh` in both projects compares every program with its `.out`; `gc_check.sh`, the Windows emulation/Wine checks and `shared_link_check.sh` now iterate over the shared programs. The two Pyre benchmark programs were migrated.
8+- Not carried over: the `matrix_mul` builtin has no new-Ash API, so it is unreachable from Ash (the builtin is still present in the IR layer of both engines).
9+
110 ## 2026-10-17 (collector: layout-noise floor measured)
211 - Two collector-free builds of the same source differing only in `-falign-functions=64` differ by up to 0.7% on the baseline benchmarks (ratios 1.000, 0.993, 1.000, 0.998, 1.000); the collector build measures 1.007, 1.004, 1.002, 1.003, 0.995 against the same base. The residue is inside the layout-noise band but not provably zero (hashmap, the most allocation-heavy, is at its edge), so `GC ?= 0` stays; enabling it is a one-line change for the owner.
312

MIGRATION.mdmodified

+12−16
@@ -185,7 +185,7 @@ Complete this table and implement it consistently:
185185 | ID | Decision |
186186 |---|---|
187187 | D1 | `import ... as` is dropped. Imports name a type or package: `import ash.util.List;`, `import ash.util.*;`. |
188-| D2 | The old front end stays frozen and runnable (`.ash` files without a `package`/`class` header go through it) until the new front end reaches parity; then it is deleted. New front end is selected per file by syntax detection (a file whose first token is `package`, `import`, `public`, `class`, `interface`, `enum`, `record`, `abstract`, `final` or `sealed` is new Ash). |
188+| D2 | The old front end is deleted (executed 2026-10-18): `ashvm` and `kiln` accept only new Ash. Old-syntax source is rejected by the new front end with migration messages (D3). |
189189 | D3 | Old syntax is diagnosed by context (e.g. `say` followed by an expression start at statement position), not by reserving the words. `next`, `stop`, `each`, `no`, `in`, `as`, `say` ... are ordinary identifiers in new Ash. |
190190 | D4 | `T[]` is the surface array syntax; `Array<T>` is its canonical type name. |
191191 | D5 | `System.out.println` is the print API. `Console.println` is removed from the spec. |
@@ -198,7 +198,7 @@ Complete this table and implement it consistently:
198198 | D12 | One shared front end (lexer, parser, AST, type checker) lives in `hearth/front/` and is linked by both `ashvm` and `kiln`. |
199199 | D13 | Primitive boolean type is spelled `bool`. |
200200 | D14 | Checked exceptions are enforced (Exception subclasses not under RuntimeException must be caught or declared with `throws`). |
201-| D2a | **Amendment (2026-10-17, taken so the migration could proceed; revert by saying so).** Old Ash is kept as the *internal IR* the new front end lowers to, because that is how one front end feeds both `ashvm` and `kiln` without touching either VM. It is no longer a supported application language: `ashfront` rejects it with migration messages. Physically deleting it needs a direct emitter from the typed AST. |
201+| D2a | **Resolved (2026-10-18).** The old-Ash-shaped text the front end lowers to is kept as a compiler-internal IR, owned by `hearth/front/src/ir` (one shared IR lexer) and passed in memory from `hearth/front/src/jdriver` to the `ashvm` compiler and the `kiln` code generator. It is not Ash source: no tool reads it from a file, there is no `@import` in it (the lowering emits one unit and the consumer calls its `start`), and no CLI, REPL, example, test or document presents it as a language. Replacing it with a binary IR driven directly by the typed AST remains possible but is not required. |
202202 | D15 | Performance (CLAUDE.md law): no change may make the recorded baselines slower. Baselines are recorded in `spec/performance-baseline.md` before runtime changes. |
203203
204204 ## Specification
@@ -232,7 +232,7 @@ Contextual: `var`, `record`, `sealed`, `permits`. `else if` is `else` followed b
232232
233233 ### Numerics
234234
235-- `int` is 32-bit two's complement, `long` 64-bit, `short` 16, `byte` 8, `char` 16-bit unsigned, `float` 32-bit IEEE, `double` 64-bit IEEE. Integer overflow wraps (`int` exactly with `ashfront --wrap-int`; `long` exactly with the same flag; without it `long` is limited to 2^53). Division by zero for integers throws `ArithmeticException`; for floating point it yields inf/NaN.
235+- `int` is 32-bit two's complement, `long` 64-bit, `short` 16, `byte` 8, `char` 16-bit unsigned, `float` 32-bit IEEE, `double` 64-bit IEEE. Integer overflow wraps (`int` exactly with `--wrap-int`; `long` exactly with the same flag; without it `long` is limited to 2^53). Division by zero for integers throws `ArithmeticException`; for floating point it yields inf/NaN.
236236 - Integer `/` truncates toward zero; `%` takes the sign of the dividend (`-7 % 3 == -1`).
237237 - Mixed arithmetic widens: byte/short/char -> int -> long -> float -> double. Narrowing needs an explicit cast.
238238 - Literals: `42` int, `42L` long, `1.5` double, `1.5f` float, `1e3` double, `0x1F`, `'a'` char, escapes `\n \t \r \\ \" \' \0 \xHH`.
@@ -290,10 +290,10 @@ Four spaces, `{` same line, `}` own line except `} else`/`} catch`/`} finally`/`
290290
291291 ### Runtime representation (as implemented)
292292
293-| Ash value | Old-Ash runtime value |
293+| Ash value | VM runtime value |
294294 |---|---|
295295 | `int long short byte float double char` | number (char is its code point; ints are exact below 2^53, see limits table) |
296-| `bool`, `null` | `yes`/`no`, `none` |
296+| `bool`, `null` | IR booleans, IR none |
297297 | `String` | string; `Integer`, `Double`, `Boolean`... are the same unboxed values (no allocation) |
298298 | `T[]`, `List<T>`, `Stream<T>` | array |
299299 | `Map`, `Set` | map keyed by a string form of the key, entries `[key, value]` |
@@ -305,14 +305,10 @@ Four spaces, `{` same line, `}` own line except `} else`/`} catch`/`} finally`/`
305305
306306 Startup and memory requirements for games and desktop apps: startup cost is proportional to the *reachable* classes only (tree-shaken module, no stdlib loaded unless used); there is no garbage collector yet, so a long-running program must bound its allocation (reuse objects, avoid building strings in loops) or its memory grows until exit. A collector is the prerequisite for unbounded-lifetime programs and is not part of this migration. Memory: allocation never frees until exit (D11 until a collector exists). Concurrency: no threads on this VM; `ash.concurrent.Executor` runs tasks to completion on the caller, there is no `async/await`.
307307
308-### Open decision (D2 versus the lowering design)
309-
310-D2 said the old front end is deleted once the new one reaches parity. The implemented design lowers new Ash **to old-Ash source**, so the old language is now the **internal IR** that `ashvm` and `kiln` consume; deleting it would require either a new bytecode/native emitter driven directly by the typed AST or renaming the IR. Until that is decided, "no old keywords remain" is true for the *application language* (the new front end rejects them with migration messages) but not for the VM-facing IR.
311-
312308 ### Transition and performance protocol
313309
314-1. Old front end remains runnable (D2); the regression suites for old tests must stay byte-identical until the old path is deleted.
315-2. New tests live beside old ones in `tests/cases/<feature>/` with names ending `_j.ash`.
310+1. The old front end is deleted (D2). Old-syntax tests were deleted or migrated; the regression suites compare every program with its `.out` file.
311+2. Shared runtime tests live in `hearth/front/tests/cases/run/` with names ending `_j.ash`; project-specific ones in `<project>/tests/cases/<feature>/`.
316312 3. Before runtime changes, `spec/performance-baseline.md` records `fib(30)` and sum-to-20M for `ashvm`; they are re-measured after each major runtime change.
317313
318314 ## Migration phases
@@ -367,7 +363,7 @@ D2 said the old front end is deleted once the new one reaches parity. The implem
367363
368364 ### Phase 3 — Type system
369365
370-Implemented in `hearth/front/src/jtypes` (`check_expr.c`, `check_stmt.c`, `check_class.c`), driven by `ashfront --check`. Covered by `hearth/front/tests/cases/types/*_j.ash`, run with `hearth/front/tests/scripts/check_types.sh`. Generics use erasure with argument-driven inference; wildcards are approximated (see `jtypes.h`).
366+Implemented in `hearth/front/src/jtypes` (`check_expr.c`, `check_stmt.c`, `check_class.c`), driven by `ashvm check`. Covered by `hearth/front/tests/cases/types/*_j.ash`, run with `hearth/front/tests/scripts/check_types.sh`. Generics use erasure with argument-driven inference; wildcards are approximated (see `jtypes.h`).
371367
372368 - [x] Implement primitive type checking.
373369 - [x] Implement class and interface type checking.
@@ -383,7 +379,7 @@ Implemented in `hearth/front/src/jtypes` (`check_expr.c`, `check_stmt.c`, `check
383379
384380 ### Phase 4 — Compiler/VM execution
385381
386-Implemented in `hearth/front/src/jlower` as a **lowering to old Ash** that runs unchanged on `ashvm` and compiles natively with `kiln` (no VM change, so the performance law holds by construction). The program becomes an imported module plus a two-line driver; objects are maps with a flat vtable, methods are top-level functions, `instanceof`/`catch` use per-class supertype tables, and native stdlib methods are `@Lower("...")` templates over the old builtins. `ashfront --run file.ash` executes on `ashvm`, `ashfront --build file.ash -o out` compiles with `kiln`. Covered by `hearth/front/tests/cases/run/*_j.ash` and `examples/`, both run on both engines by `hearth/front/tests/scripts/run_cases.sh`.
382+Implemented in `hearth/front/src/jlower` as a **lowering to the internal IR** (see D2a) that `ashvm` runs and `kiln` compiles natively (no VM change, so the performance law holds by construction). The program becomes one IR unit whose `start` function is called by the consumer; objects are maps with a flat vtable, methods are top-level functions, `instanceof`/`catch` use per-class supertype tables, and native stdlib methods are `@Lower("...")` templates over the old builtins. `ashvm run file.ash` executes on `ashvm`, `kiln file.ash -o out` compiles natively. Covered by `hearth/front/tests/cases/run/*_j.ash` and `examples/`, run on both engines by `hearth/ashvm/tests/scripts/run_regression.sh` and `hearth/kiln/tests/scripts/run_regression.sh`.
387383
388384 - [x] Compile or interpret class declarations.
389385 - [x] Compile or interpret constructors.
@@ -442,8 +438,8 @@ What still differs from the spec, and why. Everything else in the Numerics, Exce
442438
443439 | Area | Current behaviour | Spec says | To lift it |
444440 |---|---|---|---|
445-| `int` overflow | **exact Java 32-bit wrap with `ashfront --wrap-int`** (`+ - * << >>>`, unary `-`, `++ --`, compound assignment, narrowing casts; `(int) double` saturates). Default is off because it costs about 2x on arithmetic-heavy loops (the performance law); for-loop counters bounded by their own condition never pay it | wraps | none needed; choose the flag |
446-| `long` | **exact 64-bit with `ashfront --wrap-int`**: a plain number while it fits in 2^53, an exact two-halves value beyond (`+ - * / % << >> >>> & | ^ ~`, comparisons, casts, `Long.parse`, printing, `Long.MAX_VALUE/MIN_VALUE`), checked against an independent 64-bit model. Without the flag it is a double (exact below 2^53, printed correctly) so default speed is unchanged | 64-bit | none needed; choose the flag |
441+| `int` overflow | **exact Java 32-bit wrap with `--wrap-int`** (`ashvm run --wrap-int`, `kiln --wrap-int`) (`+ - * << >>>`, unary `-`, `++ --`, compound assignment, narrowing casts; `(int) double` saturates). Default is off because it costs about 2x on arithmetic-heavy loops (the performance law); for-loop counters bounded by their own condition never pay it | wraps | none needed; choose the flag |
442+| `long` | **exact 64-bit with `--wrap-int`**: a plain number while it fits in 2^53, an exact two-halves value beyond (`+ - * / % << >> >>> & | ^ ~`, comparisons, casts, `Long.parse`, printing, `Long.MAX_VALUE/MIN_VALUE`), checked against an independent 64-bit model. Without the flag it is a double (exact below 2^53, printed correctly) so default speed is unchanged | 64-bit | none needed; choose the flag |
447443 | `double` / `float` | Java's shortest round-trip printing for both (`7.068577499999999`, `1.0E-4`, `3.3000002`), verified against independent oracles; `float` operations round to 24 bits after every operation; every finite value, including denormals and `Double.MAX_VALUE`, via an exact big-integer expansion for exponents outside -5..22 (`Double.MIN_VALUE` prints `4.9E-324`, as in Java) | same | none |
448444 | Assignment, `++`, `--` inside expressions | supported with Java evaluation order (`a[k] = k++`, `x = y = 5`, `while ((n += 3) < 10)`) | allowed | none |
449445 | `switch` | full Java semantics: fallthrough, `default` anywhere, early `break`, `continue` through a switch (simple switches stay a plain if-chain) | same | none |
@@ -459,7 +455,7 @@ What still differs from the spec, and why. Everything else in the Numerics, Exce
459455
460456 The migration is complete when:
461457
462-- [x] No old Ash keywords remain in the supported language. (the application language rejects them; the old syntax survives only as the internal IR, see D2 amendment)
458+- [x] No old Ash keywords remain in the supported language. (both tools reject them; the old-shaped text survives only as the internal IR, see D2a)
463459 - [x] All examples use the new enterprise Java-like syntax.
464460 - [x] The lexer, parser, type checker, and runtime support the new model.
465461 - [x] Every public API has explicit types.

README.mdmodified

+69−145
@@ -8,9 +8,9 @@
88
99 ---
1010
11-## New Ash (Java-like, statically typed)
12-
13-Ash is being redesigned into an enterprise-style language: packages, classes, interfaces, enums, records, generics, `T?` null safety, checked exceptions, lambdas. The design contract is `MIGRATION.md`; the old scripting syntax below stays runnable until the new front end replaces it.
11+Ash is an enterprise-style, statically typed, Java-like language built from scratch in C with no
12+external dependencies: packages, classes, interfaces, enums, records, generics, `T?` null safety,
13+checked exceptions, lambdas. The design contract is `MIGRATION.md`.
1414
1515 ```java
1616 package demo;
@@ -31,171 +31,95 @@ public class Main {
3131 }
3232 ```
3333
34-```bash
35-make @front # builds bin/ashfront (plus: make @ashvm, make @kiln)
36-bin/ashfront --check Main.ash # type-check only
37-bin/ashfront --run Main.ash # lower and run on ashvm
38-bin/ashfront --build Main.ash -o main # compile natively with kiln
39-bin/ashfront --run --wrap-int Main.ash # Java's 32-bit int overflow (slower arithmetic)
40-```
41-
42-`examples/` has 17 runnable programs (classes, enums, records, generics, collections, lambdas, exceptions, file I/O, JSON, CLI, game, server, desktop-style); `hearth/front/tests/scripts/run_cases.sh` runs them on both engines. The standard library lives in `hearth/front/stdlib` (`ash.lang`, `ash.util`, `ash.io`, `ash.json`, `ash.math`, `ash.cli`, `ash.test`, `ash.time`, `ash.concurrent`, `ash.net`, `ash.web`, `ash.game`, `ash.desktop`; the last five are pure-Ash models because the VM has no threads, sockets, windows or clock). Old-syntax code (`local`, `say`, `given`, `forge`, ...) is rejected with a message naming the new construct. Known limits of the current backend are tabulated in `MIGRATION.md`.
43-
44----
45-
46-Ash is a small programming language built from scratch in C - lexer,
47-compiler, and a bytecode VM, all hand-written with no external dependencies.
48-It has closures, hash maps, first-class functions, real floating-point
49-numbers, `attempt`/`handle`, and a colorized compiler that points straight at
50-your mistakes.
51-
52-```ash
53-forge make_adder(x) {
54- yield forge(y) { yield x + y; };
55-}
56-
57-local add5 = make_adder(5);
58-say add5(3); // 8
59-
60-local nums = [1, 2, 3, 4, 5];
61-local evens = filter(nums, forge(n) { yield n % 2 == 0; });
62-say evens; // [2, 4]
63-
64-attempt {
65- local m = {"name": "Ash"};
66- say m["missing"];
67-} handle (e) {
68- say "caught: " + e;
69-}
70-
71-// conditions can drop the parens, and logical operators read as words
72-given add5(3) > 7 and not (evens[0] == 1) {
73- say "Lua-alike, not a Lua copy";
74-}
34+## Getting started
7535
76-each n in evens {
77- say n;
78-}
36+```bash
37+make # builds bin/ashvm, bin/kiln and the rest (`make @ashvm`, `make @kiln` for one)
7938
80-// stop/next, compound assignment, ternary, bitwise ops, and/none
81-local total = 0;
82-local i = 0;
83-during i < 10 {
84- i += 1;
85- given i == 3 { next; }
86- given i == 7 { stop; }
87- total += i;
88-}
89-say total > 0 ? "positive" : "zero or less"; // ternary
90-say (5 & 3) | (1 << 2); // bitwise: and, or, shift
39+bin/ashvm run Main.ash # run a program on the VM
40+bin/ashvm run --wrap-int Main.ash # Java's 32-bit int overflow (slower arithmetic loops)
41+bin/ashvm check Main.ash # parse and type-check only
42+bin/ashvm # interactive REPL
9143
92-local maybe = none;
93-say maybe == none ? "nothing there" : "something there";
44+bin/kiln Main.ash -o main # compile to a native ELF binary (no VM, no libc)
45+bin/kiln Main.ash -o main --target=windows --link=shared --wrap-int
9446 ```
9547
48+`ashvm` and `kiln` are the only front doors. Both link the one shared front end in `hearth/front`
49+(lexer, parser, AST, type checker, lowering) and differ only in the back end: a bytecode VM
50+versus a native code generator. Source written in the removed pre-migration scripting syntax
51+(`local`, `say`, `given`, `forge`, ...) is rejected with a message naming the new construct.
52+
53+`examples/` has 18 runnable programs (classes, enums, records, generics, collections, lambdas,
54+exceptions, file I/O, JSON, CLI, game, server, desktop-style); `hearth/ashvm/tests/scripts/run_regression.sh`
55+and `hearth/kiln/tests/scripts/run_regression.sh` run them on both engines. The standard library lives in
56+`hearth/front/stdlib` (`ash.lang`, `ash.util`, `ash.io`, `ash.json`, `ash.math`, `ash.cli`, `ash.test`,
57+`ash.time`, `ash.concurrent`, `ash.net`, `ash.web`, `ash.game`, `ash.desktop`; the last five are pure-Ash
58+models because the VM has no threads, sockets, windows or clock). Known limits of the current backend are
59+tabulated in `MIGRATION.md`.
60+
61+### REPL
62+
63+`ashvm` with no arguments starts a Java-like REPL. Imports, type declarations (`class`, `interface`,
64+`enum`, `record`), static methods (`int sq(int a) { return a * a; }`) and statements persist between
65+lines; a line without a trailing `;` or `}` is evaluated and printed (`x + 2`). Each line is checked and
66+run in a child process, so a compile error or uncaught exception leaves the session intact.
67+Limits: session state is rebuilt by replaying earlier lines (output suppressed), so non-idempotent side
68+effects such as file writes, stdin reads and clocks repeat; a variable name can be declared only once;
69+diagnostic line numbers refer to the generated wrapper, not to what you typed.
70+
9671 ## Engine
9772
98-Ash compiles to real bytecode and runs it on a stack-based VM (`ashvm`) with
99-computed-goto instruction dispatch and a handful of hand-fused
100-"superinstructions" for common patterns like `x = x + y` and `during (i < n)`
101-that collapse several bytecode ops into one. Ash used to ship a second,
102-slower tree-walk interpreter (`ashc`) as the place new features were
103-prototyped first - it has since been retired now that the VM has full
104-feature parity, including real `double` number semantics and Rust-style
105-runtime diagnostics.
73+Ash lowers to an internal representation that `ashvm` compiles to real bytecode and runs on a
74+stack-based VM with computed-goto instruction dispatch and a handful of hand-fused
75+"superinstructions" for common patterns like `x = x + y` and `while (i < n)` that collapse several
76+bytecode ops into one. `kiln` compiles the same representation straight to machine code.
77+That representation is a compiler internal (`hearth/front/src/ir`), not Ash source; no tool reads it
78+from a file.
10679
10780 ## Performance
10881
10982 Benchmarked against equivalent programs in C++ (`g++ -O2`) on the same machine:
11083
11184 | Benchmark | C++ | `ashvm` |
11285 |---|---|---|
113-| `fib(30)` (recursive) | 0.005s | 0.06s (~12x slower) |
114-| Sum of 0..20,000,000 | 0.004s | 0.12s (~30x slower) |
115-
116-That speed comes from three things layered on top of each other: compiling
117-to real bytecode instead of re-parsing source on every loop iteration,
118-computed-goto instruction dispatch instead of a `switch` statement, and
119-those hand-fused superinstructions. A hand-written x86-64 JIT experiment
120-(`benchmarks/native_jit.c`) that skips bytecode entirely gets within ~2x of
121-raw C++, which is roughly the ceiling for anything short of a full
122-optimizing native compiler.
123-
124-## Language features
125-
126-- Numbers (real doubles, with `1e3`-style literals), strings (with escape sequences), arrays, hash maps, `none`
127-- A real boolean type: `yes` / `no`, and everything that answers a question (`==`, `<`, `not`, `and`, `or`, `has`, `contains`, `file_exists`, ...) produces one. They print as `yes` / `no`, `type(yes)` is `"boolean"`, and `yes == 1` is false. Conditions accept booleans, numbers and `none`; a string, array, map or function in a condition is an error
128-- `and` / `or` short-circuit; strings compare bytewise with `< <= > >=`
129-- Maps keep insertion order (`keys`, `values`, printing); `delete` closes the gap
130-- Functions, recursion, closures with by-value capture, and forward declarations: a function can be called before its `forge` appears, so mutual recursion just works
131-- First-class functions - pass them around, store them in variables, call them indirectly. Builtins are values too: `map(xs, str)`, `local f = len;`
132-- Builtins:
133- - core: `len type str num exit sqrt abs floor chr ord`
134- - collections: `push pop insert slice delete sort keys values has map filter reduce` (`delete(array, i)` removes an index, `sort(array)` / `sort(array, cmp)` sorts in place and is stable; `cmp(a, b)` returns a number, `> 0` meaning `a` goes after `b`)
135- - strings: `split join substring indexOf contains starts_with ends_with replace repeat upper lower trim`
136- - files: `read_file write_file append_file file_exists rename_file delete_file make_dir list_dir`
137-- One number-to-text format on both engines: integers print plainly up to 1e15, other values with up to 6 fraction digits, and anything beyond that (or below 1e-6) in scientific form (`1e+21`, `1.5e-07`)
138-- `attempt` / `handle` error handling. Built-in errors raise a string with the same wording on both engines (`index out of bounds`, `key not found`, `invalid operand type in '+'`, ...); `raise expr;` raises any value, so a map gives a structured error. `e.message` (more generally `var.field`, same as `var["field"]`) reads a field of a raised map, and on a plain string error it is the string itself, so `e.message` works for every error
139-- `each (x in array)` and `during` loops, with `stop` / `next` for early exit and skip-to-next-iteration (leaving an `attempt` block that way pops its handler)
140-- Compound assignment (`+= -= *= /=`) and `++` / `--`
141-- Ternary `cond ? a : b`
142-- Bitwise operators: `& | ^ ~ << >>`
143-- Imports: `@import <./file.ash>;` makes the file's functions callable as `file.name(...)`; `@import <./file.ash> as alias;` picks the namespace. Imports are transitive (a file imported by a file you import is callable by its own namespace too) and each file is loaded once. A top-level `local` in an imported file is a module-level variable: its expression runs once at import, and the file's functions can read and update it (`NAME = ...;`, `NAME += ...;`, `NAME[i] = ...;`); outside, `file.NAME` reads it
144-- `exit(code)` ends the program with that status; an uncaught error exits with status 1
145-- Colorized, Rust-style diagnostics for both compile-time and runtime
146- errors: file:line:column, a source snippet, a caret, and a contextual hint
147-- An interactive REPL with persistent variables across lines
148-
149-### Performance notes
150-
151-- `len(s)`, `substring(s, i, j)` and `s[i]` are O(1) / O(length of the piece) on both engines (strings carry their length).
152-- `s += x` copies `s`, so building a long string one piece at a time is quadratic in time and, since nothing is freed, in memory. Collect the pieces in an array and `join(parts, "")` once - that is linear. `kiln`'s heap grows as needed (64MB at a time, `brk` on Linux / `VirtualAlloc` on Windows) rather than crashing at a fixed size, but nothing is freed until the process exits either way.
153-- `push` grows arrays geometrically (amortized O(1)); `sort` is a stable merge sort on `ashvm` and a stable insertion sort on `kiln`.
154-- `kiln`'s Windows output is checked without a Windows machine: `hearth/kiln/tests/scripts/windows_emulated_check.sh` runs every test case as a PE under the Unicorn x86-64 emulator (kernel32 faked over the real filesystem) and compares it with the Linux build. All of them, including the Windows file operations (`list_dir` via `FindFirstFileA`/`FindNextFileA`), match. `hearth/kiln/tests/scripts/windows_wine_check.sh` does the same under Wine (an independent `kernel32`), also with all cases matching. Neither is Windows itself.
155-
156-## Getting started
86+| `fib(30)` (recursive) | 0.005s | 0.25s |
87+| Sum of 0..20,000,000 | 0.004s | 1.5s |
15788
158-```bash
159-make @ashvm # builds bin/ashvm (plain `make` builds every project)
89+(`hearth/ashvm/benchmarks`, run with `ashvm run`; the numbers include about 4 ms of start-up for
90+loading and lowering the reachable standard library.) The speed comes from compiling to bytecode
91+instead of re-parsing source on every loop iteration, computed-goto dispatch instead of a `switch`,
92+and the fused superinstructions. A hand-written x86-64 JIT experiment
93+(`benchmarks/native_jit.c`) that skips bytecode entirely gets within ~2x of raw C++, which is
94+roughly the ceiling for anything short of a full optimizing native compiler.
16095
161-bin/ashvm script.ash # run a script
162-bin/ashvm # start the REPL
163-```
96+### Performance notes
16497
165-`kiln` (compiles `.ash` straight to a native ELF/PE binary) and `forgepack`
166-(the package manager) build the same way: `make @kiln` / `make @forgepack`.
98+- Building a long string one piece at a time with `+=` copies each time, so it is quadratic in time and, since nothing is freed by default, in memory. Collect the pieces and join them once (`StringBuilder`, `String.join`).
99+- `kiln`'s heap grows as needed (64MB at a time, `brk` on Linux / `VirtualAlloc` on Windows) rather than crashing at a fixed size, but nothing is freed until the process exits. `ashvm` has an opt-in collector (`make GC=1`).
100+- `kiln`'s Windows output is checked without a Windows machine: `hearth/kiln/tests/scripts/windows_emulated_check.sh` runs every test program as a PE under the Unicorn x86-64 emulator (kernel32 faked over the real filesystem) and compares it with the Linux build. `hearth/kiln/tests/scripts/windows_wine_check.sh` does the same under Wine (an independent `kernel32`). Neither is Windows itself.
167101
168102 ## Project structure
103+
169104 Every project lives in `hearth/` and builds into `bin/`; the repository root only holds
170105 documentation, `examples/` and the build entry point.
171106
172-hearth/ashvm/ the engine - everything below is self-contained inside here
173-src/
174-lexer/ tokenizer
175-value/ VMValue tagged type, hash map
176-vm/ bytecode Chunk format, dispatch loop, error handling, public API
177-compiler/ single-pass compiler: parser/emitter state, expression chain,
178-call/lambda emission, statement compilation
179-builtins/ len, map, filter, reduce, string/file ops
180-diagnostics/ shared Rust-style error reporting (compile-time and runtime)
181-
182-Every subsystem keeps sources in `C/`, headers in `H/`, build output in a
183-mirrored `O/` tree, and generated dependency files in `D/` (so editing a
184-shared header correctly triggers a rebuild of everything that includes it),
185-e.g. `src/vm/C/dispatch.c` includes `src/vm/H/vm.h` and builds to
107+```
108+hearth/front/ the only frontend: lexer, parser, AST, type checker, lowering, internal IR lexer,
109+ REPL session model and the standard library (stdlib/). Built as libashfront.a,
110+ linked by ashvm and kiln. There is no front-end executable.
111+hearth/ashvm/ the VM engine: compiler (IR -> bytecode), value, vm, builtins, diagnostics, app (CLI + REPL)
112+hearth/kiln/ the native compiler: IR -> machine code -> ELF/PE (no VM, no libc)
113+hearth/forgepack/ package manager: forgepack init/add/install/list, dependencies fetched from GitHub
114+ into a project-local @ash-modules/ folder; point ASH_PATH at an installed source root
115+ to compile against it (see ideas/ash_modules.md)
116+hearth/pyre/ GPU/CPU compute backends (C ABI)
117+```
118+
119+Every subsystem keeps sources in `C/`, headers in `H/`, build output in a mirrored `O/` tree, and
120+generated dependency files in `D/` (so editing a shared header correctly triggers a rebuild of
121+everything that includes it), e.g. `src/vm/C/dispatch.c` includes `src/vm/H/vm.h` and builds to
186122 `src/vm/O/dispatch.o` with `src/vm/D/dispatch.d` tracking the header.
187-app/ main.c entry point (file mode + REPL)
188-benchmarks/ .ash and .cpp benchmark programs, plus the x86-64 JIT experiment
189-tests/ cases/<feature>/*.ash programs covering each language feature; scripts/ holds the regression runners
190-
191-hearth/kiln/ a second, independent implementation: compiles .ash straight to a
192-native ELF/PE binary (no VM, no libc), same language, full feature parity
193-with ashvm, cross-validated against it directly (see CHANGELOG.md).
194-
195-hearth/forgepack/ package manager -- `forgepack init/add/install/list`, dependencies
196-fetched from GitHub into a project-local @ash-modules/ folder (same idea
197-as node_modules). `@import <name>;` already works with it. No registry
198-yet -- see ideas/ash_modules.md for the full design and what's next.
199123
200124 ## Editor support
201125

hearth/front/src/jtypes/C/classes.cmodified

+1−1
@@ -47,7 +47,7 @@ void jt_init_default_roots(const char *entry_path, const char *entry_pkg) {
4747 }
4848 }
4949 jt_add_root(root);
50- /* dependency roots (forgepack): <root>/ash.libs/<lib>, <root>/@ash-modules/<lib>/src/main/ash */
50+ /* extra source roots, e.g. forgepack-installed dependencies: ASH_PATH=dir1:dir2 */
5151 const char *env = getenv("ASH_PATH");
5252 if (env) {
5353 char *e = j_strdup(env);

hearth/kiln/README.mdmodified

+15−25
@@ -11,30 +11,21 @@ hand-encodes x86-64 machine code and writes its own ELF executables
1111 directly, the same "every line hand-written" rule that built `ashc` and
1212 `ashvm`.
1313
14-## Status: milestone 1 done (Linux only)
15-
16-`kiln file.ash -o out` compiles `say <expr>;` programs (integer literals,
17-`+ - * /`, unary `-`, parens, multiple statements) straight to a real,
18-standalone, freestanding ELF64 executable - no libc, raw syscalls only.
14+## Usage
1915
2016 ```bash
21-cd kiln && make # builds ../bin/kiln
22-../bin/kiln tests/cases/basics/hello.ash -o /tmp/hello
23-/tmp/hello # prints 7 (1 + 2 * 3)
24-```
17+make @kiln # builds ../bin/kiln
18+bin/kiln examples/HelloWorld.ash -o /tmp/hello
19+/tmp/hello # Hello, Ash!
2520
26-## What's deliberately not built yet
21+kiln <file.ash> -o <output> [--target=linux|windows] [--link=static|shared] [--wrap-int]
22+kiln --emit-runtime -o <libkilnrt.so|libkilnrt.dll> [--target=linux|windows]
23+```
2724
28-- Everything beyond numeric `say` statements - variables, functions,
29- strings, arrays, control flow, closures
30-- Multi-object linking (the whole program is one self-contained object
31- right now; there's no symbol-resolution step yet)
32-- The actual shared-runtime-library dedup feature goal 2 is ultimately
33- about - that needs real dynamic linking (PIC codegen, dynamic symbol
34- table, PLT/GOT, ELF dynamic sections), a substantial milestone of its
35- own, deliberately not tackled alongside "does codegen produce a runnable
36- binary at all"
37-- Windows PE output, macOS Mach-O output
25+Kiln compiles Java-like Ash (packages, classes, generics, ...) to native ELF (Linux) or PE (Windows)
26+executables. It has no frontend of its own: `hearth/front` (lexer, parser, type checker, lowering) is
27+linked in and hands kiln the internal IR (`hearth/front/src/ir`), which `src/parser` turns into machine
28+code in one pass. Old pre-migration scripting syntax is rejected by the front end.
3829
3930 ## Layout
4031
@@ -43,12 +34,10 @@ kiln/
4334 Makefile builds ../bin/kiln
4435 src/ each subsystem: C/ (sources), H/ (headers), D/ (deps), O/ (objects), mirrored
4536 app/ main.c argv parsing, drives the pipeline; target.c/.h
46- lexer/ tokenizer (own copy, same shape as ashvm's)
47- parser/C/ H/ D/ O/
37+ parser/C/ H/ D/ O/ one-pass IR -> machine code (the IR lexer lives in front/src/ir)
4838 core/ parser entry, shared parser state
49- statements/ control flow, each loops, attempt/handle, loop stack
39+ statements/ control flow, loops, try/catch lowering, loop stack
5040 declarations/ variables, functions, constants
51- imports/ import resolution
5241 codegen/C/ H/ D/ O/ (same feature folders in each tree)
5342 emit/ raw x86-64 encoding, byte buffers, memory layout, value tags
5443 expressions/ expression -> arithmetic instructions, calls, booleans
@@ -62,5 +51,6 @@ kiln/
6251 elf/ ELF executable and shared-object writers
6352 pe/ PE executable and DLL writers
6453 diagnostics/ error reporting
65- tests/ cases/<feature>/*.ash programs (+ scripts/run_regression.sh)
54+ tests/ cases/<feature>/*.ash programs (+ scripts/run_regression.sh, which also runs
55+ hearth/front/tests/cases/run and examples/)
6656 ```
\ No newline at end of file

ideas/ash_modules.mdmodified

+2−0
@@ -1,5 +1,7 @@
11 # Ash Modules — a package manager for Ash
22
3+> **Status note (2026-10-18):** this design predates the Java-like migration. `@import <name>;` no longer exists; dependencies are now source roots for `import package.Type;` (set `ASH_PATH` to an installed root). The sections below describe the original `@import`-based design and are kept for the manifest and layout decisions only.
4+
35 > **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.
46
57 ---

main.ashmodified

+3−17
@@ -13,26 +13,12 @@ import ash.util.HashMap;
1313 // [x] Phase 0 specification locked (MIGRATION.md decisions D1-D15)
1414 // [x] Phase 1 lexer: new keywords, // /* */ /// comments
1515 // [x] Phase 2 parser + AST: everything in this file parses
16-// [x] Phase 3 type checker: `ashfront --check main.ash` passes
16+// [x] Phase 3 type checker: `ashvm check main.ash` passes
1717 // (types, nullability, generics, overloads, exceptions, switch)
18-// [x] Phase 4 lowering: `ashfront --run main.ash` (ashvm), `--build` (kiln native)
18+// [x] Phase 4 lowering: `ashvm run main.ash`, `kiln main.ash -o main` (native)
1919 // [x] Phase 5 stdlib: lang util io json math cli test time concurrent net web game desktop
2020 // (the last five are pure-Ash models; real OS access needs new VM builtins)
21-// [x] Phase 6 examples/ (17 programs), README, API docs on every stdlib declaration
22-//
23-// Old New
24-// local x = 5; int x = 5; / var x = 5;
25-// say x; System.out.println(x);
26-// given / otherwise if / else
27-// during while
28-// each x in xs for (int x : xs)
29-// stop; next; break; continue;
30-// forge f(a) { yield } int f(int a) { return ...; }
31-// attempt / handle try / catch
32-// raise v; throw new SomeException(...);
33-// yes / no / none true / false / null
34-// @import <f.ash>; import pkg.Type;
35-// and / or / not && / || / !
21+// [x] Phase 6 examples/ (18 programs), README, API docs on every stdlib declaration
3622 // ---------------------------------------------------------------------------
3723
3824 /// A sealed interface with a default method.