← Commit history

init

lennoxrose committed on Oct 2, 2026 at 22:49 UTC

a26d22b

300 files changed+16148−0

.gitignoreadded

+26−0
@@ -0,0 +1,26 @@
1+# Build artifacts
2+*.o
3+/bin/
4+
5+# Compiled benchmark binaries (keep the .cpp sources, ignore the built outputs)
6+ashvm/benchmarks/fib_cpp
7+ashvm/benchmarks/loop_cpp
8+ashvm/benchmarks/countdown_cpp
9+ashvm/benchmarks/hashmap_cpp
10+ashvm/benchmarks/native_jit
11+
12+# OS / editor cruft
13+.DS_Store
14+*.swp
15+*~
16+
17+# Dont put ai Slop in my Code
18+.claude
19+CLAUDE.md
20+.agents
21+skills-lock.json
22+
23+# Private Notes
24+goals.md
25+rundown.md
26+missing.md
\ No newline at end of file

CHANGELOG.mdadded

+259−0
@@ -0,0 +1,259 @@
1+## 2026-07-21 (modules/imports + library storage, both engines)
2+kiln and ashvm both now support `@import <./relative/path.ash>;` and `@import <libname>;` (the latter resolving to `ash.libs/<libname>/<libname>.ash`), with imported functions and constants accessed via namespaced `ns.func(...)`/`ns.CONST` syntax. Implementation is source-level recursive merge, matching the design spec's Approach A: no linker, no new object format — the parser recursively parses an imported file's `fn` declarations and literal top-level `let` constants inline and registers them under a namespaced name (e.g. `"math.add"`) in the same flat function/constant tables everything else already uses. Constants are compile-time-folded to their literal value at each `ns.CONST` use site, not stored as real runtime state. Diamond dependencies (the same file reached via more than one import path) dedup by canonical filesystem path; circular imports and namespace collisions (two different files/libraries deriving the same auto-namespace) are both compile errors, with the collision message naming both colliding resolved paths. Verified by a real regression suite on both engines: `kiln/tests/imports_basic.ash`, `imports_lib.ash`, `imports_const.ash`, `imports_diamond.ash`, `imports_circular_check.sh`, `imports_collision_check.sh`, mirrored in `ashvm/tests/`.
3+
4+**Explicitly not done here, by design (see `docs/superpowers/specs/2026-07-21-kiln-modules-imports-design.md`'s accepted tradeoffs), not oversight:** no library installer tool (no npm-equivalent fetch/publish mechanism — this only builds where libraries live and how they resolve/compile in); no real global mutable variable storage (constant folding solves the importable-constant case only, not shared mutable state across functions); no caching/incremental compilation (every imported file is recompiled from source on every build).
5+
6+## 2026-07-20 (Plan B, phase B5 completed — Windows DLL equivalent)
7+`--link=shared` now works for `--target=windows` too: compiled programs import from a real `libkilnrt.dll`, loaded and symbol-resolved by the actual Windows PE loader (Wine, in this environment — no Windows box available, same honest caveat as Plan A's PE work). Plan B is now fully complete on both platforms.
8+- **New `pe_dll_writer.c`** — the PE twin of `elf_so_writer.c`: an `IMAGE_FILE_DLL`-flagged PE32+ file with a real Export Directory (function/name/ordinal arrays), exporting the same 5 runtime functions by name. One real correctness requirement ELF's hash-table lookup doesn't have: the Windows loader resolves imports-by-name via **binary search** over the Name Pointer Table, so that table has to be sorted lexicographically — got this right the first time by explicitly sorting rather than assuming enum-declaration order would happen to work.
9+- **Generalized `pe_writer.c`'s import table** (`pe_dynamic.c`, new) to support a SECOND imported DLL (`libkilnrt.dll` alongside `kernel32.dll` — previously hardcoded for exactly one) via a fully parallel `KILN_PE_SHARED_*` constant set, so `--link=static` output stays byte-for-byte unchanged (verified, not assumed). One real layout bug caught here: the two DLLs' IAT regions were interleaved (ILT_k32, IAT_k32, ILT_rt, IAT_rt), which broke the optional header's IAT data directory (a single `{RVA,Size}` pair can only describe one *contiguous* range) — fixed by grouping both ILTs before both IATs.
10+- **New `win_call_runtime_import`** for calling into `libkilnrt.dll`: needed a genuinely different mechanism than `win_call_import`'s kernel32 calls, and not just cosmetically. kiln's runtime routines use kiln's own bare per-routine register conventions (not the Windows ABI), so — same reasoning as the Linux GOT call — no register was safe to clobber loading the IAT address first. Linux's fix (`call [disp32]`, absolute-displacement addressing) doesn't carry over: kiln's PE image base (`0x140000000`) is already past the 32-bit absolute-displacement range. `call [rip+disp32]` does work, since everything in a PE image is close together, and still touches zero registers.
11+- One real, structural bug found by testing the Windows shared build under Wine (not assumed correct from the Linux case): `pe_import_addr()` — used by every EXISTING kernel32 call site (`VirtualAlloc`, `WriteFile`, ...) — unconditionally returned the static-mode IAT address, even in shared mode, where kernel32's IAT sits at a different offset (a second DLL's import machinery is now ahead of it). Every kernel32 call in a shared-mode Windows binary was jumping through the wrong slot — manifested as a jump to a garbage address on the very first `VirtualAlloc` call, caught immediately by running under Wine rather than trusting the structural PE checks alone.
12+
13+Verified for real: 29/30 tests byte-identical between `--link=static` and `--link=shared` output for `--target=windows` too (same `argv[0]`-filename non-bug as Linux), all executed under Wine, not just structurally validated. Both static-mode paths (Linux and Windows) confirmed fully unaffected — 0 regressions across the whole matrix (static/shared × Linux/Windows). `make runtime-dll` builds `libkilnrt.dll`; no install step needed on Windows (standard DLL search order checks the launching `.exe`'s own directory first).
14+
15+## 2026-07-20 (Plan B, phases B2-B5 — real ELF dynamic linking for kiln)
16+kiln now supports `--link=shared`: compiled programs dynamically link against a real `libkilnrt.so` at runtime, loaded and symbol-resolved by the actual system dynamic linker (`/lib64/ld-linux-x86-64.so.2`) — not a kiln-invented format, a genuine SysV-ABI-compliant shared object inspectable with `nm -D`/`readelf`/`ldd` like any other `.so`. Chose the hard path deliberately after flagging the risk (hand-implementing dynamic-linking internals is real linker/loader engineering, not comparable in difficulty to anything else in this project, and a subtly-wrong implementation degrades to unpredictable segfaults rather than a clean error) — validated the entire binary format with a from-scratch Python prototype against the REAL linker before writing a line of kiln C code, catching two layout bugs (VA-vs-file-offset for `.dynamic`/`.rela` entries) at that stage instead of inside kiln's own codegen.
17+- **B2** position-independence: audited every runtime routine and found the ACTUAL scope was much smaller than expected — kiln's fixed, non-PIE executable load base means the globals region address is a genuine process-wide constant even for code that will live in an ASLR'd `.so` (the `.so` doesn't own the globals, whichever executable calls it does, and every kiln executable is fixed-base), so only `print_int.c`'s one embedded `"nil\n"` literal needed real RIP-relative addressing (new `emit_lea_rip_disp32`/`emit_lea_rip_back` primitives). Everything else either had no embedded data or already used relative jumps.
18+- **B3** `src/elf/elf_so_writer.c` — hand-written ET_DYN ELF64 shared object: SysV `.hash` (not GNU hash — simpler, still what real linkers accept), `.dynsym`/`.dynstr`, no section headers (matches this project's existing "tiny ELF" philosophy; `nm -D`/`readelf` don't need them, only `ld.so`'s own `PT_DYNAMIC`-driven parsing does, and that's section-header-free by design). Exports 5 functions via `codegen/runtime_exports.def` (X-macro, mirrors Plan A's `pe_imports.def` pattern): `kiln_heap_alloc`, `kiln_bytes_copy`, `kiln_string_alloc_prefixed`, `kiln_print_top`, `kiln_raise`.
19+- **B4** `src/elf/elf_dynamic.c` — the executable side: `PT_INTERP` (real system linker), `PT_DYNAMIC` with `DT_NEEDED=libkilnrt.so` + `DT_RPATH=/usr/local/lib/kiln`, a GOT with eager (non-lazy) `R_X86_64_GLOB_DAT` relocations — deliberately skipped lazy PLT-stub binding (real linkers' default) since it needs a resolver-callback dance with `ld.so` for zero behavioral benefit at kiln's scale. New `emit_call_abs32` primitive (`call [disp32]`, x86-64's absolute-displacement addressing mode) calls through a GOT slot touching **zero** general-purpose registers — necessary because kiln's 5 runtime routines each have their own fixed argument registers (`heap_alloc`: RDI/RAX, `print_top`: RAX/RBX, `bytes_copy`: RDI/RBX/RDX...), so no single register was safe to clobber loading the GOT address first without silently breaking one of them (`bytes_emit_copy` in particular documents RSI as preserved for callers — a register-based GOT-address scratch would have violated that contract).
20+- **B5** `make install-runtime` (root required, never run by `make all`) installs `libkilnrt.so` to the fixed RPATH.
21+
22+One real, structural bug found via full-suite testing (not assumed away): the first working end-to-end version crashed on the very first heap allocation. Root cause was a conflated flag — `elf_so_writer.c` temporarily forced link mode to `static` while compiling the runtime, so that the runtime's own internal calls (e.g. `string_alloc_prefixed` calling `heap_alloc`) would resolve as local same-`.so` calls instead of GOT calls back into itself — but `codegen/layout.c`'s globals-address selection reads that SAME flag, and forcing it to `static` also silently made the runtime compute the WRONG globals address (the static-executable layout instead of the shared one) for its own heap pointer. Fixed by splitting into two independent flags (`KilnLinkMode` for "which layout owns the globals", `kiln_is_compiling_so()` for "should this routine's own calls be local or GOT-based") — the two questions looked identical but weren't.
23+
24+Verified for real, not assumed: 29/30 `kiln/tests/*.ash` byte-identical between `--link=static` and `--link=shared` output (the 1 "difference," `argv.ash`, is `argv[0]` correctly reflecting each binary's own distinct filename — not a bug). `ldd` on a `--link=shared` binary shows the real `libkilnrt.so` dependency; `/lib64/ld-linux-x86-64.so.2` loads and resolves it with zero kiln-side involvement at runtime. Full 30/30 static-mode and Windows-target regression confirmed unaffected.
25+
26+**Not done: Windows DLL equivalent.** Flagging honestly rather than claiming completeness — this needs (a) a full PE *export* table writer (new, `pe_writer.c` only ever wrote *import* tables before) and (b) generalizing `pe_writer.c`'s import-table layout to support a second imported DLL (`libkilnrt.dll` alongside `kernel32.dll` — currently hardcoded for exactly one). Comparable in scope to B3+B4 combined, not a quick extension, even though the *design* is proven out by the Linux work and Plan A's existing IAT/`win_call.c` machinery gives a clear analogous path. Left for a dedicated follow-up session rather than rushed.
27+
28+## 2026-07-20 (Plan B, phase B1 — kiln intra-binary runtime deduplication)
29+First step of "shared runtime library": before a runtime can be shared ACROSS binaries (B3+), the copy inside ONE binary needed to stop being duplicated at every call site in the first place — investigation found kiln's codegen re-emitted several "runtime" routines' full instruction sequences inline at every use (e.g. `heap_emit_alloc`'s 4 instructions at all 27 of its call sites, `codegen_print_top`'s ~150-instruction number-formatting routine at every single `print` statement in the user's source). Converted the four hottest offenders to real subroutines: emitted once at program start (jumped over, same technique `errors.c`'s raise routine already used), called via `emit_call_back` from every use site instead of re-emitted — `heap_emit_alloc`, `bytes_emit_copy`, `string_alloc_emit_prefixed`, `codegen_print_top`. Result on `tests/arrays.ash`: 20302 → 11588 bytes (43% smaller), output byte-identical on both Linux and Wine-executed Windows builds, full 30-test regression clean on both targets.
30+
31+One real bug hit and fixed during this: converting `codegen_print_top` initially left a stray leftover `pop rbx` inside the shared routine body (an artifact of a multi-step edit that only removed one of the original two `pop` instructions) — since the caller now pops the value into registers *before* the `call` (a real `CALL` pushes a return address on top of whatever's already on the stack, so a `pop` inside the callee would grab the return address instead), that leftover pop corrupted the stack and crashed on the second-ever print statement. Caught via a minimal 2-statement repro + hand-disassembly, not assumed away.
32+
33+## 2026-07-20 (Plan A complete — kiln Windows PE output)
34+kiln now emits real, runnable Windows PE32+ binaries via `--target=windows` (auto-appends `.exe`), verified end-to-end under Wine (wine-11.13), not just structurally. Phases A2-A6:
35+- **A2** `src/pe/pe_writer.c/.h` — hand-written PE32+ writer mirroring elf_writer.c's "tiny ELF" technique: one RWX section, fixed non-relocated image base (`0x140000000`, matching MSVC's own default), no external linker. `src/codegen/layout.c/.h` added as the single dispatch point (`kiln_code_base()`, `kiln_heap_ptr_addr()`, etc.) so the ~8 call sites that used to hardcode `KILN_LOAD_BASE+KILN_CODE_START_OFFSET` now route through one target-aware indirection instead of each growing its own branch. `MAX_KILN_TRY_DEPTH`/handler-size constants extracted into shared `codegen/runtime_layout.h` (both writers include it) since they were never actually ELF-specific.
36+- **A3** `src/codegen/win_call.c/.h` — Windows x64 ABI call emission: dynamic 16-byte RSP realignment + shadow-space reservation (saves/restores the exact pre-call RSP via RBP-relative scratch rather than assuming any fixed alignment, since kiln never tracked RSP parity before — raw Linux `syscall` never needed it). R8/R9 argument loading via small file-local REX.B-extended helpers (kiln's public `Reg` enum never needed to grow past RDI). Hand-verified byte-for-byte against the expected encoding via objdump before building anything on top of it, per the explicit ask to treat this phase with extra care.
37+- **A4** Import table + IAT for kernel32.dll (`ExitProcess`, `WriteFile`, `ReadFile`, `CreateFileA`, `CloseHandle`, `GetFileSize`, `VirtualAlloc`, `GetCommandLineA`, `GetStdHandle`) via `pe/pe_imports.def`, an X-macro list shared between the address-computing enum and the actual table bytes so they can't drift out of sync.
38+- **A5** every raw-syscall site rewired behind small platform-dispatch modules (`platform_console.c`, `platform_file.c`, `win_file_call.c`) rather than scattering `if (target==...)` at each of the ~20 call sites: `heap.c` (VirtualAlloc, actually simpler than Linux's brk query-then-grow dance), `print_int.c`/`errors.c` (WriteFile/ExitProcess), file builtins (CreateFileA/ReadFile/WriteFile/CloseHandle/GetFileSize — the last replacing Linux's lseek(END)+lseek(SET) dance with one direct call), `input_builtin.c` (ReadFile, handle cached once per call instead of once per byte), `argv_builtin_windows.c` (new file — GetCommandLineA has no kernel-provided argv array, so Windows gets a hand-rolled whitespace tokenizer instead of reading the process-startup stack Linux's argv_builtin.c relies on; also strips the quote pair GetCommandLineA wraps the exe path in).
39+- **A6** all 30 `kiln/tests/*.ash` compiled for both targets and diff-checked: 28/30 byte-identical output between native Linux execution and Wine execution of the Windows binary; the 1 `argv.ash` "difference" is Wine's `Z:\...` path translation (correct, expected, not a bug — same as running on a real mapped Windows drive); `input_builtin.ash` piped-stdin reads return empty under Wine specifically, traced via strace/objdump to Wine's own console-emulation layer for CUI-subsystem apps rather than kiln's codegen (hand-verified ABI-correct, and the identical byte-loop algorithm works correctly natively on Linux) — unresolved pending access to a real Windows machine to confirm one way or the other, since it cannot be conclusively distinguished from a genuine Wine limitation in this environment. One real bug found and fixed along the way: the quote-strip logic for `argv()`'s Windows path initially reused the same scratch slot for both "adjusted copy bounds" and "resume-scanning position," corrupting the latter and crashing on multi-token command lines — fixed by giving the adjusted bounds their own scratch slots.
40+
41+## 2026-07-19 (Plan A, phase A1 — kiln target flag)
42+kiln: added `--target=linux|windows` CLI flag (`src/target.c/.h`, `KilnTarget` global) — plumbing only, no codegen fork yet. Investigation confirmed kiln's own `fn` calling convention (stack-push based) never depended on SysV register-argument rules, so it needs no changes for Windows; the real OS-specific surface is the ~7 files issuing raw Linux `syscall` instructions plus the ELF writer, both addressed in later Plan A phases. Also confirmed no `libkilnrt.a` exists — the "runtime" is inline-generated codegen per binary, not statically linked at all, which reshapes Plan B (shared runtime library) into "build a real linkable runtime first, then make it shared" rather than "convert static to dynamic." Verified default and `--target=linux` output byte-identical to pre-flag builds via `cmp`; `--target=windows` accepted as a no-op stub; invalid target values rejected with a clear error. Zero regressions across all 30 `kiln/tests/*.ash`.
43+
44+## 2026-07-16 04:57
45+restructured into component-based file tree (lexer/value/env/interpreter/builtins); added arrays, indexing, index-assignment, for-in loops, logical operators (&& || !), modulo, and builtins (len, push, input, str, num, sqrt, abs, floor)
46+
47+## 2026-07-16 05:05
48+added bytecode compiler + stack-based VM (src/vm/) as ashvm binary alongside the tree-walk ashc interpreter; numeric-only for now (no strings/arrays in VM path yet)
49+
50+## 2026-07-16 05:10
51+added native_jit.c: hand-assembled x86-64 machine code JIT (mmap+PROT_EXEC) for one fixed loop pattern, proving native codegen closes the gap to C++; not a general Ash compiler yet
52+
53+## 2026-07-16 05:14
54+ashvm: switched stack/constants from double to int64_t and replaced switch dispatch with computed-goto threading — measuring real impact on fib(30) and loop benchmarks
55+
56+## 2026-07-16 05:19
57+ashvm: added peephole-fused superinstructions (OP_INC_LOCAL, OP_ADD_LOCAL_LOCAL, OP_LOOP_LT_JUMP) mirroring GCC's instruction-combining passes; targets the loop accumulate/compare/branch pattern specifically
58+
59+## 2026-07-16 05:24
60+generalized VM peephole fusion: OP_ACC_LOCAL now handles +,-,* with local-or-constant operand; OP_CMP_JUMP now handles all 6 comparisons with local-or-constant operand, applied to BOTH if and while (not just while); verified against multiple distinct benchmark shapes, not just the original loop
61+
62+## 2026-07-16 05:29
63+ashc: added first-class functions (functions as values, function-typed variables, indirect calls), lambda literals (fn(params){body} as an expression), and builtins map/filter/reduce; reorganized root directory (benchmarks/ for all bench files, tests/ for example .ash programs, removed stray test.ash)
64+
65+## 2026-07-16 05:33
66+fixed critical bug: map/filter/reduce's nested function calls were clobbering the outer parser's token position without restoring it (call_by_index now saves/restores caller position at the single shared call site)
67+
68+## 2026-07-16 05:39
69+ashc: added real closures (lambdas capture enclosing scope by value at creation time), new Closure type in src/interpreter/closures.c, map/filter/reduce accept both plain functions and closures
70+
71+## 2026-07-16 05:42
72+ashc: added hash maps (VAL_MAP), map literals {"key": val}, get/set via existing bracket syntax, builtins keys/values/has/delete, extended len() to maps; new src/value/hashmap.c component
73+
74+## 2026-07-16 05:45
75+ashc: added file I/O builtins (read_file, write_file, append_file, file_exists) and string builtins (split, join, substring, indexOf, replace, upper, lower, trim)
76+
77+## 2026-07-16 05:47
78+fixed bug: string literals never processed escape sequences (\n, \t, \", \\ were passed through literally). Added copy_string_escaped() in value.c, used for string literals and map keys.
79+
80+## 2026-07-16 05:49
81+fixed lexer bug: string scanning didn't skip escaped quotes (\"), so strings containing an escaped quote were truncated early. Lexer now skips over any backslash-escaped character while scanning for the closing quote.
82+
83+## 2026-07-16 05:52
84+benchmarked ashc hashmap performance vs C++ std::unordered_map after rounds 1-3 (closures/hashmaps/string+file builtins) - confirms ashc has had no speed optimization work, unlike ashvm
85+
86+## 2026-07-16 07:42
87+ashc: added try/catch error handling via setjmp/longjmp, with explicit scope-depth and parser-position repair after a caught error (new src/interpreter/error.c); converted genuine runtime errors (undefined var, type errors, out-of-bounds, missing map keys, arg mismatches, file I/O failures) to be catchable, while parse/syntax errors remain hard crashes
88+
89+## 2026-07-16 07:52
90+cleaned up trailing embedded newlines in auto-converted runtime_error() messages (leftover from their original fprintf format strings), fixing double-blank-line output when caught errors are printed
91+
92+## 2026-07-16 07:58
93+ashc: colorized Rust-style error output (file:line:col, source snippet, caret pointer, contextual help hints); fixed line/column accuracy by computing position via a scan of the original untouched source buffer instead of the interpreter's resettable internal line counter
94+
95+## 2026-07-16 08:02
96+fixed off-by-one bug in error hint lookup (hardcoded prefix lengths were wrong, e.g. 19 instead of 18 for 'undefined variable', silently failing every strncmp); now computes prefix length via strlen() automatically instead of manual counting
97+
98+## 2026-07-16 08:05
99+ashvm round 1 of feature-merge: added tagged VMValue type (numbers + strings) replacing raw int64_t stack values; string literals, concatenation via +, string equality via ==/!=, all opcodes and fused superinstructions updated to be type-aware
100+
101+## 2026-07-16 08:10
102+ashvm round 2 of feature-merge: added arrays (VM_ARRAY type, array literals, indexing read/write) with new opcodes OP_ARRAY/OP_INDEX_GET/OP_INDEX_SET; full-file rewrites this round to avoid the silent-corruption pattern seen with partial regex edits earlier
103+
104+## 2026-07-16 08:18
105+ashvm: ported hash maps, function values, indirect calls, map/filter/reduce (via named functions), and all 23 ashc builtins into the VM; new synchronous nested-call mechanism (vm_call_function_sync) enables higher-order builtins; true inline lambda closures still pending as a final round
106+
107+## 2026-07-16 08:27
108+ashvm: added real closures (fn(x){...} lambda literals capturing enclosing scope by value, via reserved local slots placed after params + OP_MAKE_CLOSURE), map/filter/reduce now accept closures as well as named functions
109+
110+## 2026-07-16 08:28
111+fixed cosmetic strncpy-truncation warnings in the lambda-naming code by clamping namelen to the buffer size
112+
113+## 2026-07-16 08:29
114+fixed persistent strncpy-truncation warnings by switching to memcpy+manual-null-terminate (GCC's -Wstringop-truncation is overly conservative for variable-length strncpy even when provably safe)
115+
116+## 2026-07-16 08:34
117+ashvm: added try/catch via setjmp/longjmp with compile-time-known catch-block bytecode offsets (OP_TRY_PUSH/OP_TRY_POP), converted all vm.c and builtins.c runtime error sites to be catchable; VM merge project now feature-complete relative to ashc
118+
119+## 2026-07-16 08:35
120+fixed real setjmp/longjmp bug: 'frame' and 'newf' locals in vm_execute needed volatile qualifiers, since they're reassigned after setjmp() and the C standard leaves their post-longjmp value undefined otherwise (GCC's -Wclobbered caught this) — was causing corrupted error messages and cascading state after any caught error
121+
122+## 2026-07-16 08:50
123+added interactive REPLs to both ashc and ashvm; ashc auto-wraps each line in try/catch for safe error recovery, ashvm uses a separate C-level recovery jmp_buf (vm_set_repl_recovery) instead of synthetic try/catch, to avoid reintroducing the variable-scoping bug from earlier; both support multi-line input via brace counting, persist variables/functions across lines, syntax errors still end the session (known limitation, consistent with both engines treating syntax errors as fatal)
124+
125+## 2026-07-18 11:52
126+ashvm: VMValue.number is now double (real float support) — matches ashc's number semantics exactly (str/num/sqrt/abs/floor, %-as-fmod), closing the int-truncation gap called out in goals.md
127+
128+## 2026-07-18 11:55
129+Add src/diagnostics/ — Rust-style colorized error reporting extracted from ashc's error.c into a shared, source-agnostic module. Wired into ashvm's compiler.c for all parse/compile-time errors (previously raw one-line fprintf).
130+
131+## 2026-07-18 11:58
132+ashvm: runtime errors now get the same Rust-style pretty diagnostics as compile-time errors. Chunk gained parallel pos/poslen arrays (chunk_write_pos), compiler.c's opcode emission funnels through a new emit_op() that records previous token's source span, DISPATCH() captures the executing instruction's position each cycle so vm_runtime_error() can point at it.
133+
134+## 2026-07-18 12:00
135+Delete ashc entirely (src/interpreter, src/value, src/env, src/builtins, src/main.c) now that ashvm has full feature parity (real floats, Rust-style diagnostics on both compile-time and runtime errors). Makefile builds only ashvm. README.md and rundown.md rewritten for the single-engine reality.
136+
137+## 2026-07-18 12:04
138+File-structure pass (200-line rule): split vm.c into vm_state.c/vm_errors.c/vm_dispatch.c/vm_api.c (matching rundown.md's own prescribed split — dispatch loop, error handling, public API as separate concerns), and builtins.c into builtins_core/collection/string/file.c behind a thin id-range dispatcher, mirroring ashc's old parser_state.c pattern for shared internal state.
139+
140+## 2026-07-18 12:09
141+File-structure pass continued: split vm/compiler.c (654 lines) into compiler_state.c (shared parser/emitter state, mirroring ashc's old parser_state.c pattern), compiler_expr.c (precedence chain), compiler_calls.c (call/lambda/condition-fusion emission), compiler_stmt.c (statement dispatch + simple statements), compiler_stmt_control.c (fn/if/try/while). Every ashvm file is now under 200 lines except vm_dispatch.c (295, documented exception -- computed-goto requires all opcode labels in one function scope).
142+
143+## 2026-07-18 12:11
144+Final docs pass: rundown.md's Architecture and Project Layout sections rewritten to match the actual post-split file names (compiler_state/expr/calls/stmt/stmt_control.c, vm_state/errors/dispatch/api.c). Checked off goals.md item 1 (Interpreters) now that ashvm has full ashc feature parity plus the requested file-structure cleanup.
145+
146+## 2026-07-18 12:18
147+Reorg: monorepo layout -- ashvm/ now holds the entire engine self-contained (src/, tests/, benchmarks/, Makefile), with src/ split into real subfolders by concern (lexer/, value/, vm/, compiler/, builtins/, diagnostics/) instead of flat-prefixed files in one vm/ dir. Added empty kiln/ placeholder for goal 2. Root keeps only project-wide files (README, CHANGELOG, goals/rundown, changes.sh). Fixed .gitignore's bare 'ashvm'/'ashc' patterns that were shadowing the new folder.
148+
149+## 2026-07-18 12:21
150+Build output moved to a shared root bin/ (gitignored): ashvm/Makefile now builds ../bin/ashvm instead of dropping the binary inside ashvm/. Reserves the same bin/kiln spot for goal 2 once it exists. Updated .gitignore, README, rundown.md accordingly.
151+
152+## 2026-07-18 12:37
153+Kiln milestone 1: kiln compiles print <expr>; programs (int literals, + - * /, unary -, parens, multiple statements) to a standalone freestanding Linux x86-64 ELF executable -- hand-encoded machine code, hand-written ELF writer, zero external tools (no gcc/clang/ld anywhere in the pipeline). Verified: correct operator precedence, parens, negative numbers, integer division, multi-statement programs (single exit at end), clean parse errors on malformed input. Deliberately deferred: multi-object linking, the shared-runtime-library dedup linker feature, Windows/macOS output, and every language feature beyond numeric print.
154+
155+## 2026-07-18 12:47
156+Kiln milestone 2: variables (let/assignment), comparisons (== != < <= > >=), short-circuit && ||, if/else, while -- still integers only. Frame-pointer variable model (mov rbp,rsp; sub rsp,512) matching real compiler convention, name->slot table mirroring ashvm's resolve_local/declare_local. Verified: precedence-correct arithmetic still works, variables read/write correctly, if/else branches, while loops, all 6 comparisons, && and || with GENUINE short-circuit (div-by-zero on the untaken side doesn't crash), malformed input still fails cleanly, zero regression on M1. Split codegen/emit.c (211 lines) into emit.c (register arithmetic) + emit_mem.c (memory/stack/jump ops) to stay under the 200-line rule as the instruction set grows.
157+
158+## 2026-07-18 17:46
159+Kiln: pretty Rust-style error diagnostics, same module ashvm uses (src/diagnostics/, copied in as kiln's own copy per the established per-component pattern). Wired into parser.c's parse_error() and main.c (diagnostics_set_source before compile_program). Verified: kiln's parse errors now render identically to ashvm's (file:line:col, source snippet, caret, contextual hint), zero regression on M1/M2 tests.
160+
161+## 2026-07-18 17:57
162+Kiln milestone 3: functions -- fn declarations, calls, return, recursion. Own calling convention (no external ABI to match): jump-over-body at declaration site, push-rbp/mov-rbp,rsp/sub-rsp prologue per call establishing a fresh frame, caller pushes args left-to-right and cleans them up after call, callee copies args into its own local slots at entry so param/let access share one addressing formula, return value in RAX. Functions can only call ones declared earlier in the source (same single-pass limitation ashvm's compiler has). Verified: simple calls, factorial and fibonacci recursion (fib(10)=55, real nested self-calls), no-arg functions, function calls composing inside larger expressions, undefined-function and wrong-arity errors still pretty-printed and clean, zero regression on M1/M2. Split parser.c/expr.c (both crept over 200 lines) into parser.c+parser_control.c and expr.c+expr_bool.c.
163+
164+## 2026-07-18 18:09
165+Kiln milestone 4: real double numbers replacing integer-only, throughout -- literals, arithmetic, comparisons, variables, function params/returns, and printing, matching ashvm's VMValue.number (always double). New src/codegen/emit_sse.c/.h: SSE2 scalar-double primitives (movsd/addsd/subsd/mulsd/divsd/ucomisd/cvttsd2si/cvtsi2sd/pxor), plus hand-built push_xmm/pop_xmm since x86-64 has no hardware PUSH/POP for XMM registers -- keeps the exact same stack-machine codegen model expr.c already used, just on XMM instead of GP registers. Float comparisons use ucomisd + unsigned-style condition codes (COND_B/BE/A/AE), distinct from the signed integer codes -- a real, easy-to-get-wrong distinction, called out explicitly in emit_sse.h. Print routine rewritten: sign handling, integer part via cvttsd2si, up to 6 fractional digits with trailing-zero trimming (matching ashvm's whole-vs-fractional dual format), plus a round-half-up epsilon before digit extraction -- caught and fixed a real bug where repeated float arithmetic (0.1+0.1+0.1) printed as 0.799999 instead of 0.8 due to truncation instead of rounding. Verified: zero regression on the entire M1-M3 suite (byte-identical output for all whole-number results), plus new fractional arithmetic, real division (10/4=2.5, no longer truncated), negative doubles, float comparisons, and recursive functions computing with doubles, all correct.
166+
167+## 2026-07-18 18:29
168+Kiln milestone 5: strings, via a real tagged runtime value. Every ash value is now a 16-byte (tag, payload) pair (codegen/value.h) instead of a bare double -- moved generically via GP registers (bit-copying doesn't care what the bits mean), only touching XMM transiently for actual float arithmetic. Variable slots, function params, and returns all widened to carry both words. New src/codegen/heap.c: a brk-based bump allocator (one syscall arg, sidesteps mmap's need for R10/R8/R9 which this project's encoders have never supported) grabbing a 16MB arena at startup, never freeing (matches ashvm's own lack of GC). New src/codegen/strings.c: string literals embed directly in the code buffer as length-prefixed bytes at a fixed absolute address (jumped over so they're never executed as instructions) -- no heap needed; concatenation is the one thing that does need the heap, since its result size is runtime-only. elf_writer.c reserves a small fixed-address 'globals' region (currently just the heap pointer) between the program headers and the code, and the PT_LOAD segment is now RWX instead of R-X to allow writing to it (a known simplification -- a real compiler would split .text/.data into separate segments). Plus and equal/not-equal are tag-aware (runtime branch: both-string vs numeric path); minus, times, divide, and the ordering comparisons stay NUMBER-only, matching ashvm's own type-strictness (its truthy()/arithmetic also require numbers) -- full runtime type errors are a future milestone, not silently faked here. Caught and fixed a real bug during verification: fn_statement's parameter-copy loop had tag/payload offsets swapped within each argument's 16-byte block (everywhere else this project pops in LIFO order, which gets this right automatically; here the callee reads the caller's still-live stack directly, and the offset derivation was wrong on the first pass). Verified: zero regression on the entire M1-M4 numeric suite, plus string literals, concatenation (including repeated/recursive, exercising the allocator across many calls), equality/inequality, escape sequences, functions taking and returning strings, and mixed number/string programs, all correct.
169+
170+## 2026-07-18 18:47
171+Kiln milestone 6: arrays. Literals [e0,e1,...], indexing read+write (bounds-checked), push/len builtins. Elements are (tag,payload) pairs like everything else, so arrays hold any mix of numbers and strings.
172+
173+Real design fix caught during verification: arrays initially used one flat heap block (capacity+count+elements together). Growing an array (push past capacity) needs to allocate a new bigger block, which moves the whole thing -- but the caller's variable still held the OLD address, so push(a,x) as a bare statement silently left `a` pointing at stale memory. Fixed with an indirection level matching ashvm's own VMArray design: a small stable "array object" (capacity, count, data_ptr) whose own address never moves, plus a separate resizable elements block that data_ptr can be repointed to on growth. Verified with 10 sequential pushes crossing multiple growth boundaries.
174+
175+Also added: bare call statements (push(a,2); with no let/print wrapper) -- a real pre-existing gap since M3 only supported calls inside expressions. New codegen/errors.c: a reusable runtime-die routine (jumped to, never called -- nothing to return to) for bounds violations, upgradeable to a real catchable error once M9's try/catch exists. Extracted bytes_emit_copy (shared by string concat and array growth) into its own codegen/bytes.c, and codegen_call into expr_call.c, keeping every file under/at the 200-line target.
176+
177+Verified: zero regression on the entire M1-M5 suite, plus array literals, indexed read/write, string arrays, push growth (including multi-growth), len, and clean out-of-bounds errors.
178+
179+## 2026-07-18 18:59
180+Kiln milestone 7: maps. Literals {"k":v,...}, indexed read+write (update-in-place or append), has(), delete(). Keys must be string literals in the source (matching ashvm's own map-literal restriction).
181+
182+Same stable-object-plus-resizable-data shape as arrays (M6's design fix applies here too -- map growth can't move the address every reference already holds). Lookup is a documented simplification vs ashvm: a linear scan comparing keys via the existing string-compare routine (O(n) instead of a real hash table's O(1)) -- same visible behavior, just slower, a reasonable trade for implementation reach at this stage.
183+
184+Real design point: arrays and maps both use `x[i]` syntax, and which one `x` actually is can only be known at runtime -- postfix()'s read path and parser.c's index-assignment now both pop just far enough to check the base value's tag, push everything back in the same order, and dispatch to the array or map codegen accordingly.
185+
186+delete() removes an entry by swapping the last entry into the removed slot and decrementing count (order doesn't matter for a map) -- much simpler than shifting subsequent entries down.
187+
188+Scope trim: keys()/values() (each needs to build a new array sized only at runtime -- real extra complexity for comparatively rare use) deferred to M10 alongside ashvm's other remaining builtins, same reasoning as deferring map/filter/reduce out of M6.
189+
190+Verified: zero regression on the entire M1-M6 suite, plus map literals, read, write (both update-existing and append-new, crossing multiple growth boundaries with 10 distinct keys), has/delete, and a clean "key not found" error.
191+
192+
193+## 2026-07-18 (M8)
194+Kiln milestone 8: closures / first-class functions. Lambda literals (fn(params){body} as an expression), named functions used as bare values, and calls through a variable all flow through one uniform TAG_FUNCTION runtime shape -- a small, never-resized heap object: [0]=absolute code address, [8]=capture_count, then each capture's (tag,payload). A named function referenced as a value is just a zero-capture closure; codegen_indirect_call never special-cases zero captures, it runs a genuine 0-iteration runtime loop.
195+
196+Two real design problems solved before writing any code (see codegen/closures.c and lambda.c headers for the full reasoning): (1) a lambda's code address isn't known until after its body compiles, but the closure-creation code that embeds it runs first -- solved with a new "patchable imm64" primitive (emit_mov_reg_imm64_patchable / emit_patch_imm64), generalizing the project's existing emit-now-patch-later jump pattern to an absolute-address immediate. (2) capture_count is only known at runtime (read out of the closure object) and must survive the indirect `call` instruction, but every GP register is freely clobbered by the callee -- solved with a new fixed RBP-relative scratch slot (KILN_FRAME_RESERVE, one slot beyond the normal MAX_KILN_VARS table) rather than the CPU stack, since pushing extra data there would have shifted every existing parameter-offset formula the callee's prologue relies on.
197+
198+Captures are snapshotted BY VALUE at closure-creation time (documented scope limit, same spirit as M7's linear-scan map note) -- every currently-visible outer variable is captured automatically, no free-variable analysis needed given the single-pass architecture. This supports partial application / currying and higher-order functions (a function returning a closure, a closure passed into another function) but not a closure that mutates a captured variable and expects that mutation visible on the next call (the classic "counter" idiom) -- that needs real upvalues (boxed variables), which conflicts with kiln's single-pass fixed-stack-slot variable model and is out of scope here.
199+
200+New src/codegen/closures.c (object creation, named-function values, indirect calls) and lambda.c (lambda literal parsing/codegen), split to stay under the 200-line guideline. codegen_call in expr_call.c gained one new branch (variable resolves before function-name resolves) ahead of its existing direct-call path, which is otherwise untouched.
201+
202+Verified: zero regression on the entire M1-M7 suite, plus a lambda assigned to a variable and called, a named function passed as a value and called indirectly, make_adder-style closures capturing a parameter (add5/add10 independently correct), a closure capturing two outer variables (make_scaler), and a closure passed into a higher-order function (apply_twice(square, 3) = 81).
203+
204+## 2026-07-18 (M9)
205+Kiln milestone 9: try/catch. A hand-rolled setjmp/longjmp equivalent, since this is a freestanding binary with no libc to call into -- a fixed-depth (MAX_KILN_TRY_DEPTH=16) handler stack living in the globals region (elf/elf_writer.h), each entry {saved_rsp, saved_rbp, catch target address, error-variable rbp-offset}. Every existing runtime error (array bounds, missing map key) now routes through one reusable "raise" routine (codegen/errors.c) instead of unconditionally dying: if a try is active it pops the innermost handler, restores RSP/RBP to their try-entry values (discarding however deep the call stack had gotten, including through function calls -- verified with an error raised inside a called function, unwinding cleanly back to the try), binds the message as a real TAG_STRING value at the handler's recorded slot, and jumps to the catch block; otherwise it falls through to the original write-to-stderr-and-exit(1) path, unchanged.
206+
207+Two forward-reference problems, both reusing machinery already built for M8's closures rather than inventing new patterns: the catch block's absolute code address isn't known until after the try body compiles, so it's written via the same patchable-imm64 primitive lambdas use; and the catch variable's slot has to be reserved (for its rbp-relative offset, embedded into the handler struct at try-entry) before its real name is even parsed (`catch (e)` comes after the try body) -- solved with a new small vars.c primitive, rename_var, that reserves a placeholder slot up front and retroactively overwrites its name once `(e)` is seen.
208+
209+Real bug caught during verification: a caught error's message was initially embedded as raw bytes for a direct syscall write, not as a proper length-prefixed string -- printing a caught error value directly (`catch (e) { print e; }`) read the 8 bytes before the message as its "length" and dumped raw code-segment bytes. Fixed by giving error messages the exact same length-prefixed layout codegen/strings.c's literals use, so a caught error is a fully legitimate string usable with print/concat/equality like any other.
210+
211+New: a bounds check on the handler-stack push itself (exceeding MAX_KILN_TRY_DEPTH is treated as an unconditional fatal error, never catchable -- silently overflowing past the fixed table would corrupt the adjacent, writable code segment). Verified with a recursive function pushing a try 20 levels deep, dying cleanly instead of corrupting memory.
212+
213+Scope trim: no explicit `throw` statement -- this milestone catches kiln's own existing runtime errors (array/map), not user-raised ones; deferred alongside the other scope trims already on record (M6's map/filter/reduce, M7's keys/values).
214+
215+Verified: zero regression on the entire M1-M8 suite, plus catching an array-bounds error and a missing-map-key error (with normal execution continuing after each), a try whose body raises nothing (catch correctly skipped), nested try/catch (inner catches its own error, outer's handler correctly left untouched), an error raised deep inside a called function unwinding back to the try, and the uncaught (no try present) path still dying with the same message and exit status as before this milestone.
216+
217+## 2026-07-18 (M10)
218+Kiln milestone 10: the remaining 23 ashvm builtins -- sqrt/abs/floor, str/num conversion, map/filter/reduce, keys/values, split/join/substring/indexOf/replace/upper/lower/trim, read_file/write_file/append_file/file_exists, input(). Every one hand-encoded, zero libc: math via new SQRTSD/ANDPD SSE primitives, file I/O via raw open/read/write/close/lseek syscalls, num()/str() via hand-written digit parsing and formatting (no strtod/snprintf).
219+
220+Two real design points carried the rest: (1) map/filter/reduce needed to call an arbitrary closure value from INSIDE codegen, not from parsed `f(...)` syntax -- closures.c's codegen_indirect_call was split into a reusable tail, codegen_call_closure_value(argc), that assumes the closure and its arguments are already pushed. (2) most of these builtins need more live state across a loop (and across nested calls, which clobber every GP register) than the six general-purpose registers can hold at once -- extended the RBP-relative scratch-slot technique from M8/M9's call_scratch_offset into a full 128-byte higher_order_scratch_offset() region, reused with different field layouts by whichever builtin is currently running. Proven safe by the same reasoning as call_scratch_offset: source-level arguments evaluate left-to-right, so nested calls like map(map(a,f),g) never have two builtins "live" in the same scratch region at once, and a callback that itself calls another higher-order builtin runs in its OWN call frame (own rbp), landing at a completely different address.
221+
222+Two real bugs caught during verification, both in newly-added machinery: editing vars.h to grow the frame-reserve constant didn't trigger a rebuild of files that only include it (not modify it) -- the Makefile had no header-dependency tracking, so a stale binary mixed old and new frame-size assumptions across functions and segfaulted. Fixed properly: -MMD -MP dependency generation, verified by touching a header and confirming the right files rebuild without a full clean. Second bug: input()'s byte-read loop used RSP directly as a byte-load base register, which this project's minimal instruction encoder explicitly doesn't support (needs a SIB byte it doesn't emit) -- same class of bug flagged in the existing emit.h comments for exactly this reason; fixed by copying RSP into RCX first, matching the pattern print_int.c already established.
223+
224+Verified: zero regression on the entire M1-M9 suite, plus dedicated tests for every new builtin including edge cases (empty array/map/string inputs, out-of-range substring/indexOf, empty-delimiter split, empty-search replace, nested map(map(...)) calls, file round-trips through write/append/read, and piped stdin for input()).
225+
226+## 2026-07-18 (M11)
227+Kiln milestone 11: `for (x in array) { block }` loops. The array expression evaluates once up front; the loop variable is bound to each element via the same var-slot mechanism `let` already uses (an ordinary, reassignable variable inside the body -- reassigning it doesn't affect iteration, matching a plain for-each's usual semantics).
228+
229+Real design point: the loop's own state (data_ptr, count, running index) needs to survive whatever the body's own codegen does -- including calls, and including ANOTHER for loop nested inside it. Unlike M9's try/catch (genuinely runtime-recursive, needing a runtime-tracked handler stack) or M10's higher-order builtins (never concurrently active within one frame, since source arguments evaluate left-to-right), `for` loop NESTING is fully visible at COMPILE TIME -- nested `for` statements are literally nested calls in this single-pass compiler. So each nesting level just gets its own fixed RBP-relative offset (vars.h's for_level_offset(depth)) computed from a plain compile-time counter, bounded the same way every other table here is (MAX_KILN_FOR_DEPTH=8), rather than another runtime stack. The counter resets to 0 when entering a function or lambda body (mirroring vars_save/vars_clear/vars_restore) since that body gets its own RBP at runtime and can safely reuse the same offsets independently of the enclosing code's own nesting depth.
230+
231+Verified: zero regression on the entire M1-M10 suite, plus a basic loop, nested for-in-for (i*j over two arrays), a loop body calling a higher-order builtin (map) without corrupting the outer loop's own iteration state, an empty array (body never runs), a for loop inside a recursive-capable function, and loop-variable reassignment inside the body not affecting which elements get visited.
232+
233+This closes out the full M1-M11 roadmap from the original plan: Kiln now has variables, control flow, functions/recursion, real doubles, strings, arrays, maps, closures, try/catch, the complete ashvm builtin set, and for-in loops -- all hand-written x86-64 codegen and a hand-written ELF writer, zero external tools anywhere in the pipeline.
234+
235+## 2026-07-19 (missing.md pass: correctness fix + language core + argv)
236+Addressed missing.md's #1 (correctness bugs), the tractable subset of #2 (language core), and #4's argv() -- deliberately deferring structs/classes, modules, multi-return/destructuring, spread/varargs, and nested-function scoping, each a standalone design effort rather than a bolt-on (see missing.md for the updated status).
237+
238+**Correctness fix**: `arr[i]`'s read dispatch only branched on TAG_MAP vs "else assume array" -- `"hello"[0]` silently read a string's own content bytes as if they were an array-object struct instead of erroring. Added a real TAG_STRING branch (codegen/strings.c's codegen_string_index_read, bounds-checked, returns a 1-char string) and made index-assignment into a string a clean runtime error (strings are immutable). Found and fixed a second, unrelated pre-existing bug while testing this: two SEQUENTIAL (not nested) try/catch blocks reusing the same catch variable name created two table entries, and resolve_var's first-match-wins scan bound the second catch body to the first catch's stale slot -- fixed by scanning newest-first (standard shadowing semantics), parser/vars.c.
239+
240+**Language core additions**: break/continue (a compile-time loop-context stack, parser/loop_stack.c, since continue's target -- the per-iteration advance step -- isn't emitted until after the loop body compiles, so it's forward-patched like break rather than backward-jumped); true/false/nil literals (true/false are sugar for NUMBER 1.0/0.0, not a new type; nil is a real new TAG_NIL whose payload is always exactly 0, which lets it ride the EXISTING truthy-test and numeric-comparison codegen for free); ternary a?b:c; bitwise operators (& | ^ << >> ~, one flat precedence level, new AND/OR/XOR/SHL/SAR/NOT emit primitives); compound assignment (+= -= *= /=, `+=` reusing the exact same tag-aware add codegen `+` uses, extracted into codegen_apply_plus) and ++/--; exponent `**` (integer exponents only, via exponentiation-by-squaring -- a genuine runtime loop since the exponent is only known at runtime, unlike every other operator here; no x87 FYL2X/F2XM1 in this SSE2-only project, so fractional exponents are out of scope); hex/scientific-notation/underscore numeric literals (the lexer only had to stop truncating the token early -- strtod(), real libc, already runs at COMPILE TIME on the host in expr.c's primary() and natively parses "0x1A"/"1e10"; underscores are the one thing it doesn't understand, stripped into a clean buffer first); block comments; throw (reuses codegen/errors.c's raise routine with a new dynamic-message variant, errors_emit_die_dynamic, for a runtime string instead of a compile-time-constant one); and kiln's missing unary `!` (ashvm had TOKEN_BANG -> OP_NOT all along, kiln never wired it up).
241+
242+**argv()**: the kernel hands a freshly-started process RSP pointing at [argc, argv[0], ..., NULL, envp...] -- the ONLY point where that's still available is the very first instruction, before anything (including the frame prologue) touches RSP. Saved to a new fixed globals address (KILN_ARGV_ADDR) as compile_program's literal first instruction, reachable from any frame later (a called function's own RBP is no longer the original one). codegen/argv_builtin.c walks the raw C-string argv array (kernel-provided, NOT length-prefixed -- needs its own strlen scan) building a real kiln array of length-prefixed strings.
243+
244+Verified: zero regression on the entire M1-M11 suite at every step (18 test files), plus dedicated new tests for every item above, including edge cases (nil vs 0 vs false tag-distinctness, chained/nested ternary, negative and chained exponents, sequential non-nested try/catch with the same variable name, throw unwinding out of a function call, argv() with and without extra arguments).
245+
246+## 2026-07-20 (block scoping)
247+Closed missing.md's block-scoping gap: kiln's variable table was one flat, append-only, per-*function* name->slot array -- `if`/`while`/`for`/`try`/`catch` bodies called the shared `block()` helper with no scope bracketing of their own, so a `let` declared inside stayed resolvable for the rest of the enclosing function forever, and re-`let`-ing a name that already existed anywhere in the function REUSED that slot (mutating it) rather than creating an independent inner binding. `for_loop.c`'s loop variable and `try_catch.c`'s catch variable had the identical pattern, declared outside their own `block()` call.
248+
249+Fix reuses a pattern already in the codebase for exactly this shape of problem (compile-time nesting state, saved/restored across recursive descent -- the same technique as `for_depth_save/restore` and `loop_depth_save/restore`): a new `scope_start` mark in `parser/vars.c`, paired `vars_scope_begin()`/`vars_scope_end()` bracketing `block()` itself (covers if/while/try-body/catch-body/fn-body/lambda-body in one choke point) plus `for_statement()`/`try_statement()` themselves (their loop/catch variable lives outside their nested `block()` call). A new `resolve_var_in_current_scope()` (scans only slots >= `scope_start`) replaces the full-scan `resolve_var()` in `let_statement`'s and `for_statement`'s reuse-or-declare check specifically, so a name only visible from an enclosing scope shadows instead of mutating; every plain read/`=`/compound-assignment/index-assignment site keeps using the unchanged full-scan `resolve_var()`, since those must still reach into enclosing scopes.
250+
251+Checked against `closures.c`/`lambda.c` before writing any code: scope enter/exit is compile-time-only (zero runtime instructions, just static slot-index bookkeeping), and lambda capture copies each captured variable's value into the heap closure object at the moment the lambda literal is evaluated -- always before the enclosing block's `}` is reached, so slot reuse afterward can't corrupt an already-captured closure. Confirmed with three dedicated tests rather than just assuming it: a closure created and returned from inside an `if` block whose capture slot is later reused, a closure capturing a variable that shadows an outer same-named one (closure sees the inner value, outer stays untouched), and closures created per-iteration inside a `while` body each keeping their own snapshot.
252+
253+One existing test depended on the very leak being fixed: `for_loops.ash`'s trailing `print x;` after a `for (x in nums) {...}` loop relied on the loop variable leaking (previously printed `1030`, the last iteration's mutated value); removed, since the reassignment-doesn't-affect-iteration behavior it was piggybacking on is already covered by the three prints inside that same loop body.
254+
255+New tests: `block_scoping.ash` (shadow-vs-mutate, plus 20 sibling if-blocks x 4 unique variables each to prove slot reuse rather than leaking into `MAX_KILN_VARS`'s 64-slot ceiling), `block_scoping_closures.ash` (the three closure-interaction cases above), `for_loop_scoping.ash` (loop variable no longer leaks or mutates an outer same-named variable; nested loops with the same variable name at each level stay independent), `try_catch_scoping.ash` (catch variable scoped to its own statement; a second, previously-unentered `catch (e)` no longer shadows the first via a garbage, never-written slot).
256+
257+Verified: zero regression on the entire M1-M11 + missing.md-pass suite (every file's output byte-identical before/after, via a new `kiln/tests/run_regression.sh` baseline/diff harness) except `for_loops.ash`'s intentional, documented change above, plus the four new test files and negative compile-error checks (referencing a block/loop/catch-scoped variable after its block now fails cleanly with `undefined variable: <name>` instead of silently resolving to a leaked or mutated value).
258+
259+Still open per missing.md: modules/imports, structs/classes, switch/match, multi-return/destructuring, spread/varargs/default params, nested/private functions.

README.mdadded

+107−0
@@ -0,0 +1,107 @@
1+<div align="center">
2+
3+# Ash
4+
5+<img src="logo.png" width="180" alt="Ash logo" />
6+
7+</div>
8+
9+---
10+
11+Ash is a small programming language built from scratch in C - lexer,
12+compiler, and a bytecode VM, all hand-written with no external dependencies.
13+It has closures, hash maps, first-class functions, real floating-point
14+numbers, `try`/`catch`, and a colorized compiler that points straight at
15+your mistakes.
16+
17+```ash
18+fn make_adder(x) {
19+ return fn(y) { return x + y; };
20+}
21+
22+let add5 = make_adder(5);
23+print add5(3); // 8
24+
25+let nums = [1, 2, 3, 4, 5];
26+let evens = filter(nums, fn(n) { return n % 2 == 0; });
27+print evens; // [2, 4]
28+
29+try {
30+ let m = {"name": "Ash"};
31+ print m["missing"];
32+} catch (e) {
33+ print "caught: " + e;
34+}
35+```
36+
37+## Engine
38+
39+Ash compiles to real bytecode and runs it on a stack-based VM (`ashvm`) with
40+computed-goto instruction dispatch and a handful of hand-fused
41+"superinstructions" for common patterns like `x = x + y` and `while (i < n)`
42+that collapse several bytecode ops into one. Ash used to ship a second,
43+slower tree-walk interpreter (`ashc`) as the place new features were
44+prototyped first - it has since been retired now that the VM has full
45+feature parity, including real `double` number semantics and Rust-style
46+runtime diagnostics.
47+
48+## Performance
49+
50+Benchmarked against equivalent programs in C++ (`g++ -O2`) on the same machine:
51+
52+| Benchmark | C++ | `ashvm` |
53+|---|---|---|
54+| `fib(30)` (recursive) | 0.005s | 0.06s (~12x slower) |
55+| Sum of 0..20,000,000 | 0.004s | 0.12s (~30x slower) |
56+
57+That speed comes from three things layered on top of each other: compiling
58+to real bytecode instead of re-parsing source on every loop iteration,
59+computed-goto instruction dispatch instead of a `switch` statement, and
60+those hand-fused superinstructions. A hand-written x86-64 JIT experiment
61+(`benchmarks/native_jit.c`) that skips bytecode entirely gets within ~2x of
62+raw C++, which is roughly the ceiling for anything short of a full
63+optimizing native compiler.
64+
65+## Language features
66+
67+- Numbers (real doubles), strings (with escape sequences), arrays, hash maps
68+- Functions, recursion, closures with by-value capture
69+- First-class functions - pass them around, store them in variables, call them indirectly
70+- `map`, `filter`, `reduce`, and 20+ other built-ins (string ops, file I/O, math)
71+- `try` / `catch` error handling
72+- `for (x in array)` and `while` loops
73+- Colorized, Rust-style diagnostics for both compile-time and runtime
74+ errors: file:line:column, a source snippet, a caret, and a contextual hint
75+- An interactive REPL with persistent variables across lines
76+
77+## Getting started
78+
79+```bash
80+cd ashvm && make # builds ../bin/ashvm
81+
82+../bin/ashvm script.ash # run a script
83+../bin/ashvm # start the REPL
84+```
85+
86+## Project structure
87+ashvm/ the engine - everything below is self-contained inside here
88+src/
89+lexer/ tokenizer
90+value/ VMValue tagged type, hash map
91+vm/ bytecode Chunk format, dispatch loop, error handling, public API
92+compiler/ single-pass compiler: parser/emitter state, expression chain,
93+call/lambda emission, statement compilation
94+builtins/ len, map, filter, reduce, string/file ops
95+diagnostics/ shared Rust-style error reporting (compile-time and runtime)
96+main.c entry point (file mode + REPL)
97+benchmarks/ .ash and .cpp benchmark programs, plus the x86-64 JIT experiment
98+tests/ example programs covering each language feature
99+kiln/ a standalone .ash compiler (Windows + Linux). Not started yet.
100+
101+## Editor support
102+
103+A VS Code extension with syntax highlighting will come soon.
104+We could build an icon theme too, but we'd rather not
105+it's too much work to maintain, so we'll leave that to other people.
106+In the future, though, we plan to release a kind of IDE/code editor built with Ash,
107+for building things in Ash.

ashvm/Makefileadded

+25−0
@@ -0,0 +1,25 @@
1+CC = gcc
2+CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc
3+LDFLAGS = -lm
4+
5+ASHVM_SRC = src/main.c src/lexer/lexer.c src/lexer/lexer_import.c src/value/value.c src/value/hashmap.c src/vm/chunk.c \
6+ src/compiler/state.c src/compiler/expr.c src/compiler/calls.c src/compiler/stmt.c src/compiler/stmt_control.c \
7+ src/compiler/import_paths.c src/compiler/stmt_import.c src/compiler/constants.c \
8+ src/builtins/builtins.c src/builtins/core.c src/builtins/collection.c src/builtins/string.c src/builtins/file.c \
9+ src/vm/state.c src/vm/errors.c src/vm/dispatch.c src/vm/api.c \
10+ src/diagnostics/diagnostics.c
11+ASHVM_OBJ = $(ASHVM_SRC:.c=.o)
12+
13+BIN = ../bin/ashvm
14+
15+all: $(BIN)
16+
17+$(BIN): $(ASHVM_OBJ)
18+ mkdir -p ../bin
19+ $(CC) $(CFLAGS) -o $(BIN) $(ASHVM_OBJ) $(LDFLAGS)
20+
21+clean:
22+ find src -name '*.o' -delete
23+ rm -f $(BIN)
24+
25+.PHONY: all clean

ashvm/benchmarks/countdown.ashadded

+7−0
@@ -0,0 +1,7 @@
1+let i = 20000000;
2+let count = 0;
3+while (i > 0) {
4+ let count = count + 1;
5+ let i = i - 1;
6+}
7+print count;

ashvm/benchmarks/countdown.cppadded

+11−0
@@ -0,0 +1,11 @@
1+#include <cstdio>
2+int main() {
3+ long long i = 20000000;
4+ long long count = 0;
5+ while (i > 0) {
6+ count += 1;
7+ i -= 1;
8+ }
9+ printf("%lld\n", count);
10+ return 0;
11+}

ashvm/benchmarks/fib.ashadded

+7−0
@@ -0,0 +1,7 @@
1+fn fib(n) {
2+ if (n < 2) {
3+ return n;
4+ }
5+ return fib(n - 1) + fib(n - 2);
6+}
7+print fib(30);

ashvm/benchmarks/fib.cppadded

+9−0
@@ -0,0 +1,9 @@
1+#include <cstdio>
2+long long fib(long long n) {
3+ if (n < 2) return n;
4+ return fib(n - 1) + fib(n - 2);
5+}
6+int main() {
7+ printf("%lld\n", fib(30));
8+ return 0;
9+}

ashvm/benchmarks/fib20.ashadded

+1−0
@@ -0,0 +1 @@
1+fn fib(n) { if (n < 2) { return n; } return fib(n-1)+fib(n-2); } print fib(20);

ashvm/benchmarks/hashmap.ashadded

+13−0
@@ -0,0 +1,13 @@
1+let m = {};
2+let i = 0;
3+while (i < 50000) {
4+ m[str(i)] = i * 2;
5+ let i = i + 1;
6+}
7+let j = 0;
8+let total = 0;
9+while (j < 50000) {
10+ let total = total + m[str(j)];
11+ let j = j + 1;
12+}
13+print total;

ashvm/benchmarks/hashmap.cppadded

+15−0
@@ -0,0 +1,15 @@
1+#include <cstdio>
2+#include <unordered_map>
3+#include <string>
4+int main() {
5+ std::unordered_map<std::string, long long> m;
6+ for (int i = 0; i < 50000; i++) {
7+ m[std::to_string(i)] = (long long)i * 2;
8+ }
9+ long long total = 0;
10+ for (int j = 0; j < 50000; j++) {
11+ total += m[std::to_string(j)];
12+ }
13+ printf("%lld\n", total);
14+ return 0;
15+}

ashvm/benchmarks/loop.ashadded

+7−0
@@ -0,0 +1,7 @@
1+let sum = 0;
2+let i = 0;
3+while (i < 20000000) {
4+ let sum = sum + i;
5+ let i = i + 1;
6+}
7+print sum;

ashvm/benchmarks/loop.cppadded

+10−0
@@ -0,0 +1,10 @@
1+#include <cstdio>
2+int main() {
3+ long long sum = 0;
4+ long long N = 20000000;
5+ for (long long i = 0; i < N; i++) {
6+ sum += i;
7+ }
8+ printf("%lld\n", sum);
9+ return 0;
10+}

ashvm/benchmarks/loop_varbound.ashadded

+8−0
@@ -0,0 +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;
7+}
8+print sum;

ashvm/benchmarks/native_jit.cadded

+72−0
@@ -0,0 +1,72 @@
1+// Hand-written x86-64 JIT for one fixed pattern:
2+// sum = 0; i = 0; while (i < N) { sum += i; i++; }
3+// No VM, no bytecode dispatch, no interpreter — this mmaps a page,
4+// writes real machine code bytes into it, marks it executable,
5+// and calls it directly as a C function pointer.
6+
7+#include <stdio.h>
8+#include <stdint.h>
9+#include <sys/mman.h>
10+#include <string.h>
11+
12+typedef long long (*fn_t)(void);
13+
14+static void emit8(uint8_t *buf, int *pos, uint8_t b) {
15+ buf[(*pos)++] = b;
16+}
17+static void emit32(uint8_t *buf, int *pos, int32_t v) {
18+ memcpy(buf + *pos, &v, 4);
19+ *pos += 4;
20+}
21+
22+int main(void) {
23+ long long N = 20000000; // 20 million iterations, same as the Ash benchmark
24+
25+ void *mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
26+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
27+ if (mem == MAP_FAILED) { perror("mmap"); return 1; }
28+ uint8_t *code = (uint8_t *)mem;
29+ int pos = 0;
30+
31+ // xor eax, eax ; sum = 0
32+ emit8(code, &pos, 0x31); emit8(code, &pos, 0xC0);
33+ // xor ecx, ecx ; i = 0
34+ emit8(code, &pos, 0x31); emit8(code, &pos, 0xC9);
35+
36+ int loop_start = pos;
37+ // cmp rcx, N (imm32)
38+ emit8(code, &pos, 0x48); emit8(code, &pos, 0x81); emit8(code, &pos, 0xF9);
39+ emit32(code, &pos, (int32_t)N);
40+
41+ // jge exit (rel32, patched below)
42+ emit8(code, &pos, 0x0F); emit8(code, &pos, 0x8D);
43+ int jge_operand = pos;
44+ emit32(code, &pos, 0); // placeholder
45+
46+ // add rax, rcx ; sum += i
47+ emit8(code, &pos, 0x48); emit8(code, &pos, 0x01); emit8(code, &pos, 0xC8);
48+ // add rcx, 1 ; i++
49+ emit8(code, &pos, 0x48); emit8(code, &pos, 0x83); emit8(code, &pos, 0xC1); emit8(code, &pos, 0x01);
50+
51+ // jmp loop_start
52+ int jmp_pos = pos;
53+ emit8(code, &pos, 0xE9);
54+ int32_t jmp_rel = loop_start - (jmp_pos + 5);
55+ emit32(code, &pos, jmp_rel);
56+
57+ int exit_pos = pos;
58+ // ret ; sum already in rax (SysV ABI return register)
59+ emit8(code, &pos, 0xC3);
60+
61+ // patch the jge target now that we know where 'exit' actually is
62+ int32_t jge_rel = exit_pos - (jge_operand + 4);
63+ memcpy(code + jge_operand, &jge_rel, 4);
64+
65+ if (mprotect(mem, 4096, PROT_READ | PROT_EXEC) != 0) { perror("mprotect"); return 1; }
66+
67+ fn_t fn = (fn_t)(void *)code;
68+ long long result = fn();
69+ printf("%lld\n", result);
70+
71+ return 0;
72+}

ashvm/src/builtins/builtins.cadded

+27−0
@@ -0,0 +1,27 @@
1+#include <string.h>
2+#include "builtins/builtins.h"
3+#include "builtins/internal.h"
4+#include "vm/vm.h"
5+
6+static const char *builtin_names[] = {
7+ "len", "push", "input", "str", "num", "sqrt", "abs", "floor",
8+ "map", "filter", "reduce", "keys", "values", "has", "delete",
9+ "split", "join", "substring", "indexOf", "replace", "upper", "lower", "trim",
10+ "read_file", "write_file", "append_file", "file_exists"
11+};
12+#define NUM_BUILTINS (int)(sizeof(builtin_names) / sizeof(builtin_names[0]))
13+
14+int vm_builtin_lookup(const char *name, int len) {
15+ for (int i = 0; i < NUM_BUILTINS; i++) {
16+ if ((int)strlen(builtin_names[i]) == len && strncmp(builtin_names[i], name, len) == 0) return i;
17+ }
18+ return -1;
19+}
20+
21+VMValue vm_call_builtin(int id, VMValue *args, int argc) {
22+ if (id >= 0 && id <= 7) return vm_call_builtin_core(id, args, argc);
23+ if (id >= 8 && id <= 14) return vm_call_builtin_collection(id, args, argc);
24+ if (id >= 15 && id <= 22) return vm_call_builtin_string(id, args, argc);
25+ if (id >= 23 && id <= 26) return vm_call_builtin_file(id, args, argc);
26+ vm_runtime_error("unknown builtin id: %d\n", id);
27+}

ashvm/src/builtins/builtins.hadded

+8−0
@@ -0,0 +1,8 @@
1+#ifndef ASH_VM_BUILTINS_H
2+#define ASH_VM_BUILTINS_H
3+#include "value/value.h"
4+
5+int vm_builtin_lookup(const char *name, int len);
6+VMValue vm_call_builtin(int id, VMValue *args, int argc);
7+
8+#endif

ashvm/src/builtins/collection.cadded

+68−0
@@ -0,0 +1,68 @@
1+#include <stdlib.h>
2+#include <string.h>
3+#include "builtins/internal.h"
4+#include "vm/vm.h"
5+#include "value/hashmap.h"
6+
7+VMValue vm_call_builtin_collection(int id, VMValue *args, int argc) {
8+ switch (id) {
9+ case 8: { // map
10+ if (argc != 2 || args[0].type != VM_ARRAY || (args[1].type != VM_FUNCTION && args[1].type != VM_CLOSURE)) { vm_runtime_error("map(array, fn) expected\n"); }
11+ VMArray *result = vm_array_new();
12+ for (int i = 0; i < args[0].array->count; i++) {
13+ VMValue call_args[1] = { args[0].array->items[i] };
14+ vm_array_push(result, vm_call_callable_sync(args[1], call_args, 1));
15+ }
16+ return vm_array_val(result);
17+ }
18+ case 9: { // filter
19+ if (argc != 2 || args[0].type != VM_ARRAY || (args[1].type != VM_FUNCTION && args[1].type != VM_CLOSURE)) { vm_runtime_error("filter(array, fn) expected\n"); }
20+ VMArray *result = vm_array_new();
21+ for (int i = 0; i < args[0].array->count; i++) {
22+ VMValue call_args[1] = { args[0].array->items[i] };
23+ VMValue keep = vm_call_callable_sync(args[1], call_args, 1);
24+ if (keep.type == VM_NUM && keep.number != 0) vm_array_push(result, args[0].array->items[i]);
25+ }
26+ return vm_array_val(result);
27+ }
28+ case 10: { // reduce
29+ if (argc != 3 || args[0].type != VM_ARRAY || (args[1].type != VM_FUNCTION && args[1].type != VM_CLOSURE)) { vm_runtime_error("reduce(array, fn, initial) expected\n"); }
30+ VMValue acc = args[2];
31+ for (int i = 0; i < args[0].array->count; i++) {
32+ VMValue call_args[2] = { acc, args[0].array->items[i] };
33+ acc = vm_call_callable_sync(args[1], call_args, 2);
34+ }
35+ return acc;
36+ }
37+ case 11: { // keys
38+ if (argc != 1 || args[0].type != VM_MAP) { vm_runtime_error("keys() expects a map\n"); }
39+ VMArray *result = vm_array_new();
40+ for (int i = 0; i < args[0].map->capacity; i++) {
41+ if (args[0].map->entries[i].used) {
42+ char *k = args[0].map->entries[i].key;
43+ char *copy = malloc(strlen(k) + 1); strcpy(copy, k);
44+ vm_array_push(result, vm_str(copy));
45+ }
46+ }
47+ return vm_array_val(result);
48+ }
49+ case 12: { // values
50+ if (argc != 1 || args[0].type != VM_MAP) { vm_runtime_error("values() expects a map\n"); }
51+ VMArray *result = vm_array_new();
52+ for (int i = 0; i < args[0].map->capacity; i++) {
53+ if (args[0].map->entries[i].used) vm_array_push(result, args[0].map->entries[i].value);
54+ }
55+ return vm_array_val(result);
56+ }
57+ case 13: { // has
58+ if (argc != 2 || args[0].type != VM_MAP || args[1].type != VM_STR) { vm_runtime_error("has(map, key) expected\n"); }
59+ return vm_num(vm_map_has(args[0].map, args[1].str) ? 1 : 0);
60+ }
61+ case 14: { // delete
62+ if (argc != 2 || args[0].type != VM_MAP || args[1].type != VM_STR) { vm_runtime_error("delete(map, key) expected\n"); }
63+ vm_map_delete(args[0].map, args[1].str);
64+ return args[0];
65+ }
66+ }
67+ vm_runtime_error("unknown builtin id: %d\n", id);
68+}

ashvm/src/builtins/core.cadded

+57−0
@@ -0,0 +1,57 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include <math.h>
5+#include "builtins/internal.h"
6+#include "vm/vm.h"
7+#include "value/hashmap.h"
8+
9+VMValue vm_call_builtin_core(int id, VMValue *args, int argc) {
10+ switch (id) {
11+ case 0: { // len
12+ if (argc != 1) { vm_runtime_error("len() expects 1 argument\n"); }
13+ if (args[0].type == VM_STR) return vm_num((int64_t)strlen(args[0].str));
14+ if (args[0].type == VM_ARRAY) return vm_num(args[0].array->count);
15+ if (args[0].type == VM_MAP) return vm_num(args[0].map->count);
16+ vm_runtime_error("len() expects a string, array, or map\n");
17+ }
18+ case 1: { // push
19+ if (argc != 2 || args[0].type != VM_ARRAY) { vm_runtime_error("push(array, value) expected\n"); }
20+ vm_array_push(args[0].array, args[1]);
21+ return args[0];
22+ }
23+ case 2: { // input
24+ char buf[1024];
25+ if (!fgets(buf, sizeof(buf), stdin)) buf[0] = '\0';
26+ size_t l = strlen(buf);
27+ if (l > 0 && buf[l - 1] == '\n') buf[l - 1] = '\0';
28+ char *copy = malloc(strlen(buf) + 1); strcpy(copy, buf);
29+ return vm_str(copy);
30+ }
31+ case 3: { // str
32+ if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("str() expects a number\n"); }
33+ char buf[64];
34+ if (args[0].number == (long long)args[0].number) snprintf(buf, sizeof(buf), "%lld", (long long)args[0].number);
35+ else snprintf(buf, sizeof(buf), "%g", args[0].number);
36+ char *copy = malloc(strlen(buf) + 1); strcpy(copy, buf);
37+ return vm_str(copy);
38+ }
39+ case 4: { // num
40+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("num() expects a string\n"); }
41+ return vm_num(strtod(args[0].str, NULL));
42+ }
43+ case 5: { // sqrt
44+ if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("sqrt() expects a number\n"); }
45+ return vm_num(sqrt(args[0].number));
46+ }
47+ case 6: { // abs
48+ if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("abs() expects a number\n"); }
49+ return vm_num(fabs(args[0].number));
50+ }
51+ case 7: { // floor
52+ if (argc != 1 || args[0].type != VM_NUM) { vm_runtime_error("floor() expects a number\n"); }
53+ return vm_num(floor(args[0].number));
54+ }
55+ }
56+ vm_runtime_error("unknown builtin id: %d\n", id);
57+}

ashvm/src/builtins/file.cadded

+39−0
@@ -0,0 +1,39 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "builtins/internal.h"
5+#include "vm/vm.h"
6+
7+VMValue vm_call_builtin_file(int id, VMValue *args, int argc) {
8+ switch (id) {
9+ case 23: { // read_file
10+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("read_file(path) expected\n"); }
11+ FILE *f = fopen(args[0].str, "rb");
12+ if (!f) { vm_runtime_error("could not open file: %s\n", args[0].str); }
13+ fseek(f, 0, SEEK_END); long size = ftell(f); rewind(f);
14+ char *buf = malloc(size + 1); size_t n = fread(buf, 1, size, f); buf[n] = '\0'; fclose(f);
15+ return vm_str(buf);
16+ }
17+ case 24: { // write_file
18+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("write_file(path, content) expected\n"); }
19+ FILE *f = fopen(args[0].str, "wb");
20+ if (!f) { vm_runtime_error("could not write file: %s\n", args[0].str); }
21+ fwrite(args[1].str, 1, strlen(args[1].str), f); fclose(f);
22+ return vm_num(1);
23+ }
24+ case 25: { // append_file
25+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("append_file(path, content) expected\n"); }
26+ FILE *f = fopen(args[0].str, "ab");
27+ if (!f) { vm_runtime_error("could not open file for append: %s\n", args[0].str); }
28+ fwrite(args[1].str, 1, strlen(args[1].str), f); fclose(f);
29+ return vm_num(1);
30+ }
31+ case 26: { // file_exists
32+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("file_exists(path) expected\n"); }
33+ FILE *f = fopen(args[0].str, "rb");
34+ if (f) { fclose(f); return vm_num(1); }
35+ return vm_num(0);
36+ }
37+ }
38+ vm_runtime_error("unknown builtin id: %d\n", id);
39+}

ashvm/src/builtins/internal.hadded

+12−0
@@ -0,0 +1,12 @@
1+#ifndef ASH_VM_BUILTINS_INTERNAL_H
2+#define ASH_VM_BUILTINS_INTERNAL_H
3+#include "value/value.h"
4+
5+// Each group handles a contiguous id range within vm_call_builtin's switch;
6+// see builtins.c for the range -> group mapping and the shared name table.
7+VMValue vm_call_builtin_core(int id, VMValue *args, int argc);
8+VMValue vm_call_builtin_collection(int id, VMValue *args, int argc);
9+VMValue vm_call_builtin_string(int id, VMValue *args, int argc);
10+VMValue vm_call_builtin_file(int id, VMValue *args, int argc);
11+
12+#endif

ashvm/src/builtins/string.cadded

+102−0
@@ -0,0 +1,102 @@
1+#include <stdlib.h>
2+#include <string.h>
3+#include <ctype.h>
4+#include "builtins/internal.h"
5+#include "vm/vm.h"
6+
7+VMValue vm_call_builtin_string(int id, VMValue *args, int argc) {
8+ switch (id) {
9+ case 15: { // split
10+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("split(str, delim) expected\n"); }
11+ const char *s = args[0].str; const char *delim = args[1].str;
12+ int dlen = (int)strlen(delim);
13+ VMArray *result = vm_array_new();
14+ if (dlen == 0) {
15+ int slen = (int)strlen(s);
16+ for (int i = 0; i < slen; i++) { char *c = malloc(2); c[0] = s[i]; c[1] = '\0'; vm_array_push(result, vm_str(c)); }
17+ return vm_array_val(result);
18+ }
19+ const char *cur = s; const char *found;
20+ while ((found = strstr(cur, delim)) != NULL) {
21+ int len = (int)(found - cur);
22+ char *piece = malloc(len + 1); memcpy(piece, cur, len); piece[len] = '\0';
23+ vm_array_push(result, vm_str(piece));
24+ cur = found + dlen;
25+ }
26+ char *piece = malloc(strlen(cur) + 1); strcpy(piece, cur);
27+ vm_array_push(result, vm_str(piece));
28+ return vm_array_val(result);
29+ }
30+ case 16: { // join
31+ if (argc != 2 || args[0].type != VM_ARRAY || args[1].type != VM_STR) { vm_runtime_error("join(array, delim) expected\n"); }
32+ int total = 0; int dlen = (int)strlen(args[1].str);
33+ for (int i = 0; i < args[0].array->count; i++) {
34+ if (args[0].array->items[i].type != VM_STR) { vm_runtime_error("join() expects an array of strings\n"); }
35+ total += (int)strlen(args[0].array->items[i].str);
36+ if (i < args[0].array->count - 1) total += dlen;
37+ }
38+ char *buf = malloc(total + 1); buf[0] = '\0';
39+ for (int i = 0; i < args[0].array->count; i++) {
40+ strcat(buf, args[0].array->items[i].str);
41+ if (i < args[0].array->count - 1) strcat(buf, args[1].str);
42+ }
43+ return vm_str(buf);
44+ }
45+ case 17: { // substring
46+ if (argc != 3 || args[0].type != VM_STR) { vm_runtime_error("substring(str, start, end) expected\n"); }
47+ int slen = (int)strlen(args[0].str);
48+ int start = (int)args[1].number; int end = (int)args[2].number;
49+ if (start < 0) start = 0;
50+ if (end > slen) end = slen;
51+ if (start > end) start = end;
52+ char *buf = malloc(end - start + 1); memcpy(buf, args[0].str + start, end - start); buf[end - start] = '\0';
53+ return vm_str(buf);
54+ }
55+ case 18: { // indexOf
56+ if (argc != 2 || args[0].type != VM_STR || args[1].type != VM_STR) { vm_runtime_error("indexOf(str, search) expected\n"); }
57+ char *found = strstr(args[0].str, args[1].str);
58+ if (!found) return vm_num(-1);
59+ return vm_num(found - args[0].str);
60+ }
61+ case 19: { // replace
62+ if (argc != 3 || args[0].type != VM_STR || args[1].type != VM_STR || args[2].type != VM_STR) { vm_runtime_error("replace(str, search, replacement) expected\n"); }
63+ const char *search = args[1].str; int search_len = (int)strlen(search);
64+ if (search_len == 0) { char *c = malloc(strlen(args[0].str) + 1); strcpy(c, args[0].str); return vm_str(c); }
65+ int cap = (int)strlen(args[0].str) * 2 + 16; char *buf = malloc(cap); int blen = 0;
66+ const char *cur = args[0].str; const char *found; int rep_len = (int)strlen(args[2].str);
67+ while ((found = strstr(cur, search)) != NULL) {
68+ int prefix = (int)(found - cur);
69+ while (blen + prefix + rep_len + 1 > cap) { cap *= 2; buf = realloc(buf, cap); }
70+ memcpy(buf + blen, cur, prefix); blen += prefix;
71+ memcpy(buf + blen, args[2].str, rep_len); blen += rep_len;
72+ cur = found + search_len;
73+ }
74+ int remaining = (int)strlen(cur);
75+ while (blen + remaining + 1 > cap) { cap *= 2; buf = realloc(buf, cap); }
76+ memcpy(buf + blen, cur, remaining); blen += remaining; buf[blen] = '\0';
77+ return vm_str(buf);
78+ }
79+ case 20: { // upper
80+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("upper() expects a string\n"); }
81+ char *copy = malloc(strlen(args[0].str) + 1); strcpy(copy, args[0].str);
82+ for (char *p = copy; *p; p++) *p = (char)toupper((unsigned char)*p);
83+ return vm_str(copy);
84+ }
85+ case 21: { // lower
86+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("lower() expects a string\n"); }
87+ char *copy = malloc(strlen(args[0].str) + 1); strcpy(copy, args[0].str);
88+ for (char *p = copy; *p; p++) *p = (char)tolower((unsigned char)*p);
89+ return vm_str(copy);
90+ }
91+ case 22: { // trim
92+ if (argc != 1 || args[0].type != VM_STR) { vm_runtime_error("trim() expects a string\n"); }
93+ const char *s = args[0].str;
94+ while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
95+ int slen = (int)strlen(s);
96+ while (slen > 0 && (s[slen - 1] == ' ' || s[slen - 1] == '\t' || s[slen - 1] == '\n' || s[slen - 1] == '\r')) slen--;
97+ char *buf = malloc(slen + 1); memcpy(buf, s, slen); buf[slen] = '\0';
98+ return vm_str(buf);
99+ }
100+ }
101+ vm_runtime_error("unknown builtin id: %d\n", id);
102+}

ashvm/src/common.hadded

+10−0
@@ -0,0 +1,10 @@
1+#ifndef ASH_COMMON_H
2+#define ASH_COMMON_H
3+
4+#define MAX_VARS 64
5+#define MAX_SCOPES 65
6+#define MAX_FUNCTIONS 64
7+#define MAX_PARAMS 8
8+#define MAX_CALL_ARGS 8
9+
10+#endif

ashvm/src/compiler/calls.cadded

+199−0
@@ -0,0 +1,199 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "compiler/internal.h"
5+#include "builtins/builtins.h"
6+#include "diagnostics/diagnostics.h"
7+
8+void emit_call(Token id) {
9+ int builtin_id = vm_builtin_lookup(id.start, id.length);
10+ int fn_idx = (builtin_id == -1) ? find_function_in_context(id.start, id.length) : -1;
11+ int local_slot = (builtin_id == -1 && fn_idx == -1) ? resolve_local(id.start, id.length) : -1;
12+
13+ int argc = 0;
14+ if (current.type != TOKEN_RPAREN) {
15+ expression(); argc++;
16+ while (current.type == TOKEN_COMMA) { advance_token(); expression(); argc++; }
17+ }
18+ expect(TOKEN_RPAREN, "expected ')' after arguments");
19+
20+ if (builtin_id != -1) {
21+ emit_op(OP_CALL_BUILTIN);
22+ emit((uint8_t)builtin_id);
23+ emit((uint8_t)argc);
24+ } else if (fn_idx != -1) {
25+ if (argc != vm_functions[fn_idx].arity) {
26+ char msg[128];
27+ snprintf(msg, sizeof(msg), "function %.*s expected %d args, got %d", id.length, id.start, vm_functions[fn_idx].arity, argc);
28+ diagnostics_report("error", msg, id.start, id.length);
29+ exit(1);
30+ }
31+ emit_op(OP_CALL);
32+ emit((uint8_t)fn_idx);
33+ emit((uint8_t)argc);
34+ } else if (local_slot != -1) {
35+ emit2(OP_GET_LOCAL, (uint8_t)local_slot);
36+ emit_op(OP_CALL_VALUE);
37+ emit((uint8_t)argc);
38+ } else {
39+ char msg[128];
40+ snprintf(msg, sizeof(msg), "undefined function: %.*s", id.length, id.start);
41+ diagnostics_report("error", msg, id.start, id.length);
42+ exit(1);
43+ }
44+}
45+
46+// Emits a call to an already-fully-qualified function name (e.g.
47+// "ns.func"), used by expr.c's `ns.func(...)` dispatch -- unlike
48+// emit_call(), this never falls back to builtins/locals/context-namespace
49+// lookup, since a namespaced call can only ever mean an imported file's
50+// function.
51+void emit_call_named(const char *name, int len) {
52+ int fn_idx = find_function(name, len); // already fully-qualified
53+
54+ int argc = 0;
55+ if (current.type != TOKEN_RPAREN) {
56+ expression(); argc++;
57+ while (current.type == TOKEN_COMMA) { advance_token(); expression(); argc++; }
58+ }
59+ expect(TOKEN_RPAREN, "expected ')' after arguments");
60+
61+ if (fn_idx == -1) {
62+ char msg[128];
63+ snprintf(msg, sizeof(msg), "undefined function: %.*s", len, name);
64+ diagnostics_report("error", msg, current.start, current.length);
65+ exit(1);
66+ }
67+ if (argc != vm_functions[fn_idx].arity) {
68+ char msg[128];
69+ snprintf(msg, sizeof(msg), "function %.*s expected %d args, got %d", len, name, vm_functions[fn_idx].arity, argc);
70+ diagnostics_report("error", msg, current.start, current.length);
71+ exit(1);
72+ }
73+ emit_op(OP_CALL);
74+ emit((uint8_t)fn_idx);
75+ emit((uint8_t)argc);
76+}
77+
78+// fn(params) { body } as an expression. Captures the ENTIRE enclosing scope's
79+// locals by value at creation time (same rule as ashc's closures used to
80+// have). The captured variables get their own reserved local slots in the
81+// lambda's own chunk, placed right after its parameters, so the compiled
82+// body can reference them via the normal resolve_local() mechanism just
83+// like any other local.
84+void lambda_literal(void) {
85+ advance_token();
86+ expect(TOKEN_LPAREN, "expected '(' after 'fn'");
87+
88+ char namebuf[32];
89+ int namelen = snprintf(namebuf, sizeof(namebuf), "__lambda_%d", lambda_counter++);
90+ if (namelen > (int)sizeof(namebuf) - 1) namelen = (int)sizeof(namebuf) - 1;
91+
92+ if (vm_function_count >= MAX_VM_FUNCS) { diagnostics_report("error", "too many functions", current.start, current.length); exit(1); }
93+ int fn_idx = vm_function_count++;
94+ memcpy(vm_functions[fn_idx].name, namebuf, namelen);
95+ vm_functions[fn_idx].name[namelen] = '\0';
96+ vm_functions[fn_idx].arity = 0;
97+ chunk_init(&vm_functions[fn_idx].chunk);
98+
99+ char param_names[8][64];
100+ int param_lens[8];
101+ int argc = 0;
102+ if (current.type != TOKEN_RPAREN) {
103+ expect(TOKEN_IDENTIFIER, "expected parameter name");
104+ memcpy(param_names[argc], previous.start, previous.length);
105+ param_lens[argc] = previous.length;
106+ argc++;
107+ while (current.type == TOKEN_COMMA) {
108+ advance_token();
109+ expect(TOKEN_IDENTIFIER, "expected parameter name");
110+ memcpy(param_names[argc], previous.start, previous.length);
111+ param_lens[argc] = previous.length;
112+ argc++;
113+ }
114+ }
115+ expect(TOKEN_RPAREN, "expected ')' after parameters");
116+ vm_functions[fn_idx].arity = argc;
117+
118+ int capture_count = local_count;
119+ char captured_names[MAX_VM_LOCALS][64];
120+ memcpy(captured_names, local_names, sizeof(local_names));
121+
122+ Chunk *outer_chunk = chunk;
123+ char outer_locals[MAX_VM_LOCALS][64];
124+ int outer_local_count = local_count;
125+ memcpy(outer_locals, local_names, sizeof(local_names));
126+
127+ chunk = &vm_functions[fn_idx].chunk;
128+ local_count = 0;
129+ for (int i = 0; i < argc; i++) declare_local(param_names[i], param_lens[i]);
130+ for (int i = 0; i < capture_count; i++) declare_local(captured_names[i], (int)strlen(captured_names[i]));
131+
132+ block();
133+ emit_constant(vm_num(0));
134+ emit_op(OP_RETURN);
135+
136+ chunk = outer_chunk;
137+ local_count = outer_local_count;
138+ memcpy(local_names, outer_locals, sizeof(local_names));
139+
140+ emit_op(OP_MAKE_CLOSURE);
141+ emit((uint8_t)fn_idx);
142+ emit((uint8_t)capture_count);
143+}
144+
145+// Tries to compile `identifier CMP (number|identifier)` directly into a
146+// fused OP_CMP_JUMP, used by if/while conditions. Returns the bytecode
147+// offset to patch_jump() once the branch target is known, or -1 if the
148+// condition didn't match the fusable shape (caller falls back to a plain
149+// expression() + OP_JUMP_IF_FALSE).
150+int try_fuse_condition(void) {
151+ if (current.type != TOKEN_IDENTIFIER) return -1;
152+ Token save = current;
153+ Token id = current;
154+ advance_token();
155+
156+ int cmp_code;
157+ switch (current.type) {
158+ case TOKEN_LESS: cmp_code = 0; break;
159+ case TOKEN_LESS_EQUAL: cmp_code = 1; break;
160+ case TOKEN_GREATER: cmp_code = 2; break;
161+ case TOKEN_GREATER_EQUAL: cmp_code = 3; break;
162+ case TOKEN_EQUAL_EQUAL: cmp_code = 4; break;
163+ case TOKEN_BANG_EQUAL: cmp_code = 5; break;
164+ default:
165+ lexer_init(save.start + save.length); current = save; return -1;
166+ }
167+ advance_token();
168+
169+ int slot_a = resolve_local(id.start, id.length);
170+ if (slot_a == -1) { lexer_init(save.start + save.length); current = save; return -1; }
171+
172+ uint8_t flags = (uint8_t)cmp_code;
173+ uint8_t b_operand;
174+ if (current.type == TOKEN_NUMBER) {
175+ double v = strtod(current.start, NULL);
176+ b_operand = (uint8_t)chunk_add_constant(chunk, vm_num(v));
177+ flags |= 0x80;
178+ advance_token();
179+ } else if (current.type == TOKEN_IDENTIFIER) {
180+ int slot_b = resolve_local(current.start, current.length);
181+ if (slot_b == -1) { lexer_init(save.start + save.length); current = save; return -1; }
182+ b_operand = (uint8_t)slot_b;
183+ advance_token();
184+ } else {
185+ lexer_init(save.start + save.length); current = save; return -1;
186+ }
187+
188+ if (current.type != TOKEN_RPAREN) {
189+ lexer_init(save.start + save.length); current = save; return -1;
190+ }
191+
192+ emit_op(OP_CMP_JUMP);
193+ emit((uint8_t)slot_a);
194+ emit(flags);
195+ emit(b_operand);
196+ int patch_loc = chunk->count;
197+ emit(0xff); emit(0xff);
198+ return patch_loc;
199+}

ashvm/src/compiler/compiler.hadded

+20−0
@@ -0,0 +1,20 @@
1+#ifndef ASH_VM_COMPILER_H
2+#define ASH_VM_COMPILER_H
3+#include "vm/chunk.h"
4+
5+#define MAX_VM_FUNCS 64
6+#define MAX_VM_LOCALS 128
7+
8+typedef struct {
9+ char name[64];
10+ int arity;
11+ Chunk chunk;
12+} VMFunction;
13+
14+extern VMFunction vm_functions[MAX_VM_FUNCS];
15+extern int vm_function_count;
16+
17+Chunk *compile(const char *source, const char *source_path);
18+Chunk *compile_repl_line(const char *source, Chunk *target_chunk, int *out_start_offset);
19+
20+#endif

ashvm/src/compiler/constants.cadded

+54−0
@@ -0,0 +1,54 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "compiler/constants.h"
5+#include "compiler/internal.h"
6+#include "diagnostics/diagnostics.h"
7+
8+static VMConstant constants[MAX_VM_CONSTS];
9+static int constant_count = 0;
10+
11+VMConstant *declare_constant(const char *name, int len, VMValue value) {
12+ if (constant_count >= MAX_VM_CONSTS) { diagnostics_report("error", "too many imported constants", current.start, current.length); exit(1); }
13+ if (len >= (int)sizeof(constants[0].name)) { diagnostics_report("error", "constant name too long (including namespace prefix)", current.start, current.length); exit(1); }
14+ if (resolve_constant(name, len) != NULL) { diagnostics_report("error", "constant already declared", current.start, current.length); exit(1); }
15+ VMConstant *k = &constants[constant_count++];
16+ memcpy(k->name, name, (size_t)len);
17+ k->name[len] = '\0';
18+ k->value = value;
19+ return k;
20+}
21+
22+VMConstant *resolve_constant(const char *name, int len) {
23+ for (int i = 0; i < constant_count; i++) {
24+ if ((int)strlen(constants[i].name) == len && strncmp(constants[i].name, name, len) == 0) return &constants[i];
25+ }
26+ return NULL;
27+}
28+
29+void codegen_constant_value(VMConstant *k) {
30+ emit_constant(k->value);
31+}
32+
33+int try_declare_import_constant(const char *name, int len) {
34+ const char *ns; int ns_len;
35+ get_import_namespace(&ns, &ns_len);
36+ if (ns_len == 0) return 0;
37+
38+ char combined[96];
39+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s", ns_len, ns, len, name);
40+ VMValue value;
41+ if (current.type == TOKEN_NUMBER) {
42+ value = vm_num(strtod(current.start, NULL));
43+ advance_token();
44+ } else if (current.type == TOKEN_STRING) {
45+ value = vm_str(vm_copy_string_escaped(current.start, current.length));
46+ advance_token();
47+ } else {
48+ diagnostics_report("error", "imported top-level 'let' must be a literal number or string constant", current.start, current.length);
49+ exit(1);
50+ }
51+ declare_constant(combined, combined_len, value);
52+ expect(TOKEN_SEMICOLON, "expected ';' after statement");
53+ return 1;
54+}

ashvm/src/compiler/constants.hadded

+30−0
@@ -0,0 +1,30 @@
1+#ifndef ASH_VM_COMPILER_CONSTANTS_H
2+#define ASH_VM_COMPILER_CONSTANTS_H
3+#include "value/value.h"
4+
5+#define MAX_VM_CONSTS 32
6+
7+typedef struct {
8+ char name[96];
9+ VMValue value; // built once at declare time via vm_num/vm_str, reused
10+ // (by re-emitting, not by sharing the VMValue struct
11+ // itself -- see codegen_constant_value) at every use site
12+} VMConstant;
13+
14+VMConstant *declare_constant(const char *name, int len, VMValue value);
15+VMConstant *resolve_constant(const char *name, int len);
16+
17+// Emits the same OP_CONST codegen a literal of this constant's value
18+// would have -- used at every `ns.CONST` use site.
19+void codegen_constant_value(VMConstant *k);
20+
21+// Called at the top of let_statement(), right after the variable name and
22+// '=' have been consumed: if an import namespace is currently active,
23+// consumes the RHS (must be a literal number or string), registers it as
24+// a namespaced constant, and consumes the trailing ';'. Returns 1 if it
25+// handled the whole statement (caller should return immediately), 0 if no
26+// namespace is active (caller should fall through to normal
27+// variable-declaration handling).
28+int try_declare_import_constant(const char *name, int len);
29+
30+#endif

ashvm/src/compiler/expr.cadded

+183−0
@@ -0,0 +1,183 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include "compiler/internal.h"
4+#include "diagnostics/diagnostics.h"
5+
6+// Precedence climbing: expression -> logical_or -> logical_and ->
7+// comparison -> additive -> term -> unary -> postfix -> primary.
8+
9+static void primary(void) {
10+ if (current.type == TOKEN_FN) { lambda_literal(); return; }
11+ if (current.type == TOKEN_NUMBER) {
12+ double v = strtod(current.start, NULL);
13+ advance_token();
14+ emit_constant(vm_num(v));
15+ return;
16+ }
17+ if (current.type == TOKEN_STRING) {
18+ char *s = vm_copy_string_escaped(current.start, current.length);
19+ advance_token();
20+ emit_constant(vm_str(s));
21+ return;
22+ }
23+ if (current.type == TOKEN_LBRACKET) {
24+ advance_token();
25+ int count = 0;
26+ if (current.type != TOKEN_RBRACKET) {
27+ expression(); count++;
28+ while (current.type == TOKEN_COMMA) { advance_token(); expression(); count++; }
29+ }
30+ expect(TOKEN_RBRACKET, "expected ']' after array literal");
31+ emit_op(OP_ARRAY);
32+ emit((uint8_t)count);
33+ return;
34+ }
35+ if (current.type == TOKEN_LBRACE) {
36+ advance_token();
37+ int n = 0;
38+ if (current.type != TOKEN_RBRACE) {
39+ for (;;) {
40+ if (current.type != TOKEN_STRING) { diagnostics_report("error", "expected string key in map literal", current.start, current.length); exit(1); }
41+ char *key = vm_copy_string_escaped(current.start, current.length);
42+ advance_token();
43+ expect(TOKEN_COLON, "expected ':' after map key");
44+ emit_constant(vm_str(key));
45+ expression();
46+ n++;
47+ if (current.type == TOKEN_COMMA) { advance_token(); continue; }
48+ break;
49+ }
50+ }
51+ expect(TOKEN_RBRACE, "expected '}' after map literal");
52+ emit_op(OP_MAP);
53+ emit((uint8_t)n);
54+ return;
55+ }
56+ if (current.type == TOKEN_IDENTIFIER) {
57+ Token id = current;
58+ advance_token();
59+
60+ if (current.type == TOKEN_DOT) {
61+ advance_token();
62+ expect(TOKEN_IDENTIFIER, "expected name after '.'");
63+ Token member = previous;
64+
65+ char combined[128];
66+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s",
67+ id.length, id.start, member.length, member.start);
68+
69+ if (current.type == TOKEN_LPAREN) {
70+ advance_token();
71+ emit_call_named(combined, combined_len);
72+ return;
73+ }
74+
75+ VMConstant *k = resolve_constant(combined, combined_len);
76+ if (k != NULL) { codegen_constant_value(k); return; }
77+
78+ diagnostics_report("error", "undefined namespaced function or constant", id.start, id.length);
79+ exit(1);
80+ }
81+
82+ if (current.type == TOKEN_LPAREN) {
83+ advance_token();
84+ emit_call(id);
85+ return;
86+ }
87+ int slot = resolve_local(id.start, id.length);
88+ if (slot != -1) { emit2(OP_GET_LOCAL, (uint8_t)slot); return; }
89+ int fn_idx = find_function_in_context(id.start, id.length);
90+ if (fn_idx != -1) { emit_constant(vm_function_val(fn_idx)); return; }
91+
92+ // Not a function either -- if we're compiling inside an imported
93+ // file's own namespace context, a bare reference may be that same
94+ // file's own top-level constant.
95+ const char *ns; int ns_len;
96+ get_import_namespace(&ns, &ns_len);
97+ if (ns_len > 0) {
98+ char combined[128];
99+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s", ns_len, ns, id.length, id.start);
100+ VMConstant *k = resolve_constant(combined, combined_len);
101+ if (k != NULL) { codegen_constant_value(k); return; }
102+ }
103+
104+ char msg[128];
105+ snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
106+ diagnostics_report("error", msg, id.start, id.length);
107+ exit(1);
108+ }
109+ if (current.type == TOKEN_LPAREN) {
110+ advance_token();
111+ expression();
112+ expect(TOKEN_RPAREN, "expected ')' after expression");
113+ return;
114+ }
115+ diagnostics_report("error", "expected value", current.start, current.length);
116+ exit(1);
117+}
118+
119+static void postfix(void) {
120+ primary();
121+ while (current.type == TOKEN_LBRACKET) {
122+ advance_token();
123+ expression();
124+ expect(TOKEN_RBRACKET, "expected ']' after index");
125+ emit_op(OP_INDEX_GET);
126+ }
127+}
128+
129+static void unary(void) {
130+ if (current.type == TOKEN_BANG) { advance_token(); unary(); emit_op(OP_NOT); return; }
131+ if (current.type == TOKEN_MINUS) { advance_token(); unary(); emit_op(OP_NEG); return; }
132+ postfix();
133+}
134+
135+static void term(void) {
136+ unary();
137+ while (current.type == TOKEN_STAR || current.type == TOKEN_SLASH || current.type == TOKEN_PERCENT) {
138+ TokenType op = current.type;
139+ advance_token();
140+ unary();
141+ emit_op(op == TOKEN_STAR ? OP_MUL : op == TOKEN_SLASH ? OP_DIV : OP_MOD);
142+ }
143+}
144+
145+static void additive(void) {
146+ term();
147+ while (current.type == TOKEN_PLUS || current.type == TOKEN_MINUS) {
148+ TokenType op = current.type;
149+ advance_token();
150+ term();
151+ emit_op(op == TOKEN_PLUS ? OP_ADD : OP_SUB);
152+ }
153+}
154+
155+static void comparison(void) {
156+ additive();
157+ if (current.type == TOKEN_EQUAL_EQUAL || current.type == TOKEN_BANG_EQUAL ||
158+ current.type == TOKEN_LESS || current.type == TOKEN_LESS_EQUAL ||
159+ current.type == TOKEN_GREATER || current.type == TOKEN_GREATER_EQUAL) {
160+ TokenType op = current.type;
161+ advance_token();
162+ additive();
163+ switch (op) {
164+ case TOKEN_EQUAL_EQUAL: emit_op(OP_EQ); break;
165+ case TOKEN_BANG_EQUAL: emit_op(OP_NEQ); break;
166+ case TOKEN_LESS: emit_op(OP_LT); break;
167+ case TOKEN_LESS_EQUAL: emit_op(OP_LE); break;
168+ case TOKEN_GREATER: emit_op(OP_GT); break;
169+ case TOKEN_GREATER_EQUAL: emit_op(OP_GE); break;
170+ default: break;
171+ }
172+ }
173+}
174+
175+static void logical_and(void) {
176+ comparison();
177+ while (current.type == TOKEN_AND) { advance_token(); comparison(); emit_op(OP_AND); }
178+}
179+static void logical_or(void) {
180+ logical_and();
181+ while (current.type == TOKEN_OR) { advance_token(); logical_and(); emit_op(OP_OR); }
182+}
183+void expression(void) { logical_or(); }

ashvm/src/compiler/import_paths.cadded

+62−0
@@ -0,0 +1,62 @@
1+#define _XOPEN_SOURCE 700 // exposes realpath() and PATH_MAX under -std=c11
2+#include <string.h>
3+#include <stdio.h>
4+#include <stdlib.h>
5+#include "compiler/import_paths.h"
6+
7+void import_path_dirname(const char *path, char *out, int out_size) {
8+ const char *slash = strrchr(path, '/');
9+ if (!slash) { snprintf(out, (size_t)out_size, "."); return; }
10+ int len = (int)(slash - path);
11+ if (len >= out_size) len = out_size - 1;
12+ memcpy(out, path, (size_t)len);
13+ out[len] = '\0';
14+}
15+
16+void import_derive_namespace(const char *canonical_path, char *out, int out_size) {
17+ const char *slash = strrchr(canonical_path, '/');
18+ const char *base = slash ? slash + 1 : canonical_path;
19+ int len = (int)strlen(base);
20+ if (len > 4 && strcmp(base + len - 4, ".ash") == 0) len -= 4;
21+ if (len >= out_size) len = out_size - 1;
22+ memcpy(out, base, (size_t)len);
23+ out[len] = '\0';
24+}
25+
26+static int is_relative_prefix(const char *raw, int len) {
27+ if (len >= 2 && raw[0] == '.' && raw[1] == '/') return 1;
28+ if (len >= 3 && raw[0] == '.' && raw[1] == '.' && raw[2] == '/') return 1;
29+ return 0;
30+}
31+
32+int import_resolve_path(const char *raw_path, int raw_len,
33+ const char *importing_file_dir,
34+ const char *project_root_dir,
35+ char *canonical_out,
36+ char *message_out, int message_out_size) {
37+ char candidate[PATH_MAX];
38+
39+ if (is_relative_prefix(raw_path, raw_len)) {
40+ if (raw_len < 4 || strncmp(raw_path + raw_len - 4, ".ash", 4) != 0) {
41+ snprintf(message_out, (size_t)message_out_size, "relative import path must end in '.ash'");
42+ return 0;
43+ }
44+ snprintf(candidate, sizeof(candidate), "%s/%.*s", importing_file_dir, raw_len, raw_path);
45+ } else {
46+ for (int i = 0; i < raw_len; i++) {
47+ if (raw_path[i] == '.' || raw_path[i] == '/') {
48+ snprintf(message_out, (size_t)message_out_size,
49+ "library import name must not contain '.' or '/' (did you mean './%.*s'?)", raw_len, raw_path);
50+ return 0;
51+ }
52+ }
53+ snprintf(candidate, sizeof(candidate), "%s/ash.libs/%.*s/%.*s.ash",
54+ project_root_dir, raw_len, raw_path, raw_len, raw_path);
55+ }
56+
57+ if (realpath(candidate, canonical_out) == NULL) {
58+ snprintf(message_out, (size_t)message_out_size, "could not resolve '%.*s'", raw_len, raw_path);
59+ return 0;
60+ }
61+ return 1;
62+}

ashvm/src/compiler/import_paths.hadded

+39−0
@@ -0,0 +1,39 @@
1+#ifndef ASH_VM_COMPILER_IMPORT_PATHS_H
2+#define ASH_VM_COMPILER_IMPORT_PATHS_H
3+#include <limits.h>
4+
5+// Resolves the raw text inside `@import <...>` to a canonicalized
6+// (realpath()'d) absolute filesystem path.
7+//
8+// raw_path/raw_len: the text between '<' and '>', exactly as scanned by
9+// lexer_scan_import_path (NOT null-terminated -- callers must use len).
10+// importing_file_dir: directory of the file containing this @import
11+// statement (relative imports resolve against this).
12+// project_root_dir: directory of the entry .ash file passed on the
13+// command line (library imports resolve against this, regardless of
14+// how deeply nested the importing file is).
15+// canonical_out: buffer of at least PATH_MAX bytes.
16+//
17+// Returns 1 and fills canonical_out on success. Returns 0 on failure
18+// (message_out is filled with a short human-readable reason, e.g.
19+// "could not resolve '<path>'" or "library import name must not contain
20+// '.' or '/'" -- callers pass this straight into diagnostics_report).
21+int import_resolve_path(const char *raw_path, int raw_len,
22+ const char *importing_file_dir,
23+ const char *project_root_dir,
24+ char *canonical_out,
25+ char *message_out, int message_out_size);
26+
27+// Derives the auto-namespace for a resolved canonical path: the file's
28+// own basename with the trailing ".ash" stripped. Works uniformly for
29+// both relative and library imports because library entry files are
30+// required to be named "<libname>/<libname>.ash" -- the folder name and
31+// the basename-minus-extension are always identical by construction.
32+void import_derive_namespace(const char *canonical_path, char *out, int out_size);
33+
34+// Directory-of-a-path helper (hand-rolled, not libgen.h's dirname(),
35+// which mutates its input and reuses a static buffer across calls --
36+// both a bad fit here). Writes "." if path has no '/'.
37+void import_path_dirname(const char *path, char *out, int out_size);
38+
39+#endif

ashvm/src/compiler/internal.hadded

+45−0
@@ -0,0 +1,45 @@
1+#ifndef ASH_VM_COMPILER_INTERNAL_H
2+#define ASH_VM_COMPILER_INTERNAL_H
3+#include "compiler/state.h"
4+#include "compiler/constants.h"
5+
6+// Cross-file forward declarations between compiler_expr.c and
7+// compiler_stmt.c -- not part of the public API (compiler.h).
8+
9+// compiler_expr.c
10+void expression(void);
11+
12+// compiler_calls.c
13+void emit_call(Token id);
14+void emit_call_named(const char *name, int len);
15+void lambda_literal(void);
16+// Tries to compile `identifier CMP (number|identifier)` directly into a
17+// fused OP_CMP_JUMP, used by if/while conditions. Returns the bytecode
18+// offset to patch_jump() once the branch target is known, or -1 if the
19+// condition didn't match the fusable shape (caller falls back to a plain
20+// expression() + OP_JUMP_IF_FALSE).
21+int try_fuse_condition(void);
22+
23+// compiler_stmt.c
24+void block(void);
25+// statement() itself (compiler_stmt.c's dispatcher) also needs to be
26+// callable from compiler_stmt_import.c's parse_imported_file_body(),
27+// which drives the same top-level "while not EOF, parse a statement"
28+// loop for a recursively parsed imported file -- so it's declared here
29+// rather than staying static to compiler_stmt.c.
30+void statement(void);
31+
32+// compiler_stmt_control.c -- the four statement kinds involved enough to
33+// warrant their own function (each assumes its keyword token has already
34+// 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+
40+// compiler_stmt_import.c
41+void compile_import_stmt(void);
42+void imports_close_block(void);
43+void imports_init(const char *entry_file_path);
44+
45+#endif

ashvm/src/compiler/state.cadded

+114−0
@@ -0,0 +1,114 @@
1+#include <stdlib.h>
2+#include <string.h>
3+#include "compiler/state.h"
4+#include "diagnostics/diagnostics.h"
5+
6+VMFunction vm_functions[MAX_VM_FUNCS];
7+int vm_function_count = 0;
8+
9+Token current;
10+Token previous;
11+Chunk *chunk;
12+char local_names[MAX_VM_LOCALS][64];
13+int local_count;
14+int lambda_counter = 0;
15+
16+void advance_token(void) { previous = current; current = lexer_next_token(); }
17+
18+void expect(TokenType type, const char *message) {
19+ if (current.type == type) { advance_token(); return; }
20+ diagnostics_report("error", message, current.start, current.length);
21+ exit(1);
22+}
23+
24+int resolve_local(const char *name, int len) {
25+ for (int i = 0; i < local_count; i++) {
26+ if ((int)strlen(local_names[i]) == len && strncmp(local_names[i], name, len) == 0) return i;
27+ }
28+ return -1;
29+}
30+
31+int declare_local(const char *name, int len) {
32+ if (local_count >= MAX_VM_LOCALS) { diagnostics_report("error", "too many locals", current.start, current.length); exit(1); }
33+ memcpy(local_names[local_count], name, len);
34+ local_names[local_count][len] = '\0';
35+ return local_count++;
36+}
37+
38+int find_function(const char *name, int len) {
39+ for (int i = 0; i < vm_function_count; i++) {
40+ if ((int)strlen(vm_functions[i].name) == len && strncmp(vm_functions[i].name, name, len) == 0) return i;
41+ }
42+ return -1;
43+}
44+
45+static const char *g_import_ns = NULL;
46+static int g_import_ns_len = 0;
47+
48+void set_import_namespace(const char *ns, int len) {
49+ g_import_ns = ns;
50+ g_import_ns_len = len;
51+}
52+
53+void get_import_namespace(const char **out_ns, int *out_len) {
54+ *out_ns = g_import_ns;
55+ *out_len = g_import_ns_len;
56+}
57+
58+static int build_namespaced_name(char *buf, int buf_size, const char *name, int len) {
59+ if (g_import_ns_len + 1 + len >= buf_size) return -1;
60+ memcpy(buf, g_import_ns, (size_t)g_import_ns_len);
61+ buf[g_import_ns_len] = '.';
62+ memcpy(buf + g_import_ns_len + 1, name, (size_t)len);
63+ return g_import_ns_len + 1 + len;
64+}
65+
66+int declare_function_in_context(const char *name, int len) {
67+ char buf[128];
68+ const char *final_name = name;
69+ int final_len = len;
70+ if (g_import_ns_len > 0) {
71+ int combined_len = build_namespaced_name(buf, sizeof(buf), name, len);
72+ if (combined_len < 0) { diagnostics_report("error", "function name too long (including namespace prefix)", current.start, current.length); exit(1); }
73+ final_name = buf;
74+ final_len = combined_len;
75+ }
76+ if (vm_function_count >= MAX_VM_FUNCS) { diagnostics_report("error", "too many functions", current.start, current.length); exit(1); }
77+ if (final_len >= (int)sizeof(vm_functions[0].name)) { diagnostics_report("error", "function name too long", current.start, current.length); exit(1); }
78+ int fn_idx = vm_function_count++;
79+ memcpy(vm_functions[fn_idx].name, final_name, (size_t)final_len);
80+ vm_functions[fn_idx].name[final_len] = '\0';
81+ return fn_idx;
82+}
83+
84+int find_function_in_context(const char *name, int len) {
85+ if (g_import_ns_len == 0) return find_function(name, len);
86+ char buf[128];
87+ int combined_len = build_namespaced_name(buf, sizeof(buf), name, len);
88+ if (combined_len < 0) return -1;
89+ return find_function(buf, combined_len);
90+}
91+
92+void emit(uint8_t byte) { chunk_write(chunk, byte); }
93+void emit_op(uint8_t op) { chunk_write_pos(chunk, op, previous.start, previous.length); }
94+void emit2(uint8_t a, uint8_t b) { emit_op(a); emit(b); }
95+
96+void emit_constant(VMValue v) {
97+ int idx = chunk_add_constant(chunk, v);
98+ emit2(OP_CONST, (uint8_t)idx);
99+}
100+
101+int emit_jump(uint8_t op) { emit_op(op); emit(0xff); emit(0xff); return chunk->count - 2; }
102+
103+void patch_jump(int offset) {
104+ int jump = chunk->count - offset - 2;
105+ chunk->code[offset] = (jump >> 8) & 0xff;
106+ chunk->code[offset + 1] = jump & 0xff;
107+}
108+
109+void emit_loop(int loop_start) {
110+ emit_op(OP_LOOP);
111+ int offset = chunk->count - loop_start + 2;
112+ emit((offset >> 8) & 0xff);
113+ emit(offset & 0xff);
114+}

ashvm/src/compiler/state.hadded

+50−0
@@ -0,0 +1,50 @@
1+#ifndef ASH_VM_COMPILER_STATE_H
2+#define ASH_VM_COMPILER_STATE_H
3+#include "compiler/compiler.h"
4+#include "lexer/lexer.h"
5+
6+// Shared parser/emitter state for compiler_expr.c and compiler_stmt.c --
7+// not part of the public API (compiler.h). Mirrors the same
8+// declare-storage-once, share-via-header pattern ashc used to use for its
9+// own parser state (see rundown.md).
10+
11+extern Token current;
12+extern Token previous;
13+extern Chunk *chunk;
14+extern char local_names[MAX_VM_LOCALS][64];
15+extern int local_count;
16+extern int lambda_counter;
17+
18+void advance_token(void);
19+void expect(TokenType type, const char *message);
20+int resolve_local(const char *name, int len);
21+int declare_local(const char *name, int len);
22+int find_function(const char *name, int len);
23+
24+// Anchors the opcode's source position on `previous`, mirroring ashc's own
25+// error-reporting convention (see diagnostics_report call sites).
26+void emit(uint8_t byte);
27+void emit_op(uint8_t op);
28+void emit2(uint8_t a, uint8_t b);
29+void emit_constant(VMValue v);
30+int emit_jump(uint8_t op);
31+void patch_jump(int offset);
32+void emit_loop(int loop_start);
33+
34+void set_import_namespace(const char *ns, int len);
35+void get_import_namespace(const char **out_ns, int *out_len);
36+
37+// Declares a NEW vm_functions[] entry under "ns.NAME" if a namespace is
38+// currently set, else under the bare NAME. Returns the new entry's index
39+// (mirrors compile_fn_decl's own prior inline logic, now centralized so
40+// imports.c-equivalent code and compile_fn_decl share it). Exits via
41+// diagnostics_report + exit(1) on table-full or name-too-long, matching
42+// every other error path in this file.
43+int declare_function_in_context(const char *name, int len);
44+
45+// While a namespace is set: resolves ONLY "ns.NAME" (no bare fallback).
46+// While no namespace is set: resolves NAME directly (identical to
47+// find_function's existing behavior). Returns -1 if not found.
48+int find_function_in_context(const char *name, int len);
49+
50+#endif

ashvm/src/compiler/stmt.cadded

+185−0
@@ -0,0 +1,185 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "compiler/internal.h"
5+#include "diagnostics/diagnostics.h"
6+
7+// statement() is shared between top-level parsing (compile()'s own loop,
8+// and stmt_import.c's parse_imported_file_body()) and here, inside a
9+// fn/if/while/try body -- block_depth distinguishes the two so the "only
10+// fn/let at an imported file's top level" guard in statement() only
11+// fires for genuinely top-level statements, never for an ordinary
12+// return/if/etc. nested inside an imported function's own body.
13+static int block_depth = 0;
14+
15+void block(void) {
16+ expect(TOKEN_LBRACE, "expected '{'");
17+ block_depth++;
18+ while (current.type != TOKEN_RBRACE && current.type != TOKEN_EOF) statement();
19+ block_depth--;
20+ expect(TOKEN_RBRACE, "expected '}'");
21+}
22+
23+// let name = value; -- also tries to fuse `let x = x OP (number|local);`
24+// into a single OP_ACC_LOCAL instead of a full expression compile, when the
25+// shape matches exactly (see vm/compiler.c's history / rundown.md).
26+static void let_statement(void) {
27+ advance_token();
28+ expect(TOKEN_IDENTIFIER, "expected variable name after 'let'");
29+ const char *name = previous.start;
30+ int len = previous.length;
31+ expect(TOKEN_EQUAL, "expected '=' after variable name");
32+
33+ if (try_declare_import_constant(name, len)) return;
34+
35+ int slot = resolve_local(name, len);
36+
37+ int fused = 0;
38+ if (slot != -1 && current.type == TOKEN_IDENTIFIER &&
39+ current.length == len && strncmp(current.start, name, len) == 0) {
40+ Token save = current;
41+ advance_token();
42+ int op_code = -1;
43+ if (current.type == TOKEN_PLUS) op_code = 0;
44+ else if (current.type == TOKEN_MINUS) op_code = 1;
45+ else if (current.type == TOKEN_STAR) op_code = 2;
46+
47+ if (op_code != -1) {
48+ advance_token();
49+ uint8_t flags = (uint8_t)op_code;
50+ uint8_t b_operand;
51+ int have_operand = 0;
52+ if (current.type == TOKEN_NUMBER) {
53+ double v = strtod(current.start, NULL);
54+ b_operand = (uint8_t)chunk_add_constant(chunk, vm_num(v));
55+ flags |= 0x80;
56+ advance_token();
57+ have_operand = 1;
58+ } else if (current.type == TOKEN_IDENTIFIER) {
59+ int src_slot = resolve_local(current.start, current.length);
60+ if (src_slot != -1) {
61+ b_operand = (uint8_t)src_slot;
62+ advance_token();
63+ have_operand = 1;
64+ } else {
65+ b_operand = 0;
66+ }
67+ } else {
68+ b_operand = 0;
69+ }
70+
71+ if (have_operand && current.type == TOKEN_SEMICOLON) {
72+ advance_token();
73+ emit_op(OP_ACC_LOCAL);
74+ emit((uint8_t)slot);
75+ emit(flags);
76+ emit(b_operand);
77+ fused = 1;
78+ }
79+ }
80+ if (!fused) { lexer_init(save.start + save.length); current = save; }
81+ }
82+
83+ if (!fused) {
84+ expression();
85+ expect(TOKEN_SEMICOLON, "expected ';' after statement");
86+ if (slot != -1) { emit2(OP_SET_LOCAL, (uint8_t)slot); emit_op(OP_POP); }
87+ else declare_local(name, len);
88+ }
89+}
90+
91+// Either a bare call (`foo();`) or an index assignment (`foo[i] = value;`) --
92+// the two statement shapes that start with a bare identifier.
93+static void identifier_statement(void) {
94+ Token id = current;
95+ advance_token();
96+ if (current.type == TOKEN_LPAREN) {
97+ advance_token();
98+ emit_call(id);
99+ expect(TOKEN_SEMICOLON, "expected ';' after call");
100+ emit_op(OP_POP);
101+ return;
102+ }
103+ 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+ }
111+ emit2(OP_GET_LOCAL, (uint8_t)slot);
112+ advance_token();
113+ expression();
114+ expect(TOKEN_RBRACKET, "expected ']'");
115+ expect(TOKEN_EQUAL, "expected '=' for index assignment");
116+ expression();
117+ expect(TOKEN_SEMICOLON, "expected ';' after assignment");
118+ emit_op(OP_INDEX_SET);
119+ return;
120+ }
121+ diagnostics_report("error", "unexpected identifier statement", current.start, current.length);
122+ exit(1);
123+}
124+
125+void statement(void) {
126+ if (current.type == TOKEN_IMPORT) { advance_token(); compile_import_stmt(); return; }
127+ imports_close_block();
128+
129+ const char *ns; int ns_len;
130+ 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);
133+ exit(1);
134+ }
135+
136+ if (current.type == TOKEN_PRINT) {
137+ advance_token();
138+ expression();
139+ expect(TOKEN_SEMICOLON, "expected ';' after statement");
140+ emit_op(OP_PRINT);
141+ return;
142+ }
143+ if (current.type == TOKEN_LET) { let_statement(); return; }
144+ if (current.type == TOKEN_RETURN) {
145+ advance_token();
146+ if (current.type != TOKEN_SEMICOLON) expression();
147+ else emit_constant(vm_num(0));
148+ expect(TOKEN_SEMICOLON, "expected ';' after return");
149+ emit_op(OP_RETURN);
150+ return;
151+ }
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; }
156+ if (current.type == TOKEN_IDENTIFIER) { identifier_statement(); return; }
157+
158+ diagnostics_report("error", "unexpected token", current.start, current.length);
159+ exit(1);
160+}
161+
162+static Chunk main_chunk;
163+
164+Chunk *compile(const char *source, const char *source_path) {
165+ vm_function_count = 0;
166+ local_count = 0;
167+ chunk_init(&main_chunk);
168+ chunk = &main_chunk;
169+ lexer_init(source);
170+ advance_token();
171+ imports_init(source_path);
172+ while (current.type != TOKEN_EOF) statement();
173+ return &main_chunk;
174+}
175+
176+// Appends compiled bytecode to an EXISTING chunk without resetting locals or
177+// function definitions -- used by the REPL so state persists between lines.
178+Chunk *compile_repl_line(const char *source, Chunk *target_chunk, int *out_start_offset) {
179+ chunk = target_chunk;
180+ *out_start_offset = chunk->count;
181+ lexer_init(source);
182+ advance_token();
183+ while (current.type != TOKEN_EOF) statement();
184+ return chunk;
185+}

ashvm/src/compiler/stmt_control.cadded

+129−0
@@ -0,0 +1,129 @@
1+#include <stdlib.h>
2+#include <string.h>
3+#include "compiler/internal.h"
4+#include "diagnostics/diagnostics.h"
5+
6+// Each function here assumes statement() already consumed the keyword token
7+// (TOKEN_FN / TOKEN_IF / TOKEN_TRY / TOKEN_WHILE) that identified it.
8+
9+void compile_fn_decl(void) {
10+ expect(TOKEN_IDENTIFIER, "expected function name after 'fn'");
11+ const char *name = previous.start;
12+ int name_len = previous.length;
13+ expect(TOKEN_LPAREN, "expected '(' after function name");
14+
15+ int fn_idx = declare_function_in_context(name, name_len);
16+ vm_functions[fn_idx].arity = 0;
17+ chunk_init(&vm_functions[fn_idx].chunk);
18+
19+ char param_names[8][64];
20+ int param_lens[8];
21+ int argc = 0;
22+ if (current.type != TOKEN_RPAREN) {
23+ expect(TOKEN_IDENTIFIER, "expected parameter name");
24+ memcpy(param_names[argc], previous.start, previous.length);
25+ param_lens[argc] = previous.length;
26+ argc++;
27+ while (current.type == TOKEN_COMMA) {
28+ advance_token();
29+ expect(TOKEN_IDENTIFIER, "expected parameter name");
30+ memcpy(param_names[argc], previous.start, previous.length);
31+ param_lens[argc] = previous.length;
32+ argc++;
33+ }
34+ }
35+ expect(TOKEN_RPAREN, "expected ')' after parameters");
36+ vm_functions[fn_idx].arity = argc;
37+
38+ Chunk *outer_chunk = chunk;
39+ char outer_locals[MAX_VM_LOCALS][64];
40+ int outer_local_count = local_count;
41+ memcpy(outer_locals, local_names, sizeof(local_names));
42+
43+ chunk = &vm_functions[fn_idx].chunk;
44+ local_count = 0;
45+ for (int i = 0; i < argc; i++) declare_local(param_names[i], param_lens[i]);
46+
47+ block();
48+ emit_constant(vm_num(0));
49+ emit_op(OP_RETURN);
50+
51+ chunk = outer_chunk;
52+ local_count = outer_local_count;
53+ memcpy(local_names, outer_locals, sizeof(local_names));
54+}
55+
56+void compile_if_stmt(void) {
57+ expect(TOKEN_LPAREN, "expected '(' after 'if'");
58+ int patch_loc = try_fuse_condition();
59+ int else_jump;
60+ if (patch_loc != -1) {
61+ expect(TOKEN_RPAREN, "expected ')' after condition");
62+ else_jump = patch_loc;
63+ } else {
64+ expression();
65+ expect(TOKEN_RPAREN, "expected ')' after condition");
66+ else_jump = emit_jump(OP_JUMP_IF_FALSE);
67+ }
68+ block();
69+ if (current.type == TOKEN_ELSE) {
70+ int end_jump = emit_jump(OP_JUMP);
71+ patch_jump(else_jump);
72+ advance_token();
73+ block();
74+ patch_jump(end_jump);
75+ } else {
76+ patch_jump(else_jump);
77+ }
78+}
79+
80+void compile_try_stmt(void) {
81+ int saved_local_count = local_count;
82+
83+ int try_jump = emit_jump(OP_TRY_PUSH);
84+
85+ block();
86+
87+ emit_op(OP_TRY_POP);
88+ int skip_catch = emit_jump(OP_JUMP);
89+
90+ patch_jump(try_jump);
91+
92+ expect(TOKEN_CATCH, "expected 'catch' after try block");
93+ expect(TOKEN_LPAREN, "expected '(' after 'catch'");
94+ expect(TOKEN_IDENTIFIER, "expected error variable name");
95+ // 'e' must land at EXACTLY saved_local_count, not wherever local_count
96+ // ended up after compiling the try block -- since the try block may have
97+ // declared its own locals before erroring, but the runtime error-value
98+ // push always lands at the try-entry stack position, regardless of what
99+ // the try block attempted (all of that gets discarded on the error path).
100+ memcpy(local_names[saved_local_count], previous.start, previous.length);
101+ local_names[saved_local_count][previous.length] = '\0';
102+ local_count = saved_local_count + 1;
103+ expect(TOKEN_RPAREN, "expected ')' after catch variable");
104+
105+ block();
106+ emit_op(OP_POP);
107+
108+ patch_jump(skip_catch);
109+
110+ local_count = saved_local_count;
111+}
112+
113+void compile_while_stmt(void) {
114+ expect(TOKEN_LPAREN, "expected '(' after 'while'");
115+ int loop_start = chunk->count;
116+ int patch_loc = try_fuse_condition();
117+ int exit_jump;
118+ if (patch_loc != -1) {
119+ expect(TOKEN_RPAREN, "expected ')' after condition");
120+ exit_jump = patch_loc;
121+ } else {
122+ expression();
123+ expect(TOKEN_RPAREN, "expected ')' after condition");
124+ exit_jump = emit_jump(OP_JUMP_IF_FALSE);
125+ }
126+ block();
127+ emit_loop(loop_start);
128+ patch_jump(exit_jump);
129+}

ashvm/src/compiler/stmt_import.cadded

+220−0
@@ -0,0 +1,220 @@
1+#define _XOPEN_SOURCE 700 // exposes realpath() and PATH_MAX under -std=c11
2+#include <string.h>
3+#include <stdio.h>
4+#include <stdlib.h>
5+#include <limits.h>
6+#include "compiler/internal.h"
7+#include "compiler/import_paths.h"
8+#include "diagnostics/diagnostics.h"
9+
10+#define MAX_VM_IMPORTS 16
11+#define MAX_VM_IMPORT_DEPTH 8
12+
13+typedef struct {
14+ char canonical_path[PATH_MAX];
15+ char namespace_name[64];
16+} ImportedFile;
17+
18+static ImportedFile imported_files[MAX_VM_IMPORTS];
19+static int imported_file_count = 0;
20+
21+static char resolving_stack[MAX_VM_IMPORT_DEPTH][PATH_MAX];
22+static int resolving_depth = 0;
23+
24+static char project_root_dir[PATH_MAX];
25+static char current_file_dir[PATH_MAX];
26+
27+static int import_block_open = 1;
28+
29+static void fail(const char *msg) {
30+ diagnostics_report("error", msg, current.start, current.length);
31+ exit(1);
32+}
33+
34+void imports_init(const char *entry_file_path) {
35+ char resolved[PATH_MAX];
36+ if (realpath(entry_file_path, resolved) == NULL) fail("could not resolve entry file path");
37+ import_path_dirname(resolved, project_root_dir, sizeof(project_root_dir));
38+ size_t root_len = strlen(project_root_dir);
39+ if (root_len >= sizeof(current_file_dir)) root_len = sizeof(current_file_dir) - 1;
40+ memcpy(current_file_dir, project_root_dir, root_len);
41+ current_file_dir[root_len] = '\0';
42+ import_block_open = 1;
43+}
44+
45+void imports_close_block(void) {
46+ import_block_open = 0;
47+}
48+
49+static ImportedFile *registry_find_by_path(const char *canonical) {
50+ for (int i = 0; i < imported_file_count; i++) {
51+ if (strcmp(imported_files[i].canonical_path, canonical) == 0) return &imported_files[i];
52+ }
53+ return NULL;
54+}
55+
56+static ImportedFile *registry_find_by_namespace(const char *ns) {
57+ for (int i = 0; i < imported_file_count; i++) {
58+ if (strcmp(imported_files[i].namespace_name, ns) == 0) return &imported_files[i];
59+ }
60+ return NULL;
61+}
62+
63+static int resolving_stack_contains(const char *canonical) {
64+ for (int i = 0; i < resolving_depth; i++) {
65+ if (strcmp(resolving_stack[i], canonical) == 0) return 1;
66+ }
67+ return 0;
68+}
69+
70+static void report_circular_import(const char *canonical) {
71+ char msg[1024];
72+ int off = snprintf(msg, sizeof(msg), "circular import detected: ");
73+ for (int i = 0; i < resolving_depth; i++) {
74+ off += snprintf(msg + off, sizeof(msg) - (size_t)off, "%s -> ", resolving_stack[i]);
75+ if (off >= (int)sizeof(msg)) break;
76+ }
77+ snprintf(msg + off, sizeof(msg) - (size_t)off, "%s", canonical);
78+ fail(msg);
79+}
80+
81+static char *read_import_file(const char *path) {
82+ FILE *file = fopen(path, "rb");
83+ if (!file) fail("could not open resolved import file");
84+ fseek(file, 0L, SEEK_END);
85+ long size = ftell(file);
86+ rewind(file);
87+ // Never freed -- an imported file's source buffer must outlive every
88+ // later use site that points diagnostics/lexer state back into it,
89+ // which can be arbitrarily far ahead in the importing program. The
90+ // compiler is a one-shot host process; leaking this until it exits is
91+ // the same documented, intentional tradeoff kiln's imports.c makes.
92+ char *buffer = malloc((size_t)size + 1);
93+ fread(buffer, 1, (size_t)size, file);
94+ buffer[size] = '\0';
95+ fclose(file);
96+ return buffer;
97+}
98+
99+// The restricted top-level loop for a recursively-parsed imported file:
100+// identical shape to compile()'s own `while (current.type != TOKEN_EOF)
101+// statement();` loop (stmt.c). statement() itself enforces the "only
102+// fn/let allowed" restriction by checking get_import_namespace()'s length
103+// AND that block_depth == 0 (see stmt.c) -- so it fires only for this
104+// file's genuine top level, never for statements nested inside one of its
105+// own function bodies.
106+static void parse_imported_file_body(void) {
107+ // Each imported file gets its OWN fresh import-block window: OPEN (not
108+ // closed), so the imported file's own leading @import statements (if
109+ // any -- e.g. a diamond-shaped or chained import) are allowed, exactly
110+ // like the entry file's. Calling imports_close_block() here would do
111+ // the opposite (close it), which is wrong -- statement() itself closes
112+ // the window the moment it dispatches this file's own first
113+ // non-import statement, same as it does for the entry file.
114+ import_block_open = 1;
115+ while (current.type != TOKEN_EOF) statement();
116+}
117+
118+void compile_import_stmt(void) {
119+ if (!import_block_open) fail("@import statements must form a contiguous block at the top of the file");
120+
121+ if (current.type != TOKEN_LESS) fail("expected '<' after '@import'");
122+ Token path = lexer_scan_import_path();
123+ if (path.type == TOKEN_ERROR) fail(path.start);
124+ advance_token();
125+
126+ char canonical[PATH_MAX];
127+ char message[256];
128+ if (!import_resolve_path(path.start, path.length, current_file_dir, project_root_dir,
129+ canonical, message, sizeof(message))) {
130+ fail(message);
131+ }
132+
133+ expect(TOKEN_SEMICOLON, "expected ';' after import path");
134+
135+ // Cycle check MUST come before the dedup check: a file already in
136+ // imported_files but STILL on resolving_stack is mid-parse (its
137+ // registry entry was added before recursing, same as every entry
138+ // here) -- that's a genuine cycle, not a finished diamond import.
139+ // Checking the registry first would silently treat a still-parsing
140+ // file as "already done" and swallow the cycle instead of reporting
141+ // it. Only a registry hit with NO matching resolving_stack entry is a
142+ // real, already-completed diamond dedup.
143+ if (resolving_stack_contains(canonical)) report_circular_import(canonical);
144+
145+ ImportedFile *existing = registry_find_by_path(canonical);
146+ if (existing != NULL) return; // diamond dedup -- already fully processed, nothing more to do
147+
148+ char ns[64];
149+ import_derive_namespace(canonical, ns, sizeof(ns));
150+
151+ ImportedFile *collision = registry_find_by_namespace(ns);
152+ if (collision != NULL) {
153+ // Sized to comfortably fit two PATH_MAX-bounded paths plus the
154+ // namespace and surrounding literal text with no truncation, so
155+ // -Wformat-truncation can prove this snprintf never overflows.
156+ char msg[2 * PATH_MAX + 128];
157+ snprintf(msg, sizeof(msg), "namespace '%s' already used by %s (colliding with %s)",
158+ ns, collision->canonical_path, canonical);
159+ fail(msg);
160+ }
161+
162+ if (imported_file_count >= MAX_VM_IMPORTS) fail("too many imported files");
163+ ImportedFile *entry = &imported_files[imported_file_count++];
164+ size_t canonical_len = strlen(canonical);
165+ if (canonical_len >= sizeof(entry->canonical_path)) canonical_len = sizeof(entry->canonical_path) - 1;
166+ memcpy(entry->canonical_path, canonical, canonical_len);
167+ entry->canonical_path[canonical_len] = '\0';
168+ size_t ns_len = strlen(ns);
169+ if (ns_len >= sizeof(entry->namespace_name)) ns_len = sizeof(entry->namespace_name) - 1;
170+ memcpy(entry->namespace_name, ns, ns_len);
171+ entry->namespace_name[ns_len] = '\0';
172+
173+ if (resolving_depth >= MAX_VM_IMPORT_DEPTH) fail("import nesting too deep");
174+ size_t stack_len = strlen(canonical);
175+ if (stack_len >= sizeof(resolving_stack[0])) stack_len = sizeof(resolving_stack[0]) - 1;
176+ memcpy(resolving_stack[resolving_depth], canonical, stack_len);
177+ resolving_stack[resolving_depth][stack_len] = '\0';
178+ resolving_depth++;
179+
180+ // --- Save everything the recursive parse of the imported file will
181+ // clobber, mirroring compiler/state.c's own save-recurse-restore
182+ // pattern for locals (the C call stack gives correct nesting for
183+ // free, same as those). ---
184+ DiagnosticsSource saved_diag = diagnostics_save_source();
185+ LexerState saved_lexer = lexer_save_state();
186+ Token saved_current = current;
187+ Token saved_previous = previous;
188+ char saved_file_dir[PATH_MAX];
189+ size_t saved_len = strlen(current_file_dir);
190+ if (saved_len >= sizeof(saved_file_dir)) saved_len = sizeof(saved_file_dir) - 1;
191+ memcpy(saved_file_dir, current_file_dir, saved_len);
192+ saved_file_dir[saved_len] = '\0';
193+ const char *saved_ns; int saved_ns_len;
194+ get_import_namespace(&saved_ns, &saved_ns_len);
195+ int saved_import_block_open = import_block_open;
196+
197+ char *imported_source = read_import_file(canonical);
198+ diagnostics_set_source(imported_source, canonical);
199+ import_path_dirname(canonical, current_file_dir, sizeof(current_file_dir));
200+ set_import_namespace(entry->namespace_name, (int)strlen(entry->namespace_name));
201+
202+ lexer_init(imported_source);
203+ advance_token();
204+ parse_imported_file_body();
205+
206+ // --- Restore, in the reverse order, so the importing file's parse
207+ // resumes exactly as if the @import line had been a no-op cursor-wise. ---
208+ set_import_namespace(saved_ns, saved_ns_len);
209+ size_t restore_len = strlen(saved_file_dir);
210+ if (restore_len >= sizeof(current_file_dir)) restore_len = sizeof(current_file_dir) - 1;
211+ memcpy(current_file_dir, saved_file_dir, restore_len);
212+ current_file_dir[restore_len] = '\0';
213+ lexer_restore_state(saved_lexer);
214+ diagnostics_restore_source(saved_diag);
215+ current = saved_current;
216+ previous = saved_previous;
217+ import_block_open = saved_import_block_open;
218+
219+ resolving_depth--;
220+}

ashvm/src/compiler/stmt_import.hadded

+8−0
@@ -0,0 +1,8 @@
1+#ifndef ASH_VM_COMPILER_STMT_IMPORT_H
2+#define ASH_VM_COMPILER_STMT_IMPORT_H
3+
4+void imports_init(const char *entry_file_path);
5+void compile_import_stmt(void); // assumes statement() already consumed TOKEN_IMPORT
6+void imports_close_block(void);
7+
8+#endif

ashvm/src/diagnostics/diagnostics.cadded

+126−0
@@ -0,0 +1,126 @@
1+#define _POSIX_C_SOURCE 200809L
2+#include <stdio.h>
3+#include <string.h>
4+#include <unistd.h>
5+#include "diagnostics/diagnostics.h"
6+
7+static const char *g_source = NULL;
8+static const char *g_filename = "source";
9+
10+#define COL_RED "\033[38;5;203m"
11+#define COL_CYAN "\033[38;5;117m"
12+#define COL_GRAY "\033[38;5;242m"
13+#define COL_GREEN "\033[38;5;150m"
14+#define COL_RESET "\033[0m"
15+#define COL_BOLD "\033[1m"
16+
17+void diagnostics_set_source(const char *source, const char *filename) {
18+ g_source = source;
19+ g_filename = filename;
20+}
21+
22+DiagnosticsSource diagnostics_save_source(void) {
23+ DiagnosticsSource saved;
24+ saved.source = g_source;
25+ saved.filename = g_filename;
26+ return saved;
27+}
28+
29+void diagnostics_restore_source(DiagnosticsSource saved) {
30+ g_source = saved.source;
31+ g_filename = saved.filename;
32+}
33+
34+static int use_color(void) {
35+ return isatty(fileno(stderr));
36+}
37+
38+// Computes the TRUE line/column by scanning the original source buffer from
39+// the start, rather than trusting any resettable per-stage line counter.
40+static void compute_line_col(const char *pos, int *line, int *col) {
41+ int l = 1, c = 1;
42+ const char *p = g_source;
43+ while (p < pos && *p) {
44+ if (*p == '\n') { l++; c = 1; } else { c++; }
45+ p++;
46+ }
47+ *line = l;
48+ *col = c;
49+}
50+
51+static void get_source_line(const char *pos, const char **line_start, int *line_len) {
52+ const char *start = pos;
53+ while (start > g_source && start[-1] != '\n') start--;
54+ const char *end = pos;
55+ while (*end && *end != '\n') end++;
56+ *line_start = start;
57+ *line_len = (int)(end - start);
58+}
59+
60+static int has_prefix(const char *message, const char *prefix) {
61+ return strncmp(message, prefix, strlen(prefix)) == 0;
62+}
63+
64+static const char *lookup_hint(const char *message) {
65+ if (has_prefix(message, "undefined variable")) return "did you forget to declare it with `let`?";
66+ if (has_prefix(message, "undefined function")) return "check the function name is spelled correctly";
67+ if (has_prefix(message, "index out of bounds")) return "check the array's length before indexing";
68+ if (has_prefix(message, "key not found")) return "use `has(map, key)` to check before accessing";
69+ if (has_prefix(message, "could not open file")) return "check the file path is correct";
70+ if (has_prefix(message, "type error")) return "check the value's type before using it this way";
71+ if (has_prefix(message, "could not resolve")) return "check the import path is correct and the file exists";
72+ if (has_prefix(message, "circular import")) return "break the cycle by removing one of the imports in the chain shown above";
73+ if (has_prefix(message, "namespace")) return "give one of the colliding files/libraries a different name, or move one out of the way";
74+ if (strstr(message, "expected") != NULL) return "check for a missing token nearby, like `;`, `)`, or `}`";
75+ return NULL;
76+}
77+
78+void diagnostics_report(const char *label, const char *message, const char *pos, int len) {
79+ int color = use_color();
80+ if (len <= 0) len = 1;
81+
82+ if (!g_source || !pos) {
83+ fprintf(stderr, "%s: %s\n", label, message);
84+ return;
85+ }
86+
87+ int line, col;
88+ compute_line_col(pos, &line, &col);
89+ const char *line_start; int line_len;
90+ get_source_line(pos, &line_start, &line_len);
91+
92+ char linenum_buf[16];
93+ snprintf(linenum_buf, sizeof(linenum_buf), "%d", line);
94+ int gutter_width = (int)strlen(linenum_buf);
95+
96+ if (color) {
97+ fprintf(stderr, COL_BOLD COL_RED "%s" COL_RESET COL_BOLD ": %s" COL_RESET "\n", label, message);
98+ fprintf(stderr, COL_CYAN " --> %s:%d:%d" COL_RESET "\n", g_filename, line, col);
99+ fprintf(stderr, COL_GRAY "%*s |" COL_RESET "\n", gutter_width, "");
100+ fprintf(stderr, COL_GRAY "%s |" COL_RESET " %.*s\n", linenum_buf, line_len, line_start);
101+ fprintf(stderr, COL_GRAY "%*s |" COL_RESET " ", gutter_width, "");
102+ for (int i = 1; i < col; i++) fputc(' ', stderr);
103+ fprintf(stderr, COL_RED);
104+ for (int i = 0; i < len; i++) fputc('^', stderr);
105+ fprintf(stderr, COL_RESET "\n");
106+ const char *hint = lookup_hint(message);
107+ if (hint) {
108+ fprintf(stderr, COL_GRAY "%*s |" COL_RESET "\n", gutter_width, "");
109+ fprintf(stderr, " = " COL_GREEN COL_BOLD "help" COL_RESET ": %s\n", hint);
110+ }
111+ } else {
112+ fprintf(stderr, "%s: %s\n", label, message);
113+ fprintf(stderr, " --> %s:%d:%d\n", g_filename, line, col);
114+ fprintf(stderr, "%*s |\n", gutter_width, "");
115+ fprintf(stderr, "%s | %.*s\n", linenum_buf, line_len, line_start);
116+ fprintf(stderr, "%*s | ", gutter_width, "");
117+ for (int i = 1; i < col; i++) fputc(' ', stderr);
118+ for (int i = 0; i < len; i++) fputc('^', stderr);
119+ fputc('\n', stderr);
120+ const char *hint = lookup_hint(message);
121+ if (hint) {
122+ fprintf(stderr, "%*s |\n", gutter_width, "");
123+ fprintf(stderr, " = help: %s\n", hint);
124+ }
125+ }
126+}

ashvm/src/diagnostics/diagnostics.hadded

+26−0
@@ -0,0 +1,26 @@
1+#ifndef ASH_DIAGNOSTICS_H
2+#define ASH_DIAGNOSTICS_H
3+
4+// Rust-style colorized error reporting, shared by every stage (compile-time
5+// and runtime) that needs to point at a spot in the original source text.
6+
7+void diagnostics_set_source(const char *source, const char *filename);
8+
9+// pos/len locate the offending span in the source buffer passed to
10+// diagnostics_set_source(); len <= 0 is treated as a single-character span.
11+void diagnostics_report(const char *label, const char *message,
12+ const char *pos, int len);
13+
14+typedef struct {
15+ const char *source;
16+ const char *filename;
17+} DiagnosticsSource;
18+
19+// Saves the currently-active (source, filename) pair, so stmt_import.c
20+// can point diagnostics at an imported file's own buffer while
21+// recursively parsing it, then restore the importing file's pair
22+// afterward -- mirrors lexer.h's LexerState save/restore.
23+DiagnosticsSource diagnostics_save_source(void);
24+void diagnostics_restore_source(DiagnosticsSource saved);
25+
26+#endif

ashvm/src/lexer/lexer.cadded

+172−0
@@ -0,0 +1,172 @@
1+#include <string.h>
2+#include <ctype.h>
3+#include "lexer/lexer.h"
4+#include "lexer/lexer_import.h"
5+
6+// lex_* (not the bare start/current/line these had as file-static
7+// before) -- shared with lexer_import.c now, and a plain `current` would
8+// collide with compiler/state.h's own global `Token current` at link time.
9+const char *lex_start;
10+const char *lex_current;
11+int lex_line;
12+
13+void lexer_init(const char *source) {
14+ lex_start = source;
15+ lex_current = source;
16+ lex_line = 1;
17+}
18+
19+int is_at_end(void) { return *lex_current == '\0'; }
20+char advance(void) { lex_current++; return lex_current[-1]; }
21+char peek(void) { return *lex_current; }
22+static char peek_next(void) { if (is_at_end()) return '\0'; return lex_current[1]; }
23+
24+static int match(char expected) {
25+ if (is_at_end()) return 0;
26+ if (*lex_current != expected) return 0;
27+ lex_current++;
28+ return 1;
29+}
30+
31+static void skip_whitespace(void) {
32+ for (;;) {
33+ char c = peek();
34+ switch (c) {
35+ case ' ': case '\r': case '\t':
36+ advance();
37+ break;
38+ case '\n':
39+ lex_line++;
40+ advance();
41+ break;
42+ case '/':
43+ if (peek_next() == '/') {
44+ while (peek() != '\n' && !is_at_end()) advance();
45+ } else {
46+ return;
47+ }
48+ break;
49+ default:
50+ return;
51+ }
52+ }
53+}
54+
55+static Token make_token(TokenType type) {
56+ Token token;
57+ token.type = type;
58+ token.start = lex_start;
59+ token.length = (int)(lex_current - lex_start);
60+ token.line = lex_line;
61+ return token;
62+}
63+
64+Token error_token(const char *message) {
65+ Token token;
66+ token.type = TOKEN_ERROR;
67+ token.start = message;
68+ token.length = (int)strlen(message);
69+ token.line = lex_line;
70+ return token;
71+}
72+
73+static Token number(void) {
74+ while (isdigit(peek())) advance();
75+ if (peek() == '.' && isdigit(peek_next())) {
76+ advance(); // consume '.'
77+ while (isdigit(peek())) advance();
78+ }
79+ return make_token(TOKEN_NUMBER);
80+}
81+
82+static Token string_token(void) {
83+ const char *content_start = lex_current;
84+ while (peek() != '"' && !is_at_end()) {
85+ if (peek() == '\\' && peek_next() != '\0') {
86+ advance(); // skip the backslash
87+ advance(); // skip the escaped character itself, so a \" doesn't end the string early
88+ continue;
89+ }
90+ if (peek() == '\n') lex_line++;
91+ advance();
92+ }
93+ if (is_at_end()) return error_token("unterminated string");
94+
95+ int length = (int)(lex_current - content_start);
96+ advance();
97+
98+ Token token;
99+ token.type = TOKEN_STRING;
100+ token.start = content_start;
101+ token.length = length;
102+ token.line = lex_line;
103+ return token;
104+}
105+
106+static Token identifier(void) {
107+ while (isalnum(peek()) || peek() == '_') advance();
108+
109+ 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);
116+ 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);
121+
122+ return make_token(TOKEN_IDENTIFIER);
123+}
124+
125+Token lexer_next_token(void) {
126+ skip_whitespace();
127+ lex_start = lex_current;
128+
129+ if (is_at_end()) return make_token(TOKEN_EOF);
130+
131+ char c = advance();
132+
133+ if (isdigit(c)) return number();
134+ if (isalpha(c) || c == '_') return identifier();
135+ if (c == '"') return string_token();
136+
137+ switch (c) {
138+ case '.': return make_token(TOKEN_DOT);
139+ case '@':
140+ if (strncmp(lex_current, "import", 6) == 0 && !(isalnum((unsigned char)lex_current[6]) || lex_current[6] == '_')) {
141+ lex_current += 6;
142+ return make_token(TOKEN_IMPORT);
143+ }
144+ return error_token("expected 'import' after '@'");
145+ case ';': return make_token(TOKEN_SEMICOLON);
146+ case ',': return make_token(TOKEN_COMMA);
147+ 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);
152+ case '%': return make_token(TOKEN_PERCENT);
153+ case '(': return make_token(TOKEN_LPAREN);
154+ case ')': return make_token(TOKEN_RPAREN);
155+ case '{': return make_token(TOKEN_LBRACE);
156+ case '}': return make_token(TOKEN_RBRACE);
157+ case '[': return make_token(TOKEN_LBRACKET);
158+ case ']': return make_token(TOKEN_RBRACKET);
159+ 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);
168+ return error_token("unexpected character");
169+ }
170+
171+ return error_token("unexpected character");
172+}

ashvm/src/lexer/lexer.hadded

+70−0
@@ -0,0 +1,70 @@
1+#ifndef LEXER_H
2+#define LEXER_H
3+
4+typedef enum {
5+ TOKEN_PRINT,
6+ TOKEN_LET,
7+ TOKEN_IF,
8+ TOKEN_ELSE,
9+ TOKEN_WHILE,
10+ TOKEN_FOR,
11+ TOKEN_IN,
12+ TOKEN_IMPORT,
13+ TOKEN_FN,
14+ TOKEN_RETURN,
15+ TOKEN_TRY,
16+ TOKEN_CATCH,
17+ TOKEN_IDENTIFIER,
18+ TOKEN_STRING,
19+ TOKEN_IMPORT_PATH,
20+ TOKEN_EQUAL,
21+ TOKEN_EQUAL_EQUAL,
22+ TOKEN_BANG_EQUAL,
23+ TOKEN_BANG,
24+ TOKEN_AND,
25+ TOKEN_OR,
26+ TOKEN_LESS,
27+ TOKEN_LESS_EQUAL,
28+ TOKEN_GREATER,
29+ TOKEN_GREATER_EQUAL,
30+ TOKEN_PLUS,
31+ TOKEN_MINUS,
32+ TOKEN_STAR,
33+ TOKEN_SLASH,
34+ TOKEN_PERCENT,
35+ TOKEN_LPAREN,
36+ TOKEN_RPAREN,
37+ TOKEN_LBRACE,
38+ TOKEN_RBRACE,
39+ TOKEN_LBRACKET,
40+ TOKEN_RBRACKET,
41+ TOKEN_COMMA,
42+ TOKEN_COLON,
43+ TOKEN_DOT,
44+ TOKEN_NUMBER,
45+ TOKEN_SEMICOLON,
46+ TOKEN_EOF,
47+ TOKEN_ERROR
48+} TokenType;
49+
50+typedef struct {
51+ TokenType type;
52+ const char *start;
53+ int length;
54+ int line;
55+} Token;
56+
57+void lexer_init(const char *source);
58+Token lexer_next_token(void);
59+
60+typedef struct {
61+ const char *start;
62+ const char *current;
63+ int line;
64+} LexerState;
65+
66+LexerState lexer_save_state(void);
67+void lexer_restore_state(LexerState state);
68+Token lexer_scan_import_path(void);
69+
70+#endif

ashvm/src/lexer/lexer_import.cadded

+37−0
@@ -0,0 +1,37 @@
1+#include "lexer/lexer_import.h"
2+
3+LexerState lexer_save_state(void) {
4+ LexerState state;
5+ state.start = lex_start;
6+ state.current = lex_current;
7+ state.line = lex_line;
8+ return state;
9+}
10+
11+void lexer_restore_state(LexerState state) {
12+ lex_start = state.start;
13+ lex_current = state.current;
14+ lex_line = state.line;
15+}
16+
17+// Called with the lexer's internal `current` pointer sitting immediately
18+// after the '<' the parser just confirmed (via current.type==TOKEN_LESS)
19+// WITHOUT calling advance_token() on it -- so `current` here still points
20+// at the first path character, untouched by normal tokenization. Scans
21+// raw bytes up to '>', consuming the '>' too, so the parser's next
22+// advance_token() call correctly picks up whatever follows it (';').
23+Token lexer_scan_import_path(void) {
24+ const char *content_start = lex_current;
25+ while (peek() != '>' && peek() != '\n' && !is_at_end()) advance();
26+ if (peek() != '>') return error_token("unterminated import path (expected '>')");
27+
28+ int length = (int)(lex_current - content_start);
29+ advance(); // consume '>'
30+
31+ Token token;
32+ token.type = TOKEN_IMPORT_PATH;
33+ token.start = content_start;
34+ token.length = length;
35+ token.line = lex_line;
36+ return token;
37+}

ashvm/src/lexer/lexer_import.hadded

+29−0
@@ -0,0 +1,29 @@
1+#ifndef ASHVM_LEXER_IMPORT_H
2+#define ASHVM_LEXER_IMPORT_H
3+#include "lexer/lexer.h"
4+
5+// Cross-file link between lexer.c (the tokenizer's cursor state and its
6+// low-level scan primitives) and lexer_import.c (state save/restore plus
7+// import-path scanning, split out here to keep lexer.c under the
8+// project's ~200-line guideline) -- not part of the public API. The
9+// public entry points the two files implement together
10+// (lexer_save_state/lexer_restore_state/lexer_scan_import_path) stay
11+// declared in lexer.h exactly as before, so callers outside the lexer
12+// directory don't need to change anything. Mirrors kiln's identical split
13+// between lexer.h (public surface) and lexer_import.h (private,
14+// cross-file plumbing).
15+//
16+// Named lex_* rather than the bare start/current/line: compiler/state.h
17+// already declares its own global `Token current`, and since these now
18+// need external linkage to be shared across the two .c files, a second
19+// plain `current` global here would collide with it at link time.
20+extern const char *lex_start;
21+extern const char *lex_current;
22+extern int lex_line;
23+
24+int is_at_end(void);
25+char advance(void);
26+char peek(void);
27+Token error_token(const char *message);
28+
29+#endif

ashvm/src/main.cadded

+112−0
@@ -0,0 +1,112 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include <setjmp.h>
5+#include "compiler/compiler.h"
6+#include "vm/vm.h"
7+#include "diagnostics/diagnostics.h"
8+
9+static char *read_file(const char *path) {
10+ FILE *file = fopen(path, "rb");
11+ if (!file) { fprintf(stderr, "could not open file: %s\n", path); exit(1); }
12+ fseek(file, 0L, SEEK_END);
13+ long size = ftell(file);
14+ rewind(file);
15+ char *buffer = malloc(size + 1);
16+ fread(buffer, 1, size, file);
17+ buffer[size] = '\0';
18+ fclose(file);
19+ return buffer;
20+}
21+
22+static void count_braces(const char *line, int *depth, int *in_string) {
23+ for (const char *p = line; *p; p++) {
24+ if (*in_string) {
25+ if (*p == '\\' && p[1] != '\0') { p++; continue; }
26+ if (*p == '"') *in_string = 0;
27+ continue;
28+ }
29+ if (*p == '"') { *in_string = 1; continue; }
30+ if (*p == '{') (*depth)++;
31+ if (*p == '}') (*depth)--;
32+ }
33+}
34+
35+static int is_blank(const char *s) {
36+ for (; *s; s++) if (*s != ' ' && *s != '\t' && *s != '\n' && *s != '\r') return 0;
37+ return 1;
38+}
39+
40+static jmp_buf repl_recovery;
41+
42+static void run_repl(void) {
43+ static Chunk repl_chunk;
44+ chunk_init(&repl_chunk);
45+ vm_set_repl_recovery(&repl_recovery);
46+
47+ printf("Ash REPL (ashvm) -- variables and functions persist between lines.\n");
48+ printf("Numeric/string/array/map only (no closures over REPL-level captures across lines).\n");
49+ printf("Type 'exit' to quit. Runtime errors are caught automatically; syntax errors will end the session.\n\n");
50+
51+ int first_call = 1;
52+ char buffer[16384];
53+
54+ for (;;) {
55+ buffer[0] = '\0';
56+ int depth = 0;
57+ int in_string = 0;
58+ int has_content = 0;
59+
60+ for (;;) {
61+ printf(depth == 0 ? "ashvm> " : "... ");
62+ fflush(stdout);
63+
64+ char line[2048];
65+ if (!fgets(line, sizeof(line), stdin)) { printf("\n"); return; }
66+
67+ if (depth == 0 && !has_content) {
68+ char trimmed[2048];
69+ strncpy(trimmed, line, sizeof(trimmed) - 1);
70+ trimmed[sizeof(trimmed) - 1] = '\0';
71+ size_t l = strlen(trimmed);
72+ while (l > 0 && (trimmed[l-1] == '\n' || trimmed[l-1] == '\r')) trimmed[--l] = '\0';
73+ if (strcmp(trimmed, "exit") == 0) return;
74+ }
75+
76+ if (!is_blank(line)) has_content = 1;
77+ count_braces(line, &depth, &in_string);
78+
79+ strncat(buffer, line, sizeof(buffer) - strlen(buffer) - 1);
80+
81+ if (depth <= 0) { depth = 0; break; }
82+ }
83+
84+ if (!has_content) continue;
85+
86+ int this_is_first = first_call;
87+ first_call = 0;
88+
89+ if (setjmp(repl_recovery) != 0) {
90+ continue;
91+ }
92+
93+ int start_offset;
94+ compile_repl_line(buffer, &repl_chunk, &start_offset);
95+
96+ vm_repl_checkpoint();
97+ vm_run_from(&repl_chunk, start_offset, this_is_first);
98+ }
99+}
100+
101+int main(int argc, char *argv[]) {
102+ if (argc < 2) {
103+ run_repl();
104+ return 0;
105+ }
106+ char *source = read_file(argv[1]);
107+ diagnostics_set_source(source, argv[1]);
108+ Chunk *main_chunk = compile(source, argv[1]);
109+ vm_run(main_chunk);
110+ free(source);
111+ return 0;
112+}

ashvm/src/value/hashmap.cadded

+78−0
@@ -0,0 +1,78 @@
1+#include <stdlib.h>
2+#include <string.h>
3+#include "value/hashmap.h"
4+
5+static unsigned long hash_string(const char *s) {
6+ unsigned long h = 5381;
7+ int c;
8+ while ((c = *s++)) h = ((h << 5) + h) + (unsigned long)c;
9+ return h;
10+}
11+
12+VMMap *vm_map_new(void) {
13+ VMMap *m = malloc(sizeof(VMMap));
14+ m->capacity = 8;
15+ m->count = 0;
16+ m->entries = calloc(m->capacity, sizeof(VMMapEntry));
17+ return m;
18+}
19+
20+static void vm_map_resize(VMMap *m) {
21+ int old_capacity = m->capacity;
22+ VMMapEntry *old_entries = m->entries;
23+ m->capacity *= 2;
24+ m->entries = calloc(m->capacity, sizeof(VMMapEntry));
25+ m->count = 0;
26+ for (int i = 0; i < old_capacity; i++) {
27+ if (old_entries[i].used) vm_map_set(m, old_entries[i].key, old_entries[i].value);
28+ }
29+ free(old_entries);
30+}
31+
32+void vm_map_set(VMMap *m, char *key, VMValue value) {
33+ if ((double)(m->count + 1) / m->capacity > 0.7) vm_map_resize(m);
34+ unsigned long h = hash_string(key) % (unsigned long)m->capacity;
35+ while (m->entries[h].used) {
36+ if (strcmp(m->entries[h].key, key) == 0) {
37+ m->entries[h].value = value;
38+ free(key);
39+ return;
40+ }
41+ h = (h + 1) % (unsigned long)m->capacity;
42+ }
43+ m->entries[h].key = key;
44+ m->entries[h].value = value;
45+ m->entries[h].used = 1;
46+ m->count++;
47+}
48+
49+int vm_map_get(VMMap *m, const char *key, VMValue *out) {
50+ unsigned long h = hash_string(key) % (unsigned long)m->capacity;
51+ unsigned long start = h;
52+ while (m->entries[h].used) {
53+ if (strcmp(m->entries[h].key, key) == 0) { *out = m->entries[h].value; return 1; }
54+ h = (h + 1) % (unsigned long)m->capacity;
55+ if (h == start) break;
56+ }
57+ return 0;
58+}
59+
60+int vm_map_has(VMMap *m, const char *key) {
61+ VMValue tmp;
62+ return vm_map_get(m, key, &tmp);
63+}
64+
65+void vm_map_delete(VMMap *m, const char *key) {
66+ unsigned long h = hash_string(key) % (unsigned long)m->capacity;
67+ unsigned long start = h;
68+ while (m->entries[h].used) {
69+ if (strcmp(m->entries[h].key, key) == 0) {
70+ free(m->entries[h].key);
71+ m->entries[h].used = 0;
72+ m->count--;
73+ return;
74+ }
75+ h = (h + 1) % (unsigned long)m->capacity;
76+ if (h == start) break;
77+ }
78+}

ashvm/src/value/hashmap.hadded

+23−0
@@ -0,0 +1,23 @@
1+#ifndef ASH_VM_HASHMAP_H
2+#define ASH_VM_HASHMAP_H
3+#include "value/value.h"
4+
5+typedef struct {
6+ char *key;
7+ VMValue value;
8+ int used;
9+} VMMapEntry;
10+
11+struct VMMap {
12+ VMMapEntry *entries;
13+ int count;
14+ int capacity;
15+};
16+
17+VMMap *vm_map_new(void);
18+void vm_map_set(VMMap *m, char *key, VMValue value);
19+int vm_map_get(VMMap *m, const char *key, VMValue *out);
20+int vm_map_has(VMMap *m, const char *key);
21+void vm_map_delete(VMMap *m, const char *key);
22+
23+#endif

ashvm/src/value/value.cadded

+111−0
@@ -0,0 +1,111 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "value/value.h"
5+#include "value/hashmap.h"
6+
7+VMValue vm_num(double n) {
8+ VMValue v; v.type = VM_NUM; v.number = n; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
9+}
10+VMValue vm_str(char *s) {
11+ VMValue v; v.type = VM_STR; v.number = 0; v.str = s; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
12+}
13+VMValue vm_array_val(VMArray *a) {
14+ VMValue v; v.type = VM_ARRAY; v.number = 0; v.str = NULL; v.array = a; v.map = NULL; v.closure = NULL; return v;
15+}
16+VMValue vm_map_val(VMMap *m) {
17+ VMValue v; v.type = VM_MAP; v.number = 0; v.str = NULL; v.array = NULL; v.map = m; v.closure = NULL; return v;
18+}
19+VMValue vm_function_val(int function_index) {
20+ VMValue v; v.type = VM_FUNCTION; v.number = function_index; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = NULL; return v;
21+}
22+VMValue vm_closure_val(VMClosureObj *c) {
23+ VMValue v; v.type = VM_CLOSURE; v.number = 0; v.str = NULL; v.array = NULL; v.map = NULL; v.closure = c; return v;
24+}
25+
26+char *vm_copy_string_escaped(const char *start, int length) {
27+ char *buf = malloc(length + 1);
28+ int out = 0;
29+ for (int i = 0; i < length; i++) {
30+ if (start[i] == '\\' && i + 1 < length) {
31+ char next = start[i + 1];
32+ char c;
33+ switch (next) {
34+ case 'n': c = '\n'; break;
35+ case 't': c = '\t'; break;
36+ case 'r': c = '\r'; break;
37+ case '"': c = '"'; break;
38+ case '\\': c = '\\'; break;
39+ default: c = next; break;
40+ }
41+ buf[out++] = c;
42+ i++;
43+ } else {
44+ buf[out++] = start[i];
45+ }
46+ }
47+ buf[out] = '\0';
48+ return buf;
49+}
50+
51+char *vm_concat_strings(const char *a, const char *b) {
52+ int la = (int)strlen(a);
53+ int lb = (int)strlen(b);
54+ char *buf = malloc(la + lb + 1);
55+ memcpy(buf, a, la);
56+ memcpy(buf + la, b, lb);
57+ buf[la + lb] = '\0';
58+ return buf;
59+}
60+
61+VMArray *vm_array_new(void) {
62+ VMArray *arr = malloc(sizeof(VMArray));
63+ arr->items = NULL;
64+ arr->count = 0;
65+ arr->capacity = 0;
66+ return arr;
67+}
68+
69+void vm_array_push(VMArray *arr, VMValue v) {
70+ if (arr->count >= arr->capacity) {
71+ arr->capacity = arr->capacity == 0 ? 4 : arr->capacity * 2;
72+ arr->items = realloc(arr->items, sizeof(VMValue) * arr->capacity);
73+ }
74+ arr->items[arr->count++] = v;
75+}
76+
77+void vm_print_value(VMValue v) {
78+ if (v.type == VM_NUM) {
79+ if (v.number == (long long)v.number) printf("%lld", (long long)v.number);
80+ else printf("%g", v.number);
81+ } else if (v.type == VM_STR) {
82+ printf("%s", v.str);
83+ } else if (v.type == VM_FUNCTION) {
84+ printf("<function>");
85+ } else if (v.type == VM_CLOSURE) {
86+ printf("<closure>");
87+ } else if (v.type == VM_ARRAY) {
88+ printf("[");
89+ for (int i = 0; i < v.array->count; i++) {
90+ VMValue item = v.array->items[i];
91+ if (item.type == VM_STR) printf("\"%s\"", item.str);
92+ else vm_print_value(item);
93+ if (i < v.array->count - 1) printf(", ");
94+ }
95+ printf("]");
96+ } else if (v.type == VM_MAP) {
97+ printf("{");
98+ int first = 1;
99+ for (int i = 0; i < v.map->capacity; i++) {
100+ if (v.map->entries[i].used) {
101+ if (!first) printf(", ");
102+ printf("\"%s\": ", v.map->entries[i].key);
103+ VMValue val = v.map->entries[i].value;
104+ if (val.type == VM_STR) printf("\"%s\"", val.str);
105+ else vm_print_value(val);
106+ first = 0;
107+ }
108+ }
109+ printf("}");
110+ }
111+}

ashvm/src/value/value.hadded

+45−0
@@ -0,0 +1,45 @@
1+#ifndef ASH_VM_VALUE_H
2+#define ASH_VM_VALUE_H
3+#include <stdint.h>
4+
5+typedef enum { VM_NUM, VM_STR, VM_ARRAY, VM_MAP, VM_FUNCTION, VM_CLOSURE } VMType;
6+
7+typedef struct VMArray {
8+ struct VMValue *items;
9+ int count;
10+ int capacity;
11+} VMArray;
12+
13+typedef struct VMMap VMMap;
14+
15+typedef struct VMClosureObj {
16+ int function_index;
17+ int capture_count;
18+ struct VMValue *captured;
19+} VMClosureObj;
20+
21+typedef struct VMValue {
22+ VMType type;
23+ double number;
24+ char *str;
25+ VMArray *array;
26+ VMMap *map;
27+ VMClosureObj *closure;
28+} VMValue;
29+
30+VMValue vm_num(double n);
31+VMValue vm_str(char *s);
32+VMValue vm_array_val(VMArray *a);
33+VMValue vm_map_val(VMMap *m);
34+VMValue vm_function_val(int function_index);
35+VMValue vm_closure_val(VMClosureObj *c);
36+
37+char *vm_copy_string_escaped(const char *start, int length);
38+char *vm_concat_strings(const char *a, const char *b);
39+
40+VMArray *vm_array_new(void);
41+void vm_array_push(VMArray *arr, VMValue v);
42+
43+void vm_print_value(VMValue v);
44+
45+#endif

ashvm/src/vm/api.cadded

+70−0
@@ -0,0 +1,70 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include "vm/vm.h"
4+#include "vm/state.h"
5+#include "compiler/compiler.h"
6+
7+void vm_set_repl_recovery(jmp_buf *jb) {
8+ repl_recovery_jmp = jb;
9+}
10+
11+void vm_repl_checkpoint(void) {
12+ repl_saved_frame_count = frame_count;
13+ repl_saved_stack_top = stack_top;
14+}
15+
16+void vm_run(Chunk *main_chunk) {
17+ stack_top = stack;
18+ frame_count = 1;
19+ frames[0].chunk = main_chunk;
20+ frames[0].ip = main_chunk->code;
21+ frames[0].slots = stack;
22+ vm_execute(0);
23+}
24+
25+// Runs bytecode starting at start_offset within the SAME persistent chunk,
26+// used by the REPL: on the first call, resets the stack/frames like vm_run;
27+// on later calls, keeps the existing stack/frames (so variables persist) and
28+// just resumes execution at the newly-appended bytecode's start.
29+void vm_run_from(Chunk *chunk, int start_offset, int is_first_call) {
30+ if (is_first_call) {
31+ stack_top = stack;
32+ frame_count = 1;
33+ frames[0].chunk = chunk;
34+ frames[0].slots = stack;
35+ }
36+ frames[0].ip = chunk->code + start_offset;
37+ vm_execute(0);
38+}
39+
40+VMValue vm_call_function_sync(int func_idx, VMValue *args, int argc) {
41+ for (int i = 0; i < argc; i++) *stack_top++ = args[i];
42+ int caller_frame_count = frame_count;
43+ VMFunction *fn = &vm_functions[func_idx];
44+ CallFrame *volatile newf = &frames[frame_count++];
45+ newf->chunk = &fn->chunk;
46+ newf->ip = fn->chunk.code;
47+ newf->slots = stack_top - argc;
48+ vm_execute(caller_frame_count);
49+ return *--stack_top;
50+}
51+
52+VMValue vm_call_closure_sync(VMClosureObj *c, VMValue *args, int argc) {
53+ for (int i = 0; i < argc; i++) *stack_top++ = args[i];
54+ for (int i = 0; i < c->capture_count; i++) *stack_top++ = c->captured[i];
55+ int caller_frame_count = frame_count;
56+ VMFunction *fn = &vm_functions[c->function_index];
57+ CallFrame *volatile newf = &frames[frame_count++];
58+ newf->chunk = &fn->chunk;
59+ newf->ip = fn->chunk.code;
60+ newf->slots = stack_top - argc - c->capture_count;
61+ vm_execute(caller_frame_count);
62+ return *--stack_top;
63+}
64+
65+VMValue vm_call_callable_sync(VMValue callable, VMValue *args, int argc) {
66+ if (callable.type == VM_FUNCTION) return vm_call_function_sync((int)callable.number, args, argc);
67+ if (callable.type == VM_CLOSURE) return vm_call_closure_sync(callable.closure, args, argc);
68+ fprintf(stderr, "not callable\n");
69+ exit(1);
70+}

ashvm/src/vm/chunk.cadded

+42−0
@@ -0,0 +1,42 @@
1+#include <stdlib.h>
2+#include "vm/chunk.h"
3+
4+void chunk_init(Chunk *c) {
5+ c->count = 0; c->capacity = 0; c->code = NULL;
6+ c->pos = NULL; c->poslen = NULL;
7+ c->const_count = 0; c->const_capacity = 0; c->constants = NULL;
8+}
9+
10+static void chunk_grow(Chunk *c) {
11+ if (c->count >= c->capacity) {
12+ c->capacity = c->capacity == 0 ? 64 : c->capacity * 2;
13+ c->code = realloc(c->code, c->capacity);
14+ c->pos = realloc(c->pos, c->capacity * sizeof(const char *));
15+ c->poslen = realloc(c->poslen, c->capacity * sizeof(int));
16+ }
17+}
18+
19+int chunk_write(Chunk *c, uint8_t byte) {
20+ chunk_grow(c);
21+ c->code[c->count] = byte;
22+ c->pos[c->count] = NULL;
23+ c->poslen[c->count] = 0;
24+ return c->count++;
25+}
26+
27+int chunk_write_pos(Chunk *c, uint8_t byte, const char *pos, int len) {
28+ chunk_grow(c);
29+ c->code[c->count] = byte;
30+ c->pos[c->count] = pos;
31+ c->poslen[c->count] = len;
32+ return c->count++;
33+}
34+
35+int chunk_add_constant(Chunk *c, VMValue value) {
36+ if (c->const_count >= c->const_capacity) {
37+ c->const_capacity = c->const_capacity == 0 ? 16 : c->const_capacity * 2;
38+ c->constants = realloc(c->constants, c->const_capacity * sizeof(VMValue));
39+ }
40+ c->constants[c->const_count] = value;
41+ return c->const_count++;
42+}

ashvm/src/vm/chunk.hadded

+42−0
@@ -0,0 +1,42 @@
1+#ifndef ASH_VM_CHUNK_H
2+#define ASH_VM_CHUNK_H
3+#include <stdint.h>
4+#include "value/value.h"
5+
6+typedef enum {
7+ OP_CONST,
8+ OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, OP_NEG,
9+ OP_NOT, OP_EQ, OP_NEQ, OP_LT, OP_LE, OP_GT, OP_GE,
10+ OP_AND, OP_OR,
11+ OP_PRINT, OP_POP,
12+ OP_GET_LOCAL, OP_SET_LOCAL,
13+ OP_JUMP, OP_JUMP_IF_FALSE, OP_LOOP,
14+ OP_CALL, OP_RETURN,
15+ OP_ACC_LOCAL,
16+ OP_CMP_JUMP,
17+ OP_ARRAY,
18+ OP_INDEX_GET,
19+ OP_INDEX_SET,
20+ OP_MAP,
21+ OP_CALL_VALUE,
22+ OP_CALL_BUILTIN,
23+ OP_MAKE_CLOSURE,
24+ OP_TRY_PUSH,
25+ OP_TRY_POP
26+} OpCode;
27+
28+typedef struct {
29+ int count, capacity;
30+ uint8_t *code;
31+ const char **pos; // parallel to code; populated only at opcode-start bytes
32+ int *poslen;
33+ int const_count, const_capacity;
34+ VMValue *constants;
35+} Chunk;
36+
37+void chunk_init(Chunk *c);
38+int chunk_write(Chunk *c, uint8_t byte);
39+int chunk_write_pos(Chunk *c, uint8_t byte, const char *pos, int len);
40+int chunk_add_constant(Chunk *c, VMValue value);
41+
42+#endif

ashvm/src/vm/dispatch.cadded

+295−0
@@ -0,0 +1,295 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include <setjmp.h>
5+#include <math.h>
6+#include "vm/vm.h"
7+#include "vm/state.h"
8+#include "compiler/compiler.h"
9+#include "builtins/builtins.h"
10+#include "value/hashmap.h"
11+
12+// The dispatch loop stays one function despite exceeding this project's
13+// usual 200-line target: computed-goto (`&&label` / `goto *table[i]`)
14+// requires every label to live in the same function scope, so splitting it
15+// by opcode group isn't possible without giving up the dispatch technique
16+// that makes this VM fast in the first place. Everything that CAN live
17+// outside it (error handling, the public call/run API, shared state) does,
18+// in vm_errors.c / vm_api.c / vm_state.c.
19+void vm_execute(int stop_at_frame_count) {
20+ CallFrame *volatile frame = &frames[frame_count - 1];
21+
22+ static void *dispatch_table[] = {
23+ &&do_CONST, &&do_ADD, &&do_SUB, &&do_MUL, &&do_DIV, &&do_MOD, &&do_NEG,
24+ &&do_NOT, &&do_EQ, &&do_NEQ, &&do_LT, &&do_LE, &&do_GT, &&do_GE,
25+ &&do_AND, &&do_OR,
26+ &&do_PRINT, &&do_POP,
27+ &&do_GET_LOCAL, &&do_SET_LOCAL,
28+ &&do_JUMP, &&do_JUMP_IF_FALSE, &&do_LOOP,
29+ &&do_CALL, &&do_RETURN,
30+ &&do_ACC_LOCAL, &&do_CMP_JUMP,
31+ &&do_ARRAY, &&do_INDEX_GET, &&do_INDEX_SET,
32+ &&do_MAP, &&do_CALL_VALUE, &&do_CALL_BUILTIN,
33+ &&do_MAKE_CLOSURE,
34+ &&do_TRY_PUSH, &&do_TRY_POP
35+ };
36+
37+ #define DISPATCH() \
38+ do { \
39+ if (frame_count == 1 && frame->ip >= frame->chunk->code + frame->chunk->count) return; \
40+ size_t __instr_off = (size_t)(frame->ip - frame->chunk->code); \
41+ current_pos = frame->chunk->pos[__instr_off]; \
42+ current_poslen = frame->chunk->poslen[__instr_off]; \
43+ goto *dispatch_table[*frame->ip++]; \
44+ } while (0)
45+
46+ DISPATCH();
47+
48+ do_CONST: { uint8_t idx = *frame->ip++; *stack_top++ = frame->chunk->constants[idx]; DISPATCH(); }
49+ do_ADD: {
50+ VMValue b = *--stack_top;
51+ VMValue *a = stack_top - 1;
52+ if (a->type == VM_STR && b.type == VM_STR) *a = vm_str(vm_concat_strings(a->str, b.str));
53+ else *a = vm_num(require_num(*a, "'+'") + require_num(b, "'+'"));
54+ DISPATCH();
55+ }
56+ do_SUB: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "'-'") - require_num(b, "'-'")); DISPATCH(); }
57+ do_MUL: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "'*'") * require_num(b, "'*'")); DISPATCH(); }
58+ do_DIV: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "'/'") / require_num(b, "'/'")); DISPATCH(); }
59+ do_MOD: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(fmod(require_num(*a, "'%'"), require_num(b, "'%'"))); DISPATCH(); }
60+ do_NEG: { stack_top[-1] = vm_num(-require_num(stack_top[-1], "unary '-'")); DISPATCH(); }
61+ do_NOT: { stack_top[-1] = vm_num(require_num(stack_top[-1], "'!'") == 0 ? 1 : 0); DISPATCH(); }
62+ do_EQ: {
63+ VMValue b = *--stack_top; VMValue *a = stack_top - 1;
64+ int eq;
65+ if (a->type == VM_STR && b.type == VM_STR) eq = strcmp(a->str, b.str) == 0;
66+ else if (a->type == VM_NUM && b.type == VM_NUM) eq = a->number == b.number;
67+ else eq = 0;
68+ *a = vm_num(eq);
69+ DISPATCH();
70+ }
71+ do_NEQ: {
72+ VMValue b = *--stack_top; VMValue *a = stack_top - 1;
73+ int eq;
74+ if (a->type == VM_STR && b.type == VM_STR) eq = strcmp(a->str, b.str) == 0;
75+ else if (a->type == VM_NUM && b.type == VM_NUM) eq = a->number == b.number;
76+ else eq = 0;
77+ *a = vm_num(!eq);
78+ DISPATCH();
79+ }
80+ do_LT: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") < require_num(b, "comparison")); DISPATCH(); }
81+ do_LE: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") <= require_num(b, "comparison")); DISPATCH(); }
82+ do_GT: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") > require_num(b, "comparison")); DISPATCH(); }
83+ do_GE: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num(require_num(*a, "comparison") >= require_num(b, "comparison")); DISPATCH(); }
84+ do_AND: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((truthy(*a, "'&&'") && truthy(b, "'&&'")) ? 1 : 0); DISPATCH(); }
85+ do_OR: { VMValue b = *--stack_top; VMValue *a = stack_top - 1; *a = vm_num((truthy(*a, "'||'") || truthy(b, "'||'")) ? 1 : 0); DISPATCH(); }
86+ do_PRINT: { VMValue v = *--stack_top; vm_print_value(v); printf("\n"); DISPATCH(); }
87+ do_POP: { --stack_top; DISPATCH(); }
88+ do_GET_LOCAL: { uint8_t slot = *frame->ip++; *stack_top++ = frame->slots[slot]; DISPATCH(); }
89+ do_SET_LOCAL: { uint8_t slot = *frame->ip++; frame->slots[slot] = stack_top[-1]; DISPATCH(); }
90+ do_JUMP: {
91+ uint16_t off = (uint16_t)((frame->ip[0] << 8) | frame->ip[1]);
92+ frame->ip += 2; frame->ip += off; DISPATCH();
93+ }
94+ do_JUMP_IF_FALSE: {
95+ uint16_t off = (uint16_t)((frame->ip[0] << 8) | frame->ip[1]);
96+ frame->ip += 2;
97+ VMValue cond = *--stack_top;
98+ if (!truthy(cond, "condition")) frame->ip += off;
99+ DISPATCH();
100+ }
101+ do_LOOP: {
102+ uint16_t off = (uint16_t)((frame->ip[0] << 8) | frame->ip[1]);
103+ frame->ip += 2; frame->ip -= off; DISPATCH();
104+ }
105+ do_CALL: {
106+ uint8_t func_idx = *frame->ip++;
107+ uint8_t argc = *frame->ip++;
108+ VMFunction *fn = &vm_functions[func_idx];
109+ CallFrame *volatile newf = &frames[frame_count++];
110+ newf->chunk = &fn->chunk;
111+ newf->ip = fn->chunk.code;
112+ newf->slots = stack_top - argc;
113+ frame = newf;
114+ DISPATCH();
115+ }
116+ do_CALL_VALUE: {
117+ uint8_t argc = *frame->ip++;
118+ VMValue callable = *--stack_top;
119+ VMFunction *fn;
120+ int extra = 0;
121+ if (callable.type == VM_FUNCTION) {
122+ fn = &vm_functions[(int)callable.number];
123+ } else if (callable.type == VM_CLOSURE) {
124+ fn = &vm_functions[callable.closure->function_index];
125+ extra = callable.closure->capture_count;
126+ for (int i = 0; i < extra; i++) *stack_top++ = callable.closure->captured[i];
127+ } else {
128+ type_error("call");
129+ fn = NULL;
130+ }
131+ CallFrame *volatile newf = &frames[frame_count++];
132+ newf->chunk = &fn->chunk;
133+ newf->ip = fn->chunk.code;
134+ newf->slots = stack_top - argc - extra;
135+ frame = newf;
136+ DISPATCH();
137+ }
138+ do_CALL_BUILTIN: {
139+ uint8_t builtin_id = *frame->ip++;
140+ uint8_t argc = *frame->ip++;
141+ VMValue args[16];
142+ for (int i = argc - 1; i >= 0; i--) args[i] = *--stack_top;
143+ *stack_top++ = vm_call_builtin(builtin_id, args, argc);
144+ DISPATCH();
145+ }
146+ do_RETURN: {
147+ VMValue result = *--stack_top;
148+ stack_top = frames[frame_count - 1].slots;
149+ frame_count--;
150+ *stack_top++ = result;
151+ frame = &frames[frame_count - 1];
152+ if (stop_at_frame_count != 0 && frame_count == stop_at_frame_count) return;
153+ DISPATCH();
154+ }
155+ do_ACC_LOCAL: {
156+ uint8_t slot_a = *frame->ip++;
157+ uint8_t flags = *frame->ip++;
158+ uint8_t b_operand = *frame->ip++;
159+ VMValue b = (flags & 0x80) ? frame->chunk->constants[b_operand] : frame->slots[b_operand];
160+ int op = flags & 0x03;
161+ VMValue *a = &frame->slots[slot_a];
162+ if (op == 0) {
163+ if (a->type == VM_STR && b.type == VM_STR) *a = vm_str(vm_concat_strings(a->str, b.str));
164+ else *a = vm_num(require_num(*a, "'+'") + require_num(b, "'+'"));
165+ } else if (op == 1) {
166+ *a = vm_num(require_num(*a, "'-'") - require_num(b, "'-'"));
167+ } else {
168+ *a = vm_num(require_num(*a, "'*'") * require_num(b, "'*'"));
169+ }
170+ DISPATCH();
171+ }
172+ do_CMP_JUMP: {
173+ uint8_t slot_a = *frame->ip++;
174+ uint8_t flags = *frame->ip++;
175+ uint8_t b_operand = *frame->ip++;
176+ uint16_t off = (uint16_t)((frame->ip[0] << 8) | frame->ip[1]);
177+ frame->ip += 2;
178+ VMValue a = frame->slots[slot_a];
179+ VMValue b = (flags & 0x80) ? frame->chunk->constants[b_operand] : frame->slots[b_operand];
180+ int cmp_code = flags & 0x0F;
181+ int taken;
182+ if (cmp_code == 4 || cmp_code == 5) {
183+ int eq;
184+ if (a.type == VM_STR && b.type == VM_STR) eq = strcmp(a.str, b.str) == 0;
185+ else if (a.type == VM_NUM && b.type == VM_NUM) eq = a.number == b.number;
186+ else eq = 0;
187+ taken = (cmp_code == 4) ? eq : !eq;
188+ } else {
189+ double av = require_num(a, "comparison");
190+ double bv = require_num(b, "comparison");
191+ switch (cmp_code) {
192+ case 0: taken = av < bv; break;
193+ case 1: taken = av <= bv; break;
194+ case 2: taken = av > bv; break;
195+ default: taken = av >= bv; break;
196+ }
197+ }
198+ if (!taken) frame->ip += off;
199+ DISPATCH();
200+ }
201+ do_ARRAY: {
202+ uint8_t n = *frame->ip++;
203+ VMArray *arr = vm_array_new();
204+ for (int i = 0; i < n; i++) vm_array_push(arr, stack_top[-n + i]);
205+ stack_top -= n;
206+ *stack_top++ = vm_array_val(arr);
207+ DISPATCH();
208+ }
209+ do_INDEX_GET: {
210+ VMValue idx = *--stack_top;
211+ VMValue arr = *--stack_top;
212+ if (arr.type == VM_ARRAY) {
213+ int64_t i = (int64_t)require_num(idx, "array index");
214+ if (i < 0 || i >= arr.array->count) vm_runtime_error("index out of bounds: %lld", (long long)i);
215+ *stack_top++ = arr.array->items[i];
216+ } else if (arr.type == VM_MAP) {
217+ if (idx.type != VM_STR) vm_runtime_error("map keys must be strings");
218+ VMValue out;
219+ if (!vm_map_get(arr.map, idx.str, &out)) vm_runtime_error("key not found: %s", idx.str);
220+ *stack_top++ = out;
221+ } else {
222+ type_error("indexing");
223+ }
224+ DISPATCH();
225+ }
226+ do_INDEX_SET: {
227+ VMValue value = *--stack_top;
228+ VMValue idx = *--stack_top;
229+ VMValue arr = *--stack_top;
230+ if (arr.type == VM_ARRAY) {
231+ int64_t i = (int64_t)require_num(idx, "array index");
232+ if (i < 0 || i >= arr.array->count) vm_runtime_error("index out of bounds: %lld", (long long)i);
233+ arr.array->items[i] = value;
234+ } else if (arr.type == VM_MAP) {
235+ if (idx.type != VM_STR) vm_runtime_error("map keys must be strings");
236+ char *key_copy = malloc(strlen(idx.str) + 1); strcpy(key_copy, idx.str);
237+ vm_map_set(arr.map, key_copy, value);
238+ } else {
239+ type_error("index-assign");
240+ }
241+ DISPATCH();
242+ }
243+ do_MAP: {
244+ uint8_t n = *frame->ip++;
245+ VMMap *m = vm_map_new();
246+ for (int i = 0; i < n; i++) {
247+ VMValue key = stack_top[-(2 * n) + 2 * i];
248+ VMValue val = stack_top[-(2 * n) + 2 * i + 1];
249+ char *key_copy = malloc(strlen(key.str) + 1); strcpy(key_copy, key.str);
250+ vm_map_set(m, key_copy, val);
251+ }
252+ stack_top -= 2 * n;
253+ *stack_top++ = vm_map_val(m);
254+ DISPATCH();
255+ }
256+ do_MAKE_CLOSURE: {
257+ uint8_t fn_idx = *frame->ip++;
258+ uint8_t cap_count = *frame->ip++;
259+ VMClosureObj *c = malloc(sizeof(VMClosureObj));
260+ c->function_index = fn_idx;
261+ c->capture_count = cap_count;
262+ c->captured = malloc(sizeof(VMValue) * (cap_count > 0 ? cap_count : 1));
263+ for (int i = 0; i < cap_count; i++) c->captured[i] = frame->slots[i];
264+ *stack_top++ = vm_closure_val(c);
265+ DISPATCH();
266+ }
267+ do_TRY_PUSH: {
268+ uint16_t catch_offset = (uint16_t)((frame->ip[0] << 8) | frame->ip[1]);
269+ frame->ip += 2;
270+ if (try_depth >= MAX_TRY_DEPTH) { fprintf(stderr, "too many nested try blocks\n"); exit(1); }
271+ TryHandler *h = &try_handlers[try_depth];
272+ h->saved_frame_count = frame_count;
273+ h->saved_stack_top = stack_top;
274+ h->target_frame = frame;
275+ h->target_ip = frame->ip + catch_offset;
276+ try_depth++;
277+ if (setjmp(h->jmp) == 0) {
278+ DISPATCH();
279+ } else {
280+ try_depth--;
281+ frame_count = h->saved_frame_count;
282+ stack_top = h->saved_stack_top;
283+ frame = h->target_frame;
284+ 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);
288+ DISPATCH();
289+ }
290+ }
291+ do_TRY_POP: {
292+ try_depth--;
293+ DISPATCH();
294+ }
295+}

ashvm/src/vm/errors.cadded

+42−0
@@ -0,0 +1,42 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <stdarg.h>
4+#include "vm/vm.h"
5+#include "vm/state.h"
6+#include "diagnostics/diagnostics.h"
7+
8+// Every genuinely-runtime error (as opposed to a compile-time/structural one,
9+// which stays a hard crash since ashvm resolves names at compile time) funnels
10+// through here. If a try block is active, longjmp back to its handler instead
11+// of crashing the whole program.
12+void vm_runtime_error(const char *fmt, ...) {
13+ va_list args;
14+ va_start(args, fmt);
15+ vsnprintf(error_message, sizeof(error_message), fmt, args);
16+ va_end(args);
17+
18+ if (try_depth > 0) {
19+ longjmp(try_handlers[try_depth - 1].jmp, 1);
20+ } else if (repl_recovery_jmp != NULL) {
21+ diagnostics_report("error", error_message, current_pos, current_poslen);
22+ frame_count = repl_saved_frame_count;
23+ stack_top = repl_saved_stack_top;
24+ longjmp(*repl_recovery_jmp, 1);
25+ } else {
26+ diagnostics_report("error", error_message, current_pos, current_poslen);
27+ exit(1);
28+ }
29+}
30+
31+void type_error(const char *context) {
32+ vm_runtime_error("type error: invalid operand type in %s", context);
33+}
34+
35+double require_num(VMValue v, const char *context) {
36+ if (v.type != VM_NUM) type_error(context);
37+ return v.number;
38+}
39+
40+int truthy(VMValue v, const char *context) {
41+ return require_num(v, context) != 0;
42+}

ashvm/src/vm/state.cadded

+18−0
@@ -0,0 +1,18 @@
1+#include <stddef.h>
2+#include "vm/state.h"
3+
4+VMValue stack[STACK_MAX];
5+VMValue *stack_top;
6+CallFrame frames[FRAMES_MAX];
7+int frame_count;
8+
9+TryHandler try_handlers[MAX_TRY_DEPTH];
10+int try_depth = 0;
11+char error_message[512];
12+
13+jmp_buf *repl_recovery_jmp = NULL;
14+int repl_saved_frame_count;
15+VMValue *repl_saved_stack_top;
16+
17+const char *current_pos = NULL;
18+int current_poslen = 0;

ashvm/src/vm/state.hadded

+55−0
@@ -0,0 +1,55 @@
1+#ifndef ASH_VM_STATE_H
2+#define ASH_VM_STATE_H
3+#include <setjmp.h>
4+#include "vm/chunk.h"
5+
6+// Internal VM state shared between vm_dispatch.c, vm_errors.c, and vm_api.c
7+// -- not part of the public API (vm.h). Mirrors the same
8+// declare-storage-once, share-via-header pattern ashc used to use for its
9+// parser state (see rundown.md).
10+
11+#define STACK_MAX 65536
12+#define FRAMES_MAX 256
13+#define MAX_TRY_DEPTH 32
14+
15+typedef struct {
16+ Chunk *chunk;
17+ uint8_t *ip;
18+ VMValue *slots;
19+} CallFrame;
20+
21+typedef struct {
22+ jmp_buf jmp;
23+ int saved_frame_count;
24+ VMValue *saved_stack_top;
25+ CallFrame *target_frame;
26+ uint8_t *target_ip;
27+} TryHandler;
28+
29+extern VMValue stack[STACK_MAX];
30+extern VMValue *stack_top;
31+extern CallFrame frames[FRAMES_MAX];
32+extern int frame_count;
33+
34+extern TryHandler try_handlers[MAX_TRY_DEPTH];
35+extern int try_depth;
36+extern char error_message[512];
37+
38+extern jmp_buf *repl_recovery_jmp;
39+extern int repl_saved_frame_count;
40+extern VMValue *repl_saved_stack_top;
41+
42+// Source position of the instruction currently executing, captured once per
43+// DISPATCH() (the single point every instruction passes through) so
44+// vm_runtime_error() can point diagnostics at the right spot without every
45+// opcode handler having to thread it through.
46+extern const char *current_pos;
47+extern int current_poslen;
48+
49+void type_error(const char *context) __attribute__((noreturn));
50+double require_num(VMValue v, const char *context);
51+int truthy(VMValue v, const char *context);
52+
53+void vm_execute(int stop_at_frame_count);
54+
55+#endif

ashvm/src/vm/vm.hadded

+16−0
@@ -0,0 +1,16 @@
1+#ifndef ASH_VM_VM_H
2+#define ASH_VM_VM_H
3+#include <setjmp.h>
4+#include "vm/chunk.h"
5+
6+void vm_run(Chunk *main_chunk);
7+void vm_run_from(Chunk *chunk, int start_offset, int is_first_call);
8+void vm_set_repl_recovery(jmp_buf *jb);
9+void vm_repl_checkpoint(void);
10+
11+VMValue vm_call_function_sync(int func_idx, VMValue *args, int argc);
12+VMValue vm_call_closure_sync(VMClosureObj *c, VMValue *args, int argc);
13+VMValue vm_call_callable_sync(VMValue callable, VMValue *args, int argc);
14+void vm_runtime_error(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));
15+
16+#endif

ashvm/tests/ash.libs/testlib/testlib.ashadded

+5−0
@@ -0,0 +1,5 @@
1+let VERSION = "1.0";
2+
3+fn square(x) {
4+ return x * x;
5+}

ashvm/tests/closures_test.ashadded

+21−0
@@ -0,0 +1,21 @@
1+fn make_adder(x) {
2+ return fn(y) { return x + y; };
3+}
4+
5+let add5 = make_adder(5);
6+let add10 = make_adder(10);
7+print add5(3);
8+print add10(3);
9+
10+let nums = [1, 2, 3];
11+let shift100 = make_adder(100);
12+let shifted = map(nums, shift100);
13+print shifted;
14+
15+fn compose(f, g) {
16+ return fn(x) { return f(g(x)); };
17+}
18+fn double(x) { return x * 2; }
19+fn inc(x) { return x + 1; }
20+let doubleThenInc = compose(inc, double);
21+print doubleThenInc(5);

ashvm/tests/error_handling_test.ashadded

+57−0
@@ -0,0 +1,57 @@
1+fn risky(n) {
2+ if (n > 3) {
3+ let undefined_thing = does_not_exist + 1;
4+ }
5+ return n;
6+}
7+
8+let result = 0;
9+try {
10+ let result = risky(5);
11+ print "should not reach here";
12+} catch (e) {
13+ print "caught: " + e;
14+}
15+print "after try 1, result still: " + str(result);
16+
17+let arr = [1, 2, 3];
18+try {
19+ print arr[10];
20+} catch (e) {
21+ print "caught: " + e;
22+}
23+
24+let m = {"a": 1};
25+try {
26+ print m["missing_key"];
27+} catch (e) {
28+ print "caught: " + e;
29+}
30+
31+fn needs_two(a, b) { return a + b; }
32+try {
33+ print needs_two(1);
34+} catch (e) {
35+ print "caught: " + e;
36+}
37+
38+try {
39+ let content = read_file("/tmp/does_not_exist_xyz_ash.txt");
40+} catch (e) {
41+ print "caught: " + e;
42+}
43+
44+try {
45+ try {
46+ let x = totally_undefined_var;
47+ } catch (inner) {
48+ print "inner caught: " + inner;
49+ }
50+ print "inner try/catch resolved, continuing outer try";
51+} catch (outer) {
52+ print "outer should not see this: " + outer;
53+}
54+
55+print "program completed normally";
56+let final_check = 42;
57+print final_check;

ashvm/tests/escapes_test.ashadded

+9−0
@@ -0,0 +1,9 @@
1+print "line one\nline two";
2+print "tab\there";
3+print "quote: \"hello\"";
4+print "backslash: \\";
5+write_file("/tmp/ash_escape_test.txt", "Hello from Ash!\n");
6+let content = read_file("/tmp/ash_escape_test.txt");
7+print content;
8+append_file("/tmp/ash_escape_test.txt", "Second line\n");
9+print read_file("/tmp/ash_escape_test.txt");

ashvm/tests/fixtures/collision_dir_one/shared.ashadded

+3−0
@@ -0,0 +1,3 @@
1+fn fromOne() {
2+ return 1;
3+}

ashvm/tests/fixtures/collision_dir_two/shared.ashadded

+3−0
@@ -0,0 +1,3 @@
1+fn fromTwo() {
2+ return 2;
3+}

ashvm/tests/fixtures/imports_basic_lib.ashadded

+7−0
@@ -0,0 +1,7 @@
1+fn helper(x) {
2+ return x * 2;
3+}
4+
5+fn add(a, b) {
6+ return a + b + helper(1);
7+}

ashvm/tests/fixtures/imports_circular_a.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_circular_b.ash>;
2+
3+fn fromA() {
4+ return 1;
5+}

ashvm/tests/fixtures/imports_circular_b.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_circular_a.ash>;
2+
3+fn fromB() {
4+ return 2;
5+}

ashvm/tests/fixtures/imports_collision_main.ashadded

+4−0
@@ -0,0 +1,4 @@
1+@import <./collision_dir_one/shared.ash>;
2+@import <./collision_dir_two/shared.ash>;
3+
4+print shared.fromOne();

ashvm/tests/fixtures/imports_const_lib.ashadded

+6−0
@@ -0,0 +1,6 @@
1+let PI = 3.14159;
2+let GREETING = "hello from lib";
3+
4+fn circleArea(r) {
5+ return PI * r * r;
6+}

ashvm/tests/fixtures/imports_diamond_b.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_diamond_d.ash>;
2+
3+fn fromB() {
4+ return imports_diamond_d.base() + 1;
5+}

ashvm/tests/fixtures/imports_diamond_c.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_diamond_d.ash>;
2+
3+fn fromC() {
4+ return imports_diamond_d.base() + 2;
5+}

ashvm/tests/fixtures/imports_diamond_d.ashadded

+3−0
@@ -0,0 +1,3 @@
1+fn base() {
2+ return 100;
3+}

ashvm/tests/functional_test.ashadded

+20−0
@@ -0,0 +1,20 @@
1+fn double(x) { return x * 2; }
2+fn is_even(x) { return x % 2 == 0; }
3+
4+let nums = [1, 2, 3, 4, 5, 6];
5+
6+let doubled = map(nums, double);
7+print doubled;
8+
9+let evens = filter(nums, is_even);
10+print evens;
11+
12+let total = reduce(nums, fn(acc, x) { return acc + x; }, 0);
13+print total;
14+
15+let squareLambda = fn(x) { return x * x; };
16+let squares = map(nums, squareLambda);
17+print squares;
18+
19+let f = double;
20+print f(21);

ashvm/tests/imports_basic_test.ashadded

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

ashvm/tests/imports_circular_check.shadded

+23−0
@@ -0,0 +1,23 @@
1+#!/usr/bin/env bash
2+set -uo pipefail
3+cd "$(dirname "$0")/.."
4+
5+OUT=$(../bin/ashvm tests/fixtures/imports_circular_a.ash 2>&1)
6+STATUS=$?
7+
8+if [[ $STATUS -eq 0 ]]; then
9+ echo "FAIL: expected nonzero exit, got 0"
10+ exit 1
11+fi
12+if [[ "$OUT" != *"circular import"* ]]; then
13+ echo "FAIL: expected 'circular import' in output, got:"
14+ echo "$OUT"
15+ exit 1
16+fi
17+if [[ "$OUT" != *"imports_circular_a.ash"* || "$OUT" != *"imports_circular_b.ash"* ]]; then
18+ echo "FAIL: expected both file names in the cycle message, got:"
19+ echo "$OUT"
20+ exit 1
21+fi
22+
23+echo "OK: imports_circular_check"

ashvm/tests/imports_collision_check.shadded

+23−0
@@ -0,0 +1,23 @@
1+#!/usr/bin/env bash
2+set -uo pipefail
3+cd "$(dirname "$0")/.."
4+
5+OUT=$(../bin/ashvm tests/fixtures/imports_collision_main.ash 2>&1)
6+STATUS=$?
7+
8+if [[ $STATUS -eq 0 ]]; then
9+ echo "FAIL: expected nonzero exit, got 0"
10+ exit 1
11+fi
12+if [[ "$OUT" != *"namespace"* ]]; then
13+ echo "FAIL: expected 'namespace' in output, got:"
14+ echo "$OUT"
15+ exit 1
16+fi
17+if [[ "$OUT" != *"collision_dir_one/shared.ash"* || "$OUT" != *"collision_dir_two/shared.ash"* ]]; then
18+ echo "FAIL: expected BOTH colliding resolved paths in the message, got:"
19+ echo "$OUT"
20+ exit 1
21+fi
22+
23+echo "OK: imports_collision_check"

ashvm/tests/imports_const_test.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./fixtures/imports_const_lib.ash>;
2+
3+print imports_const_lib.PI;
4+print imports_const_lib.GREETING;
5+print imports_const_lib.circleArea(2);

ashvm/tests/imports_diamond_test.ashadded

+6−0
@@ -0,0 +1,6 @@
1+@import <./fixtures/imports_diamond_b.ash>;
2+@import <./fixtures/imports_diamond_c.ash>;
3+
4+print imports_diamond_b.fromB();
5+print imports_diamond_c.fromC();
6+print imports_diamond_d.base();

ashvm/tests/imports_lib_test.ashadded

+4−0
@@ -0,0 +1,4 @@
1+@import <testlib>;
2+
3+print testlib.VERSION;
4+print testlib.square(6);

ashvm/tests/maps_test.ashadded

+22−0
@@ -0,0 +1,22 @@
1+let person = {"name": "Lennox", "age": 20};
2+print person;
3+print person["name"];
4+print person["age"];
5+
6+person["age"] = 21;
7+print person["age"];
8+
9+person["city"] = "Luebeck";
10+print person;
11+
12+print keys(person);
13+print values(person);
14+print has(person, "name");
15+print has(person, "zip");
16+
17+delete(person, "city");
18+print person;
19+
20+let empty = {};
21+print empty;
22+print len(person);

ashvm/tests/run_regression.shadded

+60−0
@@ -0,0 +1,60 @@
1+#!/usr/bin/env bash
2+# ashvm/tests/run_regression.sh baseline|check
3+# Compiles+runs every ashvm/tests/*.ash and diffs stdout against a
4+# captured baseline -- mirrors kiln/tests/run_regression.sh. ashvm has no
5+# --target/--link flags and no input()-driven test today, so this is
6+# simpler than kiln's version.
7+set -uo pipefail
8+cd "$(dirname "$0")/.." # -> ashvm/
9+
10+MODE="${1:-check}"
11+if [[ "$MODE" != "baseline" && "$MODE" != "check" ]]; then
12+ echo "usage: $0 baseline|check" >&2
13+ exit 2
14+fi
15+
16+OUTDIR="/tmp/ashvm_regression/$MODE"
17+rm -rf "$OUTDIR"
18+mkdir -p "$OUTDIR"
19+
20+rm -f /tmp/ash_test.txt
21+
22+make -s || { echo "build failed"; exit 1; }
23+
24+for f in tests/*.ash; do
25+ name=$(basename "$f" .ash)
26+ out="$OUTDIR/${name}.out"
27+ if ! ../bin/ashvm "$f" > "$out" 2>&1; then
28+ echo "RUN FAIL" >> "$out"
29+ fi
30+done
31+
32+if [[ "$MODE" == "baseline" ]]; then
33+ echo "baseline captured in $OUTDIR"
34+ exit 0
35+fi
36+
37+fail=0
38+for f in "$OUTDIR"/*.out; do
39+ name=$(basename "$f" .out)
40+ base="/tmp/ashvm_regression/baseline/${name}.out"
41+ if [[ ! -f "$base" ]]; then
42+ echo "NEW: $name (no baseline to compare)"
43+ continue
44+ fi
45+ if diff -q "$base" "$f" > /dev/null; then
46+ echo "OK: $name"
47+ else
48+ echo "DIFF: $name"
49+ diff "$base" "$f"
50+ fail=1
51+ fi
52+done
53+if ! ./tests/imports_circular_check.sh; then
54+ fail=1
55+fi
56+if ! ./tests/imports_collision_check.sh; then
57+ fail=1
58+fi
59+
60+exit $fail

ashvm/tests/strings_files_test.ashadded

+19−0
@@ -0,0 +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 ");
11+
12+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;
16+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");

ashvm/tests/vm_arrays_test.ashadded

+23−0
@@ -0,0 +1,23 @@
1+let nums = [1, 2, 3, 4, 5];
2+print nums;
3+print nums[0];
4+print nums[4];
5+
6+nums[0] = 99;
7+print nums;
8+
9+let sum = 0;
10+let i = 0;
11+while (i < 5) {
12+ let sum = sum + nums[i];
13+ let i = i + 1;
14+}
15+print sum;
16+
17+fn first(arr) {
18+ return arr[0];
19+}
20+print first(nums);
21+
22+let names = ["Ash", "Sonnet"];
23+print names[1];

ashvm/tests/vm_closures_test.ashadded

+24−0
@@ -0,0 +1,24 @@
1+fn make_adder(x) {
2+ return fn(y) { return x + y; };
3+}
4+
5+let add5 = make_adder(5);
6+let add10 = make_adder(10);
7+print add5(3);
8+print add10(3);
9+
10+let nums = [1, 2, 3];
11+let shift100 = make_adder(100);
12+let shifted = map(nums, shift100);
13+print shifted;
14+
15+fn compose(f, g) {
16+ return fn(x) { return f(g(x)); };
17+}
18+fn double(x) { return x * 2; }
19+fn inc(x) { return x + 1; }
20+let doubleThenInc = compose(inc, double);
21+print doubleThenInc(5);
22+
23+let total = reduce(nums, fn(acc, n) { return acc + n; }, 0);
24+print total;

ashvm/tests/vm_full_test.ashadded

+39−0
@@ -0,0 +1,39 @@
1+let person = {"name": "Ash", "version": 2};
2+print person;
3+print person["name"];
4+person["version"] = 3;
5+print person;
6+print keys(person);
7+print values(person);
8+print has(person, "name");
9+delete(person, "version");
10+print person;
11+
12+fn double(x) { return x * 2; }
13+let f = double;
14+print f(21);
15+
16+let nums = [1, 2, 3, 4, 5];
17+let doubled = map(nums, double);
18+print doubled;
19+
20+fn isEven(x) { return x % 2 == 0; }
21+let evens = filter(nums, isEven);
22+print evens;
23+
24+fn add(a, b) { return a + b; }
25+let total = reduce(nums, add, 0);
26+print total;
27+
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");
36+
37+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");

ashvm/tests/vm_strings_test.ashadded

+12−0
@@ -0,0 +1,12 @@
1+let name = "Ash";
2+print "Hello, " + name + "!";
3+
4+let a = "abc";
5+let b = "abc";
6+print a == b;
7+print a == "xyz";
8+
9+fn greet(who) {
10+ return "Hi " + who;
11+}
12+print greet("world");

ashvm/tests/vm_trycatch_test.ashadded

+52−0
@@ -0,0 +1,52 @@
1+let arr = [1, 2, 3];
2+try {
3+ print arr[10];
4+} catch (e) {
5+ print "caught: " + e;
6+}
7+print "after array test";
8+
9+let m = {"a": 1};
10+try {
11+ print m["missing"];
12+} catch (e) {
13+ print "caught: " + e;
14+}
15+
16+try {
17+ let content = read_file("/tmp/nonexistent_ashvm_xyz.txt");
18+} catch (e) {
19+ print "caught: " + e;
20+}
21+
22+fn risky(n) {
23+ if (n > 3) {
24+ let arr2 = [1, 2];
25+ print arr2[99];
26+ }
27+ return n;
28+}
29+
30+try {
31+ let result = risky(5);
32+ print "should not print";
33+} catch (e) {
34+ print "caught in function: " + e;
35+}
36+print "still running after function error";
37+
38+try {
39+ try {
40+ let arr3 = [1];
41+ print arr3[5];
42+ } catch (inner) {
43+ print "inner caught: " + inner;
44+ }
45+ print "inner resolved";
46+} catch (outer) {
47+ print "outer should not fire";
48+}
49+
50+print "program completed";
51+let final = 42;
52+print final;

changes.shadded

+16−0
@@ -0,0 +1,16 @@
1+#!/usr/bin/env bash
2+
3+if [ -z "$1" ]; then
4+ echo "usage: ./changes.sh \"description of what changed\""
5+ exit 1
6+fi
7+
8+TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
9+
10+{
11+ echo "## $TIMESTAMP"
12+ echo "$1"
13+ echo ""
14+} >> CHANGELOG.md
15+
16+echo "logged to CHANGELOG.md"

kiln/Makefileadded

+73−0
@@ -0,0 +1,73 @@
1+CC = gcc
2+# -MMD -MP: emit a per-object .d file listing the headers it includes, so
3+# editing a shared header (e.g. vars.h) correctly triggers a rebuild of
4+# every .o that includes it, not just files whose own .c changed. Without
5+# this, make's only dependency is "the .c is newer than the .o" -- a
6+# header-only edit silently produces a binary built from mismatched
7+# offsets/constants across files, with no error, until something crashes
8+# at runtime for reasons the source doesn't explain.
9+CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -MMD -MP
10+LDFLAGS =
11+
12+KILN_SRC = src/main.c src/target.c src/lexer/lexer.c src/lexer/lexer_import.c \
13+ src/parser/parser.c src/parser/parser_control.c src/parser/vars.c src/parser/functions.c src/parser/try_catch.c src/parser/for_loop.c src/parser/loop_stack.c \
14+ src/parser/import_paths.c src/parser/imports.c src/parser/constants.c \
15+ src/codegen/emit.c src/codegen/emit_mem.c src/codegen/emit_sse.c src/codegen/heap.c src/codegen/bytes.c src/codegen/layout.c \
16+ src/codegen/strings.c src/codegen/errors.c src/codegen/arrays.c src/codegen/maps.c src/codegen/maps_mutate.c src/codegen/builtins.c \
17+ src/codegen/math_builtins.c src/codegen/str_builtin.c src/codegen/num_builtin.c \
18+ src/codegen/map_builtin.c src/codegen/filter_builtin.c src/codegen/reduce_builtin.c src/codegen/keys_values.c \
19+ src/codegen/string_alloc.c src/codegen/case_builtins.c src/codegen/trim_builtin.c \
20+ src/codegen/substring_builtin.c src/codegen/indexof_builtin.c \
21+ src/codegen/join_builtin.c src/codegen/split_builtin.c src/codegen/replace_builtin.c \
22+ src/codegen/file_path.c src/codegen/read_file_builtin.c src/codegen/write_file_builtins.c \
23+ src/codegen/input_builtin.c src/codegen/argv_builtin.c src/codegen/argv_builtin_windows.c \
24+ src/codegen/expr.c src/codegen/expr_call.c src/codegen/expr_bool.c src/codegen/print_int.c \
25+ src/codegen/closures.c src/codegen/lambda.c src/codegen/win_call.c src/codegen/win_file_call.c \
26+ src/codegen/platform_console.c src/codegen/platform_file.c \
27+ src/elf/elf_writer.c src/elf/elf_dynamic.c src/elf/elf_dynamic_call.c src/elf/elf_so_writer.c \
28+ src/pe/pe_writer.c src/pe/pe_dynamic.c src/pe/pe_dll_writer.c \
29+ src/diagnostics/diagnostics.c
30+KILN_OBJ = $(KILN_SRC:.c=.o)
31+KILN_DEP = $(KILN_SRC:.c=.d)
32+
33+BIN = ../bin/kiln
34+
35+all: $(BIN)
36+
37+$(BIN): $(KILN_OBJ)
38+ mkdir -p ../bin
39+ $(CC) $(CFLAGS) -o $(BIN) $(KILN_OBJ) $(LDFLAGS)
40+
41+-include $(KILN_DEP)
42+
43+clean:
44+ find src -name '*.o' -delete
45+ find src -name '*.d' -delete
46+ rm -f $(BIN)
47+
48+# Plan B: builds libkilnrt.so and installs it at the fixed path every
49+# --link=shared kiln-compiled program's DT_RPATH points at (see
50+# src/elf/elf_dynamic.h's KILN_RPATH) -- needs root to write there, so
51+# this is a separate, explicit, opt-in step, never run by `make all`.
52+KILNRT = ../bin/libkilnrt.so
53+KILN_RPATH_DIR = /usr/local/lib/kiln
54+
55+$(KILNRT): $(BIN)
56+ $(BIN) --emit-runtime -o $(KILNRT)
57+
58+install-runtime: $(KILNRT)
59+ install -d $(KILN_RPATH_DIR)
60+ install -m 0755 $(KILNRT) $(KILN_RPATH_DIR)/libkilnrt.so
61+
62+# Windows equivalent: no install step needed -- the standard DLL search
63+# order checks the launching .exe's own directory first, so
64+# libkilnrt.dll just needs to sit next to whatever --link=shared
65+# --target=windows binary is using it (or on PATH).
66+KILNRT_DLL = ../bin/libkilnrt.dll
67+
68+$(KILNRT_DLL): $(BIN)
69+ $(BIN) --emit-runtime -o $(KILNRT_DLL) --target=windows
70+
71+runtime-dll: $(KILNRT_DLL)
72+
73+.PHONY: all clean install-runtime runtime-dll

kiln/README.mdadded

+55−0
@@ -0,0 +1,55 @@
1+# Kiln
2+
3+a standalone `.ash` compiler - not an interpreter -
4+working on Windows and Linux (macOS best-effort), invoked like
5+`kiln example.ash -o example`, with its own linker that eventually shares
6+a common runtime library across compiled binaries instead of duplicating
7+it in every executable.
8+
9+Zero external tools anywhere in the pipeline: no gcc/clang/ld/as. Kiln
10+hand-encodes x86-64 machine code and writes its own ELF executables
11+directly, the same "every line hand-written" rule that built `ashc` and
12+`ashvm`.
13+
14+## Status: milestone 1 done (Linux only)
15+
16+`kiln file.ash -o out` compiles `print <expr>;` programs (integer literals,
17+`+ - * /`, unary `-`, parens, multiple statements) straight to a real,
18+standalone, freestanding ELF64 executable - no libc, raw syscalls only.
19+
20+```bash
21+cd kiln && make # builds ../bin/kiln
22+../bin/kiln tests/hello.ash -o /tmp/hello
23+/tmp/hello # prints 7 (1 + 2 * 3)
24+```
25+
26+## What's deliberately not built yet
27+
28+- Everything beyond numeric `print` 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
38+
39+## Layout
40+
41+```
42+kiln/
43+ Makefile builds ../bin/kiln
44+ src/
45+ lexer/ tokenizer (own copy, same shape as ashvm's)
46+ parser/ token state + statement grammar
47+ codegen/
48+ emit.c/.h raw x86-64 instruction encoding primitives
49+ expr.c/.h expression -> real arithmetic instructions
50+ print_int.c/.h int-to-decimal-ASCII + write(2) + exit(2)
51+ elf/
52+ elf_writer.c/.h minimal "tiny ELF" executable writer
53+ main.c argv parsing, drives the pipeline
54+ tests/ *.ash programs exercised by hand for now
55+```
\ No newline at end of file

kiln/src/codegen/argv_builtin.cadded

+114−0
@@ -0,0 +1,114 @@
1+#include <stdint.h>
2+#include "codegen/argv_builtin.h"
3+#include "codegen/argv_builtin_internal.h"
4+#include "codegen/string_alloc.h"
5+#include "codegen/bytes.h"
6+#include "codegen/heap.h"
7+#include "codegen/value.h"
8+#include "elf/elf_writer.h"
9+#include "parser/parser.h"
10+#include "parser/vars.h"
11+#include "target.h"
12+
13+static void codegen_builtin_argv_linux(void);
14+
15+void codegen_builtin_argv(void) {
16+ if (kiln_get_target() == KILN_TARGET_WINDOWS) { codegen_builtin_argv_windows(); return; }
17+ codegen_builtin_argv_linux();
18+}
19+
20+// Scratch layout: [0]=argv_base (points at the kernel's saved argc slot,
21+// so argv[i]'s pointer lives at argv_base+8+8*i) [8]=argc [16]=out_data_ptr
22+// [24]=i [32]=c_str (current arg's raw null-terminated pointer) [40]=length
23+static void codegen_builtin_argv_linux(void) {
24+ int32_t base = higher_order_scratch_offset();
25+
26+ emit_mov_reg_imm64(code, REG_RSI, KILN_ARGV_ADDR);
27+ emit_load_mem_disp32(code, REG_RSI, REG_RSI, 0); // RSI = saved original RSP (-> argc)
28+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 0); // argc
29+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RSI);
30+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RCX);
31+
32+ emit_mov_reg_reg(code, REG_RDI, REG_RCX);
33+ emit_mov_reg_imm64(code, REG_RBX, 16);
34+ emit_imul_reg_reg(code, REG_RDI, REG_RBX);
35+ emit_cmp_reg_imm32(code, REG_RDI, 0);
36+ int nz = emit_jcc_rel32(code, COND_NE);
37+ emit_mov_reg_imm64(code, REG_RDI, 1);
38+ emit_patch_jump(code, nz);
39+ heap_emit_alloc(code); // RAX = out block
40+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);
41+
42+ emit_mov_reg_imm64(code, REG_RCX, 0);
43+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RCX); // i = 0
44+
45+ int loop_start = code->count;
46+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24);
47+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8);
48+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
49+ int loop_exit = emit_jcc_rel32(code, COND_GE);
50+
51+ // c_str = *(argv_base + 8 + 8*i)
52+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
53+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
54+ emit_mov_reg_imm64(code, REG_RDX, 8);
55+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
56+ emit_add_reg_imm8(code, REG_RAX, 8); // skip the argc slot itself
57+ emit_add_reg_reg(code, REG_RSI, REG_RAX);
58+ emit_load_mem_disp32(code, REG_RSI, REG_RSI, 0); // the char* value itself
59+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RSI);
60+
61+ // strlen (C strings from the kernel aren't length-prefixed)
62+ emit_mov_reg_reg(code, REG_RCX, REG_RSI);
63+ int strlen_loop = code->count;
64+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
65+ emit_cmp_reg_imm32(code, REG_RAX, 0);
66+ int strlen_done = emit_jcc_rel32(code, COND_E);
67+ emit_add_reg_imm8(code, REG_RCX, 1);
68+ emit_jmp_back(code, strlen_loop);
69+ emit_patch_jump(code, strlen_done);
70+
71+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 32); // c_str
72+ emit_sub_reg_reg(code, REG_RCX, REG_RDX); // length = cursor - c_str
73+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX);
74+
75+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40); // length
76+ string_alloc_emit_prefixed(code); // RAX = new block
77+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash (bytes_emit_copy preserves RSI)
78+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
79+ emit_add_reg_imm8(code, REG_RDI, 8);
80+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 32); // src = c_str
81+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40); // length
82+ bytes_emit_copy(code);
83+
84+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
85+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
86+
87+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i
88+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_data_ptr
89+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);
90+ emit_mov_reg_imm64(code, REG_RDI, 16);
91+ emit_imul_reg_reg(code, REG_RDX, REG_RDI);
92+ emit_add_reg_reg(code, REG_RSI, REG_RDX);
93+ emit_mov_reg_imm64(code, REG_RDI, TAG_STRING);
94+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI);
95+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
96+
97+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24);
98+ emit_add_reg_imm8(code, REG_RCX, 1);
99+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RCX);
100+ emit_jmp_back(code, loop_start);
101+ emit_patch_jump(code, loop_exit);
102+
103+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 8); // argc
104+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
105+ heap_emit_alloc(code); // RAX = array object
106+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_data_ptr (reload)
107+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
108+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX);
109+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
110+
111+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
112+ emit_push_reg(code, REG_RBX);
113+ emit_push_reg(code, REG_RAX);
114+}

kiln/src/codegen/argv_builtin.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/codegen/argv_builtin.o: src/codegen/argv_builtin.c \
2+ src/codegen/argv_builtin.h src/codegen/argv_builtin_internal.h \
3+ src/codegen/emit.h src/codegen/string_alloc.h src/codegen/bytes.h \
4+ src/codegen/heap.h src/codegen/value.h src/elf/elf_writer.h \
5+ src/codegen/runtime_layout.h src/parser/parser.h src/lexer/lexer.h \
6+ src/parser/vars.h src/target.h
7+src/codegen/argv_builtin.h:
8+src/codegen/argv_builtin_internal.h:
9+src/codegen/emit.h:
10+src/codegen/string_alloc.h:
11+src/codegen/bytes.h:
12+src/codegen/heap.h:
13+src/codegen/value.h:
14+src/elf/elf_writer.h:
15+src/codegen/runtime_layout.h:
16+src/parser/parser.h:
17+src/lexer/lexer.h:
18+src/parser/vars.h:
19+src/target.h:

kiln/src/codegen/argv_builtin.hadded

+10−0
@@ -0,0 +1,10 @@
1+#ifndef KILN_ARGV_BUILTIN_H
2+#define KILN_ARGV_BUILTIN_H
3+
4+// argv(): no arguments -- codegen/builtins.c's dispatch just expects the
5+// closing ')' before calling this. Pushes an array of strings, one per
6+// process argument (argv[0] is the executable's own path, same
7+// convention as C's main(argc, argv)).
8+void codegen_builtin_argv(void);
9+
10+#endif

kiln/src/codegen/argv_builtin_internal.hadded

+13−0
@@ -0,0 +1,13 @@
1+#ifndef KILN_ARGV_BUILTIN_INTERNAL_H
2+#define KILN_ARGV_BUILTIN_INTERNAL_H
3+#include "codegen/emit.h"
4+
5+// Windows has no kernel-provided argv array to read (see
6+// argv_builtin.c's header comment) -- GetCommandLineA() returns the raw
7+// command-line string instead, so the Windows path needs its own tokenizer
8+// rather than sharing codegen with the Linux path. Split into its own
9+// file to keep argv_builtin.c under ~200 lines; not part of the public
10+// codegen/argv_builtin.h surface, only argv_builtin.c calls this.
11+void codegen_builtin_argv_windows(void);
12+
13+#endif

kiln/src/codegen/argv_builtin_windows.cadded

+181−0
@@ -0,0 +1,181 @@
1+#include <stdint.h>
2+#include "codegen/argv_builtin_internal.h"
3+#include "codegen/string_alloc.h"
4+#include "codegen/bytes.h"
5+#include "codegen/heap.h"
6+#include "codegen/value.h"
7+#include "codegen/win_call.h"
8+#include "parser/parser.h"
9+#include "parser/vars.h"
10+
11+// GetCommandLineA() returns the whole command line as one string (e.g.
12+// `prog.exe arg1 arg2`), not a pre-split argv array -- so unlike the
13+// Linux path (which reads an already-split array straight from the
14+// kernel's process-startup stack), this has to tokenize it by hand.
15+// Simple whitespace splitting only, no quoted-argument handling (an
16+// argument containing a space can't be represented) -- documented
17+// simplification, same spirit as this project's other "don't
18+// over-engineer for what can't practically happen" scope limits.
19+//
20+// Scratch layout: [0]=cmdline [8]=argc [16]=out_data_ptr [24]=i
21+// [32]=token_start [40]=token_end [48]=token_len [56]=copy_start
22+// [64]=copy_end (the latter two are token_start/end with surrounding
23+// quotes stripped, kept separate so stripping doesn't corrupt the
24+// resume-scanning position -- see the strip logic below)
25+void codegen_builtin_argv_windows(void) {
26+ int32_t base = higher_order_scratch_offset();
27+
28+ win_call_begin(code, 0);
29+ win_call_import(code, PE_IMPORT_GET_COMMAND_LINE_A);
30+ win_call_end(code);
31+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RAX); // cmdline
32+
33+ // ---- pass 1: count whitespace-separated tokens ----
34+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 0);
35+ emit_mov_reg_imm64(code, REG_RDX, 0); // count
36+
37+ int p1_loop = code->count;
38+ int p1_skip = code->count;
39+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
40+ emit_cmp_reg_imm32(code, REG_RAX, ' ');
41+ int p1_not_space = emit_jcc_rel32(code, COND_NE);
42+ emit_add_reg_imm8(code, REG_RCX, 1);
43+ emit_jmp_back(code, p1_skip);
44+ emit_patch_jump(code, p1_not_space);
45+ emit_cmp_reg_imm32(code, REG_RAX, 0);
46+ int p1_done = emit_jcc_rel32(code, COND_E);
47+ emit_add_reg_imm8(code, REG_RDX, 1);
48+
49+ int p1_word = code->count;
50+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
51+ emit_cmp_reg_imm32(code, REG_RAX, 0);
52+ int p1_end1 = emit_jcc_rel32(code, COND_E);
53+ emit_cmp_reg_imm32(code, REG_RAX, ' ');
54+ int p1_end2 = emit_jcc_rel32(code, COND_E);
55+ emit_add_reg_imm8(code, REG_RCX, 1);
56+ emit_jmp_back(code, p1_word);
57+ emit_patch_jump(code, p1_end1);
58+ emit_patch_jump(code, p1_end2);
59+ emit_jmp_back(code, p1_loop);
60+ emit_patch_jump(code, p1_done);
61+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX); // argc
62+
63+ // ---- allocate array data ----
64+ emit_mov_reg_reg(code, REG_RAX, REG_RDX);
65+ emit_mov_reg_imm64(code, REG_RBX, 16);
66+ emit_imul_reg_reg(code, REG_RAX, REG_RBX);
67+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
68+ emit_cmp_reg_imm32(code, REG_RDI, 0);
69+ int nz = emit_jcc_rel32(code, COND_NE);
70+ emit_mov_reg_imm64(code, REG_RDI, 1);
71+ emit_patch_jump(code, nz);
72+ heap_emit_alloc(code); // RAX = out block
73+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);
74+
75+ // ---- pass 2: build each token as a string, fill the array ----
76+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 0);
77+ emit_mov_reg_imm64(code, REG_RSI, 0);
78+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RSI); // i = 0
79+
80+ int p2_loop = code->count;
81+ int p2_skip = code->count;
82+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
83+ emit_cmp_reg_imm32(code, REG_RAX, ' ');
84+ int p2_not_space = emit_jcc_rel32(code, COND_NE);
85+ emit_add_reg_imm8(code, REG_RCX, 1);
86+ emit_jmp_back(code, p2_skip);
87+ emit_patch_jump(code, p2_not_space);
88+ emit_cmp_reg_imm32(code, REG_RAX, 0);
89+ int p2_done = emit_jcc_rel32(code, COND_E);
90+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RCX); // token_start
91+
92+ int p2_word = code->count;
93+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
94+ emit_cmp_reg_imm32(code, REG_RAX, 0);
95+ int p2_end1 = emit_jcc_rel32(code, COND_E);
96+ emit_cmp_reg_imm32(code, REG_RAX, ' ');
97+ int p2_end2 = emit_jcc_rel32(code, COND_E);
98+ emit_add_reg_imm8(code, REG_RCX, 1);
99+ emit_jmp_back(code, p2_word);
100+ emit_patch_jump(code, p2_end1);
101+ emit_patch_jump(code, p2_end2);
102+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // token_end
103+
104+ // GetCommandLineA() wraps the executable path in quotes (standard
105+ // Windows convention, always applied by real programs too, not just
106+ // when the path has spaces) -- strip a leading+trailing '"' pair if
107+ // both are present on this token. Doesn't handle quoted arguments
108+ // with embedded spaces (still out of scope, see the file header).
109+ // Adjusted bounds go in their OWN slots (copy_start/copy_end,
110+ // +56/+64) rather than overwriting token_start/token_end (+32/+40):
111+ // the outer loop reloads token_end below to resume scanning for the
112+ // NEXT token, and stripping a trailing quote there would skip past
113+ // it into whatever follows, corrupting where the next token starts.
114+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32); // token_start
115+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40); // token_end
116+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RCX); // copy_start = token_start
117+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RDX); // copy_end = token_end
118+
119+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
120+ emit_cmp_reg_imm32(code, REG_RAX, '"');
121+ int no_open_quote = emit_jcc_rel32(code, COND_NE);
122+ emit_mov_reg_reg(code, REG_RAX, REG_RDX);
123+ emit_sub_reg_imm8(code, REG_RAX, 1);
124+ emit_load_byte_reg(code, REG_RAX, REG_RAX);
125+ emit_cmp_reg_imm32(code, REG_RAX, '"');
126+ int no_close_quote = emit_jcc_rel32(code, COND_NE);
127+ emit_add_reg_imm8(code, REG_RCX, 1);
128+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RCX); // copy_start++
129+ emit_sub_reg_imm8(code, REG_RDX, 1);
130+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RDX); // copy_end--
131+ emit_patch_jump(code, no_close_quote);
132+ emit_patch_jump(code, no_open_quote);
133+
134+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 64);
135+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 56);
136+ emit_sub_reg_reg(code, REG_RAX, REG_RDX); // len = copy_end - copy_start
137+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RAX);
138+
139+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 48);
140+ string_alloc_emit_prefixed(code); // RAX = new block
141+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash (bytes_emit_copy preserves RSI)
142+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
143+ emit_add_reg_imm8(code, REG_RDI, 8);
144+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 56); // src = copy_start
145+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 48);
146+ bytes_emit_copy(code);
147+
148+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
149+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
150+
151+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i
152+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_data_ptr
153+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);
154+ emit_mov_reg_imm64(code, REG_RDI, 16);
155+ emit_imul_reg_reg(code, REG_RDX, REG_RDI);
156+ emit_add_reg_reg(code, REG_RSI, REG_RDX);
157+ emit_mov_reg_imm64(code, REG_RDI, TAG_STRING);
158+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI);
159+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
160+
161+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24);
162+ emit_add_reg_imm8(code, REG_RCX, 1);
163+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RCX); // i++
164+
165+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // resume from token_end
166+ emit_jmp_back(code, p2_loop);
167+ emit_patch_jump(code, p2_done);
168+
169+ // ---- build the array object ----
170+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 8); // argc
171+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
172+ heap_emit_alloc(code); // RAX = array object
173+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);
174+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
175+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX);
176+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
177+
178+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
179+ emit_push_reg(code, REG_RBX);
180+ emit_push_reg(code, REG_RAX);
181+}

kiln/src/codegen/argv_builtin_windows.dadded

+22−0
@@ -0,0 +1,22 @@
1+src/codegen/argv_builtin_windows.o: src/codegen/argv_builtin_windows.c \
2+ src/codegen/argv_builtin_internal.h src/codegen/emit.h \
3+ src/codegen/string_alloc.h src/codegen/bytes.h src/codegen/heap.h \
4+ src/codegen/value.h src/codegen/win_call.h src/pe/pe_writer.h \
5+ src/codegen/runtime_layout.h src/codegen/runtime_import_enum.h \
6+ src/codegen/runtime_exports.def src/pe/pe_imports.def \
7+ src/parser/parser.h src/lexer/lexer.h src/parser/vars.h
8+src/codegen/argv_builtin_internal.h:
9+src/codegen/emit.h:
10+src/codegen/string_alloc.h:
11+src/codegen/bytes.h:
12+src/codegen/heap.h:
13+src/codegen/value.h:
14+src/codegen/win_call.h:
15+src/pe/pe_writer.h:
16+src/codegen/runtime_layout.h:
17+src/codegen/runtime_import_enum.h:
18+src/codegen/runtime_exports.def:
19+src/pe/pe_imports.def:
20+src/parser/parser.h:
21+src/lexer/lexer.h:
22+src/parser/vars.h:

kiln/src/codegen/arrays.cadded

+201−0
@@ -0,0 +1,201 @@
1+#include "codegen/arrays.h"
2+#include "codegen/expr.h"
3+#include "codegen/emit_internal.h"
4+#include "codegen/emit_sse.h"
5+#include "codegen/value.h"
6+#include "codegen/heap.h"
7+#include "codegen/bytes.h"
8+#include "codegen/errors.h"
9+#include "parser/parser.h"
10+
11+void codegen_array_literal(void) {
12+ advance_token(); // consume '['
13+ int count = 0;
14+ if (current.type != TOKEN_RBRACKET) {
15+ codegen_expression(); count++;
16+ while (current.type == TOKEN_COMMA) { advance_token(); codegen_expression(); count++; }
17+ }
18+ expect(TOKEN_RBRACKET, "expected ']' after array literal");
19+
20+ // elements block first (its address becomes the object's data_ptr)
21+ emit_mov_reg_imm64(code, REG_RDI, (uint64_t)(count > 0 ? count * 16 : 1));
22+ heap_emit_alloc(code); // RAX = elements block
23+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash: RSI survives the pop loop below
24+
25+ // Elements were pushed left to right, so the last one is on top --
26+ // pop from the end of the array backward to land each in the right slot.
27+ for (int i = count - 1; i >= 0; i--) {
28+ emit_pop_reg(code, REG_RAX); // payload
29+ emit_pop_reg(code, REG_RBX); // tag
30+ emit_store_mem_disp32(code, REG_RSI, 16 * i, REG_RBX);
31+ emit_store_mem_disp32(code, REG_RSI, 16 * i + 8, REG_RAX);
32+ }
33+
34+ // now the stable array object
35+ emit_push_reg(code, REG_RSI); // save elements block address across this alloc
36+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
37+ heap_emit_alloc(code); // RAX = array object
38+ emit_pop_reg(code, REG_RSI);
39+
40+ emit_mov_reg_imm64(code, REG_RCX, (uint64_t)count);
41+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // capacity
42+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count
43+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI); // data_ptr
44+
45+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
46+ emit_push_reg(code, REG_RBX);
47+ emit_push_reg(code, REG_RAX); // payload = the stable array object's address
48+}
49+
50+// Given an array object address (RSI) and a raw index payload (RAX, a
51+// double's bits), converts to an integer index (RCX), dies with `msg` if
52+// out of [0, count), and leaves data_ptr in RDX.
53+static void bounds_check(const char *msg) {
54+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
55+ emit_cvttsd2si(code, REG_RCX, XMM0);
56+ emit_cmp_reg_imm32(code, REG_RCX, 0);
57+ int neg = emit_jcc_rel32(code, COND_LT);
58+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
59+ emit_cmp_reg_reg(code, REG_RCX, REG_RAX);
60+ int toobig = emit_jcc_rel32(code, COND_GE);
61+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr
62+ int ok = emit_jmp_rel32(code);
63+ emit_patch_jump(code, neg);
64+ emit_patch_jump(code, toobig);
65+ errors_emit_die(code, msg);
66+ emit_patch_jump(code, ok);
67+}
68+
69+void codegen_array_index_read(void) {
70+ emit_pop_reg(code, REG_RAX); // index payload
71+ emit_pop_reg(code, REG_RBX); // index tag (assumed NUMBER)
72+ emit_pop_reg(code, REG_RSI); // array object address
73+ emit_pop_reg(code, REG_RBX); // array tag (assumed ARRAY)
74+
75+ bounds_check("runtime error: array index out of bounds"); // RCX=index, RDX=data_ptr
76+
77+ // element address = data_ptr + RCX*16
78+ emit_mov_reg_imm64(code, REG_RAX, 16);
79+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
80+ emit_add_reg_reg(code, REG_RCX, REG_RDX);
81+
82+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 0);
83+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8);
84+ emit_push_reg(code, REG_RBX);
85+ emit_push_reg(code, REG_RAX);
86+}
87+
88+void codegen_array_index_store(void) {
89+ emit_pop_reg(code, REG_RAX); // value payload
90+ emit_pop_reg(code, REG_RBX); // value tag
91+ emit_pop_reg(code, REG_RCX); // index payload
92+ emit_pop_reg(code, REG_RDI); // index tag (ignored)
93+ emit_pop_reg(code, REG_RSI); // array object address
94+ emit_pop_reg(code, REG_RDI); // array tag (ignored)
95+
96+ // stash value on the real stack while bounds_check uses RAX/RCX/RDX
97+ emit_push_reg(code, REG_RBX); // value tag
98+ emit_push_reg(code, REG_RAX); // value payload
99+ emit_mov_reg_reg(code, REG_RAX, REG_RCX); // index payload -> RAX, where bounds_check expects it
100+
101+ bounds_check("runtime error: array index out of bounds"); // RCX=index, RDX=data_ptr
102+
103+ emit_mov_reg_imm64(code, REG_RAX, 16);
104+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
105+ emit_add_reg_reg(code, REG_RCX, REG_RDX); // element address
106+
107+ emit_pop_reg(code, REG_RAX); // value payload
108+ emit_pop_reg(code, REG_RBX); // value tag
109+ emit_store_mem_disp32(code, REG_RCX, 0, REG_RBX);
110+ emit_store_mem_disp32(code, REG_RCX, 8, REG_RAX);
111+}
112+
113+// Scratch layout (stable base in RCX, same technique as
114+// codegen_string_concat): [0]=val_tag [8]=val_payload [16]=array_object
115+void codegen_builtin_push(void) {
116+ emit_pop_reg(code, REG_RAX); // val payload
117+ emit_pop_reg(code, REG_RBX); // val tag
118+ emit_pop_reg(code, REG_RSI); // array object address
119+ emit_pop_reg(code, REG_RDX); // array tag (ignored)
120+
121+ emit_sub_reg_imm8(code, REG_RSP, 24);
122+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
123+ emit_store_mem_disp32(code, REG_RCX, 0, REG_RBX);
124+ emit_store_mem_disp32(code, REG_RCX, 8, REG_RAX);
125+ emit_store_mem_disp32(code, REG_RCX, 16, REG_RSI);
126+
127+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // capacity
128+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count
129+ emit_cmp_reg_reg(code, REG_RDX, REG_RAX);
130+ int has_room = emit_jcc_rel32(code, COND_LT);
131+
132+ // --- grow: new data block sized capacity+4, copy the old elements over ---
133+ emit_add_reg_imm8(code, REG_RAX, 4); // RAX = new_capacity
134+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
135+ emit_mov_reg_imm64(code, REG_RBX, 16);
136+ emit_imul_reg_reg(code, REG_RDI, REG_RBX); // RDI = new data block size
137+
138+ emit_push_reg(code, REG_RCX); // save scratch base across the call
139+ emit_push_reg(code, REG_RAX); // save new_capacity too (heap_emit_alloc doesn't touch the real stack)
140+ heap_emit_alloc(code); // RAX = new data block
141+ emit_pop_reg(code, REG_RDX); // RDX = new_capacity
142+ emit_pop_reg(code, REG_RCX); // scratch base
143+
144+ emit_push_reg(code, REG_RAX); // save new data block across bytes_emit_copy
145+ emit_push_reg(code, REG_RDX); // save new_capacity
146+ emit_push_reg(code, REG_RCX); // save scratch base
147+
148+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 16); // RBX = array object
149+ emit_load_mem_disp32(code, REG_RDX, REG_RBX, 16); // RDX = old data_ptr
150+ emit_mov_reg_reg(code, REG_RBX, REG_RDX); // src = old data_ptr
151+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, 16); // reload array object (RBX just got overwritten)
152+ emit_load_mem_disp32(code, REG_RDX, REG_RDX, 0); // RDX = old capacity
153+ emit_mov_reg_reg(code, REG_RDI, REG_RAX); // dest = new data block (about to be overwritten by copy loop, ok, saved on stack too)
154+ emit_mov_reg_imm64(code, REG_RAX, 16);
155+ emit_imul_reg_reg(code, REG_RDX, REG_RAX); // RDX = old_capacity*16 bytes to copy
156+ bytes_emit_copy(code);
157+
158+ emit_pop_reg(code, REG_RCX); // scratch base
159+ emit_pop_reg(code, REG_RDX); // new_capacity
160+ emit_pop_reg(code, REG_RAX); // new data block
161+
162+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 16); // array object
163+ emit_store_mem_disp32(code, REG_RBX, 0, REG_RDX); // capacity = new_capacity
164+ emit_store_mem_disp32(code, REG_RBX, 16, REG_RAX); // data_ptr = new data block
165+
166+ emit_patch_jump(code, has_room);
167+
168+ // --- append: element goes at data[count], then count++ ---
169+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 16); // array object (reload -- has_room's jump skips straight here)
170+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count
171+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // data_ptr
172+ emit_mov_reg_imm64(code, REG_RAX, 16);
173+ emit_imul_reg_reg(code, REG_RDX, REG_RAX);
174+ emit_add_reg_reg(code, REG_RDX, REG_RDI); // RDX = new element's address
175+
176+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 0); // val_tag
177+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8); // val_payload
178+ emit_store_mem_disp32(code, REG_RDX, 0, REG_RBX);
179+ emit_store_mem_disp32(code, REG_RDX, 8, REG_RAX);
180+
181+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
182+ emit_add_reg_imm8(code, REG_RAX, 1);
183+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX); // count++
184+
185+ emit_add_reg_imm8(code, REG_RSP, 24);
186+
187+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
188+ emit_push_reg(code, REG_RBX);
189+ emit_push_reg(code, REG_RSI); // push() returns the array itself, matching ashvm
190+}
191+
192+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); // count
196+ emit_cvtsi2sd(code, XMM0, REG_RAX);
197+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
198+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
199+ emit_push_reg(code, REG_RBX);
200+ emit_push_reg(code, REG_RAX);
201+}

kiln/src/codegen/arrays.dadded

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

kiln/src/codegen/arrays.hadded

+31−0
@@ -0,0 +1,31 @@
1+#ifndef KILN_ARRAYS_H
2+#define KILN_ARRAYS_H
3+
4+// primary()'s '[' handling: parses `[e0, e1, ...]`, allocates a heap
5+// block sized for exactly that many elements (see codegen/value.h for
6+// the layout), and pushes (tag=TAG_ARRAY, payload=<block address>).
7+void codegen_array_literal(void);
8+
9+// Assumes index already pushed (top) and array already pushed (below
10+// it), both (tag, payload) pairs -- postfix()'s `arr[i]` read. Pops both,
11+// bounds-checks, pushes the element.
12+void codegen_array_index_read(void);
13+
14+// Assumes value pushed (top), index pushed (middle), array pushed
15+// (bottom) -- parser.c's `arr[i] = v;` write. Pops all three,
16+// bounds-checks, stores, pushes nothing (statement context).
17+void codegen_array_index_store(void);
18+
19+// Both assume their arguments already pushed left-to-right (matching
20+// the normal call-argument convention) -- codegen/builtins.c's dispatch
21+// calls these after parsing push(arr, val) / len(arr)'s arguments.
22+
23+// push(array, value): appends, growing (a fresh, bigger heap block plus
24+// a copy -- the bump allocator can't grow in place) if out of capacity.
25+// Pushes the array back (matching ashvm's push(), which returns args[0]).
26+void codegen_builtin_push(void);
27+
28+// len(array): pushes the element count as a NUMBER.
29+void codegen_builtin_len(void);
30+
31+#endif

kiln/src/codegen/builtins.cadded

+170−0
@@ -0,0 +1,170 @@
1+#include <string.h>
2+#include "codegen/builtins.h"
3+#include "codegen/expr.h"
4+#include "codegen/arrays.h"
5+#include "codegen/maps.h"
6+#include "codegen/math_builtins.h"
7+#include "codegen/convert_builtins.h"
8+#include "codegen/higher_order.h"
9+#include "codegen/keys_values.h"
10+#include "codegen/string_builtins.h"
11+#include "codegen/file_builtins.h"
12+#include "codegen/input_builtin.h"
13+#include "codegen/argv_builtin.h"
14+#include "parser/parser.h"
15+
16+int codegen_try_builtin_call(const char *name, int len) {
17+ if (len == 4 && strncmp(name, "argv", 4) == 0) {
18+ expect(TOKEN_RPAREN, "expected ')' after 'argv('");
19+ codegen_builtin_argv();
20+ return 1;
21+ }
22+ if (len == 5 && strncmp(name, "input", 5) == 0) {
23+ expect(TOKEN_RPAREN, "expected ')' after 'input('");
24+ codegen_builtin_input();
25+ return 1;
26+ }
27+ if (len == 9 && strncmp(name, "read_file", 9) == 0) {
28+ codegen_expression();
29+ expect(TOKEN_RPAREN, "expected ')' after argument");
30+ codegen_builtin_read_file();
31+ return 1;
32+ }
33+ if (len == 10 && strncmp(name, "write_file", 10) == 0) {
34+ codegen_builtin_write_file();
35+ return 1;
36+ }
37+ if (len == 11 && strncmp(name, "append_file", 11) == 0) {
38+ codegen_builtin_append_file();
39+ return 1;
40+ }
41+ if (len == 11 && strncmp(name, "file_exists", 11) == 0) {
42+ codegen_expression();
43+ expect(TOKEN_RPAREN, "expected ')' after argument");
44+ codegen_builtin_file_exists();
45+ return 1;
46+ }
47+ if (len == 5 && strncmp(name, "upper", 5) == 0) {
48+ codegen_expression();
49+ expect(TOKEN_RPAREN, "expected ')' after argument");
50+ codegen_builtin_upper();
51+ return 1;
52+ }
53+ if (len == 5 && strncmp(name, "lower", 5) == 0) {
54+ codegen_expression();
55+ expect(TOKEN_RPAREN, "expected ')' after argument");
56+ codegen_builtin_lower();
57+ return 1;
58+ }
59+ if (len == 4 && strncmp(name, "trim", 4) == 0) {
60+ codegen_expression();
61+ expect(TOKEN_RPAREN, "expected ')' after argument");
62+ codegen_builtin_trim();
63+ return 1;
64+ }
65+ if (len == 9 && strncmp(name, "substring", 9) == 0) {
66+ codegen_builtin_substring();
67+ return 1;
68+ }
69+ if (len == 7 && strncmp(name, "indexOf", 7) == 0) {
70+ codegen_builtin_indexof();
71+ return 1;
72+ }
73+ if (len == 5 && strncmp(name, "split", 5) == 0) {
74+ codegen_builtin_split();
75+ return 1;
76+ }
77+ if (len == 4 && strncmp(name, "join", 4) == 0) {
78+ codegen_builtin_join();
79+ return 1;
80+ }
81+ if (len == 7 && strncmp(name, "replace", 7) == 0) {
82+ codegen_builtin_replace();
83+ return 1;
84+ }
85+ if (len == 4 && strncmp(name, "keys", 4) == 0) {
86+ codegen_expression();
87+ expect(TOKEN_RPAREN, "expected ')' after argument");
88+ codegen_builtin_keys();
89+ return 1;
90+ }
91+ if (len == 6 && strncmp(name, "values", 6) == 0) {
92+ codegen_expression();
93+ expect(TOKEN_RPAREN, "expected ')' after argument");
94+ codegen_builtin_values();
95+ return 1;
96+ }
97+ if (len == 3 && strncmp(name, "map", 3) == 0) {
98+ codegen_builtin_map();
99+ return 1;
100+ }
101+ if (len == 6 && strncmp(name, "filter", 6) == 0) {
102+ codegen_builtin_filter();
103+ return 1;
104+ }
105+ if (len == 6 && strncmp(name, "reduce", 6) == 0) {
106+ codegen_builtin_reduce();
107+ return 1;
108+ }
109+ if (len == 3 && strncmp(name, "str", 3) == 0) {
110+ codegen_expression();
111+ expect(TOKEN_RPAREN, "expected ')' after argument");
112+ codegen_builtin_str();
113+ return 1;
114+ }
115+ if (len == 3 && strncmp(name, "num", 3) == 0) {
116+ codegen_expression();
117+ expect(TOKEN_RPAREN, "expected ')' after argument");
118+ codegen_builtin_num();
119+ return 1;
120+ }
121+ if (len == 4 && strncmp(name, "sqrt", 4) == 0) {
122+ codegen_expression();
123+ expect(TOKEN_RPAREN, "expected ')' after argument");
124+ codegen_builtin_sqrt();
125+ return 1;
126+ }
127+ if (len == 3 && strncmp(name, "abs", 3) == 0) {
128+ codegen_expression();
129+ expect(TOKEN_RPAREN, "expected ')' after argument");
130+ codegen_builtin_abs();
131+ return 1;
132+ }
133+ if (len == 5 && strncmp(name, "floor", 5) == 0) {
134+ codegen_expression();
135+ expect(TOKEN_RPAREN, "expected ')' after argument");
136+ codegen_builtin_floor();
137+ return 1;
138+ }
139+ if (len == 4 && strncmp(name, "push", 4) == 0) {
140+ codegen_expression();
141+ expect(TOKEN_COMMA, "expected ',' after array argument");
142+ codegen_expression();
143+ expect(TOKEN_RPAREN, "expected ')' after arguments");
144+ codegen_builtin_push();
145+ return 1;
146+ }
147+ if (len == 3 && strncmp(name, "len", 3) == 0) {
148+ codegen_expression();
149+ expect(TOKEN_RPAREN, "expected ')' after argument");
150+ codegen_builtin_len();
151+ return 1;
152+ }
153+ if (len == 3 && strncmp(name, "has", 3) == 0) {
154+ codegen_expression();
155+ expect(TOKEN_COMMA, "expected ',' after map argument");
156+ codegen_expression();
157+ expect(TOKEN_RPAREN, "expected ')' after arguments");
158+ codegen_builtin_has();
159+ return 1;
160+ }
161+ if (len == 6 && strncmp(name, "delete", 6) == 0) {
162+ codegen_expression();
163+ expect(TOKEN_COMMA, "expected ',' after map argument");
164+ codegen_expression();
165+ expect(TOKEN_RPAREN, "expected ')' after arguments");
166+ codegen_builtin_delete();
167+ return 1;
168+ }
169+ return 0;
170+}

kiln/src/codegen/builtins.dadded

+22−0
@@ -0,0 +1,22 @@
1+src/codegen/builtins.o: src/codegen/builtins.c src/codegen/builtins.h \
2+ src/codegen/expr.h src/lexer/lexer.h src/codegen/arrays.h \
3+ src/codegen/maps.h src/codegen/math_builtins.h \
4+ src/codegen/convert_builtins.h src/codegen/higher_order.h \
5+ src/codegen/keys_values.h src/codegen/string_builtins.h \
6+ src/codegen/file_builtins.h src/codegen/input_builtin.h \
7+ src/codegen/argv_builtin.h src/parser/parser.h src/codegen/emit.h
8+src/codegen/builtins.h:
9+src/codegen/expr.h:
10+src/lexer/lexer.h:
11+src/codegen/arrays.h:
12+src/codegen/maps.h:
13+src/codegen/math_builtins.h:
14+src/codegen/convert_builtins.h:
15+src/codegen/higher_order.h:
16+src/codegen/keys_values.h:
17+src/codegen/string_builtins.h:
18+src/codegen/file_builtins.h:
19+src/codegen/input_builtin.h:
20+src/codegen/argv_builtin.h:
21+src/parser/parser.h:
22+src/codegen/emit.h:

kiln/src/codegen/builtins.hadded

+11−0
@@ -0,0 +1,11 @@
1+#ifndef KILN_BUILTINS_H
2+#define KILN_BUILTINS_H
3+
4+// Called from primary()'s function-call handling right after the '('
5+// has been consumed. If `name` (length `len`) is a known builtin, parses
6+// its arguments itself (through the closing ')'), emits its codegen, and
7+// returns 1. Returns 0 without consuming anything otherwise, so the
8+// caller falls through to a user-defined function lookup.
9+int codegen_try_builtin_call(const char *name, int len);
10+
11+#endif

kiln/src/codegen/bytes.cadded

+54−0
@@ -0,0 +1,54 @@
1+#include "codegen/bytes.h"
2+#include "codegen/win_call.h"
3+#include "elf/elf_dynamic_call.h"
4+#include "target.h"
5+
6+// Plan B, phase B1: emitted once (jumped over), called via emit_call_back
7+// from all 18 call sites instead of each re-emitting this loop inline --
8+// same pattern as codegen/heap.c's alloc routine. Phase B3/B4: skipped
9+// entirely under --link=shared (this routine lives in libkilnrt.so/.dll
10+// instead, emitted by the separate "compile the runtime" driver via
11+// bytes_emit_copy_routine_only), and bytes_emit_copy becomes a call
12+// through that library instead.
13+static int copy_routine_offset = -1;
14+
15+static void emit_copy_routine(CodeBuf *code) {
16+ int skip = emit_jmp_rel32(code);
17+ copy_routine_offset = code->count;
18+ emit_mov_reg_reg(code, REG_RCX, REG_RDX);
19+ int loop_start = code->count;
20+ emit_cmp_reg_imm32(code, REG_RCX, 0);
21+ int done = emit_jcc_rel32(code, COND_E);
22+ emit_load_byte_reg(code, REG_RAX, REG_RBX);
23+ emit_store_byte_reg(code, REG_RDI, REG_RAX);
24+ emit_add_reg_imm8(code, REG_RBX, 1);
25+ emit_add_reg_imm8(code, REG_RDI, 1);
26+ emit_dec_reg(code, REG_RCX);
27+ emit_jmp_back(code, loop_start);
28+ emit_patch_jump(code, done);
29+ emit_ret(code);
30+ emit_patch_jump(code, skip);
31+}
32+
33+void bytes_emit_copy_routine_only(CodeBuf *code) {
34+ emit_copy_routine(code);
35+}
36+
37+int bytes_copy_routine_offset(void) { return copy_routine_offset; }
38+
39+static int is_shared(void) {
40+ return kiln_get_link_mode() == KILN_LINK_SHARED && !kiln_is_compiling_so();
41+}
42+
43+void bytes_emit_startup(CodeBuf *code) {
44+ if (!is_shared()) emit_copy_routine(code);
45+}
46+
47+void bytes_emit_copy(CodeBuf *code) {
48+ if (is_shared()) {
49+ if (kiln_get_target() == KILN_TARGET_WINDOWS) win_call_runtime_import(code, RUNTIME_IMPORT_BYTES_COPY);
50+ else elf_dynamic_call(code, RUNTIME_IMPORT_BYTES_COPY);
51+ return;
52+ }
53+ emit_call_back(code, copy_routine_offset);
54+}

kiln/src/codegen/bytes.dadded

+18−0
@@ -0,0 +1,18 @@
1+src/codegen/bytes.o: src/codegen/bytes.c src/codegen/bytes.h \
2+ src/codegen/emit.h src/codegen/win_call.h src/pe/pe_writer.h \
3+ src/codegen/runtime_layout.h src/codegen/runtime_import_enum.h \
4+ src/codegen/runtime_exports.def src/pe/pe_imports.def \
5+ src/elf/elf_dynamic_call.h src/elf/elf_dynamic.h src/elf/elf_writer.h \
6+ src/target.h
7+src/codegen/bytes.h:
8+src/codegen/emit.h:
9+src/codegen/win_call.h:
10+src/pe/pe_writer.h:
11+src/codegen/runtime_layout.h:
12+src/codegen/runtime_import_enum.h:
13+src/codegen/runtime_exports.def:
14+src/pe/pe_imports.def:
15+src/elf/elf_dynamic_call.h:
16+src/elf/elf_dynamic.h:
17+src/elf/elf_writer.h:
18+src/target.h:

kiln/src/codegen/bytes.hadded

+22−0
@@ -0,0 +1,22 @@
1+#ifndef KILN_BYTES_H
2+#define KILN_BYTES_H
3+#include "codegen/emit.h"
4+
5+// Emitted once, at program start (see parser.c's compile_program) --
6+// must run before any call to bytes_emit_copy below.
7+void bytes_emit_startup(CodeBuf *code);
8+
9+// Plan B: emits ONLY the copy routine's body -- used by the standalone
10+// "compile the runtime" driver that builds libkilnrt.so.
11+void bytes_emit_copy_routine_only(CodeBuf *code);
12+int bytes_copy_routine_offset(void);
13+
14+// RDI=dest, RBX=src, RDX=count (input) -- clobbers RAX,RBX,RCX,RDX,RDI.
15+// Deliberately doesn't touch RSI, so callers can keep a stable scratch
16+// pointer there across multiple calls. Shared between codegen/strings.c
17+// (concatenation) and codegen/arrays.c (growth). Plan B, phase B1: a real
18+// CALL to one shared copy (emitted by bytes_emit_startup) rather than
19+// re-emitting this loop inline at each of its ~18 call sites.
20+void bytes_emit_copy(CodeBuf *code);
21+
22+#endif

kiln/src/codegen/case_builtins.cadded

+53−0
@@ -0,0 +1,53 @@
1+#include <stdint.h>
2+#include "codegen/string_builtins.h"
3+#include "codegen/string_alloc.h"
4+#include "codegen/emit.h"
5+#include "codegen/value.h"
6+#include "parser/parser.h"
7+
8+// Shared shape for upper()/lower(): allocate a same-length copy, then a
9+// byte-by-byte transform loop advancing two raw pointers (source cursor
10+// in RSI, dest cursor in RDI) rather than indexing -- same technique
11+// codegen/closures.c's capture-copy loop uses, avoiding any need for a
12+// scaled-index addressing mode this project's emitter doesn't support.
13+static void case_transform(char lo, char hi, int8_t delta) {
14+ emit_pop_reg(code, REG_RAX); // src payload
15+ emit_pop_reg(code, REG_RBX); // tag (ignored, assumed STRING)
16+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length
17+ emit_mov_reg_reg(code, REG_RBX, REG_RAX); // stash src base (survives the alloc call)
18+
19+ string_alloc_emit_prefixed(code); // RAX = new block, RDX = length (preserved)
20+
21+ emit_mov_reg_reg(code, REG_RSI, REG_RBX); // src cursor
22+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
23+ emit_add_reg_imm8(code, REG_RDI, 8); // dest cursor
24+ emit_mov_reg_reg(code, REG_RCX, REG_RDX); // remaining count
25+
26+ int loop_start = code->count;
27+ emit_cmp_reg_imm32(code, REG_RCX, 0);
28+ int loop_exit = emit_jcc_rel32(code, COND_E);
29+ emit_load_byte_reg(code, REG_RBX, REG_RSI); // current byte
30+ emit_cmp_reg_imm32(code, REG_RBX, lo);
31+ int too_low = emit_jcc_rel32(code, COND_LT);
32+ emit_cmp_reg_imm32(code, REG_RBX, hi);
33+ int too_high = emit_jcc_rel32(code, COND_GT);
34+ emit_add_reg_imm8(code, REG_RBX, delta);
35+ int skip = emit_jmp_rel32(code);
36+ emit_patch_jump(code, too_low);
37+ emit_patch_jump(code, too_high);
38+ emit_patch_jump(code, skip);
39+ emit_store_byte_reg(code, REG_RDI, REG_RBX);
40+ emit_add_reg_imm8(code, REG_RSI, 1);
41+ emit_add_reg_imm8(code, REG_RDI, 1);
42+ emit_dec_reg(code, REG_RCX);
43+ emit_jmp_back(code, loop_start);
44+ emit_patch_jump(code, loop_exit);
45+
46+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
47+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
48+ emit_push_reg(code, REG_RBX);
49+ emit_push_reg(code, REG_RAX);
50+}
51+
52+void codegen_builtin_upper(void) { case_transform('a', 'z', -32); }
53+void codegen_builtin_lower(void) { case_transform('A', 'Z', 32); }

kiln/src/codegen/case_builtins.dadded

+10−0
@@ -0,0 +1,10 @@
1+src/codegen/case_builtins.o: src/codegen/case_builtins.c \
2+ src/codegen/string_builtins.h src/codegen/string_alloc.h \
3+ src/codegen/emit.h src/codegen/value.h src/parser/parser.h \
4+ src/lexer/lexer.h
5+src/codegen/string_builtins.h:
6+src/codegen/string_alloc.h:
7+src/codegen/emit.h:
8+src/codegen/value.h:
9+src/parser/parser.h:
10+src/lexer/lexer.h:

kiln/src/codegen/closures.cadded

+117−0
@@ -0,0 +1,117 @@
1+#include "codegen/closures.h"
2+#include "codegen/closures_internal.h"
3+#include "codegen/emit.h"
4+#include "codegen/expr.h"
5+#include "codegen/value.h"
6+#include "codegen/heap.h"
7+#include "codegen/layout.h"
8+#include "parser/parser.h"
9+#include "parser/vars.h"
10+
11+// Closure-object creation and the two ways a TAG_FUNCTION value gets
12+// built (a lambda literal's runtime header, and a zero-capture wrapper
13+// around an already-declared named function). The lambda body's own
14+// codegen (parsing params, compiling the block) lives in lambda.c --
15+// split so neither file drifts past ~200 lines.
16+
17+// Allocates a closure object with room for `capture_count` captures and
18+// stores capture_count into [obj+8]. Leaves RBX = object address; the
19+// code address at [obj+0] is the caller's job (patchable for a lambda
20+// literal, immediate for a named function value -- see below).
21+void closures_alloc_object(int capture_count) {
22+ emit_mov_reg_imm64(code, REG_RDI, (uint64_t)(FUNCTION_OBJECT_HEADER_SIZE + 16 * capture_count));
23+ heap_emit_alloc(code); // RAX = address, clobbers RSI
24+ emit_mov_reg_reg(code, REG_RBX, REG_RAX);
25+ emit_mov_reg_imm64(code, REG_RCX, (uint64_t)capture_count);
26+ emit_store_mem_disp32(code, REG_RBX, 8, REG_RCX);
27+}
28+
29+// Pushes (TAG_FUNCTION, RBX) as this expression's result -- RBX must
30+// already hold the finished closure object's address.
31+void closures_push_value(void) {
32+ emit_mov_reg_imm64(code, REG_RCX, TAG_FUNCTION);
33+ emit_push_reg(code, REG_RCX);
34+ emit_push_reg(code, REG_RBX);
35+}
36+
37+void codegen_named_function_value(KilnFunction *fn) {
38+ closures_alloc_object(0);
39+ uint64_t addr = kiln_code_base() + (uint64_t)fn->code_offset;
40+ emit_mov_reg_imm64(code, REG_RCX, addr);
41+ emit_store_mem_disp32(code, REG_RBX, 0, REG_RCX);
42+ closures_push_value();
43+}
44+
45+// Assumes a closure's (tag, payload) is already pushed, followed by
46+// `argc` more (tag, payload) argument pairs already pushed on top of it
47+// (in order) -- the shared tail of every indirect call, regardless of
48+// where the closure value and arguments came from. codegen_indirect_call
49+// below is the "closure from a source-level variable, args from parsed
50+// syntax" case; codegen/higher_order.c's map/filter/reduce build both
51+// programmatically instead and call this directly.
52+//
53+// Stack shape right before the indirect call (bottom = pushed first):
54+// [closure_tag][closure_payload][arg0_tag][arg0_payload]...[argN-1]
55+// [capture0_tag][capture0_payload]...[captureM-1] <- RSP here
56+// M (capture_count) is only known at runtime (read out of the closure
57+// object), so it's pushed via a genuine runtime loop -- unlike every
58+// other loop-shaped codegen in this project, which is always
59+// compile-time-bounded. Since M must survive the `call` (every GP
60+// register is freely clobbered by the callee) it's stashed in the fixed
61+// RBP-relative scratch slot from vars.h for the RSP cleanup afterward.
62+void codegen_call_closure_value(int argc) {
63+ emit_mov_reg_reg(code, REG_RAX, REG_RSP);
64+ emit_load_mem_disp32(code, REG_RSI, REG_RAX, 16 * argc); // closure object address
65+
66+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 8); // capture_count
67+ emit_store_mem_disp32(code, REG_RBP, call_scratch_offset(), REG_RCX);
68+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 0); // absolute code address
69+ emit_add_reg_imm8(code, REG_RSI, 16); // -> first capture
70+
71+ int loop_start = code->count;
72+ emit_cmp_reg_imm32(code, REG_RCX, 0);
73+ int loop_exit = emit_jcc_rel32(code, COND_E);
74+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // capture tag
75+ emit_push_reg(code, REG_RAX);
76+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // capture payload
77+ emit_push_reg(code, REG_RAX);
78+ emit_add_reg_imm8(code, REG_RSI, 16);
79+ emit_dec_reg(code, REG_RCX);
80+ emit_jmp_back(code, loop_start);
81+ emit_patch_jump(code, loop_exit);
82+
83+ emit_call_indirect(code, REG_RDX);
84+
85+ // Clean up: closure entry + args (compile-time known) + captures
86+ // (runtime-known, recovered from the scratch slot -- registers were
87+ // free to be clobbered by the call, RBP-relative memory wasn't).
88+ emit_add_reg_imm32(code, REG_RSP, 16 * (1 + argc));
89+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, call_scratch_offset());
90+ emit_add_reg_reg(code, REG_RCX, REG_RCX); // *2
91+ emit_add_reg_reg(code, REG_RCX, REG_RCX); // *4
92+ emit_add_reg_reg(code, REG_RCX, REG_RCX); // *8
93+ emit_add_reg_reg(code, REG_RCX, REG_RCX); // *16
94+ emit_add_reg_reg(code, REG_RSP, REG_RCX);
95+
96+ emit_push_reg(code, REG_RBX); // return convention: RBX=tag, RAX=payload
97+ emit_push_reg(code, REG_RAX);
98+}
99+
100+// `id` already resolved to variable `slot`; current sits on the first
101+// argument or ')'. Pushes the closure and parses+pushes the
102+// source-level arguments, then hands off to the shared tail above.
103+void codegen_indirect_call(int slot) {
104+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(slot));
105+ emit_push_reg(code, REG_RBX);
106+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
107+ emit_push_reg(code, REG_RAX);
108+
109+ int argc = 0;
110+ if (current.type != TOKEN_RPAREN) {
111+ codegen_expression(); argc++;
112+ while (current.type == TOKEN_COMMA) { advance_token(); codegen_expression(); argc++; }
113+ }
114+ expect(TOKEN_RPAREN, "expected ')' after arguments");
115+
116+ codegen_call_closure_value(argc);
117+}

kiln/src/codegen/closures.dadded

+16−0
@@ -0,0 +1,16 @@
1+src/codegen/closures.o: src/codegen/closures.c src/codegen/closures.h \
2+ src/parser/functions.h src/codegen/closures_internal.h \
3+ src/codegen/emit.h src/codegen/expr.h src/lexer/lexer.h \
4+ src/codegen/value.h src/codegen/heap.h src/codegen/layout.h \
5+ src/parser/parser.h src/parser/vars.h
6+src/codegen/closures.h:
7+src/parser/functions.h:
8+src/codegen/closures_internal.h:
9+src/codegen/emit.h:
10+src/codegen/expr.h:
11+src/lexer/lexer.h:
12+src/codegen/value.h:
13+src/codegen/heap.h:
14+src/codegen/layout.h:
15+src/parser/parser.h:
16+src/parser/vars.h:

kiln/src/codegen/closures.hadded

+31−0
@@ -0,0 +1,31 @@
1+#ifndef KILN_CLOSURES_H
2+#define KILN_CLOSURES_H
3+#include "parser/functions.h"
4+
5+// `fn (params) { body }` as an EXPRESSION -- current sits on the 'fn'
6+// token. Captures (by value, snapshotted at creation time -- see the
7+// scope-limit note in codegen/value.h) every variable currently visible
8+// in the enclosing scope, then pushes a TAG_FUNCTION value.
9+void codegen_lambda_expr(void);
10+
11+// A bare reference to an already-declared named function used as a value
12+// (not immediately called) -- wraps it in a zero-capture closure object so
13+// it flows through the same TAG_FUNCTION/indirect-call machinery as a
14+// lambda literal.
15+void codegen_named_function_value(KilnFunction *fn);
16+
17+// `id` already resolved to variable `slot` holding a TAG_FUNCTION value;
18+// current sits on the first argument or ')' (the '(' already consumed,
19+// same convention as codegen/expr_call.c's codegen_call). Parses args,
20+// calls indirectly through the closure object, and pushes the (tag,
21+// payload) result.
22+void codegen_indirect_call(int slot);
23+
24+// Assumes a closure's (tag, payload) is already pushed, followed by
25+// `argc` more (tag, payload) argument pairs already pushed on top of it
26+// (in order) -- e.g. codegen/higher_order.c's map/filter/reduce, which
27+// build both programmatically rather than from source syntax. Performs
28+// the indirect call and pushes the (tag, payload) result.
29+void codegen_call_closure_value(int argc);
30+
31+#endif

kiln/src/codegen/closures_internal.hadded

+10−0
@@ -0,0 +1,10 @@
1+#ifndef KILN_CLOSURES_INTERNAL_H
2+#define KILN_CLOSURES_INTERNAL_H
3+
4+// Cross-file link between closures.c (object creation, named-function
5+// values, indirect calls) and lambda.c (lambda literal parsing/codegen) --
6+// not part of the public API (closures.h).
7+void closures_alloc_object(int capture_count);
8+void closures_push_value(void);
9+
10+#endif

kiln/src/codegen/convert_builtins.hadded

+24−0
@@ -0,0 +1,24 @@
1+#ifndef KILN_CONVERT_BUILTINS_H
2+#define KILN_CONVERT_BUILTINS_H
3+
4+// Assumes its one argument already pushed -- codegen/builtins.c's dispatch
5+// calls these after parsing str(x)/num(x)'s argument.
6+
7+// str(number): formats into a heap-allocated, length-prefixed string
8+// (same layout as any other TAG_STRING value) -- the exact same digit
9+// algorithm codegen/print_int.c uses (sign, integer part backward, up to
10+// 6 fraction digits with round-half-up and trailing-zero trim), just
11+// targeting a heap block instead of a direct stdout write. Kept as its
12+// own self-contained copy rather than sharing code with print_int.c's
13+// already-tested routine, to not risk regressing it.
14+void codegen_builtin_str(void);
15+
16+// num(string): a hand-written strtod-equivalent (no libc in this
17+// freestanding binary) -- skips leading spaces/tabs, an optional
18+// sign, integer digits, an optional '.' and fraction digits. Stops at
19+// the first character it can't consume; a string with no valid numeric
20+// prefix at all yields 0.0, matching strtod's own "no conversion"
21+// behavior.
22+void codegen_builtin_num(void);
23+
24+#endif

kiln/src/codegen/emit.cadded

+199−0
@@ -0,0 +1,199 @@
1+#include <stdlib.h>
2+#include <string.h>
3+#include "codegen/emit_internal.h"
4+
5+void code_init(CodeBuf *buf) {
6+ buf->code = NULL;
7+ buf->count = 0;
8+ buf->capacity = 0;
9+}
10+
11+void emit_byte(CodeBuf *buf, uint8_t b) {
12+ if (buf->count >= buf->capacity) {
13+ buf->capacity = buf->capacity == 0 ? 256 : buf->capacity * 2;
14+ buf->code = realloc(buf->code, buf->capacity);
15+ }
16+ buf->code[buf->count++] = b;
17+}
18+
19+void emit_i32(CodeBuf *buf, int32_t v) {
20+ uint8_t b[4];
21+ memcpy(b, &v, 4);
22+ for (int i = 0; i < 4; i++) emit_byte(buf, b[i]);
23+}
24+
25+void emit_u64(CodeBuf *buf, uint64_t v) {
26+ uint8_t b[8];
27+ memcpy(b, &v, 8);
28+ for (int i = 0; i < 8; i++) emit_byte(buf, b[i]);
29+}
30+
31+// ModRM byte for two register operands, register-direct addressing mode.
32+uint8_t modrm_reg(Reg reg_field, Reg rm_field) {
33+ return (uint8_t)(0xC0 | ((reg_field & 7) << 3) | (rm_field & 7));
34+}
35+
36+// ModRM byte for a register-indirect memory operand [base], mod=00 -- only
37+// valid when base isn't RSP (needs a SIB byte) or RBP (mod=00,rm=101 means
38+// RIP-relative instead), which is why this module's callers are required
39+// to route byte-buffer pointers through a plain register like RCX.
40+uint8_t modrm_mem(Reg reg_field, Reg base) {
41+ return (uint8_t)(((reg_field & 7) << 3) | (base & 7));
42+}
43+
44+// ModRM byte for a register-indirect memory operand with a 32-bit
45+// displacement [base+disp32], mod=10 -- safe for RBP (mod=10 has no
46+// special-case meaning, unlike mod=00). RSP as base still needs a SIB
47+// byte even at mod=10, so this is only used with RBP in practice.
48+uint8_t modrm_mem_disp32(Reg reg_field, Reg base) {
49+ return (uint8_t)(0x80 | ((reg_field & 7) << 3) | (base & 7));
50+}
51+
52+void emit_mov_reg_imm64(CodeBuf *buf, Reg dst, uint64_t imm) {
53+ emit_byte(buf, REX_W);
54+ emit_byte(buf, (uint8_t)(0xB8 + dst));
55+ emit_u64(buf, imm);
56+}
57+
58+int emit_mov_reg_imm64_patchable(CodeBuf *buf, Reg dst) {
59+ emit_byte(buf, REX_W);
60+ emit_byte(buf, (uint8_t)(0xB8 + dst));
61+ int patch_offset = buf->count;
62+ emit_u64(buf, 0); // placeholder
63+ return patch_offset;
64+}
65+
66+void emit_patch_imm64(CodeBuf *buf, int patch_offset, uint64_t value) {
67+ memcpy(buf->code + patch_offset, &value, 8);
68+}
69+
70+void emit_mov_reg_reg(CodeBuf *buf, Reg dst, Reg src) {
71+ emit_byte(buf, REX_W);
72+ emit_byte(buf, 0x89); // MOV r/m64, r64
73+ emit_byte(buf, modrm_reg(src, dst));
74+}
75+
76+void emit_add_reg_reg(CodeBuf *buf, Reg dst, Reg src) {
77+ emit_byte(buf, REX_W);
78+ emit_byte(buf, 0x01); // ADD r/m64, r64
79+ emit_byte(buf, modrm_reg(src, dst));
80+}
81+
82+void emit_add_reg_imm8(CodeBuf *buf, Reg reg, int8_t imm) {
83+ emit_byte(buf, REX_W);
84+ emit_byte(buf, 0x83); // ADD/SUB r/m64, imm8 group
85+ emit_byte(buf, modrm_reg((Reg)0, reg)); // /0 = ADD
86+ emit_byte(buf, (uint8_t)imm);
87+}
88+
89+void emit_add_reg_imm32(CodeBuf *buf, Reg reg, int32_t imm) {
90+ emit_byte(buf, REX_W);
91+ emit_byte(buf, 0x81);
92+ emit_byte(buf, modrm_reg((Reg)0, reg)); // /0 = ADD
93+ emit_i32(buf, imm);
94+}
95+
96+void emit_sub_reg_reg(CodeBuf *buf, Reg dst, Reg src) {
97+ emit_byte(buf, REX_W);
98+ emit_byte(buf, 0x29); // SUB r/m64, r64
99+ emit_byte(buf, modrm_reg(src, dst));
100+}
101+
102+void emit_sub_reg_imm8(CodeBuf *buf, Reg reg, int8_t imm) {
103+ emit_byte(buf, REX_W);
104+ emit_byte(buf, 0x83);
105+ emit_byte(buf, modrm_reg((Reg)5, reg)); // /5 = SUB
106+ emit_byte(buf, (uint8_t)imm);
107+}
108+
109+void emit_sub_reg_imm32(CodeBuf *buf, Reg reg, int32_t imm) {
110+ emit_byte(buf, REX_W);
111+ emit_byte(buf, 0x81);
112+ emit_byte(buf, modrm_reg((Reg)5, reg)); // /5 = SUB
113+ emit_i32(buf, imm);
114+}
115+
116+void emit_imul_reg_reg(CodeBuf *buf, Reg dst, Reg src) {
117+ emit_byte(buf, REX_W);
118+ emit_byte(buf, 0x0F);
119+ emit_byte(buf, 0xAF); // IMUL r64, r/m64
120+ emit_byte(buf, modrm_reg(dst, src));
121+}
122+
123+void emit_idiv_reg(CodeBuf *buf, Reg reg) {
124+ emit_byte(buf, REX_W);
125+ emit_byte(buf, 0xF7);
126+ emit_byte(buf, modrm_reg((Reg)7, reg)); // /7 = IDIV
127+}
128+
129+void emit_neg_reg(CodeBuf *buf, Reg reg) {
130+ emit_byte(buf, REX_W);
131+ emit_byte(buf, 0xF7);
132+ emit_byte(buf, modrm_reg((Reg)3, reg)); // /3 = NEG
133+}
134+
135+void emit_dec_reg(CodeBuf *buf, Reg reg) {
136+ emit_byte(buf, REX_W);
137+ emit_byte(buf, 0xFF);
138+ emit_byte(buf, modrm_reg((Reg)1, reg)); // /1 = DEC
139+}
140+
141+void emit_cqo(CodeBuf *buf) {
142+ emit_byte(buf, REX_W);
143+ emit_byte(buf, 0x99);
144+}
145+
146+void emit_cmp_reg_imm32(CodeBuf *buf, Reg reg, int32_t imm) {
147+ emit_byte(buf, REX_W);
148+ emit_byte(buf, 0x81);
149+ emit_byte(buf, modrm_reg((Reg)7, reg)); // /7 = CMP
150+ emit_i32(buf, imm);
151+}
152+
153+void emit_cmp_reg_reg(CodeBuf *buf, Reg a, Reg b) {
154+ emit_byte(buf, REX_W);
155+ emit_byte(buf, 0x39); // CMP r/m64, r64 (computes a - b, sets flags)
156+ emit_byte(buf, modrm_reg(b, a));
157+}
158+
159+void emit_and_reg_reg(CodeBuf *buf, Reg dst, Reg src) {
160+ emit_byte(buf, REX_W);
161+ emit_byte(buf, 0x21); // AND r/m64, r64
162+ emit_byte(buf, modrm_reg(src, dst));
163+}
164+
165+void emit_or_reg_reg(CodeBuf *buf, Reg dst, Reg src) {
166+ emit_byte(buf, REX_W);
167+ emit_byte(buf, 0x09); // OR r/m64, r64
168+ emit_byte(buf, modrm_reg(src, dst));
169+}
170+
171+void emit_xor_reg_reg(CodeBuf *buf, Reg dst, Reg src) {
172+ emit_byte(buf, REX_W);
173+ emit_byte(buf, 0x31); // XOR r/m64, r64
174+ emit_byte(buf, modrm_reg(src, dst));
175+}
176+
177+void emit_not_reg(CodeBuf *buf, Reg reg) {
178+ emit_byte(buf, REX_W);
179+ emit_byte(buf, 0xF7);
180+ emit_byte(buf, modrm_reg((Reg)2, reg)); // /2 = NOT
181+}
182+
183+void emit_shl_reg_cl(CodeBuf *buf, Reg reg) {
184+ emit_byte(buf, REX_W);
185+ emit_byte(buf, 0xD3);
186+ emit_byte(buf, modrm_reg((Reg)4, reg)); // /4 = SHL r/m64, CL
187+}
188+
189+void emit_sar_reg_cl(CodeBuf *buf, Reg reg) {
190+ emit_byte(buf, REX_W);
191+ emit_byte(buf, 0xD3);
192+ emit_byte(buf, modrm_reg((Reg)7, reg)); // /7 = SAR r/m64, CL
193+}
194+
195+void emit_sar_reg_by1(CodeBuf *buf, Reg reg) {
196+ emit_byte(buf, REX_W);
197+ emit_byte(buf, 0xD1);
198+ emit_byte(buf, modrm_reg((Reg)7, reg)); // /7 = SAR r/m64, 1
199+}

kiln/src/codegen/emit.dadded

+4−0
@@ -0,0 +1,4 @@
1+src/codegen/emit.o: src/codegen/emit.c src/codegen/emit_internal.h \
2+ src/codegen/emit.h
3+src/codegen/emit_internal.h:
4+src/codegen/emit.h:

kiln/src/codegen/emit.hadded

+145−0
@@ -0,0 +1,145 @@
1+#ifndef KILN_EMIT_H
2+#define KILN_EMIT_H
3+#include <stdint.h>
4+
5+// Raw growable machine-code byte buffer, filled in directly (no assembler,
6+// no external tool) -- same "hand-encode the bytes yourself" spirit as
7+// ashvm/benchmarks/native_jit.c, generalized into a reusable module.
8+typedef struct {
9+ uint8_t *code;
10+ int count;
11+ int capacity;
12+} CodeBuf;
13+
14+void code_init(CodeBuf *buf);
15+
16+// x86-64 general-purpose register numbers (SysV encoding, low 8 only --
17+// kiln milestone 1 never needs r8-r15, so no REX.R/X/B is ever required).
18+typedef enum {
19+ REG_RAX = 0, REG_RCX = 1, REG_RDX = 2, REG_RBX = 3,
20+ REG_RSP = 4, REG_RBP = 5, REG_RSI = 6, REG_RDI = 7
21+} Reg;
22+
23+typedef enum {
24+ COND_E = 0x4, // je / jz
25+ COND_NE = 0x5, // jne / jnz
26+ COND_LT = 0xC, // jl (signed <) -- integer cmp only
27+ COND_GE = 0xD, // jge (signed >=) -- integer cmp only
28+ COND_LE = 0xE, // jle (signed <=) -- integer cmp only
29+ COND_GT = 0xF, // jg (signed >) -- integer cmp only
30+ COND_B = 0x2, // jb (CF=1) -- after ucomisd: a < b
31+ COND_AE = 0x3, // jae (CF=0) -- after ucomisd: a >= b
32+ COND_BE = 0x6, // jbe (CF=1|ZF=1) -- after ucomisd: a <= b
33+ COND_A = 0x7 // ja (CF=0&ZF=0) -- after ucomisd: a > b
34+} Cond;
35+
36+// All instructions below operate on full 64-bit registers unless noted.
37+void emit_mov_reg_imm64(CodeBuf *buf, Reg dst, uint64_t imm);
38+void emit_mov_reg_reg(CodeBuf *buf, Reg dst, Reg src);
39+void emit_add_reg_reg(CodeBuf *buf, Reg dst, Reg src);
40+void emit_add_reg_imm8(CodeBuf *buf, Reg reg, int8_t imm);
41+void emit_add_reg_imm32(CodeBuf *buf, Reg reg, int32_t imm);
42+void emit_sub_reg_reg(CodeBuf *buf, Reg dst, Reg src);
43+void emit_sub_reg_imm8(CodeBuf *buf, Reg reg, int8_t imm);
44+void emit_sub_reg_imm32(CodeBuf *buf, Reg reg, int32_t imm);
45+void emit_imul_reg_reg(CodeBuf *buf, Reg dst, Reg src);
46+void emit_idiv_reg(CodeBuf *buf, Reg reg);
47+void emit_neg_reg(CodeBuf *buf, Reg reg);
48+void emit_dec_reg(CodeBuf *buf, Reg reg);
49+void emit_cqo(CodeBuf *buf);
50+void emit_cmp_reg_imm32(CodeBuf *buf, Reg reg, int32_t imm);
51+void emit_cmp_reg_reg(CodeBuf *buf, Reg a, Reg b);
52+
53+// Bitwise operators (missing.md): operands are truncated to int64 first
54+// (same cvttsd2si conversion floor()/the array-index paths already use),
55+// matching this project's existing "assume NUMBER, no runtime type
56+// error" scope limit for non-tag-aware operators.
57+void emit_and_reg_reg(CodeBuf *buf, Reg dst, Reg src);
58+void emit_or_reg_reg(CodeBuf *buf, Reg dst, Reg src);
59+void emit_xor_reg_reg(CodeBuf *buf, Reg dst, Reg src);
60+void emit_not_reg(CodeBuf *buf, Reg reg); // unary ~
61+// Shift count comes from CL (the only register-specified form x86-64
62+// offers for a variable shift amount) -- reg must not be RCX itself.
63+void emit_shl_reg_cl(CodeBuf *buf, Reg reg);
64+void emit_sar_reg_cl(CodeBuf *buf, Reg reg); // arithmetic (sign-preserving) shift right
65+void emit_sar_reg_by1(CodeBuf *buf, Reg reg); // shift right by exactly 1 (no CL needed) -- the exponent-by-squaring loop's n >>= 1
66+void emit_push_reg(CodeBuf *buf, Reg reg);
67+void emit_pop_reg(CodeBuf *buf, Reg reg);
68+
69+// mov [base+disp32], src / mov dst, [base+disp32] -- mod=10 disp32
70+// addressing, safe for RBP (unlike the mod=00 forms emit_store_byte_* use,
71+// mod=10 sidesteps the "mod=00,rm=101 means RIP-relative" special case).
72+// disp32 (not disp8) because variable slots are 8 bytes apart and a
73+// 64-slot table needs offsets up to -512, past disp8's -128..127 range.
74+void emit_store_mem_disp32(CodeBuf *buf, Reg base, int32_t disp, Reg src);
75+void emit_load_mem_disp32(CodeBuf *buf, Reg dst, Reg base, int32_t disp);
76+
77+// Byte (8-bit) memory stores through a register-indirect address [reg] --
78+// `reg` must not be RSP/RBP (both need a SIB/disp encoding this module
79+// deliberately doesn't support; callers route byte-buffer pointers through
80+// RCX instead, see codegen/print_int.c).
81+void emit_store_byte_imm(CodeBuf *buf, Reg base, uint8_t imm);
82+void emit_store_byte_reg(CodeBuf *buf, Reg base, Reg src); // src must be RAX/RCX/RDX/RBX (low byte AL/CL/DL/BL)
83+void emit_load_byte_reg(CodeBuf *buf, Reg dst, Reg base); // dst = zero-extended byte from [base] (MOVZX)
84+
85+void emit_syscall(CodeBuf *buf);
86+
87+// Returns the byte offset of the rel32 placeholder to pass to
88+// emit_patch_jump() once the jump target is known (forward jump).
89+int emit_jcc_rel32(CodeBuf *buf, Cond cc);
90+int emit_jmp_rel32(CodeBuf *buf);
91+void emit_patch_jump(CodeBuf *buf, int patch_offset);
92+
93+// Backward jump to an already-known target (loop back-edges), computed
94+// immediately -- no patching needed.
95+void emit_jmp_back(CodeBuf *buf, int target_offset);
96+void emit_jcc_back(CodeBuf *buf, Cond cc, int target_offset);
97+
98+// Backward-only relative CALL (kiln only calls functions declared earlier
99+// in the source, same single-pass limitation ashvm's compiler has -- so
100+// the target's code offset is always already known, no patching needed).
101+void emit_call_back(CodeBuf *buf, int target_offset);
102+void emit_ret(CodeBuf *buf);
103+
104+// CALL r/m64 (opcode FF /2) -- indirect call through a register holding
105+// an absolute runtime address. Needed for calling through a closure
106+// object whose target isn't known until the value is loaded at runtime,
107+// unlike emit_call_back's compile-time-known backward-only calls.
108+void emit_call_indirect(CodeBuf *buf, Reg target);
109+
110+// JMP r/m64 (opcode FF /4) -- indirect jump through a register holding an
111+// absolute runtime address, no return address pushed (unlike
112+// emit_call_indirect). Used by try/catch's raise routine to resume at a
113+// catch block once RSP/RBP have already been unwound to the handler's
114+// saved values -- this is a resumption, not a call, so nothing should be
115+// pushed.
116+void emit_jmp_indirect(CodeBuf *buf, Reg target);
117+
118+// "Emit now, patch later" for an 8-byte absolute-address immediate
119+// (mov reg, imm64), generalizing emit_jmp_rel32/emit_jcc_rel32's pattern
120+// to something other than a rel32 jump target: a lambda literal's
121+// absolute code address isn't known until AFTER its body compiles (which
122+// happens after the closure-creation code that needs to embed it), so the
123+// mov is emitted with a zero placeholder and the returned offset is
124+// passed to emit_patch_imm64() once the real address is known.
125+int emit_mov_reg_imm64_patchable(CodeBuf *buf, Reg dst);
126+void emit_patch_imm64(CodeBuf *buf, int patch_offset, uint64_t value);
127+
128+// LEA reg, [rip+disp32] -- Plan B: the one primitive kiln's runtime code
129+// needs to reference its OWN embedded data once it can be loaded at a
130+// position ld.so chooses (libkilnrt.so), unlike every other address in
131+// this project which is computed as a KILN_LOAD_BASE-relative compile-time
132+// constant (only valid for kiln's always-fixed-base EXECUTABLES). Same
133+// "emit now, patch later" shape as emit_jmp_rel32: returns the patch
134+// offset, pass it to emit_patch_jump() once the target offset is known
135+// (the disp32 is relative to the byte right after itself, i.e. RIP at
136+// that point -- identical math to a rel32 jump, just a different opcode).
137+int emit_lea_rip_disp32(CodeBuf *buf, Reg dst);
138+void emit_lea_rip_back(CodeBuf *buf, Reg dst, int target_offset);
139+
140+// CALL [addr] via 32-bit absolute-displacement addressing -- touches no
141+// register at all. `addr` must fit in 31 bits (true for every kiln
142+// fixed-base address). See emit_mem.c for the encoding rationale.
143+void emit_call_abs32(CodeBuf *buf, uint32_t addr);
144+
145+#endif

kiln/src/codegen/emit_internal.hadded

+18−0
@@ -0,0 +1,18 @@
1+#ifndef KILN_EMIT_INTERNAL_H
2+#define KILN_EMIT_INTERNAL_H
3+#include "codegen/emit.h"
4+
5+// Shared byte-buffer primitives and ModRM builders, used by both emit.c
6+// (register arithmetic) and emit_mem.c (memory/stack/jump instructions).
7+// Not part of the public API (emit.h).
8+#define REX_W 0x48
9+
10+void emit_byte(CodeBuf *buf, uint8_t b);
11+void emit_i32(CodeBuf *buf, int32_t v);
12+void emit_u64(CodeBuf *buf, uint64_t v);
13+
14+uint8_t modrm_reg(Reg reg_field, Reg rm_field);
15+uint8_t modrm_mem(Reg reg_field, Reg base);
16+uint8_t modrm_mem_disp32(Reg reg_field, Reg base);
17+
18+#endif

kiln/src/codegen/emit_mem.cadded

+138−0
@@ -0,0 +1,138 @@
1+#include <string.h>
2+#include "codegen/emit_internal.h"
3+
4+void emit_store_mem_disp32(CodeBuf *buf, Reg base, int32_t disp, Reg src) {
5+ emit_byte(buf, REX_W);
6+ emit_byte(buf, 0x89); // MOV r/m64, r64
7+ emit_byte(buf, modrm_mem_disp32(src, base));
8+ emit_i32(buf, disp);
9+}
10+
11+void emit_load_mem_disp32(CodeBuf *buf, Reg dst, Reg base, int32_t disp) {
12+ emit_byte(buf, REX_W);
13+ emit_byte(buf, 0x8B); // MOV r64, r/m64
14+ emit_byte(buf, modrm_mem_disp32(dst, base));
15+ emit_i32(buf, disp);
16+}
17+
18+void emit_push_reg(CodeBuf *buf, Reg reg) {
19+ emit_byte(buf, (uint8_t)(0x50 + reg));
20+}
21+
22+void emit_pop_reg(CodeBuf *buf, Reg reg) {
23+ emit_byte(buf, (uint8_t)(0x58 + reg));
24+}
25+
26+void emit_store_byte_imm(CodeBuf *buf, Reg base, uint8_t imm) {
27+ emit_byte(buf, 0xC6); // MOV r/m8, imm8
28+ emit_byte(buf, modrm_mem((Reg)0, base));
29+ emit_byte(buf, imm);
30+}
31+
32+void emit_store_byte_reg(CodeBuf *buf, Reg base, Reg src) {
33+ emit_byte(buf, 0x88); // MOV r/m8, r8 (src must be AL/CL/DL/BL -- no REX needed)
34+ emit_byte(buf, modrm_mem(src, base));
35+}
36+
37+void emit_load_byte_reg(CodeBuf *buf, Reg dst, Reg base) {
38+ emit_byte(buf, REX_W);
39+ emit_byte(buf, 0x0F);
40+ emit_byte(buf, 0xB6); // MOVZX r64, r/m8
41+ emit_byte(buf, modrm_mem(dst, base));
42+}
43+
44+void emit_syscall(CodeBuf *buf) {
45+ emit_byte(buf, 0x0F);
46+ emit_byte(buf, 0x05);
47+}
48+
49+int emit_jcc_rel32(CodeBuf *buf, Cond cc) {
50+ emit_byte(buf, 0x0F);
51+ emit_byte(buf, (uint8_t)(0x80 | cc));
52+ int patch_offset = buf->count;
53+ emit_i32(buf, 0); // placeholder
54+ return patch_offset;
55+}
56+
57+int emit_jmp_rel32(CodeBuf *buf) {
58+ emit_byte(buf, 0xE9);
59+ int patch_offset = buf->count;
60+ emit_i32(buf, 0); // placeholder
61+ return patch_offset;
62+}
63+
64+void emit_patch_jump(CodeBuf *buf, int patch_offset) {
65+ int32_t rel = buf->count - (patch_offset + 4);
66+ memcpy(buf->code + patch_offset, &rel, 4);
67+}
68+
69+void emit_jmp_back(CodeBuf *buf, int target_offset) {
70+ emit_byte(buf, 0xE9);
71+ int32_t rel = target_offset - (buf->count + 4);
72+ emit_i32(buf, rel);
73+}
74+
75+void emit_jcc_back(CodeBuf *buf, Cond cc, int target_offset) {
76+ emit_byte(buf, 0x0F);
77+ emit_byte(buf, (uint8_t)(0x80 | cc));
78+ int32_t rel = target_offset - (buf->count + 4);
79+ emit_i32(buf, rel);
80+}
81+
82+void emit_call_back(CodeBuf *buf, int target_offset) {
83+ emit_byte(buf, 0xE8);
84+ int32_t rel = target_offset - (buf->count + 4);
85+ emit_i32(buf, rel);
86+}
87+
88+void emit_ret(CodeBuf *buf) {
89+ emit_byte(buf, 0xC3);
90+}
91+
92+void emit_call_indirect(CodeBuf *buf, Reg target) {
93+ emit_byte(buf, 0xFF);
94+ emit_byte(buf, modrm_reg((Reg)2, target)); // /2 = CALL r/m64
95+}
96+
97+void emit_jmp_indirect(CodeBuf *buf, Reg target) {
98+ emit_byte(buf, 0xFF);
99+ emit_byte(buf, modrm_reg((Reg)4, target)); // /4 = JMP r/m64
100+}
101+
102+int emit_lea_rip_disp32(CodeBuf *buf, Reg dst) {
103+ emit_byte(buf, REX_W);
104+ emit_byte(buf, 0x8D); // LEA r64, m
105+ emit_byte(buf, (uint8_t)(((dst & 7) << 3) | 5)); // mod=00, rm=101 -> RIP-relative
106+ int patch_offset = buf->count;
107+ emit_i32(buf, 0); // placeholder
108+ return patch_offset;
109+}
110+
111+// Same instruction, for the already-emitted-earlier case (e.g. embedded
112+// data emitted before the code that references it, jumped over) -- same
113+// "compute the offset immediately" shape as emit_jmp_back/emit_call_back.
114+void emit_lea_rip_back(CodeBuf *buf, Reg dst, int target_offset) {
115+ emit_byte(buf, REX_W);
116+ emit_byte(buf, 0x8D);
117+ emit_byte(buf, (uint8_t)(((dst & 7) << 3) | 5));
118+ int32_t rel = target_offset - (buf->count + 4);
119+ emit_i32(buf, rel);
120+}
121+
122+// CALL [disp32] -- x86-64's 32-bit-absolute-displacement addressing mode
123+// (mod=00, rm=100 selects SIB, SIB base=101 with no index/base register
124+// means "just disp32", distinct from mod=00,rm=101's RIP-relative
125+// meaning). Dereferences the given absolute address and calls through
126+// the result, touching NO general-purpose register at all -- needed for
127+// Plan B's GOT calls specifically because kiln's runtime routines each
128+// have their own fixed argument registers (RAX/RBX/RDI/RDX depending on
129+// which routine), so there's no register universally safe to clobber
130+// just to hold the GOT address first. Only valid for addresses that fit
131+// in 32 bits (sign-extended) -- true for every address kiln's fixed,
132+// low, non-PIE load base ever produces.
133+void emit_call_abs32(CodeBuf *buf, uint32_t addr) {
134+ emit_byte(buf, 0xFF);
135+ emit_byte(buf, 0x14); // ModRM: mod=00, reg=010(/2=CALL), rm=100(SIB)
136+ emit_byte(buf, 0x25); // SIB: scale=00, index=100(none), base=101(disp32 only)
137+ emit_i32(buf, (int32_t)addr);
138+}

kiln/src/codegen/emit_mem.dadded

+4−0
@@ -0,0 +1,4 @@
1+src/codegen/emit_mem.o: src/codegen/emit_mem.c \
2+ src/codegen/emit_internal.h src/codegen/emit.h
3+src/codegen/emit_internal.h:
4+src/codegen/emit.h:

kiln/src/codegen/emit_sse.cadded

+126−0
@@ -0,0 +1,126 @@
1+#include "codegen/emit_sse.h"
2+#include "codegen/emit_internal.h"
3+
4+// ModRM for two xmm operands (or GP<->xmm), register-direct -- same
5+// mod=11 shape modrm_reg() builds, xmm register numbers 0-7 encode
6+// identically to GP register numbers 0-7 in ModRM.
7+static uint8_t modrm_xmm(int reg_field, int rm_field) {
8+ return (uint8_t)(0xC0 | ((reg_field & 7) << 3) | (rm_field & 7));
9+}
10+
11+void emit_movq_xmm_from_reg(CodeBuf *buf, XReg dst, Reg src) {
12+ emit_byte(buf, 0x66);
13+ emit_byte(buf, REX_W);
14+ emit_byte(buf, 0x0F);
15+ emit_byte(buf, 0x6E); // MOVQ xmm, r/m64
16+ emit_byte(buf, modrm_xmm(dst, src));
17+}
18+
19+void emit_movq_reg_from_xmm(CodeBuf *buf, Reg dst, XReg src) {
20+ emit_byte(buf, 0x66);
21+ emit_byte(buf, REX_W);
22+ emit_byte(buf, 0x0F);
23+ emit_byte(buf, 0x7E); // MOVQ r/m64, xmm
24+ emit_byte(buf, modrm_xmm(src, dst));
25+}
26+
27+void emit_movsd_xmm_xmm(CodeBuf *buf, XReg dst, XReg src) {
28+ emit_byte(buf, 0xF2);
29+ emit_byte(buf, 0x0F);
30+ emit_byte(buf, 0x10); // MOVSD xmm1, xmm2
31+ emit_byte(buf, modrm_xmm(dst, src));
32+}
33+
34+void emit_movsd_load_disp32(CodeBuf *buf, XReg dst, Reg base, int32_t disp) {
35+ emit_byte(buf, 0xF2);
36+ emit_byte(buf, 0x0F);
37+ emit_byte(buf, 0x10); // MOVSD xmm, [base+disp32]
38+ emit_byte(buf, modrm_mem_disp32((Reg)dst, base));
39+ emit_i32(buf, disp);
40+}
41+
42+void emit_movsd_store_disp32(CodeBuf *buf, Reg base, int32_t disp, XReg src) {
43+ emit_byte(buf, 0xF2);
44+ emit_byte(buf, 0x0F);
45+ emit_byte(buf, 0x11); // MOVSD [base+disp32], xmm
46+ emit_byte(buf, modrm_mem_disp32((Reg)src, base));
47+ emit_i32(buf, disp);
48+}
49+
50+void emit_pxor_xmm_xmm(CodeBuf *buf, XReg reg) {
51+ emit_byte(buf, 0x66);
52+ emit_byte(buf, 0x0F);
53+ emit_byte(buf, 0xEF); // PXOR xmm, xmm
54+ emit_byte(buf, modrm_xmm(reg, reg));
55+}
56+
57+void emit_addsd(CodeBuf *buf, XReg dst, XReg src) {
58+ emit_byte(buf, 0xF2); emit_byte(buf, 0x0F); emit_byte(buf, 0x58);
59+ emit_byte(buf, modrm_xmm(dst, src));
60+}
61+
62+void emit_subsd(CodeBuf *buf, XReg dst, XReg src) {
63+ emit_byte(buf, 0xF2); emit_byte(buf, 0x0F); emit_byte(buf, 0x5C);
64+ emit_byte(buf, modrm_xmm(dst, src));
65+}
66+
67+void emit_mulsd(CodeBuf *buf, XReg dst, XReg src) {
68+ emit_byte(buf, 0xF2); emit_byte(buf, 0x0F); emit_byte(buf, 0x59);
69+ emit_byte(buf, modrm_xmm(dst, src));
70+}
71+
72+void emit_divsd(CodeBuf *buf, XReg dst, XReg src) {
73+ emit_byte(buf, 0xF2); emit_byte(buf, 0x0F); emit_byte(buf, 0x5E);
74+ emit_byte(buf, modrm_xmm(dst, src));
75+}
76+
77+void emit_sqrtsd(CodeBuf *buf, XReg dst, XReg src) {
78+ emit_byte(buf, 0xF2); emit_byte(buf, 0x0F); emit_byte(buf, 0x51);
79+ emit_byte(buf, modrm_xmm(dst, src));
80+}
81+
82+void emit_andpd(CodeBuf *buf, XReg dst, XReg src) {
83+ emit_byte(buf, 0x66); emit_byte(buf, 0x0F); emit_byte(buf, 0x54);
84+ emit_byte(buf, modrm_xmm(dst, src));
85+}
86+
87+void emit_cvttsd2si(CodeBuf *buf, Reg dst, XReg src) {
88+ emit_byte(buf, 0xF2);
89+ emit_byte(buf, REX_W);
90+ emit_byte(buf, 0x0F);
91+ emit_byte(buf, 0x2C); // CVTTSD2SI r64, xmm
92+ emit_byte(buf, modrm_xmm(dst, src));
93+}
94+
95+void emit_cvtsi2sd(CodeBuf *buf, XReg dst, Reg src) {
96+ emit_byte(buf, 0xF2);
97+ emit_byte(buf, REX_W);
98+ emit_byte(buf, 0x0F);
99+ emit_byte(buf, 0x2A); // CVTSI2SD xmm, r/m64
100+ emit_byte(buf, modrm_xmm(dst, src));
101+}
102+
103+void emit_ucomisd(CodeBuf *buf, XReg a, XReg b) {
104+ emit_byte(buf, 0x66);
105+ emit_byte(buf, 0x0F);
106+ emit_byte(buf, 0x2E); // UCOMISD xmm1, xmm2
107+ emit_byte(buf, modrm_xmm(a, b));
108+}
109+
110+void emit_push_xmm(CodeBuf *buf, XReg src) {
111+ emit_sub_reg_imm8(buf, REG_RSP, 8);
112+ emit_byte(buf, 0xF2);
113+ emit_byte(buf, 0x0F);
114+ emit_byte(buf, 0x11); // MOVSD [rsp], xmm
115+ emit_byte(buf, (uint8_t)(((src & 7) << 3) | 0x04)); // mod=00, rm=100 (SIB follows)
116+ emit_byte(buf, 0x24); // SIB: scale=00, index=100(none), base=100(rsp)
117+}
118+
119+void emit_pop_xmm(CodeBuf *buf, XReg dst) {
120+ emit_byte(buf, 0xF2);
121+ emit_byte(buf, 0x0F);
122+ emit_byte(buf, 0x10); // MOVSD xmm, [rsp]
123+ emit_byte(buf, (uint8_t)(((dst & 7) << 3) | 0x04));
124+ emit_byte(buf, 0x24);
125+ emit_add_reg_imm8(buf, REG_RSP, 8);
126+}

kiln/src/codegen/emit_sse.dadded

+5−0
@@ -0,0 +1,5 @@
1+src/codegen/emit_sse.o: src/codegen/emit_sse.c src/codegen/emit_sse.h \
2+ src/codegen/emit.h src/codegen/emit_internal.h
3+src/codegen/emit_sse.h:
4+src/codegen/emit.h:
5+src/codegen/emit_internal.h:

kiln/src/codegen/emit_sse.hadded

+45−0
@@ -0,0 +1,45 @@
1+#ifndef KILN_EMIT_SSE_H
2+#define KILN_EMIT_SSE_H
3+#include "codegen/emit.h"
4+
5+// Scalar double-precision (SSE2) primitives -- milestone 4 replaces
6+// kiln's integer-only numbers with real doubles throughout, matching
7+// ashvm's VMValue.number (always double). Kiln milestone 1-3 never
8+// needed these; everything user-visible now flows through XMM registers
9+// instead of GP registers. GP registers are still used for internal
10+// plumbing that isn't an ash-visible value (loop counters in the print
11+// routine, syscall arguments, frame pointers).
12+typedef enum { XMM0 = 0, XMM1 = 1, XMM2 = 2, XMM3 = 3 } XReg;
13+
14+void emit_movq_xmm_from_reg(CodeBuf *buf, XReg dst, Reg src); // MOVQ xmm, r64 -- loads a GP register's raw 64 bits as a double's bit pattern (used for literals: the literal's IEEE-754 bits are computed once in kiln itself, at compile time)
15+void emit_movq_reg_from_xmm(CodeBuf *buf, Reg dst, XReg src); // MOVQ r64, xmm -- the reverse: extracts a computed double's raw bits into a GP register, milestone 5's generic (tag, payload) values are always GP-transported (see codegen/value.h), so an arithmetic result has to come back out of XMM before it can be pushed
16+void emit_movsd_xmm_xmm(CodeBuf *buf, XReg dst, XReg src);
17+void emit_movsd_load_disp32(CodeBuf *buf, XReg dst, Reg base, int32_t disp);
18+void emit_movsd_store_disp32(CodeBuf *buf, Reg base, int32_t disp, XReg src);
19+void emit_pxor_xmm_xmm(CodeBuf *buf, XReg reg); // zeroes a register (reg,reg) -- also used as "load +0.0"
20+
21+void emit_addsd(CodeBuf *buf, XReg dst, XReg src);
22+void emit_subsd(CodeBuf *buf, XReg dst, XReg src);
23+void emit_mulsd(CodeBuf *buf, XReg dst, XReg src);
24+void emit_divsd(CodeBuf *buf, XReg dst, XReg src);
25+void emit_sqrtsd(CodeBuf *buf, XReg dst, XReg src);
26+void emit_andpd(CodeBuf *buf, XReg dst, XReg src); // milestone 10's abs(): AND against a sign-bit-clearing mask
27+
28+void emit_cvttsd2si(CodeBuf *buf, Reg dst, XReg src); // truncate double -> int64
29+void emit_cvtsi2sd(CodeBuf *buf, XReg dst, Reg src); // int64 -> double
30+
31+// Sets flags the same way an unsigned integer compare would (CF/ZF, not
32+// SF/OF) -- callers must use COND_B/COND_BE/COND_A/COND_AE/COND_E/COND_NE
33+// after this, never the signed COND_LT/LE/GT/GE from emit.h.
34+void emit_ucomisd(CodeBuf *buf, XReg a, XReg b);
35+
36+// x86-64 has no hardware PUSH/POP for XMM registers -- these hand-build
37+// the equivalent (sub rsp,8 + movsd [rsp] / movsd [rsp] + add rsp,8) so
38+// the same stack-machine codegen model expr.c/expr_bool.c already use for
39+// GP registers works unchanged for doubles. The fixed SIB byte (0x24)
40+// these use is the standard encoding for "[rsp]" with no displacement --
41+// RSP as a base always needs a SIB byte, unlike every other register.
42+void emit_push_xmm(CodeBuf *buf, XReg src);
43+void emit_pop_xmm(CodeBuf *buf, XReg dst);
44+
45+#endif

kiln/src/codegen/errors.cadded

+164−0
@@ -0,0 +1,164 @@
1+#include <string.h>
2+#include "codegen/errors.h"
3+#include "codegen/emit_internal.h"
4+#include "codegen/value.h"
5+#include "codegen/layout.h"
6+#include "codegen/runtime_layout.h"
7+#include "codegen/platform_console.h"
8+#include "codegen/win_call.h"
9+#include "elf/elf_dynamic_call.h"
10+#include "target.h"
11+
12+static int raise_routine_offset = -1;
13+
14+static int is_shared(void) {
15+ return kiln_get_link_mode() == KILN_LINK_SHARED && !kiln_is_compiling_so();
16+}
17+
18+// Embeds `msg` as a proper length-prefixed string (jumped over, never
19+// executed as instructions) -- the SAME layout codegen/strings.c's
20+// literals use ([payload-8]=length, no trailing newline in the data
21+// itself), since milestone 9 makes a caught error a real, usable
22+// TAG_STRING value (print/concat/equality all assume this layout). This
23+// embedding always happens in the EXECUTABLE (call sites for
24+// errors_emit_die/fatal live all over the codebase, never in the
25+// extracted runtime), so kiln_code_base() -- which only knows a fixed
26+// EXECUTABLE's own base -- is always the right address to use here, even
27+// under --link=shared.
28+static void embed_message(CodeBuf *code, const char *msg, uint64_t *out_addr, int *out_len) {
29+ int len = (int)strlen(msg);
30+ int skip = emit_jmp_rel32(code);
31+ int data_offset = code->count;
32+ emit_u64(code, (uint64_t)len);
33+ for (int i = 0; i < len; i++) emit_byte(code, (uint8_t)msg[i]);
34+ emit_patch_jump(code, skip);
35+ *out_addr = kiln_code_base() + (uint64_t)data_offset + 8;
36+ *out_len = len;
37+}
38+
39+// Raise routine -- inputs RSI=message addr, RDX=message len. If a `try`
40+// is active (kiln_try_depth_addr() > 0), pops the innermost handler,
41+// restores its saved RSP/RBP (kiln's hand-rolled longjmp -- no libc
42+// setjmp in a freestanding binary), stores the message as a TAG_STRING
43+// value into the handler's recorded error-variable slot, and jumps to
44+// its catch block. Otherwise falls through to an inline fatal path
45+// (write to stderr, exit 1).
46+static void emit_raise_routine(CodeBuf *code) {
47+ int skip = emit_jmp_rel32(code);
48+ raise_routine_offset = code->count;
49+
50+ emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());
51+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 0);
52+ emit_cmp_reg_imm32(code, REG_RAX, 0);
53+ int go_fatal = emit_jcc_rel32(code, COND_E);
54+
55+ emit_dec_reg(code, REG_RAX);
56+ emit_mov_reg_imm64(code, REG_RCX, kiln_try_depth_addr());
57+ emit_store_mem_disp32(code, REG_RCX, 0, REG_RAX); // pop handler
58+
59+ emit_mov_reg_imm64(code, REG_RCX, KILN_TRY_HANDLER_SIZE);
60+ emit_imul_reg_reg(code, REG_RAX, REG_RCX); // RAX = index * handler size
61+ emit_mov_reg_imm64(code, REG_RDI, kiln_try_handlers_addr());
62+ emit_add_reg_reg(code, REG_RAX, REG_RDI); // RAX = handler slot address
63+
64+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 0); // saved rsp
65+ emit_load_mem_disp32(code, REG_RDI, REG_RAX, 8); // saved rbp
66+ 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)
67+ emit_load_mem_disp32(code, REG_RBX, REG_RAX, 24); // error variable's rbp-relative tag offset
68+
69+ emit_mov_reg_reg(code, REG_RSP, REG_RCX);
70+ emit_mov_reg_reg(code, REG_RBP, REG_RDI);
71+
72+ emit_mov_reg_reg(code, REG_RCX, REG_RBP);
73+ emit_add_reg_reg(code, REG_RCX, REG_RBX); // RCX = error variable's address
74+ emit_mov_reg_imm64(code, REG_RAX, TAG_STRING);
75+ emit_store_mem_disp32(code, REG_RCX, 0, REG_RAX); // tag
76+ emit_store_mem_disp32(code, REG_RCX, 8, REG_RSI); // payload = message address
77+
78+ emit_jmp_indirect(code, REG_RDX);
79+
80+ emit_patch_jump(code, go_fatal);
81+ // RSI/RDX still hold this call's original message addr/len -- nothing
82+ // between the raise routine's entry and here touches them on the path
83+ // that skips straight past the try-active handling.
84+ platform_emit_write_stderr(code);
85+ emit_sub_reg_imm8(code, REG_RSP, 8);
86+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
87+ emit_store_byte_imm(code, REG_RCX, '\n');
88+ emit_mov_reg_reg(code, REG_RSI, REG_RCX);
89+ emit_mov_reg_imm64(code, REG_RDX, 1);
90+ platform_emit_write_stderr(code);
91+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
92+ win_call_begin(code, 0);
93+ win_call_arg_imm64(code, 0, 1); // ExitProcess(1)
94+ win_call_import(code, PE_IMPORT_EXIT_PROCESS);
95+ } else {
96+ emit_mov_reg_imm64(code, REG_RDI, 1); // exit status 1
97+ emit_mov_reg_imm64(code, REG_RAX, 60); // syscall: exit
98+ emit_syscall(code);
99+ }
100+
101+ emit_patch_jump(code, skip);
102+}
103+
104+void errors_emit_raise_routine_only(CodeBuf *code) {
105+ emit_raise_routine(code);
106+}
107+
108+int errors_raise_routine_offset(void) { return raise_routine_offset; }
109+
110+void errors_emit_startup(CodeBuf *code) {
111+ if (!is_shared()) emit_raise_routine(code);
112+}
113+
114+static void emit_die(CodeBuf *code) {
115+ if (is_shared()) {
116+ if (kiln_get_target() == KILN_TARGET_WINDOWS) win_call_runtime_import(code, RUNTIME_IMPORT_RAISE);
117+ else elf_dynamic_call(code, RUNTIME_IMPORT_RAISE);
118+ return;
119+ }
120+ emit_jmp_back(code, raise_routine_offset); // never returns -- no need to come back
121+}
122+
123+void errors_emit_die(CodeBuf *code, const char *msg) {
124+ uint64_t addr; int len;
125+ embed_message(code, msg, &addr, &len);
126+ emit_mov_reg_imm64(code, REG_RSI, addr);
127+ emit_mov_reg_imm64(code, REG_RDX, (uint64_t)len);
128+ emit_die(code);
129+}
130+
131+// The ONE case that must never be "catchable" (exceeding
132+// MAX_KILN_TRY_DEPTH itself -- silently continuing there would mean
133+// pushing a handler past the fixed table, corrupting the adjacent code
134+// segment). Only ever called from one site (parser/try_catch.c), so
135+// there's no deduplication payoff in routing it through the shared
136+// runtime at all -- it just emits its own small inline write+exit
137+// sequence directly, in every link mode, decoupled from wherever RAISE
138+// itself happens to live.
139+void errors_emit_fatal(CodeBuf *code, const char *msg) {
140+ uint64_t addr; int len;
141+ embed_message(code, msg, &addr, &len);
142+ emit_mov_reg_imm64(code, REG_RSI, addr);
143+ emit_mov_reg_imm64(code, REG_RDX, (uint64_t)len);
144+ platform_emit_write_stderr(code);
145+ emit_sub_reg_imm8(code, REG_RSP, 8);
146+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
147+ emit_store_byte_imm(code, REG_RCX, '\n');
148+ emit_mov_reg_reg(code, REG_RSI, REG_RCX);
149+ emit_mov_reg_imm64(code, REG_RDX, 1);
150+ platform_emit_write_stderr(code);
151+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
152+ win_call_begin(code, 0);
153+ win_call_arg_imm64(code, 0, 1);
154+ win_call_import(code, PE_IMPORT_EXIT_PROCESS);
155+ } else {
156+ emit_mov_reg_imm64(code, REG_RDI, 1);
157+ emit_mov_reg_imm64(code, REG_RAX, 60);
158+ emit_syscall(code);
159+ }
160+}
161+
162+void errors_emit_die_dynamic(CodeBuf *code) {
163+ emit_die(code);
164+}

kiln/src/codegen/errors.dadded

+23−0
@@ -0,0 +1,23 @@
1+src/codegen/errors.o: src/codegen/errors.c src/codegen/errors.h \
2+ src/codegen/emit.h src/codegen/emit_internal.h src/codegen/value.h \
3+ src/codegen/layout.h src/codegen/runtime_layout.h \
4+ src/codegen/platform_console.h src/codegen/win_call.h src/pe/pe_writer.h \
5+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
6+ src/pe/pe_imports.def src/elf/elf_dynamic_call.h src/elf/elf_dynamic.h \
7+ src/elf/elf_writer.h src/target.h
8+src/codegen/errors.h:
9+src/codegen/emit.h:
10+src/codegen/emit_internal.h:
11+src/codegen/value.h:
12+src/codegen/layout.h:
13+src/codegen/runtime_layout.h:
14+src/codegen/platform_console.h:
15+src/codegen/win_call.h:
16+src/pe/pe_writer.h:
17+src/codegen/runtime_import_enum.h:
18+src/codegen/runtime_exports.def:
19+src/pe/pe_imports.def:
20+src/elf/elf_dynamic_call.h:
21+src/elf/elf_dynamic.h:
22+src/elf/elf_writer.h:
23+src/target.h:

kiln/src/codegen/errors.hadded

+42−0
@@ -0,0 +1,42 @@
1+#ifndef KILN_ERRORS_H
2+#define KILN_ERRORS_H
3+#include "codegen/emit.h"
4+
5+// Runtime (not compile-time -- see parser/parser.h's parse_error for
6+// that) errors: array out of bounds, missing map key, and so on.
7+// Emitted once, at the very start of the generated program (right after
8+// codegen/heap.c's heap_emit_startup): a single reusable "raise" routine
9+// that every runtime error jumps to (never called -- there's nothing to
10+// return to, control either resumes at a catch block or the process
11+// exits).
12+void errors_emit_startup(CodeBuf *code);
13+
14+// Plan B: emits ONLY the raise routine's body -- used by the standalone
15+// "compile the runtime" driver that builds libkilnrt.so.
16+void errors_emit_raise_routine_only(CodeBuf *code);
17+int errors_raise_routine_offset(void);
18+
19+// Embeds `msg` (compile-time-known, jumped over so it's never executed
20+// as instructions -- same technique codegen/strings.c uses for string
21+// literals) and raises it: if a `try` is currently active (see
22+// parser/try_catch.c), unwinds to its `catch` block with `msg` bound as
23+// a TAG_STRING value; otherwise writes it to stderr and exits with
24+// status 1, same as before milestone 9.
25+void errors_emit_die(CodeBuf *code, const char *msg);
26+
27+// Like errors_emit_die, but ALWAYS fatal -- bypasses the try/catch check
28+// entirely. Used only for the one internal case that must never be
29+// "catchable": exceeding MAX_KILN_TRY_DEPTH itself (silently continuing
30+// there would mean pushing a handler past the fixed table, corrupting
31+// the adjacent code segment).
32+void errors_emit_fatal(CodeBuf *code, const char *msg);
33+
34+// Like errors_emit_die, but for a RUNTIME string value (parser.c's
35+// `throw expr;`) instead of a compile-time-constant C string -- the
36+// caller must already have RSI=message payload and RDX=message length
37+// loaded (a throw expression's already-popped (tag,payload), payload is
38+// already a valid length-prefixed TAG_STRING pointer, so there's nothing
39+// to embed, just the same jump to the raise routine errors_emit_die uses.
40+void errors_emit_die_dynamic(CodeBuf *code);
41+
42+#endif

kiln/src/codegen/expr.cadded

+421−0
@@ -0,0 +1,421 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "codegen/expr.h"
5+#include "codegen/expr_internal.h"
6+#include "codegen/emit.h"
7+#include "codegen/emit_sse.h"
8+#include "codegen/value.h"
9+#include "codegen/strings.h"
10+#include "codegen/arrays.h"
11+#include "codegen/maps.h"
12+#include "codegen/builtins.h"
13+#include "codegen/closures.h"
14+#include "parser/parser.h"
15+#include "parser/vars.h"
16+#include "parser/functions.h"
17+#include "parser/constants.h"
18+
19+// Precedence climbing: expression -> logical_or -> logical_and ->
20+// comparison -> additive -> term -> unary -> postfix -> primary
21+// (comparison and up live in expr_bool.c). Every level, on completion,
22+// has pushed exactly
23+// one (tag, payload) pair onto the real CPU stack -- tag first (deeper),
24+// payload second (on top), so popping retrieves payload then tag.
25+//
26+// Milestone 5: a value can now be a number OR a string (codegen/value.h),
27+// so both halves travel generically via GP registers (raw bit-copying
28+// doesn't care what the bits mean). XMM registers only ever hold the
29+// payload transiently, at the point of actually doing floating-point
30+// arithmetic on a NUMBER-tagged value -- extracted via
31+// emit_movq_xmm_from_reg, put back via emit_movq_reg_from_xmm.
32+//
33+// Scope limit (see the plan): only `+` and `==`/`!=` are tag-aware.
34+// `- * / < <= > >=` assume NUMBER operands, same as ashvm's own
35+// operators do when given the wrong type (ashvm raises a runtime type
36+// error there; kiln doesn't have runtime errors yet -- a future
37+// milestone, not silently pretended-away here).
38+
39+static void power(void);
40+
41+static void primary(void) {
42+ if (current.type == TOKEN_NUMBER) {
43+ // strtod() (real libc, called here at compile time on the host --
44+ // not codegen) natively parses hex ("0x1A") and scientific
45+ // notation ("1e10") once the lexer hands back the right token
46+ // boundaries (see lexer.c's number()); the one thing it doesn't
47+ // understand is underscore digit separators (1_000_000), so
48+ // those get stripped into a clean buffer first.
49+ char clean[128];
50+ int clean_len = 0;
51+ for (int i = 0; i < current.length; i++) {
52+ if (current.start[i] == '_') continue;
53+ if (clean_len >= (int)sizeof(clean) - 1) parse_error("number literal too long");
54+ clean[clean_len++] = current.start[i];
55+ }
56+ clean[clean_len] = '\0';
57+ double v = strtod(clean, NULL);
58+ advance_token();
59+ uint64_t bits;
60+ memcpy(&bits, &v, sizeof(bits));
61+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
62+ emit_push_reg(code, REG_RBX);
63+ emit_mov_reg_imm64(code, REG_RAX, bits);
64+ emit_push_reg(code, REG_RAX);
65+ return;
66+ }
67+ if (current.type == TOKEN_STRING) {
68+ codegen_string_literal();
69+ return;
70+ }
71+ if (current.type == TOKEN_FN) {
72+ codegen_lambda_expr();
73+ return;
74+ }
75+ if (current.type == TOKEN_TRUE || current.type == TOKEN_FALSE) {
76+ double v = current.type == TOKEN_TRUE ? 1.0 : 0.0;
77+ advance_token();
78+ uint64_t bits;
79+ memcpy(&bits, &v, sizeof(bits));
80+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
81+ emit_push_reg(code, REG_RBX);
82+ emit_mov_reg_imm64(code, REG_RAX, bits);
83+ emit_push_reg(code, REG_RAX);
84+ return;
85+ }
86+ if (current.type == TOKEN_NIL) {
87+ advance_token();
88+ emit_mov_reg_imm64(code, REG_RBX, TAG_NIL);
89+ emit_push_reg(code, REG_RBX);
90+ emit_mov_reg_imm64(code, REG_RAX, 0);
91+ emit_push_reg(code, REG_RAX);
92+ return;
93+ }
94+ if (current.type == TOKEN_IDENTIFIER) {
95+ Token id = current;
96+ advance_token();
97+
98+ if (current.type == TOKEN_DOT) {
99+ advance_token();
100+ expect(TOKEN_IDENTIFIER, "expected name after '.'");
101+ Token member = previous;
102+
103+ char combined[128];
104+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s",
105+ id.length, id.start, member.length, member.start);
106+
107+ if (current.type == TOKEN_LPAREN) {
108+ advance_token();
109+ codegen_call_named(combined, combined_len);
110+ return;
111+ }
112+
113+ KilnConstant *k = resolve_constant(combined, combined_len);
114+ if (k != NULL) { codegen_constant_value(k); return; }
115+
116+ parse_error("undefined namespaced function or constant");
117+ }
118+
119+ if (current.type == TOKEN_LPAREN) {
120+ advance_token();
121+ codegen_call(id);
122+ return;
123+ }
124+ int slot = resolve_var(id.start, id.length);
125+ if (slot != -1) {
126+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(slot));
127+ emit_push_reg(code, REG_RBX);
128+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
129+ emit_push_reg(code, REG_RAX);
130+ return;
131+ }
132+ // Not a variable -- fall back to a bare named-function reference
133+ // used as a value (not immediately called), wrapped as a
134+ // zero-capture closure (see codegen/closures.c).
135+ KilnFunction *fn = resolve_function_in_context(id.start, id.length);
136+ if (fn != NULL) {
137+ codegen_named_function_value(fn);
138+ return;
139+ }
140+ // Not a function either -- if we're compiling inside an imported
141+ // file's own namespace context, a bare reference may be that same
142+ // file's own top-level constant: let_statement always registers
143+ // imported `let`s under their namespaced name (e.g. "lib.PI"),
144+ // even for references from within lib.ash itself, so a bare "PI"
145+ // used inside lib.ash needs this same "in context" namespacing
146+ // resolve_function_in_context already does for bare function
147+ // names above -- otherwise a constant could never be used from
148+ // within the file that declares it.
149+ const char *ns; int ns_len;
150+ get_import_namespace(&ns, &ns_len);
151+ if (ns_len > 0) {
152+ char combined[128];
153+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s", ns_len, ns, id.length, id.start);
154+ KilnConstant *k = resolve_constant(combined, combined_len);
155+ if (k != NULL) { codegen_constant_value(k); return; }
156+ }
157+ char msg[128];
158+ snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
159+ parse_error(msg);
160+ }
161+ if (current.type == TOKEN_LPAREN) {
162+ advance_token();
163+ codegen_expression();
164+ expect(TOKEN_RPAREN, "expected ')' after expression");
165+ return;
166+ }
167+ if (current.type == TOKEN_LBRACKET) {
168+ codegen_array_literal();
169+ return;
170+ }
171+ if (current.type == TOKEN_LBRACE) {
172+ codegen_map_literal();
173+ return;
174+ }
175+ parse_error("expected a number, string, array, map, variable, or '('");
176+}
177+
178+// `x[index]` reads -- arrays and maps both use this syntax, and which one
179+// `x` actually is can only be known at runtime, so this pops both values
180+// just far enough to check the base's tag, pushes them straight back in
181+// the same order, and dispatches. A while loop (not a single check) so
182+// chains like `arr[0][1]` work, matching ashvm/src/compiler/expr.c's
183+// postfix() shape.
184+static void postfix(void) {
185+ primary();
186+ while (current.type == TOKEN_LBRACKET) {
187+ advance_token();
188+ codegen_expression();
189+ expect(TOKEN_RBRACKET, "expected ']' after index");
190+
191+ emit_pop_reg(code, REG_RAX); // index payload
192+ emit_pop_reg(code, REG_RBX); // index tag
193+ emit_pop_reg(code, REG_RCX); // base payload
194+ emit_pop_reg(code, REG_RDX); // base tag
195+ emit_push_reg(code, REG_RDX);
196+ emit_push_reg(code, REG_RCX);
197+ emit_push_reg(code, REG_RBX);
198+ emit_push_reg(code, REG_RAX);
199+ emit_cmp_reg_imm32(code, REG_RDX, TAG_MAP);
200+ int is_map = emit_jcc_rel32(code, COND_E);
201+ emit_cmp_reg_imm32(code, REG_RDX, TAG_STRING);
202+ int is_string = emit_jcc_rel32(code, COND_E);
203+ codegen_array_index_read();
204+ int done = emit_jmp_rel32(code);
205+ emit_patch_jump(code, is_map);
206+ codegen_map_index_read();
207+ int done2 = emit_jmp_rel32(code);
208+ emit_patch_jump(code, is_string);
209+ codegen_string_index_read();
210+ emit_patch_jump(code, done);
211+ emit_patch_jump(code, done2);
212+ }
213+}
214+
215+static void unary(void) {
216+ // Kiln-vs-ashvm parity gap (missing.md): ashvm implements this
217+ // (TOKEN_BANG -> OP_NOT); kiln never wired it up. Logical not: pushes
218+ // 1.0 if the operand's payload is zero, else 0.0 -- matches
219+ // pop_and_test_truthy's "NUMBER != 0" definition of truthy elsewhere
220+ // in this file, and works for nil too since nil's payload is always
221+ // exactly 0 (see value.h's TAG_NIL comment).
222+ if (current.type == TOKEN_BANG) {
223+ advance_token();
224+ unary();
225+ emit_pop_reg(code, REG_RAX); // payload
226+ emit_pop_reg(code, REG_RBX); // tag (ignored)
227+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
228+ emit_pxor_xmm_xmm(code, XMM1);
229+ emit_ucomisd(code, XMM0, XMM1);
230+ int is_zero = emit_jcc_rel32(code, COND_E);
231+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
232+ emit_push_reg(code, REG_RBX);
233+ emit_pxor_xmm_xmm(code, XMM0);
234+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
235+ emit_push_reg(code, REG_RAX); // nonzero operand -> 0.0
236+ int done = emit_jmp_rel32(code);
237+ emit_patch_jump(code, is_zero);
238+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
239+ emit_push_reg(code, REG_RBX);
240+ double one = 1.0;
241+ uint64_t one_bits;
242+ memcpy(&one_bits, &one, sizeof(one_bits));
243+ emit_mov_reg_imm64(code, REG_RAX, one_bits);
244+ emit_push_reg(code, REG_RAX); // zero operand -> 1.0
245+ emit_patch_jump(code, done);
246+ return;
247+ }
248+ if (current.type == TOKEN_MINUS) {
249+ advance_token();
250+ unary();
251+ emit_pop_reg(code, REG_RAX); // payload
252+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
253+ emit_movq_xmm_from_reg(code, XMM1, REG_RAX);
254+ emit_pxor_xmm_xmm(code, XMM0);
255+ emit_subsd(code, XMM0, XMM1); // XMM0 = 0.0 - operand
256+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
257+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
258+ emit_push_reg(code, REG_RBX);
259+ emit_push_reg(code, REG_RAX);
260+ return;
261+ }
262+ if (current.type == TOKEN_TILDE) {
263+ advance_token();
264+ unary();
265+ emit_pop_reg(code, REG_RAX); // payload
266+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
267+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
268+ emit_cvttsd2si(code, REG_RAX, XMM0); // -> int64
269+ emit_not_reg(code, REG_RAX);
270+ emit_cvtsi2sd(code, XMM0, REG_RAX);
271+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
272+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
273+ emit_push_reg(code, REG_RBX);
274+ emit_push_reg(code, REG_RAX);
275+ return;
276+ }
277+ power();
278+}
279+
280+// x ** y -- sits between unary and postfix, so `-2 ** 2` parses as
281+// `-(2 ** 2)` (Python's convention) and `2 ** 3 ** 2` is right-associative
282+// (the exponent is parsed via unary(), not power(), so it recurses back
283+// through power() for the next `**`): `2 ** 3 ** 2` = `2 ** (3 ** 2)`.
284+//
285+// Integer exponents only (documented simplification, not silently wrong:
286+// the exponent is truncated via cvttsd2si same as the bitwise operators
287+// truncate their operands) -- computed by exponentiation-by-squaring, a
288+// genuine runtime loop since the exponent is only known at runtime,
289+// unlike every other arithmetic operator here. Negative exponents work
290+// via computing on the absolute value then reciprocating. No x87
291+// FYL2X/F2XM1 in this project (SSE2 only), so fractional exponents
292+// (x ** 0.5) aren't supported -- that would need a real pow(), out of
293+// scope here.
294+static void power(void) {
295+ postfix();
296+ if (current.type == TOKEN_STAR_STAR) {
297+ advance_token();
298+ unary(); // exponent -- right-associative, allows `2 ** -3`
299+
300+ emit_pop_reg(code, REG_RAX); // exponent payload
301+ emit_pop_reg(code, REG_RBX); // exponent tag (ignored)
302+ emit_pop_reg(code, REG_RCX); // base payload
303+ emit_pop_reg(code, REG_RDX); // base tag (ignored)
304+
305+ emit_movq_xmm_from_reg(code, XMM0, REG_RCX); // base
306+ emit_movq_xmm_from_reg(code, XMM2, REG_RAX); // exponent (double), temp
307+ emit_cvttsd2si(code, REG_RCX, XMM2); // n = truncate(exponent)
308+
309+ emit_mov_reg_imm64(code, REG_RBX, 0); // neg flag
310+ emit_cmp_reg_imm32(code, REG_RCX, 0);
311+ int not_neg = emit_jcc_rel32(code, COND_GE);
312+ emit_mov_reg_imm64(code, REG_RBX, 1);
313+ emit_neg_reg(code, REG_RCX); // n = |n|
314+ emit_patch_jump(code, not_neg);
315+
316+ double one = 1.0;
317+ uint64_t one_bits;
318+ memcpy(&one_bits, &one, sizeof(one_bits));
319+ emit_mov_reg_imm64(code, REG_RAX, one_bits);
320+ emit_movq_xmm_from_reg(code, XMM1, REG_RAX); // result = 1.0
321+
322+ int loop_start = code->count;
323+ emit_cmp_reg_imm32(code, REG_RCX, 0);
324+ int loop_exit = emit_jcc_rel32(code, COND_LE);
325+
326+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
327+ emit_mov_reg_imm64(code, REG_RDX, 1);
328+ emit_and_reg_reg(code, REG_RAX, REG_RDX); // RAX = n & 1
329+ emit_cmp_reg_imm32(code, REG_RAX, 0);
330+ int skip_mul = emit_jcc_rel32(code, COND_E);
331+ emit_mulsd(code, XMM1, XMM0); // result *= base
332+ emit_patch_jump(code, skip_mul);
333+
334+ emit_mulsd(code, XMM0, XMM0); // base *= base
335+ emit_sar_reg_by1(code, REG_RCX); // n >>= 1
336+ emit_jmp_back(code, loop_start);
337+ emit_patch_jump(code, loop_exit);
338+
339+ emit_cmp_reg_imm32(code, REG_RBX, 0);
340+ int not_negative = emit_jcc_rel32(code, COND_E);
341+ emit_mov_reg_imm64(code, REG_RAX, one_bits);
342+ emit_movq_xmm_from_reg(code, XMM2, REG_RAX);
343+ emit_divsd(code, XMM2, XMM1); // XMM2 = 1.0 / result
344+ emit_movsd_xmm_xmm(code, XMM1, XMM2);
345+ emit_patch_jump(code, not_negative);
346+
347+ emit_movq_reg_from_xmm(code, REG_RAX, XMM1);
348+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
349+ emit_push_reg(code, REG_RBX);
350+ emit_push_reg(code, REG_RAX);
351+ }
352+}
353+
354+static void term(void) {
355+ unary();
356+ while (current.type == TOKEN_STAR || current.type == TOKEN_SLASH) {
357+ TokenType op = current.type;
358+ advance_token();
359+ unary();
360+ emit_pop_reg(code, REG_RCX); // b payload
361+ emit_pop_reg(code, REG_RDX); // b tag (assumed NUMBER)
362+ emit_pop_reg(code, REG_RAX); // a payload
363+ emit_pop_reg(code, REG_RBX); // a tag (assumed NUMBER)
364+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
365+ 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);
368+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
369+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
370+ emit_push_reg(code, REG_RBX);
371+ emit_push_reg(code, REG_RAX);
372+ }
373+}
374+
375+// See expr.h's header comment -- shared by additive()'s `+` and
376+// parser.c's `x += e` compound assignment.
377+void codegen_apply_plus(void) {
378+ // Runtime tag check: both STRING -> concatenate, else assume both
379+ // NUMBER (see the scope-limit note at the top of this file).
380+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
381+ int not_string = emit_jcc_rel32(code, COND_NE);
382+ emit_cmp_reg_imm32(code, REG_RDX, TAG_STRING);
383+ int not_string2 = emit_jcc_rel32(code, COND_NE);
384+ codegen_string_concat(); // consumes a_payload=RAX, b_payload=RCX; pushes its own result
385+ int done = emit_jmp_rel32(code);
386+ emit_patch_jump(code, not_string);
387+ emit_patch_jump(code, not_string2);
388+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
389+ emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
390+ emit_addsd(code, XMM0, XMM1);
391+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
392+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
393+ emit_push_reg(code, REG_RBX);
394+ emit_push_reg(code, REG_RAX);
395+ emit_patch_jump(code, done);
396+}
397+
398+void additive(void) {
399+ term();
400+ while (current.type == TOKEN_PLUS || current.type == TOKEN_MINUS) {
401+ TokenType op = current.type;
402+ advance_token();
403+ term();
404+ emit_pop_reg(code, REG_RCX); // b payload
405+ emit_pop_reg(code, REG_RDX); // b tag
406+ emit_pop_reg(code, REG_RAX); // a payload
407+ emit_pop_reg(code, REG_RBX); // a tag
408+
409+ if (op == TOKEN_PLUS) {
410+ codegen_apply_plus();
411+ } else {
412+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
413+ emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
414+ emit_subsd(code, XMM0, XMM1);
415+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
416+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
417+ emit_push_reg(code, REG_RBX);
418+ emit_push_reg(code, REG_RAX);
419+ }
420+ }
421+}

kiln/src/codegen/expr.dadded

+20−0
@@ -0,0 +1,20 @@
1+src/codegen/expr.o: src/codegen/expr.c src/codegen/expr.h \
2+ src/lexer/lexer.h src/codegen/expr_internal.h src/codegen/emit.h \
3+ src/codegen/emit_sse.h src/codegen/value.h src/codegen/strings.h \
4+ src/codegen/arrays.h src/codegen/maps.h src/codegen/builtins.h \
5+ src/codegen/closures.h src/parser/functions.h src/parser/parser.h \
6+ src/parser/vars.h
7+src/codegen/expr.h:
8+src/lexer/lexer.h:
9+src/codegen/expr_internal.h:
10+src/codegen/emit.h:
11+src/codegen/emit_sse.h:
12+src/codegen/value.h:
13+src/codegen/strings.h:
14+src/codegen/arrays.h:
15+src/codegen/maps.h:
16+src/codegen/builtins.h:
17+src/codegen/closures.h:
18+src/parser/functions.h:
19+src/parser/parser.h:
20+src/parser/vars.h:

kiln/src/codegen/expr.hadded

+36−0
@@ -0,0 +1,36 @@
1+#ifndef KILN_CODEGEN_EXPR_H
2+#define KILN_CODEGEN_EXPR_H
3+#include "lexer/lexer.h"
4+
5+// Parses one expression and emits real x86-64 code for it via the
6+// shared `code` buffer (see parser/parser.h) -- consumes tokens
7+// directly, no AST. Leaves the computed value's (tag, payload) pushed on
8+// the (simulated, real CPU) stack.
9+void codegen_expression(void);
10+
11+// Parses a call's arguments and emits it -- `id` is the already-consumed
12+// function/builtin name token, and the '(' must already be consumed too
13+// (current sits on the first argument or ')'). Leaves the call's (tag,
14+// payload) result pushed, same as any other expression. Exposed (not
15+// static to expr.c) so parser.c can also compile a bare `f(...);`
16+// statement -- calls aren't only usable inside larger expressions.
17+void codegen_call(Token id);
18+
19+// codegen_call's counterpart for a namespaced `ns.func(...)` call site,
20+// where the combined name ("math.add") lives in a local buffer rather
21+// than a contiguous span of the source text, so there's no single Token
22+// to hand codegen_call. Already-fully-qualified name -- resolved via
23+// resolve_function (not resolve_function_in_context), since the caller
24+// (codegen/expr.c's primary()) has already done the namespace lookup by
25+// building the combined name.
26+void codegen_call_named(const char *name, int len);
27+
28+// Assumes a_tag=RBX, a_payload=RAX, b_tag=RDX, b_payload=RCX already
29+// popped (matching the pop order every binary-operator codegen in this
30+// project uses) -- pushes the tag-aware sum (string concat if both are
31+// STRING, numeric add otherwise). Factored out of additive()'s `+`
32+// branch so parser.c's `x += e` compound-assignment codegen can reuse
33+// the exact same logic instead of a second, drifting copy of it.
34+void codegen_apply_plus(void);
35+
36+#endif

kiln/src/codegen/expr_bool.cadded

+211−0
@@ -0,0 +1,211 @@
1+#include <string.h>
2+#include "codegen/expr.h"
3+#include "codegen/expr_internal.h"
4+#include "codegen/emit.h"
5+#include "codegen/emit_sse.h"
6+#include "codegen/value.h"
7+#include "codegen/strings.h"
8+#include "parser/parser.h"
9+
10+// Precedence climbing continued from expr.c: comparison -> logical_and ->
11+// logical_or -> expression. See expr.c's header comment for the full
12+// chain, the (tag, payload) push/pop convention, and the scope-limit
13+// note (only `+` and `==`/`!=` are tag-aware; the rest assume NUMBER).
14+
15+// Pushes a literal double 0.0 or 1.0, tagged NUMBER.
16+static void push_number_literal(double v) {
17+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
18+ emit_push_reg(code, REG_RBX);
19+ if (v == 0.0) {
20+ emit_pxor_xmm_xmm(code, XMM0);
21+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
22+ } else {
23+ uint64_t bits;
24+ memcpy(&bits, &v, sizeof(bits));
25+ emit_mov_reg_imm64(code, REG_RAX, bits);
26+ }
27+ emit_push_reg(code, REG_RAX);
28+}
29+
30+// Turns "condition true/false" (as of the last ucomisd) into a clean
31+// pushed 0.0/1.0, via the same jcc-and-patch technique
32+// codegen/print_int.c already uses for its digit loop -- just applied to
33+// a general boolean result instead of a hardcoded loop exit. `cc_if_true`
34+// must be one of ucomisd's unsigned-style condition codes (COND_B/BE/A/AE
35+// or COND_E/NE), never the signed integer ones.
36+static void push_bool(Cond cc_if_true) {
37+ int to_true = emit_jcc_rel32(code, cc_if_true);
38+ push_number_literal(0.0);
39+ int to_end = emit_jmp_rel32(code);
40+ emit_patch_jump(code, to_true);
41+ push_number_literal(1.0);
42+ emit_patch_jump(code, to_end);
43+}
44+
45+// Pops the top-of-stack value and sets flags as if its payload were
46+// compared against 0.0 (ucomisd), for truthiness tests -- assumes
47+// NUMBER, matching ashvm's own truthy(), which requires a number too.
48+static void pop_and_test_truthy(void) {
49+ emit_pop_reg(code, REG_RAX); // payload
50+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
51+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
52+ emit_pxor_xmm_xmm(code, XMM1);
53+ emit_ucomisd(code, XMM0, XMM1);
54+}
55+
56+// Not chainable (`a < b < c` isn't a single comparison), matching
57+// ashvm/src/compiler/expr.c's comparison() shape exactly (an `if`, not a
58+// `while`).
59+static void comparison(void) {
60+ additive();
61+ if (current.type == TOKEN_EQUAL_EQUAL || current.type == TOKEN_BANG_EQUAL ||
62+ current.type == TOKEN_LESS || current.type == TOKEN_LESS_EQUAL ||
63+ current.type == TOKEN_GREATER || current.type == TOKEN_GREATER_EQUAL) {
64+ TokenType op = current.type;
65+ advance_token();
66+ additive();
67+ emit_pop_reg(code, REG_RCX); // b payload
68+ emit_pop_reg(code, REG_RDX); // b tag
69+ emit_pop_reg(code, REG_RAX); // a payload
70+ emit_pop_reg(code, REG_RBX); // a tag
71+
72+ if (op == TOKEN_EQUAL_EQUAL || op == TOKEN_BANG_EQUAL) {
73+ int invert = (op == TOKEN_BANG_EQUAL);
74+ // Different tags can never be equal (matches ashvm's do_EQ:
75+ // mismatched types just aren't equal, no error).
76+ emit_cmp_reg_reg(code, REG_RBX, REG_RDX);
77+ int tags_differ = emit_jcc_rel32(code, COND_NE);
78+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);
79+ int not_string = emit_jcc_rel32(code, COND_NE);
80+ codegen_string_compare(invert); // consumes a_payload=RAX, b_payload=RCX; pushes its own result
81+ int done = emit_jmp_rel32(code);
82+ emit_patch_jump(code, not_string);
83+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
84+ emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
85+ emit_ucomisd(code, XMM0, XMM1);
86+ push_bool(invert ? COND_NE : COND_E);
87+ int done2 = emit_jmp_rel32(code);
88+ emit_patch_jump(code, tags_differ);
89+ push_number_literal(invert ? 1.0 : 0.0);
90+ emit_patch_jump(code, done);
91+ emit_patch_jump(code, done2);
92+ } else {
93+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
94+ emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
95+ emit_ucomisd(code, XMM0, XMM1);
96+ switch (op) {
97+ case TOKEN_LESS: push_bool(COND_B); break;
98+ case TOKEN_LESS_EQUAL: push_bool(COND_BE); break;
99+ case TOKEN_GREATER: push_bool(COND_A); break;
100+ case TOKEN_GREATER_EQUAL: push_bool(COND_AE); break;
101+ default: break;
102+ }
103+ }
104+ }
105+}
106+
107+// Bitwise operators (& | ^ << >>), one flat precedence level (left to
108+// right) sitting between comparison and logical_and -- not real C
109+// precedence (which splits shift/bitand/bitxor/bitor into four separate
110+// levels), a deliberate simplification since mixed bitwise+comparison
111+// expressions are rare enough that parens can disambiguate when needed.
112+// Operands are truncated to int64 (cvttsd2si), same "assume NUMBER"
113+// scope limit as -, *, /, and the comparison operators.
114+static void bitwise(void) {
115+ comparison();
116+ while (current.type == TOKEN_AMP || current.type == TOKEN_PIPE ||
117+ current.type == TOKEN_CARET || current.type == TOKEN_SHL || current.type == TOKEN_SHR) {
118+ TokenType op = current.type;
119+ advance_token();
120+ comparison();
121+ emit_pop_reg(code, REG_RCX); // b payload
122+ emit_pop_reg(code, REG_RDX); // b tag (ignored)
123+ emit_pop_reg(code, REG_RAX); // a payload
124+ emit_pop_reg(code, REG_RBX); // a tag (ignored)
125+
126+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
127+ emit_cvttsd2si(code, REG_RAX, XMM0); // a -> int64
128+ emit_movq_xmm_from_reg(code, XMM0, REG_RCX);
129+ emit_cvttsd2si(code, REG_RCX, XMM0); // b -> int64
130+
131+ switch (op) {
132+ case TOKEN_AMP: emit_and_reg_reg(code, REG_RAX, REG_RCX); break;
133+ case TOKEN_PIPE: emit_or_reg_reg(code, REG_RAX, REG_RCX); break;
134+ case TOKEN_CARET: emit_xor_reg_reg(code, REG_RAX, REG_RCX); break;
135+ case TOKEN_SHL: emit_shl_reg_cl(code, REG_RAX); break;
136+ case TOKEN_SHR: emit_sar_reg_cl(code, REG_RAX); break;
137+ default: break;
138+ }
139+
140+ emit_cvtsi2sd(code, XMM0, REG_RAX);
141+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
142+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
143+ emit_push_reg(code, REG_RBX);
144+ emit_push_reg(code, REG_RAX);
145+ }
146+}
147+
148+// Short-circuit &&: if the left side is falsy, the right side never gets
149+// evaluated (its code doesn't even run, not just "result ignored").
150+static void logical_and(void) {
151+ bitwise();
152+ while (current.type == TOKEN_AND) {
153+ advance_token();
154+ pop_and_test_truthy();
155+ int to_false = emit_jcc_rel32(code, COND_E);
156+ bitwise();
157+ pop_and_test_truthy();
158+ int to_false2 = emit_jcc_rel32(code, COND_E);
159+ push_number_literal(1.0);
160+ int to_end = emit_jmp_rel32(code);
161+ emit_patch_jump(code, to_false);
162+ emit_patch_jump(code, to_false2);
163+ push_number_literal(0.0);
164+ emit_patch_jump(code, to_end);
165+ }
166+}
167+
168+// Short-circuit ||: if the left side is truthy, the right side never gets
169+// evaluated.
170+static void logical_or(void) {
171+ logical_and();
172+ while (current.type == TOKEN_OR) {
173+ advance_token();
174+ pop_and_test_truthy();
175+ int to_true = emit_jcc_rel32(code, COND_NE);
176+ logical_and();
177+ pop_and_test_truthy();
178+ int to_true2 = emit_jcc_rel32(code, COND_NE);
179+ push_number_literal(0.0);
180+ int to_end = emit_jmp_rel32(code);
181+ emit_patch_jump(code, to_true);
182+ emit_patch_jump(code, to_true2);
183+ push_number_literal(1.0);
184+ emit_patch_jump(code, to_end);
185+ }
186+}
187+
188+// a ? b : c -- lowest precedence, sits above logical_or. Right-associative
189+// via the recursive codegen_expression() calls for both branches (so
190+// `a ? b : c ? d : e` parses as `a ? b : (c ? d : e)`), same shape as
191+// if/else's truthy-test-then-jump pattern, just as an expression instead
192+// of a statement.
193+void codegen_expression(void) {
194+ logical_or();
195+ if (current.type == TOKEN_QUESTION) {
196+ advance_token();
197+ emit_pop_reg(code, REG_RAX); // condition payload
198+ emit_pop_reg(code, REG_RBX); // condition tag (assumed NUMBER)
199+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
200+ emit_pxor_xmm_xmm(code, XMM1);
201+ emit_ucomisd(code, XMM0, XMM1);
202+ int else_jump = emit_jcc_rel32(code, COND_E);
203+
204+ codegen_expression(); // true branch
205+ int end_jump = emit_jmp_rel32(code);
206+ expect(TOKEN_COLON, "expected ':' in ternary expression");
207+ emit_patch_jump(code, else_jump);
208+ codegen_expression(); // false branch
209+ emit_patch_jump(code, end_jump);
210+ }
211+}

kiln/src/codegen/expr_bool.dadded

+12−0
@@ -0,0 +1,12 @@
1+src/codegen/expr_bool.o: src/codegen/expr_bool.c src/codegen/expr.h \
2+ src/lexer/lexer.h src/codegen/expr_internal.h src/codegen/emit.h \
3+ src/codegen/emit_sse.h src/codegen/value.h src/codegen/strings.h \
4+ src/parser/parser.h
5+src/codegen/expr.h:
6+src/lexer/lexer.h:
7+src/codegen/expr_internal.h:
8+src/codegen/emit.h:
9+src/codegen/emit_sse.h:
10+src/codegen/value.h:
11+src/codegen/strings.h:
12+src/parser/parser.h:

kiln/src/codegen/expr_call.cadded

+74−0
@@ -0,0 +1,74 @@
1+#include <stdio.h>
2+#include "codegen/expr.h"
3+#include "codegen/emit.h"
4+#include "codegen/builtins.h"
5+#include "codegen/closures.h"
6+#include "parser/parser.h"
7+#include "parser/functions.h"
8+#include "parser/vars.h"
9+
10+// Call codegen -- its own file since it's used from two places
11+// (expr.c's primary(), for calls used as expressions, and parser.c's
12+// bare `f(...);` statement) and is a clean, self-contained unit on its
13+// own, mirroring ashvm's own compiler/calls.c split.
14+
15+// The '(' is already consumed; current sits on the first argument or ')'.
16+void codegen_call(Token id) {
17+ if (codegen_try_builtin_call(id.start, id.length)) return;
18+ // Milestone 8: a name that resolves to a VARIABLE (holding a
19+ // TAG_FUNCTION closure/lambda value) goes through the indirect-call
20+ // path instead of the fast compile-time-known-target path below --
21+ // purely additive, every existing direct call to a declared `fn` is
22+ // untouched.
23+ int slot = resolve_var(id.start, id.length);
24+ if (slot != -1) { codegen_indirect_call(slot); return; }
25+ KilnFunction *fn = resolve_function_in_context(id.start, id.length);
26+ if (fn == NULL) {
27+ char msg[128];
28+ snprintf(msg, sizeof(msg), "undefined function: %.*s", id.length, id.start);
29+ parse_error(msg);
30+ }
31+ int argc = 0;
32+ if (current.type != TOKEN_RPAREN) {
33+ codegen_expression(); argc++;
34+ while (current.type == TOKEN_COMMA) { advance_token(); codegen_expression(); argc++; }
35+ }
36+ expect(TOKEN_RPAREN, "expected ')' after arguments");
37+ if (argc != fn->arity) {
38+ char msg[128];
39+ snprintf(msg, sizeof(msg), "function %.*s expected %d args, got %d", id.length, id.start, fn->arity, argc);
40+ parse_error(msg);
41+ }
42+ emit_call_back(code, fn->code_offset);
43+ if (argc > 0) emit_add_reg_imm32(code, REG_RSP, 16 * argc); // caller cleans up pushed args (each now 16 bytes)
44+ emit_push_reg(code, REG_RBX); // return convention: RBX=tag, RAX=payload
45+ emit_push_reg(code, REG_RAX);
46+}
47+
48+// codegen_call's body with the name-resolution swapped for a raw (name,
49+// len) pair instead of a source Token, since a namespaced call site's
50+// combined name ("math.add") isn't a contiguous span in the original
51+// source text.
52+void codegen_call_named(const char *name, int len) {
53+ KilnFunction *fn = resolve_function(name, len); // already fully-qualified -- no _in_context wrapping
54+ if (fn == NULL) {
55+ char msg[128];
56+ snprintf(msg, sizeof(msg), "undefined function: %.*s", len, name);
57+ parse_error(msg);
58+ }
59+ int argc = 0;
60+ if (current.type != TOKEN_RPAREN) {
61+ codegen_expression(); argc++;
62+ while (current.type == TOKEN_COMMA) { advance_token(); codegen_expression(); argc++; }
63+ }
64+ expect(TOKEN_RPAREN, "expected ')' after arguments");
65+ if (argc != fn->arity) {
66+ char msg[128];
67+ snprintf(msg, sizeof(msg), "function %.*s expected %d args, got %d", len, name, fn->arity, argc);
68+ parse_error(msg);
69+ }
70+ emit_call_back(code, fn->code_offset);
71+ if (argc > 0) emit_add_reg_imm32(code, REG_RSP, 16 * argc);
72+ emit_push_reg(code, REG_RBX);
73+ emit_push_reg(code, REG_RAX);
74+}

kiln/src/codegen/expr_call.dadded

+12−0
@@ -0,0 +1,12 @@
1+src/codegen/expr_call.o: src/codegen/expr_call.c src/codegen/expr.h \
2+ src/lexer/lexer.h src/codegen/emit.h src/codegen/builtins.h \
3+ src/codegen/closures.h src/parser/functions.h src/parser/parser.h \
4+ src/parser/vars.h
5+src/codegen/expr.h:
6+src/lexer/lexer.h:
7+src/codegen/emit.h:
8+src/codegen/builtins.h:
9+src/codegen/closures.h:
10+src/parser/functions.h:
11+src/parser/parser.h:
12+src/parser/vars.h:

kiln/src/codegen/expr_internal.hadded

+9−0
@@ -0,0 +1,9 @@
1+#ifndef KILN_EXPR_INTERNAL_H
2+#define KILN_EXPR_INTERNAL_H
3+
4+// Cross-file link between expr.c (arithmetic: primary -> unary -> term ->
5+// additive) and expr_bool.c (comparison -> logical_and -> logical_or ->
6+// codegen_expression), not part of the public API (codegen/expr.h).
7+void additive(void);
8+
9+#endif

kiln/src/codegen/file_builtins.hadded

+12−0
@@ -0,0 +1,12 @@
1+#ifndef KILN_FILE_BUILTINS_H
2+#define KILN_FILE_BUILTINS_H
3+
4+// read_file/file_exists assume their one PATH argument already pushed --
5+// matching sqrt/abs/floor's convention. write_file/append_file self-parse
6+// both arguments, matching map/filter's convention (comma-separated).
7+void codegen_builtin_read_file(void);
8+void codegen_builtin_write_file(void);
9+void codegen_builtin_append_file(void);
10+void codegen_builtin_file_exists(void);
11+
12+#endif

kiln/src/codegen/file_path.cadded

+28−0
@@ -0,0 +1,28 @@
1+#include <stdint.h>
2+#include "codegen/file_path.h"
3+#include "codegen/bytes.h"
4+#include "codegen/heap.h"
5+#include "parser/vars.h"
6+
7+void filepath_emit_nullterm(CodeBuf *code) {
8+ int32_t base = higher_order_scratch_offset();
9+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RSI); // src
10+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX); // length
11+
12+ emit_mov_reg_reg(code, REG_RDI, REG_RDX);
13+ emit_add_reg_imm8(code, REG_RDI, 1); // +1 for the null terminator
14+ heap_emit_alloc(code); // RAX = buf
15+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);
16+
17+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
18+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0); // src
19+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // length
20+ bytes_emit_copy(code);
21+
22+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 16); // buf
23+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // length
24+ emit_mov_reg_reg(code, REG_RCX, REG_RAX);
25+ emit_add_reg_reg(code, REG_RCX, REG_RDX);
26+ emit_store_byte_imm(code, REG_RCX, 0); // null terminator
27+ // RAX already holds buf, the return value
28+}

kiln/src/codegen/file_path.dadded

+8−0
@@ -0,0 +1,8 @@
1+src/codegen/file_path.o: src/codegen/file_path.c src/codegen/file_path.h \
2+ src/codegen/emit.h src/codegen/bytes.h src/codegen/heap.h \
3+ src/parser/vars.h
4+src/codegen/file_path.h:
5+src/codegen/emit.h:
6+src/codegen/bytes.h:
7+src/codegen/heap.h:
8+src/parser/vars.h:

kiln/src/codegen/file_path.hadded

+12−0
@@ -0,0 +1,12 @@
1+#ifndef KILN_FILE_PATH_H
2+#define KILN_FILE_PATH_H
3+#include "codegen/emit.h"
4+
5+// Kiln strings are length-prefixed, not null-terminated -- but the raw
6+// `open` syscall (like libc's) requires a null-terminated path. Input:
7+// RSI=string payload, RDX=length. Output: RAX=address of a fresh
8+// heap-allocated null-terminated copy. Clobbers RBX, RCX, RDI (and RSI,
9+// RDX transiently, restored from scratch internally).
10+void filepath_emit_nullterm(CodeBuf *code);
11+
12+#endif

kiln/src/codegen/filter_builtin.cadded

+121−0
@@ -0,0 +1,121 @@
1+#include "codegen/higher_order.h"
2+#include "codegen/expr.h"
3+#include "codegen/closures.h"
4+#include "codegen/emit.h"
5+#include "codegen/emit_sse.h"
6+#include "codegen/value.h"
7+#include "codegen/heap.h"
8+#include "parser/parser.h"
9+#include "parser/vars.h"
10+
11+// Scratch layout: [0]=fn_tag [8]=fn_payload [16]=data_ptr [24]=count
12+// [32]=out_data_ptr [40]=i [48]=out_count
13+//
14+// The output block is sized to the INPUT count (an upper bound on how
15+// many survive the filter) -- simpler than a second pass or a grow loop,
16+// and leaves a perfectly ordinary array afterward (capacity=input count,
17+// count=out_count<=capacity, same shape push() already knows how to grow
18+// from).
19+void codegen_builtin_filter(void) {
20+ codegen_expression(); // array
21+ expect(TOKEN_COMMA, "expected ',' after array argument");
22+ codegen_expression(); // fn
23+ expect(TOKEN_RPAREN, "expected ')' after arguments");
24+
25+ emit_pop_reg(code, REG_RAX); // fn payload
26+ emit_pop_reg(code, REG_RBX); // fn tag
27+ emit_pop_reg(code, REG_RSI); // array object
28+ emit_pop_reg(code, REG_RCX); // array tag (ignored)
29+
30+ int32_t base = higher_order_scratch_offset();
31+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RBX);
32+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX);
33+
34+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
35+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr
36+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RDX);
37+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
38+
39+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
40+ emit_mov_reg_imm64(code, REG_RBX, 16);
41+ emit_imul_reg_reg(code, REG_RDI, REG_RBX);
42+ emit_cmp_reg_imm32(code, REG_RDI, 0);
43+ int nonzero = emit_jcc_rel32(code, COND_NE);
44+ emit_mov_reg_imm64(code, REG_RDI, 1);
45+ emit_patch_jump(code, nonzero);
46+ heap_emit_alloc(code); // RAX = out block
47+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX);
48+
49+ emit_mov_reg_imm64(code, REG_RCX, 0);
50+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // i = 0
51+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RCX); // out_count = 0
52+
53+ int loop_start = code->count;
54+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
55+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
56+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
57+ int loop_exit = emit_jcc_rel32(code, COND_GE);
58+
59+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // data_ptr
60+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
61+ emit_mov_reg_imm64(code, REG_RDX, 16);
62+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
63+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // element address
64+
65+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 0); // fn_tag
66+ emit_push_reg(code, REG_RAX);
67+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // fn_payload
68+ emit_push_reg(code, REG_RAX);
69+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // element tag
70+ emit_push_reg(code, REG_RAX);
71+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload
72+ emit_push_reg(code, REG_RAX);
73+ codegen_call_closure_value(1);
74+
75+ emit_pop_reg(code, REG_RAX); // result payload
76+ emit_pop_reg(code, REG_RBX); // result tag (ignored, assumed NUMBER)
77+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
78+ emit_pxor_xmm_xmm(code, XMM1);
79+ emit_ucomisd(code, XMM0, XMM1);
80+ int skip_keep = emit_jcc_rel32(code, COND_E); // falsy (0.0) -> don't keep
81+
82+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // i (still valid)
83+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // data_ptr (recompute -- the call clobbered RSI)
84+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
85+ emit_mov_reg_imm64(code, REG_RDX, 16);
86+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
87+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // element address again
88+
89+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 48); // out_count
90+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 32); // out_data_ptr
91+ emit_mov_reg_reg(code, REG_RAX, REG_RDI);
92+ emit_mov_reg_imm64(code, REG_RCX, 16);
93+ emit_imul_reg_reg(code, REG_RAX, REG_RCX);
94+ emit_add_reg_reg(code, REG_RDX, REG_RAX); // out slot address
95+
96+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 0); emit_store_mem_disp32(code, REG_RDX, 0, REG_RCX);
97+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 8); emit_store_mem_disp32(code, REG_RDX, 8, REG_RCX);
98+
99+ emit_add_reg_imm8(code, REG_RDI, 1);
100+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RDI); // out_count++
101+ emit_patch_jump(code, skip_keep);
102+
103+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
104+ emit_add_reg_imm8(code, REG_RCX, 1);
105+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // i++
106+ emit_jmp_back(code, loop_start);
107+ emit_patch_jump(code, loop_exit);
108+
109+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // capacity = input count
110+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 48); // count = out_count
111+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
112+ heap_emit_alloc(code); // RAX = array object
113+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32); // out_data_ptr (reload)
114+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
115+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RDX);
116+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
117+
118+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
119+ emit_push_reg(code, REG_RBX);
120+ emit_push_reg(code, REG_RAX);
121+}

kiln/src/codegen/filter_builtin.dadded

+16−0
@@ -0,0 +1,16 @@
1+src/codegen/filter_builtin.o: src/codegen/filter_builtin.c \
2+ src/codegen/higher_order.h src/codegen/expr.h src/lexer/lexer.h \
3+ src/codegen/closures.h src/parser/functions.h src/codegen/emit.h \
4+ src/codegen/emit_sse.h src/codegen/value.h src/codegen/heap.h \
5+ src/parser/parser.h src/parser/vars.h
6+src/codegen/higher_order.h:
7+src/codegen/expr.h:
8+src/lexer/lexer.h:
9+src/codegen/closures.h:
10+src/parser/functions.h:
11+src/codegen/emit.h:
12+src/codegen/emit_sse.h:
13+src/codegen/value.h:
14+src/codegen/heap.h:
15+src/parser/parser.h:
16+src/parser/vars.h:

kiln/src/codegen/heap.cadded

+101−0
@@ -0,0 +1,101 @@
1+#include "codegen/heap.h"
2+#include "codegen/layout.h"
3+#include "codegen/win_call.h"
4+#include "elf/elf_dynamic_call.h"
5+#include "target.h"
6+
7+#define HEAP_SIZE (16 * 1024 * 1024)
8+#define MEM_COMMIT_RESERVE 0x3000u // MEM_COMMIT(0x1000) | MEM_RESERVE(0x2000)
9+#define PAGE_EXECUTE_READWRITE 0x40u
10+
11+// Plan B, phase B1: emitted ONCE (jumped over, like errors.c's raise
12+// routine), called from every allocation site via emit_call_back instead
13+// of each of the ~27 call sites re-emitting these 4 instructions inline.
14+// Phase B3/B4: in --link=shared mode this routine's body isn't emitted
15+// into the executable at all -- it lives in libkilnrt.so instead (see
16+// heap_emit_alloc_routine_only, called by the separate "compile the
17+// runtime" driver), and heap_emit_alloc becomes a GOT call instead of a
18+// local one.
19+static int alloc_routine_offset = -1;
20+
21+static void emit_alloc_routine(CodeBuf *code) {
22+ int skip = emit_jmp_rel32(code);
23+ alloc_routine_offset = code->count;
24+ emit_mov_reg_imm64(code, REG_RSI, kiln_heap_ptr_addr());
25+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // rax = heap_ptr (becomes the returned block address)
26+ emit_add_reg_reg(code, REG_RDI, REG_RAX); // rdi = heap_ptr + size = new heap_ptr
27+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI);
28+ emit_ret(code);
29+ emit_patch_jump(code, skip);
30+}
31+
32+// Called by the dedicated "compile the runtime" driver (see
33+// elf/elf_so_writer.c) to emit this routine into libkilnrt.so's OWN code
34+// buffer, completely separate from any executable's compile_program.
35+void heap_emit_alloc_routine_only(CodeBuf *code) {
36+ emit_alloc_routine(code);
37+}
38+
39+int heap_alloc_routine_offset(void) { return alloc_routine_offset; }
40+
41+// !kiln_is_compiling_so(): while THIS runtime routine's body is itself
42+// being compiled INTO libkilnrt.so/.dll, its own call sites must stay
43+// local (same-buffer relative calls), not calls back out to the library
44+// it's already part of -- see target.h's comment on why this is a
45+// separate question from link mode. True for both targets: which OS
46+// decides how the actual call happens (elf_dynamic_call vs
47+// win_call_runtime_import), not whether it happens at all.
48+static int is_shared(void) {
49+ return kiln_get_link_mode() == KILN_LINK_SHARED && !kiln_is_compiling_so();
50+}
51+
52+void heap_emit_startup(CodeBuf *code) {
53+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
54+ // VirtualAlloc hands back a ready-to-use base directly -- no
55+ // "query then grow" dance needed the way brk requires.
56+ win_call_begin(code, 0);
57+ win_call_arg_imm64(code, 0, 0); // lpAddress = NULL
58+ win_call_arg_imm64(code, 1, HEAP_SIZE);
59+ win_call_arg_imm64(code, 2, MEM_COMMIT_RESERVE);
60+ win_call_arg_imm64(code, 3, PAGE_EXECUTE_READWRITE);
61+ win_call_import(code, PE_IMPORT_VIRTUAL_ALLOC);
62+ win_call_end(code);
63+ emit_mov_reg_imm64(code, REG_RSI, kiln_heap_ptr_addr());
64+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX);
65+ if (!is_shared()) emit_alloc_routine(code);
66+ return;
67+ }
68+
69+ // brk(0) -- query the current break, becomes our arena's start. This
70+ // part always happens in the EXECUTABLE, even under --link=shared:
71+ // the arena and its heap_ptr live in the calling process's own
72+ // memory regardless of which binary's code manages the bump pointer.
73+ emit_mov_reg_imm64(code, REG_RAX, 12); // syscall: brk
74+ emit_mov_reg_imm64(code, REG_RDI, 0);
75+ emit_syscall(code);
76+
77+ // heap_ptr = current break (save BEFORE growing -- after the next
78+ // brk call, RAX holds the new break, not the arena's start)
79+ emit_mov_reg_imm64(code, REG_RSI, kiln_heap_ptr_addr());
80+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX);
81+
82+ // brk(current_break + HEAP_SIZE) -- grow once, upfront. Not checking
83+ // the result: a fixed, modest 16MB request failing is not a case
84+ // this project's "don't over-engineer error handling for things that
85+ // can't practically happen" rule asks for defensive code around.
86+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
87+ emit_add_reg_imm32(code, REG_RDI, HEAP_SIZE);
88+ emit_mov_reg_imm64(code, REG_RAX, 12);
89+ emit_syscall(code);
90+
91+ if (!is_shared()) emit_alloc_routine(code);
92+}
93+
94+void heap_emit_alloc(CodeBuf *code) {
95+ if (is_shared()) {
96+ if (kiln_get_target() == KILN_TARGET_WINDOWS) win_call_runtime_import(code, RUNTIME_IMPORT_HEAP_ALLOC);
97+ else elf_dynamic_call(code, RUNTIME_IMPORT_HEAP_ALLOC);
98+ return;
99+ }
100+ emit_call_back(code, alloc_routine_offset);
101+}

kiln/src/codegen/heap.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/codegen/heap.o: src/codegen/heap.c src/codegen/heap.h \
2+ src/codegen/emit.h src/codegen/layout.h src/codegen/win_call.h \
3+ src/pe/pe_writer.h src/codegen/runtime_layout.h \
4+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
5+ src/pe/pe_imports.def src/elf/elf_dynamic_call.h src/elf/elf_dynamic.h \
6+ src/elf/elf_writer.h src/target.h
7+src/codegen/heap.h:
8+src/codegen/emit.h:
9+src/codegen/layout.h:
10+src/codegen/win_call.h:
11+src/pe/pe_writer.h:
12+src/codegen/runtime_layout.h:
13+src/codegen/runtime_import_enum.h:
14+src/codegen/runtime_exports.def:
15+src/pe/pe_imports.def:
16+src/elf/elf_dynamic_call.h:
17+src/elf/elf_dynamic.h:
18+src/elf/elf_writer.h:
19+src/target.h:

kiln/src/codegen/heap.hadded

+38−0
@@ -0,0 +1,38 @@
1+#ifndef KILN_HEAP_H
2+#define KILN_HEAP_H
3+#include "codegen/emit.h"
4+
5+// A bump allocator, no free (matches ashvm's own lack of GC -- neither
6+// engine reclaims memory during a run, only at process exit). Backed by
7+// `brk` rather than `mmap`: brk takes exactly one argument (the new break
8+// address, in RDI), while mmap needs six, three of which (R10/R8/R9) kiln
9+// has deliberately never needed until now and would require extending
10+// emit.c's register handling to reach (those are all >= 8, needing
11+// REX.R/X/B bits this project's encoders don't set). brk sidesteps that
12+// entirely for a one-time, fixed-size arena grab.
13+
14+// Emitted once, at the very start of the generated program (before the
15+// top-level statement loop): grabs a 16MB arena and records its start as
16+// the heap bump pointer at KILN_GLOBALS_ADDR (see elf/elf_writer.h).
17+void heap_emit_startup(CodeBuf *code);
18+
19+// Plan B: emits ONLY the alloc routine's body (no arena grab) -- used by
20+// the standalone "compile the runtime" driver that builds libkilnrt.so,
21+// where the arena grab still happens in the calling EXECUTABLE instead
22+// (see heap_emit_startup's own comment on this split).
23+void heap_emit_alloc_routine_only(CodeBuf *code);
24+// Offset (within whatever CodeBuf the *_routine_only variant just ran
25+// against) of the alloc routine's entry point -- used by the "compile
26+// the runtime" driver to fill in libkilnrt.so's dynsym st_value.
27+int heap_alloc_routine_offset(void);
28+
29+// Fixed internal calling convention (not the user-facing one -- this is
30+// never reachable from ash source, only from other codegen, so it can be
31+// as simple as it wants): size to allocate in RDI, returns the allocated
32+// block's address in RAX, clobbers RSI. Plan B, phase B1: this now emits
33+// a real CALL to one shared copy of the routine (emitted once by
34+// heap_emit_startup) instead of re-emitting the routine's body inline at
35+// every one of its ~27 call sites.
36+void heap_emit_alloc(CodeBuf *code);
37+
38+#endif

kiln/src/codegen/higher_order.hadded

+19−0
@@ -0,0 +1,19 @@
1+#ifndef KILN_HIGHER_ORDER_H
2+#define KILN_HIGHER_ORDER_H
3+
4+// Each parses its own arguments (array, fn, and for reduce, an initial
5+// value) starting right after the already-consumed '(' -- codegen/
6+// builtins.c's dispatch calls these directly, matching push()/len()'s
7+// convention of self-parsing rather than the caller pre-evaluating args.
8+
9+// map(array, fn): pushes a new array, each element replaced by fn(element).
10+void codegen_builtin_map(void);
11+
12+// filter(array, fn): pushes a new array containing only the elements for
13+// which fn(element) was truthy (a nonzero NUMBER).
14+void codegen_builtin_filter(void);
15+
16+// reduce(array, fn, initial): pushes fn(...fn(fn(initial, e0), e1)..., eN).
17+void codegen_builtin_reduce(void);
18+
19+#endif

kiln/src/codegen/indexof_builtin.cadded

+111−0
@@ -0,0 +1,111 @@
1+#include "codegen/string_builtins.h"
2+#include "codegen/expr.h"
3+#include "codegen/emit.h"
4+#include "codegen/emit_sse.h"
5+#include "codegen/value.h"
6+#include "parser/parser.h"
7+#include "parser/vars.h"
8+
9+static void push_number(double v) {
10+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
11+ emit_push_reg(code, REG_RBX);
12+ if (v == 0.0) {
13+ emit_pxor_xmm_xmm(code, XMM0);
14+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
15+ } else {
16+ uint64_t bits;
17+ __builtin_memcpy(&bits, &v, sizeof(bits));
18+ emit_mov_reg_imm64(code, REG_RAX, bits);
19+ }
20+ emit_push_reg(code, REG_RAX);
21+}
22+
23+// Scratch layout: [0]=s_ptr [8]=s_len [16]=search_ptr [24]=search_len
24+// [32]=i (candidate start position) [40]=last_start
25+//
26+// A plain O(n*m) scan (compare the search string against every candidate
27+// start position) -- matches this project's existing "documented
28+// simplification for implementation reach" precedent (e.g. maps.c's
29+// linear-scan lookup instead of a hash table).
30+void codegen_builtin_indexof(void) {
31+ codegen_expression(); // s
32+ expect(TOKEN_COMMA, "expected ',' after string argument");
33+ codegen_expression(); // search
34+ expect(TOKEN_RPAREN, "expected ')' after arguments");
35+
36+ emit_pop_reg(code, REG_RCX); // search payload
37+ emit_pop_reg(code, REG_RBX); // ignored
38+ emit_pop_reg(code, REG_RSI); // s payload
39+ emit_pop_reg(code, REG_RBX); // ignored
40+
41+ int32_t base = higher_order_scratch_offset();
42+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RSI);
43+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RCX);
44+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // s_len
45+ emit_load_mem_disp32(code, REG_RDI, REG_RCX, -8); // search_len
46+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX);
47+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RDI);
48+
49+ int end_jumps[4]; int end_count = 0;
50+
51+ emit_cmp_reg_imm32(code, REG_RDI, 0);
52+ int not_empty = emit_jcc_rel32(code, COND_NE);
53+ push_number(0.0);
54+ end_jumps[end_count++] = emit_jmp_rel32(code);
55+ emit_patch_jump(code, not_empty);
56+
57+ emit_cmp_reg_reg(code, REG_RDI, REG_RDX);
58+ int fits = emit_jcc_rel32(code, COND_LE);
59+ push_number(-1.0);
60+ end_jumps[end_count++] = emit_jmp_rel32(code);
61+ emit_patch_jump(code, fits);
62+
63+ emit_mov_reg_reg(code, REG_RAX, REG_RDX);
64+ emit_sub_reg_reg(code, REG_RAX, REG_RDI); // last valid start position
65+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX);
66+ emit_mov_reg_imm64(code, REG_RBX, 0);
67+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RBX); // i = 0
68+
69+ int outer = code->count;
70+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 32);
71+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 40);
72+ emit_cmp_reg_reg(code, REG_RBX, REG_RAX);
73+ int not_found = emit_jcc_rel32(code, COND_GT);
74+
75+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
76+ emit_add_reg_reg(code, REG_RSI, REG_RBX); // s + i
77+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 16); // search_ptr
78+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len (used as inner counter)
79+
80+ int inner = code->count;
81+ emit_cmp_reg_imm32(code, REG_RDX, 0);
82+ int matched = emit_jcc_rel32(code, COND_E);
83+ emit_load_byte_reg(code, REG_RDI, REG_RSI);
84+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
85+ emit_cmp_reg_reg(code, REG_RDI, REG_RAX);
86+ int mismatch = emit_jcc_rel32(code, COND_NE);
87+ emit_add_reg_imm8(code, REG_RSI, 1);
88+ emit_add_reg_imm8(code, REG_RCX, 1);
89+ emit_dec_reg(code, REG_RDX);
90+ emit_jmp_back(code, inner);
91+
92+ emit_patch_jump(code, mismatch);
93+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 32);
94+ emit_add_reg_imm8(code, REG_RBX, 1);
95+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RBX);
96+ emit_jmp_back(code, outer);
97+
98+ emit_patch_jump(code, matched);
99+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 32);
100+ emit_cvtsi2sd(code, XMM0, REG_RAX);
101+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
102+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
103+ emit_push_reg(code, REG_RBX);
104+ emit_push_reg(code, REG_RAX);
105+ end_jumps[end_count++] = emit_jmp_rel32(code);
106+
107+ emit_patch_jump(code, not_found);
108+ push_number(-1.0);
109+
110+ for (int i = 0; i < end_count; i++) emit_patch_jump(code, end_jumps[i]);
111+}

kiln/src/codegen/indexof_builtin.dadded

+12−0
@@ -0,0 +1,12 @@
1+src/codegen/indexof_builtin.o: src/codegen/indexof_builtin.c \
2+ src/codegen/string_builtins.h src/codegen/expr.h src/lexer/lexer.h \
3+ src/codegen/emit.h src/codegen/emit_sse.h src/codegen/value.h \
4+ src/parser/parser.h src/parser/vars.h
5+src/codegen/string_builtins.h:
6+src/codegen/expr.h:
7+src/lexer/lexer.h:
8+src/codegen/emit.h:
9+src/codegen/emit_sse.h:
10+src/codegen/value.h:
11+src/parser/parser.h:
12+src/parser/vars.h:

kiln/src/codegen/input_builtin.cadded

+80−0
@@ -0,0 +1,80 @@
1+#include <stdint.h>
2+#include "codegen/input_builtin.h"
3+#include "codegen/string_alloc.h"
4+#include "codegen/bytes.h"
5+#include "codegen/heap.h"
6+#include "codegen/value.h"
7+#include "codegen/platform_console.h"
8+#include "parser/parser.h"
9+#include "parser/vars.h"
10+
11+// Reads one line from stdin (matching ashvm's fgets(buf,1024,stdin) +
12+// trailing-newline-strip): a fixed 1024-byte cap, same as ashvm's own
13+// buffer size. No libc getline/fgets in this freestanding binary, so
14+// bytes come in one at a time via raw read(2) syscalls -- slow, but
15+// input() is never a hot loop, and "simple and obviously correct" beats
16+// hand-rolling a buffered reader for this.
17+void codegen_builtin_input(void) {
18+ emit_mov_reg_imm64(code, REG_RDI, 1024);
19+ heap_emit_alloc(code); // RAX = buf
20+ int32_t base = higher_order_scratch_offset();
21+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RAX); // buf
22+ emit_mov_reg_imm64(code, REG_RCX, 0);
23+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RCX); // pos = 0
24+
25+ // Looked up once, reused every iteration -- on Windows this turns a
26+ // multi-byte line into one GetStdHandle round-trip instead of one per
27+ // byte (also closes a race window under Wine, where extra round-trips
28+ // gave another process time to drain the piped input first).
29+ platform_emit_get_stdin_handle(code);
30+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);
31+
32+ int end_jumps[3]; int end_count = 0;
33+
34+ int loop_start = code->count;
35+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 8);
36+ emit_cmp_reg_imm32(code, REG_RCX, 1024);
37+ end_jumps[end_count++] = emit_jcc_rel32(code, COND_GE); // buffer full
38+
39+ emit_sub_reg_imm8(code, REG_RSP, 8);
40+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
41+ emit_mov_reg_imm64(code, REG_RDX, 1);
42+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 16); // cached handle
43+ platform_emit_read_stdin_byte_h(code);
44+ emit_cmp_reg_imm32(code, REG_RAX, 1);
45+ int got_byte = emit_jcc_rel32(code, COND_E);
46+ emit_add_reg_imm8(code, REG_RSP, 8);
47+ end_jumps[end_count++] = emit_jmp_rel32(code); // EOF or error
48+ emit_patch_jump(code, got_byte);
49+
50+ emit_mov_reg_reg(code, REG_RCX, REG_RSP); // RSP can't be a byte-load base directly (needs a SIB byte this module doesn't emit)
51+ emit_load_byte_reg(code, REG_RCX, REG_RCX);
52+ emit_add_reg_imm8(code, REG_RSP, 8);
53+ emit_cmp_reg_imm32(code, REG_RCX, '\n');
54+ end_jumps[end_count++] = emit_jcc_rel32(code, COND_E); // newline -- don't store it
55+
56+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
57+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8);
58+ emit_add_reg_reg(code, REG_RSI, REG_RDX);
59+ emit_store_byte_reg(code, REG_RSI, REG_RCX);
60+ emit_add_reg_imm8(code, REG_RDX, 1);
61+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX); // pos++
62+ emit_jmp_back(code, loop_start);
63+
64+ for (int i = 0; i < end_count; i++) emit_patch_jump(code, end_jumps[i]);
65+
66+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // final length
67+ string_alloc_emit_prefixed(code); // RAX = block
68+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash (bytes_emit_copy preserves RSI)
69+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
70+ emit_add_reg_imm8(code, REG_RDI, 8);
71+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0); // src = line buffer
72+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // length
73+ bytes_emit_copy(code);
74+
75+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
76+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
77+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
78+ emit_push_reg(code, REG_RBX);
79+ emit_push_reg(code, REG_RAX);
80+}

kiln/src/codegen/input_builtin.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/codegen/input_builtin.o: src/codegen/input_builtin.c \
2+ src/codegen/input_builtin.h src/codegen/string_alloc.h \
3+ src/codegen/emit.h src/codegen/bytes.h src/codegen/heap.h \
4+ src/codegen/value.h src/codegen/platform_console.h src/parser/parser.h \
5+ src/lexer/lexer.h src/parser/vars.h
6+src/codegen/input_builtin.h:
7+src/codegen/string_alloc.h:
8+src/codegen/emit.h:
9+src/codegen/bytes.h:
10+src/codegen/heap.h:
11+src/codegen/value.h:
12+src/codegen/platform_console.h:
13+src/parser/parser.h:
14+src/lexer/lexer.h:
15+src/parser/vars.h:

kiln/src/codegen/input_builtin.hadded

+8−0
@@ -0,0 +1,8 @@
1+#ifndef KILN_INPUT_BUILTIN_H
2+#define KILN_INPUT_BUILTIN_H
3+
4+// input(): no arguments -- codegen/builtins.c's dispatch just expects the
5+// closing ')' before calling this.
6+void codegen_builtin_input(void);
7+
8+#endif

kiln/src/codegen/join_builtin.cadded

+142−0
@@ -0,0 +1,142 @@
1+#include "codegen/string_builtins.h"
2+#include "codegen/string_alloc.h"
3+#include "codegen/expr.h"
4+#include "codegen/bytes.h"
5+#include "codegen/emit.h"
6+#include "codegen/value.h"
7+#include "parser/parser.h"
8+#include "parser/vars.h"
9+
10+// Two-pass: pass 1 sums each element's length (+ delim length between
11+// consecutive elements) to get the exact output size; pass 2 allocates
12+// once and copies. Avoids any resize-while-building complexity.
13+//
14+// Scratch layout: [0]=data_ptr [8]=count [16]=delim_ptr [24]=delim_len
15+// [32]=total_len [40]=i [48]=out_block [56]=cursor [64]=elem_len (temp)
16+void codegen_builtin_join(void) {
17+ codegen_expression(); // array
18+ expect(TOKEN_COMMA, "expected ',' after array argument");
19+ codegen_expression(); // delim
20+ expect(TOKEN_RPAREN, "expected ')' after arguments");
21+
22+ emit_pop_reg(code, REG_RSI); // delim payload
23+ emit_pop_reg(code, REG_RBX); // delim tag (ignored)
24+ emit_pop_reg(code, REG_RCX); // array object
25+ emit_pop_reg(code, REG_RBX); // array tag (ignored)
26+
27+ int32_t base = higher_order_scratch_offset();
28+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RSI); // delim_ptr
29+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // delim_len
30+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RDX);
31+
32+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8); // count
33+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, 16); // data_ptr
34+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RDX);
35+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX);
36+
37+ // ---- pass 1: total length ----
38+ emit_mov_reg_imm64(code, REG_RAX, 0);
39+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // total = 0
40+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // i = 0
41+
42+ int p1 = code->count;
43+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 40);
44+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 8);
45+ emit_cmp_reg_reg(code, REG_RBX, REG_RCX);
46+ int p1_exit = emit_jcc_rel32(code, COND_GE);
47+
48+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
49+ emit_mov_reg_reg(code, REG_RAX, REG_RBX);
50+ emit_mov_reg_imm64(code, REG_RDX, 16);
51+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
52+ emit_add_reg_reg(code, REG_RSI, REG_RAX);
53+ emit_load_mem_disp32(code, REG_RSI, REG_RSI, 8); // elem payload (assumed STRING)
54+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, -8); // elem len
55+
56+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 32);
57+ emit_add_reg_reg(code, REG_RDX, REG_RAX);
58+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RDX); // total += elem_len
59+
60+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
61+ emit_sub_reg_imm8(code, REG_RAX, 1);
62+ emit_cmp_reg_reg(code, REG_RBX, REG_RAX);
63+ int last1 = emit_jcc_rel32(code, COND_GE); // last element -> no trailing delim
64+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 32);
65+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 24);
66+ emit_add_reg_reg(code, REG_RDX, REG_RAX);
67+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RDX);
68+ emit_patch_jump(code, last1);
69+
70+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 40);
71+ emit_add_reg_imm8(code, REG_RBX, 1);
72+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RBX);
73+ emit_jmp_back(code, p1);
74+ emit_patch_jump(code, p1_exit);
75+
76+ // ---- allocate output ----
77+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 32); // total_len
78+ string_alloc_emit_prefixed(code); // RAX = block
79+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RAX); // out_block
80+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
81+ emit_add_reg_imm8(code, REG_RDX, 8);
82+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RDX); // cursor = payload start
83+
84+ // ---- pass 2: copy ----
85+ emit_mov_reg_imm64(code, REG_RCX, 0);
86+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // i = 0
87+
88+ int p2 = code->count;
89+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 40);
90+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 8);
91+ emit_cmp_reg_reg(code, REG_RBX, REG_RCX);
92+ int p2_exit = emit_jcc_rel32(code, COND_GE);
93+
94+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
95+ emit_mov_reg_reg(code, REG_RAX, REG_RBX);
96+ emit_mov_reg_imm64(code, REG_RDX, 16);
97+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
98+ emit_add_reg_reg(code, REG_RSI, REG_RAX);
99+ emit_load_mem_disp32(code, REG_RSI, REG_RSI, 8); // elem payload
100+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // elem len
101+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RDX); // stash elem_len
102+
103+ emit_mov_reg_reg(code, REG_RBX, REG_RSI); // src
104+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 56); // dest = cursor
105+ bytes_emit_copy(code);
106+
107+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 56);
108+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 64);
109+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
110+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX); // cursor += elem_len
111+
112+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 40);
113+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 8);
114+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
115+ emit_sub_reg_imm8(code, REG_RAX, 1);
116+ emit_cmp_reg_reg(code, REG_RBX, REG_RAX);
117+ int last2 = emit_jcc_rel32(code, COND_GE);
118+
119+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 16); // delim_ptr (src)
120+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // delim_len
121+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 56); // cursor (dest)
122+ bytes_emit_copy(code);
123+
124+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 56);
125+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
126+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
127+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX);
128+
129+ emit_patch_jump(code, last2);
130+
131+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 40);
132+ emit_add_reg_imm8(code, REG_RBX, 1);
133+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RBX);
134+ emit_jmp_back(code, p2);
135+ emit_patch_jump(code, p2_exit);
136+
137+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 48); // out block
138+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
139+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
140+ emit_push_reg(code, REG_RBX);
141+ emit_push_reg(code, REG_RAX);
142+}

kiln/src/codegen/join_builtin.dadded

+14−0
@@ -0,0 +1,14 @@
1+src/codegen/join_builtin.o: src/codegen/join_builtin.c \
2+ src/codegen/string_builtins.h src/codegen/string_alloc.h \
3+ src/codegen/emit.h src/codegen/expr.h src/lexer/lexer.h \
4+ src/codegen/bytes.h src/codegen/value.h src/parser/parser.h \
5+ src/parser/vars.h
6+src/codegen/string_builtins.h:
7+src/codegen/string_alloc.h:
8+src/codegen/emit.h:
9+src/codegen/expr.h:
10+src/lexer/lexer.h:
11+src/codegen/bytes.h:
12+src/codegen/value.h:
13+src/parser/parser.h:
14+src/parser/vars.h:

kiln/src/codegen/keys_values.cadded

+147−0
@@ -0,0 +1,147 @@
1+#include "codegen/keys_values.h"
2+#include "codegen/emit.h"
3+#include "codegen/value.h"
4+#include "codegen/heap.h"
5+#include "parser/parser.h"
6+#include "parser/vars.h"
7+
8+// This implementation's map (unlike ashvm's capacity-sized hash table
9+// with scattered "used" flags) keeps every live entry packed into
10+// [0, count) -- delete() swaps the last entry into a removed slot rather
11+// than leaving a hole (see codegen/maps_mutate.c) -- so both builtins
12+// below can just iterate 0..count directly, no "used" check needed.
13+//
14+// Scratch layout: [0]=count [8]=entries_ptr [16]=out_block [24]=i
15+
16+void codegen_builtin_keys(void) {
17+ emit_pop_reg(code, REG_RSI); // map object
18+ emit_pop_reg(code, REG_RBX); // map tag (ignored)
19+
20+ int32_t base = higher_order_scratch_offset();
21+
22+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 8); // count
23+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // entries_ptr
24+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RCX);
25+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX);
26+
27+ emit_mov_reg_reg(code, REG_RDI, REG_RCX);
28+ emit_mov_reg_imm64(code, REG_RBX, 16);
29+ emit_imul_reg_reg(code, REG_RDI, REG_RBX);
30+ emit_cmp_reg_imm32(code, REG_RDI, 0);
31+ int nonzero = emit_jcc_rel32(code, COND_NE);
32+ emit_mov_reg_imm64(code, REG_RDI, 1);
33+ emit_patch_jump(code, nonzero);
34+ heap_emit_alloc(code); // RAX = out block
35+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);
36+
37+ emit_mov_reg_imm64(code, REG_RCX, 0);
38+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RCX); // i = 0
39+
40+ int loop_start = code->count;
41+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i
42+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 0); // count
43+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
44+ int loop_exit = emit_jcc_rel32(code, COND_GE);
45+
46+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 8); // entries_ptr
47+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
48+ emit_mov_reg_imm64(code, REG_RDX, MAP_ENTRY_SIZE);
49+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
50+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // entry address
51+
52+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // key payload (keys are always strings)
53+
54+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_block
55+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);
56+ emit_mov_reg_imm64(code, REG_RDI, 16);
57+ emit_imul_reg_reg(code, REG_RDX, REG_RDI);
58+ emit_add_reg_reg(code, REG_RSI, REG_RDX); // out slot address
59+
60+ emit_mov_reg_imm64(code, REG_RDI, TAG_STRING);
61+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI);
62+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
63+
64+ emit_add_reg_imm8(code, REG_RCX, 1);
65+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RCX);
66+ emit_jmp_back(code, loop_start);
67+ emit_patch_jump(code, loop_exit);
68+
69+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 0); // count
70+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
71+ heap_emit_alloc(code); // RAX = array object
72+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out block (reload -- alloc clobbers RSI)
73+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
74+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX);
75+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
76+
77+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
78+ emit_push_reg(code, REG_RBX);
79+ emit_push_reg(code, REG_RAX);
80+}
81+
82+void codegen_builtin_values(void) {
83+ emit_pop_reg(code, REG_RSI); // map object
84+ emit_pop_reg(code, REG_RBX); // map tag (ignored)
85+
86+ int32_t base = higher_order_scratch_offset();
87+
88+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 8); // count
89+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // entries_ptr
90+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RCX);
91+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX);
92+
93+ emit_mov_reg_reg(code, REG_RDI, REG_RCX);
94+ emit_mov_reg_imm64(code, REG_RBX, 16);
95+ emit_imul_reg_reg(code, REG_RDI, REG_RBX);
96+ emit_cmp_reg_imm32(code, REG_RDI, 0);
97+ int nonzero = emit_jcc_rel32(code, COND_NE);
98+ emit_mov_reg_imm64(code, REG_RDI, 1);
99+ emit_patch_jump(code, nonzero);
100+ heap_emit_alloc(code); // RAX = out block
101+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);
102+
103+ emit_mov_reg_imm64(code, REG_RCX, 0);
104+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RCX); // i = 0
105+
106+ int loop_start = code->count;
107+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i
108+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 0); // count
109+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
110+ int loop_exit = emit_jcc_rel32(code, COND_GE);
111+
112+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 8); // entries_ptr
113+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
114+ emit_mov_reg_imm64(code, REG_RDX, MAP_ENTRY_SIZE);
115+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
116+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // entry address
117+
118+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // value tag
119+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 24); // value payload
120+
121+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_block
122+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);
123+ emit_mov_reg_imm64(code, REG_RBX, 16);
124+ emit_imul_reg_reg(code, REG_RDX, REG_RBX);
125+ emit_add_reg_reg(code, REG_RSI, REG_RDX); // out slot address
126+
127+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI);
128+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
129+
130+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24);
131+ emit_add_reg_imm8(code, REG_RCX, 1);
132+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RCX);
133+ emit_jmp_back(code, loop_start);
134+ emit_patch_jump(code, loop_exit);
135+
136+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 0); // count
137+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
138+ heap_emit_alloc(code); // RAX = array object
139+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out block (reload)
140+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
141+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX);
142+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
143+
144+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
145+ emit_push_reg(code, REG_RBX);
146+ emit_push_reg(code, REG_RAX);
147+}

kiln/src/codegen/keys_values.dadded

+11−0
@@ -0,0 +1,11 @@
1+src/codegen/keys_values.o: src/codegen/keys_values.c \
2+ src/codegen/keys_values.h src/codegen/emit.h src/codegen/value.h \
3+ src/codegen/heap.h src/parser/parser.h src/lexer/lexer.h \
4+ src/parser/vars.h
5+src/codegen/keys_values.h:
6+src/codegen/emit.h:
7+src/codegen/value.h:
8+src/codegen/heap.h:
9+src/parser/parser.h:
10+src/lexer/lexer.h:
11+src/parser/vars.h:

kiln/src/codegen/keys_values.hadded

+9−0
@@ -0,0 +1,9 @@
1+#ifndef KILN_KEYS_VALUES_H
2+#define KILN_KEYS_VALUES_H
3+
4+// Both assume their one MAP argument already pushed -- codegen/builtins.c's
5+// dispatch calls these after parsing keys(m)/values(m)'s argument.
6+void codegen_builtin_keys(void);
7+void codegen_builtin_values(void);
8+
9+#endif

kiln/src/codegen/lambda.cadded

+111−0
@@ -0,0 +1,111 @@
1+#include <string.h>
2+#include "codegen/closures.h"
3+#include "codegen/closures_internal.h"
4+#include "codegen/emit.h"
5+#include "codegen/value.h"
6+#include "codegen/expr.h"
7+#include "codegen/layout.h"
8+#include "parser/parser.h"
9+#include "parser/parser_internal.h"
10+#include "parser/vars.h"
11+#include "parser/functions.h"
12+#include "parser/for_loop.h"
13+#include "parser/loop_stack.h"
14+
15+// `fn (params) { body }` as an expression. The closure-object creation
16+// code (which runs in the OUTER frame, snapshotting captures by value)
17+// is emitted first, then a jump-over, then the body -- same shape as
18+// parser_control.c's fn_statement, generalized with a capture region
19+// ahead of the parameter region. The object's code address isn't known
20+// until after the body compiles, so it's written via a patchable imm64
21+// (see emit_mov_reg_imm64_patchable) and patched once the body's end is
22+// reached.
23+void codegen_lambda_expr(void) {
24+ advance_token(); // consume 'fn'
25+
26+ VarScope outer_vars;
27+ vars_save(&outer_vars);
28+ int capture_count = outer_vars.count;
29+
30+ closures_alloc_object(capture_count); // RBX = object address
31+ int patch_offset = emit_mov_reg_imm64_patchable(code, REG_RCX);
32+ emit_store_mem_disp32(code, REG_RBX, 0, REG_RCX); // [obj+0] = code addr, patched below
33+
34+ for (int i = 0; i < capture_count; i++) {
35+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_tag_offset(i));
36+ emit_store_mem_disp32(code, REG_RBX, 16 + 16 * i, REG_RAX);
37+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(i));
38+ emit_store_mem_disp32(code, REG_RBX, 16 + 16 * i + 8, REG_RAX);
39+ }
40+ closures_push_value();
41+
42+ int skip_jump = emit_jmp_rel32(code);
43+ int lambda_code_offset = code->count;
44+
45+ vars_clear();
46+ int outer_for_depth;
47+ for_depth_save(&outer_for_depth);
48+ for_depth_reset();
49+ int outer_loop_depth;
50+ loop_depth_save(&outer_loop_depth);
51+ loop_depth_reset();
52+ for (int i = 0; i < capture_count; i++) {
53+ declare_var(outer_vars.names[i], (int)strlen(outer_vars.names[i]));
54+ }
55+
56+ expect(TOKEN_LPAREN, "expected '(' after 'fn'");
57+ char param_names[MAX_KILN_PARAMS][64];
58+ int param_lens[MAX_KILN_PARAMS];
59+ int argc = 0;
60+ if (current.type != TOKEN_RPAREN) {
61+ for (;;) {
62+ expect(TOKEN_IDENTIFIER, "expected parameter name");
63+ if (argc >= MAX_KILN_PARAMS) parse_error("too many parameters");
64+ param_lens[argc] = previous.length;
65+ memcpy(param_names[argc], previous.start, (size_t)previous.length);
66+ argc++;
67+ if (current.type != TOKEN_COMMA) break;
68+ advance_token();
69+ }
70+ }
71+ expect(TOKEN_RPAREN, "expected ')' after parameters");
72+
73+ emit_push_reg(code, REG_RBP);
74+ emit_mov_reg_reg(code, REG_RBP, REG_RSP);
75+ emit_sub_reg_imm32(code, REG_RSP, KILN_FRAME_RESERVE);
76+
77+ // Captures sit nearest to rbp (pushed last by codegen_indirect_call),
78+ // args sit further out, past all the captures -- see closures.c's
79+ // header comment for the full stack shape.
80+ for (int i = 0; i < capture_count; i++) {
81+ int base = 16 + 16 * (capture_count - 1 - i);
82+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base);
83+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(i), REG_RAX);
84+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 8);
85+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(i), REG_RBX);
86+ }
87+ for (int i = 0; i < argc; i++) {
88+ int slot = declare_var(param_names[i], param_lens[i]);
89+ int base = 16 + 16 * capture_count + 16 * (argc - 1 - i);
90+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base);
91+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
92+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 8);
93+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
94+ }
95+
96+ block();
97+
98+ emit_mov_reg_imm64(code, REG_RAX, 0); // implicit `return 0;`
99+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
100+ emit_mov_reg_reg(code, REG_RSP, REG_RBP);
101+ emit_pop_reg(code, REG_RBP);
102+ emit_ret(code);
103+
104+ vars_restore(&outer_vars);
105+ for_depth_restore(outer_for_depth);
106+ loop_depth_restore(outer_loop_depth);
107+ emit_patch_jump(code, skip_jump);
108+
109+ uint64_t addr = kiln_code_base() + (uint64_t)lambda_code_offset;
110+ emit_patch_imm64(code, patch_offset, addr);
111+}

kiln/src/codegen/lambda.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/codegen/lambda.o: src/codegen/lambda.c src/codegen/closures.h \
2+ src/parser/functions.h src/codegen/closures_internal.h \
3+ src/codegen/emit.h src/codegen/value.h src/codegen/expr.h \
4+ src/lexer/lexer.h src/codegen/layout.h src/parser/parser.h \
5+ src/parser/parser_internal.h src/parser/vars.h src/parser/for_loop.h \
6+ src/parser/loop_stack.h
7+src/codegen/closures.h:
8+src/parser/functions.h:
9+src/codegen/closures_internal.h:
10+src/codegen/emit.h:
11+src/codegen/value.h:
12+src/codegen/expr.h:
13+src/lexer/lexer.h:
14+src/codegen/layout.h:
15+src/parser/parser.h:
16+src/parser/parser_internal.h:
17+src/parser/vars.h:
18+src/parser/for_loop.h:
19+src/parser/loop_stack.h:

kiln/src/codegen/layout.cadded

+44−0
@@ -0,0 +1,44 @@
1+#include "codegen/layout.h"
2+#include "target.h"
3+#include "elf/elf_writer.h"
4+#include "elf/elf_dynamic.h"
5+#include "pe/pe_writer.h"
6+#include "pe/pe_dll_writer.h"
7+
8+static int is_linux_shared(void) {
9+ return kiln_get_target() == KILN_TARGET_LINUX && kiln_get_link_mode() == KILN_LINK_SHARED;
10+}
11+
12+uint64_t kiln_code_base(void) {
13+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
14+ // Three different fixed bases depending on exactly what's being
15+ // compiled: libkilnrt.dll's own code (own fixed base, distinct
16+ // from any executable's), a --link=shared executable's code
17+ // (globals stay at the same spot either way, but the import
18+ // table before it is a different size -- see pe_writer.h), or a
19+ // plain static executable's code (unchanged since Plan A).
20+ if (kiln_is_compiling_so()) return KILN_PE_DLL_IMAGE_BASE + KILN_PE_DLL_CODE_START_OFFSET;
21+ if (kiln_get_link_mode() == KILN_LINK_SHARED) return KILN_PE_IMAGE_BASE + KILN_PE_SHARED_CODE_START_OFFSET;
22+ return KILN_PE_IMAGE_BASE + KILN_PE_CODE_START_OFFSET;
23+ }
24+ if (is_linux_shared()) return KILN_LOAD_BASE + KILN_DYN_CODE_START_OFFSET;
25+ return KILN_LOAD_BASE + KILN_CODE_START_OFFSET;
26+}
27+
28+uint64_t kiln_heap_ptr_addr(void) {
29+ if (kiln_get_target() == KILN_TARGET_WINDOWS) return KILN_PE_HEAP_PTR_ADDR; // same address either link mode -- globals sit before the (variably-sized) import tables
30+ if (is_linux_shared()) return KILN_DYN_GLOBALS_ADDR;
31+ return KILN_GLOBALS_ADDR;
32+}
33+
34+uint64_t kiln_try_depth_addr(void) {
35+ if (kiln_get_target() == KILN_TARGET_WINDOWS) return KILN_PE_TRY_DEPTH_ADDR;
36+ if (is_linux_shared()) return KILN_DYN_GLOBALS_ADDR + 8;
37+ return KILN_TRY_DEPTH_ADDR;
38+}
39+
40+uint64_t kiln_try_handlers_addr(void) {
41+ if (kiln_get_target() == KILN_TARGET_WINDOWS) return KILN_PE_TRY_HANDLERS_ADDR;
42+ if (is_linux_shared()) return KILN_DYN_GLOBALS_ADDR + 8 + KILN_TRY_DEPTH_SIZE;
43+ return KILN_TRY_HANDLERS_ADDR;
44+}

kiln/src/codegen/layout.dadded

+16−0
@@ -0,0 +1,16 @@
1+src/codegen/layout.o: src/codegen/layout.c src/codegen/layout.h \
2+ src/target.h src/elf/elf_writer.h src/codegen/emit.h \
3+ src/codegen/runtime_layout.h src/elf/elf_dynamic.h \
4+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
5+ src/pe/pe_writer.h src/pe/pe_imports.def src/pe/pe_dll_writer.h
6+src/codegen/layout.h:
7+src/target.h:
8+src/elf/elf_writer.h:
9+src/codegen/emit.h:
10+src/codegen/runtime_layout.h:
11+src/elf/elf_dynamic.h:
12+src/codegen/runtime_import_enum.h:
13+src/codegen/runtime_exports.def:
14+src/pe/pe_writer.h:
15+src/pe/pe_imports.def:
16+src/pe/pe_dll_writer.h:

kiln/src/codegen/layout.hadded

+18−0
@@ -0,0 +1,18 @@
1+#ifndef KILN_LAYOUT_H
2+#define KILN_LAYOUT_H
3+#include <stdint.h>
4+
5+// Every place in codegen that needs "the absolute address of code offset
6+// N" or "the absolute address of a globals-region field" used to hardcode
7+// KILN_LOAD_BASE+KILN_CODE_START_OFFSET / KILN_TRY_DEPTH_ADDR / etc.
8+// directly from elf/elf_writer.h. Now that kiln can target two different
9+// executable formats with two different fixed base addresses (see
10+// elf/elf_writer.h vs pe/pe_writer.h), those hardcodes would each need
11+// their own target check -- so instead every call site goes through one
12+// of these, and the target check lives here exactly once.
13+uint64_t kiln_code_base(void); // add a code offset to get an absolute address
14+uint64_t kiln_heap_ptr_addr(void);
15+uint64_t kiln_try_depth_addr(void);
16+uint64_t kiln_try_handlers_addr(void);
17+
18+#endif

kiln/src/codegen/map_builtin.cadded

+99−0
@@ -0,0 +1,99 @@
1+#include "codegen/higher_order.h"
2+#include "codegen/expr.h"
3+#include "codegen/closures.h"
4+#include "codegen/emit.h"
5+#include "codegen/value.h"
6+#include "codegen/heap.h"
7+#include "parser/parser.h"
8+#include "parser/vars.h"
9+
10+// Scratch layout (see vars.h's higher_order_scratch_offset -- everything
11+// needed after codegen_call_closure_value must live here, never in a
12+// register, since the call clobbers all of them):
13+// [0]=fn_tag [8]=fn_payload [16]=data_ptr [24]=count
14+// [32]=out_data_ptr [40]=i
15+void codegen_builtin_map(void) {
16+ codegen_expression(); // array
17+ expect(TOKEN_COMMA, "expected ',' after array argument");
18+ codegen_expression(); // fn
19+ expect(TOKEN_RPAREN, "expected ')' after arguments");
20+
21+ emit_pop_reg(code, REG_RAX); // fn payload
22+ emit_pop_reg(code, REG_RBX); // fn tag
23+ emit_pop_reg(code, REG_RSI); // array object
24+ emit_pop_reg(code, REG_RCX); // array tag (ignored, assumed ARRAY)
25+
26+ int32_t base = higher_order_scratch_offset();
27+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RBX);
28+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX);
29+
30+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
31+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr
32+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RDX);
33+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
34+
35+ // output elements block, sized to match (count could be 0 at runtime)
36+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
37+ emit_mov_reg_imm64(code, REG_RBX, 16);
38+ emit_imul_reg_reg(code, REG_RDI, REG_RBX);
39+ emit_cmp_reg_imm32(code, REG_RDI, 0);
40+ int nonzero = emit_jcc_rel32(code, COND_NE);
41+ emit_mov_reg_imm64(code, REG_RDI, 1);
42+ emit_patch_jump(code, nonzero);
43+ heap_emit_alloc(code); // RAX = out block
44+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX);
45+
46+ emit_mov_reg_imm64(code, REG_RCX, 0);
47+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // i = 0
48+
49+ int loop_start = code->count;
50+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
51+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
52+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
53+ int loop_exit = emit_jcc_rel32(code, COND_GE);
54+
55+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // data_ptr
56+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
57+ emit_mov_reg_imm64(code, REG_RDX, 16);
58+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
59+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // element address
60+
61+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 0); // fn_tag
62+ emit_push_reg(code, REG_RAX);
63+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // fn_payload
64+ emit_push_reg(code, REG_RAX);
65+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // element tag
66+ emit_push_reg(code, REG_RAX);
67+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload
68+ emit_push_reg(code, REG_RAX);
69+ codegen_call_closure_value(1);
70+
71+ emit_pop_reg(code, REG_RAX); // result payload
72+ emit_pop_reg(code, REG_RBX); // result tag
73+
74+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // i
75+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32); // out_data_ptr
76+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);
77+ emit_mov_reg_imm64(code, REG_RDI, 16);
78+ emit_imul_reg_reg(code, REG_RDX, REG_RDI);
79+ emit_add_reg_reg(code, REG_RSI, REG_RDX);
80+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RBX);
81+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
82+
83+ emit_add_reg_imm8(code, REG_RCX, 1);
84+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX);
85+ emit_jmp_back(code, loop_start);
86+ emit_patch_jump(code, loop_exit);
87+
88+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // count
89+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
90+ heap_emit_alloc(code); // RAX = array object
91+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32); // out_data_ptr (reload -- alloc clobbers RSI)
92+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // capacity
93+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count
94+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI); // data_ptr
95+
96+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
97+ emit_push_reg(code, REG_RBX);
98+ emit_push_reg(code, REG_RAX);
99+}

kiln/src/codegen/map_builtin.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/codegen/map_builtin.o: src/codegen/map_builtin.c \
2+ src/codegen/higher_order.h src/codegen/expr.h src/lexer/lexer.h \
3+ src/codegen/closures.h src/parser/functions.h src/codegen/emit.h \
4+ src/codegen/value.h src/codegen/heap.h src/parser/parser.h \
5+ src/parser/vars.h
6+src/codegen/higher_order.h:
7+src/codegen/expr.h:
8+src/lexer/lexer.h:
9+src/codegen/closures.h:
10+src/parser/functions.h:
11+src/codegen/emit.h:
12+src/codegen/value.h:
13+src/codegen/heap.h:
14+src/parser/parser.h:
15+src/parser/vars.h:

kiln/src/codegen/maps.cadded

+116−0
@@ -0,0 +1,116 @@
1+#include "codegen/maps.h"
2+#include "codegen/maps_internal.h"
3+#include "codegen/expr.h"
4+#include "codegen/strings.h"
5+#include "codegen/emit_internal.h"
6+#include "codegen/value.h"
7+#include "codegen/heap.h"
8+#include "codegen/errors.h"
9+#include "parser/parser.h"
10+
11+void codegen_map_literal(void) {
12+ advance_token(); // consume '{'
13+ int count = 0;
14+ if (current.type != TOKEN_RBRACE) {
15+ for (;;) {
16+ if (current.type != TOKEN_STRING) parse_error("expected string key in map literal");
17+ codegen_string_literal(); // pushes (TAG_STRING, key address)
18+ expect(TOKEN_COLON, "expected ':' after map key");
19+ codegen_expression(); // pushes the value
20+ count++;
21+ if (current.type != TOKEN_COMMA) break;
22+ advance_token();
23+ }
24+ }
25+ expect(TOKEN_RBRACE, "expected '}' after map literal");
26+
27+ emit_mov_reg_imm64(code, REG_RDI, (uint64_t)(count > 0 ? count * MAP_ENTRY_SIZE : 1));
28+ heap_emit_alloc(code); // RAX = entries block
29+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash across the pop loop
30+
31+ // Pairs were pushed key-then-value, left to right, so the LAST
32+ // pair's value is on top -- pop in reverse to land each pair right.
33+ for (int i = count - 1; i >= 0; i--) {
34+ emit_pop_reg(code, REG_RAX); // value payload
35+ emit_pop_reg(code, REG_RBX); // value tag
36+ int off = MAP_ENTRY_SIZE * i;
37+ emit_store_mem_disp32(code, REG_RSI, off + 16, REG_RBX);
38+ emit_store_mem_disp32(code, REG_RSI, off + 24, REG_RAX);
39+ emit_pop_reg(code, REG_RAX); // key payload
40+ emit_pop_reg(code, REG_RBX); // key tag
41+ emit_store_mem_disp32(code, REG_RSI, off, REG_RBX);
42+ emit_store_mem_disp32(code, REG_RSI, off + 8, REG_RAX);
43+ }
44+
45+ emit_push_reg(code, REG_RSI); // save entries block across this alloc
46+ emit_mov_reg_imm64(code, REG_RDI, MAP_OBJECT_SIZE);
47+ heap_emit_alloc(code); // RAX = map object
48+ emit_pop_reg(code, REG_RSI);
49+
50+ emit_mov_reg_imm64(code, REG_RCX, (uint64_t)count);
51+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // capacity
52+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count
53+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI); // entries_ptr
54+
55+ emit_mov_reg_imm64(code, REG_RBX, TAG_MAP);
56+ emit_push_reg(code, REG_RBX);
57+ emit_push_reg(code, REG_RAX);
58+}
59+
60+int find_entry_index(void) {
61+ emit_mov_reg_imm64(code, REG_RCX, 0); // i
62+
63+ int loop_start = code->count;
64+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count
65+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
66+ int not_found = emit_jcc_rel32(code, COND_GE);
67+
68+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // entries_ptr
69+ emit_mov_reg_reg(code, REG_RBX, REG_RCX);
70+ emit_push_reg(code, REG_RCX);
71+ emit_mov_reg_imm64(code, REG_RCX, MAP_ENTRY_SIZE);
72+ emit_imul_reg_reg(code, REG_RBX, REG_RCX);
73+ emit_pop_reg(code, REG_RCX);
74+ emit_add_reg_reg(code, REG_RBX, REG_RDX); // RBX = entry addr
75+
76+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 8); // entry key payload (a_payload)
77+ emit_push_reg(code, REG_RCX); // i
78+ emit_push_reg(code, REG_RSI); // map object
79+ emit_push_reg(code, REG_RDI); // target key
80+ emit_push_reg(code, REG_RBX); // entry addr
81+ emit_mov_reg_reg(code, REG_RCX, REG_RDI); // b_payload = target key
82+ codegen_string_compare(0); // pushes (tag=NUMBER, payload=0.0/1.0)
83+ emit_pop_reg(code, REG_RAX); // compare result payload
84+ emit_pop_reg(code, REG_RDX); // compare result tag (ignored)
85+ emit_pop_reg(code, REG_RBX); // entry addr
86+ emit_pop_reg(code, REG_RDI); // target key
87+ emit_pop_reg(code, REG_RSI); // map object
88+ emit_pop_reg(code, REG_RCX); // i
89+ emit_cmp_reg_imm32(code, REG_RAX, 0);
90+ int is_match = emit_jcc_rel32(code, COND_NE);
91+
92+ emit_add_reg_imm8(code, REG_RCX, 1);
93+ emit_jmp_back(code, loop_start);
94+
95+ emit_patch_jump(code, is_match);
96+ return not_found;
97+}
98+
99+void codegen_map_index_read(void) {
100+ emit_pop_reg(code, REG_RDI); // key payload
101+ emit_pop_reg(code, REG_RBX); // key tag (assumed STRING)
102+ emit_pop_reg(code, REG_RSI); // map object
103+ emit_pop_reg(code, REG_RBX); // map tag (assumed MAP)
104+
105+ int not_found = find_entry_index();
106+ emit_load_mem_disp32(code, REG_RDX, REG_RBX, 16); // value tag
107+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 24); // value payload
108+ emit_push_reg(code, REG_RDX);
109+ emit_push_reg(code, REG_RAX);
110+ int done = emit_jmp_rel32(code);
111+
112+ emit_patch_jump(code, not_found);
113+ errors_emit_die(code, "runtime error: key not found");
114+
115+ emit_patch_jump(code, done);
116+}

kiln/src/codegen/maps.dadded

+16−0
@@ -0,0 +1,16 @@
1+src/codegen/maps.o: src/codegen/maps.c src/codegen/maps.h \
2+ src/codegen/maps_internal.h src/codegen/expr.h src/lexer/lexer.h \
3+ src/codegen/strings.h src/codegen/emit_internal.h src/codegen/emit.h \
4+ src/codegen/value.h src/codegen/heap.h src/codegen/errors.h \
5+ src/parser/parser.h
6+src/codegen/maps.h:
7+src/codegen/maps_internal.h:
8+src/codegen/expr.h:
9+src/lexer/lexer.h:
10+src/codegen/strings.h:
11+src/codegen/emit_internal.h:
12+src/codegen/emit.h:
13+src/codegen/value.h:
14+src/codegen/heap.h:
15+src/codegen/errors.h:
16+src/parser/parser.h:

kiln/src/codegen/maps.hadded

+28−0
@@ -0,0 +1,28 @@
1+#ifndef KILN_MAPS_H
2+#define KILN_MAPS_H
3+
4+// primary()'s '{' handling: parses `{"k0": v0, "k1": v1, ...}` (keys must
5+// be string literals, matching ashvm's own map-literal restriction),
6+// allocates, pushes (tag=TAG_MAP, payload=<map object address>).
7+void codegen_map_literal(void);
8+
9+// Assumes key already pushed (top) and map already pushed (below it),
10+// both (tag, payload) pairs -- postfix()'s `m[k]` read. Dies with a
11+// clean error if the key isn't present (matching ashvm's "key not found").
12+void codegen_map_index_read(void);
13+
14+// Assumes value pushed (top), key pushed (middle), map pushed (bottom)
15+// -- parser.c's `m[k] = v;` write. Updates in place if the key already
16+// exists, otherwise appends a new entry (growing if needed).
17+void codegen_map_index_store(void);
18+
19+// Builtins, dispatched from codegen/builtins.c -- each assumes its
20+// arguments already pushed left to right, matching the normal
21+// call-argument convention. keys()/values() (each needs to build a new
22+// ARRAY sized only-known-at-runtime, real extra complexity for
23+// comparatively rare use) are deferred to milestone 10 alongside the
24+// rest of ashvm's remaining builtins -- not implemented here.
25+void codegen_builtin_has(void); // has(map, key) -> NUMBER 1.0/0.0
26+void codegen_builtin_delete(void); // delete(map, key) -> the map (matches ashvm), no-op if key absent
27+
28+#endif

kiln/src/codegen/maps_internal.hadded

+14−0
@@ -0,0 +1,14 @@
1+#ifndef KILN_MAPS_INTERNAL_H
2+#define KILN_MAPS_INTERNAL_H
3+
4+// Cross-file link between maps.c (literal, find_entry_index, read) and
5+// maps_mutate.c (store/has/delete -- everything that needs to find AND
6+// change an entry), not part of the public API (maps.h).
7+
8+// INPUT: RSI=map object, RDI=target key payload -- both untouched by the
9+// loop until a match is found. On the "found" path (falls through),
10+// RBX=matching entry address. Returns the not-found jump's patch offset;
11+// the caller decides what "not found" means and must patch it.
12+int find_entry_index(void);
13+
14+#endif

kiln/src/codegen/maps_mutate.cadded

+176−0
@@ -0,0 +1,176 @@
1+#include "codegen/maps.h"
2+#include "codegen/maps_internal.h"
3+#include "codegen/emit_internal.h"
4+#include "codegen/value.h"
5+#include "codegen/heap.h"
6+#include "codegen/bytes.h"
7+#include "parser/parser.h"
8+
9+static void push_number_local(double v) {
10+ uint64_t bits;
11+ __builtin_memcpy(&bits, &v, sizeof(bits));
12+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
13+ emit_push_reg(code, REG_RBX);
14+ emit_mov_reg_imm64(code, REG_RAX, bits);
15+ emit_push_reg(code, REG_RAX);
16+}
17+
18+// Scratch layout (base recomputed via `mov reg,rsp` after any call that
19+// might have clobbered whichever register held it -- same technique
20+// codegen/arrays.c's codegen_builtin_push uses): [0]=val_tag
21+// [8]=val_payload [16]=key_payload [24]=map_object [32]=new_capacity
22+// (grow path only) [40]=new_entries_block (grow path only)
23+void codegen_map_index_store(void) {
24+ emit_pop_reg(code, REG_RAX); // value payload
25+ emit_pop_reg(code, REG_RBX); // value tag
26+ emit_pop_reg(code, REG_RDI); // key payload
27+ emit_pop_reg(code, REG_RCX); // key tag (ignored)
28+ emit_pop_reg(code, REG_RSI); // map object
29+ emit_pop_reg(code, REG_RCX); // map tag (ignored)
30+
31+ emit_sub_reg_imm8(code, REG_RSP, 48);
32+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
33+ emit_store_mem_disp32(code, REG_RCX, 0, REG_RBX);
34+ emit_store_mem_disp32(code, REG_RCX, 8, REG_RAX);
35+ emit_store_mem_disp32(code, REG_RCX, 16, REG_RDI);
36+ emit_store_mem_disp32(code, REG_RCX, 24, REG_RSI);
37+
38+ int not_found = find_entry_index();
39+
40+ // --- found: update value in place ---
41+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
42+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 0);
43+ emit_store_mem_disp32(code, REG_RBX, 16, REG_RAX);
44+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8);
45+ emit_store_mem_disp32(code, REG_RBX, 24, REG_RAX);
46+ emit_add_reg_imm8(code, REG_RSP, 48);
47+ int done = emit_jmp_rel32(code);
48+
49+ // --- not found: append, growing first if at capacity ---
50+ emit_patch_jump(code, not_found);
51+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
52+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 24);
53+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // capacity
54+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count
55+ emit_cmp_reg_reg(code, REG_RDX, REG_RAX);
56+ int has_room = emit_jcc_rel32(code, COND_LT);
57+
58+ emit_add_reg_imm8(code, REG_RAX, 4); // new_capacity
59+ emit_store_mem_disp32(code, REG_RCX, 32, REG_RAX);
60+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
61+ emit_mov_reg_imm64(code, REG_RBX, MAP_ENTRY_SIZE);
62+ emit_imul_reg_reg(code, REG_RDI, REG_RBX);
63+ heap_emit_alloc(code); // RAX = new entries block
64+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
65+ emit_store_mem_disp32(code, REG_RCX, 40, REG_RAX);
66+
67+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 24);
68+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 0); // old capacity
69+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 16); // old entries_ptr (src)
70+ emit_load_mem_disp32(code, REG_RDI, REG_RCX, 40); // new block (dest)
71+ emit_mov_reg_reg(code, REG_RAX, REG_RDX);
72+ emit_mov_reg_imm64(code, REG_RDX, MAP_ENTRY_SIZE);
73+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
74+ emit_mov_reg_reg(code, REG_RDX, REG_RAX); // byte count
75+ bytes_emit_copy(code);
76+
77+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
78+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 24);
79+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 32);
80+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX); // capacity = new_capacity
81+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 40);
82+ emit_store_mem_disp32(code, REG_RSI, 16, REG_RAX); // entries_ptr = new block
83+
84+ emit_patch_jump(code, has_room);
85+
86+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
87+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 24);
88+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count
89+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // entries_ptr
90+ emit_mov_reg_reg(code, REG_RAX, REG_RDX);
91+ emit_mov_reg_imm64(code, REG_RDX, MAP_ENTRY_SIZE);
92+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
93+ emit_add_reg_reg(code, REG_RAX, REG_RDI); // RAX = new entry's address
94+
95+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
96+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RBX);
97+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 16); // key_payload
98+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RBX);
99+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 0); // val_tag
100+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RBX);
101+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 8); // val_payload
102+ emit_store_mem_disp32(code, REG_RAX, 24, REG_RBX);
103+
104+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 8);
105+ emit_add_reg_imm8(code, REG_RBX, 1);
106+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RBX); // count++
107+
108+ emit_add_reg_imm8(code, REG_RSP, 48);
109+
110+ emit_patch_jump(code, done);
111+}
112+
113+void codegen_builtin_has(void) {
114+ emit_pop_reg(code, REG_RDI); // key payload
115+ emit_pop_reg(code, REG_RBX); // key tag (ignored)
116+ emit_pop_reg(code, REG_RSI); // map object
117+ emit_pop_reg(code, REG_RBX); // map tag (ignored)
118+
119+ int not_found = find_entry_index();
120+ push_number_local(1.0);
121+ int done = emit_jmp_rel32(code);
122+ emit_patch_jump(code, not_found);
123+ push_number_local(0.0);
124+ emit_patch_jump(code, done);
125+}
126+
127+void codegen_builtin_delete(void) {
128+ emit_pop_reg(code, REG_RDI); // key payload
129+ emit_pop_reg(code, REG_RBX); // key tag (ignored)
130+ emit_pop_reg(code, REG_RSI); // map object
131+ emit_pop_reg(code, REG_RBX); // map tag (ignored)
132+
133+ emit_sub_reg_imm8(code, REG_RSP, 8);
134+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
135+ emit_store_mem_disp32(code, REG_RCX, 0, REG_RSI);
136+
137+ int not_found = find_entry_index();
138+ // found: RBX = entry addr to remove -- overwrite it with the last
139+ // entry's data (order doesn't matter for a map), then count--
140+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
141+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 0);
142+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
143+ emit_sub_reg_imm8(code, REG_RAX, 1); // last_index
144+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // entries_ptr
145+ emit_mov_reg_reg(code, REG_RCX, REG_RAX);
146+ emit_mov_reg_imm64(code, REG_RAX, MAP_ENTRY_SIZE);
147+ emit_imul_reg_reg(code, REG_RCX, REG_RAX);
148+ emit_add_reg_reg(code, REG_RCX, REG_RDX); // RCX = last entry's address
149+
150+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 0); emit_store_mem_disp32(code, REG_RBX, 0, REG_RAX);
151+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8); emit_store_mem_disp32(code, REG_RBX, 8, REG_RAX);
152+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 16); emit_store_mem_disp32(code, REG_RBX, 16, REG_RAX);
153+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 24); emit_store_mem_disp32(code, REG_RBX, 24, REG_RAX);
154+
155+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8);
156+ emit_sub_reg_imm8(code, REG_RAX, 1);
157+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX); // count--
158+
159+ emit_add_reg_imm8(code, REG_RSP, 8);
160+ emit_mov_reg_imm64(code, REG_RBX, TAG_MAP);
161+ emit_push_reg(code, REG_RBX);
162+ emit_push_reg(code, REG_RSI); // delete() returns the map, matching ashvm
163+ int done = emit_jmp_rel32(code);
164+
165+ // key absent -- no-op, still return the map (matches ashvm's
166+ // unconditional "vm_map_delete then return args[0]")
167+ emit_patch_jump(code, not_found);
168+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
169+ emit_load_mem_disp32(code, REG_RSI, REG_RCX, 0);
170+ emit_add_reg_imm8(code, REG_RSP, 8);
171+ emit_mov_reg_imm64(code, REG_RBX, TAG_MAP);
172+ emit_push_reg(code, REG_RBX);
173+ emit_push_reg(code, REG_RSI);
174+
175+ emit_patch_jump(code, done);
176+}

kiln/src/codegen/maps_mutate.dadded

+13−0
@@ -0,0 +1,13 @@
1+src/codegen/maps_mutate.o: src/codegen/maps_mutate.c src/codegen/maps.h \
2+ src/codegen/maps_internal.h src/codegen/emit_internal.h \
3+ src/codegen/emit.h src/codegen/value.h src/codegen/heap.h \
4+ src/codegen/bytes.h src/parser/parser.h src/lexer/lexer.h
5+src/codegen/maps.h:
6+src/codegen/maps_internal.h:
7+src/codegen/emit_internal.h:
8+src/codegen/emit.h:
9+src/codegen/value.h:
10+src/codegen/heap.h:
11+src/codegen/bytes.h:
12+src/parser/parser.h:
13+src/lexer/lexer.h:

kiln/src/codegen/math_builtins.cadded

+51−0
@@ -0,0 +1,51 @@
1+#include "codegen/math_builtins.h"
2+#include "codegen/emit.h"
3+#include "codegen/emit_sse.h"
4+#include "codegen/value.h"
5+#include "parser/parser.h"
6+
7+void codegen_builtin_sqrt(void) {
8+ emit_pop_reg(code, REG_RAX); // payload
9+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
10+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
11+ emit_sqrtsd(code, XMM0, XMM0);
12+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
13+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
14+ emit_push_reg(code, REG_RBX);
15+ emit_push_reg(code, REG_RAX);
16+}
17+
18+void codegen_builtin_abs(void) {
19+ emit_pop_reg(code, REG_RAX); // payload
20+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
21+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
22+ emit_mov_reg_imm64(code, REG_RAX, 0x7FFFFFFFFFFFFFFFULL); // clears just the sign bit
23+ emit_movq_xmm_from_reg(code, XMM1, REG_RAX);
24+ emit_andpd(code, XMM0, XMM1);
25+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
26+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
27+ emit_push_reg(code, REG_RBX);
28+ emit_push_reg(code, REG_RAX);
29+}
30+
31+// cvttsd2si truncates TOWARD ZERO, which is already floor() for x>=0 but
32+// rounds the wrong way for a negative x with a fractional part (e.g.
33+// -3.5 truncates to -3, but floor(-3.5) is -4) -- converting the
34+// truncated result back to a double and checking whether it OVERSHOT the
35+// original value catches exactly that case.
36+void codegen_builtin_floor(void) {
37+ emit_pop_reg(code, REG_RAX); // payload
38+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
39+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
40+ emit_cvttsd2si(code, REG_RCX, XMM0);
41+ emit_cvtsi2sd(code, XMM1, REG_RCX);
42+ emit_ucomisd(code, XMM1, XMM0);
43+ int no_overshoot = emit_jcc_rel32(code, COND_BE); // truncated <= original -> already floor
44+ emit_dec_reg(code, REG_RCX);
45+ emit_patch_jump(code, no_overshoot);
46+ emit_cvtsi2sd(code, XMM0, REG_RCX);
47+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
48+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
49+ emit_push_reg(code, REG_RBX);
50+ emit_push_reg(code, REG_RAX);
51+}

kiln/src/codegen/math_builtins.dadded

+9−0
@@ -0,0 +1,9 @@
1+src/codegen/math_builtins.o: src/codegen/math_builtins.c \
2+ src/codegen/math_builtins.h src/codegen/emit.h src/codegen/emit_sse.h \
3+ src/codegen/value.h src/parser/parser.h src/lexer/lexer.h
4+src/codegen/math_builtins.h:
5+src/codegen/emit.h:
6+src/codegen/emit_sse.h:
7+src/codegen/value.h:
8+src/parser/parser.h:
9+src/lexer/lexer.h:

kiln/src/codegen/math_builtins.hadded

+10−0
@@ -0,0 +1,10 @@
1+#ifndef KILN_MATH_BUILTINS_H
2+#define KILN_MATH_BUILTINS_H
3+
4+// Each assumes its one NUMBER argument already pushed -- codegen/builtins.c's
5+// dispatch calls these after parsing sqrt(x)/abs(x)/floor(x)'s argument.
6+void codegen_builtin_sqrt(void);
7+void codegen_builtin_abs(void);
8+void codegen_builtin_floor(void);
9+
10+#endif

kiln/src/codegen/num_builtin.cadded

+132−0
@@ -0,0 +1,132 @@
1+#include <string.h>
2+#include "codegen/convert_builtins.h"
3+#include "codegen/emit.h"
4+#include "codegen/emit_sse.h"
5+#include "codegen/value.h"
6+#include "parser/parser.h"
7+
8+static void load_double_const(XReg dst, double v) {
9+ uint64_t bits;
10+ memcpy(&bits, &v, sizeof(bits));
11+ emit_mov_reg_imm64(code, REG_RAX, bits);
12+ emit_movq_xmm_from_reg(code, dst, REG_RAX);
13+}
14+
15+// num(string): RSI scans the string's bytes, RDI is the fixed end
16+// pointer (payload+length) -- no libc strtod in this freestanding
17+// binary, so this hand-parses the same grammar kiln's own number
18+// literals accept (plus a leading sign and leading whitespace, matching
19+// strtod). Stops cleanly at the first byte it can't consume.
20+void codegen_builtin_num(void) {
21+ emit_pop_reg(code, REG_RSI); // string payload address
22+ emit_pop_reg(code, REG_RBX); // tag (assumed STRING)
23+
24+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, -8); // length
25+ emit_add_reg_reg(code, REG_RDI, REG_RSI); // RDI = end pointer
26+
27+ // ---- skip leading whitespace ----
28+ int ws_loop = code->count;
29+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
30+ int ws_done = emit_jcc_rel32(code, COND_GE);
31+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
32+ emit_cmp_reg_imm32(code, REG_RCX, ' ');
33+ int not_space = emit_jcc_rel32(code, COND_NE);
34+ emit_add_reg_imm8(code, REG_RSI, 1);
35+ emit_jmp_back(code, ws_loop);
36+ emit_patch_jump(code, not_space);
37+ emit_cmp_reg_imm32(code, REG_RCX, '\t');
38+ int not_tab = emit_jcc_rel32(code, COND_NE);
39+ emit_add_reg_imm8(code, REG_RSI, 1);
40+ emit_jmp_back(code, ws_loop);
41+ emit_patch_jump(code, not_tab);
42+ emit_patch_jump(code, ws_done);
43+
44+ // ---- optional sign ----
45+ emit_mov_reg_imm64(code, REG_RBX, 0); // sign flag (tag no longer needed)
46+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
47+ int no_sign_check = emit_jcc_rel32(code, COND_GE);
48+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
49+ emit_cmp_reg_imm32(code, REG_RCX, '-');
50+ int not_minus = emit_jcc_rel32(code, COND_NE);
51+ emit_mov_reg_imm64(code, REG_RBX, 1);
52+ emit_add_reg_imm8(code, REG_RSI, 1);
53+ int sign_done = emit_jmp_rel32(code);
54+ emit_patch_jump(code, not_minus);
55+ emit_cmp_reg_imm32(code, REG_RCX, '+');
56+ int not_plus = emit_jcc_rel32(code, COND_NE);
57+ emit_add_reg_imm8(code, REG_RSI, 1);
58+ emit_patch_jump(code, not_plus);
59+ emit_patch_jump(code, sign_done);
60+ emit_patch_jump(code, no_sign_check);
61+
62+ emit_pxor_xmm_xmm(code, XMM0); // accumulator = 0.0
63+
64+ // ---- integer digits ----
65+ int int_loop = code->count;
66+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
67+ int int_done = emit_jcc_rel32(code, COND_GE);
68+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
69+ emit_cmp_reg_imm32(code, REG_RCX, '0');
70+ int int_lt = emit_jcc_rel32(code, COND_LT);
71+ emit_cmp_reg_imm32(code, REG_RCX, '9');
72+ int int_gt = emit_jcc_rel32(code, COND_GT);
73+
74+ load_double_const(XMM1, 10.0);
75+ emit_mulsd(code, XMM0, XMM1);
76+ emit_sub_reg_imm8(code, REG_RCX, '0');
77+ emit_cvtsi2sd(code, XMM1, REG_RCX);
78+ emit_addsd(code, XMM0, XMM1);
79+ emit_add_reg_imm8(code, REG_RSI, 1);
80+ emit_jmp_back(code, int_loop);
81+
82+ emit_patch_jump(code, int_lt);
83+ emit_patch_jump(code, int_gt);
84+ emit_patch_jump(code, int_done);
85+
86+ // ---- optional fraction ----
87+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
88+ int no_frac_check = emit_jcc_rel32(code, COND_GE);
89+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
90+ emit_cmp_reg_imm32(code, REG_RCX, '.');
91+ int not_dot = emit_jcc_rel32(code, COND_NE);
92+ emit_add_reg_imm8(code, REG_RSI, 1);
93+
94+ load_double_const(XMM2, 0.1); // place value, halves each digit
95+ int frac_loop = code->count;
96+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
97+ int frac_done = emit_jcc_rel32(code, COND_GE);
98+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
99+ emit_cmp_reg_imm32(code, REG_RCX, '0');
100+ int frac_lt = emit_jcc_rel32(code, COND_LT);
101+ emit_cmp_reg_imm32(code, REG_RCX, '9');
102+ int frac_gt = emit_jcc_rel32(code, COND_GT);
103+
104+ emit_sub_reg_imm8(code, REG_RCX, '0');
105+ emit_cvtsi2sd(code, XMM1, REG_RCX);
106+ emit_mulsd(code, XMM1, XMM2); // digit * place
107+ emit_addsd(code, XMM0, XMM1);
108+ load_double_const(XMM1, 0.1);
109+ emit_mulsd(code, XMM2, XMM1); // place *= 0.1
110+ emit_add_reg_imm8(code, REG_RSI, 1);
111+ emit_jmp_back(code, frac_loop);
112+
113+ emit_patch_jump(code, frac_lt);
114+ emit_patch_jump(code, frac_gt);
115+ emit_patch_jump(code, frac_done);
116+
117+ emit_patch_jump(code, not_dot);
118+ emit_patch_jump(code, no_frac_check);
119+
120+ // ---- apply sign ----
121+ emit_cmp_reg_imm32(code, REG_RBX, 0);
122+ int no_negate = emit_jcc_rel32(code, COND_E);
123+ emit_pxor_xmm_xmm(code, XMM1);
124+ emit_subsd(code, XMM1, XMM0);
125+ emit_movsd_xmm_xmm(code, XMM0, XMM1);
126+ emit_patch_jump(code, no_negate);
127+
128+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
129+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
130+ emit_push_reg(code, REG_RBX);
131+ emit_push_reg(code, REG_RAX);
132+}

kiln/src/codegen/num_builtin.dadded

+9−0
@@ -0,0 +1,9 @@
1+src/codegen/num_builtin.o: src/codegen/num_builtin.c \
2+ src/codegen/convert_builtins.h src/codegen/emit.h src/codegen/emit_sse.h \
3+ src/codegen/value.h src/parser/parser.h src/lexer/lexer.h
4+src/codegen/convert_builtins.h:
5+src/codegen/emit.h:
6+src/codegen/emit_sse.h:
7+src/codegen/value.h:
8+src/parser/parser.h:
9+src/lexer/lexer.h:

kiln/src/codegen/platform_console.cadded

+73−0
@@ -0,0 +1,73 @@
1+#include "codegen/platform_console.h"
2+#include "codegen/win_call.h"
3+#include "codegen/win_file_call.h"
4+#include "parser/vars.h"
5+#include "target.h"
6+
7+#define STD_INPUT_HANDLE ((uint64_t)(int64_t)-10)
8+#define STD_OUTPUT_HANDLE ((uint64_t)(int64_t)-11)
9+#define STD_ERROR_HANDLE ((uint64_t)(int64_t)-12)
10+
11+// GetStdHandle(nStdHandle) -- result in RAX. RSI/RDI (whatever the caller
12+// is using to hold buf/len across this) survive untouched: both are
13+// non-volatile (callee-saved) in the Windows x64 ABI, unlike RDX/R8-R11.
14+static void get_std_handle(CodeBuf *code, uint64_t which) {
15+ win_call_begin(code, 0);
16+ win_call_arg_imm64(code, 0, which);
17+ win_call_import(code, PE_IMPORT_GET_STD_HANDLE);
18+ win_call_end(code);
19+}
20+
21+static void windows_write(CodeBuf *code, uint64_t handle) {
22+ emit_mov_reg_reg(code, REG_RDI, REG_RDX); // len survives get_std_handle in RDI (non-volatile)
23+ get_std_handle(code, handle);
24+ win_file_call(code, PE_IMPORT_WRITE_FILE);
25+}
26+
27+void platform_emit_write_stdout(CodeBuf *code) {
28+ if (kiln_get_target() == KILN_TARGET_WINDOWS) { windows_write(code, STD_OUTPUT_HANDLE); return; }
29+ emit_mov_reg_imm64(code, REG_RDI, 1);
30+ emit_mov_reg_imm64(code, REG_RAX, 1); // syscall: write
31+ emit_syscall(code);
32+}
33+
34+void platform_emit_write_stderr(CodeBuf *code) {
35+ if (kiln_get_target() == KILN_TARGET_WINDOWS) { windows_write(code, STD_ERROR_HANDLE); return; }
36+ emit_mov_reg_imm64(code, REG_RDI, 2);
37+ emit_mov_reg_imm64(code, REG_RAX, 1); // syscall: write
38+ emit_syscall(code);
39+}
40+
41+void platform_emit_read_stdin_byte(CodeBuf *code) {
42+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
43+ get_std_handle(code, STD_INPUT_HANDLE);
44+ platform_emit_read_stdin_byte_h(code);
45+ return;
46+ }
47+ emit_mov_reg_imm64(code, REG_RDI, 0);
48+ emit_mov_reg_imm64(code, REG_RAX, 0); // syscall: read
49+ emit_syscall(code);
50+}
51+
52+void platform_emit_get_stdin_handle(CodeBuf *code) {
53+ if (kiln_get_target() == KILN_TARGET_WINDOWS) get_std_handle(code, STD_INPUT_HANDLE);
54+ // Linux has no per-call handle to fetch (fd 0 is a fixed constant) --
55+ // callers that cache "the handle" across a loop just get an unused
56+ // RAX on that target, harmless.
57+}
58+
59+void platform_emit_read_stdin_byte_h(CodeBuf *code) {
60+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
61+ emit_mov_reg_reg(code, REG_RDI, REG_RDX); // len (always 1) survives in RDI
62+ win_file_call(code, PE_IMPORT_READ_FILE); // RAX already holds the handle
63+ // ReadFile doesn't report "bytes read" in RAX (it's a BOOL success
64+ // flag there) -- read it back out of the out-param scratch this
65+ // call just wrote, so the caller's "did we get a byte" check
66+ // still works the same way it does on the Linux read(2) path.
67+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, win_outparam_offset());
68+ return;
69+ }
70+ emit_mov_reg_imm64(code, REG_RDI, 0);
71+ emit_mov_reg_imm64(code, REG_RAX, 0); // syscall: read
72+ emit_syscall(code);
73+}

kiln/src/codegen/platform_console.dadded

+17−0
@@ -0,0 +1,17 @@
1+src/codegen/platform_console.o: src/codegen/platform_console.c \
2+ src/codegen/platform_console.h src/codegen/emit.h src/codegen/win_call.h \
3+ src/pe/pe_writer.h src/codegen/runtime_layout.h \
4+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
5+ src/pe/pe_imports.def src/codegen/win_file_call.h src/parser/vars.h \
6+ src/target.h
7+src/codegen/platform_console.h:
8+src/codegen/emit.h:
9+src/codegen/win_call.h:
10+src/pe/pe_writer.h:
11+src/codegen/runtime_layout.h:
12+src/codegen/runtime_import_enum.h:
13+src/codegen/runtime_exports.def:
14+src/pe/pe_imports.def:
15+src/codegen/win_file_call.h:
16+src/parser/vars.h:
17+src/target.h:

kiln/src/codegen/platform_console.hadded

+33−0
@@ -0,0 +1,33 @@
1+#ifndef KILN_PLATFORM_CONSOLE_H
2+#define KILN_PLATFORM_CONSOLE_H
3+#include "codegen/emit.h"
4+
5+// Console I/O, dispatching on kiln_get_target() internally so call sites
6+// (codegen/print_int.c, codegen/errors.c, codegen/input_builtin.c) don't
7+// need their own target branch. Linux writes/reads a fixed fd (1/2/0)
8+// directly via raw syscall; Windows has no such fixed handle -- it has to
9+// call GetStdHandle first, so each of these costs one extra call there.
10+//
11+// platform_emit_write_stdout/stderr: RSI=buf, RDX=len in; no output.
12+// platform_emit_read_stdin_byte: RSI=1-byte buf, RDX=1 in; RAX=bytes
13+// actually read (0 or 1) out, matching Linux read(2)'s own return
14+// convention so input_builtin.c's existing "did we get a byte" check
15+// (cmp rax,1) works unchanged on both targets.
16+void platform_emit_write_stdout(CodeBuf *code);
17+void platform_emit_write_stderr(CodeBuf *code);
18+void platform_emit_read_stdin_byte(CodeBuf *code);
19+
20+// Split out of platform_emit_read_stdin_byte for callers that read many
21+// bytes in a loop (codegen/input_builtin.c): looking the handle up once
22+// outside the loop instead of once per byte cuts a multi-byte line's
23+// worth of GetStdHandle round-trips down to one, which matters on
24+// Windows -- unlike Linux's fixed fd 0, nothing here is free.
25+// platform_emit_get_stdin_handle: out RAX=handle (Windows only -- no-op,
26+// nothing to call, on Linux). platform_emit_read_stdin_byte_h: in
27+// RAX=handle (as returned above; ignored on Linux), RSI=1-byte buf,
28+// RDX=1; out RAX=bytes read (0 or 1), same convention as
29+// platform_emit_read_stdin_byte.
30+void platform_emit_get_stdin_handle(CodeBuf *code);
31+void platform_emit_read_stdin_byte_h(CodeBuf *code);
32+
33+#endif

kiln/src/codegen/platform_file.cadded

+110−0
@@ -0,0 +1,110 @@
1+#include "codegen/platform_file.h"
2+#include "codegen/win_call.h"
3+#include "codegen/win_file_call.h"
4+#include "target.h"
5+
6+#define GENERIC_READ 0x80000000u
7+#define GENERIC_WRITE 0x40000000u
8+#define FILE_APPEND_DATA 0x00000004u // access-right-only open => every write auto-goes to EOF, no seek needed
9+#define FILE_SHARE_READ 1u
10+#define OPEN_EXISTING 3u
11+#define OPEN_ALWAYS 4u
12+#define CREATE_ALWAYS 2u
13+#define FILE_ATTRIBUTE_NORMAL 0x80u
14+
15+static void create_file(CodeBuf *code, uint64_t access, uint64_t disposition) {
16+ win_call_begin(code, 3);
17+ win_call_arg_reg(code, 0, REG_RDI); // path
18+ win_call_arg_imm64(code, 1, access);
19+ win_call_arg_imm64(code, 2, FILE_SHARE_READ);
20+ win_call_arg_imm64(code, 3, 0); // lpSecurityAttributes = NULL
21+ win_call_stack_arg_imm64(code, 0, disposition);
22+ win_call_stack_arg_imm64(code, 1, FILE_ATTRIBUTE_NORMAL);
23+ win_call_stack_arg_imm64(code, 2, 0); // hTemplateFile = NULL
24+ win_call_import(code, PE_IMPORT_CREATE_FILE_A);
25+ win_call_end(code);
26+}
27+
28+void platform_emit_open_read(CodeBuf *code) {
29+ if (kiln_get_target() == KILN_TARGET_WINDOWS) { create_file(code, GENERIC_READ, OPEN_EXISTING); return; }
30+ emit_mov_reg_imm64(code, REG_RSI, 0); // O_RDONLY
31+ emit_mov_reg_imm64(code, REG_RDX, 0);
32+ emit_mov_reg_imm64(code, REG_RAX, 2); // syscall: open
33+ emit_syscall(code);
34+}
35+
36+void platform_emit_open_write(CodeBuf *code, int append) {
37+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
38+ create_file(code, append ? FILE_APPEND_DATA : GENERIC_WRITE, append ? OPEN_ALWAYS : CREATE_ALWAYS);
39+ return;
40+ }
41+ // O_WRONLY|O_CREAT|O_TRUNC = 577; O_WRONLY|O_CREAT|O_APPEND = 1089
42+ // (octal flag bits from Linux's fcntl.h).
43+ emit_mov_reg_imm64(code, REG_RSI, (uint64_t)(append ? 1089 : 577));
44+ emit_mov_reg_imm64(code, REG_RDX, 420); // mode 0644
45+ emit_mov_reg_imm64(code, REG_RAX, 2); // syscall: open
46+ emit_syscall(code);
47+}
48+
49+void platform_emit_close(CodeBuf *code) {
50+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
51+ win_call_begin(code, 0);
52+ win_call_arg_reg(code, 0, REG_RDI);
53+ win_call_import(code, PE_IMPORT_CLOSE_HANDLE);
54+ win_call_end(code);
55+ return;
56+ }
57+ emit_mov_reg_imm64(code, REG_RAX, 3); // syscall: close
58+ emit_syscall(code);
59+}
60+
61+void platform_emit_read_bytes(CodeBuf *code) {
62+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
63+ emit_mov_reg_reg(code, REG_RAX, REG_RDI); // handle
64+ emit_mov_reg_reg(code, REG_RDI, REG_RDX); // len
65+ win_file_call(code, PE_IMPORT_READ_FILE); // RAX=handle, RSI=buf (unchanged), RDI=len
66+ return;
67+ }
68+ emit_mov_reg_imm64(code, REG_RAX, 0); // syscall: read
69+ emit_syscall(code);
70+}
71+
72+void platform_emit_write_bytes(CodeBuf *code) {
73+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
74+ emit_mov_reg_reg(code, REG_RAX, REG_RDI); // handle
75+ emit_mov_reg_reg(code, REG_RDI, REG_RDX); // len
76+ win_file_call(code, PE_IMPORT_WRITE_FILE);
77+ return;
78+ }
79+ emit_mov_reg_imm64(code, REG_RAX, 1); // syscall: write
80+ emit_syscall(code);
81+}
82+
83+// Contract on both targets: returns the size in RAX, leaves the file's
84+// read position untouched. GetFileSize gets this for free; Linux's lseek
85+// doesn't have a "size" query, only "seek and report new position", so
86+// the Linux path pairs a seek-to-end with a seek back to the start it
87+// found the fd at (offset 0, since this is only ever called right after
88+// platform_emit_open_read).
89+void platform_emit_file_size(CodeBuf *code) {
90+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
91+ win_call_begin(code, 0);
92+ win_call_arg_reg(code, 0, REG_RDI);
93+ win_call_arg_imm64(code, 1, 0); // lpFileSizeHigh = NULL (files under 4GB only)
94+ win_call_import(code, PE_IMPORT_GET_FILE_SIZE);
95+ win_call_end(code);
96+ return; // GetFileSize's DWORD result lands in EAX, which -- per
97+ // standard x86-64 semantics and every real-world Windows
98+ // DLL's own codegen -- already zero-extends into RAX.
99+ }
100+ emit_mov_reg_imm64(code, REG_RSI, 0);
101+ emit_mov_reg_imm64(code, REG_RDX, 2); // SEEK_END
102+ emit_mov_reg_imm64(code, REG_RAX, 8); // syscall: lseek
103+ emit_syscall(code);
104+ emit_push_reg(code, REG_RAX); // save size -- the rewind below clobbers RAX
105+ emit_mov_reg_imm64(code, REG_RSI, 0);
106+ emit_mov_reg_imm64(code, REG_RDX, 0); // SEEK_SET
107+ emit_mov_reg_imm64(code, REG_RAX, 8);
108+ emit_syscall(code); // rewind (syscall preserves RDI, the fd survives for the caller's next use)
109+ emit_pop_reg(code, REG_RAX);
110+}

kiln/src/codegen/platform_file.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/codegen/platform_file.o: src/codegen/platform_file.c \
2+ src/codegen/platform_file.h src/codegen/emit.h src/codegen/win_call.h \
3+ src/pe/pe_writer.h src/codegen/runtime_layout.h \
4+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
5+ src/pe/pe_imports.def src/codegen/win_file_call.h src/target.h
6+src/codegen/platform_file.h:
7+src/codegen/emit.h:
8+src/codegen/win_call.h:
9+src/pe/pe_writer.h:
10+src/codegen/runtime_layout.h:
11+src/codegen/runtime_import_enum.h:
12+src/codegen/runtime_exports.def:
13+src/pe/pe_imports.def:
14+src/codegen/win_file_call.h:
15+src/target.h:

kiln/src/codegen/platform_file.hadded

+24−0
@@ -0,0 +1,24 @@
1+#ifndef KILN_PLATFORM_FILE_H
2+#define KILN_PLATFORM_FILE_H
3+#include "codegen/emit.h"
4+
5+// Real file I/O, dispatching on kiln_get_target() internally so call
6+// sites (codegen/read_file_builtin.c, codegen/write_file_builtins.c)
7+// don't need their own target branch. The fd/HANDLE kiln gets back is
8+// used the same way on both targets: a signed value, negative means
9+// "open failed" (Linux's -1 and Windows' INVALID_HANDLE_VALUE, which is
10+// also -1, both fail the exact same `cmp rax,0; jge ok` check already in
11+// use -- no call-site change needed there).
12+void platform_emit_open_read(CodeBuf *code); // in: RDI=nullterm path -> out: RAX=fd/handle
13+void platform_emit_open_write(CodeBuf *code, int append); // in: RDI=nullterm path -> out: RAX=fd/handle
14+void platform_emit_close(CodeBuf *code); // in: RDI=fd/handle
15+void platform_emit_read_bytes(CodeBuf *code); // in: RDI=fd/handle, RSI=buf, RDX=len
16+void platform_emit_write_bytes(CodeBuf *code); // in: RDI=fd/handle, RSI=buf, RDX=len
17+// Replaces the lseek(END)+lseek(SET) dance read_file_builtin.c used on
18+// Linux: Windows has a direct GetFileSize call, no seek round-trip
19+// needed. Files over 4GB aren't supported (matches this project's
20+// existing "don't over-engineer for what can't practically happen" scope
21+// limit already documented elsewhere for file I/O).
22+void platform_emit_file_size(CodeBuf *code); // in: RDI=fd/handle -> out: RAX=size
23+
24+#endif

kiln/src/codegen/print_int.cadded

+238−0
@@ -0,0 +1,238 @@
1+#include <string.h>
2+#include "codegen/print_int.h"
3+#include "codegen/emit_internal.h"
4+#include "codegen/emit_sse.h"
5+#include "codegen/value.h"
6+#include "codegen/layout.h"
7+#include "codegen/win_call.h"
8+#include "codegen/platform_console.h"
9+#include "elf/elf_dynamic_call.h"
10+#include "target.h"
11+
12+// Loads a compile-time-known double literal into an xmm register, via a
13+// GP register holding its raw IEEE-754 bit pattern -- same technique
14+// codegen/expr.c uses for user-written number literals (there's no "mov
15+// xmm, imm64" instruction).
16+static void load_double_const(CodeBuf *code, XReg dst, double v) {
17+ uint64_t bits;
18+ memcpy(&bits, &v, sizeof(bits));
19+ emit_mov_reg_imm64(code, REG_RAX, bits);
20+ emit_movq_xmm_from_reg(code, dst, REG_RAX);
21+}
22+
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 decrements
26+// 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 FORWARD
29+// dynamic '\n', placed right after whichever of the above
30+// actually got used, so the whole thing writes in ONE
31+// contiguous write(2) call regardless of whether this
32+// number turned out to be whole or fractional.
33+// RBX holds the sign flag through phase 3, then gets reused as frac_len
34+// (how many of the 6 fraction digits are significant, i.e. up to the
35+// last nonzero one) -- safe, sign has already been written to the
36+// buffer by the time phase 4 starts.
37+// Dispatches on the popped value's tag: a STRING just gets written
38+// 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 an
41+// already-popped payload (in RAX) instead of popping XMM0 itself.
42+//
43+// Plan B, phase B1: emitted once (jumped over), called via emit_call_back
44+// from codegen_print_top below -- this is the single biggest deduplication
45+// win of B1, since this routine used to be re-emitted in full at EVERY
46+// 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 return
49+// address on top of whatever the caller already pushed, so a `pop` here
50+// would grab the return address instead of the caller's value -- same
51+// register-based-argument reasoning codegen/heap.c's alloc routine
52+// already uses, not something print specifically needed before this
53+// 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 real
55+// 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);
63+
64+ // ---- STRING path: [payload-8] = length, [payload..] = bytes ----
65+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length
66+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // buf
67+ platform_emit_write_stdout(code);
68+ // newline, as its own tiny write (the string's bytes aren't
69+ // guaranteed to be followed by one in memory)
70+ emit_sub_reg_imm8(code, REG_RSP, 8);
71+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
72+ emit_store_byte_imm(code, REG_RCX, '\n');
73+ emit_mov_reg_reg(code, REG_RSI, REG_RCX);
74+ emit_mov_reg_imm64(code, REG_RDX, 1);
75+ platform_emit_write_stdout(code);
76+ emit_add_reg_imm8(code, REG_RSP, 8);
77+ int done = emit_jmp_rel32(code);
78+ emit_patch_jump(code, is_number);
79+
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, whose
89+ // load address ld.so chooses at runtime. libkilnrt.dll (Windows)
90+ // is fixed-base like everything else kiln emits (see
91+ // pe/pe_dll_writer.h), so kiln_code_base() already resolves
92+ // correctly there without RIP-relative addressing -- it now
93+ // knows about DLL compilation specifically (see codegen/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);
105+
106+ // ---- NUMBER path ----
107+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX); // payload bits -> double
108+ emit_sub_reg_imm8(code, REG_RSP, 40);
109+
110+ // ---- sign ----
111+ emit_pxor_xmm_xmm(code, XMM2); // XMM2 = +0.0, used as the comparison zero
112+ emit_ucomisd(code, XMM0, XMM2);
113+ emit_mov_reg_imm64(code, REG_RBX, 0);
114+ int skip_negate = emit_jcc_rel32(code, COND_AE); // XMM0 >= 0 -> not negative
115+ emit_pxor_xmm_xmm(code, XMM2);
116+ emit_subsd(code, XMM2, XMM0); // XMM2 = 0.0 - XMM0 = -XMM0
117+ emit_movsd_xmm_xmm(code, XMM0, XMM2);
118+ emit_mov_reg_imm64(code, REG_RBX, 1);
119+ emit_patch_jump(code, skip_negate);
120+
121+ // ---- split into integer part (RAX) and fractional remainder (XMM0, now in [0,1)) ----
122+ emit_cvttsd2si(code, REG_RAX, XMM0);
123+ emit_cvtsi2sd(code, XMM1, REG_RAX);
124+ emit_subsd(code, XMM0, XMM1);
125+
126+ // ---- integer digits, backward, same loop shape as milestone 1 ----
127+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
128+ emit_add_reg_imm8(code, REG_RCX, 32);
129+ int loop_start = code->count;
130+ emit_dec_reg(code, REG_RCX);
131+ emit_cqo(code);
132+ emit_mov_reg_imm64(code, REG_RSI, 10);
133+ emit_idiv_reg(code, REG_RSI);
134+ emit_add_reg_imm8(code, REG_RDX, '0');
135+ emit_store_byte_reg(code, REG_RCX, REG_RDX);
136+ emit_cmp_reg_imm32(code, REG_RAX, 0);
137+ emit_jcc_back(code, COND_NE, loop_start);
138+
139+ // ---- sign prepend ----
140+ emit_cmp_reg_imm32(code, REG_RBX, 0);
141+ int skip_sign = emit_jcc_rel32(code, COND_E);
142+ emit_dec_reg(code, REG_RCX);
143+ emit_store_byte_imm(code, REG_RCX, '-');
144+ emit_patch_jump(code, skip_sign);
145+
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) routinely
150+ // lands a hair below the "true" decimal value (0.7999999999999999
151+ // instead of 0.8) due to ordinary double rounding, and truncating
152+ // that directly would print "0.799999" instead of "0.8". Adding half
153+ // a unit-in-the-last-place first is the standard fix; it's too small
154+ // to introduce a spurious digit for values that are already exact
155+ // (see the loop comment below).
156+ load_double_const(code, XMM1, 0.0000005);
157+ emit_addsd(code, XMM0, XMM1);
158+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
159+ emit_add_reg_imm8(code, REG_RSI, 33);
160+ load_double_const(code, XMM3, 10.0);
161+ emit_mov_reg_imm64(code, REG_RBX, 0); // repurposed: frac_len (sign already applied above)
162+ for (int i = 0; i < 6; i++) {
163+ emit_mulsd(code, XMM0, XMM3);
164+ emit_cvttsd2si(code, REG_RAX, XMM0); // this step's digit, 0-9
165+ emit_cvtsi2sd(code, XMM1, REG_RAX);
166+ emit_subsd(code, XMM0, XMM1); // remainder for the next digit
167+ emit_cmp_reg_imm32(code, REG_RAX, 0);
168+ int skip_mark = emit_jcc_rel32(code, COND_E);
169+ emit_mov_reg_imm64(code, REG_RBX, (uint64_t)(i + 1)); // "kept up through here"
170+ emit_patch_jump(code, skip_mark);
171+ emit_add_reg_imm8(code, REG_RAX, '0');
172+ emit_store_byte_reg(code, REG_RSI, REG_RAX);
173+ emit_add_reg_imm8(code, REG_RSI, 1);
174+ }
175+
176+ // ---- decide '.'/newline placement, then a single write(2) ----
177+ emit_mov_reg_reg(code, REG_RDX, REG_RSP);
178+ emit_add_reg_imm8(code, REG_RDX, 32);
179+ emit_cmp_reg_imm32(code, REG_RBX, 0);
180+ int whole_number = emit_jcc_rel32(code, COND_E);
181+ emit_store_byte_imm(code, REG_RDX, '.');
182+ emit_mov_reg_reg(code, REG_RDX, REG_RSP);
183+ emit_add_reg_imm8(code, REG_RDX, 33);
184+ emit_add_reg_reg(code, REG_RDX, REG_RBX); // + frac_len
185+ int after_dot = emit_jmp_rel32(code);
186+ emit_patch_jump(code, whole_number);
187+ // RDX already points at rsp+32, the correct newline spot when there's no fraction
188+ emit_patch_jump(code, after_dot);
189+ emit_store_byte_imm(code, REG_RDX, '\n');
190+
191+ emit_mov_reg_reg(code, REG_RSI, REG_RCX); // buf start
192+ emit_add_reg_imm8(code, REG_RDX, 1);
193+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // length = (newline_pos + 1) - buf_start
194+ platform_emit_write_stdout(code);
195+
196+ emit_add_reg_imm8(code, REG_RSP, 40); // restore stack for the next statement
197+ emit_patch_jump(code, done);
198+ emit_patch_jump(code, done_nil);
199+ emit_ret(code);
200+ emit_patch_jump(code, routine_skip);
201+}
202+
203+void print_emit_top_routine_only(CodeBuf *code) {
204+ emit_print_routine(code);
205+}
206+
207+int print_top_routine_offset(void) { return print_routine_offset; }
208+
209+static int is_shared(void) {
210+ return kiln_get_link_mode() == KILN_LINK_SHARED && !kiln_is_compiling_so();
211+}
212+
213+void print_emit_startup(CodeBuf *code) {
214+ if (!is_shared()) emit_print_routine(code);
215+}
216+
217+void codegen_print_top(CodeBuf *code) {
218+ emit_pop_reg(code, REG_RAX); // payload
219+ emit_pop_reg(code, REG_RBX); // tag
220+ if (is_shared()) {
221+ if (kiln_get_target() == KILN_TARGET_WINDOWS) win_call_runtime_import(code, RUNTIME_IMPORT_PRINT_TOP);
222+ else elf_dynamic_call(code, RUNTIME_IMPORT_PRINT_TOP);
223+ return;
224+ }
225+ emit_call_back(code, print_routine_offset);
226+}
227+
228+void codegen_exit0(CodeBuf *code) {
229+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
230+ win_call_begin(code, 0);
231+ win_call_arg_imm64(code, 0, 0); // ExitProcess(0)
232+ win_call_import(code, PE_IMPORT_EXIT_PROCESS);
233+ return; // never returns -- no win_call_end needed
234+ }
235+ emit_mov_reg_imm64(code, REG_RDI, 0);
236+ emit_mov_reg_imm64(code, REG_RAX, 60); // syscall: exit
237+ emit_syscall(code);
238+}

kiln/src/codegen/print_int.dadded

+25−0
@@ -0,0 +1,25 @@
1+src/codegen/print_int.o: src/codegen/print_int.c src/codegen/print_int.h \
2+ src/codegen/emit.h src/codegen/emit_internal.h src/codegen/emit_sse.h \
3+ src/codegen/value.h src/codegen/layout.h src/codegen/win_call.h \
4+ src/pe/pe_writer.h src/codegen/runtime_layout.h \
5+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
6+ src/pe/pe_imports.def src/codegen/platform_console.h \
7+ src/elf/elf_dynamic_call.h src/elf/elf_dynamic.h src/elf/elf_writer.h \
8+ src/target.h
9+src/codegen/print_int.h:
10+src/codegen/emit.h:
11+src/codegen/emit_internal.h:
12+src/codegen/emit_sse.h:
13+src/codegen/value.h:
14+src/codegen/layout.h:
15+src/codegen/win_call.h:
16+src/pe/pe_writer.h:
17+src/codegen/runtime_layout.h:
18+src/codegen/runtime_import_enum.h:
19+src/codegen/runtime_exports.def:
20+src/pe/pe_imports.def:
21+src/codegen/platform_console.h:
22+src/elf/elf_dynamic_call.h:
23+src/elf/elf_dynamic.h:
24+src/elf/elf_writer.h:
25+src/target.h:

kiln/src/codegen/print_int.hadded

+28−0
@@ -0,0 +1,28 @@
1+#ifndef KILN_CODEGEN_PRINT_INT_H
2+#define KILN_CODEGEN_PRINT_INT_H
3+#include "codegen/emit.h"
4+
5+// Emitted once, at program start (see parser.c's compile_program) --
6+// must run before any call to codegen_print_top below.
7+void print_emit_startup(CodeBuf *code);
8+
9+// Plan B: emits ONLY the print routine's body -- used by the standalone
10+// "compile the runtime" driver that builds libkilnrt.so.
11+void print_emit_top_routine_only(CodeBuf *code);
12+int print_top_routine_offset(void);
13+
14+// Pops the top-of-stack expression result (a double, milestone 4) into
15+// XMM0, converts it to decimal ASCII (handles negative values and a
16+// fractional part, trimming trailing fraction zeros so whole numbers
17+// print with no decimal point -- matching ashvm's dual %lld/%g print
18+// format), and writes it + a trailing newline to stdout via a single raw
19+// write(2) syscall. No libc, no external symbols. Plan B, phase B1: a
20+// real CALL to one shared copy (emitted by print_emit_startup) rather
21+// than re-emitting this whole routine inline at every print statement.
22+void codegen_print_top(CodeBuf *code);
23+
24+// mov rdi, 0; mov rax, 60 (exit); syscall -- always the last instruction
25+// in the generated program.
26+void codegen_exit0(CodeBuf *code);
27+
28+#endif

kiln/src/codegen/read_file_builtin.cadded

+53−0
@@ -0,0 +1,53 @@
1+#include <stdint.h>
2+#include "codegen/file_builtins.h"
3+#include "codegen/file_path.h"
4+#include "codegen/string_alloc.h"
5+#include "codegen/errors.h"
6+#include "codegen/value.h"
7+#include "codegen/platform_file.h"
8+#include "parser/parser.h"
9+#include "parser/vars.h"
10+
11+// A single read() call is assumed to fill the whole file in one go (matches
12+// ashvm's own single fread call, not a retry loop) -- reasonable for
13+// regular files, not a guarantee POSIX makes for every file type.
14+void codegen_builtin_read_file(void) {
15+ emit_pop_reg(code, REG_RSI); // path payload
16+ emit_pop_reg(code, REG_RBX); // tag (ignored, assumed STRING)
17+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length
18+ filepath_emit_nullterm(code); // RAX = null-terminated path buffer
19+
20+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
21+ platform_emit_open_read(code);
22+
23+ emit_cmp_reg_imm32(code, REG_RAX, 0);
24+ int ok = emit_jcc_rel32(code, COND_GE);
25+ errors_emit_die(code, "runtime error: could not open file");
26+ emit_patch_jump(code, ok);
27+
28+ int32_t base = higher_order_scratch_offset();
29+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RAX); // fd
30+
31+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
32+ platform_emit_file_size(code);
33+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX); // size
34+
35+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // size
36+ string_alloc_emit_prefixed(code); // RAX = block, length prefix already written
37+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);
38+
39+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 0); // fd
40+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);
41+ emit_add_reg_imm8(code, REG_RSI, 8); // buf = block+8
42+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // size
43+ platform_emit_read_bytes(code);
44+
45+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 0); // fd
46+ platform_emit_close(code);
47+
48+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 16);
49+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
50+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
51+ emit_push_reg(code, REG_RBX);
52+ emit_push_reg(code, REG_RAX);
53+}

kiln/src/codegen/read_file_builtin.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/codegen/read_file_builtin.o: src/codegen/read_file_builtin.c \
2+ src/codegen/file_builtins.h src/codegen/file_path.h src/codegen/emit.h \
3+ src/codegen/string_alloc.h src/codegen/errors.h src/codegen/value.h \
4+ src/codegen/platform_file.h src/parser/parser.h src/lexer/lexer.h \
5+ src/parser/vars.h
6+src/codegen/file_builtins.h:
7+src/codegen/file_path.h:
8+src/codegen/emit.h:
9+src/codegen/string_alloc.h:
10+src/codegen/errors.h:
11+src/codegen/value.h:
12+src/codegen/platform_file.h:
13+src/parser/parser.h:
14+src/lexer/lexer.h:
15+src/parser/vars.h:

kiln/src/codegen/reduce_builtin.cadded

+81−0
@@ -0,0 +1,81 @@
1+#include "codegen/higher_order.h"
2+#include "codegen/expr.h"
3+#include "codegen/closures.h"
4+#include "codegen/emit.h"
5+#include "parser/parser.h"
6+#include "parser/vars.h"
7+
8+// Scratch layout: [0]=acc_tag [8]=acc_payload [16]=fn_tag [24]=fn_payload
9+// [32]=data_ptr [40]=count [48]=i
10+void codegen_builtin_reduce(void) {
11+ codegen_expression(); // array
12+ expect(TOKEN_COMMA, "expected ',' after array argument");
13+ codegen_expression(); // fn
14+ expect(TOKEN_COMMA, "expected ',' after fn argument");
15+ codegen_expression(); // initial
16+ expect(TOKEN_RPAREN, "expected ')' after arguments");
17+
18+ int32_t base = higher_order_scratch_offset();
19+
20+ emit_pop_reg(code, REG_RAX); // initial payload
21+ emit_pop_reg(code, REG_RBX); // initial tag
22+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RBX);
23+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX);
24+
25+ emit_pop_reg(code, REG_RAX); // fn payload
26+ emit_pop_reg(code, REG_RBX); // fn tag
27+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RBX);
28+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RAX);
29+
30+ emit_pop_reg(code, REG_RSI); // array object
31+ emit_pop_reg(code, REG_RCX); // array tag (ignored)
32+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count
33+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr
34+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RDX);
35+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX);
36+
37+ emit_mov_reg_imm64(code, REG_RCX, 0);
38+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RCX); // i = 0
39+
40+ int loop_start = code->count;
41+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 48);
42+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40);
43+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
44+ int loop_exit = emit_jcc_rel32(code, COND_GE);
45+
46+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32); // data_ptr
47+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);
48+ emit_mov_reg_imm64(code, REG_RDX, 16);
49+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);
50+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // element address
51+
52+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 16); // fn_tag
53+ emit_push_reg(code, REG_RAX);
54+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 24); // fn_payload
55+ emit_push_reg(code, REG_RAX);
56+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 0); // acc_tag (arg0)
57+ emit_push_reg(code, REG_RAX);
58+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // acc_payload
59+ emit_push_reg(code, REG_RAX);
60+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // element tag (arg1)
61+ emit_push_reg(code, REG_RAX);
62+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload
63+ emit_push_reg(code, REG_RAX);
64+ codegen_call_closure_value(2);
65+
66+ emit_pop_reg(code, REG_RAX); // result payload
67+ emit_pop_reg(code, REG_RBX); // result tag
68+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RBX); // acc = result
69+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX);
70+
71+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 48);
72+ emit_add_reg_imm8(code, REG_RCX, 1);
73+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RCX); // i++
74+ emit_jmp_back(code, loop_start);
75+ emit_patch_jump(code, loop_exit);
76+
77+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0); // acc_tag
78+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // acc_payload
79+ emit_push_reg(code, REG_RBX);
80+ emit_push_reg(code, REG_RAX);
81+}

kiln/src/codegen/reduce_builtin.dadded

+12−0
@@ -0,0 +1,12 @@
1+src/codegen/reduce_builtin.o: src/codegen/reduce_builtin.c \
2+ src/codegen/higher_order.h src/codegen/expr.h src/lexer/lexer.h \
3+ src/codegen/closures.h src/parser/functions.h src/codegen/emit.h \
4+ src/parser/parser.h src/parser/vars.h
5+src/codegen/higher_order.h:
6+src/codegen/expr.h:
7+src/lexer/lexer.h:
8+src/codegen/closures.h:
9+src/parser/functions.h:
10+src/codegen/emit.h:
11+src/parser/parser.h:
12+src/parser/vars.h:

kiln/src/codegen/replace_builtin.cadded

+217−0
@@ -0,0 +1,217 @@
1+#include <stdint.h>
2+#include "codegen/string_builtins.h"
3+#include "codegen/string_alloc.h"
4+#include "codegen/expr.h"
5+#include "codegen/bytes.h"
6+#include "codegen/emit.h"
7+#include "codegen/value.h"
8+#include "parser/parser.h"
9+#include "parser/vars.h"
10+
11+// Scratch layout: [0]=s_ptr [8]=s_len [16]=search_ptr [24]=search_len
12+// [32]=cur [40]=total_len/occurrences [48]=out_block [56]=cursor
13+// [64]=find_p [72]=find_last_start [80]=find_result
14+// [88]=replacement_ptr [96]=replacement_len
15+
16+// Same shape as split_builtin.c's helper of the same name (own copy --
17+// tight enough coupling to this file's scratch layout that sharing it
18+// wouldn't simplify anything). Searches for [search_ptr,search_len]
19+// within s starting at [cur]; writes the match position or -1 to
20+// [base+80].
21+static void emit_find_delim(int32_t base) {
22+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8);
23+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
24+ emit_sub_reg_reg(code, REG_RAX, REG_RDX);
25+ emit_store_mem_disp32(code, REG_RBP, base + 72, REG_RAX); // last_start
26+
27+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 32);
28+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RBX); // p = cur
29+
30+ int outer = code->count;
31+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 64);
32+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 72);
33+ emit_cmp_reg_reg(code, REG_RBX, REG_RAX);
34+ int no_match = emit_jcc_rel32(code, COND_GT);
35+
36+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
37+ emit_add_reg_reg(code, REG_RSI, REG_RBX);
38+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 16);
39+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
40+
41+ int inner = code->count;
42+ emit_cmp_reg_imm32(code, REG_RDX, 0);
43+ int found = emit_jcc_rel32(code, COND_E);
44+ emit_load_byte_reg(code, REG_RDI, REG_RSI);
45+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
46+ emit_cmp_reg_reg(code, REG_RDI, REG_RAX);
47+ int mismatch = emit_jcc_rel32(code, COND_NE);
48+ emit_add_reg_imm8(code, REG_RSI, 1);
49+ emit_add_reg_imm8(code, REG_RCX, 1);
50+ emit_dec_reg(code, REG_RDX);
51+ emit_jmp_back(code, inner);
52+
53+ emit_patch_jump(code, mismatch);
54+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 64);
55+ emit_add_reg_imm8(code, REG_RBX, 1);
56+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RBX);
57+ emit_jmp_back(code, outer);
58+
59+ emit_patch_jump(code, found);
60+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 64);
61+ emit_store_mem_disp32(code, REG_RBP, base + 80, REG_RAX);
62+ int done = emit_jmp_rel32(code);
63+
64+ emit_patch_jump(code, no_match);
65+ emit_mov_reg_imm64(code, REG_RAX, (uint64_t)(int64_t)-1);
66+ emit_store_mem_disp32(code, REG_RBP, base + 80, REG_RAX);
67+
68+ emit_patch_jump(code, done);
69+}
70+
71+void codegen_builtin_replace(void) {
72+ codegen_expression(); // s
73+ expect(TOKEN_COMMA, "expected ',' after string argument");
74+ codegen_expression(); // search
75+ expect(TOKEN_COMMA, "expected ',' after search argument");
76+ codegen_expression(); // replacement
77+ expect(TOKEN_RPAREN, "expected ')' after arguments");
78+
79+ emit_pop_reg(code, REG_RSI); // replacement payload
80+ emit_pop_reg(code, REG_RBX); // ignored
81+ int32_t base = higher_order_scratch_offset();
82+ emit_store_mem_disp32(code, REG_RBP, base + 88, REG_RSI);
83+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8);
84+ emit_store_mem_disp32(code, REG_RBP, base + 96, REG_RDX); // replacement_len
85+
86+ emit_pop_reg(code, REG_RCX); // search payload
87+ emit_pop_reg(code, REG_RBX); // ignored
88+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RCX);
89+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, -8);
90+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RDX); // search_len
91+
92+ emit_pop_reg(code, REG_RSI); // s payload
93+ emit_pop_reg(code, REG_RBX); // ignored
94+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RSI);
95+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8);
96+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX); // s_len
97+
98+ // ---- search is empty: return s unchanged (matches ashvm) ----
99+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len
100+ emit_cmp_reg_imm32(code, REG_RDX, 0);
101+ int not_empty_search = emit_jcc_rel32(code, COND_NE);
102+
103+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // s_len
104+ string_alloc_emit_prefixed(code); // RAX = block
105+ emit_mov_reg_reg(code, REG_RSI, REG_RAX);
106+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
107+ emit_add_reg_imm8(code, REG_RDI, 8);
108+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0); // src = s_ptr
109+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // len = s_len
110+ bytes_emit_copy(code);
111+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
112+ emit_add_reg_imm8(code, REG_RAX, 8);
113+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
114+ emit_push_reg(code, REG_RBX);
115+ emit_push_reg(code, REG_RAX);
116+
117+ int skip_general = emit_jmp_rel32(code);
118+ emit_patch_jump(code, not_empty_search);
119+
120+ // ---- pass 1: count occurrences, compute total output length ----
121+ emit_mov_reg_imm64(code, REG_RAX, 0);
122+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = 0
123+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // occurrences = 0
124+
125+ int p1 = code->count;
126+ emit_find_delim(base);
127+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 80);
128+ emit_cmp_reg_imm32(code, REG_RAX, -1);
129+ int p1_done = emit_jcc_rel32(code, COND_E);
130+
131+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
132+ emit_add_reg_imm8(code, REG_RCX, 1);
133+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // occurrences++
134+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len
135+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
136+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = match + search_len
137+ emit_jmp_back(code, p1);
138+ emit_patch_jump(code, p1_done);
139+
140+ // total_len = s_len + occurrences*(replacement_len - search_len)
141+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 96); // replacement_len
142+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len
143+ emit_sub_reg_reg(code, REG_RAX, REG_RDX); // delta (signed)
144+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // occurrences
145+ emit_imul_reg_reg(code, REG_RAX, REG_RCX); // occurrences * delta
146+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // s_len
147+ emit_add_reg_reg(code, REG_RAX, REG_RDX); // total_len
148+
149+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
150+ string_alloc_emit_prefixed(code); // RAX = block
151+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RAX);
152+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
153+ emit_add_reg_imm8(code, REG_RDX, 8);
154+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RDX); // cursor
155+
156+ // ---- pass 2: copy segments, substituting each match ----
157+ emit_mov_reg_imm64(code, REG_RAX, 0);
158+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = 0
159+
160+ int p2 = code->count;
161+ emit_find_delim(base);
162+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 80);
163+ emit_cmp_reg_imm32(code, REG_RAX, -1);
164+ int p2_done = emit_jcc_rel32(code, COND_E);
165+
166+ // copy s[cur..match) to cursor
167+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32); // cur
168+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
169+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // segment length
170+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0);
171+ emit_add_reg_reg(code, REG_RBX, REG_RCX); // src = s_ptr + cur
172+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 56); // dest = cursor
173+ bytes_emit_copy(code);
174+
175+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 56);
176+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32);
177+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 80); // match
178+ emit_sub_reg_reg(code, REG_RDX, REG_RCX);
179+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
180+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX); // cursor advanced
181+
182+ // copy replacement to cursor
183+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 88); // replacement_ptr
184+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 96); // replacement_len
185+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 56);
186+ bytes_emit_copy(code);
187+
188+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 56);
189+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 96);
190+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
191+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX); // cursor advanced
192+
193+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 80); // match
194+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len
195+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
196+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = match + search_len
197+ emit_jmp_back(code, p2);
198+ emit_patch_jump(code, p2_done);
199+
200+ // trailing remainder s[cur..s_len)
201+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32);
202+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8);
203+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
204+ emit_sub_reg_reg(code, REG_RDX, REG_RCX);
205+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0);
206+ emit_add_reg_reg(code, REG_RBX, REG_RCX);
207+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 56);
208+ bytes_emit_copy(code);
209+
210+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 48); // out block
211+ emit_add_reg_imm8(code, REG_RAX, 8);
212+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
213+ emit_push_reg(code, REG_RBX);
214+ emit_push_reg(code, REG_RAX);
215+
216+ emit_patch_jump(code, skip_general);
217+}

kiln/src/codegen/replace_builtin.dadded

+14−0
@@ -0,0 +1,14 @@
1+src/codegen/replace_builtin.o: src/codegen/replace_builtin.c \
2+ src/codegen/string_builtins.h src/codegen/string_alloc.h \
3+ src/codegen/emit.h src/codegen/expr.h src/lexer/lexer.h \
4+ src/codegen/bytes.h src/codegen/value.h src/parser/parser.h \
5+ src/parser/vars.h
6+src/codegen/string_builtins.h:
7+src/codegen/string_alloc.h:
8+src/codegen/emit.h:
9+src/codegen/expr.h:
10+src/lexer/lexer.h:
11+src/codegen/bytes.h:
12+src/codegen/value.h:
13+src/parser/parser.h:
14+src/parser/vars.h:

kiln/src/codegen/runtime_exports.defadded

+13−0
@@ -0,0 +1,13 @@
1+// X-macro list of every runtime routine that moves out of the binary and
2+// into libkilnrt.so under --link=shared. Single source of truth shared
3+// between the .so writer (which needs to export these by name) and the
4+// executable's codegen (which needs a GOT slot per import) -- same
5+// "adding one means adding one line here, nowhere else" reasoning as
6+// pe/pe_imports.def.
7+//
8+// RUNTIME_EXPORT(enum_suffix, "exported symbol name")
9+RUNTIME_EXPORT(HEAP_ALLOC, "kiln_heap_alloc")
10+RUNTIME_EXPORT(BYTES_COPY, "kiln_bytes_copy")
11+RUNTIME_EXPORT(STRING_ALLOC_PREFIXED, "kiln_string_alloc_prefixed")
12+RUNTIME_EXPORT(PRINT_TOP, "kiln_print_top")
13+RUNTIME_EXPORT(RAISE, "kiln_raise")

kiln/src/codegen/runtime_import_enum.hadded

+16−0
@@ -0,0 +1,16 @@
1+#ifndef KILN_RUNTIME_IMPORT_ENUM_H
2+#define KILN_RUNTIME_IMPORT_ENUM_H
3+
4+// The RuntimeImport enum itself, factored out of elf/elf_dynamic.h so
5+// pe/pe_dll_writer.h can share it too (Windows --link=shared imports the
6+// exact same 5 functions from libkilnrt.dll that Linux imports from
7+// libkilnrt.so) without two conflicting `typedef enum RuntimeImport`
8+// definitions in the same translation unit.
9+typedef enum {
10+#define RUNTIME_EXPORT(name, str) RUNTIME_IMPORT_##name,
11+#include "codegen/runtime_exports.def"
12+#undef RUNTIME_EXPORT
13+ RUNTIME_IMPORT_COUNT_
14+} RuntimeImport;
15+
16+#endif

kiln/src/codegen/runtime_layout.hadded

+14−0
@@ -0,0 +1,14 @@
1+#ifndef KILN_RUNTIME_LAYOUT_H
2+#define KILN_RUNTIME_LAYOUT_H
3+
4+// OS-agnostic sizes for kiln's own hand-rolled try/catch handler stack
5+// (see codegen/errors.c's raise routine, parser/try_catch.c) -- the byte
6+// layout is a kiln-internal invention, not tied to ELF or PE, so both
7+// elf/elf_writer.h and pe/pe_writer.h include this rather than each
8+// re-declaring the same numbers.
9+#define MAX_KILN_TRY_DEPTH 16
10+#define KILN_TRY_HANDLER_SIZE 32
11+#define KILN_TRY_HANDLERS_SIZE (MAX_KILN_TRY_DEPTH * KILN_TRY_HANDLER_SIZE)
12+#define KILN_TRY_DEPTH_SIZE 8
13+
14+#endif

kiln/src/codegen/split_builtin.cadded

+269−0
@@ -0,0 +1,269 @@
1+#include <stdint.h>
2+#include "codegen/string_builtins.h"
3+#include "codegen/string_alloc.h"
4+#include "codegen/expr.h"
5+#include "codegen/bytes.h"
6+#include "codegen/emit.h"
7+#include "codegen/value.h"
8+#include "codegen/heap.h"
9+#include "parser/parser.h"
10+#include "parser/vars.h"
11+
12+// Scratch layout: [0]=s_ptr [8]=s_len [16]=delim_ptr [24]=delim_len
13+// [32]=cur [40]=count [48]=out_data_ptr [56]=out_index
14+// [64]=find_p [72]=find_last_start [80]=find_result (emit_find_delim's own)
15+// [88]=piece_start [96]=piece_len [112]=char byte (per-char branch only)
16+
17+// Scans candidate start positions from [cur] up to s_len-delim_len,
18+// comparing byte-by-byte against delim (same O(n*m) shape as
19+// indexof_builtin.c's search, reimplemented here since it's bounded by a
20+// caller-supplied `cur` rather than always starting at 0). Writes the
21+// match position to [base+80], or -1 if none. Called twice -- once to
22+// count pieces, once to extract them -- so the two passes can never
23+// disagree about where the boundaries are.
24+static void emit_find_delim(int32_t base) {
25+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8);
26+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
27+ emit_sub_reg_reg(code, REG_RAX, REG_RDX);
28+ emit_store_mem_disp32(code, REG_RBP, base + 72, REG_RAX); // last_start
29+
30+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 32); // cur
31+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RBX); // p = cur
32+
33+ int outer = code->count;
34+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 64);
35+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 72);
36+ emit_cmp_reg_reg(code, REG_RBX, REG_RAX);
37+ int no_match = emit_jcc_rel32(code, COND_GT);
38+
39+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
40+ emit_add_reg_reg(code, REG_RSI, REG_RBX);
41+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 16);
42+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
43+
44+ int inner = code->count;
45+ emit_cmp_reg_imm32(code, REG_RDX, 0);
46+ int found = emit_jcc_rel32(code, COND_E);
47+ emit_load_byte_reg(code, REG_RDI, REG_RSI);
48+ emit_load_byte_reg(code, REG_RAX, REG_RCX);
49+ emit_cmp_reg_reg(code, REG_RDI, REG_RAX);
50+ int mismatch = emit_jcc_rel32(code, COND_NE);
51+ emit_add_reg_imm8(code, REG_RSI, 1);
52+ emit_add_reg_imm8(code, REG_RCX, 1);
53+ emit_dec_reg(code, REG_RDX);
54+ emit_jmp_back(code, inner);
55+
56+ emit_patch_jump(code, mismatch);
57+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 64);
58+ emit_add_reg_imm8(code, REG_RBX, 1);
59+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RBX);
60+ emit_jmp_back(code, outer);
61+
62+ emit_patch_jump(code, found);
63+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 64);
64+ emit_store_mem_disp32(code, REG_RBP, base + 80, REG_RAX);
65+ int done = emit_jmp_rel32(code);
66+
67+ emit_patch_jump(code, no_match);
68+ emit_mov_reg_imm64(code, REG_RAX, (uint64_t)(int64_t)-1);
69+ emit_store_mem_disp32(code, REG_RBP, base + 80, REG_RAX);
70+
71+ emit_patch_jump(code, done);
72+}
73+
74+// Copies [base+96] bytes from [base+88] into a fresh heap string, stores
75+// it (STRING, payload) into out_data_ptr[out_index] (base+48/[base+56]),
76+// and increments out_index. Shared by both the delim match-piece and the
77+// final trailing-piece writes in the general (non-empty delim) branch.
78+static void emit_store_piece(int32_t base) {
79+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 96); // len
80+ string_alloc_emit_prefixed(code); // RAX = block
81+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash (bytes_emit_copy preserves RSI)
82+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
83+ emit_add_reg_imm8(code, REG_RDI, 8);
84+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 88); // src
85+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 96); // len
86+ bytes_emit_copy(code);
87+
88+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
89+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
90+
91+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 56); // out_index
92+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 48); // out_data_ptr
93+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);
94+ emit_mov_reg_imm64(code, REG_RDI, 16);
95+ emit_imul_reg_reg(code, REG_RDX, REG_RDI);
96+ emit_add_reg_reg(code, REG_RSI, REG_RDX);
97+ emit_mov_reg_imm64(code, REG_RDI, TAG_STRING);
98+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI);
99+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
100+
101+ emit_add_reg_imm8(code, REG_RCX, 1);
102+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RCX); // out_index++
103+}
104+
105+static void emit_alloc_output_block(int32_t base, Reg count_reg) {
106+ emit_mov_reg_reg(code, REG_RDI, count_reg);
107+ emit_mov_reg_imm64(code, REG_RBX, 16);
108+ emit_imul_reg_reg(code, REG_RDI, REG_RBX);
109+ emit_cmp_reg_imm32(code, REG_RDI, 0);
110+ int nz = emit_jcc_rel32(code, COND_NE);
111+ emit_mov_reg_imm64(code, REG_RDI, 1);
112+ emit_patch_jump(code, nz);
113+ heap_emit_alloc(code); // RAX = block
114+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RAX); // out_data_ptr
115+}
116+
117+void codegen_builtin_split(void) {
118+ codegen_expression(); // s
119+ expect(TOKEN_COMMA, "expected ',' after string argument");
120+ codegen_expression(); // delim
121+ expect(TOKEN_RPAREN, "expected ')' after arguments");
122+
123+ emit_pop_reg(code, REG_RSI); // delim payload
124+ emit_pop_reg(code, REG_RBX); // ignored
125+ emit_pop_reg(code, REG_RCX); // s payload
126+ emit_pop_reg(code, REG_RBX); // ignored
127+
128+ int32_t base = higher_order_scratch_offset();
129+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RCX);
130+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, -8);
131+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RAX); // s_len
132+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RSI);
133+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8);
134+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RDX); // delim_len
135+
136+ emit_cmp_reg_imm32(code, REG_RDX, 0);
137+ int not_char_split = emit_jcc_rel32(code, COND_NE);
138+
139+ // ---- delim is empty: split into individual characters ----
140+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8);
141+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // count = s_len
142+ emit_alloc_output_block(base, REG_RAX);
143+
144+ emit_mov_reg_imm64(code, REG_RCX, 0);
145+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RCX); // i = 0 (reuses out_index slot)
146+
147+ int cloop = code->count;
148+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 56);
149+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40);
150+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
151+ int cexit = emit_jcc_rel32(code, COND_GE);
152+
153+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
154+ emit_add_reg_reg(code, REG_RSI, REG_RCX); // char address
155+ emit_load_byte_reg(code, REG_RBX, REG_RSI);
156+ emit_store_mem_disp32(code, REG_RBP, base + 112, REG_RBX); // stash char byte
157+
158+ emit_mov_reg_imm64(code, REG_RDX, 1);
159+ string_alloc_emit_prefixed(code); // RAX = new 1-byte block
160+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
161+
162+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 112);
163+ emit_store_byte_reg(code, REG_RAX, REG_RBX);
164+
165+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 56);
166+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 48);
167+ emit_mov_reg_reg(code, REG_RDX, REG_RCX);
168+ emit_mov_reg_imm64(code, REG_RDI, 16);
169+ emit_imul_reg_reg(code, REG_RDX, REG_RDI);
170+ emit_add_reg_reg(code, REG_RSI, REG_RDX);
171+ emit_mov_reg_imm64(code, REG_RDI, TAG_STRING);
172+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RDI);
173+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RAX);
174+
175+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 56);
176+ emit_add_reg_imm8(code, REG_RCX, 1);
177+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RCX);
178+ emit_jmp_back(code, cloop);
179+ emit_patch_jump(code, cexit);
180+
181+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // count
182+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
183+ heap_emit_alloc(code); // RAX = array object
184+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 48);
185+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
186+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX);
187+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
188+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
189+ emit_push_reg(code, REG_RBX);
190+ emit_push_reg(code, REG_RAX);
191+
192+ int skip_general = emit_jmp_rel32(code);
193+ emit_patch_jump(code, not_char_split);
194+
195+ // ---- general branch: non-empty delim, two passes ----
196+ emit_mov_reg_imm64(code, REG_RAX, 0);
197+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = 0
198+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // count = 0
199+
200+ int p1 = code->count;
201+ emit_find_delim(base);
202+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 80);
203+ emit_cmp_reg_imm32(code, REG_RAX, -1);
204+ int p1_no_more = emit_jcc_rel32(code, COND_E);
205+
206+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
207+ emit_add_reg_imm8(code, REG_RCX, 1);
208+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // count++
209+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
210+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
211+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = match + delim_len
212+ emit_jmp_back(code, p1);
213+
214+ emit_patch_jump(code, p1_no_more);
215+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40);
216+ emit_add_reg_imm8(code, REG_RCX, 1);
217+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX); // + trailing piece
218+
219+ emit_alloc_output_block(base, REG_RCX);
220+
221+ emit_mov_reg_imm64(code, REG_RAX, 0);
222+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = 0
223+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX); // out_index = 0
224+
225+ int p2 = code->count;
226+ emit_find_delim(base);
227+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 80);
228+ emit_cmp_reg_imm32(code, REG_RAX, -1);
229+ int p2_no_more = emit_jcc_rel32(code, COND_E);
230+
231+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32); // cur
232+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
233+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // piece_len
234+ emit_store_mem_disp32(code, REG_RBP, base + 96, REG_RDX);
235+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
236+ emit_add_reg_reg(code, REG_RSI, REG_RCX); // piece_start
237+ emit_store_mem_disp32(code, REG_RBP, base + 88, REG_RSI);
238+ emit_store_piece(base);
239+
240+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 80); // match
241+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24);
242+ emit_add_reg_reg(code, REG_RAX, REG_RDX);
243+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = match + delim_len
244+ emit_jmp_back(code, p2);
245+
246+ emit_patch_jump(code, p2_no_more);
247+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32); // cur
248+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // s_len
249+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);
250+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // trailing piece_len
251+ emit_store_mem_disp32(code, REG_RBP, base + 96, REG_RDX);
252+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 0);
253+ emit_add_reg_reg(code, REG_RSI, REG_RCX);
254+ emit_store_mem_disp32(code, REG_RBP, base + 88, REG_RSI);
255+ emit_store_piece(base);
256+
257+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // count
258+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);
259+ heap_emit_alloc(code); // RAX = array object
260+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 48);
261+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);
262+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX);
263+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI);
264+ emit_mov_reg_imm64(code, REG_RBX, TAG_ARRAY);
265+ emit_push_reg(code, REG_RBX);
266+ emit_push_reg(code, REG_RAX);
267+
268+ emit_patch_jump(code, skip_general);
269+}

kiln/src/codegen/split_builtin.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/codegen/split_builtin.o: src/codegen/split_builtin.c \
2+ src/codegen/string_builtins.h src/codegen/string_alloc.h \
3+ src/codegen/emit.h src/codegen/expr.h src/lexer/lexer.h \
4+ src/codegen/bytes.h src/codegen/value.h src/codegen/heap.h \
5+ src/parser/parser.h src/parser/vars.h
6+src/codegen/string_builtins.h:
7+src/codegen/string_alloc.h:
8+src/codegen/emit.h:
9+src/codegen/expr.h:
10+src/lexer/lexer.h:
11+src/codegen/bytes.h:
12+src/codegen/value.h:
13+src/codegen/heap.h:
14+src/parser/parser.h:
15+src/parser/vars.h:

kiln/src/codegen/str_builtin.cadded

+121−0
@@ -0,0 +1,121 @@
1+#include <string.h>
2+#include "codegen/convert_builtins.h"
3+#include "codegen/emit.h"
4+#include "codegen/emit_sse.h"
5+#include "codegen/value.h"
6+#include "codegen/heap.h"
7+#include "codegen/bytes.h"
8+#include "parser/parser.h"
9+
10+static void load_double_const(XReg dst, double v) {
11+ uint64_t bits;
12+ memcpy(&bits, &v, sizeof(bits));
13+ emit_mov_reg_imm64(code, REG_RAX, bits);
14+ emit_movq_xmm_from_reg(code, dst, REG_RAX);
15+}
16+
17+// Scratch layout while building digits, same shape as print_int.c:
18+// [rsp, rsp+32) integer digits (+ optional '-'), built backward
19+// rsp+32 '.' (only if there's a fraction)
20+// [rsp+33, rsp+39) up to 6 fraction digits, built forward
21+void codegen_builtin_str(void) {
22+ emit_pop_reg(code, REG_RAX); // payload
23+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
24+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
25+ emit_sub_reg_imm8(code, REG_RSP, 40);
26+
27+ // ---- sign ----
28+ emit_pxor_xmm_xmm(code, XMM2);
29+ emit_ucomisd(code, XMM0, XMM2);
30+ emit_mov_reg_imm64(code, REG_RBX, 0);
31+ int skip_negate = emit_jcc_rel32(code, COND_AE);
32+ emit_pxor_xmm_xmm(code, XMM2);
33+ emit_subsd(code, XMM2, XMM0);
34+ emit_movsd_xmm_xmm(code, XMM0, XMM2);
35+ emit_mov_reg_imm64(code, REG_RBX, 1);
36+ emit_patch_jump(code, skip_negate);
37+
38+ // ---- split into integer part (RAX) and fractional remainder (XMM0) ----
39+ emit_cvttsd2si(code, REG_RAX, XMM0);
40+ emit_cvtsi2sd(code, XMM1, REG_RAX);
41+ emit_subsd(code, XMM0, XMM1);
42+
43+ // ---- integer digits, backward ----
44+ emit_mov_reg_reg(code, REG_RCX, REG_RSP);
45+ emit_add_reg_imm8(code, REG_RCX, 32);
46+ int loop_start = code->count;
47+ emit_dec_reg(code, REG_RCX);
48+ emit_cqo(code);
49+ emit_mov_reg_imm64(code, REG_RSI, 10);
50+ emit_idiv_reg(code, REG_RSI);
51+ emit_add_reg_imm8(code, REG_RDX, '0');
52+ emit_store_byte_reg(code, REG_RCX, REG_RDX);
53+ emit_cmp_reg_imm32(code, REG_RAX, 0);
54+ emit_jcc_back(code, COND_NE, loop_start);
55+
56+ // ---- sign prepend ----
57+ emit_cmp_reg_imm32(code, REG_RBX, 0);
58+ int skip_sign = emit_jcc_rel32(code, COND_E);
59+ emit_dec_reg(code, REG_RCX);
60+ emit_store_byte_imm(code, REG_RCX, '-');
61+ emit_patch_jump(code, skip_sign);
62+
63+ // ---- fraction digits, forward, round-half-up first (see
64+ // print_int.c's comment for why) ----
65+ load_double_const(XMM1, 0.0000005);
66+ emit_addsd(code, XMM0, XMM1);
67+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
68+ emit_add_reg_imm8(code, REG_RSI, 33);
69+ load_double_const(XMM3, 10.0);
70+ emit_mov_reg_imm64(code, REG_RBX, 0); // frac_len
71+ for (int i = 0; i < 6; i++) {
72+ emit_mulsd(code, XMM0, XMM3);
73+ emit_cvttsd2si(code, REG_RAX, XMM0);
74+ emit_cvtsi2sd(code, XMM1, REG_RAX);
75+ emit_subsd(code, XMM0, XMM1);
76+ emit_cmp_reg_imm32(code, REG_RAX, 0);
77+ int skip_mark = emit_jcc_rel32(code, COND_E);
78+ emit_mov_reg_imm64(code, REG_RBX, (uint64_t)(i + 1));
79+ emit_patch_jump(code, skip_mark);
80+ emit_add_reg_imm8(code, REG_RAX, '0');
81+ emit_store_byte_reg(code, REG_RSI, REG_RAX);
82+ emit_add_reg_imm8(code, REG_RSI, 1);
83+ }
84+
85+ // ---- decide '.' placement, compute the content's end pointer (no
86+ // trailing newline -- this is a string value, not a print) ----
87+ emit_mov_reg_reg(code, REG_RDX, REG_RSP);
88+ emit_add_reg_imm8(code, REG_RDX, 32);
89+ emit_cmp_reg_imm32(code, REG_RBX, 0);
90+ int whole_number = emit_jcc_rel32(code, COND_E);
91+ emit_store_byte_imm(code, REG_RDX, '.');
92+ emit_mov_reg_reg(code, REG_RDX, REG_RSP);
93+ emit_add_reg_imm8(code, REG_RDX, 33);
94+ emit_add_reg_reg(code, REG_RDX, REG_RBX);
95+ int after_dot = emit_jmp_rel32(code);
96+ emit_patch_jump(code, whole_number);
97+ emit_patch_jump(code, after_dot);
98+
99+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // RDX = content length
100+
101+ // ---- heap-allocate a length-prefixed block and copy the digits in ----
102+ emit_mov_reg_reg(code, REG_RDI, REG_RDX);
103+ emit_add_reg_imm8(code, REG_RDI, 8);
104+ heap_emit_alloc(code); // RAX = new block, clobbers RSI
105+
106+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RDX); // length prefix
107+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash block addr (bytes_emit_copy preserves RSI)
108+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
109+ emit_add_reg_imm8(code, REG_RDI, 8); // dest = block+8
110+ emit_mov_reg_reg(code, REG_RBX, REG_RCX); // src = digit buffer start
111+ bytes_emit_copy(code); // clobbers RAX,RBX,RCX,RDX,RDI; leaves RSI = block addr
112+
113+ emit_add_reg_imm8(code, REG_RSP, 40); // release the digit scratch
114+
115+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
116+ emit_add_reg_imm8(code, REG_RAX, 8); // payload = block+8
117+
118+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
119+ emit_push_reg(code, REG_RBX);
120+ emit_push_reg(code, REG_RAX);
121+}

kiln/src/codegen/str_builtin.dadded

+12−0
@@ -0,0 +1,12 @@
1+src/codegen/str_builtin.o: src/codegen/str_builtin.c \
2+ src/codegen/convert_builtins.h src/codegen/emit.h src/codegen/emit_sse.h \
3+ src/codegen/value.h src/codegen/heap.h src/codegen/bytes.h \
4+ src/parser/parser.h src/lexer/lexer.h
5+src/codegen/convert_builtins.h:
6+src/codegen/emit.h:
7+src/codegen/emit_sse.h:
8+src/codegen/value.h:
9+src/codegen/heap.h:
10+src/codegen/bytes.h:
11+src/parser/parser.h:
12+src/lexer/lexer.h:

kiln/src/codegen/string_alloc.cadded

+51−0
@@ -0,0 +1,51 @@
1+#include "codegen/string_alloc.h"
2+#include "codegen/heap.h"
3+#include "codegen/win_call.h"
4+#include "elf/elf_dynamic_call.h"
5+#include "target.h"
6+
7+// Plan B, phase B1: emitted once (jumped over), called via emit_call_back
8+// from all 13 call sites -- same pattern as codegen/heap.c's alloc
9+// routine and codegen/bytes.c's copy routine (which this one itself
10+// calls, via heap_emit_alloc -- routines calling other routines nest
11+// exactly like any other CALL/RET, no special handling needed). Note this
12+// stays a LOCAL call even when compiling libkilnrt.so/.dll: the "compile
13+// the runtime" driver sets kiln_set_compiling_so(1), which
14+// heap_emit_alloc's own is_shared() check also looks at, so it correctly
15+// resolves to a same-library relative call rather than calling back out
16+// to itself (link mode itself stays "shared" throughout -- see target.h).
17+static int prefixed_routine_offset = -1;
18+
19+static void emit_prefixed_routine(CodeBuf *code) {
20+ int skip = emit_jmp_rel32(code);
21+ prefixed_routine_offset = code->count;
22+ emit_mov_reg_reg(code, REG_RDI, REG_RDX);
23+ emit_add_reg_imm8(code, REG_RDI, 8);
24+ heap_emit_alloc(code); // RAX = block address; RDX (length) survives
25+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RDX);
26+ emit_ret(code);
27+ emit_patch_jump(code, skip);
28+}
29+
30+void string_alloc_emit_prefixed_routine_only(CodeBuf *code) {
31+ emit_prefixed_routine(code);
32+}
33+
34+int string_alloc_prefixed_routine_offset(void) { return prefixed_routine_offset; }
35+
36+static int is_shared(void) {
37+ return kiln_get_link_mode() == KILN_LINK_SHARED && !kiln_is_compiling_so();
38+}
39+
40+void string_alloc_emit_startup(CodeBuf *code) {
41+ if (!is_shared()) emit_prefixed_routine(code);
42+}
43+
44+void string_alloc_emit_prefixed(CodeBuf *code) {
45+ if (is_shared()) {
46+ if (kiln_get_target() == KILN_TARGET_WINDOWS) win_call_runtime_import(code, RUNTIME_IMPORT_STRING_ALLOC_PREFIXED);
47+ else elf_dynamic_call(code, RUNTIME_IMPORT_STRING_ALLOC_PREFIXED);
48+ return;
49+ }
50+ emit_call_back(code, prefixed_routine_offset);
51+}

kiln/src/codegen/string_alloc.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/codegen/string_alloc.o: src/codegen/string_alloc.c \
2+ src/codegen/string_alloc.h src/codegen/emit.h src/codegen/heap.h \
3+ src/codegen/win_call.h src/pe/pe_writer.h src/codegen/runtime_layout.h \
4+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
5+ src/pe/pe_imports.def src/elf/elf_dynamic_call.h src/elf/elf_dynamic.h \
6+ src/elf/elf_writer.h src/target.h
7+src/codegen/string_alloc.h:
8+src/codegen/emit.h:
9+src/codegen/heap.h:
10+src/codegen/win_call.h:
11+src/pe/pe_writer.h:
12+src/codegen/runtime_layout.h:
13+src/codegen/runtime_import_enum.h:
14+src/codegen/runtime_exports.def:
15+src/pe/pe_imports.def:
16+src/elf/elf_dynamic_call.h:
17+src/elf/elf_dynamic.h:
18+src/elf/elf_writer.h:
19+src/target.h:

kiln/src/codegen/string_alloc.hadded

+27−0
@@ -0,0 +1,27 @@
1+#ifndef KILN_STRING_ALLOC_H
2+#define KILN_STRING_ALLOC_H
3+#include "codegen/emit.h"
4+
5+// Emitted once, at program start (see parser.c's compile_program) --
6+// must run after heap_emit_startup (this calls heap_emit_alloc) and
7+// before any call to string_alloc_emit_prefixed below.
8+void string_alloc_emit_startup(CodeBuf *code);
9+
10+// Plan B: emits ONLY the prefixed-alloc routine's body -- used by the
11+// standalone "compile the runtime" driver that builds libkilnrt.so.
12+void string_alloc_emit_prefixed_routine_only(CodeBuf *code);
13+int string_alloc_prefixed_routine_offset(void);
14+
15+// Allocates a heap block for a new TAG_STRING value's content: input
16+// RDX=content length, output RAX=block address with the length prefix
17+// already written at [RAX+0] (the payload is RAX+8, same layout
18+// codegen/strings.c's literals use). Clobbers RSI, RDI (via
19+// codegen/heap.c's heap_emit_alloc) -- RDX itself survives untouched.
20+// Shared by every milestone-10 string builtin that produces a new string
21+// (upper/lower/trim/substring/join/replace) so the "+8 for the length
22+// prefix" arithmetic exists in exactly one place. Plan B, phase B1: a
23+// real CALL to one shared copy rather than re-emitting inline at each of
24+// its ~13 call sites.
25+void string_alloc_emit_prefixed(CodeBuf *code);
26+
27+#endif

kiln/src/codegen/string_builtins.hadded

+19−0
@@ -0,0 +1,19 @@
1+#ifndef KILN_STRING_BUILTINS_H
2+#define KILN_STRING_BUILTINS_H
3+
4+// upper/lower/trim assume their one STRING argument already pushed --
5+// codegen/builtins.c's dispatch calls these after parsing the argument.
6+void codegen_builtin_upper(void);
7+void codegen_builtin_lower(void);
8+void codegen_builtin_trim(void);
9+
10+// substring/indexOf/split/join/replace self-parse their (multiple)
11+// arguments starting right after the already-consumed '(', same
12+// convention as codegen/higher_order.h's map/filter/reduce.
13+void codegen_builtin_substring(void);
14+void codegen_builtin_indexof(void);
15+void codegen_builtin_split(void);
16+void codegen_builtin_join(void);
17+void codegen_builtin_replace(void);
18+
19+#endif

kiln/src/codegen/strings.cadded

+176−0
@@ -0,0 +1,176 @@
1+#include "codegen/strings.h"
2+#include "codegen/emit_internal.h"
3+#include "codegen/emit_sse.h"
4+#include "codegen/value.h"
5+#include "codegen/heap.h"
6+#include "codegen/bytes.h"
7+#include "codegen/string_alloc.h"
8+#include "codegen/errors.h"
9+#include "codegen/layout.h"
10+#include "parser/parser.h"
11+
12+static void push_number_literal(double v) {
13+ uint64_t bits;
14+ __builtin_memcpy(&bits, &v, sizeof(bits));
15+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
16+ emit_push_reg(code, REG_RBX);
17+ emit_mov_reg_imm64(code, REG_RAX, bits);
18+ emit_push_reg(code, REG_RAX);
19+}
20+
21+void codegen_string_literal_bytes(const char *text, int len) {
22+ int actual_len = 0;
23+ for (int i = 0; i < len; i++) {
24+ if (text[i] == '\\' && i + 1 < len) i++;
25+ actual_len++;
26+ }
27+
28+ int skip_jump = emit_jmp_rel32(code); // don't execute the embedded data as instructions
29+ int data_offset = code->count;
30+ emit_u64(code, (uint64_t)actual_len);
31+ for (int i = 0; i < len; i++) {
32+ char c = text[i];
33+ if (c == '\\' && i + 1 < len) {
34+ char next = text[i + 1];
35+ switch (next) {
36+ case 'n': c = '\n'; break;
37+ case 't': c = '\t'; break;
38+ case 'r': c = '\r'; break;
39+ case '"': c = '"'; break;
40+ case '\\': c = '\\'; break;
41+ default: c = next; break;
42+ }
43+ i++;
44+ }
45+ emit_byte(code, (uint8_t)c);
46+ }
47+ emit_patch_jump(code, skip_jump);
48+
49+ uint64_t payload_addr = kiln_code_base() + (uint64_t)data_offset + 8;
50+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
51+ emit_push_reg(code, REG_RBX);
52+ emit_mov_reg_imm64(code, REG_RAX, payload_addr);
53+ emit_push_reg(code, REG_RAX);
54+}
55+
56+void codegen_string_literal(void) {
57+ codegen_string_literal_bytes(current.start, current.length);
58+ advance_token();
59+}
60+
61+// Scratch layout (a stable base pointer copied from RSP into RSI, since
62+// [rsp+disp] itself needs a SIB byte this project's encoders don't
63+// support -- same technique codegen/print_int.c uses):
64+// [0]=a_payload [8]=b_payload [16]=length_a [24]=length_b [32]=new_block
65+void codegen_string_concat(void) {
66+ emit_sub_reg_imm8(code, REG_RSP, 40);
67+ emit_mov_reg_reg(code, REG_RSI, REG_RSP);
68+
69+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RAX);
70+ emit_store_mem_disp32(code, REG_RSI, 8, REG_RCX);
71+
72+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, -8); // length_a (length prefix sits right before the payload)
73+ emit_store_mem_disp32(code, REG_RSI, 16, REG_RAX);
74+ emit_load_mem_disp32(code, REG_RCX, REG_RCX, -8); // length_b
75+ emit_store_mem_disp32(code, REG_RSI, 24, REG_RCX);
76+
77+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
78+ emit_add_reg_reg(code, REG_RDI, REG_RCX);
79+ emit_add_reg_imm8(code, REG_RDI, 8); // +8 for the new string's own length prefix
80+
81+ emit_push_reg(code, REG_RSI); // heap_emit_alloc clobbers RSI -- save/restore around it
82+ heap_emit_alloc(code); // RAX = new block
83+ emit_pop_reg(code, REG_RSI);
84+ emit_store_mem_disp32(code, REG_RSI, 32, REG_RAX);
85+
86+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 16);
87+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 24);
88+ emit_add_reg_reg(code, REG_RCX, REG_RDX);
89+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX); // [new_block] = combined length
90+
91+ // copy A right after the length prefix
92+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 32);
93+ emit_add_reg_imm8(code, REG_RDI, 8);
94+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 0); // src = a_payload
95+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // count = length_a
96+ bytes_emit_copy(code);
97+
98+ // copy B right after A
99+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 32);
100+ emit_add_reg_imm8(code, REG_RDI, 8);
101+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 16); // length_a
102+ emit_add_reg_reg(code, REG_RDI, REG_RCX);
103+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 8); // src = b_payload
104+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 24); // count = length_b
105+ bytes_emit_copy(code);
106+
107+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 32);
108+ emit_add_reg_imm8(code, REG_RAX, 8); // RAX = result payload address
109+
110+ emit_add_reg_imm8(code, REG_RSP, 40);
111+
112+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
113+ emit_push_reg(code, REG_RBX);
114+ emit_push_reg(code, REG_RAX);
115+}
116+
117+void codegen_string_compare(int invert) {
118+ emit_load_mem_disp32(code, REG_RBX, REG_RAX, -8); // length_a
119+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, -8); // length_b
120+ emit_cmp_reg_reg(code, REG_RBX, REG_RDX);
121+ int to_false = emit_jcc_rel32(code, COND_NE); // different lengths -> can't be equal
122+
123+ int loop_start = code->count;
124+ emit_cmp_reg_imm32(code, REG_RBX, 0);
125+ int to_true = emit_jcc_rel32(code, COND_E); // ran out of bytes with no mismatch -> equal
126+ emit_load_byte_reg(code, REG_RSI, REG_RAX);
127+ emit_load_byte_reg(code, REG_RDI, REG_RCX);
128+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
129+ int mismatch = emit_jcc_rel32(code, COND_NE);
130+ emit_add_reg_imm8(code, REG_RAX, 1);
131+ emit_add_reg_imm8(code, REG_RCX, 1);
132+ emit_dec_reg(code, REG_RBX);
133+ emit_jmp_back(code, loop_start);
134+
135+ emit_patch_jump(code, mismatch);
136+ emit_patch_jump(code, to_false);
137+ push_number_literal(invert ? 1.0 : 0.0);
138+ int skip_true = emit_jmp_rel32(code);
139+ emit_patch_jump(code, to_true);
140+ push_number_literal(invert ? 0.0 : 1.0);
141+ emit_patch_jump(code, skip_true);
142+}
143+
144+void codegen_string_index_read(void) {
145+ emit_pop_reg(code, REG_RAX); // index payload
146+ emit_pop_reg(code, REG_RBX); // index tag (ignored, assumed NUMBER)
147+ emit_pop_reg(code, REG_RSI); // string payload
148+ emit_pop_reg(code, REG_RBX); // string tag (ignored, assumed STRING)
149+
150+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
151+ emit_cvttsd2si(code, REG_RCX, XMM0); // index
152+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length
153+
154+ emit_cmp_reg_imm32(code, REG_RCX, 0);
155+ int neg = emit_jcc_rel32(code, COND_LT);
156+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);
157+ int toobig = emit_jcc_rel32(code, COND_GE);
158+ int ok = emit_jmp_rel32(code);
159+ emit_patch_jump(code, neg);
160+ emit_patch_jump(code, toobig);
161+ errors_emit_die(code, "runtime error: string index out of bounds");
162+ emit_patch_jump(code, ok);
163+
164+ emit_add_reg_reg(code, REG_RSI, REG_RCX); // char address
165+ emit_load_byte_reg(code, REG_RBX, REG_RSI); // char byte (RBX survives string_alloc_emit_prefixed)
166+
167+ emit_mov_reg_imm64(code, REG_RDX, 1);
168+ string_alloc_emit_prefixed(code); // RAX = new 1-byte block, length prefix already written
169+
170+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
171+ emit_store_byte_reg(code, REG_RAX, REG_RBX);
172+
173+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
174+ emit_push_reg(code, REG_RBX);
175+ emit_push_reg(code, REG_RAX);
176+}

kiln/src/codegen/strings.dadded

+17−0
@@ -0,0 +1,17 @@
1+src/codegen/strings.o: src/codegen/strings.c src/codegen/strings.h \
2+ src/codegen/emit_internal.h src/codegen/emit.h src/codegen/emit_sse.h \
3+ src/codegen/value.h src/codegen/heap.h src/codegen/bytes.h \
4+ src/codegen/string_alloc.h src/codegen/errors.h src/codegen/layout.h \
5+ src/parser/parser.h src/lexer/lexer.h
6+src/codegen/strings.h:
7+src/codegen/emit_internal.h:
8+src/codegen/emit.h:
9+src/codegen/emit_sse.h:
10+src/codegen/value.h:
11+src/codegen/heap.h:
12+src/codegen/bytes.h:
13+src/codegen/string_alloc.h:
14+src/codegen/errors.h:
15+src/codegen/layout.h:
16+src/parser/parser.h:
17+src/lexer/lexer.h:

kiln/src/codegen/strings.hadded

+41−0
@@ -0,0 +1,41 @@
1+#ifndef KILN_STRINGS_H
2+#define KILN_STRINGS_H
3+
4+// primary()'s TOKEN_STRING handling: embeds the literal's escaped bytes
5+// into the code buffer (jumped over, so normal control flow never tries
6+// to execute them as instructions -- same technique fn declarations use
7+// for their bodies), pushes (tag=STRING, payload=<fixed absolute
8+// address>). Kiln generates a fixed, non-PIE executable, so a literal's
9+// address is knowable at compile time -- no heap needed for these.
10+void codegen_string_literal(void);
11+
12+// Same emission as codegen_string_literal, but takes raw escaped bytes
13+// directly instead of reading the parser's global `current` token --
14+// used by constants.c to fold an imported string `let` at each use site,
15+// where `current` is the identifier/dot chain, not a string token.
16+void codegen_string_literal_bytes(const char *text, int len);
17+
18+// Both assume operands already popped with a_payload in RAX, b_payload
19+// in RCX (a_tag/b_tag, if the caller still needs them, must be read and
20+// saved BEFORE calling -- these clobber RAX,RBX,RCX,RDX,RSI,RDI freely).
21+
22+// Pushes (tag=STRING, payload=<new heap address>) -- the concatenation.
23+// Needs a real heap allocation (see codegen/heap.h) since the combined
24+// size is only known at runtime.
25+void codegen_string_concat(void);
26+
27+// Pushes (tag=NUMBER, payload=1.0/0.0) -- byte-equal (invert=0) or
28+// byte-different (invert=1, i.e. `!=`).
29+void codegen_string_compare(int invert);
30+
31+// Assumes index already pushed (top) and string already pushed (below
32+// it), both (tag, payload) pairs -- postfix()'s `s[i]` read, the
33+// TAG_STRING case (previously missing entirely -- see missing.md #1:
34+// falling through to the array path here would have read the string's
35+// own content bytes as if they were an array-object struct). Pops both,
36+// bounds-checks, pushes a fresh 1-char string. Read-only: strings are
37+// immutable, `s[i] = x` is a parse-time-caught statement shape kiln's
38+// grammar doesn't even offer a codegen path for (see parser.c).
39+void codegen_string_index_read(void);
40+
41+#endif

kiln/src/codegen/substring_builtin.cadded

+66−0
@@ -0,0 +1,66 @@
1+#include "codegen/string_builtins.h"
2+#include "codegen/string_alloc.h"
3+#include "codegen/expr.h"
4+#include "codegen/bytes.h"
5+#include "codegen/emit.h"
6+#include "codegen/emit_sse.h"
7+#include "codegen/value.h"
8+#include "parser/parser.h"
9+
10+// substring(str, start, end): clamps start>=0, end<=len, start>end->start=end
11+// (matching ashvm's own clamping exactly, not erroring on out-of-range).
12+void codegen_builtin_substring(void) {
13+ codegen_expression(); // str
14+ expect(TOKEN_COMMA, "expected ',' after string argument");
15+ codegen_expression(); // start
16+ expect(TOKEN_COMMA, "expected ',' after start argument");
17+ codegen_expression(); // end
18+ expect(TOKEN_RPAREN, "expected ')' after arguments");
19+
20+ emit_pop_reg(code, REG_RAX); // end payload
21+ emit_pop_reg(code, REG_RBX); // end tag (ignored)
22+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
23+ emit_cvttsd2si(code, REG_RDI, XMM0); // end (int)
24+
25+ emit_pop_reg(code, REG_RAX); // start payload
26+ emit_pop_reg(code, REG_RBX); // start tag (ignored)
27+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
28+ emit_cvttsd2si(code, REG_RCX, XMM0); // start (int)
29+
30+ emit_pop_reg(code, REG_RSI); // str payload
31+ emit_pop_reg(code, REG_RBX); // str tag (ignored)
32+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // slen
33+
34+ emit_cmp_reg_imm32(code, REG_RCX, 0);
35+ int start_ok = emit_jcc_rel32(code, COND_GE);
36+ emit_mov_reg_imm64(code, REG_RCX, 0);
37+ emit_patch_jump(code, start_ok);
38+
39+ emit_cmp_reg_reg(code, REG_RDI, REG_RDX);
40+ int end_ok = emit_jcc_rel32(code, COND_LE);
41+ emit_mov_reg_reg(code, REG_RDI, REG_RDX);
42+ emit_patch_jump(code, end_ok);
43+
44+ emit_cmp_reg_reg(code, REG_RCX, REG_RDI);
45+ int order_ok = emit_jcc_rel32(code, COND_LE);
46+ emit_mov_reg_reg(code, REG_RCX, REG_RDI);
47+ emit_patch_jump(code, order_ok);
48+
49+ emit_mov_reg_reg(code, REG_RDX, REG_RDI);
50+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // length = end - start
51+ emit_add_reg_reg(code, REG_RSI, REG_RCX); // src = str payload + start
52+ emit_mov_reg_reg(code, REG_RBX, REG_RSI); // stash src (survives the alloc call)
53+
54+ string_alloc_emit_prefixed(code); // RAX = new block, RDX = length (preserved)
55+
56+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash block (bytes_emit_copy preserves RSI)
57+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
58+ emit_add_reg_imm8(code, REG_RDI, 8);
59+ bytes_emit_copy(code); // RBX=src (already set), RDX=length (already set)
60+
61+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
62+ emit_add_reg_imm8(code, REG_RAX, 8);
63+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
64+ emit_push_reg(code, REG_RBX);
65+ emit_push_reg(code, REG_RAX);
66+}

kiln/src/codegen/substring_builtin.dadded

+14−0
@@ -0,0 +1,14 @@
1+src/codegen/substring_builtin.o: src/codegen/substring_builtin.c \
2+ src/codegen/string_builtins.h src/codegen/string_alloc.h \
3+ src/codegen/emit.h src/codegen/expr.h src/lexer/lexer.h \
4+ src/codegen/bytes.h src/codegen/emit_sse.h src/codegen/value.h \
5+ src/parser/parser.h
6+src/codegen/string_builtins.h:
7+src/codegen/string_alloc.h:
8+src/codegen/emit.h:
9+src/codegen/expr.h:
10+src/lexer/lexer.h:
11+src/codegen/bytes.h:
12+src/codegen/emit_sse.h:
13+src/codegen/value.h:
14+src/parser/parser.h:

kiln/src/codegen/trim_builtin.cadded

+79−0
@@ -0,0 +1,79 @@
1+#include "codegen/string_builtins.h"
2+#include "codegen/string_alloc.h"
3+#include "codegen/bytes.h"
4+#include "codegen/emit.h"
5+#include "codegen/value.h"
6+#include "parser/parser.h"
7+
8+// Advances RSI forward past leading whitespace, then RDI backward past
9+// trailing whitespace (RDI starts as the exclusive end pointer), each via
10+// a small 4-way character check ( ' ' '\t' '\n' '\r', matching ashvm's
11+// own set) inlined rather than a shared byte-classifier helper -- it's
12+// only used twice, in a shape specific to which direction it's scanning.
13+void codegen_builtin_trim(void) {
14+ emit_pop_reg(code, REG_RAX); // src payload
15+ emit_pop_reg(code, REG_RBX); // tag (ignored)
16+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length
17+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // start cursor
18+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
19+ emit_add_reg_reg(code, REG_RDI, REG_RDX); // end cursor (exclusive)
20+
21+ int trim_start = code->count;
22+ emit_cmp_reg_reg(code, REG_RSI, REG_RDI);
23+ int start_done = emit_jcc_rel32(code, COND_GE);
24+ emit_load_byte_reg(code, REG_RCX, REG_RSI);
25+ emit_cmp_reg_imm32(code, REG_RCX, ' ');
26+ int s1 = emit_jcc_rel32(code, COND_E);
27+ emit_cmp_reg_imm32(code, REG_RCX, '\t');
28+ int s2 = emit_jcc_rel32(code, COND_E);
29+ emit_cmp_reg_imm32(code, REG_RCX, '\n');
30+ int s3 = emit_jcc_rel32(code, COND_E);
31+ emit_cmp_reg_imm32(code, REG_RCX, '\r');
32+ int s4 = emit_jcc_rel32(code, COND_E);
33+ int not_ws_start = emit_jmp_rel32(code);
34+ emit_patch_jump(code, s1); emit_patch_jump(code, s2);
35+ emit_patch_jump(code, s3); emit_patch_jump(code, s4);
36+ emit_add_reg_imm8(code, REG_RSI, 1);
37+ emit_jmp_back(code, trim_start);
38+ emit_patch_jump(code, not_ws_start);
39+ emit_patch_jump(code, start_done);
40+
41+ int trim_end = code->count;
42+ emit_cmp_reg_reg(code, REG_RDI, REG_RSI);
43+ int end_done = emit_jcc_rel32(code, COND_LE);
44+ emit_mov_reg_reg(code, REG_RAX, REG_RDI);
45+ emit_sub_reg_imm8(code, REG_RAX, 1);
46+ emit_load_byte_reg(code, REG_RCX, REG_RAX);
47+ emit_cmp_reg_imm32(code, REG_RCX, ' ');
48+ int e1 = emit_jcc_rel32(code, COND_E);
49+ emit_cmp_reg_imm32(code, REG_RCX, '\t');
50+ int e2 = emit_jcc_rel32(code, COND_E);
51+ emit_cmp_reg_imm32(code, REG_RCX, '\n');
52+ int e3 = emit_jcc_rel32(code, COND_E);
53+ emit_cmp_reg_imm32(code, REG_RCX, '\r');
54+ int e4 = emit_jcc_rel32(code, COND_E);
55+ int not_ws_end = emit_jmp_rel32(code);
56+ emit_patch_jump(code, e1); emit_patch_jump(code, e2);
57+ emit_patch_jump(code, e3); emit_patch_jump(code, e4);
58+ emit_sub_reg_imm8(code, REG_RDI, 1);
59+ emit_jmp_back(code, trim_end);
60+ emit_patch_jump(code, not_ws_end);
61+ emit_patch_jump(code, end_done);
62+
63+ emit_mov_reg_reg(code, REG_RDX, REG_RDI);
64+ emit_sub_reg_reg(code, REG_RDX, REG_RSI); // length
65+ emit_mov_reg_reg(code, REG_RBX, REG_RSI); // stash trimmed start (survives the alloc call)
66+
67+ string_alloc_emit_prefixed(code); // RAX = new block, RDX = length (preserved)
68+
69+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash block (bytes_emit_copy preserves RSI)
70+ emit_mov_reg_reg(code, REG_RDI, REG_RSI);
71+ emit_add_reg_imm8(code, REG_RDI, 8); // dest
72+ bytes_emit_copy(code); // RBX=src (already set), RDX=length (already set)
73+
74+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);
75+ emit_add_reg_imm8(code, REG_RAX, 8); // payload
76+ emit_mov_reg_imm64(code, REG_RBX, TAG_STRING);
77+ emit_push_reg(code, REG_RBX);
78+ emit_push_reg(code, REG_RAX);
79+}

kiln/src/codegen/trim_builtin.dadded

+11−0
@@ -0,0 +1,11 @@
1+src/codegen/trim_builtin.o: src/codegen/trim_builtin.c \
2+ src/codegen/string_builtins.h src/codegen/string_alloc.h \
3+ src/codegen/emit.h src/codegen/bytes.h src/codegen/value.h \
4+ src/parser/parser.h src/lexer/lexer.h
5+src/codegen/string_builtins.h:
6+src/codegen/string_alloc.h:
7+src/codegen/emit.h:
8+src/codegen/bytes.h:
9+src/codegen/value.h:
10+src/parser/parser.h:
11+src/lexer/lexer.h:

kiln/src/codegen/value.hadded

+99−0
@@ -0,0 +1,99 @@
1+#ifndef KILN_VALUE_H
2+#define KILN_VALUE_H
3+
4+// Milestone 5: an ash value is no longer always a bare double -- it can
5+// also be a string. Every value is now a (tag, payload) pair, 16 bytes
6+// total, moved around generically via GP registers (raw bit-copying
7+// doesn't care what the bits mean -- see codegen/expr.c's header
8+// comment). XMM registers only ever hold the payload transiently, at the
9+// point of actually doing floating-point arithmetic on a NUMBER-tagged
10+// value.
11+//
12+// A string's payload is an absolute address (kiln generates a fixed,
13+// non-PIE executable, so absolute addresses are stable and known at
14+// compile time) pointing at a length-prefixed byte block: 8 bytes of
15+// length, followed by that many raw bytes. String literals are embedded
16+// directly in the code buffer (see codegen/strings.c); concatenation
17+// results are heap-allocated (see codegen/heap.c) since their size is
18+// only known at runtime.
19+#define TAG_NUMBER 0
20+#define TAG_STRING 1
21+#define TAG_ARRAY 2
22+#define TAG_MAP 3
23+#define TAG_FUNCTION 4
24+
25+// A nil value's payload is always exactly 0 -- deliberately, so it
26+// coincides with +0.0's bit pattern. That's what lets nil ride the
27+// EXISTING truthy-test and numeric-comparison codegen (both just
28+// ucomisd the payload against 0.0) without needing its own special case
29+// anywhere except print (see codegen/print_int.c's dispatch) and the
30+// tag-equality short-circuit in comparisons (already tag-aware since
31+// M5's `==`/`!=`, see codegen/expr_bool.c -- nil==nil compares equal
32+// tags then falls into the numeric path, 0.0==0.0; nil compared against
33+// any other tag is caught by the tags-differ branch before ever
34+// touching the payload). `true`/`false` are NOT a separate tag --
35+// they're just named literals for TAG_NUMBER 1.0/0.0 (missing.md's
36+// "boolean type" note is intentionally only half-addressed this way:
37+// the whole codebase already treats "truthy" as "NUMBER != 0", so a
38+// distinct boolean tag would mean touching that logic everywhere
39+// instead of adding one clean new case).
40+#define TAG_NIL 5
41+
42+// An array's payload is the address of a small, fixed-size, STABLE
43+// "array object" (see codegen/strings.h's comment on why absolute
44+// addresses are safe: kiln is always a fixed, non-PIE executable):
45+// [0] capacity (int64, how many elements the data block has room for)
46+// [8] count (int64, how many are actually in use)
47+// [16] data_ptr (address of a SEPARATE elements block)
48+// The elements block itself ([16*i]=tag, [16*i+8]=payload, for i in
49+// [0,capacity)) is what actually moves on growth -- a fresh, bigger
50+// block gets allocated and the old one's contents copied over (the bump
51+// allocator can't grow in place), but only data_ptr's VALUE changes.
52+// The array object's own address -- what every variable/slot holding
53+// this array actually stores -- never moves. This matters: without it,
54+// `push(arr, x)` growing the array would silently leave every existing
55+// reference to `arr` pointing at stale, abandoned memory. (ashvm gets
56+// this for free because VMArray is a pointer to a struct whose `items`
57+// field can be realloc'd independently of the struct's own address --
58+// this is kiln's equivalent of that same shape.)
59+#define ARRAY_OBJECT_SIZE 24
60+
61+// A map uses the exact same stable-object-plus-resizable-data shape as
62+// an array (see above -- same reason: growth must not move the address
63+// every existing reference to the map already holds):
64+// [0] capacity [8] count [16] entries_ptr
65+// Each entry is 32 bytes: [0]=key tag (always TAG_STRING) [8]=key
66+// payload [16]=value tag [24]=value payload. Lookup is a linear scan,
67+// not a hash table -- ashvm uses a real hash map; this is a documented
68+// simplification (O(n) instead of O(1)) for implementation reach, not a
69+// semantic difference a program could observe other than speed.
70+#define MAP_OBJECT_SIZE 24
71+#define MAP_ENTRY_SIZE 32
72+
73+// A function/closure's payload is the address of a small, fixed (NEVER
74+// resized -- closures are immutable once created, no growth concern like
75+// arrays/maps) heap block:
76+// [0] absolute code address (already resolved to KILN_LOAD_BASE +
77+// KILN_CODE_START_OFFSET + code_offset at compile time -- kiln is
78+// a fixed non-PIE executable, see elf/elf_writer.h, so this is
79+// known and stable)
80+// [8] capture_count (int64)
81+// [16+16*i] capture i's tag [16+16*i+8] capture i's payload
82+// A plain named function used as a value (not immediately called) gets
83+// capture_count=0 -- the same shape as any closure with zero captures, so
84+// codegen/closures.c's indirect-call codegen never special-cases "0
85+// captures", it just runs a 0-iteration loop.
86+//
87+// Scope limit (documented, not silently pretended away -- same spirit as
88+// the map's linear-scan note above): captures are snapshotted BY VALUE at
89+// closure-creation time, not by reference to a shared mutable cell. This
90+// supports closures that read outer variables (partial application, e.g.
91+// `fn make_adder(n) { return fn(x) { return x + n; }; }`) but NOT closures
92+// that mutate a captured variable and expect that mutation visible on the
93+// next call (the classic mutable "counter" idiom) -- that needs a real
94+// upvalue mechanism (boxed/heap-indirect variables), which conflicts with
95+// kiln's single-pass, fixed-stack-slot variable model and is out of scope
96+// for this milestone.
97+#define FUNCTION_OBJECT_HEADER_SIZE 16
98+
99+#endif

kiln/src/codegen/win_call.cadded

+111−0
@@ -0,0 +1,111 @@
1+#include "codegen/win_call.h"
2+#include "codegen/emit_internal.h"
3+#include "codegen/layout.h"
4+#include "parser/vars.h"
5+
6+// AND RSP, imm8 (sign-extended) -- opcode 0x83 /4. Nothing else in kiln
7+// needs a register-immediate AND, so this stays local instead of joining
8+// emit.c's public primitives.
9+static void emit_and_rsp_imm8(CodeBuf *code, int8_t imm) {
10+ emit_byte(code, REX_W);
11+ emit_byte(code, 0x83);
12+ emit_byte(code, modrm_reg((Reg)4, REG_RSP)); // /4 = AND
13+ emit_byte(code, (uint8_t)imm);
14+}
15+
16+// R8/R9 need REX.B to extend the 3-bit register field in both the
17+// "+reg" immediate-move opcode and a ModRM rm field -- kiln's Reg enum
18+// (emit.h) only covers RAX-RDI (0-7), so these stay as tiny local helpers
19+// parameterized by the raw register number (8 or 9) rather than growing
20+// the public enum for a feature only Windows argument-passing needs.
21+static void emit_mov_r8_9_imm64(CodeBuf *code, int reg89, uint64_t imm) {
22+ emit_byte(code, 0x49); // REX.WB
23+ emit_byte(code, (uint8_t)(0xB8 + (reg89 - 8)));
24+ emit_u64(code, imm);
25+}
26+
27+static void emit_mov_r8_9_reg(CodeBuf *code, int reg89, Reg src) {
28+ emit_byte(code, 0x49); // REX.WB
29+ emit_byte(code, 0x89); // MOV r/m64, r64
30+ emit_byte(code, modrm_reg(src, (Reg)(reg89 - 8)));
31+}
32+
33+// MOV [rsp+disp32], src -- RSP as a memory base always needs a SIB byte
34+// (index=100 "none", base=100=RSP, scale=00), unlike RBP which
35+// emit_mem.c's modrm_mem_disp32 already handles directly. Nothing else in
36+// kiln addresses [rsp+disp] (existing code routes through RBP-relative
37+// scratch instead), so this is local to the one place that genuinely
38+// needs it: Windows stack arguments (5th+), which the ABI requires to
39+// physically live above the shadow space at call time.
40+static void emit_store_rsp_disp32(CodeBuf *code, int32_t disp, Reg src) {
41+ emit_byte(code, REX_W);
42+ emit_byte(code, 0x89);
43+ emit_byte(code, (uint8_t)(0x80 | ((src & 7) << 3) | 4));
44+ emit_byte(code, 0x24); // SIB: scale=00, index=100 (none), base=100 (RSP)
45+ emit_i32(code, disp);
46+}
47+
48+void win_call_begin(CodeBuf *code, int stack_arg_count) {
49+ emit_store_mem_disp32(code, REG_RBP, win_call_scratch_offset(), REG_RSP);
50+ emit_and_rsp_imm8(code, -16);
51+ int32_t needed = 32 + 8 * stack_arg_count;
52+ int32_t reserved = (needed + 15) / 16 * 16;
53+ emit_sub_reg_imm8(code, REG_RSP, (int8_t)reserved);
54+}
55+
56+void win_call_arg_reg(CodeBuf *code, int index, Reg src) {
57+ switch (index) {
58+ case 0: emit_mov_reg_reg(code, REG_RCX, src); break;
59+ case 1: emit_mov_reg_reg(code, REG_RDX, src); break;
60+ case 2: emit_mov_r8_9_reg(code, 8, src); break;
61+ case 3: emit_mov_r8_9_reg(code, 9, src); break;
62+ }
63+}
64+
65+void win_call_arg_imm64(CodeBuf *code, int index, uint64_t imm) {
66+ switch (index) {
67+ case 0: emit_mov_reg_imm64(code, REG_RCX, imm); break;
68+ case 1: emit_mov_reg_imm64(code, REG_RDX, imm); break;
69+ case 2: emit_mov_r8_9_imm64(code, 8, imm); break;
70+ case 3: emit_mov_r8_9_imm64(code, 9, imm); break;
71+ }
72+}
73+
74+void win_call_stack_arg_reg(CodeBuf *code, int stack_index, Reg src) {
75+ emit_store_rsp_disp32(code, 32 + 8 * stack_index, src);
76+}
77+
78+void win_call_stack_arg_imm64(CodeBuf *code, int stack_index, uint64_t imm) {
79+ emit_mov_reg_imm64(code, REG_RAX, imm);
80+ emit_store_rsp_disp32(code, 32 + 8 * stack_index, REG_RAX);
81+}
82+
83+void win_call_import(CodeBuf *code, PeImport which) {
84+ emit_mov_reg_imm64(code, REG_RAX, pe_import_addr(which));
85+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 0); // resolved function pointer
86+ emit_call_indirect(code, REG_RAX);
87+}
88+
89+void win_call_end(CodeBuf *code) {
90+ emit_load_mem_disp32(code, REG_RSP, REG_RBP, win_call_scratch_offset());
91+}
92+
93+// CALL [rip+disp32] -- unlike win_call_import's kernel32.dll calls (real
94+// Windows API, needs the full shadow-space dance above), libkilnrt.dll's
95+// exports are kiln's OWN code using kiln's OWN simple per-routine
96+// register conventions (RAX/RBX/RDI/RDX depending on which routine, see
97+// codegen/heap.c etc.) -- exactly like the Linux GOT call
98+// (elf/elf_dynamic_call.c's emit_call_abs32), no register is safe to
99+// clobber loading the IAT address first. Can't reuse emit_call_abs32's
100+// technique directly, though: kiln's PE image base (0x140000000) is
101+// already past the 32-bit absolute-displacement addressing mode's
102+// range, but RIP-relative reaches it fine since the IAT slot and the
103+// calling code both live in the same small, nearby image.
104+void win_call_runtime_import(CodeBuf *code, RuntimeImport which) {
105+ uint64_t target = pe_runtime_import_addr(which);
106+ emit_byte(code, 0xFF);
107+ emit_byte(code, 0x15); // ModRM: mod=00, reg=010(/2=CALL), rm=101(RIP-relative)
108+ uint64_t next_instr_addr = kiln_code_base() + (uint64_t)(code->count + 4);
109+ int32_t rel = (int32_t)((int64_t)target - (int64_t)next_instr_addr);
110+ emit_i32(code, rel);
111+}

kiln/src/codegen/win_call.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/codegen/win_call.o: src/codegen/win_call.c src/codegen/win_call.h \
2+ src/codegen/emit.h src/pe/pe_writer.h src/codegen/runtime_layout.h \
3+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
4+ src/pe/pe_imports.def src/codegen/emit_internal.h src/codegen/layout.h \
5+ src/parser/vars.h
6+src/codegen/win_call.h:
7+src/codegen/emit.h:
8+src/pe/pe_writer.h:
9+src/codegen/runtime_layout.h:
10+src/codegen/runtime_import_enum.h:
11+src/codegen/runtime_exports.def:
12+src/pe/pe_imports.def:
13+src/codegen/emit_internal.h:
14+src/codegen/layout.h:
15+src/parser/vars.h:

kiln/src/codegen/win_call.hadded

+49−0
@@ -0,0 +1,49 @@
1+#ifndef KILN_WIN_CALL_H
2+#define KILN_WIN_CALL_H
3+#include "codegen/emit.h"
4+#include "pe/pe_writer.h"
5+
6+// Windows x64 calling convention -- the caller-side sequence for calling
7+// an imported kernel32.dll function. New territory for this codebase:
8+// unlike kiln's own internal `fn` convention (stack-push based, see
9+// parser/parser_control.c), this has to match a REAL external ABI, with
10+// rules kiln has never had to satisfy before:
11+// - first four integer/pointer args in RCX, RDX, R8, R9
12+// - a mandatory 32-byte "shadow space" the CALLEE is allowed to spill
13+// its register args into, reserved by the CALLER even if the callee
14+// never uses it
15+// - RSP must be 16-byte aligned at the `call` instruction
16+// - 5th+ args go on the stack, above the shadow space
17+//
18+// RSP alignment is the sharp edge: kiln's codegen never tracks RSP parity
19+// (arbitrary push/pop sequences leave it unpredictable at any given
20+// point, which was fine when the only "call-like" thing was a raw Linux
21+// `syscall` -- syscalls have no alignment requirement at all). So rather
22+// than trying to prove alignment holds at every call site,
23+// win_call_begin ALIGNS DYNAMICALLY at runtime (`and rsp, -16`) and saves
24+// the pre-align RSP to a fixed scratch slot; win_call_end restores that
25+// exact saved value, so the surrounding codegen's own RSP bookkeeping is
26+// completely undisturbed regardless of what it was before the call.
27+//
28+// Argument marshalling note: if a source value already lives in one of
29+// RCX/RDX/R8/R9 from earlier in this same sequence, load higher-indexed
30+// args before lower ones (or route through a scratch register like RAX/
31+// RSI first) -- win_call_arg_* does not detect or resolve register
32+// clobbers between argument slots.
33+void win_call_begin(CodeBuf *code, int stack_arg_count);
34+void win_call_arg_reg(CodeBuf *code, int index, Reg src); // index 0-3 -> RCX/RDX/R8/R9
35+void win_call_arg_imm64(CodeBuf *code, int index, uint64_t imm);
36+void win_call_stack_arg_reg(CodeBuf *code, int stack_index, Reg src); // stack_index 0-based, for args 5+
37+void win_call_stack_arg_imm64(CodeBuf *code, int stack_index, uint64_t imm);
38+// Calls through `which`'s IAT slot -- see pe_writer.h for why this is a
39+// direct absolute-address load+call rather than RIP-relative addressing.
40+// Result comes back in RAX, standard Windows x64 return convention.
41+void win_call_import(CodeBuf *code, PeImport which);
42+void win_call_end(CodeBuf *code);
43+
44+// --link=shared: calls into libkilnrt.dll instead of a local routine.
45+// See win_call.c for why this needs its own primitive rather than
46+// reusing win_call_import.
47+void win_call_runtime_import(CodeBuf *code, RuntimeImport which);
48+
49+#endif

kiln/src/codegen/win_file_call.cadded

+20−0
@@ -0,0 +1,20 @@
1+#include "codegen/win_file_call.h"
2+#include "codegen/win_call.h"
3+#include "parser/vars.h"
4+
5+// &ignored_out needs to be a real writable address (Windows doesn't
6+// accept NULL there for synchronous handles), so it points at a
7+// throwaway RBP-relative scratch DWORD -- computed via a low register
8+// first since there's no LEA primitive for the R8/R9 slot directly.
9+void win_file_call(CodeBuf *code, PeImport which) {
10+ win_call_begin(code, 1);
11+ win_call_arg_reg(code, 0, REG_RAX); // handle
12+ win_call_arg_reg(code, 1, REG_RSI); // buf
13+ win_call_arg_reg(code, 2, REG_RDI); // len
14+ emit_mov_reg_reg(code, REG_RAX, REG_RBP);
15+ emit_add_reg_imm32(code, REG_RAX, win_outparam_offset());
16+ win_call_arg_reg(code, 3, REG_RAX); // &written/&read (ignored)
17+ win_call_stack_arg_imm64(code, 0, 0); // lpOverlapped = NULL
18+ win_call_import(code, which);
19+ win_call_end(code);
20+}

kiln/src/codegen/win_file_call.dadded

+14−0
@@ -0,0 +1,14 @@
1+src/codegen/win_file_call.o: src/codegen/win_file_call.c \
2+ src/codegen/win_file_call.h src/pe/pe_writer.h src/codegen/emit.h \
3+ src/codegen/runtime_layout.h src/codegen/runtime_import_enum.h \
4+ src/codegen/runtime_exports.def src/pe/pe_imports.def \
5+ src/codegen/win_call.h src/parser/vars.h
6+src/codegen/win_file_call.h:
7+src/pe/pe_writer.h:
8+src/codegen/emit.h:
9+src/codegen/runtime_layout.h:
10+src/codegen/runtime_import_enum.h:
11+src/codegen/runtime_exports.def:
12+src/pe/pe_imports.def:
13+src/codegen/win_call.h:
14+src/parser/vars.h:

kiln/src/codegen/win_file_call.hadded

+15−0
@@ -0,0 +1,15 @@
1+#ifndef KILN_WIN_FILE_CALL_H
2+#define KILN_WIN_FILE_CALL_H
3+#include "pe/pe_writer.h"
4+
5+// Shared shape between ReadFile and WriteFile's argument lists (both are
6+// (handle, buffer, length, &ignored_out, NULL)) -- used by both
7+// codegen/platform_console.c (stdin/stdout/stderr) and
8+// codegen/platform_file.c (real files), so it lives here instead of
9+// being duplicated in each.
10+//
11+// In: RAX=handle, RSI=buf, RDI=len. `which` must be PE_IMPORT_READ_FILE
12+// or PE_IMPORT_WRITE_FILE.
13+void win_file_call(CodeBuf *code, PeImport which);
14+
15+#endif

kiln/src/codegen/write_file_builtins.cadded

+83−0
@@ -0,0 +1,83 @@
1+#include <stdint.h>
2+#include "codegen/file_builtins.h"
3+#include "codegen/file_path.h"
4+#include "codegen/expr.h"
5+#include "codegen/emit_sse.h"
6+#include "codegen/value.h"
7+#include "codegen/platform_file.h"
8+#include "parser/parser.h"
9+#include "parser/vars.h"
10+
11+static void push_number(double v) {
12+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
13+ emit_push_reg(code, REG_RBX);
14+ if (v == 0.0) {
15+ emit_pxor_xmm_xmm(code, XMM0);
16+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
17+ } else {
18+ uint64_t bits;
19+ __builtin_memcpy(&bits, &v, sizeof(bits));
20+ emit_mov_reg_imm64(code, REG_RAX, bits);
21+ }
22+ emit_push_reg(code, REG_RAX);
23+}
24+
25+static void emit_write_or_append(int append) {
26+ codegen_expression(); // path
27+ expect(TOKEN_COMMA, "expected ',' after path argument");
28+ codegen_expression(); // content
29+ expect(TOKEN_RPAREN, "expected ')' after arguments");
30+
31+ emit_pop_reg(code, REG_RSI); // content payload
32+ emit_pop_reg(code, REG_RBX); // ignored
33+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // content length
34+
35+ int32_t base = higher_order_scratch_offset();
36+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RSI); // content_ptr
37+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RDX); // content_len
38+
39+ emit_pop_reg(code, REG_RSI); // path payload
40+ emit_pop_reg(code, REG_RBX); // ignored
41+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // path length
42+ filepath_emit_nullterm(code); // RAX = null-terminated path buffer
43+
44+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
45+ platform_emit_open_write(code, append);
46+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // fd
47+
48+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
49+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 24);
50+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 32);
51+ platform_emit_write_bytes(code);
52+
53+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 40); // fd
54+ platform_emit_close(code);
55+
56+ push_number(1.0);
57+}
58+
59+void codegen_builtin_write_file(void) { emit_write_or_append(0); }
60+void codegen_builtin_append_file(void) { emit_write_or_append(1); }
61+
62+void codegen_builtin_file_exists(void) {
63+ emit_pop_reg(code, REG_RSI); // path payload
64+ emit_pop_reg(code, REG_RBX); // ignored
65+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length
66+ filepath_emit_nullterm(code); // RAX = null-terminated path buffer
67+
68+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
69+ platform_emit_open_read(code);
70+
71+ emit_cmp_reg_imm32(code, REG_RAX, 0);
72+ int not_found = emit_jcc_rel32(code, COND_LT);
73+
74+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);
75+ platform_emit_close(code);
76+ push_number(1.0);
77+ int done = emit_jmp_rel32(code);
78+
79+ emit_patch_jump(code, not_found);
80+ push_number(0.0);
81+
82+ emit_patch_jump(code, done);
83+}

kiln/src/codegen/write_file_builtins.dadded

+15−0
@@ -0,0 +1,15 @@
1+src/codegen/write_file_builtins.o: src/codegen/write_file_builtins.c \
2+ src/codegen/file_builtins.h src/codegen/file_path.h src/codegen/emit.h \
3+ src/codegen/expr.h src/lexer/lexer.h src/codegen/emit_sse.h \
4+ src/codegen/value.h src/codegen/platform_file.h src/parser/parser.h \
5+ src/parser/vars.h
6+src/codegen/file_builtins.h:
7+src/codegen/file_path.h:
8+src/codegen/emit.h:
9+src/codegen/expr.h:
10+src/lexer/lexer.h:
11+src/codegen/emit_sse.h:
12+src/codegen/value.h:
13+src/codegen/platform_file.h:
14+src/parser/parser.h:
15+src/parser/vars.h:

kiln/src/diagnostics/diagnostics.cadded

+126−0
@@ -0,0 +1,126 @@
1+#define _POSIX_C_SOURCE 200809L
2+#include <stdio.h>
3+#include <string.h>
4+#include <unistd.h>
5+#include "diagnostics/diagnostics.h"
6+
7+static const char *g_source = NULL;
8+static const char *g_filename = "source";
9+
10+#define COL_RED "\033[38;5;203m"
11+#define COL_CYAN "\033[38;5;117m"
12+#define COL_GRAY "\033[38;5;242m"
13+#define COL_GREEN "\033[38;5;150m"
14+#define COL_RESET "\033[0m"
15+#define COL_BOLD "\033[1m"
16+
17+void diagnostics_set_source(const char *source, const char *filename) {
18+ g_source = source;
19+ g_filename = filename;
20+}
21+
22+DiagnosticsSource diagnostics_save_source(void) {
23+ DiagnosticsSource saved;
24+ saved.source = g_source;
25+ saved.filename = g_filename;
26+ return saved;
27+}
28+
29+void diagnostics_restore_source(DiagnosticsSource saved) {
30+ g_source = saved.source;
31+ g_filename = saved.filename;
32+}
33+
34+static int use_color(void) {
35+ return isatty(fileno(stderr));
36+}
37+
38+// Computes the TRUE line/column by scanning the original source buffer from
39+// the start, rather than trusting any resettable per-stage line counter.
40+static void compute_line_col(const char *pos, int *line, int *col) {
41+ int l = 1, c = 1;
42+ const char *p = g_source;
43+ while (p < pos && *p) {
44+ if (*p == '\n') { l++; c = 1; } else { c++; }
45+ p++;
46+ }
47+ *line = l;
48+ *col = c;
49+}
50+
51+static void get_source_line(const char *pos, const char **line_start, int *line_len) {
52+ const char *start = pos;
53+ while (start > g_source && start[-1] != '\n') start--;
54+ const char *end = pos;
55+ while (*end && *end != '\n') end++;
56+ *line_start = start;
57+ *line_len = (int)(end - start);
58+}
59+
60+static int has_prefix(const char *message, const char *prefix) {
61+ return strncmp(message, prefix, strlen(prefix)) == 0;
62+}
63+
64+static const char *lookup_hint(const char *message) {
65+ if (has_prefix(message, "undefined variable")) return "did you forget to declare it with `let`?";
66+ if (has_prefix(message, "undefined function")) return "check the function name is spelled correctly";
67+ if (has_prefix(message, "index out of bounds")) return "check the array's length before indexing";
68+ if (has_prefix(message, "key not found")) return "use `has(map, key)` to check before accessing";
69+ if (has_prefix(message, "could not open file")) return "check the file path is correct";
70+ if (has_prefix(message, "type error")) return "check the value's type before using it this way";
71+ if (has_prefix(message, "could not resolve")) return "check the import path is correct and the file exists";
72+ if (has_prefix(message, "circular import")) return "break the cycle by removing one of the imports in the chain shown above";
73+ if (has_prefix(message, "namespace")) return "give one of the colliding files/libraries a different name, or move one out of the way";
74+ if (strstr(message, "expected") != NULL) return "check for a missing token nearby, like `;`, `)`, or `}`";
75+ return NULL;
76+}
77+
78+void diagnostics_report(const char *label, const char *message, const char *pos, int len) {
79+ int color = use_color();
80+ if (len <= 0) len = 1;
81+
82+ if (!g_source || !pos) {
83+ fprintf(stderr, "%s: %s\n", label, message);
84+ return;
85+ }
86+
87+ int line, col;
88+ compute_line_col(pos, &line, &col);
89+ const char *line_start; int line_len;
90+ get_source_line(pos, &line_start, &line_len);
91+
92+ char linenum_buf[16];
93+ snprintf(linenum_buf, sizeof(linenum_buf), "%d", line);
94+ int gutter_width = (int)strlen(linenum_buf);
95+
96+ if (color) {
97+ fprintf(stderr, COL_BOLD COL_RED "%s" COL_RESET COL_BOLD ": %s" COL_RESET "\n", label, message);
98+ fprintf(stderr, COL_CYAN " --> %s:%d:%d" COL_RESET "\n", g_filename, line, col);
99+ fprintf(stderr, COL_GRAY "%*s |" COL_RESET "\n", gutter_width, "");
100+ fprintf(stderr, COL_GRAY "%s |" COL_RESET " %.*s\n", linenum_buf, line_len, line_start);
101+ fprintf(stderr, COL_GRAY "%*s |" COL_RESET " ", gutter_width, "");
102+ for (int i = 1; i < col; i++) fputc(' ', stderr);
103+ fprintf(stderr, COL_RED);
104+ for (int i = 0; i < len; i++) fputc('^', stderr);
105+ fprintf(stderr, COL_RESET "\n");
106+ const char *hint = lookup_hint(message);
107+ if (hint) {
108+ fprintf(stderr, COL_GRAY "%*s |" COL_RESET "\n", gutter_width, "");
109+ fprintf(stderr, " = " COL_GREEN COL_BOLD "help" COL_RESET ": %s\n", hint);
110+ }
111+ } else {
112+ fprintf(stderr, "%s: %s\n", label, message);
113+ fprintf(stderr, " --> %s:%d:%d\n", g_filename, line, col);
114+ fprintf(stderr, "%*s |\n", gutter_width, "");
115+ fprintf(stderr, "%s | %.*s\n", linenum_buf, line_len, line_start);
116+ fprintf(stderr, "%*s | ", gutter_width, "");
117+ for (int i = 1; i < col; i++) fputc(' ', stderr);
118+ for (int i = 0; i < len; i++) fputc('^', stderr);
119+ fputc('\n', stderr);
120+ const char *hint = lookup_hint(message);
121+ if (hint) {
122+ fprintf(stderr, "%*s |\n", gutter_width, "");
123+ fprintf(stderr, " = help: %s\n", hint);
124+ }
125+ }
126+}

kiln/src/diagnostics/diagnostics.dadded

+3−0
@@ -0,0 +1,3 @@
1+src/diagnostics/diagnostics.o: src/diagnostics/diagnostics.c \
2+ src/diagnostics/diagnostics.h
3+src/diagnostics/diagnostics.h:

kiln/src/diagnostics/diagnostics.hadded

+26−0
@@ -0,0 +1,26 @@
1+#ifndef ASH_DIAGNOSTICS_H
2+#define ASH_DIAGNOSTICS_H
3+
4+// Rust-style colorized error reporting, shared by every stage (compile-time
5+// and runtime) that needs to point at a spot in the original source text.
6+
7+void diagnostics_set_source(const char *source, const char *filename);
8+
9+// pos/len locate the offending span in the source buffer passed to
10+// diagnostics_set_source(); len <= 0 is treated as a single-character span.
11+void diagnostics_report(const char *label, const char *message,
12+ const char *pos, int len);
13+
14+typedef struct {
15+ const char *source;
16+ const char *filename;
17+} DiagnosticsSource;
18+
19+// Saves the currently-active (source, filename) pair, so imports.c can
20+// point diagnostics at an imported file's own buffer while recursively
21+// parsing it, then restore the importing file's pair afterward -- mirrors
22+// lexer.h's LexerState save/restore.
23+DiagnosticsSource diagnostics_save_source(void);
24+void diagnostics_restore_source(DiagnosticsSource saved);
25+
26+#endif

kiln/src/elf/elf_dynamic.cadded

+183−0
@@ -0,0 +1,183 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include <sys/stat.h>
5+#include "elf/elf_dynamic.h"
6+
7+// Hand-written dynamically-linked ELF64 executable: PT_INTERP so the
8+// kernel hands off to the REAL system dynamic linker, PT_DYNAMIC with
9+// DT_NEEDED=libkilnrt.so, a SysV-hash-format .hash + .dynsym + .dynstr
10+// (no section headers, matching this project's "tiny ELF" philosophy --
11+// ld.so only ever reads PT_DYNAMIC and what it points at, never section
12+// headers), and a GOT filled in by ld.so's own relocation processing
13+// before this program's own code ever runs (eager/non-lazy binding via
14+// R_X86_64_GLOB_DAT relocations -- simpler and just as correct as a lazy
15+// PLT stub sequence for kiln's purposes, and far less to get subtly
16+// wrong). See elf_dynamic.h for why .dynstr specifically is written
17+// AFTER the code rather than at a fixed offset.
18+
19+static const char *const runtime_names[] = {
20+#define RUNTIME_EXPORT(name, str) str,
21+#include "codegen/runtime_exports.def"
22+#undef RUNTIME_EXPORT
23+};
24+
25+uint64_t elf_dynamic_got_addr(RuntimeImport which) {
26+ return KILN_DYN_GOT_ADDR + 8ULL * (uint64_t)which;
27+}
28+
29+static void put8(CodeBuf *f, uint8_t v) {
30+ if (f->count >= f->capacity) {
31+ f->capacity = f->capacity == 0 ? 256 : f->capacity * 2;
32+ f->code = realloc(f->code, (size_t)f->capacity);
33+ }
34+ f->code[f->count++] = v;
35+}
36+static void put16(CodeBuf *f, uint16_t v) { for (int i = 0; i < 2; i++) put8(f, (uint8_t)(v >> (8 * i))); }
37+static void put32(CodeBuf *f, uint32_t v) { for (int i = 0; i < 4; i++) put8(f, (uint8_t)(v >> (8 * i))); }
38+static void put64(CodeBuf *f, uint64_t v) { for (int i = 0; i < 8; i++) put8(f, (uint8_t)(v >> (8 * i))); }
39+static void put_zeros(CodeBuf *f, uint32_t n) { for (uint32_t i = 0; i < n; i++) put8(f, 0); }
40+static void put_str(CodeBuf *f, const char *s) { for (const char *p = s; *p; p++) put8(f, (uint8_t)*p); put8(f, 0); }
41+
42+// Standard SysV ELF hash (elf_hash), the same algorithm every real
43+// DT_HASH-format .hash table in the wild uses -- ld.so implements this
44+// exact function to look symbols up, so this has to match it precisely.
45+static uint32_t elf_hash(const char *name) {
46+ uint32_t h = 0, g;
47+ for (const unsigned char *p = (const unsigned char *)name; *p; p++) {
48+ h = (h << 4) + *p;
49+ g = h & 0xf0000000u;
50+ if (g) h ^= g >> 24;
51+ h &= ~g;
52+ }
53+ return h;
54+}
55+
56+int elf_write_dynamic_executable(const char *path, const CodeBuf *machine_code) {
57+ // ---- pre-compute .dynstr layout (written after the code, but its
58+ // internal offsets are needed now for dynsym/dynamic/rela) ----
59+ uint32_t dynstr_off[RUNTIME_IMPORT_COUNT_];
60+ uint32_t cursor = 1; // leading empty string at offset 0
61+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) { dynstr_off[i] = cursor; cursor += (uint32_t)strlen(runtime_names[i]) + 1; }
62+ uint32_t soname_off = cursor; cursor += (uint32_t)strlen(KILN_LIBKILNRT_SONAME) + 1;
63+ uint32_t rpath_off = cursor; cursor += (uint32_t)strlen(KILN_RPATH) + 1;
64+ uint32_t dynstr_size = cursor;
65+
66+ uint64_t code_start_file_off = KILN_DYN_CODE_START_OFFSET;
67+ uint64_t file_total = code_start_file_off + (uint64_t)machine_code->count + dynstr_size;
68+ uint64_t entry = KILN_LOAD_BASE + code_start_file_off;
69+ uint64_t dynstr_addr = KILN_LOAD_BASE + code_start_file_off + (uint64_t)machine_code->count;
70+
71+ CodeBuf f;
72+ code_init(&f);
73+
74+ // ---- Elf64_Ehdr ----
75+ put8(&f, 0x7f); put8(&f, 'E'); put8(&f, 'L'); put8(&f, 'F');
76+ put8(&f, 2); put8(&f, 1); put8(&f, 1); put8(&f, 0);
77+ put_zeros(&f, 8);
78+ put16(&f, 2); // e_type = ET_EXEC
79+ put16(&f, 62); // e_machine = EM_X86_64
80+ put32(&f, 1);
81+ put64(&f, entry);
82+ put64(&f, KILN_EHDR_SIZE);
83+ put64(&f, 0);
84+ put32(&f, 0);
85+ put16(&f, KILN_EHDR_SIZE);
86+ put16(&f, KILN_PHDR_SIZE);
87+ put16(&f, 3); // e_phnum: LOAD, INTERP, DYNAMIC
88+ put16(&f, 0); put16(&f, 0); put16(&f, 0);
89+
90+ // ---- Elf64_Phdr[0]: PT_LOAD, whole file, RWX (matches this
91+ // project's existing single-segment ELF philosophy) ----
92+ put32(&f, 1); put32(&f, 7);
93+ put64(&f, 0); put64(&f, KILN_LOAD_BASE); put64(&f, KILN_LOAD_BASE);
94+ put64(&f, file_total); put64(&f, file_total); put64(&f, 0x1000);
95+
96+ // ---- Elf64_Phdr[1]: PT_INTERP ----
97+ uint64_t interp_file_off = KILN_EHDR_SIZE + 3u * KILN_PHDR_SIZE;
98+ put32(&f, 3); put32(&f, 4); // PT_INTERP, PF_R
99+ put64(&f, interp_file_off); put64(&f, KILN_DYN_INTERP_ADDR); put64(&f, KILN_DYN_INTERP_ADDR);
100+ put64(&f, KILN_DYN_INTERP_SIZE); put64(&f, KILN_DYN_INTERP_SIZE); put64(&f, 1);
101+
102+ // ---- Elf64_Phdr[2]: PT_DYNAMIC ----
103+ uint64_t dyn_file_off = KILN_DYN_TABLE_ADDR - KILN_LOAD_BASE;
104+ put32(&f, 2); put32(&f, 6); // PT_DYNAMIC, PF_R|PF_W
105+ put64(&f, dyn_file_off); put64(&f, KILN_DYN_TABLE_ADDR); put64(&f, KILN_DYN_TABLE_ADDR);
106+ put64(&f, KILN_DYN_TABLE_SIZE); put64(&f, KILN_DYN_TABLE_SIZE); put64(&f, 8);
107+
108+ // ---- .interp ----
109+ put_str(&f, KILN_INTERP_STR);
110+
111+ // ---- .hash (SysV DT_HASH format: nbucket, nchain, bucket[], chain[]) ----
112+ uint32_t buckets[KILN_DYN_NBUCKET]; for (unsigned i = 0; i < KILN_DYN_NBUCKET; i++) buckets[i] = 0;
113+ uint32_t chains[KILN_DYN_NSYMS]; for (unsigned i = 0; i < KILN_DYN_NSYMS; i++) chains[i] = 0;
114+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) {
115+ uint32_t sym_index = (uint32_t)i + 1;
116+ uint32_t b = elf_hash(runtime_names[i]) % KILN_DYN_NBUCKET;
117+ chains[sym_index] = buckets[b];
118+ buckets[b] = sym_index;
119+ }
120+ put32(&f, KILN_DYN_NBUCKET); put32(&f, KILN_DYN_NSYMS);
121+ for (unsigned i = 0; i < KILN_DYN_NBUCKET; i++) put32(&f, buckets[i]);
122+ for (unsigned i = 0; i < KILN_DYN_NSYMS; i++) put32(&f, chains[i]);
123+
124+ // ---- .dynsym: null entry + one UNDEFINED entry per imported function
125+ // (st_shndx=SHN_UNDEF=0 -- "defined elsewhere", exactly what tells
126+ // ld.so to go resolve it against a NEEDED library rather than treat
127+ // it as locally defined) ----
128+ put32(&f, 0); put8(&f, 0); put8(&f, 0); put16(&f, 0); put64(&f, 0); put64(&f, 0); // null symbol
129+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) {
130+ put32(&f, dynstr_off[i]); // st_name
131+ put8(&f, (1 << 4) | 2); // st_info: STB_GLOBAL | STT_FUNC
132+ put8(&f, 0); // st_other
133+ put16(&f, 0); // st_shndx = SHN_UNDEF
134+ put64(&f, 0); // st_value
135+ put64(&f, 0); // st_size
136+ }
137+
138+ // ---- GOT: zeroed, ld.so fills each slot via the RELA entries below ----
139+ put_zeros(&f, KILN_DYN_GOT_SIZE);
140+
141+ // ---- .dynamic ----
142+ #define DT(tag, val) do { put64(&f, (uint64_t)(tag)); put64(&f, (uint64_t)(val)); } while (0)
143+ DT(1, soname_off); // DT_NEEDED (index into .dynstr)
144+ DT(15, rpath_off); // DT_RPATH
145+ DT(4, KILN_DYN_HASH_ADDR); // DT_HASH
146+ DT(5, dynstr_addr); // DT_STRTAB (after the code -- depends on its length)
147+ DT(6, KILN_DYN_SYMTAB_ADDR); // DT_SYMTAB
148+ DT(10, dynstr_size); // DT_STRSZ
149+ DT(11, KILN_DYN_SYMENT_SIZE); // DT_SYMENT
150+ DT(7, KILN_DYN_RELA_ADDR); // DT_RELA
151+ DT(8, KILN_DYN_RELA_SIZE); // DT_RELASZ
152+ DT(9, KILN_DYN_RELAENT_SIZE); // DT_RELAENT
153+ DT(0, 0); // DT_NULL
154+ #undef DT
155+
156+ // ---- .rela.dyn: one R_X86_64_GLOB_DAT per GOT slot ----
157+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) {
158+ uint64_t sym_index = (uint64_t)i + 1;
159+ uint64_t r_info = (sym_index << 32) | 6ULL; // R_X86_64_GLOB_DAT = 6
160+ put64(&f, KILN_DYN_GOT_ADDR + 8ULL * (uint64_t)i); // r_offset
161+ put64(&f, r_info);
162+ put64(&f, 0); // r_addend
163+ }
164+
165+ put_zeros(&f, KILN_DYN_GLOBALS_SIZE);
166+
167+ for (int i = 0; i < machine_code->count; i++) put8(&f, machine_code->code[i]);
168+
169+ // ---- .dynstr tail ----
170+ put8(&f, 0); // offset 0 = empty string
171+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put_str(&f, runtime_names[i]);
172+ put_str(&f, KILN_LIBKILNRT_SONAME);
173+ put_str(&f, KILN_RPATH);
174+
175+ FILE *out = fopen(path, "wb");
176+ if (!out) { fprintf(stderr, "kiln: could not open '%s' for writing\n", path); free(f.code); return 1; }
177+ size_t written = fwrite(f.code, 1, (size_t)f.count, out);
178+ fclose(out);
179+ free(f.code);
180+ if (written != (size_t)f.count) { fprintf(stderr, "kiln: short write to '%s'\n", path); return 1; }
181+ if (chmod(path, 0755) != 0) { fprintf(stderr, "kiln: could not chmod '%s' executable\n", path); return 1; }
182+ return 0;
183+}

kiln/src/elf/elf_dynamic.dadded

+10−0
@@ -0,0 +1,10 @@
1+src/elf/elf_dynamic.o: src/elf/elf_dynamic.c src/elf/elf_dynamic.h \
2+ src/codegen/emit.h src/codegen/runtime_import_enum.h \
3+ src/codegen/runtime_exports.def src/elf/elf_writer.h \
4+ src/codegen/runtime_layout.h
5+src/elf/elf_dynamic.h:
6+src/codegen/emit.h:
7+src/codegen/runtime_import_enum.h:
8+src/codegen/runtime_exports.def:
9+src/elf/elf_writer.h:
10+src/codegen/runtime_layout.h:

kiln/src/elf/elf_dynamic.hadded

+77−0
@@ -0,0 +1,77 @@
1+#ifndef KILN_ELF_DYNAMIC_H
2+#define KILN_ELF_DYNAMIC_H
3+#include "codegen/emit.h"
4+#include "codegen/runtime_import_enum.h"
5+#include "elf/elf_writer.h"
6+
7+// Layout for a --link=shared kiln executable: real dynamic linking, the
8+// kernel hands off to the system's actual /lib64/ld-linux-x86-64.so.2 (a
9+// PT_INTERP segment says so), which reads PT_DYNAMIC, finds
10+// libkilnrt.so via DT_NEEDED+DT_RPATH, and resolves each imported
11+// runtime function into a GOT slot this file also lays out -- see
12+// elf_dynamic.c's header comment for the full section-by-section layout
13+// and elf_so_writer.h for the library side of the same relationship.
14+//
15+// Design validated against the REAL system dynamic linker with a
16+// hand-written prototype before any of this was ported into kiln itself
17+// (two hand-built ELF files, one ET_DYN "library" and one ET_EXEC
18+// "executable" with PT_INTERP/PT_DYNAMIC/GOT/RELA, loaded and resolved
19+// correctly by /lib64/ld-linux-x86-64.so.2) -- this header encodes that
20+// same validated structure, not a first attempt.
21+#define KILN_RPATH "/usr/local/lib/kiln"
22+#define KILN_LIBKILNRT_SONAME "libkilnrt.so"
23+#define KILN_INTERP_STR "/lib64/ld-linux-x86-64.so.2"
24+
25+// nsyms includes the mandatory null 0th symbol.
26+#define KILN_DYN_NSYMS ((unsigned)RUNTIME_IMPORT_COUNT_ + 1u)
27+#define KILN_DYN_NBUCKET KILN_DYN_NSYMS
28+
29+// Section order: everything whose address codegen (or another fixed-size
30+// section) needs as a compile-time constant comes first, in FIXED-SIZE
31+// pieces only (sizes depending on RUNTIME_IMPORT_COUNT_, never on string
32+// lengths); .dynstr -- the one variable-length piece -- goes after the
33+// code instead, exactly like pe_writer.h's hint/name tail, computed at
34+// write time in elf_dynamic.c once the final code length is known.
35+// [ehdr][phdrs][interp][hash][dynsym][GOT][dynamic][rela][globals][code][dynstr]
36+#define KILN_DYN_INTERP_ADDR (KILN_LOAD_BASE + KILN_EHDR_SIZE + 3u * KILN_PHDR_SIZE)
37+#define KILN_DYN_INTERP_SIZE (sizeof(KILN_INTERP_STR)) // includes NUL
38+
39+#define KILN_DYN_HASH_ADDR (KILN_DYN_INTERP_ADDR + KILN_DYN_INTERP_SIZE)
40+#define KILN_DYN_HASH_SIZE (8u + 4u * KILN_DYN_NBUCKET + 4u * KILN_DYN_NSYMS)
41+
42+#define KILN_DYN_SYMENT_SIZE 24u
43+#define KILN_DYN_SYMTAB_ADDR (KILN_DYN_HASH_ADDR + KILN_DYN_HASH_SIZE)
44+#define KILN_DYN_SYMTAB_SIZE (KILN_DYN_NSYMS * KILN_DYN_SYMENT_SIZE)
45+
46+// GOT: one 8-byte slot per imported runtime function, zeroed at rest,
47+// filled in by ld.so with the resolved absolute address before this
48+// program's own entry point ever runs. Fixed size/offset regardless of
49+// symbol NAME lengths, exactly like pe_writer.h's IAT -- codegen can
50+// address a slot as a compile-time constant.
51+#define KILN_DYN_GOT_ADDR (KILN_DYN_SYMTAB_ADDR + KILN_DYN_SYMTAB_SIZE)
52+#define KILN_DYN_GOT_SIZE (8u * (unsigned)RUNTIME_IMPORT_COUNT_)
53+
54+#define KILN_DYN_ENTRY_SIZE 16u // Elf64_Dyn: {Elf64_Sxword d_tag; union d_val/d_ptr;}
55+#define KILN_DYN_TABLE_NENT_TOTAL 10u // NEEDED,RPATH,HASH,STRTAB,SYMTAB,STRSZ,SYMENT,RELA,RELASZ,RELAENT,NULL(11, see .c)
56+#define KILN_DYN_TABLE_ADDR (KILN_DYN_GOT_ADDR + KILN_DYN_GOT_SIZE)
57+#define KILN_DYN_TABLE_SIZE ((KILN_DYN_TABLE_NENT_TOTAL + 1u) * KILN_DYN_ENTRY_SIZE) // +1 for the DT_NULL terminator
58+
59+#define KILN_DYN_RELAENT_SIZE 24u // Elf64_Rela: {Elf64_Addr; Elf64_Xword info; Elf64_Sxword addend}
60+#define KILN_DYN_RELA_ADDR (KILN_DYN_TABLE_ADDR + KILN_DYN_TABLE_SIZE)
61+#define KILN_DYN_RELA_SIZE (KILN_DYN_RELAENT_SIZE * (unsigned)RUNTIME_IMPORT_COUNT_)
62+
63+#define KILN_DYN_GLOBALS_ADDR (KILN_DYN_RELA_ADDR + KILN_DYN_RELA_SIZE)
64+#define KILN_DYN_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE)
65+#define KILN_DYN_CODE_START_OFFSET (KILN_DYN_GLOBALS_ADDR + KILN_DYN_GLOBALS_SIZE - KILN_LOAD_BASE)
66+
67+// Absolute VA of `which`'s GOT slot -- loaded via a compile-time-known
68+// immediate (kiln's executables are always fixed-base, same reasoning as
69+// pe_import_addr) -- deref it and call through the result.
70+uint64_t elf_dynamic_got_addr(RuntimeImport which);
71+
72+// Writes `machine_code` as a dynamically-linked ELF64 executable at
73+// `path`, expecting libkilnrt.so to be installed at KILN_RPATH by the
74+// time this program is run (see elf_so_writer.h). Returns 0 on success.
75+int elf_write_dynamic_executable(const char *path, const CodeBuf *machine_code);
76+
77+#endif

kiln/src/elf/elf_dynamic_call.cadded

+5−0
@@ -0,0 +1,5 @@
1+#include "elf/elf_dynamic_call.h"
2+
3+void elf_dynamic_call(CodeBuf *code, RuntimeImport which) {
4+ emit_call_abs32(code, (uint32_t)elf_dynamic_got_addr(which));
5+}

kiln/src/elf/elf_dynamic_call.dadded

+11−0
@@ -0,0 +1,11 @@
1+src/elf/elf_dynamic_call.o: src/elf/elf_dynamic_call.c \
2+ src/elf/elf_dynamic_call.h src/elf/elf_dynamic.h src/codegen/emit.h \
3+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
4+ src/elf/elf_writer.h src/codegen/runtime_layout.h
5+src/elf/elf_dynamic_call.h:
6+src/elf/elf_dynamic.h:
7+src/codegen/emit.h:
8+src/codegen/runtime_import_enum.h:
9+src/codegen/runtime_exports.def:
10+src/elf/elf_writer.h:
11+src/codegen/runtime_layout.h:

kiln/src/elf/elf_dynamic_call.hadded

+17−0
@@ -0,0 +1,17 @@
1+#ifndef KILN_ELF_DYNAMIC_CALL_H
2+#define KILN_ELF_DYNAMIC_CALL_H
3+#include "elf/elf_dynamic.h"
4+
5+// Calls `which` through its GOT slot (filled in by ld.so before this
6+// program's own code ever runs) -- a single `call [addr]` touching no
7+// register, so it's safe to insert in front of ANY of the runtime
8+// routines regardless of which registers that specific routine's own
9+// argument convention uses (heap_alloc: RDI/RAX, print_top: RAX/RBX,
10+// bytes_copy: RDI/RBX/RDX, ...). Used for RAISE too even though the
11+// static-link build reaches it via a JMP, not a CALL (it never returns):
12+// the extra return address CALL pushes is simply abandoned, unreachable
13+// dead stack space either way, since nothing after a raise site ever
14+// falls through -- harmless, and not worth a second code path here.
15+void elf_dynamic_call(CodeBuf *code, RuntimeImport which);
16+
17+#endif

kiln/src/elf/elf_so_writer.cadded

+179−0
@@ -0,0 +1,179 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include <sys/stat.h>
5+#include "elf/elf_so_writer.h"
6+#include "elf/elf_dynamic.h"
7+#include "codegen/heap.h"
8+#include "codegen/bytes.h"
9+#include "codegen/string_alloc.h"
10+#include "codegen/print_int.h"
11+#include "codegen/errors.h"
12+#include "target.h"
13+
14+static const char *const export_names[] = {
15+#define RUNTIME_EXPORT(name, str) str,
16+#include "codegen/runtime_exports.def"
17+#undef RUNTIME_EXPORT
18+};
19+
20+static void put8(CodeBuf *f, uint8_t v) {
21+ if (f->count >= f->capacity) {
22+ f->capacity = f->capacity == 0 ? 256 : f->capacity * 2;
23+ f->code = realloc(f->code, (size_t)f->capacity);
24+ }
25+ f->code[f->count++] = v;
26+}
27+static void put16(CodeBuf *f, uint16_t v) { for (int i = 0; i < 2; i++) put8(f, (uint8_t)(v >> (8 * i))); }
28+static void put32(CodeBuf *f, uint32_t v) { for (int i = 0; i < 4; i++) put8(f, (uint8_t)(v >> (8 * i))); }
29+static void put64(CodeBuf *f, uint64_t v) { for (int i = 0; i < 8; i++) put8(f, (uint8_t)(v >> (8 * i))); }
30+static void put_zeros(CodeBuf *f, uint32_t n) { for (uint32_t i = 0; i < n; i++) put8(f, 0); }
31+static void put_str(CodeBuf *f, const char *s) { for (const char *p = s; *p; p++) put8(f, (uint8_t)*p); put8(f, 0); }
32+
33+static uint32_t elf_hash(const char *name) {
34+ uint32_t h = 0, g;
35+ for (const unsigned char *p = (const unsigned char *)name; *p; p++) {
36+ h = (h << 4) + *p;
37+ g = h & 0xf0000000u;
38+ if (g) h ^= g >> 24;
39+ h &= ~g;
40+ }
41+ return h;
42+}
43+
44+int elf_write_shared_runtime(const char *path) {
45+ // ---- compile the runtime itself, position-independent, into its own
46+ // fresh CodeBuf. Link mode STAYS shared (main.c sets it before
47+ // calling this) so codegen/layout.c resolves globals addresses using
48+ // the shared-mode executable layout -- but kiln_set_compiling_so(1)
49+ // makes each routine's OWN call sites (e.g. string_alloc_prefixed
50+ // calling heap_emit_alloc) resolve as ordinary same-buffer relative
51+ // calls instead of GOT calls back out to this very library. These
52+ // are two different questions (see target.h's comment) that a first
53+ // pass at this conflated into one flag, and testing caught it: doing
54+ // so ALSO silently broke the globals address computation. ----
55+ kiln_set_compiling_so(1);
56+
57+ CodeBuf runtime;
58+ code_init(&runtime);
59+ heap_emit_alloc_routine_only(&runtime);
60+ bytes_emit_copy_routine_only(&runtime);
61+ string_alloc_emit_prefixed_routine_only(&runtime);
62+ print_emit_top_routine_only(&runtime);
63+ errors_emit_raise_routine_only(&runtime);
64+
65+ int offsets[RUNTIME_IMPORT_COUNT_];
66+ offsets[RUNTIME_IMPORT_HEAP_ALLOC] = heap_alloc_routine_offset();
67+ offsets[RUNTIME_IMPORT_BYTES_COPY] = bytes_copy_routine_offset();
68+ offsets[RUNTIME_IMPORT_STRING_ALLOC_PREFIXED] = string_alloc_prefixed_routine_offset();
69+ offsets[RUNTIME_IMPORT_PRINT_TOP] = print_top_routine_offset();
70+ offsets[RUNTIME_IMPORT_RAISE] = errors_raise_routine_offset();
71+
72+ kiln_set_compiling_so(0);
73+
74+ // ---- lay out the .so file: [ehdr][phdr LOAD][phdr DYNAMIC]
75+ // [hash][dynsym][dynstr][dynamic][code], all offsets/addresses
76+ // relative to 0 (ET_DYN: ld.so adds its chosen load bias to
77+ // everything) ----
78+ unsigned nsyms = (unsigned)RUNTIME_IMPORT_COUNT_ + 1u;
79+ unsigned nbucket = nsyms;
80+
81+ uint32_t dynstr_off[RUNTIME_IMPORT_COUNT_];
82+ uint32_t cursor = 1; // leading empty string
83+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) { dynstr_off[i] = cursor; cursor += (uint32_t)strlen(export_names[i]) + 1; }
84+ uint32_t soname_off = cursor; cursor += (uint32_t)strlen(KILN_LIBKILNRT_SONAME) + 1;
85+ uint32_t dynstr_size = cursor;
86+
87+ const uint32_t ehdr_size = KILN_EHDR_SIZE, phdr_size = KILN_PHDR_SIZE;
88+ uint32_t hash_off = ehdr_size + 2u * phdr_size;
89+ uint32_t hash_size = 8u + 4u * nbucket + 4u * nsyms;
90+ uint32_t dynsym_off = hash_off + hash_size;
91+ uint32_t dynsym_size = 24u * nsyms;
92+ uint32_t dynstr_file_off = dynsym_off + dynsym_size;
93+ uint32_t dynamic_off = dynstr_file_off + dynstr_size;
94+ uint32_t dynamic_nent = 7; // SONAME,HASH,STRTAB,SYMTAB,STRSZ,SYMENT,NULL
95+ uint32_t dynamic_size = dynamic_nent * 16u;
96+ uint32_t code_off = dynamic_off + dynamic_size;
97+ uint32_t total_size = code_off + (uint32_t)runtime.count;
98+
99+ CodeBuf f;
100+ code_init(&f);
101+
102+ put8(&f, 0x7f); put8(&f, 'E'); put8(&f, 'L'); put8(&f, 'F');
103+ put8(&f, 2); put8(&f, 1); put8(&f, 1); put8(&f, 0);
104+ put_zeros(&f, 8);
105+ put16(&f, 3); // e_type = ET_DYN
106+ put16(&f, 62);
107+ put32(&f, 1);
108+ put64(&f, 0); // e_entry (none -- a library, not directly executed)
109+ put64(&f, ehdr_size);
110+ put64(&f, 0);
111+ put32(&f, 0);
112+ put16(&f, (uint16_t)ehdr_size);
113+ put16(&f, (uint16_t)phdr_size);
114+ put16(&f, 2); // LOAD, DYNAMIC
115+ put16(&f, 0); put16(&f, 0); put16(&f, 0);
116+
117+ put32(&f, 1); put32(&f, 7); // PT_LOAD, RWX
118+ put64(&f, 0); put64(&f, 0); put64(&f, 0);
119+ put64(&f, total_size); put64(&f, total_size); put64(&f, 0x1000);
120+
121+ put32(&f, 2); put32(&f, 6); // PT_DYNAMIC, PF_R|PF_W
122+ put64(&f, dynamic_off); put64(&f, dynamic_off); put64(&f, dynamic_off);
123+ put64(&f, dynamic_size); put64(&f, dynamic_size); put64(&f, 8);
124+
125+ // .hash
126+ uint32_t *buckets = calloc(nbucket, sizeof(uint32_t));
127+ uint32_t *chains = calloc(nsyms, sizeof(uint32_t));
128+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) {
129+ uint32_t sym_index = (uint32_t)i + 1;
130+ uint32_t b = elf_hash(export_names[i]) % nbucket;
131+ chains[sym_index] = buckets[b];
132+ buckets[b] = sym_index;
133+ }
134+ put32(&f, nbucket); put32(&f, nsyms);
135+ for (unsigned i = 0; i < nbucket; i++) put32(&f, buckets[i]);
136+ for (unsigned i = 0; i < nsyms; i++) put32(&f, chains[i]);
137+ free(buckets); free(chains);
138+
139+ // .dynsym: null + one DEFINED entry per export (st_value = its offset
140+ // within this image, st_shndx=1 -- a nonzero placeholder, since this
141+ // file has no section headers to index; ld.so never needs one).
142+ put32(&f, 0); put8(&f, 0); put8(&f, 0); put16(&f, 0); put64(&f, 0); put64(&f, 0);
143+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) {
144+ put32(&f, dynstr_off[i]);
145+ put8(&f, (1 << 4) | 2); // STB_GLOBAL | STT_FUNC
146+ put8(&f, 0);
147+ put16(&f, 1);
148+ put64(&f, (uint64_t)(code_off + (uint32_t)offsets[i]));
149+ put64(&f, 0);
150+ }
151+
152+ // .dynstr
153+ put8(&f, 0);
154+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put_str(&f, export_names[i]);
155+ put_str(&f, KILN_LIBKILNRT_SONAME);
156+
157+ // .dynamic
158+ #define DT(tag, val) do { put64(&f, (uint64_t)(tag)); put64(&f, (uint64_t)(val)); } while (0)
159+ DT(14, soname_off); // DT_SONAME
160+ DT(4, hash_off); // DT_HASH
161+ DT(5, dynstr_file_off); // DT_STRTAB
162+ DT(6, dynsym_off); // DT_SYMTAB
163+ DT(10, dynstr_size); // DT_STRSZ
164+ DT(11, 24); // DT_SYMENT
165+ DT(0, 0); // DT_NULL
166+ #undef DT
167+
168+ for (int i = 0; i < runtime.count; i++) put8(&f, runtime.code[i]);
169+ free(runtime.code);
170+
171+ FILE *out = fopen(path, "wb");
172+ if (!out) { fprintf(stderr, "kiln: could not open '%s' for writing\n", path); free(f.code); return 1; }
173+ size_t written = fwrite(f.code, 1, (size_t)f.count, out);
174+ fclose(out);
175+ free(f.code);
176+ if (written != (size_t)f.count) { fprintf(stderr, "kiln: short write to '%s'\n", path); return 1; }
177+ if (chmod(path, 0755) != 0) { fprintf(stderr, "kiln: could not chmod '%s'\n", path); return 1; }
178+ return 0;
179+}

kiln/src/elf/elf_so_writer.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/elf/elf_so_writer.o: src/elf/elf_so_writer.c src/elf/elf_so_writer.h \
2+ src/elf/elf_dynamic.h src/codegen/emit.h \
3+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
4+ src/elf/elf_writer.h src/codegen/runtime_layout.h src/codegen/heap.h \
5+ src/codegen/bytes.h src/codegen/string_alloc.h src/codegen/print_int.h \
6+ src/codegen/errors.h src/target.h
7+src/elf/elf_so_writer.h:
8+src/elf/elf_dynamic.h:
9+src/codegen/emit.h:
10+src/codegen/runtime_import_enum.h:
11+src/codegen/runtime_exports.def:
12+src/elf/elf_writer.h:
13+src/codegen/runtime_layout.h:
14+src/codegen/heap.h:
15+src/codegen/bytes.h:
16+src/codegen/string_alloc.h:
17+src/codegen/print_int.h:
18+src/codegen/errors.h:
19+src/target.h:

kiln/src/elf/elf_so_writer.hadded

+12−0
@@ -0,0 +1,12 @@
1+#ifndef KILN_ELF_SO_WRITER_H
2+#define KILN_ELF_SO_WRITER_H
3+
4+// Compiles the runtime (heap/bytes/string_alloc/print/errors-raise) into
5+// a fresh, position-independent CodeBuf and writes it as a real ET_DYN
6+// ELF64 shared object at `path` -- one exported symbol per
7+// codegen/runtime_exports.def entry, loadable by the system's real
8+// dynamic linker (validated structure, see elf/elf_dynamic.h's header
9+// comment). Returns 0 on success.
10+int elf_write_shared_runtime(const char *path);
11+
12+#endif

kiln/src/elf/elf_writer.cadded

+84−0
@@ -0,0 +1,84 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include <sys/stat.h>
5+#include "elf/elf_writer.h"
6+
7+static void put8(CodeBuf *f, uint8_t v) {
8+ if (f->count >= f->capacity) {
9+ f->capacity = f->capacity == 0 ? 256 : f->capacity * 2;
10+ f->code = realloc(f->code, f->capacity);
11+ }
12+ f->code[f->count++] = v;
13+}
14+static void put16(CodeBuf *f, uint16_t v) { for (int i = 0; i < 2; i++) put8(f, (uint8_t)(v >> (8 * i))); }
15+static void put32(CodeBuf *f, uint32_t v) { for (int i = 0; i < 4; i++) put8(f, (uint8_t)(v >> (8 * i))); }
16+static void put64(CodeBuf *f, uint64_t v) { for (int i = 0; i < 8; i++) put8(f, (uint8_t)(v >> (8 * i))); }
17+
18+int elf_write_executable(const char *path, const CodeBuf *machine_code) {
19+ CodeBuf file;
20+ code_init(&file);
21+
22+ uint64_t total_size = KILN_CODE_START_OFFSET + (uint64_t)machine_code->count;
23+ uint64_t entry = KILN_LOAD_BASE + KILN_CODE_START_OFFSET;
24+
25+ // ---- Elf64_Ehdr ----
26+ put8(&file, 0x7f); put8(&file, 'E'); put8(&file, 'L'); put8(&file, 'F');
27+ put8(&file, 2); // ELFCLASS64
28+ put8(&file, 1); // ELFDATA2LSB (little-endian)
29+ put8(&file, 1); // EV_CURRENT
30+ put8(&file, 0); // ELFOSABI_SYSV
31+ for (int i = 0; i < 8; i++) put8(&file, 0); // ABI version + padding, e_ident[9..15]
32+
33+ put16(&file, 2); // e_type = ET_EXEC
34+ put16(&file, 62); // e_machine = EM_X86_64
35+ put32(&file, 1); // e_version = EV_CURRENT
36+ put64(&file, entry); // e_entry
37+ put64(&file, KILN_EHDR_SIZE); // e_phoff (program header follows Ehdr immediately)
38+ put64(&file, 0); // e_shoff (no section headers)
39+ put32(&file, 0); // e_flags
40+ put16(&file, KILN_EHDR_SIZE); // e_ehsize
41+ put16(&file, KILN_PHDR_SIZE); // e_phentsize
42+ put16(&file, 1); // e_phnum
43+ put16(&file, 0); // e_shentsize
44+ put16(&file, 0); // e_shnum
45+ put16(&file, 0); // e_shstrndx
46+
47+ // ---- Elf64_Phdr: one PT_LOAD segment covering the whole file ----
48+ put32(&file, 1); // p_type = PT_LOAD
49+ // p_flags = PF_R | PF_W | PF_X. Milestone 1-4 only needed R|X (code
50+ // never wrote into its own segment); milestone 5's heap bump pointer
51+ // (see elf_writer.h / codegen/heap.c) is a global living inside this
52+ // same segment, so it needs to be writable too. A real compiler would
53+ // split .text (R|X) from .data (R|W) into separate segments -- kiln
54+ // doesn't do that yet, so the whole thing is RWX for now.
55+ put32(&file, 7);
56+ put64(&file, 0); // p_offset
57+ put64(&file, KILN_LOAD_BASE); // p_vaddr
58+ put64(&file, KILN_LOAD_BASE); // p_paddr
59+ put64(&file, total_size); // p_filesz
60+ put64(&file, total_size); // p_memsz
61+ put64(&file, 0x1000); // p_align
62+
63+ // ---- globals (heap_ptr etc.) -- zero-initialized ----
64+ for (int i = 0; i < KILN_GLOBALS_SIZE; i++) put8(&file, 0);
65+
66+ // ---- code ----
67+ for (int i = 0; i < machine_code->count; i++) put8(&file, machine_code->code[i]);
68+
69+ FILE *out = fopen(path, "wb");
70+ if (!out) { fprintf(stderr, "kiln: could not open '%s' for writing\n", path); free(file.code); return 1; }
71+ size_t written = fwrite(file.code, 1, (size_t)file.count, out);
72+ fclose(out);
73+ free(file.code);
74+ if (written != (size_t)file.count) {
75+ fprintf(stderr, "kiln: short write to '%s'\n", path);
76+ return 1;
77+ }
78+
79+ if (chmod(path, 0755) != 0) {
80+ fprintf(stderr, "kiln: could not chmod '%s' executable\n", path);
81+ return 1;
82+ }
83+ return 0;
84+}

kiln/src/elf/elf_writer.dadded

+5−0
@@ -0,0 +1,5 @@
1+src/elf/elf_writer.o: src/elf/elf_writer.c src/elf/elf_writer.h \
2+ src/codegen/emit.h src/codegen/runtime_layout.h
3+src/elf/elf_writer.h:
4+src/codegen/emit.h:
5+src/codegen/runtime_layout.h:

kiln/src/elf/elf_writer.hadded

+51−0
@@ -0,0 +1,51 @@
1+#ifndef KILN_ELF_WRITER_H
2+#define KILN_ELF_WRITER_H
3+#include "codegen/emit.h"
4+#include "codegen/runtime_layout.h"
5+
6+// Layout constants shared with codegen/heap.c: kiln generates a fixed,
7+// non-PIE executable (always loaded at KILN_LOAD_BASE), so absolute
8+// addresses are known at compile time. Milestone 5 needs one such fixed
9+// address for the heap bump pointer (a "global" -- there's no per-call
10+// frame that's reachable from every function, since each call gets its
11+// own fresh RBP), reserved as a small region between the program headers
12+// and the actual code.
13+#define KILN_LOAD_BASE 0x400000ULL // page-aligned, traditional non-PIE x86-64 base
14+#define KILN_EHDR_SIZE 64
15+#define KILN_PHDR_SIZE 56
16+
17+// Milestone 9: the globals region also holds kiln's own hand-rolled
18+// setjmp/longjmp equivalent for try/catch (see codegen/errors.c's raise
19+// routine and parser/try_catch.c) -- a fixed-depth stack of "handler"
20+// records, since this is a freestanding binary with no libc setjmp to
21+// call. Each handler is {saved_rsp, saved_rbp, target_addr,
22+// error_slot_rbp_offset}, 4 x int64. Bounded the same way every other
23+// table in this project is (MAX_KILN_VARS, MAX_KILN_FUNCS, ...) --
24+// exceeding it is a hard (uncatchable) fatal error, not undefined
25+// behavior, since overflowing past it would silently corrupt the
26+// adjacent code segment.
27+// Milestone: argv(). The kernel hands a freshly-started process its
28+// stack laid out as [argc, argv[0], argv[1], ..., NULL, envp...], with
29+// RSP pointing at argc, before any of THIS program's own code runs.
30+// compile_program's very first instruction (before even `mov rbp,rsp`)
31+// saves that original RSP here -- a fixed absolute address, reachable
32+// from any frame later on, since a called function's own RBP is no
33+// longer the original one by the time argv() might be called from
34+// inside it (see codegen/argv_builtin.c).
35+#define KILN_ARGV_SIZE 8
36+
37+#define KILN_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE) // heap_ptr, try_depth, handler stack, argv
38+#define KILN_GLOBALS_ADDR (KILN_LOAD_BASE + KILN_EHDR_SIZE + KILN_PHDR_SIZE)
39+#define KILN_TRY_DEPTH_ADDR (KILN_GLOBALS_ADDR + 8)
40+#define KILN_TRY_HANDLERS_ADDR (KILN_TRY_DEPTH_ADDR + KILN_TRY_DEPTH_SIZE)
41+#define KILN_ARGV_ADDR (KILN_TRY_HANDLERS_ADDR + KILN_TRY_HANDLERS_SIZE)
42+#define KILN_CODE_START_OFFSET (KILN_EHDR_SIZE + KILN_PHDR_SIZE + KILN_GLOBALS_SIZE)
43+
44+// Writes `machine_code` as a minimal, standalone, directly-executable
45+// Linux x86-64 ELF64 binary at `path` and marks it executable (chmod
46+// +x). No section headers at all -- the kernel loader only reads the ELF
47+// header and program headers to run a binary ("tiny ELF" technique).
48+// Returns 0 on success, prints an error and returns nonzero on failure.
49+int elf_write_executable(const char *path, const CodeBuf *machine_code);
50+
51+#endif

kiln/src/lexer/lexer.cadded

+224−0
@@ -0,0 +1,224 @@
1+#include <string.h>
2+#include <ctype.h>
3+#include "lexer/lexer.h"
4+#include "lexer/lexer_import.h"
5+
6+// lex_* (not the bare start/current/line these had as file-static
7+// before) -- shared with lexer_import.c now, and a plain `current` would
8+// collide with parser.h's own global `Token current` at link time.
9+const char *lex_start;
10+const char *lex_current;
11+int lex_line;
12+
13+void lexer_init(const char *source) {
14+ lex_start = source;
15+ lex_current = source;
16+ lex_line = 1;
17+}
18+
19+int is_at_end(void) { return *lex_current == '\0'; }
20+char advance(void) { lex_current++; return lex_current[-1]; }
21+char peek(void) { return *lex_current; }
22+static char peek_next(void) { if (is_at_end()) return '\0'; return lex_current[1]; }
23+
24+static int match(char expected) {
25+ if (is_at_end()) return 0;
26+ if (*lex_current != expected) return 0;
27+ lex_current++;
28+ return 1;
29+}
30+
31+static void skip_whitespace(void) {
32+ for (;;) {
33+ char c = peek();
34+ switch (c) {
35+ case ' ': case '\r': case '\t':
36+ advance();
37+ break;
38+ case '\n':
39+ lex_line++;
40+ advance();
41+ break;
42+ case '/':
43+ if (peek_next() == '/') {
44+ while (peek() != '\n' && !is_at_end()) advance();
45+ } else if (peek_next() == '*') {
46+ advance(); advance(); // consume "/*"
47+ // An unterminated block comment just runs to EOF
48+ // rather than producing a lex error -- the same
49+ // "good enough, not worth a signature change to
50+ // skip_whitespace for" tradeoff line comments
51+ // already make (a `//` with no trailing newline).
52+ while (!is_at_end() && !(peek() == '*' && peek_next() == '/')) {
53+ if (peek() == '\n') lex_line++;
54+ advance();
55+ }
56+ if (!is_at_end()) { advance(); advance(); } // consume "*/"
57+ } else {
58+ return;
59+ }
60+ break;
61+ default:
62+ return;
63+ }
64+ }
65+}
66+
67+static Token make_token(TokenType type) {
68+ Token token;
69+ token.type = type;
70+ token.start = lex_start;
71+ token.length = (int)(lex_current - lex_start);
72+ token.line = lex_line;
73+ return token;
74+}
75+
76+Token error_token(const char *message) {
77+ Token token;
78+ token.type = TOKEN_ERROR;
79+ token.start = message;
80+ token.length = (int)strlen(message);
81+ token.line = lex_line;
82+ return token;
83+}
84+
85+// The lexer's only job here is to decide the token's BOUNDARIES --
86+// primary()'s strtod() call (real libc, running at compile time on the
87+// host, not codegen) does the actual conversion and natively understands
88+// hex ("0x1A") and scientific notation ("1e10", "1.5e-3") already; the
89+// lexer just has to not stop early and hand back a truncated token.
90+// Underscore digit separators (1_000_000) are the one thing strtod
91+// itself doesn't understand -- primary() strips those out of the token
92+// text before calling strtod, see codegen/expr.c.
93+static Token number(void) {
94+ if (lex_start[0] == '0' && (peek() == 'x' || peek() == 'X')) {
95+ advance(); // consume 'x'/'X'
96+ while (isxdigit(peek()) || peek() == '_') advance();
97+ return make_token(TOKEN_NUMBER);
98+ }
99+ while (isdigit(peek()) || peek() == '_') advance();
100+ if (peek() == '.' && isdigit(peek_next())) {
101+ advance(); // consume '.'
102+ while (isdigit(peek()) || peek() == '_') advance();
103+ }
104+ if (peek() == 'e' || peek() == 'E') {
105+ char after = peek_next();
106+ if (isdigit(after) || after == '+' || after == '-') {
107+ advance(); // consume 'e'/'E'
108+ if (peek() == '+' || peek() == '-') advance();
109+ while (isdigit(peek())) advance();
110+ }
111+ }
112+ return make_token(TOKEN_NUMBER);
113+}
114+
115+static Token string_token(void) {
116+ const char *content_start = lex_current;
117+ while (peek() != '"' && !is_at_end()) {
118+ if (peek() == '\\' && peek_next() != '\0') {
119+ advance(); // skip the backslash
120+ advance(); // skip the escaped character itself, so a \" doesn't end the string early
121+ continue;
122+ }
123+ if (peek() == '\n') lex_line++;
124+ advance();
125+ }
126+ if (is_at_end()) return error_token("unterminated string");
127+
128+ int length = (int)(lex_current - content_start);
129+ advance();
130+
131+ Token token;
132+ token.type = TOKEN_STRING;
133+ token.start = content_start;
134+ token.length = length;
135+ token.line = lex_line;
136+ return token;
137+}
138+
139+static Token identifier(void) {
140+ while (isalnum(peek()) || peek() == '_') advance();
141+
142+ 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);
149+ 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);
160+
161+ return make_token(TOKEN_IDENTIFIER);
162+}
163+
164+Token lexer_next_token(void) {
165+ skip_whitespace();
166+ lex_start = lex_current;
167+
168+ if (is_at_end()) return make_token(TOKEN_EOF);
169+
170+ char c = advance();
171+
172+ if (isdigit(c)) return number();
173+ if (isalpha(c) || c == '_') return identifier();
174+ if (c == '"') return string_token();
175+
176+ switch (c) {
177+ case ';': return make_token(TOKEN_SEMICOLON);
178+ case ',': return make_token(TOKEN_COMMA);
179+ case ':': return make_token(TOKEN_COLON);
180+ case '?': return make_token(TOKEN_QUESTION);
181+ case '+':
182+ if (match('+')) return make_token(TOKEN_PLUS_PLUS);
183+ return make_token(match('=') ? TOKEN_PLUS_EQUAL : TOKEN_PLUS);
184+ case '-':
185+ if (match('-')) return make_token(TOKEN_MINUS_MINUS);
186+ return make_token(match('=') ? TOKEN_MINUS_EQUAL : TOKEN_MINUS);
187+ case '*':
188+ if (match('*')) return make_token(TOKEN_STAR_STAR);
189+ return make_token(match('=') ? TOKEN_STAR_EQUAL : TOKEN_STAR);
190+ case '/': return make_token(match('=') ? TOKEN_SLASH_EQUAL : TOKEN_SLASH);
191+ case '%': return make_token(TOKEN_PERCENT);
192+ case '(': return make_token(TOKEN_LPAREN);
193+ case ')': return make_token(TOKEN_RPAREN);
194+ case '{': return make_token(TOKEN_LBRACE);
195+ case '}': return make_token(TOKEN_RBRACE);
196+ case '[': return make_token(TOKEN_LBRACKET);
197+ case ']': return make_token(TOKEN_RBRACKET);
198+ case '=': return make_token(match('=') ? TOKEN_EQUAL_EQUAL : TOKEN_EQUAL);
199+ case '!': return make_token(match('=') ? TOKEN_BANG_EQUAL : TOKEN_BANG);
200+ case '<':
201+ if (match('<')) return make_token(TOKEN_SHL);
202+ return make_token(match('=') ? TOKEN_LESS_EQUAL : TOKEN_LESS);
203+ case '>':
204+ if (match('>')) return make_token(TOKEN_SHR);
205+ 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);
212+ case '^': return make_token(TOKEN_CARET);
213+ case '~': return make_token(TOKEN_TILDE);
214+ case '@':
215+ if (strncmp(lex_current, "import", 6) == 0 && !(isalnum((unsigned char)lex_current[6]) || lex_current[6] == '_')) {
216+ lex_current += 6;
217+ return make_token(TOKEN_IMPORT);
218+ }
219+ return error_token("expected 'import' after '@'");
220+ case '.': return make_token(TOKEN_DOT);
221+ }
222+
223+ return error_token("unexpected character");
224+}

kiln/src/lexer/lexer.dadded

+2−0
@@ -0,0 +1,2 @@
1+src/lexer/lexer.o: src/lexer/lexer.c src/lexer/lexer.h
2+src/lexer/lexer.h:

kiln/src/lexer/lexer.hadded

+102−0
@@ -0,0 +1,102 @@
1+#ifndef LEXER_H
2+#define LEXER_H
3+
4+typedef enum {
5+ TOKEN_PRINT,
6+ TOKEN_LET,
7+ TOKEN_IF,
8+ TOKEN_ELSE,
9+ TOKEN_WHILE,
10+ TOKEN_FOR,
11+ TOKEN_IN,
12+ 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,
23+ TOKEN_IDENTIFIER,
24+ TOKEN_STRING,
25+ TOKEN_IMPORT_PATH,
26+ TOKEN_EQUAL,
27+ TOKEN_EQUAL_EQUAL,
28+ TOKEN_BANG_EQUAL,
29+ TOKEN_BANG,
30+ TOKEN_AND,
31+ TOKEN_OR,
32+ TOKEN_LESS,
33+ TOKEN_LESS_EQUAL,
34+ TOKEN_GREATER,
35+ TOKEN_GREATER_EQUAL,
36+ TOKEN_PLUS,
37+ TOKEN_MINUS,
38+ TOKEN_STAR,
39+ TOKEN_SLASH,
40+ TOKEN_PERCENT,
41+ TOKEN_LPAREN,
42+ TOKEN_RPAREN,
43+ TOKEN_LBRACE,
44+ TOKEN_RBRACE,
45+ TOKEN_LBRACKET,
46+ TOKEN_RBRACKET,
47+ TOKEN_COMMA,
48+ TOKEN_COLON,
49+ 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,
63+ TOKEN_STAR_STAR,
64+ TOKEN_NUMBER,
65+ TOKEN_SEMICOLON,
66+ TOKEN_EOF,
67+ TOKEN_ERROR
68+} TokenType;
69+
70+typedef struct {
71+ TokenType type;
72+ const char *start;
73+ int length;
74+ int line;
75+} Token;
76+
77+void lexer_init(const char *source);
78+Token lexer_next_token(void);
79+
80+// Saved lexer cursor position -- used by imports.c to recursively lex a
81+// different source buffer for an imported file, then resume exactly where
82+// the importing file's tokenizer left off (mirrors vars_save/restore's
83+// save-a-struct, recurse, restore pattern).
84+typedef struct {
85+ const char *start;
86+ const char *current;
87+ int line;
88+} LexerState;
89+
90+LexerState lexer_save_state(void);
91+void lexer_restore_state(LexerState state);
92+
93+// Called by imports.c right after the parser has consumed a TOKEN_LESS
94+// that opened an `@import <...>` path. Scans raw characters (not through
95+// the normal keyword/operator rules -- a path like "./foo.ash" contains
96+// '.' and '/' which aren't otherwise valid outside this context) up to
97+// and INCLUDING the closing '>', and returns a token whose start/length
98+// cover just the path text (excluding both brackets). Errors (via the
99+// TOKEN_ERROR convention) on a newline or EOF before '>'.
100+Token lexer_scan_import_path(void);
101+
102+#endif

kiln/src/lexer/lexer_import.cadded

+37−0
@@ -0,0 +1,37 @@
1+#include "lexer/lexer_import.h"
2+
3+LexerState lexer_save_state(void) {
4+ LexerState state;
5+ state.start = lex_start;
6+ state.current = lex_current;
7+ state.line = lex_line;
8+ return state;
9+}
10+
11+void lexer_restore_state(LexerState state) {
12+ lex_start = state.start;
13+ lex_current = state.current;
14+ lex_line = state.line;
15+}
16+
17+// Called with the lexer's internal `current` pointer sitting immediately
18+// after the '<' the parser just confirmed (via current.type==TOKEN_LESS)
19+// WITHOUT calling advance_token() on it -- so `current` here still points
20+// at the first path character, untouched by normal tokenization. Scans
21+// raw bytes up to '>', consuming the '>' too, so the parser's next
22+// advance_token() call correctly picks up whatever follows it (';').
23+Token lexer_scan_import_path(void) {
24+ const char *content_start = lex_current;
25+ while (peek() != '>' && peek() != '\n' && !is_at_end()) advance();
26+ if (peek() != '>') return error_token("unterminated import path (expected '>')");
27+
28+ int length = (int)(lex_current - content_start);
29+ advance(); // consume '>'
30+
31+ Token token;
32+ token.type = TOKEN_IMPORT_PATH;
33+ token.start = content_start;
34+ token.length = length;
35+ token.line = lex_line;
36+ return token;
37+}

kiln/src/lexer/lexer_import.hadded

+29−0
@@ -0,0 +1,29 @@
1+#ifndef KILN_LEXER_IMPORT_H
2+#define KILN_LEXER_IMPORT_H
3+#include "lexer/lexer.h"
4+
5+// Cross-file link between lexer.c (the tokenizer's cursor state and its
6+// low-level scan primitives) and lexer_import.c (state save/restore plus
7+// import-path scanning, split out here to keep lexer.c under the
8+// project's ~200-line guideline) -- not part of the public API. The
9+// public entry points the two files implement together
10+// (lexer_save_state/lexer_restore_state/lexer_scan_import_path) stay
11+// declared in lexer.h exactly as before, so callers outside the lexer
12+// directory don't need to change anything. Mirrors parser.c's split
13+// between parser.h (public surface) and parser_internal.h (private,
14+// cross-file plumbing).
15+//
16+// Named lex_* rather than the bare start/current/line: parser.h already
17+// declares its own global `Token current`, and since these now need
18+// external linkage to be shared across the two .c files, a second plain
19+// `current` global here would collide with it at link time.
20+extern const char *lex_start;
21+extern const char *lex_current;
22+extern int lex_line;
23+
24+int is_at_end(void);
25+char advance(void);
26+char peek(void);
27+Token error_token(const char *message);
28+
29+#endif

kiln/src/main.cadded

+115−0
@@ -0,0 +1,115 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "parser/parser.h"
5+#include "codegen/emit.h"
6+#include "elf/elf_writer.h"
7+#include "elf/elf_dynamic.h"
8+#include "elf/elf_so_writer.h"
9+#include "pe/pe_writer.h"
10+#include "pe/pe_dll_writer.h"
11+#include "diagnostics/diagnostics.h"
12+#include "target.h"
13+
14+static char *read_file(const char *path) {
15+ FILE *file = fopen(path, "rb");
16+ if (!file) { fprintf(stderr, "kiln: could not open file: %s\n", path); exit(1); }
17+ fseek(file, 0L, SEEK_END);
18+ long size = ftell(file);
19+ rewind(file);
20+ char *buffer = malloc((size_t)size + 1);
21+ fread(buffer, 1, (size_t)size, file);
22+ buffer[size] = '\0';
23+ fclose(file);
24+ return buffer;
25+}
26+
27+static void usage(const char *prog) {
28+ fprintf(stderr, "usage: %s <file.ash> -o <output> [--target=linux|windows] [--link=static|shared]\n", prog);
29+ fprintf(stderr, " %s --emit-runtime -o <libkilnrt.so|libkilnrt.dll> [--target=linux|windows]\n", prog);
30+ exit(1);
31+}
32+
33+static KilnTarget parse_target(const char *arg, const char *prog) {
34+ if (strcmp(arg, "--target=linux") == 0) return KILN_TARGET_LINUX;
35+ if (strcmp(arg, "--target=windows") == 0) return KILN_TARGET_WINDOWS;
36+ fprintf(stderr, "kiln: unrecognized target '%s' (expected linux or windows)\n", arg);
37+ usage(prog);
38+ return KILN_TARGET_LINUX; // unreachable -- usage() exits
39+}
40+
41+static KilnLinkMode parse_link(const char *arg, const char *prog) {
42+ if (strcmp(arg, "--link=static") == 0) return KILN_LINK_STATIC;
43+ if (strcmp(arg, "--link=shared") == 0) return KILN_LINK_SHARED;
44+ fprintf(stderr, "kiln: unrecognized link mode '%s' (expected static or shared)\n", arg);
45+ usage(prog);
46+ return KILN_LINK_STATIC; // unreachable
47+}
48+
49+int main(int argc, char *argv[]) {
50+ // --emit-runtime -o <path> [--target=...]: writes libkilnrt.so/.dll
51+ // (Plan B) and exits, no .ash input at all -- an entirely separate
52+ // mode from compiling a program.
53+ if (argc >= 4 && argc <= 5 && strcmp(argv[1], "--emit-runtime") == 0 && strcmp(argv[2], "-o") == 0) {
54+ if (argc == 5) kiln_set_target(parse_target(argv[4], argv[0]));
55+ // The runtime's own globals-address computation (codegen/layout.c)
56+ // needs to resolve as if for a --link=shared executable, since
57+ // that's whose globals this code reads/writes at runtime --
58+ // elf_write_shared_runtime/pe_write_shared_runtime additionally
59+ // set kiln_set_compiling_so to keep the runtime's OWN internal
60+ // calls (e.g. string_alloc calling heap_alloc) local instead of
61+ // calls back into itself.
62+ kiln_set_link_mode(KILN_LINK_SHARED);
63+ return kiln_get_target() == KILN_TARGET_WINDOWS
64+ ? pe_write_shared_runtime(argv[3])
65+ : elf_write_shared_runtime(argv[3]);
66+ }
67+
68+ if (argc < 4 || argc > 6) usage(argv[0]);
69+ if (strcmp(argv[2], "-o") != 0) usage(argv[0]);
70+
71+ const char *input_path = argv[1];
72+ const char *output_path = argv[3];
73+
74+ for (int i = 4; i < argc; i++) {
75+ if (strncmp(argv[i], "--target=", 9) == 0) kiln_set_target(parse_target(argv[i], argv[0]));
76+ else if (strncmp(argv[i], "--link=", 7) == 0) kiln_set_link_mode(parse_link(argv[i], argv[0]));
77+ else usage(argv[0]);
78+ }
79+
80+ // A Windows binary without a .exe suffix won't be recognized as
81+ // executable by the shell/loader -- match what every other compiler
82+ // targeting Windows does and append it when the caller didn't already.
83+ char *owned_output_path = NULL;
84+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
85+ size_t len = strlen(output_path);
86+ int has_exe = len >= 4 && strcmp(output_path + len - 4, ".exe") == 0;
87+ if (!has_exe) {
88+ owned_output_path = malloc(len + 5);
89+ memcpy(owned_output_path, output_path, len);
90+ memcpy(owned_output_path + len, ".exe", 5);
91+ output_path = owned_output_path;
92+ }
93+ }
94+
95+ char *source = read_file(input_path);
96+ diagnostics_set_source(source, input_path);
97+
98+ CodeBuf machine_code;
99+ compile_program(source, input_path, &machine_code);
100+ free(source);
101+
102+ int status;
103+ if (kiln_get_target() == KILN_TARGET_WINDOWS) {
104+ status = kiln_get_link_mode() == KILN_LINK_SHARED
105+ ? pe_write_dynamic_executable(output_path, &machine_code)
106+ : pe_write_executable(output_path, &machine_code);
107+ } else {
108+ status = kiln_get_link_mode() == KILN_LINK_SHARED
109+ ? elf_write_dynamic_executable(output_path, &machine_code)
110+ : elf_write_executable(output_path, &machine_code);
111+ }
112+ free(machine_code.code);
113+ free(owned_output_path);
114+ return status;
115+}

kiln/src/main.dadded

+24−0
@@ -0,0 +1,24 @@
1+src/main.o: src/main.c src/parser/parser.h src/lexer/lexer.h \
2+ src/codegen/emit.h src/codegen/emit.h src/elf/elf_writer.h \
3+ src/codegen/runtime_layout.h src/elf/elf_dynamic.h \
4+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
5+ src/elf/elf_writer.h src/elf/elf_so_writer.h src/pe/pe_writer.h \
6+ src/pe/pe_imports.def src/pe/pe_dll_writer.h src/pe/pe_writer.h \
7+ src/diagnostics/diagnostics.h src/target.h
8+src/parser/parser.h:
9+src/lexer/lexer.h:
10+src/codegen/emit.h:
11+src/codegen/emit.h:
12+src/elf/elf_writer.h:
13+src/codegen/runtime_layout.h:
14+src/elf/elf_dynamic.h:
15+src/codegen/runtime_import_enum.h:
16+src/codegen/runtime_exports.def:
17+src/elf/elf_writer.h:
18+src/elf/elf_so_writer.h:
19+src/pe/pe_writer.h:
20+src/pe/pe_imports.def:
21+src/pe/pe_dll_writer.h:
22+src/pe/pe_writer.h:
23+src/diagnostics/diagnostics.h:
24+src/target.h:

kiln/src/parser/constants.cadded

+55−0
@@ -0,0 +1,55 @@
1+#include <string.h>
2+#include <stdint.h>
3+#include "parser/constants.h"
4+#include "parser/parser.h"
5+#include "codegen/emit.h"
6+#include "codegen/value.h"
7+#include "codegen/strings.h"
8+
9+static KilnConstant constants[MAX_KILN_CONSTS];
10+static int constant_count = 0;
11+
12+static KilnConstant *declare_constant_common(const char *name, int len) {
13+ if (constant_count >= MAX_KILN_CONSTS) parse_error("too many imported constants");
14+ if (len >= (int)sizeof(constants[0].name)) parse_error("constant name too long (including namespace prefix)");
15+ if (resolve_constant(name, len) != NULL) parse_error("constant already declared");
16+ KilnConstant *k = &constants[constant_count++];
17+ memcpy(k->name, name, (size_t)len);
18+ k->name[len] = '\0';
19+ return k;
20+}
21+
22+KilnConstant *declare_number_constant(const char *name, int len, double value) {
23+ KilnConstant *k = declare_constant_common(name, len);
24+ k->kind = KILN_CONST_NUMBER;
25+ k->num_value = value;
26+ return k;
27+}
28+
29+KilnConstant *declare_string_constant(const char *name, int len, const char *str_bytes, int str_len) {
30+ KilnConstant *k = declare_constant_common(name, len);
31+ k->kind = KILN_CONST_STRING;
32+ k->str_bytes = (char *)str_bytes;
33+ k->str_len = str_len;
34+ return k;
35+}
36+
37+KilnConstant *resolve_constant(const char *name, int len) {
38+ for (int i = 0; i < constant_count; i++) {
39+ if ((int)strlen(constants[i].name) == len && strncmp(constants[i].name, name, len) == 0) return &constants[i];
40+ }
41+ return NULL;
42+}
43+
44+void codegen_constant_value(KilnConstant *k) {
45+ if (k->kind == KILN_CONST_STRING) {
46+ codegen_string_literal_bytes(k->str_bytes, k->str_len);
47+ return;
48+ }
49+ uint64_t bits;
50+ memcpy(&bits, &k->num_value, sizeof(bits));
51+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
52+ emit_push_reg(code, REG_RBX);
53+ emit_mov_reg_imm64(code, REG_RAX, bits);
54+ emit_push_reg(code, REG_RAX);
55+}

kiln/src/parser/constants.hadded

+42−0
@@ -0,0 +1,42 @@
1+#ifndef KILN_CONSTANTS_H
2+#define KILN_CONSTANTS_H
3+
4+#define MAX_KILN_CONSTS 32
5+
6+typedef enum { KILN_CONST_NUMBER, KILN_CONST_STRING } KilnConstantKind;
7+
8+typedef struct {
9+ char name[96]; // namespaced, e.g. "math.PI" -- longer than KilnFunction's
10+ // 64 since it also has to fit a namespace prefix on top
11+ // of a normal identifier
12+ KilnConstantKind kind;
13+ double num_value; // valid when kind == KILN_CONST_NUMBER
14+ char *str_bytes; // valid when kind == KILN_CONST_STRING -- raw
15+ // (still-escaped) bytes from the source token,
16+ // NOT yet processed; codegen_constant_value
17+ // re-runs escape processing at each use site via
18+ // codegen_string_literal_bytes, same as a normal
19+ // string literal would
20+ int str_len;
21+} KilnConstant;
22+
23+// Declares NAME (already namespaced by the caller, e.g. "math.PI") as a
24+// number constant. Calls parse_error() on table-full or name-too-long.
25+KilnConstant *declare_number_constant(const char *name, int len, double value);
26+
27+// Same, for a string constant. str_bytes/str_len are the RAW (unescaped)
28+// source token bytes -- constants.c does not own or copy this pointer
29+// beyond storing it; the caller (parser.c's let_statement) must pass
30+// bytes from a buffer that outlives the whole compile (the imported
31+// file's source buffer, which imports.c already never frees -- see
32+// imports.c's read_import_file comment).
33+KilnConstant *declare_string_constant(const char *name, int len, const char *str_bytes, int str_len);
34+
35+KilnConstant *resolve_constant(const char *name, int len);
36+
37+// Emits the same codegen a literal of this constant's value would have
38+// emitted inline (a NUMBER push or a STRING alloc+push) -- used at every
39+// `ns.CONST` use site.
40+void codegen_constant_value(KilnConstant *k);
41+
42+#endif

kiln/src/parser/for_loop.cadded

+82−0
@@ -0,0 +1,82 @@
1+#include "parser/for_loop.h"
2+#include "parser/parser.h"
3+#include "parser/parser_internal.h"
4+#include "parser/vars.h"
5+#include "parser/loop_stack.h"
6+#include "codegen/expr.h"
7+#include "codegen/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 each
18+// element in turn via the same var-slot mechanism `let` uses, so it's a
19+// perfectly ordinary variable inside the body (readable, reassignable,
20+// though reassigning it doesn't affect iteration -- the next iteration
21+// overwrites it again from the array, matching a plain for-each's usual
22+// semantics). data_ptr/count/i live in this nesting level's fixed
23+// RBP-relative slot (see vars.h's for_level_offset) so they survive
24+// 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); // count
43+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, 16); // data_ptr
44+ 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 = 0
48+
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); // i
54+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, level + 8); // count
55+ 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_ptr
59+ 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 address
63+
64+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 0); // element tag
65+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload
66+ 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 loop
79+
80+ for_nesting_depth--;
81+ vars_scope_end(saved_scope);
82+}

kiln/src/parser/for_loop.dadded

+12−0
@@ -0,0 +1,12 @@
1+src/parser/for_loop.o: src/parser/for_loop.c src/parser/for_loop.h \
2+ src/parser/parser.h src/lexer/lexer.h src/codegen/emit.h \
3+ src/parser/parser_internal.h src/parser/vars.h src/parser/loop_stack.h \
4+ src/codegen/expr.h
5+src/parser/for_loop.h:
6+src/parser/parser.h:
7+src/lexer/lexer.h:
8+src/codegen/emit.h:
9+src/parser/parser_internal.h:
10+src/parser/vars.h:
11+src/parser/loop_stack.h:
12+src/codegen/expr.h:

kiln/src/parser/for_loop.hadded

+19−0
@@ -0,0 +1,19 @@
1+#ifndef KILN_FOR_LOOP_H
2+#define KILN_FOR_LOOP_H
3+
4+// for_statement itself is declared in parser_internal.h alongside
5+// if_statement/while_statement/fn_statement/try_statement (the other
6+// statement-compiler cross-file links) -- this header exists only for
7+// the depth counter below, which fn_statement/lambda.c also need.
8+//
9+// The compile-time for-nesting-depth counter's save/reset/restore, for
10+// fn_statement/lambda.c to wrap around a called body's compilation (same
11+// spirit as parser/vars.h's vars_save/vars_clear/vars_restore): a
12+// 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 the
14+// 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+#endif

kiln/src/parser/functions.cadded

+66−0
@@ -0,0 +1,66 @@
1+#include <string.h>
2+#include <stdio.h>
3+#include "parser/functions.h"
4+#include "parser/parser.h"
5+
6+static KilnFunction functions[MAX_KILN_FUNCS];
7+static int function_count = 0;
8+
9+static const char *g_import_ns = NULL;
10+static int g_import_ns_len = 0;
11+
12+KilnFunction *resolve_function(const char *name, int len) {
13+ for (int i = 0; i < function_count; i++) {
14+ if ((int)strlen(functions[i].name) == len && strncmp(functions[i].name, name, len) == 0) return &functions[i];
15+ }
16+ return NULL;
17+}
18+
19+KilnFunction *declare_function(const char *name, int len) {
20+ if (function_count >= MAX_KILN_FUNCS) parse_error("too many functions");
21+ if (resolve_function(name, len) != NULL) parse_error("function already declared");
22+ if (len >= (int)sizeof(functions[0].name)) parse_error("function name too long (including any namespace prefix)");
23+ KilnFunction *fn = &functions[function_count++];
24+ memcpy(fn->name, name, (size_t)len);
25+ fn->name[len] = '\0';
26+ fn->code_offset = 0;
27+ fn->arity = 0;
28+ return fn;
29+}
30+
31+void set_import_namespace(const char *ns, int len) {
32+ g_import_ns = ns;
33+ g_import_ns_len = len;
34+}
35+
36+void get_import_namespace(const char **out_ns, int *out_len) {
37+ *out_ns = g_import_ns;
38+ *out_len = g_import_ns_len;
39+}
40+
41+// Shared by declare_function_in_context/resolve_function_in_context --
42+// writes "ns.name" into buf (bounded by buf_size) and returns its length,
43+// or -1 if it doesn't fit.
44+static int build_namespaced_name(char *buf, int buf_size, const char *name, int len) {
45+ if (g_import_ns_len + 1 + len >= buf_size) return -1;
46+ memcpy(buf, g_import_ns, (size_t)g_import_ns_len);
47+ buf[g_import_ns_len] = '.';
48+ memcpy(buf + g_import_ns_len + 1, name, (size_t)len);
49+ return g_import_ns_len + 1 + len;
50+}
51+
52+KilnFunction *declare_function_in_context(const char *name, int len) {
53+ if (g_import_ns_len == 0) return declare_function(name, len);
54+ char buf[128];
55+ int combined_len = build_namespaced_name(buf, sizeof(buf), name, len);
56+ if (combined_len < 0) parse_error("function name too long (including namespace prefix)");
57+ return declare_function(buf, combined_len);
58+}
59+
60+KilnFunction *resolve_function_in_context(const char *name, int len) {
61+ if (g_import_ns_len == 0) return resolve_function(name, len);
62+ char buf[128];
63+ int combined_len = build_namespaced_name(buf, sizeof(buf), name, len);
64+ if (combined_len < 0) return NULL;
65+ return resolve_function(buf, combined_len);
66+}

kiln/src/parser/functions.dadded

+6−0
@@ -0,0 +1,6 @@
1+src/parser/functions.o: src/parser/functions.c src/parser/functions.h \
2+ src/parser/parser.h src/lexer/lexer.h src/codegen/emit.h
3+src/parser/functions.h:
4+src/parser/parser.h:
5+src/lexer/lexer.h:
6+src/codegen/emit.h:

kiln/src/parser/functions.hadded

+58−0
@@ -0,0 +1,58 @@
1+#ifndef KILN_FUNCTIONS_H
2+#define KILN_FUNCTIONS_H
3+
4+// Flat name -> {code offset, arity} table, mirroring vars.h's pattern (and
5+// ashvm's vm_functions[]/find_function). Functions can only call ones
6+// already declared earlier in the source -- same single-pass limitation
7+// ashvm's compiler has (see emit_call_back's comment) -- so by the time a
8+// call site compiles, the callee's code_offset is always already known.
9+//
10+// Imports (see parser/imports.c) don't change this table's shape at all
11+// -- an imported function is declared under a namespaced string (e.g.
12+// "math.add") in the exact same flat array. 64 (up from 32) gives real
13+// programs room to import a handful of libraries without hitting the cap
14+// immediately; still a hard ceiling, noted as a tuning knob in the design
15+// spec, not solved generally here.
16+#define MAX_KILN_FUNCS 64
17+#define MAX_KILN_PARAMS 8
18+
19+typedef struct {
20+ char name[64];
21+ int code_offset;
22+ int arity;
23+} KilnFunction;
24+
25+// Returns a pointer to the function's entry, or NULL if not declared.
26+KilnFunction *resolve_function(const char *name, int len);
27+
28+// Declares a new function (code_offset filled in by the caller once known)
29+// and returns its entry. Calls parse_error() (noreturn) if the table's full,
30+// the name is a duplicate, or the name (including any namespace prefix)
31+// doesn't fit in the 64-byte name field.
32+KilnFunction *declare_function(const char *name, int len);
33+
34+// --- Import namespace context (parser/imports.c sets/restores this around
35+// each recursively-parsed imported file; parser_control.c's fn_statement
36+// and codegen/expr.c + expr_call.c's call-site resolution consult it) ---
37+
38+// Sets the namespace prefix every subsequent declare_function_in_context/
39+// resolve_function_in_context call should use. Pass len=0 (ns may be NULL)
40+// to clear it back to "not inside an import" -- the default/top-level-file
41+// state.
42+void set_import_namespace(const char *ns, int len);
43+
44+// Reads the current namespace prefix (out_len==0 means "none").
45+void get_import_namespace(const char **out_ns, int *out_len);
46+
47+// Declares NAME under "ns.NAME" if a namespace is currently set, else
48+// under the bare NAME -- the single call site fn_statement() uses,
49+// replacing a direct declare_function() call.
50+KilnFunction *declare_function_in_context(const char *name, int len);
51+
52+// While a namespace is set: resolves ONLY "ns.NAME" (no bare fallback --
53+// an imported file's own bare calls must stay self-contained to its own
54+// declarations, never accidentally reach into the importing program).
55+// While no namespace is set: resolves NAME directly, identical to today.
56+KilnFunction *resolve_function_in_context(const char *name, int len);
57+
58+#endif

kiln/src/parser/import_paths.cadded

+62−0
@@ -0,0 +1,62 @@
1+#define _XOPEN_SOURCE 700 // exposes realpath() and PATH_MAX under -std=c11
2+#include <string.h>
3+#include <stdio.h>
4+#include <stdlib.h>
5+#include "parser/import_paths.h"
6+
7+void import_path_dirname(const char *path, char *out, int out_size) {
8+ const char *slash = strrchr(path, '/');
9+ if (!slash) { snprintf(out, (size_t)out_size, "."); return; }
10+ int len = (int)(slash - path);
11+ if (len >= out_size) len = out_size - 1;
12+ memcpy(out, path, (size_t)len);
13+ out[len] = '\0';
14+}
15+
16+void import_derive_namespace(const char *canonical_path, char *out, int out_size) {
17+ const char *slash = strrchr(canonical_path, '/');
18+ const char *base = slash ? slash + 1 : canonical_path;
19+ int len = (int)strlen(base);
20+ if (len > 4 && strcmp(base + len - 4, ".ash") == 0) len -= 4;
21+ if (len >= out_size) len = out_size - 1;
22+ memcpy(out, base, (size_t)len);
23+ out[len] = '\0';
24+}
25+
26+static int is_relative_prefix(const char *raw, int len) {
27+ if (len >= 2 && raw[0] == '.' && raw[1] == '/') return 1;
28+ if (len >= 3 && raw[0] == '.' && raw[1] == '.' && raw[2] == '/') return 1;
29+ return 0;
30+}
31+
32+int import_resolve_path(const char *raw_path, int raw_len,
33+ const char *importing_file_dir,
34+ const char *project_root_dir,
35+ char *canonical_out,
36+ char *message_out, int message_out_size) {
37+ char candidate[PATH_MAX];
38+
39+ if (is_relative_prefix(raw_path, raw_len)) {
40+ if (raw_len < 4 || strncmp(raw_path + raw_len - 4, ".ash", 4) != 0) {
41+ snprintf(message_out, (size_t)message_out_size, "relative import path must end in '.ash'");
42+ return 0;
43+ }
44+ snprintf(candidate, sizeof(candidate), "%s/%.*s", importing_file_dir, raw_len, raw_path);
45+ } else {
46+ for (int i = 0; i < raw_len; i++) {
47+ if (raw_path[i] == '.' || raw_path[i] == '/') {
48+ snprintf(message_out, (size_t)message_out_size,
49+ "library import name must not contain '.' or '/' (did you mean './%.*s'?)", raw_len, raw_path);
50+ return 0;
51+ }
52+ }
53+ snprintf(candidate, sizeof(candidate), "%s/ash.libs/%.*s/%.*s.ash",
54+ project_root_dir, raw_len, raw_path, raw_len, raw_path);
55+ }
56+
57+ if (realpath(candidate, canonical_out) == NULL) {
58+ snprintf(message_out, (size_t)message_out_size, "could not resolve '%.*s'", raw_len, raw_path);
59+ return 0;
60+ }
61+ return 1;
62+}

kiln/src/parser/import_paths.hadded

+39−0
@@ -0,0 +1,39 @@
1+#ifndef KILN_IMPORT_PATHS_H
2+#define KILN_IMPORT_PATHS_H
3+#include <limits.h>
4+
5+// Resolves the raw text inside `@import <...>` to a canonicalized
6+// (realpath()'d) absolute filesystem path.
7+//
8+// raw_path/raw_len: the text between '<' and '>', exactly as scanned by
9+// lexer_scan_import_path (NOT null-terminated -- callers must use len).
10+// importing_file_dir: directory of the file containing this @import
11+// statement (relative imports resolve against this).
12+// project_root_dir: directory of the entry .ash file passed on the
13+// command line (library imports resolve against this, regardless of
14+// how deeply nested the importing file is).
15+// canonical_out: buffer of at least PATH_MAX bytes.
16+//
17+// Returns 1 and fills canonical_out on success. Returns 0 on failure
18+// (message_out is filled with a short human-readable reason, e.g.
19+// "could not resolve '<path>'" or "library import name must not contain
20+// '.' or '/'" -- callers pass this straight into parse_error).
21+int import_resolve_path(const char *raw_path, int raw_len,
22+ const char *importing_file_dir,
23+ const char *project_root_dir,
24+ char *canonical_out,
25+ char *message_out, int message_out_size);
26+
27+// Derives the auto-namespace for a resolved canonical path: the file's
28+// own basename with the trailing ".ash" stripped. Works uniformly for
29+// both relative and library imports because library entry files are
30+// required to be named "<libname>/<libname>.ash" -- the folder name and
31+// the basename-minus-extension are always identical by construction.
32+void import_derive_namespace(const char *canonical_path, char *out, int out_size);
33+
34+// Directory-of-a-path helper (hand-rolled, not libgen.h's dirname(),
35+// which mutates its input and reuses a static buffer across calls --
36+// both a bad fit here). Writes "." if path has no '/'.
37+void import_path_dirname(const char *path, char *out, int out_size);
38+
39+#endif

kiln/src/parser/imports.cadded

+219−0
@@ -0,0 +1,219 @@
1+#define _XOPEN_SOURCE 700 // exposes realpath() and PATH_MAX under -std=c11
2+#include <string.h>
3+#include <stdio.h>
4+#include <stdlib.h>
5+#include <limits.h>
6+#include "parser/imports.h"
7+#include "parser/import_paths.h"
8+#include "parser/parser.h"
9+#include "parser/parser_internal.h"
10+#include "parser/functions.h"
11+#include "lexer/lexer.h"
12+#include "diagnostics/diagnostics.h"
13+
14+#define MAX_KILN_IMPORTS 16
15+#define MAX_KILN_IMPORT_DEPTH 8
16+
17+typedef struct {
18+ char canonical_path[PATH_MAX];
19+ char namespace_name[64];
20+} ImportedFile;
21+
22+static ImportedFile imported_files[MAX_KILN_IMPORTS];
23+static int imported_file_count = 0;
24+
25+static char resolving_stack[MAX_KILN_IMPORT_DEPTH][PATH_MAX];
26+static int resolving_depth = 0;
27+
28+static char project_root_dir[PATH_MAX];
29+static char current_file_dir[PATH_MAX];
30+
31+static int import_block_open = 1;
32+
33+void imports_init(const char *entry_file_path) {
34+ char resolved[PATH_MAX];
35+ if (realpath(entry_file_path, resolved) == NULL) parse_error("could not resolve entry file path");
36+ import_path_dirname(resolved, project_root_dir, sizeof(project_root_dir));
37+ size_t root_len = strlen(project_root_dir);
38+ if (root_len >= sizeof(current_file_dir)) root_len = sizeof(current_file_dir) - 1;
39+ memcpy(current_file_dir, project_root_dir, root_len);
40+ current_file_dir[root_len] = '\0';
41+ import_block_open = 1;
42+}
43+
44+void imports_close_block(void) {
45+ import_block_open = 0;
46+}
47+
48+static ImportedFile *registry_find_by_path(const char *canonical) {
49+ for (int i = 0; i < imported_file_count; i++) {
50+ if (strcmp(imported_files[i].canonical_path, canonical) == 0) return &imported_files[i];
51+ }
52+ return NULL;
53+}
54+
55+static ImportedFile *registry_find_by_namespace(const char *ns) {
56+ for (int i = 0; i < imported_file_count; i++) {
57+ if (strcmp(imported_files[i].namespace_name, ns) == 0) return &imported_files[i];
58+ }
59+ return NULL;
60+}
61+
62+static int resolving_stack_contains(const char *canonical) {
63+ for (int i = 0; i < resolving_depth; i++) {
64+ if (strcmp(resolving_stack[i], canonical) == 0) return 1;
65+ }
66+ return 0;
67+}
68+
69+static void report_circular_import(const char *canonical) {
70+ char msg[1024];
71+ int off = snprintf(msg, sizeof(msg), "circular import detected: ");
72+ for (int i = 0; i < resolving_depth; i++) {
73+ off += snprintf(msg + off, sizeof(msg) - (size_t)off, "%s -> ", resolving_stack[i]);
74+ if (off >= (int)sizeof(msg)) break;
75+ }
76+ snprintf(msg + off, sizeof(msg) - (size_t)off, "%s", canonical);
77+ parse_error(msg);
78+}
79+
80+static char *read_import_file(const char *path) {
81+ FILE *file = fopen(path, "rb");
82+ if (!file) parse_error("could not open resolved import file");
83+ fseek(file, 0L, SEEK_END);
84+ long size = ftell(file);
85+ rewind(file);
86+ // Never freed -- an imported file's source buffer must outlive every
87+ // later use site of its constants (see constants.c), which can be
88+ // arbitrarily far ahead in the importing program. The compiler is a
89+ // one-shot host process; leaking these until it exits is the same
90+ // documented, intentional tradeoff kiln's own runtime heap allocator
91+ // already makes (see missing.md's GC entry).
92+ char *buffer = malloc((size_t)size + 1);
93+ fread(buffer, 1, (size_t)size, file);
94+ buffer[size] = '\0';
95+ fclose(file);
96+ return buffer;
97+}
98+
99+// The restricted top-level loop for a recursively-parsed imported file:
100+// identical shape to compile_program's own `while (... ) statement();`
101+// loop. statement() itself (parser.c) enforces the "only fn/let allowed"
102+// restriction by checking get_import_namespace()'s length -- see Task 4
103+// Step 6.
104+static void parse_imported_file_body(void) {
105+ // Each imported file gets its OWN fresh import-block window: OPEN
106+ // (not closed), so the imported file's own leading @import
107+ // statements (if any -- e.g. a diamond-shaped or chained import) are
108+ // allowed, exactly like a top-level file's. imports_close_block()
109+ // would do the opposite (close it), which is wrong here -- statement()
110+ // itself closes the window the moment it dispatches this file's own
111+ // first non-import statement, same as it does for the entry file.
112+ import_block_open = 1;
113+ while (current.type != TOKEN_EOF) statement();
114+}
115+
116+void import_statement(void) {
117+ if (!import_block_open) parse_error("@import statements must form a contiguous block at the top of the file");
118+ advance_token(); // consume TOKEN_IMPORT ("@import")
119+
120+ if (current.type != TOKEN_LESS) parse_error("expected '<' after '@import'");
121+ Token path = lexer_scan_import_path();
122+ if (path.type == TOKEN_ERROR) parse_error(path.start);
123+ advance_token(); // current now holds whatever follows '>' (e.g. ';')
124+
125+ char canonical[PATH_MAX];
126+ char message[256];
127+ if (!import_resolve_path(path.start, path.length, current_file_dir, project_root_dir,
128+ canonical, message, sizeof(message))) {
129+ parse_error(message);
130+ }
131+
132+ expect(TOKEN_SEMICOLON, "expected ';' after import path");
133+
134+ // Cycle check MUST come before the dedup check: a file already in
135+ // imported_files but STILL on resolving_stack is mid-parse (its
136+ // registry entry was added before recursing, same as every entry
137+ // here) -- that's a genuine cycle, not a finished diamond import.
138+ // Checking the registry first would silently treat a still-parsing
139+ // file as "already done" and swallow the cycle instead of reporting
140+ // it. Only a registry hit with NO matching resolving_stack entry is
141+ // a real, already-completed diamond dedup.
142+ if (resolving_stack_contains(canonical)) report_circular_import(canonical);
143+
144+ ImportedFile *existing = registry_find_by_path(canonical);
145+ if (existing != NULL) return; // diamond dedup -- already fully processed, nothing more to do
146+
147+ char ns[64];
148+ import_derive_namespace(canonical, ns, sizeof(ns));
149+
150+ ImportedFile *collision = registry_find_by_namespace(ns);
151+ if (collision != NULL) {
152+ // Sized to comfortably fit two PATH_MAX-bounded paths plus the
153+ // namespace and surrounding literal text with no truncation, so
154+ // GCC's -Wformat-truncation can prove this snprintf never overflows.
155+ char msg[2 * PATH_MAX + 128];
156+ snprintf(msg, sizeof(msg), "namespace '%s' already used by %s (colliding with %s)",
157+ ns, collision->canonical_path, canonical);
158+ parse_error(msg);
159+ }
160+
161+ if (imported_file_count >= MAX_KILN_IMPORTS) parse_error("too many imported files");
162+ ImportedFile *entry = &imported_files[imported_file_count++];
163+ size_t canonical_len = strlen(canonical);
164+ if (canonical_len >= sizeof(entry->canonical_path)) canonical_len = sizeof(entry->canonical_path) - 1;
165+ memcpy(entry->canonical_path, canonical, canonical_len);
166+ entry->canonical_path[canonical_len] = '\0';
167+ size_t ns_len = strlen(ns);
168+ if (ns_len >= sizeof(entry->namespace_name)) ns_len = sizeof(entry->namespace_name) - 1;
169+ memcpy(entry->namespace_name, ns, ns_len);
170+ entry->namespace_name[ns_len] = '\0';
171+
172+ if (resolving_depth >= MAX_KILN_IMPORT_DEPTH) parse_error("import nesting too deep");
173+ size_t stack_len = strlen(canonical);
174+ if (stack_len >= sizeof(resolving_stack[0])) stack_len = sizeof(resolving_stack[0]) - 1;
175+ memcpy(resolving_stack[resolving_depth], canonical, stack_len);
176+ resolving_stack[resolving_depth][stack_len] = '\0';
177+ resolving_depth++;
178+
179+ // --- Save everything the recursive parse of the imported file will
180+ // clobber, exactly mirroring vars_save/for_depth_save/loop_depth_save's
181+ // existing save-recurse-restore pattern (the C call stack gives
182+ // correct nesting for free, same as those). ---
183+ DiagnosticsSource saved_diag = diagnostics_save_source();
184+ LexerState saved_lexer = lexer_save_state();
185+ Token saved_current = current;
186+ Token saved_previous = previous;
187+ char saved_file_dir[PATH_MAX];
188+ size_t saved_len = strlen(current_file_dir);
189+ if (saved_len >= sizeof(saved_file_dir)) saved_len = sizeof(saved_file_dir) - 1;
190+ memcpy(saved_file_dir, current_file_dir, saved_len);
191+ saved_file_dir[saved_len] = '\0';
192+ const char *saved_ns; int saved_ns_len;
193+ get_import_namespace(&saved_ns, &saved_ns_len);
194+ int saved_import_block_open = import_block_open;
195+
196+ char *imported_source = read_import_file(canonical);
197+ diagnostics_set_source(imported_source, canonical);
198+ import_path_dirname(canonical, current_file_dir, sizeof(current_file_dir));
199+ set_import_namespace(entry->namespace_name, (int)strlen(entry->namespace_name));
200+
201+ lexer_init(imported_source);
202+ advance_token();
203+ parse_imported_file_body();
204+
205+ // --- Restore, in the reverse order, so the importing file's parse
206+ // resumes exactly as if the @import line had been a no-op cursor-wise. ---
207+ set_import_namespace(saved_ns, saved_ns_len);
208+ size_t restore_len = strlen(saved_file_dir);
209+ if (restore_len >= sizeof(current_file_dir)) restore_len = sizeof(current_file_dir) - 1;
210+ memcpy(current_file_dir, saved_file_dir, restore_len);
211+ current_file_dir[restore_len] = '\0';
212+ lexer_restore_state(saved_lexer);
213+ diagnostics_restore_source(saved_diag);
214+ current = saved_current;
215+ previous = saved_previous;
216+ import_block_open = saved_import_block_open;
217+
218+ resolving_depth--;
219+}

kiln/src/parser/imports.hadded

+23−0
@@ -0,0 +1,23 @@
1+#ifndef KILN_IMPORTS_H
2+#define KILN_IMPORTS_H
3+
4+// Called once, at the very start of compile_program, before the main
5+// parse loop -- establishes the entry file's own directory as the
6+// project root that library imports (ash.libs/) resolve against for the
7+// entire compile, no matter how deeply nested a later import is.
8+void imports_init(const char *entry_file_path);
9+
10+// Parses one `@import <path>;` statement. Assumes statement()'s
11+// dispatcher has NOT yet consumed the TOKEN_IMPORT (this function does
12+// that itself, matching let_statement()'s own convention).
13+void import_statement(void);
14+
15+// Called by statement() on every non-import statement dispatch. No-op
16+// after the first call in a given file's top-level parse -- marks that
17+// file's "imports must be a contiguous block at the top" window as
18+// closed, so a later @import in the SAME file is a compile error. Reset
19+// automatically on entry to each recursively-parsed imported file (their
20+// own import block starts fresh).
21+void imports_close_block(void);
22+
23+#endif

kiln/src/parser/loop_stack.cadded

+51−0
@@ -0,0 +1,51 @@
1+#include "parser/loop_stack.h"
2+#include "parser/parser.h"
3+
4+#define MAX_LOOP_NESTING 16
5+#define MAX_LOOP_JUMPS 64
6+
7+typedef struct {
8+ int break_jumps[MAX_LOOP_JUMPS];
9+ int break_count;
10+ int continue_jumps[MAX_LOOP_JUMPS];
11+ int continue_count;
12+} LoopContext;
13+
14+static LoopContext loop_stack[MAX_LOOP_NESTING];
15+static int loop_depth = 0;
16+
17+void loop_push(void) {
18+ 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;
21+ loop_depth++;
22+}
23+
24+void loop_record_break(int patch_offset) {
25+ if (loop_depth == 0) parse_error("'break' outside a loop");
26+ 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;
29+}
30+
31+void loop_record_continue(int patch_offset) {
32+ if (loop_depth == 0) parse_error("'continue' outside a loop");
33+ 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;
36+}
37+
38+void loop_patch_continues(void) {
39+ 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]);
41+}
42+
43+void loop_pop_and_patch_breaks(void) {
44+ 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]);
46+ loop_depth--;
47+}
48+
49+void loop_depth_save(int *out) { *out = loop_depth; }
50+void loop_depth_reset(void) { loop_depth = 0; }
51+void loop_depth_restore(int saved) { loop_depth = saved; }

kiln/src/parser/loop_stack.dadded

+6−0
@@ -0,0 +1,6 @@
1+src/parser/loop_stack.o: src/parser/loop_stack.c src/parser/loop_stack.h \
2+ src/parser/parser.h src/lexer/lexer.h src/codegen/emit.h
3+src/parser/loop_stack.h:
4+src/parser/parser.h:
5+src/lexer/lexer.h:
6+src/codegen/emit.h:

kiln/src/parser/loop_stack.hadded

+47−0
@@ -0,0 +1,47 @@
1+#ifndef KILN_LOOP_STACK_H
2+#define KILN_LOOP_STACK_H
3+
4+// break/continue need to know which loop they belong to, and loops
5+// nest -- but unlike for_loop.h's runtime scratch state, break/continue
6+// need NO runtime state at all: they're pure "emit now, patch later"
7+// jump bookkeeping (same primitive every other forward jump in this
8+// project uses), just tracked per-nesting-level at compile time instead
9+// of resolved immediately.
10+//
11+// continue can't simply backward-jump to a known offset the way a plain
12+// loop back-edge does: `continue`'s target (the per-iteration advance
13+// step -- a for-loop's index increment, or nothing for a while loop) is
14+// only emitted AFTER the body compiles, i.e. after the continue
15+// statement itself. So continue jumps are forward-patched too, resolved
16+// by the loop's own codegen right after block() returns, before
17+// whatever 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 parser.c's break_statement()/continue_statement() --
23+// records a forward jump's patch offset into the innermost loop's list.
24+// 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);
27+
28+// Patches every recorded continue jump to the CURRENT code position --
29+// call right after block() returns, before emitting the per-iteration
30+// advance step.
31+void loop_patch_continues(void);
32+
33+// Patches every recorded break jump to the CURRENT code position, then
34+// pops the loop context -- call once the loop is fully compiled.
35+void loop_pop_and_patch_breaks(void);
36+
37+// Save/reset/restore the loop-nesting depth, for fn_statement/lambda.c
38+// to wrap around a called body's compilation (same spirit as
39+// parser/vars.h's vars_save/vars_clear/vars_restore and for_loop.h's
40+// for_depth_save/reset/restore): a break/continue inside a function or
41+// lambda body must never reach past that body into an ENCLOSING loop it
42+// happens to be textually written inside.
43+void loop_depth_save(int *out);
44+void loop_depth_reset(void);
45+void loop_depth_restore(int saved);
46+
47+#endif

kiln/src/parser/parser.cadded

+402−0
@@ -0,0 +1,402 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "parser/parser.h"
5+#include "parser/parser_internal.h"
6+#include "parser/vars.h"
7+#include "parser/loop_stack.h"
8+#include "parser/functions.h"
9+#include "parser/imports.h"
10+#include "parser/constants.h"
11+#include "codegen/expr.h"
12+#include "codegen/emit_sse.h"
13+#include "codegen/value.h"
14+#include "codegen/heap.h"
15+#include "codegen/bytes.h"
16+#include "codegen/string_alloc.h"
17+#include "codegen/arrays.h"
18+#include "codegen/maps.h"
19+#include "codegen/errors.h"
20+#include "codegen/print_int.h"
21+#include "diagnostics/diagnostics.h"
22+#include "elf/elf_writer.h"
23+#include "target.h"
24+
25+Token current;
26+Token previous;
27+CodeBuf *code;
28+
29+void advance_token(void) { previous = current; current = lexer_next_token(); }
30+
31+void parse_error(const char *message) {
32+ diagnostics_report("error", message, current.start, current.length);
33+ exit(1);
34+}
35+
36+void expect(TokenType type, const char *message) {
37+ if (current.type == type) { advance_token(); return; }
38+ parse_error(message);
39+}
40+
41+// statement() is shared between top-level parsing (compile_program's own
42+// loop, and imports.c's parse_imported_file_body()) and here, inside a
43+// fn/if/while/for/try body -- block_depth distinguishes the two so the
44+// "only fn/let at an imported file's top level" guard in statement()
45+// only fires for genuinely top-level statements, never for an ordinary
46+// `return`/`if`/etc. nested inside an imported function's own body.
47+static int block_depth = 0;
48+
49+void block(void) {
50+ VarBlockScope saved_scope = vars_scope_begin();
51+ expect(TOKEN_LBRACE, "expected '{'");
52+ block_depth++;
53+ while (current.type != TOKEN_RBRACE && current.type != TOKEN_EOF) statement();
54+ block_depth--;
55+ expect(TOKEN_RBRACE, "expected '}'");
56+ vars_scope_end(saved_scope);
57+}
58+
59+// let IDENT = expr ; -- reassigns the existing slot if `name` is already
60+// declared instead of erroring, matching ashvm's own let-statement
61+// behavior exactly (ashvm/src/compiler/stmt.c's let_statement).
62+static void let_statement(void) {
63+ advance_token();
64+ expect(TOKEN_IDENTIFIER, "expected variable name after 'let'");
65+ const char *name = previous.start;
66+ int len = previous.length;
67+ expect(TOKEN_EQUAL, "expected '=' after variable name");
68+
69+ const char *ns; int ns_len;
70+ get_import_namespace(&ns, &ns_len);
71+ if (ns_len > 0) {
72+ char combined[96];
73+ int combined_len = snprintf(combined, sizeof(combined), "%.*s.%.*s", ns_len, ns, len, name);
74+ if (current.type == TOKEN_NUMBER) {
75+ char clean[128];
76+ int clean_len = 0;
77+ for (int i = 0; i < current.length; i++) {
78+ if (current.start[i] == '_') continue;
79+ clean[clean_len++] = current.start[i];
80+ }
81+ clean[clean_len] = '\0';
82+ double v = strtod(clean, NULL);
83+ advance_token();
84+ declare_number_constant(combined, combined_len, v);
85+ } else if (current.type == TOKEN_STRING) {
86+ declare_string_constant(combined, combined_len, current.start, current.length);
87+ advance_token();
88+ } else {
89+ parse_error("imported top-level 'let' must be a literal number or string constant");
90+ }
91+ expect(TOKEN_SEMICOLON, "expected ';' after statement");
92+ return;
93+ }
94+
95+ codegen_expression();
96+ expect(TOKEN_SEMICOLON, "expected ';' after statement");
97+
98+ int slot = resolve_var_in_current_scope(name, len);
99+ if (slot == -1) slot = declare_var(name, len);
100+ emit_pop_reg(code, REG_RAX); // payload
101+ emit_pop_reg(code, REG_RBX); // tag
102+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
103+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
104+}
105+
106+// IDENT[index] = expr ; -- writes through an existing array OR map
107+// variable (which one is only known at runtime -- see expr.c's postfix()
108+// for the read-side version of this same dispatch).
109+static void index_assignment_statement(Token id) {
110+ int slot = resolve_var(id.start, id.length);
111+ if (slot == -1) {
112+ char msg[128];
113+ snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
114+ parse_error(msg);
115+ }
116+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(slot));
117+ emit_push_reg(code, REG_RBX);
118+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
119+ emit_push_reg(code, REG_RAX);
120+
121+ advance_token(); // consume '['
122+ codegen_expression();
123+ expect(TOKEN_RBRACKET, "expected ']' after index");
124+ expect(TOKEN_EQUAL, "expected '=' after index");
125+ codegen_expression();
126+ expect(TOKEN_SEMICOLON, "expected ';' after assignment");
127+
128+ emit_pop_reg(code, REG_RAX); // value payload
129+ emit_pop_reg(code, REG_RBX); // value tag
130+ emit_pop_reg(code, REG_RCX); // index payload
131+ emit_pop_reg(code, REG_RDX); // index tag
132+ emit_pop_reg(code, REG_RDI); // base payload
133+ emit_pop_reg(code, REG_RSI); // base tag
134+ emit_push_reg(code, REG_RSI);
135+ emit_push_reg(code, REG_RDI);
136+ emit_push_reg(code, REG_RDX);
137+ emit_push_reg(code, REG_RCX);
138+ emit_push_reg(code, REG_RBX);
139+ emit_push_reg(code, REG_RAX);
140+ // Strings are immutable -- checked here rather than left to fall
141+ // through to the array path (see missing.md #1: that path assumes
142+ // its base is an array object and would corrupt memory given a
143+ // string's raw content bytes instead).
144+ emit_cmp_reg_imm32(code, REG_RSI, TAG_STRING);
145+ int not_string = emit_jcc_rel32(code, COND_NE);
146+ errors_emit_die(code, "runtime error: cannot assign to a string index");
147+ emit_patch_jump(code, not_string);
148+ emit_cmp_reg_imm32(code, REG_RSI, TAG_MAP);
149+ int is_map = emit_jcc_rel32(code, COND_E);
150+ codegen_array_index_store();
151+ int done = emit_jmp_rel32(code);
152+ emit_patch_jump(code, is_map);
153+ codegen_map_index_store();
154+ emit_patch_jump(code, done);
155+}
156+
157+// f(args); -- a call used for its side effect, result discarded.
158+static void call_statement(Token id) {
159+ advance_token(); // consume '('
160+ codegen_call(id);
161+ expect(TOKEN_SEMICOLON, "expected ';' after call");
162+ emit_pop_reg(code, REG_RAX); // discard the (unused) return value
163+ emit_pop_reg(code, REG_RBX);
164+}
165+
166+static int resolve_var_or_error(Token id) {
167+ int slot = resolve_var(id.start, id.length);
168+ if (slot == -1) {
169+ char msg[128];
170+ snprintf(msg, sizeof(msg), "undefined variable: %.*s", id.length, id.start);
171+ parse_error(msg);
172+ }
173+ return slot;
174+}
175+
176+// x += e ; / x -= e ; / x *= e ; / x /= e ; -- sugar for x = x <op> e.
177+// `+=` reuses codegen_apply_plus() (the exact same tag-aware string-concat-
178+// or-numeric-add logic `+` itself uses); `-=`/`*=`/`/=` are NUMBER-only,
179+// same scope limit as `-`/`*`/`/` themselves.
180+static void compound_assignment_statement(Token id, TokenType op) {
181+ int slot = resolve_var_or_error(id);
182+ advance_token(); // consume the += / -= / *= / /=
183+
184+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, var_slot_tag_offset(slot));
185+ emit_push_reg(code, REG_RBX);
186+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
187+ emit_push_reg(code, REG_RAX);
188+
189+ codegen_expression();
190+ expect(TOKEN_SEMICOLON, "expected ';' after assignment");
191+
192+ emit_pop_reg(code, REG_RCX); // b payload
193+ emit_pop_reg(code, REG_RDX); // b tag
194+ emit_pop_reg(code, REG_RAX); // a payload
195+ emit_pop_reg(code, REG_RBX); // a tag
196+
197+ if (op == TOKEN_PLUS_EQUAL) {
198+ codegen_apply_plus();
199+ } else {
200+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
201+ emit_movq_xmm_from_reg(code, XMM1, REG_RCX);
202+ if (op == TOKEN_MINUS_EQUAL) emit_subsd(code, XMM0, XMM1);
203+ else if (op == TOKEN_STAR_EQUAL) emit_mulsd(code, XMM0, XMM1);
204+ else emit_divsd(code, XMM0, XMM1); // TOKEN_SLASH_EQUAL
205+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
206+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
207+ emit_push_reg(code, REG_RBX);
208+ emit_push_reg(code, REG_RAX);
209+ }
210+
211+ emit_pop_reg(code, REG_RAX); // result payload
212+ emit_pop_reg(code, REG_RBX); // result tag
213+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
214+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
215+}
216+
217+// x++ ; / x-- ; -- sugar for x = x + 1 / x = x - 1. Statement-only (not
218+// a pre/post-increment EXPRESSION with a value of its own -- that needs
219+// primary()/postfix() changes this project's scope doesn't call for).
220+static void incdec_statement(Token id, TokenType op) {
221+ int slot = resolve_var_or_error(id);
222+ advance_token(); // consume ++ or --
223+ expect(TOKEN_SEMICOLON, "expected ';' after statement");
224+
225+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, var_slot_payload_offset(slot));
226+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
227+ double one = 1.0;
228+ uint64_t one_bits;
229+ memcpy(&one_bits, &one, sizeof(one_bits));
230+ emit_mov_reg_imm64(code, REG_RBX, one_bits);
231+ emit_movq_xmm_from_reg(code, XMM1, REG_RBX);
232+ if (op == TOKEN_PLUS_PLUS) emit_addsd(code, XMM0, XMM1);
233+ else emit_subsd(code, XMM0, XMM1);
234+ emit_movq_reg_from_xmm(code, REG_RAX, XMM0);
235+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
236+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
237+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
238+}
239+
240+// IDENT = expr ; -- `name` must already be declared via `let`.
241+static void assignment_statement(void) {
242+ Token id = current;
243+ advance_token();
244+ if (current.type == TOKEN_LBRACKET) { index_assignment_statement(id); return; }
245+ if (current.type == TOKEN_LPAREN) { call_statement(id); return; }
246+ if (current.type == TOKEN_PLUS_EQUAL || current.type == TOKEN_MINUS_EQUAL ||
247+ current.type == TOKEN_STAR_EQUAL || current.type == TOKEN_SLASH_EQUAL) {
248+ compound_assignment_statement(id, current.type);
249+ return;
250+ }
251+ if (current.type == TOKEN_PLUS_PLUS || current.type == TOKEN_MINUS_MINUS) {
252+ incdec_statement(id, current.type);
253+ return;
254+ }
255+ expect(TOKEN_EQUAL, "expected '=' after variable name");
256+ codegen_expression();
257+ expect(TOKEN_SEMICOLON, "expected ';' after assignment");
258+
259+ int slot = resolve_var_or_error(id);
260+ emit_pop_reg(code, REG_RAX); // payload
261+ emit_pop_reg(code, REG_RBX); // tag
262+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
263+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
264+}
265+
266+// return [expr] ; -- bare `return;` returns 0, matching ashc/ashvm.
267+// Return value comes back as RBX=tag, RAX=payload (see codegen/expr.c's
268+// call-site handling).
269+static void return_statement(void) {
270+ advance_token();
271+ if (current.type != TOKEN_SEMICOLON) {
272+ codegen_expression();
273+ expect(TOKEN_SEMICOLON, "expected ';' after return");
274+ emit_pop_reg(code, REG_RAX); // payload
275+ emit_pop_reg(code, REG_RBX); // tag
276+ } else {
277+ advance_token();
278+ emit_mov_reg_imm64(code, REG_RAX, 0); // 0.0's bits are all zero
279+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
280+ }
281+ emit_mov_reg_reg(code, REG_RSP, REG_RBP);
282+ emit_pop_reg(code, REG_RBP);
283+ emit_ret(code);
284+}
285+
286+// throw expr ; -- expr must be a STRING (matches try/catch's own
287+// convention: every caught error, built-in or user-thrown, is a plain
288+// string). Reuses codegen/errors.c's raise routine directly -- the exact
289+// same catch/unwind mechanism M9's built-in errors (array bounds, missing
290+// map key) already go through, just with a runtime message instead of a
291+// compile-time-constant one.
292+static void throw_statement(void) {
293+ advance_token();
294+ codegen_expression();
295+ expect(TOKEN_SEMICOLON, "expected ';' after 'throw'");
296+
297+ emit_pop_reg(code, REG_RSI); // message payload (assumed STRING)
298+ emit_pop_reg(code, REG_RBX); // message tag (ignored)
299+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length
300+ errors_emit_die_dynamic(code);
301+}
302+
303+// break ; -- jumps past the innermost loop; the jump is recorded, not
304+// resolved here, since the innermost loop's own codegen (parser_control.c's
305+// while_statement / parser/for_loop.c's for_statement) hasn't emitted the
306+// "after the loop" landing point yet.
307+static void break_statement(void) {
308+ advance_token();
309+ expect(TOKEN_SEMICOLON, "expected ';' after 'break'");
310+ loop_record_break(emit_jmp_rel32(code));
311+}
312+
313+// continue ; -- jumps to the innermost loop's per-iteration advance
314+// step (a for-loop's index increment, or straight back to the condition
315+// for a while loop) -- also recorded, not resolved here, for the same
316+// reason as break above.
317+static void continue_statement(void) {
318+ advance_token();
319+ expect(TOKEN_SEMICOLON, "expected ';' after 'continue'");
320+ loop_record_continue(emit_jmp_rel32(code));
321+}
322+
323+// statement := "print" expr ";"
324+// | "let" IDENT "=" expr ";"
325+// | IDENT "=" expr ";"
326+// | "if" "(" expr ")" block ["else" block]
327+// | "while" "(" expr ")" block
328+// | "fn" IDENT "(" params ")" block
329+// | "return" [expr] ";"
330+// (milestone 5's entire statement grammar -- anything else is a parse
331+// error, per the project rule that malformed input always fails cleanly)
332+void statement(void) {
333+ if (current.type == TOKEN_IMPORT) { import_statement(); return; }
334+ imports_close_block();
335+
336+ const char *ns; int ns_len;
337+ 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");
340+ }
341+
342+ if (current.type == TOKEN_PRINT) {
343+ advance_token();
344+ codegen_expression();
345+ expect(TOKEN_SEMICOLON, "expected ';' after print statement");
346+ codegen_print_top(code);
347+ return;
348+ }
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; }
359+ 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");
361+}
362+
363+void compile_program(const char *source, const char *source_path, CodeBuf *out) {
364+ code = out;
365+ code_init(code);
366+ lexer_init(source);
367+ advance_token();
368+ imports_init(source_path);
369+
370+ // The kernel hands a freshly-started process RSP pointing at
371+ // [argc, argv[0], ..., NULL, envp...] -- this is the ONLY point in
372+ // the whole program where that original RSP is still available
373+ // (nothing has touched it yet), so it's saved to a fixed globals
374+ // address here, before anything else, for argv() to read later from
375+ // any frame (see codegen/argv_builtin.c and elf/elf_writer.h's
376+ // KILN_ARGV_ADDR comment). Windows has no equivalent need: argv()
377+ // there calls GetCommandLineA() directly, callable at any point, so
378+ // there's nothing to snapshot this early.
379+ if (kiln_get_target() != KILN_TARGET_WINDOWS) {
380+ emit_mov_reg_imm64(code, REG_RSI, KILN_ARGV_ADDR);
381+ emit_store_mem_disp32(code, REG_RSI, 0, REG_RSP);
382+ }
383+
384+ // Frame prologue: RBP anchors the top-level program's own variable
385+ // slots. RSP drifts during expression push/pop; RBP never does. Each
386+ // function call below establishes and tears down its own separate
387+ // frame (see parser_control.c's fn_statement/parser.c's
388+ // return_statement) without disturbing this one -- the callee always
389+ // restores RBP before returning. Milestone 5: each slot is now 16
390+ // bytes (tag + payload, see codegen/value.h), not 8.
391+ emit_mov_reg_reg(code, REG_RBP, REG_RSP);
392+ emit_sub_reg_imm32(code, REG_RSP, KILN_FRAME_RESERVE);
393+
394+ heap_emit_startup(code);
395+ bytes_emit_startup(code);
396+ string_alloc_emit_startup(code); // after heap_emit_startup: calls heap_emit_alloc internally
397+ print_emit_startup(code);
398+ errors_emit_startup(code);
399+
400+ while (current.type != TOKEN_EOF) statement();
401+ codegen_exit0(code);
402+}

kiln/src/parser/parser.dadded

+28−0
@@ -0,0 +1,28 @@
1+src/parser/parser.o: src/parser/parser.c src/parser/parser.h \
2+ src/lexer/lexer.h src/codegen/emit.h src/parser/parser_internal.h \
3+ src/parser/vars.h src/parser/loop_stack.h src/codegen/expr.h \
4+ src/codegen/emit_sse.h src/codegen/value.h src/codegen/heap.h \
5+ src/codegen/bytes.h src/codegen/string_alloc.h src/codegen/arrays.h \
6+ src/codegen/maps.h src/codegen/errors.h src/codegen/print_int.h \
7+ src/diagnostics/diagnostics.h src/elf/elf_writer.h \
8+ src/codegen/runtime_layout.h src/target.h
9+src/parser/parser.h:
10+src/lexer/lexer.h:
11+src/codegen/emit.h:
12+src/parser/parser_internal.h:
13+src/parser/vars.h:
14+src/parser/loop_stack.h:
15+src/codegen/expr.h:
16+src/codegen/emit_sse.h:
17+src/codegen/value.h:
18+src/codegen/heap.h:
19+src/codegen/bytes.h:
20+src/codegen/string_alloc.h:
21+src/codegen/arrays.h:
22+src/codegen/maps.h:
23+src/codegen/errors.h:
24+src/codegen/print_int.h:
25+src/diagnostics/diagnostics.h:
26+src/elf/elf_writer.h:
27+src/codegen/runtime_layout.h:
28+src/target.h:

kiln/src/parser/parser.hadded

+23−0
@@ -0,0 +1,23 @@
1+#ifndef KILN_PARSER_H
2+#define KILN_PARSER_H
3+#include "lexer/lexer.h"
4+#include "codegen/emit.h"
5+
6+// Shared parser state, read by codegen/expr.c as well as parser.c --
7+// mirrors ashvm/src/compiler/state.h's split (state lives in a small
8+// header, expression codegen and statement parsing both consume it).
9+extern Token current;
10+extern Token previous;
11+extern CodeBuf *code; // the buffer currently being emitted into
12+
13+void advance_token(void);
14+void expect(TokenType type, const char *message);
15+void parse_error(const char *message) __attribute__((noreturn));
16+
17+// Parses the whole program (milestone 1: a sequence of `print <expr>;`
18+// statements) and emits machine code for it into `out`, single-pass --
19+// parsing and codegen happen together, same shape ashvm's compiler uses,
20+// no separate AST.
21+void compile_program(const char *source, const char *source_path, CodeBuf *out);
22+
23+#endif

kiln/src/parser/parser_control.cadded

+167−0
@@ -0,0 +1,167 @@
1+#include <string.h>
2+#include "parser/parser.h"
3+#include "parser/parser_internal.h"
4+#include "parser/vars.h"
5+#include "parser/functions.h"
6+#include "parser/for_loop.h"
7+#include "parser/loop_stack.h"
8+#include "codegen/expr.h"
9+#include "codegen/emit_sse.h"
10+#include "codegen/value.h"
11+
12+// Truthiness test for if/while conditions: pop the value (tag discarded,
13+// assumed NUMBER -- matches ashvm's own truthy(), which requires a
14+// number too) and compare its payload against 0.0 via ucomisd (not GP
15+// cmp -- see emit_sse.h). COND_E afterward means "was zero, i.e. falsy".
16+static void pop_and_test_truthy(void) {
17+ emit_pop_reg(code, REG_RAX); // payload
18+ emit_pop_reg(code, REG_RBX); // tag (assumed NUMBER)
19+ emit_movq_xmm_from_reg(code, XMM0, REG_RAX);
20+ emit_pxor_xmm_xmm(code, XMM1); // XMM1 = +0.0
21+ emit_ucomisd(code, XMM0, XMM1);
22+}
23+
24+// The statement kinds involved enough to warrant their own file (mirrors
25+// ashvm/src/compiler/stmt_control.c's split from stmt.c for the same
26+// reason). Each function assumes statement()'s dispatch in parser.c
27+// already consumed the keyword token that identified it.
28+
29+void if_statement(void) {
30+ advance_token();
31+ expect(TOKEN_LPAREN, "expected '(' after 'if'");
32+ codegen_expression();
33+ expect(TOKEN_RPAREN, "expected ')' after condition");
34+ pop_and_test_truthy();
35+ int else_jump = emit_jcc_rel32(code, COND_E);
36+
37+ block();
38+ if (current.type == TOKEN_ELSE) {
39+ int end_jump = emit_jmp_rel32(code);
40+ emit_patch_jump(code, else_jump);
41+ advance_token();
42+ block();
43+ emit_patch_jump(code, end_jump);
44+ } else {
45+ emit_patch_jump(code, else_jump);
46+ }
47+}
48+
49+void while_statement(void) {
50+ advance_token();
51+ expect(TOKEN_LPAREN, "expected '(' after 'while'");
52+ int loop_start = code->count;
53+ codegen_expression();
54+ expect(TOKEN_RPAREN, "expected ')' after condition");
55+ pop_and_test_truthy();
56+ int exit_jump = emit_jcc_rel32(code, COND_E);
57+
58+ loop_push();
59+ block();
60+ loop_patch_continues(); // continue lands here, right before the loop-back
61+ emit_jmp_back(code, loop_start);
62+ emit_patch_jump(code, exit_jump);
63+ loop_pop_and_patch_breaks(); // break lands here, after the loop
64+}
65+
66+// fn NAME ( [IDENT (, IDENT)*] ) block
67+//
68+// Function bodies are compiled inline into the SAME flat CodeBuf as
69+// top-level code (unlike ashvm, which gives each function its own Chunk)
70+// -- so declaring one emits an unconditional jump OVER the body first,
71+// making sure normal top-to-bottom execution never falls into a function
72+// it didn't explicitly call. The jump is patched once the body's end is
73+// known.
74+//
75+// Calling convention (kiln's own -- nothing external ever calls into
76+// generated code, so there's no ABI to match): caller pushes each
77+// argument's (tag, payload) pair -- 16 bytes, milestone 5 -- left to
78+// right via the normal expression codegen, no special reordering needed
79+// (see the offset derivation below), then `call`s the callee's
80+// already-known code offset (functions can only call ones declared
81+// earlier in the source, so the target is always known -- see
82+// emit_call_back). The standard push-rbp/mov-rbp,rsp/sub-rsp prologue
83+// establishes a fresh frame; the FIRST thing the body does is copy each
84+// argument's tag AND payload off the caller's pushed stack values into
85+// its own local slot, so afterward every variable access -- parameter or
86+// `let` -- goes through the exact same var_slot_*_offset() formulas.
87+// Because the last argument is the last one pushed (closest to the
88+// return address), and the first argument is deepest, parameter i's tag
89+// sits at [rbp + 16 + 16*(argc-1-i)] and its payload right after that
90+// (+16 skips the saved rbp and return address). Return value comes back
91+// as RBX=tag, RAX=payload (see parser.c's return_statement).
92+void fn_statement(void) {
93+ advance_token();
94+ expect(TOKEN_IDENTIFIER, "expected function name after 'fn'");
95+ const char *name = previous.start;
96+ int name_len = previous.length;
97+ KilnFunction *fn = declare_function_in_context(name, name_len);
98+
99+ expect(TOKEN_LPAREN, "expected '(' after function name");
100+ char param_names[MAX_KILN_PARAMS][64];
101+ int param_lens[MAX_KILN_PARAMS];
102+ int argc = 0;
103+ if (current.type != TOKEN_RPAREN) {
104+ for (;;) {
105+ expect(TOKEN_IDENTIFIER, "expected parameter name");
106+ if (argc >= MAX_KILN_PARAMS) parse_error("too many parameters");
107+ param_lens[argc] = previous.length;
108+ memcpy(param_names[argc], previous.start, (size_t)previous.length);
109+ argc++;
110+ if (current.type != TOKEN_COMMA) break;
111+ advance_token();
112+ }
113+ }
114+ expect(TOKEN_RPAREN, "expected ')' after parameters");
115+ fn->arity = argc;
116+
117+ int skip_jump = emit_jmp_rel32(code);
118+ fn->code_offset = code->count;
119+
120+ VarScope outer_vars;
121+ vars_save(&outer_vars);
122+ vars_clear();
123+ int outer_for_depth;
124+ for_depth_save(&outer_for_depth);
125+ for_depth_reset();
126+ int outer_loop_depth;
127+ loop_depth_save(&outer_loop_depth);
128+ loop_depth_reset();
129+
130+ emit_push_reg(code, REG_RBP);
131+ emit_mov_reg_reg(code, REG_RBP, REG_RSP);
132+ emit_sub_reg_imm32(code, REG_RSP, KILN_FRAME_RESERVE);
133+
134+ for (int i = 0; i < argc; i++) {
135+ int slot = declare_var(param_names[i], param_lens[i]);
136+ int arg_base = 16 + 16 * (argc - 1 - i);
137+ // Within each argument's 16-byte block, payload sits at the LOWER
138+ // offset: the caller pushed tag-then-payload for each arg, and
139+ // since push grows the stack downward, whatever's pushed SECOND
140+ // (payload) ends up closer to rbp. (Everywhere else this project
141+ // pops in LIFO order, which gets this right automatically --
142+ // here the callee reads the caller's still-live stack region
143+ // directly instead of popping, so the offset had to be derived
144+ // by hand, and got it backwards on the first pass.)
145+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, arg_base);
146+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);
147+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, arg_base + 8);
148+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);
149+ }
150+
151+ block();
152+
153+ // Implicit `return 0;` if the body falls through without one --
154+ // dead code on any path that already returned explicitly, harmless
155+ // (matches ashvm's compile_fn_decl, which does the same
156+ // unconditionally after compiling the body).
157+ emit_mov_reg_imm64(code, REG_RAX, 0); // 0.0's bits are all zero
158+ emit_mov_reg_imm64(code, REG_RBX, TAG_NUMBER);
159+ emit_mov_reg_reg(code, REG_RSP, REG_RBP);
160+ emit_pop_reg(code, REG_RBP);
161+ emit_ret(code);
162+
163+ vars_restore(&outer_vars);
164+ for_depth_restore(outer_for_depth);
165+ loop_depth_restore(outer_loop_depth);
166+ emit_patch_jump(code, skip_jump);
167+}

kiln/src/parser/parser_control.dadded

+16−0
@@ -0,0 +1,16 @@
1+src/parser/parser_control.o: src/parser/parser_control.c \
2+ src/parser/parser.h src/lexer/lexer.h src/codegen/emit.h \
3+ src/parser/parser_internal.h src/parser/vars.h src/parser/functions.h \
4+ src/parser/for_loop.h src/parser/loop_stack.h src/codegen/expr.h \
5+ src/codegen/emit_sse.h src/codegen/value.h
6+src/parser/parser.h:
7+src/lexer/lexer.h:
8+src/codegen/emit.h:
9+src/parser/parser_internal.h:
10+src/parser/vars.h:
11+src/parser/functions.h:
12+src/parser/for_loop.h:
13+src/parser/loop_stack.h:
14+src/codegen/expr.h:
15+src/codegen/emit_sse.h:
16+src/codegen/value.h:

kiln/src/parser/parser_internal.hadded

+23−0
@@ -0,0 +1,23 @@
1+#ifndef KILN_PARSER_INTERNAL_H
2+#define KILN_PARSER_INTERNAL_H
3+
4+// Cross-file link between parser.c (state, simple statements, the
5+// statement() dispatcher) and parser_control.c (if/while/fn -- the
6+// statement kinds involved enough to warrant their own file), not part of
7+// the public API (parser.h).
8+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);
14+
15+// statement() itself (parser.c's dispatcher) also needs to be callable
16+// from imports.c's parse_imported_file_body(), which drives the same
17+// top-level "while not EOF, parse a statement" loop for a recursively
18+// parsed imported file -- so it's declared here rather than staying
19+// static to parser.c.
20+void statement(void);
21+void import_statement(void);
22+
23+#endif

kiln/src/parser/try_catch.cadded

+86−0
@@ -0,0 +1,86 @@
1+#include "parser/parser.h"
2+#include "parser/parser_internal.h"
3+#include "parser/vars.h"
4+#include "codegen/emit.h"
5+#include "codegen/errors.h"
6+#include "codegen/layout.h"
7+#include "codegen/runtime_layout.h"
8+
9+// try { A } catch (e) { B }
10+//
11+// A handler stack living in the fixed globals region (see
12+// elf/elf_writer.h) holds, per active try, the (rsp, rbp) to restore and
13+// the (target address, error-variable rbp-offset) to resume at -- kiln's
14+// own setjmp/longjmp equivalent, hand-rolled since this is a freestanding
15+// binary with no libc to call into (see codegen/errors.c's raise routine
16+// for the runtime half of this).
17+//
18+// The catch variable's slot has to be reserved BEFORE compiling A: its
19+// rbp-relative offset must be embedded into the handler struct at
20+// try-entry, which runs before A's body -- and therefore before `e`'s
21+// real name, which only appears after A -- is even parsed. A placeholder
22+// 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 call
24+// frame, so this works the same way if/while's own bodies share the
25+// 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 would
33+ // 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 size
41+ 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 address
43+
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 known
47+ 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 A
62+
63+ // Normal completion (no error was raised): pop the handler -- past
64+ // this point, an error inside the enclosing scope should NOT be
65+ // 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 above
83+
84+ emit_patch_jump(code, skip_catch);
85+ vars_scope_end(saved_scope);
86+}

kiln/src/parser/try_catch.dadded

+12−0
@@ -0,0 +1,12 @@
1+src/parser/try_catch.o: src/parser/try_catch.c src/parser/parser.h \
2+ src/lexer/lexer.h src/codegen/emit.h src/parser/parser_internal.h \
3+ src/parser/vars.h src/codegen/errors.h src/codegen/layout.h \
4+ src/codegen/runtime_layout.h
5+src/parser/parser.h:
6+src/lexer/lexer.h:
7+src/codegen/emit.h:
8+src/parser/parser_internal.h:
9+src/parser/vars.h:
10+src/codegen/errors.h:
11+src/codegen/layout.h:
12+src/codegen/runtime_layout.h:

kiln/src/parser/vars.cadded

+88−0
@@ -0,0 +1,88 @@
1+#include <string.h>
2+#include "parser/vars.h"
3+#include "parser/parser.h"
4+
5+static char var_names[MAX_KILN_VARS][64];
6+static int var_count = 0;
7+static int scope_start = 0;
8+
9+// Scans newest-first (most-recently-declared wins), standard shadowing
10+// semantics -- required for try/catch: parser/try_catch.c reserves each
11+// catch variable's slot with a synthetic placeholder name and renames it
12+// to the real name only after the fact (the slot's rbp-offset has to be
13+// known before the try body compiles, before the real name is even
14+// parsed -- see try_catch.c's header comment), so two SEQUENTIAL (not
15+// nested) `catch (e)` blocks end up with two separate table entries both
16+// named "e". A first-match-wins scan would bind the second catch body's
17+// references to the FIRST catch's stale slot; newest-first fixes that
18+// while leaving `let`'s own reuse-if-already-declared behavior unchanged
19+// (there's normally only one live entry for a given `let` name anyway).
20+int resolve_var(const char *name, int len) {
21+ for (int i = var_count - 1; i >= 0; i--) {
22+ if ((int)strlen(var_names[i]) == len && strncmp(var_names[i], name, len) == 0) return i;
23+ }
24+ return -1;
25+}
26+
27+int resolve_var_in_current_scope(const char *name, int len) {
28+ for (int i = var_count - 1; i >= scope_start; i--) {
29+ if ((int)strlen(var_names[i]) == len && strncmp(var_names[i], name, len) == 0) return i;
30+ }
31+ return -1;
32+}
33+
34+int declare_var(const char *name, int len) {
35+ if (var_count >= MAX_KILN_VARS) parse_error("too many variables");
36+ memcpy(var_names[var_count], name, (size_t)len);
37+ var_names[var_count][len] = '\0';
38+ return var_count++;
39+}
40+
41+int32_t var_slot_tag_offset(int slot) { return -16 * (slot + 1); }
42+int32_t var_slot_payload_offset(int slot) { return var_slot_tag_offset(slot) + 8; }
43+
44+void vars_save(VarScope *out) {
45+ memcpy(out->names, var_names, sizeof(var_names));
46+ out->count = var_count;
47+ out->scope_start = scope_start;
48+}
49+
50+void vars_clear(void) { var_count = 0; scope_start = 0; }
51+
52+void vars_restore(const VarScope *saved) {
53+ memcpy(var_names, saved->names, sizeof(var_names));
54+ var_count = saved->count;
55+ scope_start = saved->scope_start;
56+}
57+
58+VarBlockScope vars_scope_begin(void) {
59+ VarBlockScope saved = { .count_mark = var_count, .prev_scope_start = scope_start };
60+ scope_start = var_count;
61+ return saved;
62+}
63+
64+void vars_scope_end(VarBlockScope saved) {
65+ var_count = saved.count_mark;
66+ scope_start = saved.prev_scope_start;
67+}
68+
69+int32_t call_scratch_offset(void) { return var_slot_tag_offset(MAX_KILN_VARS); }
70+
71+int32_t higher_order_scratch_offset(void) { return call_scratch_offset() - KILN_HIGHER_ORDER_SCRATCH_SIZE; }
72+
73+int32_t for_level_offset(int depth) {
74+ return higher_order_scratch_offset() - KILN_FOR_STACK_SIZE + depth * KILN_FOR_LEVEL_SIZE;
75+}
76+
77+int32_t win_call_scratch_offset(void) {
78+ return higher_order_scratch_offset() - KILN_FOR_STACK_SIZE - KILN_WIN_CALL_SCRATCH_SIZE;
79+}
80+
81+int32_t win_outparam_offset(void) {
82+ return win_call_scratch_offset() - KILN_WIN_OUTPARAM_SCRATCH_SIZE;
83+}
84+
85+void rename_var(int slot, const char *name, int len) {
86+ memcpy(var_names[slot], name, (size_t)len);
87+ var_names[slot][len] = '\0';
88+}

kiln/src/parser/vars.dadded

+6−0
@@ -0,0 +1,6 @@
1+src/parser/vars.o: src/parser/vars.c src/parser/vars.h \
2+ src/parser/parser.h src/lexer/lexer.h src/codegen/emit.h
3+src/parser/vars.h:
4+src/parser/parser.h:
5+src/lexer/lexer.h:
6+src/codegen/emit.h:

kiln/src/parser/vars.hadded

+137−0
@@ -0,0 +1,137 @@
1+#ifndef KILN_VARS_H
2+#define KILN_VARS_H
3+#include <stdint.h>
4+
5+// Flat name -> stack-slot table for milestone 2's single-scope variable
6+// model (no nested block scoping yet -- see the plan for why that's
7+// deliberately deferred). Mirrors ashvm/src/compiler/state.c's
8+// resolve_local/declare_local, minus the per-function scope stack.
9+#define MAX_KILN_VARS 64
10+
11+// Returns the slot index for `name`, or -1 if not yet declared.
12+int resolve_var(const char *name, int len);
13+
14+// Declares a new variable and returns its slot index. Calls parse_error()
15+// (noreturn) if the fixed slot table is full.
16+int declare_var(const char *name, int len);
17+
18+// Milestone 9: overwrites an already-declared slot's name in place (same
19+// slot index, new name) -- parser/try_catch.c needs a catch variable's
20+// slot reserved BEFORE its real name (`catch (e)`) is parsed, since the
21+// slot's rbp-relative offset must be embedded into the handler struct at
22+// try-entry, ahead of the try block's body.
23+void rename_var(int slot, const char *name, int len);
24+
25+// Milestone 5: each variable now holds a (tag, payload) pair (16 bytes,
26+// see codegen/value.h), not a bare 8-byte double -- so each slot takes
27+// 16 bytes: tag at [rbp - 16*(i+1)], payload right after it at
28+// [rbp - 16*(i+1) + 8], below the frame pointer set up by parser.c's
29+// prologue (`mov rbp, rsp; sub rsp, MAX_KILN_VARS*16`). Shared here so
30+// expr.c and parser.c can't drift on the formula.
31+int32_t var_slot_tag_offset(int slot);
32+int32_t var_slot_payload_offset(int slot);
33+
34+// Milestone 3: each function call gets its own fresh frame, so each
35+// function body needs its own fresh variable table too -- no visibility
36+// into the caller's (or top-level program's) variables yet (that needs
37+// closures, milestone 8). save/clear/restore lets parser.c's fn-statement
38+// codegen swap tables the same way ashvm's compile_fn_decl swaps
39+// local_names/local_count around compiling a function body.
40+typedef struct {
41+ char names[MAX_KILN_VARS][64];
42+ int count;
43+ int scope_start;
44+} VarScope;
45+
46+void vars_save(VarScope *out);
47+void vars_clear(void);
48+void vars_restore(const VarScope *saved);
49+
50+// Block scoping: `scope_start` marks the slot index where the innermost
51+// active block began. vars_scope_begin/end bracket a single block() call
52+// (or, for for/try statements whose own loop/catch variable is declared
53+// outside their nested block() call -- see for_loop.c/try_catch.c -- the
54+// whole statement). Same save-an-int-on-the-C-call-stack pattern as
55+// for_depth_save/restore and loop_depth_save/restore above: block() nests
56+// via ordinary recursive descent, so the C call stack gives correct
57+// nesting for free, no explicit stack structure needed.
58+typedef struct { int count_mark; int prev_scope_start; } VarBlockScope;
59+
60+VarBlockScope vars_scope_begin(void);
61+void vars_scope_end(VarBlockScope saved);
62+
63+// Like resolve_var, but only considers slots >= scope_start (the CURRENT
64+// block's own declarations). let_statement/for_statement use this for
65+// their "reuse this slot or declare fresh" decision: a name that only
66+// exists in an ENCLOSING scope must shadow (fresh slot), not mutate.
67+int resolve_var_in_current_scope(const char *name, int len);
68+
69+// Milestone 8: one extra RBP-relative slot beyond the normal variable
70+// table, reserved for codegen/closures.c's indirect-call codegen to stash
71+// a runtime-only capture_count across the `call` instruction (GP
72+// registers are all freely clobbered by the callee; RBP is the one thing
73+// provably restored to the caller's own value by the time any call
74+// returns, per the calling convention every milestone since M3 relies
75+// on).
76+#define KILN_CALL_SCRATCH_SIZE 16
77+int32_t call_scratch_offset(void);
78+
79+// Milestone 10: a further 64-byte RBP-relative scratch region for
80+// map/filter/reduce's loop state (element pointer, count, running index,
81+// accumulator, output block address...) -- same "RBP survives a call,
82+// registers don't" reasoning as call_scratch_offset. Reused by whichever
83+// of the three is currently running with builtin-specific field meanings
84+// (see codegen/higher_order_*.c) since they're never concurrently active
85+// within the same frame -- source-level arguments are evaluated
86+// left-to-right, so a nested call like `map(map(a,f),g)` has the inner
87+// call's loop fully finish before the outer one's begins. A callback
88+// that itself calls another higher-order builtin is safe for a different
89+// reason: it runs in ITS OWN frame (its own rbp from its own call
90+// prologue), so its scratch use lands at a completely different address.
91+#define KILN_HIGHER_ORDER_SCRATCH_SIZE 128
92+int32_t higher_order_scratch_offset(void);
93+
94+// Milestone 11: `for (x in array)` loop state (data_ptr, count, running
95+// index) also needs to survive across the loop body's own codegen -- but
96+// unlike try/catch's runtime recursion, for-loop NESTING is fully known
97+// at COMPILE TIME (nested `for` statements are literally nested calls in
98+// this single-pass compiler), so each nesting level just gets its own
99+// fixed offset rather than a runtime-tracked stack. Bounded the same way
100+// every other table here is; parser/for_loop.c resets its nesting
101+// counter to 0 when entering a function or lambda body (mirroring
102+// vars_save/vars_clear/vars_restore), since that body gets its own RBP
103+// at runtime and can safely reuse the same offsets independently of how
104+// deep the enclosing code's own `for` nesting was.
105+#define MAX_KILN_FOR_DEPTH 8
106+#define KILN_FOR_LEVEL_SIZE 24
107+#define KILN_FOR_STACK_SIZE (MAX_KILN_FOR_DEPTH * KILN_FOR_LEVEL_SIZE)
108+int32_t for_level_offset(int depth);
109+
110+// Plan A, phase A3: one 8-byte RBP-relative slot for codegen/win_call.c to
111+// stash the original RSP across a Windows API call (dynamic 16-byte align
112+// + shadow space reservation clobbers RSP by an amount only known at
113+// runtime, so it can't be undone with a fixed `add rsp, N` -- see
114+// win_call.c). Same "RBP survives a call, registers don't, and each
115+// function frame gets its own copy" reasoning as every other scratch slot
116+// here; a single slot suffices since one win_call sequence always fully
117+// completes (there's no way for the compiled program to interleave two)
118+// before another can start.
119+#define KILN_WIN_CALL_SCRATCH_SIZE 8
120+int32_t win_call_scratch_offset(void);
121+
122+// A second, separate slot: several Windows API calls (WriteFile,
123+// ReadFile) require a valid out-param pointer for a DWORD kiln has no use
124+// for (bytes actually written/read -- kiln already knows this from the
125+// length it requested, matching the "single read/write call assumed to
126+// fully succeed" simplification already documented in the Linux syscall
127+// call sites). Separate from win_call_scratch_offset because that slot is
128+// busy holding the saved RSP for the entire win_call_begin/end window;
129+// this one's only live transiently while such a call is being set up.
130+#define KILN_WIN_OUTPARAM_SCRATCH_SIZE 8
131+int32_t win_outparam_offset(void);
132+
133+#define KILN_FRAME_RESERVE \
134+ ((MAX_KILN_VARS + 1) * 16 + KILN_HIGHER_ORDER_SCRATCH_SIZE + KILN_FOR_STACK_SIZE + \
135+ KILN_WIN_CALL_SCRATCH_SIZE + KILN_WIN_OUTPARAM_SCRATCH_SIZE)
136+
137+#endif

kiln/src/pe/pe_dll_writer.cadded

+169−0
@@ -0,0 +1,169 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "pe/pe_dll_writer.h"
5+#include "codegen/heap.h"
6+#include "codegen/bytes.h"
7+#include "codegen/string_alloc.h"
8+#include "codegen/print_int.h"
9+#include "codegen/errors.h"
10+#include "target.h"
11+
12+// Hand-written PE32+ DLL: IMAGE_FILE_DLL characteristic, an Export Data
13+// Directory instead of an Import one -- the Windows-side twin of
14+// elf/elf_so_writer.c. The Windows loader resolves imports-by-name via a
15+// BINARY SEARCH over the DLL's Name Pointer Table, which means (unlike
16+// ELF's hash-table symbol lookup) that table must be in sorted order, or
17+// GetProcAddress-style resolution silently fails to find some exports --
18+// see the sort below.
19+
20+static const char *const export_names[] = {
21+#define RUNTIME_EXPORT(name, str) str,
22+#include "codegen/runtime_exports.def"
23+#undef RUNTIME_EXPORT
24+};
25+
26+static void put8(CodeBuf *f, uint8_t v) {
27+ if (f->count >= f->capacity) {
28+ f->capacity = f->capacity == 0 ? 256 : f->capacity * 2;
29+ f->code = realloc(f->code, (size_t)f->capacity);
30+ }
31+ f->code[f->count++] = v;
32+}
33+static void put16(CodeBuf *f, uint16_t v) { for (int i = 0; i < 2; i++) put8(f, (uint8_t)(v >> (8 * i))); }
34+static void put32(CodeBuf *f, uint32_t v) { for (int i = 0; i < 4; i++) put8(f, (uint8_t)(v >> (8 * i))); }
35+static void put64(CodeBuf *f, uint64_t v) { for (int i = 0; i < 8; i++) put8(f, (uint8_t)(v >> (8 * i))); }
36+static void put_zeros(CodeBuf *f, uint32_t n) { for (uint32_t i = 0; i < n; i++) put8(f, 0); }
37+static void put_str(CodeBuf *f, const char *s) { for (const char *p = s; *p; p++) put8(f, (uint8_t)*p); put8(f, 0); }
38+static uint32_t align_up(uint32_t v, uint32_t align) { return (v + align - 1) / align * align; }
39+
40+int pe_write_shared_runtime(const char *path) {
41+ kiln_set_target(KILN_TARGET_WINDOWS);
42+ kiln_set_link_mode(KILN_LINK_SHARED);
43+ kiln_set_compiling_so(1);
44+
45+ CodeBuf runtime;
46+ code_init(&runtime);
47+ heap_emit_alloc_routine_only(&runtime);
48+ bytes_emit_copy_routine_only(&runtime);
49+ string_alloc_emit_prefixed_routine_only(&runtime);
50+ print_emit_top_routine_only(&runtime);
51+ errors_emit_raise_routine_only(&runtime);
52+
53+ int offsets[RUNTIME_IMPORT_COUNT_];
54+ offsets[RUNTIME_IMPORT_HEAP_ALLOC] = heap_alloc_routine_offset();
55+ offsets[RUNTIME_IMPORT_BYTES_COPY] = bytes_copy_routine_offset();
56+ offsets[RUNTIME_IMPORT_STRING_ALLOC_PREFIXED] = string_alloc_prefixed_routine_offset();
57+ offsets[RUNTIME_IMPORT_PRINT_TOP] = print_top_routine_offset();
58+ offsets[RUNTIME_IMPORT_RAISE] = errors_raise_routine_offset();
59+
60+ kiln_set_compiling_so(0);
61+
62+ // Sort export indices by name (lexicographic byte compare, matching
63+ // the Windows loader's own binary search over the Name Pointer Table).
64+ int sorted[RUNTIME_IMPORT_COUNT_];
65+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) sorted[i] = i;
66+ for (int i = 1; i < RUNTIME_IMPORT_COUNT_; i++) {
67+ int key = sorted[i], j = i - 1;
68+ while (j >= 0 && strcmp(export_names[sorted[j]], export_names[key]) > 0) { sorted[j + 1] = sorted[j]; j--; }
69+ sorted[j + 1] = key;
70+ }
71+
72+ uint32_t code_rva = KILN_PE_DLL_CODE_START_OFFSET;
73+ uint32_t name_off[RUNTIME_IMPORT_COUNT_]; // RVA of each export's name string, indexed by ORIGINAL (enum) order
74+ uint32_t cursor = code_rva + (uint32_t)runtime.count;
75+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) { name_off[i] = cursor; cursor += (uint32_t)strlen(export_names[i]) + 1; }
76+ uint32_t dll_name_rva = cursor; cursor += (uint32_t)strlen(KILN_LIBKILNRT_DLL_NAME) + 1;
77+
78+ uint32_t section_virtual_size = cursor - KILN_PE_DLL_SECTION_RVA;
79+ uint32_t section_raw_size = align_up(section_virtual_size, KILN_PE_FILE_ALIGN);
80+ uint32_t image_size = align_up(KILN_PE_DLL_SECTION_RVA + section_virtual_size, KILN_PE_SECTION_ALIGN);
81+
82+ CodeBuf f;
83+ code_init(&f);
84+
85+ put8(&f, 'M'); put8(&f, 'Z');
86+ put_zeros(&f, 58);
87+ put32(&f, 64);
88+
89+ put8(&f, 'P'); put8(&f, 'E'); put8(&f, 0); put8(&f, 0);
90+ put16(&f, 0x8664);
91+ put16(&f, 1); // NumberOfSections
92+ put32(&f, 0); put32(&f, 0); put32(&f, 0);
93+ put16(&f, 240);
94+ put16(&f, 0x0002 | 0x2000); // EXECUTABLE_IMAGE | DLL
95+
96+ put16(&f, 0x20B);
97+ put8(&f, 14); put8(&f, 0);
98+ put32(&f, section_raw_size);
99+ put32(&f, 0); put32(&f, 0);
100+ put32(&f, 0); // AddressOfEntryPoint = 0 (no DllMain, nothing to run at load/unload)
101+ put32(&f, KILN_PE_DLL_SECTION_RVA); // BaseOfCode
102+ put64(&f, KILN_PE_DLL_IMAGE_BASE);
103+ put32(&f, KILN_PE_SECTION_ALIGN);
104+ put32(&f, KILN_PE_FILE_ALIGN);
105+ put16(&f, 6); put16(&f, 0);
106+ put16(&f, 0); put16(&f, 0);
107+ put16(&f, 6); put16(&f, 0);
108+ put32(&f, 0);
109+ put32(&f, image_size);
110+ put32(&f, KILN_PE_HEADERS_SIZE);
111+ put32(&f, 0);
112+ put16(&f, 3); // Subsystem = WINDOWS_CUI (inherited from the loading process; irrelevant for a DLL but harmless)
113+ put16(&f, 0); // DllCharacteristics -- no DYNAMIC_BASE, fixed base like everything else kiln emits
114+ put64(&f, 0x100000); put64(&f, 0x1000);
115+ put64(&f, 0x100000); put64(&f, 0x1000);
116+ put32(&f, 0);
117+ put32(&f, 16);
118+ for (int i = 0; i < 16; i++) {
119+ if (i == 0) { put32(&f, KILN_PE_DLL_EXPORT_DIR_ADDR - KILN_PE_DLL_IMAGE_BASE); put32(&f, KILN_PE_DLL_EXPORT_DIR_SIZE); } // Export Table
120+ else { put32(&f, 0); put32(&f, 0); }
121+ }
122+
123+ const char *sec_name = ".text";
124+ for (int i = 0; i < 8; i++) put8(&f, (uint8_t)(i < (int)strlen(sec_name) ? sec_name[i] : 0));
125+ put32(&f, section_virtual_size);
126+ put32(&f, KILN_PE_DLL_SECTION_RVA);
127+ put32(&f, section_raw_size);
128+ put32(&f, KILN_PE_HEADERS_SIZE);
129+ put32(&f, 0); put32(&f, 0);
130+ put16(&f, 0); put16(&f, 0);
131+ put32(&f, 0x20u | 0x20000000u | 0x40000000u | 0x80000000u);
132+
133+ while (f.count < (int)KILN_PE_HEADERS_SIZE) put8(&f, 0);
134+
135+ // IMAGE_EXPORT_DIRECTORY
136+ put32(&f, 0); // Characteristics
137+ put32(&f, 0); // TimeDateStamp
138+ put16(&f, 0); put16(&f, 0); // Major/MinorVersion
139+ put32(&f, dll_name_rva); // Name
140+ put32(&f, 1); // Base (ordinals start at 1)
141+ put32(&f, (uint32_t)RUNTIME_IMPORT_COUNT_); // NumberOfFunctions
142+ put32(&f, (uint32_t)RUNTIME_IMPORT_COUNT_); // NumberOfNames
143+ put32(&f, KILN_PE_DLL_FUNC_ARRAY_ADDR - KILN_PE_DLL_IMAGE_BASE);
144+ put32(&f, KILN_PE_DLL_NAME_ARRAY_ADDR - KILN_PE_DLL_IMAGE_BASE);
145+ put32(&f, KILN_PE_DLL_ORDINAL_ARRAY_ADDR - KILN_PE_DLL_IMAGE_BASE);
146+
147+ // AddressOfFunctions: natural (enum) order, RVA of each routine's entry point
148+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put32(&f, code_rva + (uint32_t)offsets[i]);
149+ // AddressOfNames: SORTED order (binary search requirement)
150+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put32(&f, name_off[sorted[i]]);
151+ // AddressOfNameOrdinals: for each sorted name, the (0-based) index into AddressOfFunctions
152+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put16(&f, (uint16_t)sorted[i]);
153+
154+ for (int i = 0; i < runtime.count; i++) put8(&f, runtime.code[i]);
155+ free(runtime.code);
156+
157+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put_str(&f, export_names[i]);
158+ put_str(&f, KILN_LIBKILNRT_DLL_NAME);
159+
160+ while (f.count < (int)(KILN_PE_HEADERS_SIZE + section_raw_size)) put8(&f, 0);
161+
162+ FILE *out = fopen(path, "wb");
163+ if (!out) { fprintf(stderr, "kiln: could not open '%s' for writing\n", path); free(f.code); return 1; }
164+ size_t written = fwrite(f.code, 1, (size_t)f.count, out);
165+ fclose(out);
166+ free(f.code);
167+ if (written != (size_t)f.count) { fprintf(stderr, "kiln: short write to '%s'\n", path); return 1; }
168+ return 0;
169+}

kiln/src/pe/pe_dll_writer.dadded

+19−0
@@ -0,0 +1,19 @@
1+src/pe/pe_dll_writer.o: src/pe/pe_dll_writer.c src/pe/pe_dll_writer.h \
2+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
3+ src/pe/pe_writer.h src/codegen/emit.h src/codegen/runtime_layout.h \
4+ src/pe/pe_imports.def src/codegen/heap.h src/codegen/bytes.h \
5+ src/codegen/string_alloc.h src/codegen/print_int.h src/codegen/errors.h \
6+ src/target.h
7+src/pe/pe_dll_writer.h:
8+src/codegen/runtime_import_enum.h:
9+src/codegen/runtime_exports.def:
10+src/pe/pe_writer.h:
11+src/codegen/emit.h:
12+src/codegen/runtime_layout.h:
13+src/pe/pe_imports.def:
14+src/codegen/heap.h:
15+src/codegen/bytes.h:
16+src/codegen/string_alloc.h:
17+src/codegen/print_int.h:
18+src/codegen/errors.h:
19+src/target.h:

kiln/src/pe/pe_dll_writer.hadded

+39−0
@@ -0,0 +1,39 @@
1+#ifndef KILN_PE_DLL_WRITER_H
2+#define KILN_PE_DLL_WRITER_H
3+#include "codegen/runtime_import_enum.h"
4+#include "pe/pe_writer.h"
5+
6+// libkilnrt.dll: same fixed-base, no-ASLR, no-relocations philosophy as
7+// every other kiln output (a real DLL would normally get a .reloc
8+// section so the loader can rebase it if its preferred address is
9+// taken; kiln skips that the same way it skips PIC/ASLR support for its
10+// own executables -- consistent, not a shortcut specific to this file).
11+// Distinct base from KILN_PE_IMAGE_BASE (the executable's own) since
12+// both could conceivably be loaded into the same process.
13+#define KILN_PE_DLL_IMAGE_BASE 0x180000000ULL
14+#define KILN_PE_DLL_SECTION_RVA KILN_PE_SECTION_ALIGN
15+
16+// IMAGE_EXPORT_DIRECTORY (40 bytes) + 3 fixed-size arrays (sized only by
17+// RUNTIME_IMPORT_COUNT_, never by name length) -- code starts right after,
18+// so its address is a compile-time constant kiln_code_base() can use
19+// (see codegen/layout.c); the variable-length name strings go in the
20+// tail after code, same trick as pe_writer.h's hint/name table.
21+#define KILN_PE_DLL_EXPORT_DIR_ADDR (KILN_PE_DLL_IMAGE_BASE + KILN_PE_DLL_SECTION_RVA)
22+#define KILN_PE_DLL_EXPORT_DIR_SIZE 40u
23+#define KILN_PE_DLL_FUNC_ARRAY_ADDR (KILN_PE_DLL_EXPORT_DIR_ADDR + KILN_PE_DLL_EXPORT_DIR_SIZE)
24+#define KILN_PE_DLL_FUNC_ARRAY_SIZE (4u * (unsigned)RUNTIME_IMPORT_COUNT_)
25+#define KILN_PE_DLL_NAME_ARRAY_ADDR (KILN_PE_DLL_FUNC_ARRAY_ADDR + KILN_PE_DLL_FUNC_ARRAY_SIZE)
26+#define KILN_PE_DLL_NAME_ARRAY_SIZE (4u * (unsigned)RUNTIME_IMPORT_COUNT_)
27+#define KILN_PE_DLL_ORDINAL_ARRAY_ADDR (KILN_PE_DLL_NAME_ARRAY_ADDR + KILN_PE_DLL_NAME_ARRAY_SIZE)
28+#define KILN_PE_DLL_ORDINAL_ARRAY_SIZE (2u * (unsigned)RUNTIME_IMPORT_COUNT_)
29+
30+#define KILN_PE_DLL_CODE_START_OFFSET \
31+ (KILN_PE_DLL_SECTION_RVA + (KILN_PE_DLL_EXPORT_DIR_SIZE + KILN_PE_DLL_FUNC_ARRAY_SIZE + \
32+ KILN_PE_DLL_NAME_ARRAY_SIZE + KILN_PE_DLL_ORDINAL_ARRAY_SIZE))
33+
34+// Compiles the runtime (same 5 routines as elf_so_writer.h) into a fresh
35+// CodeBuf and writes it as a PE32+ DLL exporting them by name, loadable
36+// by the real Windows PE loader. Returns 0 on success.
37+int pe_write_shared_runtime(const char *path);
38+
39+#endif

kiln/src/pe/pe_dynamic.cadded

+168−0
@@ -0,0 +1,168 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "pe/pe_writer.h"
5+
6+// --link=shared PE executable: same "tiny PE" single-RWX-section
7+// technique as pe_writer.c's static-mode writer, but importing from TWO
8+// DLLs (kernel32.dll for OS calls, libkilnrt.dll for the runtime -- see
9+// pe/pe_dll_writer.h for the library side).
10+//
11+// Section layout:
12+// [globals][import dir: k32,rt,null][ILT k32][IAT k32][ILT rt][IAT rt]
13+// [code][hint/name: k32's 9, rt's 5][kernel32.dll][libkilnrt.dll]
14+
15+static const char *const pe_import_names[] = {
16+#define PE_IMPORT(name, str) str,
17+#include "pe/pe_imports.def"
18+#undef PE_IMPORT
19+};
20+
21+static const char *const runtime_import_names[] = {
22+#define RUNTIME_EXPORT(name, str) str,
23+#include "codegen/runtime_exports.def"
24+#undef RUNTIME_EXPORT
25+};
26+
27+static void put8(CodeBuf *f, uint8_t v) {
28+ if (f->count >= f->capacity) {
29+ f->capacity = f->capacity == 0 ? 256 : f->capacity * 2;
30+ f->code = realloc(f->code, (size_t)f->capacity);
31+ }
32+ f->code[f->count++] = v;
33+}
34+static void put16(CodeBuf *f, uint16_t v) { for (int i = 0; i < 2; i++) put8(f, (uint8_t)(v >> (8 * i))); }
35+static void put32(CodeBuf *f, uint32_t v) { for (int i = 0; i < 4; i++) put8(f, (uint8_t)(v >> (8 * i))); }
36+static void put64(CodeBuf *f, uint64_t v) { for (int i = 0; i < 8; i++) put8(f, (uint8_t)(v >> (8 * i))); }
37+static void put_zeros(CodeBuf *f, uint32_t n) { for (uint32_t i = 0; i < n; i++) put8(f, 0); }
38+static void put_str(CodeBuf *f, const char *s) { for (const char *p = s; *p; p++) put8(f, (uint8_t)*p); put8(f, 0); }
39+static uint32_t align_up(uint32_t v, uint32_t align) { return (v + align - 1) / align * align; }
40+static uint32_t hint_name_entry_size(const char *name) {
41+ uint32_t n = 2 + (uint32_t)strlen(name) + 1;
42+ return (n % 2 == 0) ? n : n + 1;
43+}
44+
45+int pe_write_dynamic_executable(const char *path, const CodeBuf *machine_code) {
46+ uint32_t code_rva = KILN_PE_SHARED_CODE_START_OFFSET;
47+ uint32_t tail_rva = code_rva + (uint32_t)machine_code->count;
48+
49+ uint32_t k32_hint_off[PE_IMPORT_COUNT_];
50+ uint32_t cursor = tail_rva;
51+ for (int i = 0; i < PE_IMPORT_COUNT_; i++) { k32_hint_off[i] = cursor; cursor += hint_name_entry_size(pe_import_names[i]); }
52+ uint32_t k32_name_rva = cursor; cursor += (uint32_t)strlen("kernel32.dll") + 1;
53+
54+ uint32_t rt_hint_off[RUNTIME_IMPORT_COUNT_];
55+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) { rt_hint_off[i] = cursor; cursor += hint_name_entry_size(runtime_import_names[i]); }
56+ uint32_t rt_name_rva = cursor; cursor += (uint32_t)strlen(KILN_LIBKILNRT_DLL_NAME) + 1;
57+
58+ uint32_t section_virtual_size = cursor - KILN_PE_SECTION_RVA;
59+ uint32_t section_raw_size = align_up(section_virtual_size, KILN_PE_FILE_ALIGN);
60+ uint32_t image_size = align_up(KILN_PE_SECTION_RVA + section_virtual_size, KILN_PE_SECTION_ALIGN);
61+
62+ CodeBuf f;
63+ code_init(&f);
64+
65+ put8(&f, 'M'); put8(&f, 'Z');
66+ put_zeros(&f, 58);
67+ put32(&f, 64);
68+
69+ put8(&f, 'P'); put8(&f, 'E'); put8(&f, 0); put8(&f, 0);
70+ put16(&f, 0x8664);
71+ put16(&f, 1);
72+ put32(&f, 0); put32(&f, 0); put32(&f, 0);
73+ put16(&f, 240);
74+ put16(&f, 0x0002 | 0x0020);
75+
76+ put16(&f, 0x20B);
77+ put8(&f, 14); put8(&f, 0);
78+ put32(&f, section_raw_size);
79+ put32(&f, 0); put32(&f, 0);
80+ put32(&f, code_rva);
81+ put32(&f, KILN_PE_SECTION_RVA);
82+ put64(&f, KILN_PE_IMAGE_BASE);
83+ put32(&f, KILN_PE_SECTION_ALIGN);
84+ put32(&f, KILN_PE_FILE_ALIGN);
85+ put16(&f, 6); put16(&f, 0);
86+ put16(&f, 0); put16(&f, 0);
87+ put16(&f, 6); put16(&f, 0);
88+ put32(&f, 0);
89+ put32(&f, image_size);
90+ put32(&f, KILN_PE_HEADERS_SIZE);
91+ put32(&f, 0);
92+ put16(&f, 3);
93+ put16(&f, 0);
94+ put64(&f, 0x100000); put64(&f, 0x1000);
95+ put64(&f, 0x100000); put64(&f, 0x1000);
96+ put32(&f, 0);
97+ put32(&f, 16);
98+ for (int i = 0; i < 16; i++) {
99+ if (i == 1) { put32(&f, KILN_PE_IMPORT_DIR_ADDR - KILN_PE_IMAGE_BASE); put32(&f, KILN_PE_SHARED_IMPORT_DIR_SIZE); }
100+ else if (i == 12) { put32(&f, KILN_PE_SHARED_IAT_K32_ADDR - KILN_PE_IMAGE_BASE); put32(&f, KILN_PE_SHARED_IAT_K32_SIZE + KILN_PE_SHARED_IAT_RT_SIZE); }
101+ else { put32(&f, 0); put32(&f, 0); }
102+ }
103+
104+ const char *sec_name = ".text";
105+ for (int i = 0; i < 8; i++) put8(&f, (uint8_t)(i < (int)strlen(sec_name) ? sec_name[i] : 0));
106+ put32(&f, section_virtual_size);
107+ put32(&f, KILN_PE_SECTION_RVA);
108+ put32(&f, section_raw_size);
109+ put32(&f, KILN_PE_HEADERS_SIZE);
110+ put32(&f, 0); put32(&f, 0);
111+ put16(&f, 0); put16(&f, 0);
112+ put32(&f, 0x20u | 0x20000000u | 0x40000000u | 0x80000000u);
113+
114+ while (f.count < (int)KILN_PE_HEADERS_SIZE) put8(&f, 0);
115+
116+ put_zeros(&f, KILN_PE_GLOBALS_SIZE);
117+
118+ // Import Directory Table: kernel32.dll, libkilnrt.dll, null terminator
119+ put32(&f, KILN_PE_SHARED_ILT_K32_ADDR - KILN_PE_IMAGE_BASE);
120+ put32(&f, 0); put32(&f, 0);
121+ put32(&f, k32_name_rva);
122+ put32(&f, KILN_PE_SHARED_IAT_K32_ADDR - KILN_PE_IMAGE_BASE);
123+
124+ put32(&f, KILN_PE_SHARED_ILT_RT_ADDR - KILN_PE_IMAGE_BASE);
125+ put32(&f, 0); put32(&f, 0);
126+ put32(&f, rt_name_rva);
127+ put32(&f, KILN_PE_SHARED_IAT_RT_ADDR - KILN_PE_IMAGE_BASE);
128+
129+ put_zeros(&f, 20); // terminator descriptor
130+
131+ // Both ILTs, then both IATs (see pe_writer.h: keeps the combined IAT
132+ // region contiguous for the optional header's single {RVA,Size} pair).
133+ for (int i = 0; i < PE_IMPORT_COUNT_; i++) put64(&f, (uint64_t)k32_hint_off[i]);
134+ put64(&f, 0);
135+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put64(&f, (uint64_t)rt_hint_off[i]);
136+ put64(&f, 0);
137+ for (int i = 0; i < PE_IMPORT_COUNT_; i++) put64(&f, (uint64_t)k32_hint_off[i]);
138+ put64(&f, 0);
139+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) put64(&f, (uint64_t)rt_hint_off[i]);
140+ put64(&f, 0);
141+
142+ for (int i = 0; i < machine_code->count; i++) put8(&f, machine_code->code[i]);
143+
144+ for (int i = 0; i < PE_IMPORT_COUNT_; i++) {
145+ uint32_t entry_size = hint_name_entry_size(pe_import_names[i]);
146+ put16(&f, 0);
147+ put_str(&f, pe_import_names[i]);
148+ if (entry_size > 2 + strlen(pe_import_names[i]) + 1) put8(&f, 0);
149+ }
150+ put_str(&f, "kernel32.dll");
151+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) {
152+ uint32_t entry_size = hint_name_entry_size(runtime_import_names[i]);
153+ put16(&f, 0);
154+ put_str(&f, runtime_import_names[i]);
155+ if (entry_size > 2 + strlen(runtime_import_names[i]) + 1) put8(&f, 0);
156+ }
157+ put_str(&f, KILN_LIBKILNRT_DLL_NAME);
158+
159+ while (f.count < (int)(KILN_PE_HEADERS_SIZE + section_raw_size)) put8(&f, 0);
160+
161+ FILE *out = fopen(path, "wb");
162+ if (!out) { fprintf(stderr, "kiln: could not open '%s' for writing\n", path); free(f.code); return 1; }
163+ size_t written = fwrite(f.code, 1, (size_t)f.count, out);
164+ fclose(out);
165+ free(f.code);
166+ if (written != (size_t)f.count) { fprintf(stderr, "kiln: short write to '%s'\n", path); return 1; }
167+ return 0;
168+}

kiln/src/pe/pe_dynamic.dadded

+10−0
@@ -0,0 +1,10 @@
1+src/pe/pe_dynamic.o: src/pe/pe_dynamic.c src/pe/pe_writer.h \
2+ src/codegen/emit.h src/codegen/runtime_layout.h \
3+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
4+ src/pe/pe_imports.def
5+src/pe/pe_writer.h:
6+src/codegen/emit.h:
7+src/codegen/runtime_layout.h:
8+src/codegen/runtime_import_enum.h:
9+src/codegen/runtime_exports.def:
10+src/pe/pe_imports.def:

kiln/src/pe/pe_imports.defadded

+16−0
@@ -0,0 +1,16 @@
1+// X-macro list of every kernel32.dll function kiln's Windows codegen
2+// calls. Single source of truth shared between pe_writer.c (which builds
3+// the actual import/IAT tables) and any codegen file that needs to call
4+// one of these (via PE_IMPORT_<NAME> from pe_writer.h) -- adding a new
5+// import means adding one line here, nowhere else.
6+//
7+// PE_IMPORT(enum_suffix, "DLL export name")
8+PE_IMPORT(EXIT_PROCESS, "ExitProcess")
9+PE_IMPORT(WRITE_FILE, "WriteFile")
10+PE_IMPORT(READ_FILE, "ReadFile")
11+PE_IMPORT(CREATE_FILE_A, "CreateFileA")
12+PE_IMPORT(CLOSE_HANDLE, "CloseHandle")
13+PE_IMPORT(GET_FILE_SIZE, "GetFileSize")
14+PE_IMPORT(VIRTUAL_ALLOC, "VirtualAlloc")
15+PE_IMPORT(GET_COMMAND_LINE_A, "GetCommandLineA")
16+PE_IMPORT(GET_STD_HANDLE, "GetStdHandle")

kiln/src/pe/pe_writer.cadded

+181−0
@@ -0,0 +1,181 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "pe/pe_writer.h"
5+#include "target.h"
6+
7+// Hand-written minimal PE32+ console executable -- the Windows-side twin
8+// of elf/elf_writer.c's "tiny ELF" technique: one RWX section holding
9+// everything (globals, import tables, code), a fixed non-relocated image
10+// base, no external linker involved.
11+//
12+// Section layout (see pe_writer.h for the address math):
13+// [globals][import directory][ILT][IAT][code][hint/name entries][dll name]
14+// Globals/import-dir/ILT/IAT all have sizes fixed by pe_imports.def alone
15+// (never by name string lengths), so codegen can address IAT slots and
16+// the code's own start address as compile-time constants -- the only
17+// things whose size depends on `machine_code->count` or import name
18+// lengths (the hint/name table, the dll name string) are placed AFTER
19+// code, computed right here at file-write time once that length is known.
20+
21+static const char *const pe_import_names[] = {
22+#define PE_IMPORT(name, str) str,
23+#include "pe/pe_imports.def"
24+#undef PE_IMPORT
25+};
26+
27+// --link=shared moves kernel32.dll's IAT to a different offset (there's
28+// a second DLL's import machinery ahead of it -- see pe_writer.h) --
29+// this has to pick the layout actually in use or it silently points at
30+// the wrong slot, which is exactly the kind of bug that surfaces as a
31+// jump to a garbage address rather than a clean error (caught by testing
32+// the Windows shared build under Wine, not assumed correct from the
33+// static-mode case alone).
34+uint64_t pe_import_addr(PeImport which) {
35+ if (kiln_get_link_mode() == KILN_LINK_SHARED) return KILN_PE_SHARED_IAT_K32_ADDR + 8ULL * (uint64_t)which;
36+ return KILN_PE_IAT_ADDR + 8ULL * (uint64_t)which;
37+}
38+
39+uint64_t pe_runtime_import_addr(RuntimeImport which) {
40+ return KILN_PE_SHARED_IAT_RT_ADDR + 8ULL * (uint64_t)which;
41+}
42+
43+static void put8(CodeBuf *f, uint8_t v) {
44+ if (f->count >= f->capacity) {
45+ f->capacity = f->capacity == 0 ? 256 : f->capacity * 2;
46+ f->code = realloc(f->code, (size_t)f->capacity);
47+ }
48+ f->code[f->count++] = v;
49+}
50+static void put16(CodeBuf *f, uint16_t v) { for (int i = 0; i < 2; i++) put8(f, (uint8_t)(v >> (8 * i))); }
51+static void put32(CodeBuf *f, uint32_t v) { for (int i = 0; i < 4; i++) put8(f, (uint8_t)(v >> (8 * i))); }
52+static void put64(CodeBuf *f, uint64_t v) { for (int i = 0; i < 8; i++) put8(f, (uint8_t)(v >> (8 * i))); }
53+static void put_zeros(CodeBuf *f, uint32_t n) { for (uint32_t i = 0; i < n; i++) put8(f, 0); }
54+static void put_str(CodeBuf *f, const char *s) { for (const char *p = s; *p; p++) put8(f, (uint8_t)*p); put8(f, 0); }
55+
56+static uint32_t align_up(uint32_t v, uint32_t align) { return (v + align - 1) / align * align; }
57+
58+// 2-byte hint + null-terminated name, padded to an even total length (PE
59+// spec requires each Hint/Name entry to start on a 2-byte boundary).
60+static uint32_t hint_name_entry_size(const char *name) {
61+ uint32_t n = 2 + (uint32_t)strlen(name) + 1;
62+ return (n % 2 == 0) ? n : n + 1;
63+}
64+
65+int pe_write_executable(const char *path, const CodeBuf *machine_code) {
66+ // ---- compute the variable-length tail (hint/name table + DLL name),
67+ // which only PE_writer itself needs to know the layout of ----
68+ uint32_t code_rva = KILN_PE_CODE_START_OFFSET;
69+ uint32_t hint_name_rva = code_rva + (uint32_t)machine_code->count;
70+ uint32_t hint_name_offsets[PE_IMPORT_COUNT_];
71+ uint32_t cursor = hint_name_rva;
72+ for (int i = 0; i < PE_IMPORT_COUNT_; i++) {
73+ hint_name_offsets[i] = cursor;
74+ cursor += hint_name_entry_size(pe_import_names[i]);
75+ }
76+ uint32_t dll_name_rva = cursor;
77+ uint32_t section_virtual_size = dll_name_rva + (uint32_t)strlen("kernel32.dll") + 1 - KILN_PE_SECTION_RVA;
78+ uint32_t section_raw_size = align_up(section_virtual_size, KILN_PE_FILE_ALIGN);
79+ uint32_t image_size = align_up(KILN_PE_SECTION_RVA + section_virtual_size, KILN_PE_SECTION_ALIGN);
80+
81+ CodeBuf f;
82+ code_init(&f);
83+
84+ // ---- IMAGE_DOS_HEADER (64 bytes, e_lfanew points right past it) ----
85+ put8(&f, 'M'); put8(&f, 'Z');
86+ put_zeros(&f, 58);
87+ put32(&f, 64); // e_lfanew
88+
89+ // ---- PE signature + IMAGE_FILE_HEADER (COFF) ----
90+ put8(&f, 'P'); put8(&f, 'E'); put8(&f, 0); put8(&f, 0);
91+ put16(&f, 0x8664); // Machine = AMD64
92+ put16(&f, 1); // NumberOfSections
93+ put32(&f, 0); // TimeDateStamp
94+ put32(&f, 0); // PointerToSymbolTable
95+ put32(&f, 0); // NumberOfSymbols
96+ put16(&f, 240); // SizeOfOptionalHeader
97+ put16(&f, 0x0002 | 0x0020); // EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE
98+
99+ // ---- IMAGE_OPTIONAL_HEADER64 ----
100+ put16(&f, 0x20B); // Magic = PE32+
101+ put8(&f, 14); put8(&f, 0); // Linker version
102+ put32(&f, section_raw_size); // SizeOfCode
103+ put32(&f, 0); // SizeOfInitializedData
104+ put32(&f, 0); // SizeOfUninitializedData
105+ put32(&f, code_rva); // AddressOfEntryPoint
106+ put32(&f, KILN_PE_SECTION_RVA); // BaseOfCode
107+ put64(&f, KILN_PE_IMAGE_BASE);
108+ put32(&f, KILN_PE_SECTION_ALIGN);
109+ put32(&f, KILN_PE_FILE_ALIGN);
110+ put16(&f, 6); put16(&f, 0); // OS version
111+ put16(&f, 0); put16(&f, 0); // Image version
112+ put16(&f, 6); put16(&f, 0); // Subsystem version
113+ put32(&f, 0); // Win32VersionValue
114+ put32(&f, image_size);
115+ put32(&f, KILN_PE_HEADERS_SIZE);
116+ put32(&f, 0); // CheckSum -- not validated by the loader for normal exe launch
117+ put16(&f, 3); // Subsystem = WINDOWS_CUI (console)
118+ put16(&f, 0); // DllCharacteristics -- deliberately no DYNAMIC_BASE: fixed-base, no relocation table
119+ put64(&f, 0x100000); put64(&f, 0x1000); // stack reserve/commit
120+ put64(&f, 0x100000); put64(&f, 0x1000); // heap reserve/commit
121+ put32(&f, 0); // LoaderFlags
122+ put32(&f, 16); // NumberOfRvaAndSizes
123+ for (int i = 0; i < 16; i++) {
124+ if (i == 1) { put32(&f, KILN_PE_IMPORT_DIR_ADDR - KILN_PE_IMAGE_BASE); put32(&f, KILN_PE_IMPORT_DIR_SIZE); }
125+ else if (i == 12) { put32(&f, KILN_PE_IAT_ADDR - KILN_PE_IMAGE_BASE); put32(&f, KILN_PE_IAT_SIZE); }
126+ else { put32(&f, 0); put32(&f, 0); }
127+ }
128+
129+ // ---- IMAGE_SECTION_HEADER (one RWX section covering everything) ----
130+ const char *sec_name = ".text";
131+ for (int i = 0; i < 8; i++) put8(&f, (uint8_t)(i < (int)strlen(sec_name) ? sec_name[i] : 0));
132+ put32(&f, section_virtual_size);
133+ put32(&f, KILN_PE_SECTION_RVA);
134+ put32(&f, section_raw_size);
135+ put32(&f, KILN_PE_HEADERS_SIZE); // PointerToRawData
136+ put32(&f, 0); put32(&f, 0); // relocations/linenumbers pointers
137+ put16(&f, 0); put16(&f, 0); // relocation/linenumber counts
138+ put32(&f, 0x20u | 0x20000000u | 0x40000000u | 0x80000000u); // CODE|EXECUTE|READ|WRITE
139+
140+ while (f.count < (int)KILN_PE_HEADERS_SIZE) put8(&f, 0);
141+
142+ // ---- section content ----
143+ put_zeros(&f, KILN_PE_GLOBALS_SIZE);
144+
145+ // Import Directory Table: one descriptor for kernel32.dll + terminator.
146+ // dll_name_rva was already computed above (the whole tail layout is
147+ // known before any bytes are emitted), so this is a direct write, no
148+ // patch-later step needed.
149+ put32(&f, KILN_PE_ILT_ADDR - KILN_PE_IMAGE_BASE); // OriginalFirstThunk -> ILT
150+ put32(&f, 0); put32(&f, 0); // TimeDateStamp, ForwarderChain
151+ put32(&f, dll_name_rva); // Name -> DLL name string
152+ put32(&f, KILN_PE_IAT_ADDR - KILN_PE_IMAGE_BASE); // FirstThunk -> IAT
153+ put_zeros(&f, 20); // terminator descriptor
154+
155+ // ILT and IAT: identical initial content, each entry an RVA to this
156+ // import's Hint/Name entry (bit63=0 selects import-by-name).
157+ for (int pass = 0; pass < 2; pass++) {
158+ for (int i = 0; i < PE_IMPORT_COUNT_; i++) put64(&f, (uint64_t)hint_name_offsets[i]);
159+ put64(&f, 0); // terminator
160+ }
161+
162+ for (int i = 0; i < machine_code->count; i++) put8(&f, machine_code->code[i]);
163+
164+ for (int i = 0; i < PE_IMPORT_COUNT_; i++) {
165+ uint32_t entry_size = hint_name_entry_size(pe_import_names[i]);
166+ put16(&f, 0); // hint (0 = "don't trust it, look up by name")
167+ put_str(&f, pe_import_names[i]);
168+ if (entry_size > 2 + strlen(pe_import_names[i]) + 1) put8(&f, 0); // even-length pad
169+ }
170+ put_str(&f, "kernel32.dll");
171+
172+ while (f.count < (int)(KILN_PE_HEADERS_SIZE + section_raw_size)) put8(&f, 0);
173+
174+ FILE *out = fopen(path, "wb");
175+ if (!out) { fprintf(stderr, "kiln: could not open '%s' for writing\n", path); free(f.code); return 1; }
176+ size_t written = fwrite(f.code, 1, (size_t)f.count, out);
177+ fclose(out);
178+ free(f.code);
179+ if (written != (size_t)f.count) { fprintf(stderr, "kiln: short write to '%s'\n", path); return 1; }
180+ return 0;
181+}

kiln/src/pe/pe_writer.dadded

+11−0
@@ -0,0 +1,11 @@
1+src/pe/pe_writer.o: src/pe/pe_writer.c src/pe/pe_writer.h \
2+ src/codegen/emit.h src/codegen/runtime_layout.h \
3+ src/codegen/runtime_import_enum.h src/codegen/runtime_exports.def \
4+ src/pe/pe_imports.def src/target.h
5+src/pe/pe_writer.h:
6+src/codegen/emit.h:
7+src/codegen/runtime_layout.h:
8+src/codegen/runtime_import_enum.h:
9+src/codegen/runtime_exports.def:
10+src/pe/pe_imports.def:
11+src/target.h:

kiln/src/pe/pe_writer.hadded

+114−0
@@ -0,0 +1,114 @@
1+#ifndef KILN_PE_WRITER_H
2+#define KILN_PE_WRITER_H
3+#include "codegen/emit.h"
4+#include "codegen/runtime_layout.h"
5+#include "codegen/runtime_import_enum.h"
6+
7+// Kiln generates a fixed, non-relocated PE32+ executable, same "always
8+// loaded at one known address" philosophy as elf/elf_writer.h's
9+// KILN_LOAD_BASE -- absolute addresses for string literals, closure code
10+// addresses, and (this file's addition) imported-function IAT slots are
11+// all known at compile time, no relocation table needed. 0x140000000 is
12+// the address range modern PE tooling (MSVC/link.exe) itself defaults 64
13+// bit executables to, so it's a safe, unsurprising choice for loaders.
14+#define KILN_PE_IMAGE_BASE 0x140000000ULL
15+#define KILN_PE_SECTION_ALIGN 0x1000u
16+#define KILN_PE_FILE_ALIGN 0x200u
17+
18+// DOS header (64) + "PE\0\0" (4) + COFF header (20) + PE32+ optional
19+// header (112 fixed fields + 16 data directories * 8 bytes = 240) +
20+// one section header (40) = 368, rounded up to KILN_PE_FILE_ALIGN.
21+#define KILN_PE_HEADERS_RAW_SIZE 368u
22+#define KILN_PE_HEADERS_SIZE 0x200u
23+
24+// The one-and-only section: globals region, then import machinery, then
25+// code, then (past the end of `code`, so its size doesn't affect any of
26+// the fixed offsets below) the variable-length hint/name + DLL-name
27+// tail -- see pe_writer.c's header comment for why that ordering matters.
28+#define KILN_PE_SECTION_RVA KILN_PE_SECTION_ALIGN
29+
30+#define KILN_PE_GLOBALS_ADDR (KILN_PE_IMAGE_BASE + KILN_PE_SECTION_RVA)
31+#define KILN_PE_HEAP_PTR_ADDR (KILN_PE_GLOBALS_ADDR + 0)
32+#define KILN_PE_TRY_DEPTH_ADDR (KILN_PE_GLOBALS_ADDR + 8)
33+#define KILN_PE_TRY_HANDLERS_ADDR (KILN_PE_TRY_DEPTH_ADDR + KILN_TRY_DEPTH_SIZE)
34+// No argv slot: GetCommandLineA() can be called at any point, unlike
35+// Linux's argv() which has to snapshot the kernel's original RSP before
36+// anything else touches it (see codegen/argv_builtin.c).
37+#define KILN_PE_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE)
38+
39+// Every kernel32.dll function kiln's Windows codegen calls -- single list
40+// in pe_imports.def, shared by this enum and pe_writer.c's actual table
41+// bytes, so the two can never silently drift out of sync.
42+typedef enum {
43+#define PE_IMPORT(name, str) PE_IMPORT_##name,
44+#include "pe/pe_imports.def"
45+#undef PE_IMPORT
46+ PE_IMPORT_COUNT_
47+} PeImport;
48+
49+// Import Directory Table: one IMAGE_IMPORT_DESCRIPTOR (20 bytes) for
50+// kernel32.dll, plus one zeroed terminator entry.
51+#define KILN_PE_IMPORT_DIR_ADDR (KILN_PE_GLOBALS_ADDR + KILN_PE_GLOBALS_SIZE)
52+#define KILN_PE_IMPORT_DIR_SIZE 40u
53+
54+// Import Lookup Table and Import Address Table: one 8-byte entry per
55+// import plus a zero terminator each. Fixed size regardless of function
56+// NAME lengths (those live in the variable-length tail written after
57+// `code`), which is exactly what lets codegen address IAT slots as
58+// compile-time constants without knowing the final code length first.
59+#define KILN_PE_ILT_ADDR (KILN_PE_IMPORT_DIR_ADDR + KILN_PE_IMPORT_DIR_SIZE)
60+#define KILN_PE_ILT_SIZE (8u * ((unsigned)PE_IMPORT_COUNT_ + 1u))
61+#define KILN_PE_IAT_ADDR (KILN_PE_ILT_ADDR + KILN_PE_ILT_SIZE)
62+#define KILN_PE_IAT_SIZE (8u * ((unsigned)PE_IMPORT_COUNT_ + 1u))
63+
64+#define KILN_PE_CODE_START_OFFSET \
65+ (KILN_PE_SECTION_RVA + (KILN_PE_GLOBALS_SIZE + KILN_PE_IMPORT_DIR_SIZE + KILN_PE_ILT_SIZE + KILN_PE_IAT_SIZE))
66+
67+// Absolute VA of `which`'s IAT slot -- what the loader overwrites with
68+// the resolved function pointer at load time. codegen/win_call.c loads
69+// this address, dereferences it, and calls through the result (see that
70+// file for why no RIP-relative addressing is needed here).
71+uint64_t pe_import_addr(PeImport which);
72+
73+// ---- --link=shared layout: a SECOND imported DLL (libkilnrt.dll,
74+// see pe/pe_dll_writer.h) alongside kernel32.dll. Kept as a fully
75+// parallel set of constants, never touched by the static-mode ones
76+// above, so --link=static output stays byte-for-byte unchanged --
77+// pe_writer.c picks one whole layout or the other based on link mode,
78+// never mixes them. ----
79+#define KILN_LIBKILNRT_DLL_NAME "libkilnrt.dll"
80+
81+// 3 descriptors (kernel32.dll, libkilnrt.dll, null terminator) instead
82+// of 2 (kernel32.dll, null). Both ILTs are grouped before both IATs
83+// (rather than interleaved per-DLL) so the combined IAT region is
84+// CONTIGUOUS -- the optional header's IAT data directory entry is a
85+// single {RVA,Size} pair, which can only correctly describe one
86+// contiguous range.
87+#define KILN_PE_SHARED_IMPORT_DIR_SIZE 60u
88+#define KILN_PE_SHARED_ILT_K32_ADDR (KILN_PE_IMPORT_DIR_ADDR + KILN_PE_SHARED_IMPORT_DIR_SIZE)
89+#define KILN_PE_SHARED_ILT_K32_SIZE (8u * ((unsigned)PE_IMPORT_COUNT_ + 1u))
90+#define KILN_PE_SHARED_ILT_RT_ADDR (KILN_PE_SHARED_ILT_K32_ADDR + KILN_PE_SHARED_ILT_K32_SIZE)
91+#define KILN_PE_SHARED_ILT_RT_SIZE (8u * ((unsigned)RUNTIME_IMPORT_COUNT_ + 1u))
92+#define KILN_PE_SHARED_IAT_K32_ADDR (KILN_PE_SHARED_ILT_RT_ADDR + KILN_PE_SHARED_ILT_RT_SIZE)
93+#define KILN_PE_SHARED_IAT_K32_SIZE (8u * ((unsigned)PE_IMPORT_COUNT_ + 1u))
94+#define KILN_PE_SHARED_IAT_RT_ADDR (KILN_PE_SHARED_IAT_K32_ADDR + KILN_PE_SHARED_IAT_K32_SIZE)
95+#define KILN_PE_SHARED_IAT_RT_SIZE (8u * ((unsigned)RUNTIME_IMPORT_COUNT_ + 1u))
96+
97+#define KILN_PE_SHARED_CODE_START_OFFSET \
98+ (KILN_PE_SECTION_RVA + (KILN_PE_GLOBALS_SIZE + KILN_PE_SHARED_IMPORT_DIR_SIZE + \
99+ KILN_PE_SHARED_ILT_K32_SIZE + KILN_PE_SHARED_IAT_K32_SIZE + \
100+ KILN_PE_SHARED_ILT_RT_SIZE + KILN_PE_SHARED_IAT_RT_SIZE))
101+
102+// Absolute VA of `which`'s libkilnrt.dll IAT slot (shared-mode layout).
103+uint64_t pe_runtime_import_addr(RuntimeImport which);
104+
105+// Writes `machine_code` as a PE32+ console executable that imports from
106+// BOTH kernel32.dll and libkilnrt.dll (the shared-mode layout above).
107+int pe_write_dynamic_executable(const char *path, const CodeBuf *machine_code);
108+
109+// Writes `machine_code` as a minimal, standalone PE32+ console executable
110+// at `path` (hand-written headers/import tables, same "no external
111+// linker" spirit as elf_write_executable). Returns 0 on success.
112+int pe_write_executable(const char *path, const CodeBuf *machine_code);
113+
114+#endif

kiln/src/target.cadded

+31−0
@@ -0,0 +1,31 @@
1+#include "target.h"
2+
3+static KilnTarget current_target = KILN_TARGET_LINUX;
4+
5+void kiln_set_target(KilnTarget target) {
6+ current_target = target;
7+}
8+
9+KilnTarget kiln_get_target(void) {
10+ return current_target;
11+}
12+
13+static KilnLinkMode current_link_mode = KILN_LINK_STATIC;
14+
15+void kiln_set_link_mode(KilnLinkMode mode) {
16+ current_link_mode = mode;
17+}
18+
19+KilnLinkMode kiln_get_link_mode(void) {
20+ return current_link_mode;
21+}
22+
23+static int compiling_so = 0;
24+
25+void kiln_set_compiling_so(int flag) {
26+ compiling_so = flag;
27+}
28+
29+int kiln_is_compiling_so(void) {
30+ return compiling_so;
31+}

kiln/src/target.dadded

+2−0
@@ -0,0 +1,2 @@
1+src/target.o: src/target.c src/target.h
2+src/target.h:

kiln/src/target.hadded

+47−0
@@ -0,0 +1,47 @@
1+#ifndef KILN_TARGET_H
2+#define KILN_TARGET_H
3+
4+// Compile-time target selector (Plan A, phase A1). Nothing reads this yet
5+// past main.c storing it -- codegen/writer forks land in later phases.
6+// Global, not threaded as a parameter: every codegen/writer call site would
7+// otherwise need a target argument added in this phase just to ignore it,
8+// which is exactly the kind of premature plumbing the phased plan is
9+// avoiding until a phase actually needs to branch on it.
10+typedef enum {
11+ KILN_TARGET_LINUX,
12+ KILN_TARGET_WINDOWS
13+} KilnTarget;
14+
15+void kiln_set_target(KilnTarget target);
16+KilnTarget kiln_get_target(void);
17+
18+// Plan B: whether the runtime (heap/bytes/string_alloc/print/errors-raise)
19+// is emitted inline into this binary (KILN_LINK_STATIC, the default and
20+// the only mode before Plan B) or left external, calling into
21+// libkilnrt.so via real dynamic linking (KILN_LINK_SHARED, Linux only --
22+// Windows DLL equivalent is a later phase). Same "global, not threaded as
23+// a parameter" reasoning as KilnTarget above.
24+typedef enum {
25+ KILN_LINK_STATIC,
26+ KILN_LINK_SHARED
27+} KilnLinkMode;
28+
29+void kiln_set_link_mode(KilnLinkMode mode);
30+KilnLinkMode kiln_get_link_mode(void);
31+
32+// Separate from KilnLinkMode on purpose: link mode answers "should THIS
33+// routine's own call sites (heap_emit_alloc etc.) reach their callee via
34+// a local call or a GOT call" -- but codegen/layout.c's globals-address
35+// selection needs to answer a DIFFERENT question, "which executable
36+// layout owns the globals this code will read at runtime", and that
37+// answer is the shared-mode layout in BOTH cases: when compiling a
38+// normal --link=shared executable AND when compiling the runtime that
39+// goes into libkilnrt.so (its heap_alloc etc. always run on behalf of
40+// some shared-mode executable's globals, never its own). Conflating
41+// these into one flag was a real bug caught by testing (see
42+// elf/elf_so_writer.c's comment) -- kiln_set_link_mode(STATIC) to get
43+// local intra-.so calls also silently broke the globals address.
44+void kiln_set_compiling_so(int flag);
45+int kiln_is_compiling_so(void);
46+
47+#endif

kiln/tests/argv.ashadded

+6−0
@@ -0,0 +1,6 @@
1+let args = argv();
2+print len(args);
3+print args[0];
4+for (a in args) {
5+ print a;
6+}

kiln/tests/arrays.ashadded

+28−0
@@ -0,0 +1,28 @@
1+let a = [1, 2, 3];
2+print a[0];
3+print a[1];
4+print a[2];
5+print len(a);
6+
7+a[0] = 99;
8+print a[0];
9+
10+let b = ["x", "y", "z"];
11+print b[1];
12+
13+let c = [1];
14+push(c, 2);
15+push(c, 3);
16+print len(c);
17+print c[0];
18+print c[1];
19+print c[2];
20+
21+let d = [];
22+let i = 0;
23+while (i < 10) {
24+ push(d, i);
25+ i = i + 1;
26+}
27+print len(d);
28+print d[9];

kiln/tests/ash.libs/testlib/testlib.ashadded

+5−0
@@ -0,0 +1,5 @@
1+let VERSION = "1.0";
2+
3+fn square(x) {
4+ return x * x;
5+}

kiln/tests/bitwise.ashadded

+9−0
@@ -0,0 +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;

kiln/tests/block_comments.ashadded

+10−0
@@ -0,0 +1,10 @@
1+/* this is a block comment */
2+print 1;
3+/* multi
4+ line
5+ comment */
6+print 2;
7+print /* inline */ 3;
8+let x = 5; /* trailing */ print x;
9+// still works
10+print 4;

kiln/tests/block_scoping.ashadded

+41−0
@@ -0,0 +1,41 @@
1+let x = 1;
2+if (true) {
3+ let x = 2;
4+ print x;
5+}
6+print x;
7+
8+let y = 10;
9+while (y < 13) {
10+ let z = y * 2;
11+ print z;
12+ y = y + 1;
13+}
14+
15+// 20 if-blocks, each declaring 4 UNIQUE-named variables (80 total) --
16+// leaking would exceed MAX_KILN_VARS (64) and fail to compile; scoped
17+// slot reuse keeps this well under the cap. Verified during planning:
18+// this exact file (with the shadowing let above already showing the
19+// 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;

kiln/tests/block_scoping_closures.ashadded

+50−0
@@ -0,0 +1,50 @@
1+// closure created and returned from inside an if-block, capturing a
2+// block-scoped variable; call it after the block, once its slot has
3+// been reused by a later declaration
4+fn make_from_if() {
5+ let f = 0;
6+ if (true) {
7+ let x = 42;
8+ f = fn() { return x; };
9+ }
10+ let after = 999; // reuses x's old slot
11+ return f;
12+}
13+let cf = make_from_if();
14+print cf();
15+
16+// closure capturing a variable that shadows an outer same-named one --
17+// the closure must see the INNER value; the outer variable must be
18+// untouched afterward
19+fn make_shadow() {
20+ let x = 1;
21+ let f = 0;
22+ if (true) {
23+ let x = 2;
24+ f = fn() { return x; };
25+ }
26+ return [x, f()];
27+}
28+let r = make_shadow();
29+print r[0];
30+print r[1];
31+
32+// closures created inside a while body (compiled once, run 3 times),
33+// each capturing that iteration's own block-scoped value
34+fn make_list() {
35+ let fns = [];
36+ let i = 0;
37+ while (i < 3) {
38+ let captured = i;
39+ push(fns, fn() { return captured; });
40+ i = i + 1;
41+ }
42+ return fns;
43+}
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();

kiln/tests/break_continue.ashadded

+47−0
@@ -0,0 +1,47 @@
1+let i = 0;
2+while (i < 10) {
3+ i = i + 1;
4+ if (i == 5) { break; }
5+ print i;
6+}
7+print "---";
8+
9+let j = 0;
10+while (j < 5) {
11+ j = j + 1;
12+ if (j == 3) { continue; }
13+ print j;
14+}
15+print "---";
16+
17+for (x in [1,2,3,4,5]) {
18+ if (x == 3) { break; }
19+ print x;
20+}
21+print "---";
22+
23+for (x in [1,2,3,4,5]) {
24+ if (x == 3) { continue; }
25+ print x;
26+}
27+print "---";
28+
29+// nested loops: break only exits innermost
30+for (i in [1,2]) {
31+ for (j in [1,2,3]) {
32+ if (j == 2) { break; }
33+ print i * 10 + j;
34+ }
35+}
36+print "---";
37+
38+// break/continue inside function's own loop, not affecting caller
39+fn find_first_even(arr) {
40+ for (x in arr) {
41+ if (x - floor(x/2)*2 == 0) {
42+ return x;
43+ }
44+ }
45+ return -1;
46+}
47+print find_first_even([1, 3, 5, 4, 7]);

kiln/tests/closures.ashadded

+31−0
@@ -0,0 +1,31 @@
1+// M8: lambda assigned to a variable and called
2+let square = fn(x) { return x * x; };
3+print square(5);
4+
5+// named function referenced as a value and called indirectly
6+fn add(a, b) { return a + b; }
7+let op = add;
8+print op(3, 4);
9+
10+// genuine capture: adder-generator (partial application / currying)
11+fn make_adder(n) {
12+ return fn(x) { return x + n; };
13+}
14+let add5 = make_adder(5);
15+let add10 = make_adder(10);
16+print add5(1);
17+print add10(1);
18+print add5(2);
19+
20+// higher-order function: pass a closure into a plain function
21+fn apply_twice(f, x) {
22+ return f(f(x));
23+}
24+print apply_twice(square, 3);
25+
26+// closure capturing multiple outer variables
27+fn make_scaler(mul, offset) {
28+ return fn(x) { return x * mul + offset; };
29+}
30+let scale = make_scaler(2, 1);
31+print scale(10);

kiln/tests/compound_assignment.ashadded

+32−0
@@ -0,0 +1,32 @@
1+let x = 5;
2+x += 3;
3+print x;
4+x -= 2;
5+print x;
6+x *= 4;
7+print x;
8+x /= 3;
9+print x;
10+
11+let s = "hello";
12+s += " world";
13+print s;
14+
15+let i = 0;
16+i++;
17+i++;
18+print i;
19+i--;
20+print i;
21+
22+let total = 0;
23+for (n in [1,2,3,4,5]) {
24+ total += n;
25+}
26+print total;
27+
28+let j = 0;
29+while (j < 5) {
30+ j++;
31+}
32+print j;

kiln/tests/control_flow.ashadded

+20−0
@@ -0,0 +1,20 @@
1+let x = 5;
2+x = x + 1;
3+print x;
4+
5+if (x > 5) {
6+ print 1;
7+} else {
8+ print 0;
9+}
10+
11+let i = 0;
12+while (i < 3) {
13+ print i;
14+ i = i + 1;
15+}
16+
17+print 1 == 1;
18+print 1 != 1;
19+print (1 < 2) && (3 > 2);
20+print (1 > 2) || (3 < 2);

kiln/tests/convert_builtins.ashadded

+10−0
@@ -0,0 +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));

kiln/tests/exponent.ashadded

+9−0
@@ -0,0 +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;

kiln/tests/file_io.ashadded

+7−0
@@ -0,0 +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);

kiln/tests/fixtures/collision_dir_one/shared.ashadded

+3−0
@@ -0,0 +1,3 @@
1+fn fromOne() {
2+ return 1;
3+}

kiln/tests/fixtures/collision_dir_two/shared.ashadded

+3−0
@@ -0,0 +1,3 @@
1+fn fromTwo() {
2+ return 2;
3+}

kiln/tests/fixtures/imports_basic_lib.ashadded

+7−0
@@ -0,0 +1,7 @@
1+fn helper(x) {
2+ return x * 2;
3+}
4+
5+fn add(a, b) {
6+ return a + b + helper(1);
7+}

kiln/tests/fixtures/imports_circular_a.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_circular_b.ash>;
2+
3+fn fromA() {
4+ return 1;
5+}

kiln/tests/fixtures/imports_circular_b.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_circular_a.ash>;
2+
3+fn fromB() {
4+ return 2;
5+}

kiln/tests/fixtures/imports_collision_main.ashadded

+4−0
@@ -0,0 +1,4 @@
1+@import <./collision_dir_one/shared.ash>;
2+@import <./collision_dir_two/shared.ash>;
3+
4+print shared.fromOne();

kiln/tests/fixtures/imports_const_lib.ashadded

+6−0
@@ -0,0 +1,6 @@
1+let PI = 3.14159;
2+let GREETING = "hello from lib";
3+
4+fn circleArea(r) {
5+ return PI * r * r;
6+}

kiln/tests/fixtures/imports_diamond_b.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_diamond_d.ash>;
2+
3+fn fromB() {
4+ return imports_diamond_d.base() + 1;
5+}

kiln/tests/fixtures/imports_diamond_c.ashadded

+5−0
@@ -0,0 +1,5 @@
1+@import <./imports_diamond_d.ash>;
2+
3+fn fromC() {
4+ return imports_diamond_d.base() + 2;
5+}

kiln/tests/fixtures/imports_diamond_d.ashadded

+3−0
@@ -0,0 +1,3 @@
1+fn base() {
2+ return 100;
3+}

kiln/tests/floats.ashadded

+22−0
@@ -0,0 +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;
6+
7+let x = 2.5;
8+print x < 3;
9+print x > 3;
10+
11+fn avg(a, b) {
12+ return (a + b) / 2;
13+}
14+print avg(1, 2);
15+
16+fn half(n) {
17+ if (n < 1) {
18+ return n;
19+ }
20+ return half(n / 2) + 0.1;
21+}
22+print half(4);

kiln/tests/for_loop_scoping.ashadded

+15−0
@@ -0,0 +1,15 @@
1+// loop variable no longer leaks: outer x, same name, must be untouched
2+let x = 9;
3+for (x in [1, 2, 3]) {
4+ print x;
5+}
6+print x;
7+
8+// nested for loops with the same variable name at each level -- each
9+// level's binding is independent
10+for (n in [1, 2]) {
11+ for (n in [10, 20]) {
12+ print n;
13+ }
14+ print n;
15+}

kiln/tests/for_loops.ashadded

+44−0
@@ -0,0 +1,44 @@
1+let nums = [10, 20, 30];
2+for (x in nums) {
3+ print x;
4+}
5+
6+// nested for loops
7+let a = [1, 2];
8+let b = [10, 20];
9+for (i in a) {
10+ for (j in b) {
11+ print i * j;
12+ }
13+}
14+
15+// for loop calling a higher-order builtin in its body
16+fn double(x) { return x * 2; }
17+for (x in nums) {
18+ let d = map([x], double);
19+ print d[0];
20+}
21+
22+// empty array
23+let empty = [];
24+for (x in empty) {
25+ print "should not print";
26+}
27+print "after empty loop";
28+
29+// for loop inside a function, recursion-safe check
30+fn sum_array(arr) {
31+ let total = 0;
32+ for (v in arr) {
33+ total = total + v;
34+ }
35+ return total;
36+}
37+print sum_array(nums);
38+print sum_array([1, 2, 3, 4, 5]);
39+
40+// loop variable reused/reassignable, doesn't affect iteration
41+for (x in nums) {
42+ x = x + 1000;
43+ print x;
44+}

kiln/tests/functions.ashadded

+30−0
@@ -0,0 +1,30 @@
1+fn add(a, b) {
2+ return a + b;
3+}
4+print add(2, 3);
5+
6+fn fact(n) {
7+ if (n <= 1) {
8+ return 1;
9+ }
10+ return n * fact(n - 1);
11+}
12+print fact(5);
13+
14+fn fib(n) {
15+ if (n < 2) {
16+ return n;
17+ }
18+ return fib(n - 1) + fib(n - 2);
19+}
20+print fib(10);
21+
22+fn noop() {
23+}
24+print noop();
25+
26+fn square(x) {
27+ return x * x;
28+}
29+let y = 5;
30+print square(y) + square(y + 1);

kiln/tests/hello.ashadded

+1−0
@@ -0,0 +1 @@
1+print 1 + 2 * 3;

kiln/tests/higher_order.ashadded

+26−0
@@ -0,0 +1,26 @@
1+let nums = [1, 2, 3, 4, 5];
2+
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);
9+
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];
15+
16+fn add(a, b) { return a + b; }
17+print reduce(nums, add, 0);
18+print reduce(nums, add, 100);
19+
20+let squares = map(nums, fn(x) { return x * x; });
21+print squares[2];
22+print squares[4];
23+
24+let empty = [];
25+let mapped_empty = map(empty, double);
26+print len(mapped_empty);

GitHub only returns the first 300 files of a commit. View the full commit on GitHub.