a26d22b
300 files changed+16148−0
.gitignoreadded
+26−0@@ -0,0 +1,26 @@1
+# Build artifacts2
+*.o3
+/bin/4
+5
+# Compiled benchmark binaries (keep the .cpp sources, ignore the built outputs)6
+ashvm/benchmarks/fib_cpp7
+ashvm/benchmarks/loop_cpp8
+ashvm/benchmarks/countdown_cpp9
+ashvm/benchmarks/hashmap_cpp10
+ashvm/benchmarks/native_jit11
+12
+# OS / editor cruft13
+.DS_Store14
+*.swp15
+*~16
+17
+# Dont put ai Slop in my Code18
+.claude19
+CLAUDE.md20
+.agents21
+skills-lock.json22
+23
+# Private Notes24
+goals.md25
+rundown.md26
+missing.md\ No newline at end of fileCHANGELOG.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:5745
+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:0548
+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:1051
+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 yet52
+53
+## 2026-07-16 05:1454
+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 benchmarks55
+56
+## 2026-07-16 05:1957
+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 specifically58
+59
+## 2026-07-16 05:2460
+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 loop61
+62
+## 2026-07-16 05:2963
+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:3366
+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:3969
+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 closures70
+71
+## 2026-07-16 05:4272
+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 component73
+74
+## 2026-07-16 05:4575
+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:4778
+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:4981
+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:5284
+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 ashvm85
+86
+## 2026-07-16 07:4287
+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 crashes88
+89
+## 2026-07-16 07:5290
+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 printed91
+92
+## 2026-07-16 07:5893
+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 counter94
+95
+## 2026-07-16 08:0296
+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 counting97
+98
+## 2026-07-16 08:0599
+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-aware100
+101
+## 2026-07-16 08:10102
+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 earlier103
+104
+## 2026-07-16 08:18105
+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 round106
+107
+## 2026-07-16 08:27108
+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 functions109
+110
+## 2026-07-16 08:28111
+fixed cosmetic strncpy-truncation warnings in the lambda-naming code by clamping namelen to the buffer size112
+113
+## 2026-07-16 08:29114
+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:34117
+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 ashc118
+119
+## 2026-07-16 08:35120
+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 error121
+122
+## 2026-07-16 08:50123
+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:52126
+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.md127
+128
+## 2026-07-18 11:55129
+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:58132
+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:00135
+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:04138
+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:09141
+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:11144
+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:18147
+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:21150
+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:37153
+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:47156
+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:46159
+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:57162
+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:09165
+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:29168
+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:47171
+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:59180
+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
+# Ash4
+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-point14
+numbers, `try`/`catch`, and a colorized compiler that points straight at15
+your mistakes.16
+17
+```ash18
+fn make_adder(x) {19
+ return fn(y) { return x + y; };20
+}21
+22
+let add5 = make_adder(5);23
+print add5(3); // 824
+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
+## Engine38
+39
+Ash compiles to real bytecode and runs it on a stack-based VM (`ashvm`) with40
+computed-goto instruction dispatch and a handful of hand-fused41
+"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 were44
+prototyped first - it has since been retired now that the VM has full45
+feature parity, including real `double` number semantics and Rust-style46
+runtime diagnostics.47
+48
+## Performance49
+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: compiling58
+to real bytecode instead of re-parsing source on every loop iteration,59
+computed-goto instruction dispatch instead of a `switch` statement, and60
+those hand-fused superinstructions. A hand-written x86-64 JIT experiment61
+(`benchmarks/native_jit.c`) that skips bytecode entirely gets within ~2x of62
+raw C++, which is roughly the ceiling for anything short of a full63
+optimizing native compiler.64
+65
+## Language features66
+67
+- Numbers (real doubles), strings (with escape sequences), arrays, hash maps68
+- Functions, recursion, closures with by-value capture69
+- First-class functions - pass them around, store them in variables, call them indirectly70
+- `map`, `filter`, `reduce`, and 20+ other built-ins (string ops, file I/O, math)71
+- `try` / `catch` error handling72
+- `for (x in array)` and `while` loops73
+- Colorized, Rust-style diagnostics for both compile-time and runtime74
+ errors: file:line:column, a source snippet, a caret, and a contextual hint75
+- An interactive REPL with persistent variables across lines76
+77
+## Getting started78
+79
+```bash80
+cd ashvm && make # builds ../bin/ashvm81
+82
+../bin/ashvm script.ash # run a script83
+../bin/ashvm # start the REPL84
+```85
+86
+## Project structure87
+ashvm/ the engine - everything below is self-contained inside here88
+src/89
+lexer/ tokenizer90
+value/ VMValue tagged type, hash map91
+vm/ bytecode Chunk format, dispatch loop, error handling, public API92
+compiler/ single-pass compiler: parser/emitter state, expression chain,93
+call/lambda emission, statement compilation94
+builtins/ len, map, filter, reduce, string/file ops95
+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 experiment98
+tests/ example programs covering each language feature99
+kiln/ a standalone .ash compiler (Windows + Linux). Not started yet.100
+101
+## Editor support102
+103
+A VS Code extension with syntax highlighting will come soon. 104
+We could build an icon theme too, but we'd rather not105
+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 = gcc2
+CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc3
+LDFLAGS = -lm4
+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.c11
+ASHVM_OBJ = $(ASHVM_SRC:.c=.o)12
+13
+BIN = ../bin/ashvm14
+15
+all: $(BIN)16
+17
+$(BIN): $(ASHVM_OBJ)18
+ mkdir -p ../bin19
+ $(CC) $(CFLAGS) -o $(BIN) $(ASHVM_OBJ) $(LDFLAGS)20
+21
+clean:22
+ find src -name '*.o' -delete23
+ rm -f $(BIN)24
+25
+.PHONY: all cleanashvm/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 benchmark24
+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 = 032
+ emit8(code, &pos, 0x31); emit8(code, &pos, 0xC0);33
+ // xor ecx, ecx ; i = 034
+ 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); // placeholder45
+46
+ // add rax, rcx ; sum += i47
+ 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_start52
+ 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 is62
+ 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_H2
+#define ASH_VM_BUILTINS_H3
+#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
+#endifashvm/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: { // map10
+ 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: { // filter19
+ 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: { // reduce29
+ 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: { // keys38
+ 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: { // values50
+ 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: { // has58
+ 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: { // delete62
+ 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: { // len12
+ 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: { // push19
+ 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: { // input24
+ 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: { // str32
+ 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: { // num40
+ 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: { // sqrt44
+ 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: { // abs48
+ 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: { // floor52
+ 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_file10
+ 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_file18
+ 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_file25
+ 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_exists32
+ 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_H2
+#define ASH_VM_BUILTINS_INTERNAL_H3
+#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
+#endifashvm/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: { // split10
+ 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: { // join31
+ 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: { // substring46
+ 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: { // indexOf56
+ 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: { // replace62
+ 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: { // upper80
+ 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: { // lower86
+ 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: { // trim92
+ 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_H2
+#define ASH_COMMON_H3
+4
+#define MAX_VARS 645
+#define MAX_SCOPES 656
+#define MAX_FUNCTIONS 647
+#define MAX_PARAMS 88
+#define MAX_CALL_ARGS 89
+10
+#endifashvm/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 -- unlike48
+// emit_call(), this never falls back to builtins/locals/context-namespace49
+// lookup, since a namespaced call can only ever mean an imported file's50
+// function.51
+void emit_call_named(const char *name, int len) {52
+ int fn_idx = find_function(name, len); // already fully-qualified53
+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's79
+// locals by value at creation time (same rule as ashc's closures used to80
+// have). The captured variables get their own reserved local slots in the81
+// lambda's own chunk, placed right after its parameters, so the compiled82
+// body can reference them via the normal resolve_local() mechanism just83
+// like any other local.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 a146
+// fused OP_CMP_JUMP, used by if/while conditions. Returns the bytecode147
+// offset to patch_jump() once the branch target is known, or -1 if the148
+// condition didn't match the fusable shape (caller falls back to a plain149
+// 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_H2
+#define ASH_VM_COMPILER_H3
+#include "vm/chunk.h"4
+5
+#define MAX_VM_FUNCS 646
+#define MAX_VM_LOCALS 1287
+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
+#endifashvm/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_H2
+#define ASH_VM_COMPILER_CONSTANTS_H3
+#include "value/value.h"4
+5
+#define MAX_VM_CONSTS 326
+7
+typedef struct {8
+ char name[96];9
+ VMValue value; // built once at declare time via vm_num/vm_str, reused10
+ // (by re-emitting, not by sharing the VMValue struct11
+ // itself -- see codegen_constant_value) at every use site12
+} 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 value18
+// 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 and22
+// '=' have been consumed: if an import namespace is currently active,23
+// consumes the RHS (must be a literal number or string), registers it as24
+// a namespaced constant, and consumes the trailing ';'. Returns 1 if it25
+// handled the whole statement (caller should return immediately), 0 if no26
+// namespace is active (caller should fall through to normal27
+// variable-declaration handling).28
+int try_declare_import_constant(const char *name, int len);29
+30
+#endifashvm/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 imported93
+ // file's own namespace context, a bare reference may be that same94
+ // 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=c112
+#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_H2
+#define ASH_VM_COMPILER_IMPORT_PATHS_H3
+#include <limits.h>4
+5
+// Resolves the raw text inside `@import <...>` to a canonicalized6
+// (realpath()'d) absolute filesystem path.7
+//8
+// raw_path/raw_len: the text between '<' and '>', exactly as scanned by9
+// lexer_scan_import_path (NOT null-terminated -- callers must use len).10
+// importing_file_dir: directory of the file containing this @import11
+// statement (relative imports resolve against this).12
+// project_root_dir: directory of the entry .ash file passed on the13
+// command line (library imports resolve against this, regardless of14
+// 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 failure18
+// (message_out is filled with a short human-readable reason, e.g.19
+// "could not resolve '<path>'" or "library import name must not contain20
+// '.' 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's28
+// own basename with the trailing ".ash" stripped. Works uniformly for29
+// both relative and library imports because library entry files are30
+// required to be named "<libname>/<libname>.ash" -- the folder name and31
+// 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
+#endifashvm/src/compiler/internal.hadded
+45−0@@ -0,0 +1,45 @@1
+#ifndef ASH_VM_COMPILER_INTERNAL_H2
+#define ASH_VM_COMPILER_INTERNAL_H3
+#include "compiler/state.h"4
+#include "compiler/constants.h"5
+6
+// Cross-file forward declarations between compiler_expr.c and7
+// compiler_stmt.c -- not part of the public API (compiler.h).8
+9
+// compiler_expr.c10
+void expression(void);11
+12
+// compiler_calls.c13
+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 a17
+// fused OP_CMP_JUMP, used by if/while conditions. Returns the bytecode18
+// offset to patch_jump() once the branch target is known, or -1 if the19
+// condition didn't match the fusable shape (caller falls back to a plain20
+// expression() + OP_JUMP_IF_FALSE).21
+int try_fuse_condition(void);22
+23
+// compiler_stmt.c24
+void block(void);25
+// statement() itself (compiler_stmt.c's dispatcher) also needs to be26
+// 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 here29
+// rather than staying static to compiler_stmt.c.30
+void statement(void);31
+32
+// compiler_stmt_control.c -- the four statement kinds involved enough to33
+// warrant their own function (each assumes its keyword token has already34
+// 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.c41
+void compile_import_stmt(void);42
+void imports_close_block(void);43
+void imports_init(const char *entry_file_path);44
+45
+#endifashvm/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_H2
+#define ASH_VM_COMPILER_STATE_H3
+#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 same8
+// declare-storage-once, share-via-header pattern ashc used to use for its9
+// 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 own25
+// 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 is38
+// currently set, else under the bare NAME. Returns the new entry's index39
+// (mirrors compile_fn_decl's own prior inline logic, now centralized so40
+// imports.c-equivalent code and compile_fn_decl share it). Exits via41
+// diagnostics_report + exit(1) on table-full or name-too-long, matching42
+// 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 to47
+// find_function's existing behavior). Returns -1 if not found.48
+int find_function_in_context(const char *name, int len);49
+50
+#endifashvm/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 a9
+// fn/if/while/try body -- block_depth distinguishes the two so the "only10
+// fn/let at an imported file's top level" guard in statement() only11
+// fires for genuinely top-level statements, never for an ordinary12
+// return/if/etc. nested inside an imported function's own body.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 the25
+// 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 or177
+// 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 token7
+// (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_count96
+ // ended up after compiling the try block -- since the try block may have97
+ // declared its own locals before erroring, but the runtime error-value98
+ // push always lands at the try-entry stack position, regardless of what99
+ // the try block attempted (all of that gets discarded on the error path).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=c112
+#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 1611
+#define MAX_VM_IMPORT_DEPTH 812
+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 every88
+ // later use site that points diagnostics/lexer state back into it,89
+ // which can be arbitrarily far ahead in the importing program. The90
+ // compiler is a one-shot host process; leaking this until it exits is91
+ // 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 "only102
+// fn/let allowed" restriction by checking get_import_namespace()'s length103
+// AND that block_depth == 0 (see stmt.c) -- so it fires only for this104
+// file's genuine top level, never for statements nested inside one of its105
+// own function bodies.106
+static void parse_imported_file_body(void) {107
+ // Each imported file gets its OWN fresh import-block window: OPEN (not108
+ // closed), so the imported file's own leading @import statements (if109
+ // any -- e.g. a diamond-shaped or chained import) are allowed, exactly110
+ // like the entry file's. Calling imports_close_block() here would do111
+ // the opposite (close it), which is wrong -- statement() itself closes112
+ // the window the moment it dispatches this file's own first113
+ // 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 in136
+ // imported_files but STILL on resolving_stack is mid-parse (its137
+ // registry entry was added before recursing, same as every entry138
+ // here) -- that's a genuine cycle, not a finished diamond import.139
+ // Checking the registry first would silently treat a still-parsing140
+ // file as "already done" and swallow the cycle instead of reporting141
+ // it. Only a registry hit with NO matching resolving_stack entry is a142
+ // 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 do147
+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 the154
+ // namespace and surrounding literal text with no truncation, so155
+ // -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 will181
+ // clobber, mirroring compiler/state.c's own save-recurse-restore182
+ // pattern for locals (the C call stack gives correct nesting for183
+ // 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 parse207
+ // 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_H2
+#define ASH_VM_COMPILER_STMT_IMPORT_H3
+4
+void imports_init(const char *entry_file_path);5
+void compile_import_stmt(void); // assumes statement() already consumed TOKEN_IMPORT6
+void imports_close_block(void);7
+8
+#endifashvm/src/diagnostics/diagnostics.cadded
+126−0@@ -0,0 +1,126 @@1
+#define _POSIX_C_SOURCE 200809L2
+#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 from39
+// 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_H2
+#define ASH_DIAGNOSTICS_H3
+4
+// Rust-style colorized error reporting, shared by every stage (compile-time5
+// 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 to10
+// 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.c20
+// can point diagnostics at an imported file's own buffer while21
+// recursively parsing it, then restore the importing file's pair22
+// afterward -- mirrors lexer.h's LexerState save/restore.23
+DiagnosticsSource diagnostics_save_source(void);24
+void diagnostics_restore_source(DiagnosticsSource saved);25
+26
+#endifashvm/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-static7
+// before) -- shared with lexer_import.c now, and a plain `current` would8
+// 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 backslash87
+ advance(); // skip the escaped character itself, so a \" doesn't end the string early88
+ 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_H2
+#define LEXER_H3
+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_ERROR48
+} 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
+#endifashvm/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 immediately18
+// after the '<' the parser just confirmed (via current.type==TOKEN_LESS)19
+// WITHOUT calling advance_token() on it -- so `current` here still points20
+// at the first path character, untouched by normal tokenization. Scans21
+// raw bytes up to '>', consuming the '>' too, so the parser's next22
+// 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_H2
+#define ASHVM_LEXER_IMPORT_H3
+#include "lexer/lexer.h"4
+5
+// Cross-file link between lexer.c (the tokenizer's cursor state and its6
+// low-level scan primitives) and lexer_import.c (state save/restore plus7
+// import-path scanning, split out here to keep lexer.c under the8
+// project's ~200-line guideline) -- not part of the public API. The9
+// public entry points the two files implement together10
+// (lexer_save_state/lexer_restore_state/lexer_scan_import_path) stay11
+// declared in lexer.h exactly as before, so callers outside the lexer12
+// directory don't need to change anything. Mirrors kiln's identical split13
+// 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.h17
+// already declares its own global `Token current`, and since these now18
+// need external linkage to be shared across the two .c files, a second19
+// 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
+#endifashvm/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_H2
+#define ASH_VM_HASHMAP_H3
+#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
+#endifashvm/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_H2
+#define ASH_VM_VALUE_H3
+#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
+#endifashvm/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) and28
+// 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_H2
+#define ASH_VM_CHUNK_H3
+#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_POP26
+} 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 bytes32
+ 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
+#endifashvm/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's13
+// usual 200-line target: computed-goto (`&&label` / `goto *table[i]`)14
+// requires every label to live in the same function scope, so splitting it15
+// by opcode group isn't possible without giving up the dispatch technique16
+// that makes this VM fast in the first place. Everything that CAN live17
+// 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_POP35
+ };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) funnels10
+// through here. If a try block is active, longjmp back to its handler instead11
+// 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_H2
+#define ASH_VM_STATE_H3
+#include <setjmp.h>4
+#include "vm/chunk.h"5
+6
+// Internal VM state shared between vm_dispatch.c, vm_errors.c, and vm_api.c7
+// -- not part of the public API (vm.h). Mirrors the same8
+// declare-storage-once, share-via-header pattern ashc used to use for its9
+// parser state (see rundown.md).10
+11
+#define STACK_MAX 6553612
+#define FRAMES_MAX 25613
+#define MAX_TRY_DEPTH 3214
+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 per43
+// DISPATCH() (the single point every instruction passes through) so44
+// vm_runtime_error() can point diagnostics at the right spot without every45
+// 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
+#endifashvm/src/vm/vm.hadded
+16−0@@ -0,0 +1,16 @@1
+#ifndef ASH_VM_VM_H2
+#define ASH_VM_VM_H3
+#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
+#endifashvm/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 bash2
+set -uo pipefail3
+cd "$(dirname "$0")/.."4
+5
+OUT=$(../bin/ashvm tests/fixtures/imports_circular_a.ash 2>&1)6
+STATUS=$?7
+8
+if [[ $STATUS -eq 0 ]]; then9
+ echo "FAIL: expected nonzero exit, got 0"10
+ exit 111
+fi12
+if [[ "$OUT" != *"circular import"* ]]; then13
+ echo "FAIL: expected 'circular import' in output, got:"14
+ echo "$OUT"15
+ exit 116
+fi17
+if [[ "$OUT" != *"imports_circular_a.ash"* || "$OUT" != *"imports_circular_b.ash"* ]]; then18
+ echo "FAIL: expected both file names in the cycle message, got:"19
+ echo "$OUT"20
+ exit 121
+fi22
+23
+echo "OK: imports_circular_check"ashvm/tests/imports_collision_check.shadded
+23−0@@ -0,0 +1,23 @@1
+#!/usr/bin/env bash2
+set -uo pipefail3
+cd "$(dirname "$0")/.."4
+5
+OUT=$(../bin/ashvm tests/fixtures/imports_collision_main.ash 2>&1)6
+STATUS=$?7
+8
+if [[ $STATUS -eq 0 ]]; then9
+ echo "FAIL: expected nonzero exit, got 0"10
+ exit 111
+fi12
+if [[ "$OUT" != *"namespace"* ]]; then13
+ echo "FAIL: expected 'namespace' in output, got:"14
+ echo "$OUT"15
+ exit 116
+fi17
+if [[ "$OUT" != *"collision_dir_one/shared.ash"* || "$OUT" != *"collision_dir_two/shared.ash"* ]]; then18
+ echo "FAIL: expected BOTH colliding resolved paths in the message, got:"19
+ echo "$OUT"20
+ exit 121
+fi22
+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 bash2
+# ashvm/tests/run_regression.sh baseline|check3
+# Compiles+runs every ashvm/tests/*.ash and diffs stdout against a4
+# captured baseline -- mirrors kiln/tests/run_regression.sh. ashvm has no5
+# --target/--link flags and no input()-driven test today, so this is6
+# simpler than kiln's version.7
+set -uo pipefail8
+cd "$(dirname "$0")/.." # -> ashvm/9
+10
+MODE="${1:-check}"11
+if [[ "$MODE" != "baseline" && "$MODE" != "check" ]]; then12
+ echo "usage: $0 baseline|check" >&213
+ exit 214
+fi15
+16
+OUTDIR="/tmp/ashvm_regression/$MODE"17
+rm -rf "$OUTDIR"18
+mkdir -p "$OUTDIR"19
+20
+rm -f /tmp/ash_test.txt21
+22
+make -s || { echo "build failed"; exit 1; }23
+24
+for f in tests/*.ash; do25
+ name=$(basename "$f" .ash)26
+ out="$OUTDIR/${name}.out"27
+ if ! ../bin/ashvm "$f" > "$out" 2>&1; then28
+ echo "RUN FAIL" >> "$out"29
+ fi30
+done31
+32
+if [[ "$MODE" == "baseline" ]]; then33
+ echo "baseline captured in $OUTDIR"34
+ exit 035
+fi36
+37
+fail=038
+for f in "$OUTDIR"/*.out; do39
+ name=$(basename "$f" .out)40
+ base="/tmp/ashvm_regression/baseline/${name}.out"41
+ if [[ ! -f "$base" ]]; then42
+ echo "NEW: $name (no baseline to compare)"43
+ continue44
+ fi45
+ if diff -q "$base" "$f" > /dev/null; then46
+ echo "OK: $name"47
+ else48
+ echo "DIFF: $name"49
+ diff "$base" "$f"50
+ fail=151
+ fi52
+done53
+if ! ./tests/imports_circular_check.sh; then54
+ fail=155
+fi56
+if ! ./tests/imports_collision_check.sh; then57
+ fail=158
+fi59
+60
+exit $failashvm/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 bash2
+3
+if [ -z "$1" ]; then4
+ echo "usage: ./changes.sh \"description of what changed\""5
+ exit 16
+fi7
+8
+TIMESTAMP=$(date '+%Y-%m-%d %H:%M')9
+10
+{11
+ echo "## $TIMESTAMP"12
+ echo "$1"13
+ echo ""14
+} >> CHANGELOG.md15
+16
+echo "logged to CHANGELOG.md"kiln/Makefileadded
+73−0@@ -0,0 +1,73 @@1
+CC = gcc2
+# -MMD -MP: emit a per-object .d file listing the headers it includes, so3
+# editing a shared header (e.g. vars.h) correctly triggers a rebuild of4
+# every .o that includes it, not just files whose own .c changed. Without5
+# this, make's only dependency is "the .c is newer than the .o" -- a6
+# header-only edit silently produces a binary built from mismatched7
+# offsets/constants across files, with no error, until something crashes8
+# at runtime for reasons the source doesn't explain.9
+CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -MMD -MP10
+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.c30
+KILN_OBJ = $(KILN_SRC:.c=.o)31
+KILN_DEP = $(KILN_SRC:.c=.d)32
+33
+BIN = ../bin/kiln34
+35
+all: $(BIN)36
+37
+$(BIN): $(KILN_OBJ)38
+ mkdir -p ../bin39
+ $(CC) $(CFLAGS) -o $(BIN) $(KILN_OBJ) $(LDFLAGS)40
+41
+-include $(KILN_DEP)42
+43
+clean:44
+ find src -name '*.o' -delete45
+ find src -name '*.d' -delete46
+ rm -f $(BIN)47
+48
+# Plan B: builds libkilnrt.so and installs it at the fixed path every49
+# --link=shared kiln-compiled program's DT_RPATH points at (see50
+# src/elf/elf_dynamic.h's KILN_RPATH) -- needs root to write there, so51
+# this is a separate, explicit, opt-in step, never run by `make all`.52
+KILNRT = ../bin/libkilnrt.so53
+KILN_RPATH_DIR = /usr/local/lib/kiln54
+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.so61
+62
+# Windows equivalent: no install step needed -- the standard DLL search63
+# order checks the launching .exe's own directory first, so64
+# libkilnrt.dll just needs to sit next to whatever --link=shared65
+# --target=windows binary is using it (or on PATH).66
+KILNRT_DLL = ../bin/libkilnrt.dll67
+68
+$(KILNRT_DLL): $(BIN)69
+ $(BIN) --emit-runtime -o $(KILNRT_DLL) --target=windows70
+71
+runtime-dll: $(KILNRT_DLL)72
+73
+.PHONY: all clean install-runtime runtime-dllkiln/README.mdadded
+55−0@@ -0,0 +1,55 @@1
+# Kiln2
+3
+a standalone `.ash` compiler - not an interpreter -4
+working on Windows and Linux (macOS best-effort), invoked like5
+`kiln example.ash -o example`, with its own linker that eventually shares6
+a common runtime library across compiled binaries instead of duplicating7
+it in every executable.8
+9
+Zero external tools anywhere in the pipeline: no gcc/clang/ld/as. Kiln10
+hand-encodes x86-64 machine code and writes its own ELF executables11
+directly, the same "every line hand-written" rule that built `ashc` and12
+`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
+```bash21
+cd kiln && make # builds ../bin/kiln22
+../bin/kiln tests/hello.ash -o /tmp/hello23
+/tmp/hello # prints 7 (1 + 2 * 3)24
+```25
+26
+## What's deliberately not built yet27
+28
+- Everything beyond numeric `print` statements - variables, functions,29
+ strings, arrays, control flow, closures30
+- Multi-object linking (the whole program is one self-contained object31
+ right now; there's no symbol-resolution step yet)32
+- The actual shared-runtime-library dedup feature goal 2 is ultimately33
+ about - that needs real dynamic linking (PIC codegen, dynamic symbol34
+ table, PLT/GOT, ELF dynamic sections), a substantial milestone of its35
+ own, deliberately not tackled alongside "does codegen produce a runnable36
+ binary at all"37
+- Windows PE output, macOS Mach-O output38
+39
+## Layout40
+41
+```42
+kiln/43
+ Makefile builds ../bin/kiln44
+ src/45
+ lexer/ tokenizer (own copy, same shape as ashvm's)46
+ parser/ token state + statement grammar47
+ codegen/48
+ emit.c/.h raw x86-64 instruction encoding primitives49
+ expr.c/.h expression -> real arithmetic instructions50
+ print_int.c/.h int-to-decimal-ASCII + write(2) + exit(2)51
+ elf/52
+ elf_writer.c/.h minimal "tiny ELF" executable writer53
+ main.c argv parsing, drives the pipeline54
+ tests/ *.ash programs exercised by hand for now55
+```\ No newline at end of filekiln/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_ptr22
+// [24]=i [32]=c_str (current arg's raw null-terminated pointer) [40]=length23
+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); // argc29
+ 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 block40
+ 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 = 044
+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 itself57
+ emit_add_reg_reg(code, REG_RSI, REG_RAX);58
+ emit_load_mem_disp32(code, REG_RSI, REG_RSI, 0); // the char* value itself59
+ 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_str72
+ emit_sub_reg_reg(code, REG_RCX, REG_RDX); // length = cursor - c_str73
+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RCX);74
+75
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40); // length76
+ string_alloc_emit_prefixed(code); // RAX = new block77
+ 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_str81
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40); // length82
+ bytes_emit_copy(code);83
+84
+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);85
+ emit_add_reg_imm8(code, REG_RAX, 8); // payload86
+87
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i88
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_data_ptr89
+ 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); // argc104
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);105
+ heap_emit_alloc(code); // RAX = array object106
+ 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.h7
+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_H2
+#define KILN_ARGV_BUILTIN_H3
+4
+// argv(): no arguments -- codegen/builtins.c's dispatch just expects the5
+// closing ')' before calling this. Pushes an array of strings, one per6
+// process argument (argv[0] is the executable's own path, same7
+// convention as C's main(argc, argv)).8
+void codegen_builtin_argv(void);9
+10
+#endifkiln/src/codegen/argv_builtin_internal.hadded
+13−0@@ -0,0 +1,13 @@1
+#ifndef KILN_ARGV_BUILTIN_INTERNAL_H2
+#define KILN_ARGV_BUILTIN_INTERNAL_H3
+#include "codegen/emit.h"4
+5
+// Windows has no kernel-provided argv array to read (see6
+// argv_builtin.c's header comment) -- GetCommandLineA() returns the raw7
+// command-line string instead, so the Windows path needs its own tokenizer8
+// rather than sharing codegen with the Linux path. Split into its own9
+// file to keep argv_builtin.c under ~200 lines; not part of the public10
+// codegen/argv_builtin.h surface, only argv_builtin.c calls this.11
+void codegen_builtin_argv_windows(void);12
+13
+#endifkiln/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 the13
+// Linux path (which reads an already-split array straight from the14
+// kernel's process-startup stack), this has to tokenize it by hand.15
+// Simple whitespace splitting only, no quoted-argument handling (an16
+// argument containing a space can't be represented) -- documented17
+// simplification, same spirit as this project's other "don't18
+// over-engineer for what can't practically happen" scope limits.19
+//20
+// Scratch layout: [0]=cmdline [8]=argc [16]=out_data_ptr [24]=i21
+// [32]=token_start [40]=token_end [48]=token_len [56]=copy_start22
+// [64]=copy_end (the latter two are token_start/end with surrounding23
+// quotes stripped, kept separate so stripping doesn't corrupt the24
+// 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); // cmdline32
+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); // count36
+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); // argc62
+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 block73
+ 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 = 079
+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_start91
+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_end103
+104
+ // GetCommandLineA() wraps the executable path in quotes (standard105
+ // Windows convention, always applied by real programs too, not just106
+ // when the path has spaces) -- strip a leading+trailing '"' pair if107
+ // both are present on this token. Doesn't handle quoted arguments108
+ // 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 the112
+ // NEXT token, and stripping a trailing quote there would skip past113
+ // it into whatever follows, corrupting where the next token starts.114
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32); // token_start115
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 40); // token_end116
+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RCX); // copy_start = token_start117
+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RDX); // copy_end = token_end118
+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_start137
+ 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 block141
+ 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_start145
+ 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); // payload150
+151
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i152
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_data_ptr153
+ 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_end166
+ 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); // argc171
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);172
+ heap_emit_alloc(code); // RAX = array object173
+ 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.h8
+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 block23
+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash: RSI survives the pop loop below24
+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); // payload29
+ emit_pop_reg(code, REG_RBX); // tag30
+ 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 object35
+ emit_push_reg(code, REG_RSI); // save elements block address across this alloc36
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);37
+ heap_emit_alloc(code); // RAX = array object38
+ 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); // capacity42
+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count43
+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI); // data_ptr44
+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 address48
+}49
+50
+// Given an array object address (RSI) and a raw index payload (RAX, a51
+// double's bits), converts to an integer index (RCX), dies with `msg` if52
+// 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); // count59
+ 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_ptr62
+ 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 payload71
+ emit_pop_reg(code, REG_RBX); // index tag (assumed NUMBER)72
+ emit_pop_reg(code, REG_RSI); // array object address73
+ 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_ptr76
+77
+ // element address = data_ptr + RCX*1678
+ 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 payload90
+ emit_pop_reg(code, REG_RBX); // value tag91
+ emit_pop_reg(code, REG_RCX); // index payload92
+ emit_pop_reg(code, REG_RDI); // index tag (ignored)93
+ emit_pop_reg(code, REG_RSI); // array object address94
+ emit_pop_reg(code, REG_RDI); // array tag (ignored)95
+96
+ // stash value on the real stack while bounds_check uses RAX/RCX/RDX97
+ emit_push_reg(code, REG_RBX); // value tag98
+ emit_push_reg(code, REG_RAX); // value payload99
+ emit_mov_reg_reg(code, REG_RAX, REG_RCX); // index payload -> RAX, where bounds_check expects it100
+101
+ bounds_check("runtime error: array index out of bounds"); // RCX=index, RDX=data_ptr102
+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 address106
+107
+ emit_pop_reg(code, REG_RAX); // value payload108
+ emit_pop_reg(code, REG_RBX); // value tag109
+ 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 as114
+// codegen_string_concat): [0]=val_tag [8]=val_payload [16]=array_object115
+void codegen_builtin_push(void) {116
+ emit_pop_reg(code, REG_RAX); // val payload117
+ emit_pop_reg(code, REG_RBX); // val tag118
+ emit_pop_reg(code, REG_RSI); // array object address119
+ 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); // capacity128
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count129
+ 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_capacity134
+ 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 size137
+138
+ emit_push_reg(code, REG_RCX); // save scratch base across the call139
+ 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 block141
+ emit_pop_reg(code, REG_RDX); // RDX = new_capacity142
+ emit_pop_reg(code, REG_RCX); // scratch base143
+144
+ emit_push_reg(code, REG_RAX); // save new data block across bytes_emit_copy145
+ emit_push_reg(code, REG_RDX); // save new_capacity146
+ emit_push_reg(code, REG_RCX); // save scratch base147
+148
+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 16); // RBX = array object149
+ emit_load_mem_disp32(code, REG_RDX, REG_RBX, 16); // RDX = old data_ptr150
+ emit_mov_reg_reg(code, REG_RBX, REG_RDX); // src = old data_ptr151
+ 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 capacity153
+ 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 copy156
+ bytes_emit_copy(code);157
+158
+ emit_pop_reg(code, REG_RCX); // scratch base159
+ emit_pop_reg(code, REG_RDX); // new_capacity160
+ emit_pop_reg(code, REG_RAX); // new data block161
+162
+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 16); // array object163
+ emit_store_mem_disp32(code, REG_RBX, 0, REG_RDX); // capacity = new_capacity164
+ emit_store_mem_disp32(code, REG_RBX, 16, REG_RAX); // data_ptr = new data block165
+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); // count171
+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // data_ptr172
+ 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 address175
+176
+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 0); // val_tag177
+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8); // val_payload178
+ 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 ashvm190
+}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); // count196
+ 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.h6
+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_H2
+#define KILN_ARRAYS_H3
+4
+// primary()'s '[' handling: parses `[e0, e1, ...]`, allocates a heap5
+// block sized for exactly that many elements (see codegen/value.h for6
+// 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 (below10
+// 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 pushed15
+// (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 (matching20
+// the normal call-argument convention) -- codegen/builtins.c's dispatch21
+// calls these after parsing push(arr, val) / len(arr)'s arguments.22
+23
+// push(array, value): appends, growing (a fresh, bigger heap block plus24
+// 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
+#endifkiln/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.h8
+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_H2
+#define KILN_BUILTINS_H3
+4
+// Called from primary()'s function-call handling right after the '('5
+// has been consumed. If `name` (length `len`) is a known builtin, parses6
+// its arguments itself (through the closing ')'), emits its codegen, and7
+// returns 1. Returns 0 without consuming anything otherwise, so the8
+// caller falls through to a user-defined function lookup.9
+int codegen_try_builtin_call(const char *name, int len);10
+11
+#endifkiln/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_back7
+// 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: skipped9
+// entirely under --link=shared (this routine lives in libkilnrt.so/.dll10
+// instead, emitted by the separate "compile the runtime" driver via11
+// bytes_emit_copy_routine_only), and bytes_emit_copy becomes a call12
+// 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.h7
+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_H2
+#define KILN_BYTES_H3
+#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 standalone10
+// "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 scratch16
+// pointer there across multiple calls. Shared between codegen/strings.c17
+// (concatenation) and codegen/arrays.c (growth). Plan B, phase B1: a real18
+// CALL to one shared copy (emitted by bytes_emit_startup) rather than19
+// re-emitting this loop inline at each of its ~18 call sites.20
+void bytes_emit_copy(CodeBuf *code);21
+22
+#endifkiln/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 a9
+// byte-by-byte transform loop advancing two raw pointers (source cursor10
+// in RSI, dest cursor in RDI) rather than indexing -- same technique11
+// codegen/closures.c's capture-copy loop uses, avoiding any need for a12
+// 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 payload15
+ emit_pop_reg(code, REG_RBX); // tag (ignored, assumed STRING)16
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length17
+ 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 cursor22
+ emit_mov_reg_reg(code, REG_RDI, REG_RAX);23
+ emit_add_reg_imm8(code, REG_RDI, 8); // dest cursor24
+ emit_mov_reg_reg(code, REG_RCX, REG_RDX); // remaining count25
+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 byte30
+ 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); // payload47
+ 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.h5
+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 gets12
+// built (a lambda literal's runtime header, and a zero-capture wrapper13
+// around an already-declared named function). The lambda body's own14
+// 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 and18
+// stores capture_count into [obj+8]. Leaves RBX = object address; the19
+// code address at [obj+0] is the caller's job (patchable for a lambda20
+// 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 RSI24
+ 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 must30
+// 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 by46
+// `argc` more (tag, payload) argument pairs already pushed on top of it47
+// (in order) -- the shared tail of every indirect call, regardless of48
+// where the closure value and arguments came from. codegen_indirect_call49
+// below is the "closure from a source-level variable, args from parsed50
+// syntax" case; codegen/higher_order.c's map/filter/reduce build both51
+// 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 here56
+// M (capture_count) is only known at runtime (read out of the closure57
+// object), so it's pushed via a genuine runtime loop -- unlike every58
+// other loop-shaped codegen in this project, which is always59
+// compile-time-bounded. Since M must survive the `call` (every GP60
+// register is freely clobbered by the callee) it's stashed in the fixed61
+// 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 address65
+66
+ emit_load_mem_disp32(code, REG_RCX, REG_RSI, 8); // capture_count67
+ 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 address69
+ emit_add_reg_imm8(code, REG_RSI, 16); // -> first capture70
+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 tag75
+ emit_push_reg(code, REG_RAX);76
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // capture payload77
+ 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) + captures86
+ // (runtime-known, recovered from the scratch slot -- registers were87
+ // 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); // *291
+ emit_add_reg_reg(code, REG_RCX, REG_RCX); // *492
+ emit_add_reg_reg(code, REG_RCX, REG_RCX); // *893
+ emit_add_reg_reg(code, REG_RCX, REG_RCX); // *1694
+ emit_add_reg_reg(code, REG_RSP, REG_RCX);95
+96
+ emit_push_reg(code, REG_RBX); // return convention: RBX=tag, RAX=payload97
+ emit_push_reg(code, REG_RAX);98
+}99
+100
+// `id` already resolved to variable `slot`; current sits on the first101
+// argument or ')'. Pushes the closure and parses+pushes the102
+// 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.h6
+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_H2
+#define KILN_CLOSURES_H3
+#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 the7
+// scope-limit note in codegen/value.h) every variable currently visible8
+// 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 value12
+// (not immediately called) -- wraps it in a zero-capture closure object so13
+// it flows through the same TAG_FUNCTION/indirect-call machinery as a14
+// 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 by25
+// `argc` more (tag, payload) argument pairs already pushed on top of it26
+// (in order) -- e.g. codegen/higher_order.c's map/filter/reduce, which27
+// build both programmatically rather than from source syntax. Performs28
+// the indirect call and pushes the (tag, payload) result.29
+void codegen_call_closure_value(int argc);30
+31
+#endifkiln/src/codegen/closures_internal.hadded
+10−0@@ -0,0 +1,10 @@1
+#ifndef KILN_CLOSURES_INTERNAL_H2
+#define KILN_CLOSURES_INTERNAL_H3
+4
+// Cross-file link between closures.c (object creation, named-function5
+// 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
+#endifkiln/src/codegen/convert_builtins.hadded
+24−0@@ -0,0 +1,24 @@1
+#ifndef KILN_CONVERT_BUILTINS_H2
+#define KILN_CONVERT_BUILTINS_H3
+4
+// Assumes its one argument already pushed -- codegen/builtins.c's dispatch5
+// calls these after parsing str(x)/num(x)'s argument.6
+7
+// str(number): formats into a heap-allocated, length-prefixed string8
+// (same layout as any other TAG_STRING value) -- the exact same digit9
+// algorithm codegen/print_int.c uses (sign, integer part backward, up to10
+// 6 fraction digits with round-half-up and trailing-zero trim), just11
+// targeting a heap block instead of a direct stdout write. Kept as its12
+// own self-contained copy rather than sharing code with print_int.c's13
+// 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 this17
+// freestanding binary) -- skips leading spaces/tabs, an optional18
+// sign, integer digits, an optional '.' and fraction digits. Stops at19
+// the first character it can't consume; a string with no valid numeric20
+// prefix at all yields 0.0, matching strtod's own "no conversion"21
+// behavior.22
+void codegen_builtin_num(void);23
+24
+#endifkiln/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 -- only37
+// valid when base isn't RSP (needs a SIB byte) or RBP (mod=00,rm=101 means38
+// RIP-relative instead), which is why this module's callers are required39
+// 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-bit45
+// displacement [base+disp32], mod=10 -- safe for RBP (mod=10 has no46
+// special-case meaning, unlike mod=00). RSP as base still needs a SIB47
+// 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); // placeholder63
+ 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, r6473
+ 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, r6479
+ 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 group85
+ emit_byte(buf, modrm_reg((Reg)0, reg)); // /0 = ADD86
+ 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 = ADD93
+ 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, r6499
+ 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 = SUB106
+ 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 = SUB113
+ 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/m64120
+ 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 = IDIV127
+}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 = NEG133
+}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 = DEC139
+}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 = CMP150
+ 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, r64162
+ 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, r64168
+ 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, r64174
+ 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 = NOT181
+}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, CL187
+}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, CL193
+}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, 1199
+}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.h3
+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_H2
+#define KILN_EMIT_H3
+#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 as7
+// 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 = 721
+} Reg;22
+23
+typedef enum {24
+ COND_E = 0x4, // je / jz25
+ COND_NE = 0x5, // jne / jnz26
+ COND_LT = 0xC, // jl (signed <) -- integer cmp only27
+ COND_GE = 0xD, // jge (signed >=) -- integer cmp only28
+ COND_LE = 0xE, // jle (signed <=) -- integer cmp only29
+ COND_GT = 0xF, // jg (signed >) -- integer cmp only30
+ COND_B = 0x2, // jb (CF=1) -- after ucomisd: a < b31
+ COND_AE = 0x3, // jae (CF=0) -- after ucomisd: a >= b32
+ COND_BE = 0x6, // jbe (CF=1|ZF=1) -- after ucomisd: a <= b33
+ COND_A = 0x7 // ja (CF=0&ZF=0) -- after ucomisd: a > b34
+} 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 first54
+// (same cvttsd2si conversion floor()/the array-index paths already use),55
+// matching this project's existing "assume NUMBER, no runtime type56
+// 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-6462
+// 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 right65
+void emit_sar_reg_by1(CodeBuf *buf, Reg reg); // shift right by exactly 1 (no CL needed) -- the exponent-by-squaring loop's n >>= 166
+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 disp3270
+// 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 a73
+// 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 module79
+// deliberately doesn't support; callers route byte-buffer pointers through80
+// 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 to88
+// 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), computed94
+// 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 earlier99
+// in the source, same single-pass limitation ashvm's compiler has -- so100
+// 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 holding105
+// an absolute runtime address. Needed for calling through a closure106
+// 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 an111
+// absolute runtime address, no return address pushed (unlike112
+// emit_call_indirect). Used by try/catch's raise routine to resume at a113
+// catch block once RSP/RBP have already been unwound to the handler's114
+// saved values -- this is a resumption, not a call, so nothing should be115
+// pushed.116
+void emit_jmp_indirect(CodeBuf *buf, Reg target);117
+118
+// "Emit now, patch later" for an 8-byte absolute-address immediate119
+// (mov reg, imm64), generalizing emit_jmp_rel32/emit_jcc_rel32's pattern120
+// to something other than a rel32 jump target: a lambda literal's121
+// absolute code address isn't known until AFTER its body compiles (which122
+// happens after the closure-creation code that needs to embed it), so the123
+// mov is emitted with a zero placeholder and the returned offset is124
+// 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 code129
+// needs to reference its OWN embedded data once it can be loaded at a130
+// position ld.so chooses (libkilnrt.so), unlike every other address in131
+// this project which is computed as a KILN_LOAD_BASE-relative compile-time132
+// constant (only valid for kiln's always-fixed-base EXECUTABLES). Same133
+// "emit now, patch later" shape as emit_jmp_rel32: returns the patch134
+// offset, pass it to emit_patch_jump() once the target offset is known135
+// (the disp32 is relative to the byte right after itself, i.e. RIP at136
+// 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 no141
+// register at all. `addr` must fit in 31 bits (true for every kiln142
+// fixed-base address). See emit_mem.c for the encoding rationale.143
+void emit_call_abs32(CodeBuf *buf, uint32_t addr);144
+145
+#endifkiln/src/codegen/emit_internal.hadded
+18−0@@ -0,0 +1,18 @@1
+#ifndef KILN_EMIT_INTERNAL_H2
+#define KILN_EMIT_INTERNAL_H3
+#include "codegen/emit.h"4
+5
+// Shared byte-buffer primitives and ModRM builders, used by both emit.c6
+// (register arithmetic) and emit_mem.c (memory/stack/jump instructions).7
+// Not part of the public API (emit.h).8
+#define REX_W 0x489
+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
+#endifkiln/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, r647
+ 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/m6414
+ 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, imm828
+ 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/m841
+ 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); // placeholder54
+ 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); // placeholder61
+ 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/m6495
+}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/m64100
+}101
+102
+int emit_lea_rip_disp32(CodeBuf *buf, Reg dst) {103
+ emit_byte(buf, REX_W);104
+ emit_byte(buf, 0x8D); // LEA r64, m105
+ emit_byte(buf, (uint8_t)(((dst & 7) << 3) | 5)); // mod=00, rm=101 -> RIP-relative106
+ int patch_offset = buf->count;107
+ emit_i32(buf, 0); // placeholder108
+ return patch_offset;109
+}110
+111
+// Same instruction, for the already-emitted-earlier case (e.g. embedded112
+// data emitted before the code that references it, jumped over) -- same113
+// "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 mode123
+// (mod=00, rm=100 selects SIB, SIB base=101 with no index/base register124
+// means "just disp32", distinct from mod=00,rm=101's RIP-relative125
+// meaning). Dereferences the given absolute address and calls through126
+// the result, touching NO general-purpose register at all -- needed for127
+// Plan B's GOT calls specifically because kiln's runtime routines each128
+// have their own fixed argument registers (RAX/RBX/RDI/RDX depending on129
+// which routine), so there's no register universally safe to clobber130
+// just to hold the GOT address first. Only valid for addresses that fit131
+// 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.h3
+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 -- same5
+// mod=11 shape modrm_reg() builds, xmm register numbers 0-7 encode6
+// 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/m6416
+ 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, xmm24
+ 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, xmm231
+ 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], xmm46
+ 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, xmm54
+ 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, xmm92
+ 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/m64100
+ 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, xmm2107
+ 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], xmm115
+ 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.h3
+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_H2
+#define KILN_EMIT_SSE_H3
+#include "codegen/emit.h"4
+5
+// Scalar double-precision (SSE2) primitives -- milestone 4 replaces6
+// kiln's integer-only numbers with real doubles throughout, matching7
+// ashvm's VMValue.number (always double). Kiln milestone 1-3 never8
+// needed these; everything user-visible now flows through XMM registers9
+// instead of GP registers. GP registers are still used for internal10
+// plumbing that isn't an ash-visible value (loop counters in the print11
+// 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 pushed16
+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 mask27
+28
+void emit_cvttsd2si(CodeBuf *buf, Reg dst, XReg src); // truncate double -> int6429
+void emit_cvtsi2sd(CodeBuf *buf, XReg dst, Reg src); // int64 -> double30
+31
+// Sets flags the same way an unsigned integer compare would (CF/ZF, not32
+// SF/OF) -- callers must use COND_B/COND_BE/COND_A/COND_AE/COND_E/COND_NE33
+// 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-build37
+// the equivalent (sub rsp,8 + movsd [rsp] / movsd [rsp] + add rsp,8) so38
+// the same stack-machine codegen model expr.c/expr_bool.c already use for39
+// 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
+#endifkiln/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, never19
+// executed as instructions) -- the SAME layout codegen/strings.c's20
+// literals use ([payload-8]=length, no trailing newline in the data21
+// itself), since milestone 9 makes a caught error a real, usable22
+// TAG_STRING value (print/concat/equality all assume this layout). This23
+// embedding always happens in the EXECUTABLE (call sites for24
+// errors_emit_die/fatal live all over the codebase, never in the25
+// extracted runtime), so kiln_code_base() -- which only knows a fixed26
+// EXECUTABLE's own base -- is always the right address to use here, even27
+// 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 libc42
+// setjmp in a freestanding binary), stores the message as a TAG_STRING43
+// value into the handler's recorded error-variable slot, and jumps to44
+// its catch block. Otherwise falls through to an inline fatal path45
+// (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 handler58
+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 size61
+ emit_mov_reg_imm64(code, REG_RDI, kiln_try_handlers_addr());62
+ emit_add_reg_reg(code, REG_RAX, REG_RDI); // RAX = handler slot address63
+64
+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 0); // saved rsp65
+ emit_load_mem_disp32(code, REG_RDI, REG_RAX, 8); // saved rbp66
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, 16); // catch target (clobbers RDX -- fine, msg len only needed on the fatal branch we didn't take)67
+ emit_load_mem_disp32(code, REG_RBX, REG_RAX, 24); // error variable's rbp-relative tag offset68
+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 address74
+ emit_mov_reg_imm64(code, REG_RAX, TAG_STRING);75
+ emit_store_mem_disp32(code, REG_RCX, 0, REG_RAX); // tag76
+ emit_store_mem_disp32(code, REG_RCX, 8, REG_RSI); // payload = message address77
+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 -- nothing82
+ // between the raise routine's entry and here touches them on the path83
+ // 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 197
+ emit_mov_reg_imm64(code, REG_RAX, 60); // syscall: exit98
+ 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 back121
+}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" (exceeding132
+// MAX_KILN_TRY_DEPTH itself -- silently continuing there would mean133
+// pushing a handler past the fixed table, corrupting the adjacent code134
+// segment). Only ever called from one site (parser/try_catch.c), so135
+// there's no deduplication payoff in routing it through the shared136
+// runtime at all -- it just emits its own small inline write+exit137
+// sequence directly, in every link mode, decoupled from wherever RAISE138
+// 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.h8
+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_H2
+#define KILN_ERRORS_H3
+#include "codegen/emit.h"4
+5
+// Runtime (not compile-time -- see parser/parser.h's parse_error for6
+// that) errors: array out of bounds, missing map key, and so on.7
+// Emitted once, at the very start of the generated program (right after8
+// codegen/heap.c's heap_emit_startup): a single reusable "raise" routine9
+// that every runtime error jumps to (never called -- there's nothing to10
+// return to, control either resumes at a catch block or the process11
+// exits).12
+void errors_emit_startup(CodeBuf *code);13
+14
+// Plan B: emits ONLY the raise routine's body -- used by the standalone15
+// "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 executed20
+// as instructions -- same technique codegen/strings.c uses for string21
+// literals) and raises it: if a `try` is currently active (see22
+// parser/try_catch.c), unwinds to its `catch` block with `msg` bound as23
+// a TAG_STRING value; otherwise writes it to stderr and exits with24
+// 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 check28
+// entirely. Used only for the one internal case that must never be29
+// "catchable": exceeding MAX_KILN_TRY_DEPTH itself (silently continuing30
+// there would mean pushing a handler past the fixed table, corrupting31
+// 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's35
+// `throw expr;`) instead of a compile-time-constant C string -- the36
+// caller must already have RSI=message payload and RDX=message length37
+// loaded (a throw expression's already-popped (tag,payload), payload is38
+// already a valid length-prefixed TAG_STRING pointer, so there's nothing39
+// to embed, just the same jump to the raise routine errors_emit_die uses.40
+void errors_emit_die_dynamic(CodeBuf *code);41
+42
+#endifkiln/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 -> primary21
+// (comparison and up live in expr_bool.c). Every level, on completion,22
+// has pushed exactly23
+// 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-copying28
+// doesn't care what the bits mean). XMM registers only ever hold the29
+// payload transiently, at the point of actually doing floating-point30
+// arithmetic on a NUMBER-tagged value -- extracted via31
+// 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 own35
+// operators do when given the wrong type (ashvm raises a runtime type36
+// error there; kiln doesn't have runtime errors yet -- a future37
+// 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 scientific45
+ // notation ("1e10") once the lexer hands back the right token46
+ // boundaries (see lexer.c's number()); the one thing it doesn't47
+ // understand is underscore digit separators (1_000_000), so48
+ // 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 reference133
+ // used as a value (not immediately called), wrapped as a134
+ // 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 imported141
+ // file's own namespace context, a bare reference may be that same142
+ // file's own top-level constant: let_statement always registers143
+ // 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" namespacing146
+ // resolve_function_in_context already does for bare function147
+ // names above -- otherwise a constant could never be used from148
+ // 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 one179
+// `x` actually is can only be known at runtime, so this pops both values180
+// just far enough to check the base's tag, pushes them straight back in181
+// the same order, and dispatches. A while loop (not a single check) so182
+// chains like `arr[0][1]` work, matching ashvm/src/compiler/expr.c's183
+// 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 payload192
+ emit_pop_reg(code, REG_RBX); // index tag193
+ emit_pop_reg(code, REG_RCX); // base payload194
+ emit_pop_reg(code, REG_RDX); // base tag195
+ 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 this217
+ // (TOKEN_BANG -> OP_NOT); kiln never wired it up. Logical not: pushes218
+ // 1.0 if the operand's payload is zero, else 0.0 -- matches219
+ // pop_and_test_truthy's "NUMBER != 0" definition of truthy elsewhere220
+ // in this file, and works for nil too since nil's payload is always221
+ // 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); // payload226
+ 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.0236
+ 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.0245
+ 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); // payload252
+ 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 - operand256
+ 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); // payload266
+ 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); // -> int64269
+ 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 as281
+// `-(2 ** 2)` (Python's convention) and `2 ** 3 ** 2` is right-associative282
+// (the exponent is parsed via unary(), not power(), so it recurses back283
+// 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 operators287
+// truncate their operands) -- computed by exponentiation-by-squaring, a288
+// genuine runtime loop since the exponent is only known at runtime,289
+// unlike every other arithmetic operator here. Negative exponents work290
+// via computing on the absolute value then reciprocating. No x87291
+// FYL2X/F2XM1 in this project (SSE2 only), so fractional exponents292
+// (x ** 0.5) aren't supported -- that would need a real pow(), out of293
+// 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 payload301
+ emit_pop_reg(code, REG_RBX); // exponent tag (ignored)302
+ emit_pop_reg(code, REG_RCX); // base payload303
+ emit_pop_reg(code, REG_RDX); // base tag (ignored)304
+305
+ emit_movq_xmm_from_reg(code, XMM0, REG_RCX); // base306
+ emit_movq_xmm_from_reg(code, XMM2, REG_RAX); // exponent (double), temp307
+ emit_cvttsd2si(code, REG_RCX, XMM2); // n = truncate(exponent)308
+309
+ emit_mov_reg_imm64(code, REG_RBX, 0); // neg flag310
+ 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.0321
+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 & 1329
+ 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 *= base332
+ emit_patch_jump(code, skip_mul);333
+334
+ emit_mulsd(code, XMM0, XMM0); // base *= base335
+ emit_sar_reg_by1(code, REG_RCX); // n >>= 1336
+ 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 / result344
+ 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 payload361
+ emit_pop_reg(code, REG_RDX); // b tag (assumed NUMBER)362
+ emit_pop_reg(code, REG_RAX); // a payload363
+ 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 `+` and376
+// parser.c's `x += e` compound assignment.377
+void codegen_apply_plus(void) {378
+ // Runtime tag check: both STRING -> concatenate, else assume both379
+ // 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 result385
+ 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 payload405
+ emit_pop_reg(code, REG_RDX); // b tag406
+ emit_pop_reg(code, REG_RAX); // a payload407
+ emit_pop_reg(code, REG_RBX); // a tag408
+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.h7
+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_H2
+#define KILN_CODEGEN_EXPR_H3
+#include "lexer/lexer.h"4
+5
+// Parses one expression and emits real x86-64 code for it via the6
+// shared `code` buffer (see parser/parser.h) -- consumes tokens7
+// directly, no AST. Leaves the computed value's (tag, payload) pushed on8
+// the (simulated, real CPU) stack.9
+void codegen_expression(void);10
+11
+// Parses a call's arguments and emits it -- `id` is the already-consumed12
+// function/builtin name token, and the '(' must already be consumed too13
+// (current sits on the first argument or ')'). Leaves the call's (tag,14
+// payload) result pushed, same as any other expression. Exposed (not15
+// 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 rather21
+// than a contiguous span of the source text, so there's no single Token22
+// to hand codegen_call. Already-fully-qualified name -- resolved via23
+// resolve_function (not resolve_function_in_context), since the caller24
+// (codegen/expr.c's primary()) has already done the namespace lookup by25
+// 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 already29
+// popped (matching the pop order every binary-operator codegen in this30
+// project uses) -- pushes the tag-aware sum (string concat if both are31
+// STRING, numeric add otherwise). Factored out of additive()'s `+`32
+// branch so parser.c's `x += e` compound-assignment codegen can reuse33
+// the exact same logic instead of a second, drifting copy of it.34
+void codegen_apply_plus(void);35
+36
+#endifkiln/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 full12
+// chain, the (tag, payload) push/pop convention, and the scope-limit13
+// 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 clean31
+// pushed 0.0/1.0, via the same jcc-and-patch technique32
+// codegen/print_int.c already uses for its digit loop -- just applied to33
+// 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/AE35
+// 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 were46
+// compared against 0.0 (ucomisd), for truthiness tests -- assumes47
+// 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); // payload50
+ 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), matching57
+// ashvm/src/compiler/expr.c's comparison() shape exactly (an `if`, not a58
+// `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 payload68
+ emit_pop_reg(code, REG_RDX); // b tag69
+ emit_pop_reg(code, REG_RAX); // a payload70
+ emit_pop_reg(code, REG_RBX); // a tag71
+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 result81
+ 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 to108
+// right) sitting between comparison and logical_and -- not real C109
+// precedence (which splits shift/bitand/bitxor/bitor into four separate110
+// levels), a deliberate simplification since mixed bitwise+comparison111
+// 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 payload122
+ emit_pop_reg(code, REG_RDX); // b tag (ignored)123
+ emit_pop_reg(code, REG_RAX); // a payload124
+ 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 -> int64128
+ emit_movq_xmm_from_reg(code, XMM0, REG_RCX);129
+ emit_cvttsd2si(code, REG_RCX, XMM0); // b -> int64130
+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 gets149
+// 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 gets169
+// 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-associative189
+// via the recursive codegen_expression() calls for both branches (so190
+// `a ? b : c ? d : e` parses as `a ? b : (c ? d : e)`), same shape as191
+// if/else's truthy-test-then-jump pattern, just as an expression instead192
+// 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 payload198
+ 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 branch205
+ 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 branch209
+ 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.h5
+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 places11
+// (expr.c's primary(), for calls used as expressions, and parser.c's12
+// bare `f(...);` statement) and is a clean, self-contained unit on its13
+// 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 a19
+ // TAG_FUNCTION closure/lambda value) goes through the indirect-call20
+ // path instead of the fast compile-time-known-target path below --21
+ // purely additive, every existing direct call to a declared `fn` is22
+ // 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=payload45
+ 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's50
+// combined name ("math.add") isn't a contiguous span in the original51
+// 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 wrapping54
+ 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.h5
+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_H2
+#define KILN_EXPR_INTERNAL_H3
+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
+#endifkiln/src/codegen/file_builtins.hadded
+12−0@@ -0,0 +1,12 @@1
+#ifndef KILN_FILE_BUILTINS_H2
+#define KILN_FILE_BUILTINS_H3
+4
+// read_file/file_exists assume their one PATH argument already pushed --5
+// matching sqrt/abs/floor's convention. write_file/append_file self-parse6
+// 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
+#endifkiln/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); // src10
+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RDX); // length11
+12
+ emit_mov_reg_reg(code, REG_RDI, REG_RDX);13
+ emit_add_reg_imm8(code, REG_RDI, 1); // +1 for the null terminator14
+ heap_emit_alloc(code); // RAX = buf15
+ 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); // src19
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // length20
+ bytes_emit_copy(code);21
+22
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 16); // buf23
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // length24
+ 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 terminator27
+ // RAX already holds buf, the return value28
+}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.h4
+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_H2
+#define KILN_FILE_PATH_H3
+#include "codegen/emit.h"4
+5
+// Kiln strings are length-prefixed, not null-terminated -- but the raw6
+// `open` syscall (like libc's) requires a null-terminated path. Input:7
+// RSI=string payload, RDX=length. Output: RAX=address of a fresh8
+// 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
+#endifkiln/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]=count12
+// [32]=out_data_ptr [40]=i [48]=out_count13
+//14
+// The output block is sized to the INPUT count (an upper bound on how15
+// 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 grow18
+// from).19
+void codegen_builtin_filter(void) {20
+ codegen_expression(); // array21
+ expect(TOKEN_COMMA, "expected ',' after array argument");22
+ codegen_expression(); // fn23
+ expect(TOKEN_RPAREN, "expected ')' after arguments");24
+25
+ emit_pop_reg(code, REG_RAX); // fn payload26
+ emit_pop_reg(code, REG_RBX); // fn tag27
+ emit_pop_reg(code, REG_RSI); // array object28
+ 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); // count35
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr36
+ 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 block47
+ 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 = 051
+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RCX); // out_count = 052
+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_ptr60
+ 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 address64
+65
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 0); // fn_tag66
+ emit_push_reg(code, REG_RAX);67
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // fn_payload68
+ emit_push_reg(code, REG_RAX);69
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // element tag70
+ emit_push_reg(code, REG_RAX);71
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload72
+ emit_push_reg(code, REG_RAX);73
+ codegen_call_closure_value(1);74
+75
+ emit_pop_reg(code, REG_RAX); // result payload76
+ 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 keep81
+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 again88
+89
+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 48); // out_count90
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 32); // out_data_ptr91
+ 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 address95
+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 count110
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 48); // count = out_count111
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);112
+ heap_emit_alloc(code); // RAX = array object113
+ 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.h6
+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 0x40u10
+11
+// Plan B, phase B1: emitted ONCE (jumped over, like errors.c's raise12
+// routine), called from every allocation site via emit_call_back instead13
+// 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 emitted15
+// into the executable at all -- it lives in libkilnrt.so instead (see16
+// heap_emit_alloc_routine_only, called by the separate "compile the17
+// runtime" driver), and heap_emit_alloc becomes a GOT call instead of a18
+// 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_ptr27
+ 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 (see33
+// elf/elf_so_writer.c) to emit this routine into libkilnrt.so's OWN code34
+// 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 itself42
+// being compiled INTO libkilnrt.so/.dll, its own call sites must stay43
+// local (same-buffer relative calls), not calls back out to the library44
+// it's already part of -- see target.h's comment on why this is a45
+// separate question from link mode. True for both targets: which OS46
+// decides how the actual call happens (elf_dynamic_call vs47
+// 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 -- no55
+ // "query then grow" dance needed the way brk requires.56
+ win_call_begin(code, 0);57
+ win_call_arg_imm64(code, 0, 0); // lpAddress = NULL58
+ 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. This70
+ // part always happens in the EXECUTABLE, even under --link=shared:71
+ // the arena and its heap_ptr live in the calling process's own72
+ // memory regardless of which binary's code manages the bump pointer.73
+ emit_mov_reg_imm64(code, REG_RAX, 12); // syscall: brk74
+ emit_mov_reg_imm64(code, REG_RDI, 0);75
+ emit_syscall(code);76
+77
+ // heap_ptr = current break (save BEFORE growing -- after the next78
+ // 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 checking83
+ // the result: a fixed, modest 16MB request failing is not a case84
+ // this project's "don't over-engineer error handling for things that85
+ // 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.h7
+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_H2
+#define KILN_HEAP_H3
+#include "codegen/emit.h"4
+5
+// A bump allocator, no free (matches ashvm's own lack of GC -- neither6
+// engine reclaims memory during a run, only at process exit). Backed by7
+// `brk` rather than `mmap`: brk takes exactly one argument (the new break8
+// address, in RDI), while mmap needs six, three of which (R10/R8/R9) kiln9
+// has deliberately never needed until now and would require extending10
+// emit.c's register handling to reach (those are all >= 8, needing11
+// REX.R/X/B bits this project's encoders don't set). brk sidesteps that12
+// entirely for a one-time, fixed-size arena grab.13
+14
+// Emitted once, at the very start of the generated program (before the15
+// top-level statement loop): grabs a 16MB arena and records its start as16
+// 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 by20
+// the standalone "compile the runtime" driver that builds libkilnrt.so,21
+// where the arena grab still happens in the calling EXECUTABLE instead22
+// (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 ran25
+// against) of the alloc routine's entry point -- used by the "compile26
+// 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 is30
+// never reachable from ash source, only from other codegen, so it can be31
+// as simple as it wants): size to allocate in RDI, returns the allocated32
+// block's address in RAX, clobbers RSI. Plan B, phase B1: this now emits33
+// a real CALL to one shared copy of the routine (emitted once by34
+// heap_emit_startup) instead of re-emitting the routine's body inline at35
+// every one of its ~27 call sites.36
+void heap_emit_alloc(CodeBuf *code);37
+38
+#endifkiln/src/codegen/higher_order.hadded
+19−0@@ -0,0 +1,19 @@1
+#ifndef KILN_HIGHER_ORDER_H2
+#define KILN_HIGHER_ORDER_H3
+4
+// Each parses its own arguments (array, fn, and for reduce, an initial5
+// value) starting right after the already-consumed '(' -- codegen/6
+// builtins.c's dispatch calls these directly, matching push()/len()'s7
+// 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 for13
+// 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
+#endifkiln/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_len24
+// [32]=i (candidate start position) [40]=last_start25
+//26
+// A plain O(n*m) scan (compare the search string against every candidate27
+// start position) -- matches this project's existing "documented28
+// simplification for implementation reach" precedent (e.g. maps.c's29
+// linear-scan lookup instead of a hash table).30
+void codegen_builtin_indexof(void) {31
+ codegen_expression(); // s32
+ expect(TOKEN_COMMA, "expected ',' after string argument");33
+ codegen_expression(); // search34
+ expect(TOKEN_RPAREN, "expected ')' after arguments");35
+36
+ emit_pop_reg(code, REG_RCX); // search payload37
+ emit_pop_reg(code, REG_RBX); // ignored38
+ emit_pop_reg(code, REG_RSI); // s payload39
+ emit_pop_reg(code, REG_RBX); // ignored40
+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_len45
+ emit_load_mem_disp32(code, REG_RDI, REG_RCX, -8); // search_len46
+ 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 position65
+ 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 = 068
+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 + i77
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 16); // search_ptr78
+ 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.h5
+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 own13
+// buffer size. No libc getline/fgets in this freestanding binary, so14
+// bytes come in one at a time via raw read(2) syscalls -- slow, but15
+// input() is never a hot loop, and "simple and obviously correct" beats16
+// 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 = buf20
+ int32_t base = higher_order_scratch_offset();21
+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RAX); // buf22
+ emit_mov_reg_imm64(code, REG_RCX, 0);23
+ emit_store_mem_disp32(code, REG_RBP, base + 8, REG_RCX); // pos = 024
+25
+ // Looked up once, reused every iteration -- on Windows this turns a26
+ // multi-byte line into one GetStdHandle round-trip instead of one per27
+ // byte (also closes a race window under Wine, where extra round-trips28
+ // 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 full38
+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 handle43
+ 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 error48
+ 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 it55
+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 length67
+ string_alloc_emit_prefixed(code); // RAX = block68
+ 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 buffer72
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // length73
+ bytes_emit_copy(code);74
+75
+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);76
+ emit_add_reg_imm8(code, REG_RAX, 8); // payload77
+ 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.h6
+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_H2
+#define KILN_INPUT_BUILTIN_H3
+4
+// input(): no arguments -- codegen/builtins.c's dispatch just expects the5
+// closing ')' before calling this.6
+void codegen_builtin_input(void);7
+8
+#endifkiln/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 between11
+// consecutive elements) to get the exact output size; pass 2 allocates12
+// once and copies. Avoids any resize-while-building complexity.13
+//14
+// Scratch layout: [0]=data_ptr [8]=count [16]=delim_ptr [24]=delim_len15
+// [32]=total_len [40]=i [48]=out_block [56]=cursor [64]=elem_len (temp)16
+void codegen_builtin_join(void) {17
+ codegen_expression(); // array18
+ expect(TOKEN_COMMA, "expected ',' after array argument");19
+ codegen_expression(); // delim20
+ expect(TOKEN_RPAREN, "expected ')' after arguments");21
+22
+ emit_pop_reg(code, REG_RSI); // delim payload23
+ emit_pop_reg(code, REG_RBX); // delim tag (ignored)24
+ emit_pop_reg(code, REG_RCX); // array object25
+ 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_ptr29
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // delim_len30
+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RDX);31
+32
+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 8); // count33
+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, 16); // data_ptr34
+ 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 = 040
+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // i = 041
+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 len55
+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_len59
+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 delim64
+ 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_len78
+ string_alloc_emit_prefixed(code); // RAX = block79
+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RAX); // out_block80
+ 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 start83
+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 = 087
+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 payload100
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // elem len101
+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RDX); // stash elem_len102
+103
+ emit_mov_reg_reg(code, REG_RBX, REG_RSI); // src104
+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 56); // dest = cursor105
+ 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_len111
+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_len121
+ 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 block138
+ emit_add_reg_imm8(code, REG_RAX, 8); // payload139
+ 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.h6
+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 table9
+// with scattered "used" flags) keeps every live entry packed into10
+// [0, count) -- delete() swaps the last entry into a removed slot rather11
+// than leaving a hole (see codegen/maps_mutate.c) -- so both builtins12
+// below can just iterate 0..count directly, no "used" check needed.13
+//14
+// Scratch layout: [0]=count [8]=entries_ptr [16]=out_block [24]=i15
+16
+void codegen_builtin_keys(void) {17
+ emit_pop_reg(code, REG_RSI); // map object18
+ 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); // count23
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // entries_ptr24
+ 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 block35
+ 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 = 039
+40
+ int loop_start = code->count;41
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i42
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 0); // count43
+ 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_ptr47
+ 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 address51
+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_block55
+ 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 address59
+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); // count70
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);71
+ heap_emit_alloc(code); // RAX = array object72
+ 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 object84
+ 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); // count89
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // entries_ptr90
+ 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 block101
+ 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 = 0105
+106
+ int loop_start = code->count;107
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 24); // i108
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 0); // count109
+ 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_ptr113
+ 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 address117
+118
+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // value tag119
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 24); // value payload120
+121
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16); // out_block122
+ 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 address126
+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); // count137
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);138
+ heap_emit_alloc(code); // RAX = array object139
+ 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.h5
+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_H2
+#define KILN_KEYS_VALUES_H3
+4
+// Both assume their one MAP argument already pushed -- codegen/builtins.c's5
+// 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
+#endifkiln/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 creation16
+// code (which runs in the OUTER frame, snapshotting captures by value)17
+// is emitted first, then a jump-over, then the body -- same shape as18
+// parser_control.c's fn_statement, generalized with a capture region19
+// ahead of the parameter region. The object's code address isn't known20
+// until after the body compiles, so it's written via a patchable imm6421
+// (see emit_mov_reg_imm64_patchable) and patched once the body's end is22
+// 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 address31
+ 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 below33
+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's79
+ // 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.h7
+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 being15
+ // compiled: libkilnrt.dll's own code (own fixed base, distinct16
+ // from any executable's), a --link=shared executable's code17
+ // (globals stay at the same spot either way, but the import18
+ // table before it is a different size -- see pe_writer.h), or a19
+ // 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 tables30
+ 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.h6
+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_H2
+#define KILN_LAYOUT_H3
+#include <stdint.h>4
+5
+// Every place in codegen that needs "the absolute address of code offset6
+// N" or "the absolute address of a globals-region field" used to hardcode7
+// 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 different9
+// executable formats with two different fixed base addresses (see10
+// elf/elf_writer.h vs pe/pe_writer.h), those hardcodes would each need11
+// their own target check -- so instead every call site goes through one12
+// 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 address14
+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
+#endifkiln/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 -- everything11
+// needed after codegen_call_closure_value must live here, never in a12
+// register, since the call clobbers all of them):13
+// [0]=fn_tag [8]=fn_payload [16]=data_ptr [24]=count14
+// [32]=out_data_ptr [40]=i15
+void codegen_builtin_map(void) {16
+ codegen_expression(); // array17
+ expect(TOKEN_COMMA, "expected ',' after array argument");18
+ codegen_expression(); // fn19
+ expect(TOKEN_RPAREN, "expected ')' after arguments");20
+21
+ emit_pop_reg(code, REG_RAX); // fn payload22
+ emit_pop_reg(code, REG_RBX); // fn tag23
+ emit_pop_reg(code, REG_RSI); // array object24
+ 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); // count31
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr32
+ 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 block44
+ 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 = 048
+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_ptr56
+ 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 address60
+61
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 0); // fn_tag62
+ emit_push_reg(code, REG_RAX);63
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // fn_payload64
+ emit_push_reg(code, REG_RAX);65
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 0); // element tag66
+ emit_push_reg(code, REG_RAX);67
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload68
+ emit_push_reg(code, REG_RAX);69
+ codegen_call_closure_value(1);70
+71
+ emit_pop_reg(code, REG_RAX); // result payload72
+ emit_pop_reg(code, REG_RBX); // result tag73
+74
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // i75
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 32); // out_data_ptr76
+ 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); // count89
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);90
+ heap_emit_alloc(code); // RAX = array object91
+ 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); // capacity93
+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count94
+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI); // data_ptr95
+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.h6
+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 value20
+ 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 block29
+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // stash across the pop loop30
+31
+ // Pairs were pushed key-then-value, left to right, so the LAST32
+ // 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 payload35
+ emit_pop_reg(code, REG_RBX); // value tag36
+ 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 payload40
+ emit_pop_reg(code, REG_RBX); // key tag41
+ 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 alloc46
+ emit_mov_reg_imm64(code, REG_RDI, MAP_OBJECT_SIZE);47
+ heap_emit_alloc(code); // RAX = map object48
+ 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); // capacity52
+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RCX); // count53
+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RSI); // entries_ptr54
+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); // i62
+63
+ int loop_start = code->count;64
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count65
+ 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_ptr69
+ 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 addr75
+76
+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 8); // entry key payload (a_payload)77
+ emit_push_reg(code, REG_RCX); // i78
+ emit_push_reg(code, REG_RSI); // map object79
+ emit_push_reg(code, REG_RDI); // target key80
+ emit_push_reg(code, REG_RBX); // entry addr81
+ emit_mov_reg_reg(code, REG_RCX, REG_RDI); // b_payload = target key82
+ codegen_string_compare(0); // pushes (tag=NUMBER, payload=0.0/1.0)83
+ emit_pop_reg(code, REG_RAX); // compare result payload84
+ emit_pop_reg(code, REG_RDX); // compare result tag (ignored)85
+ emit_pop_reg(code, REG_RBX); // entry addr86
+ emit_pop_reg(code, REG_RDI); // target key87
+ emit_pop_reg(code, REG_RSI); // map object88
+ emit_pop_reg(code, REG_RCX); // i89
+ 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 payload101
+ emit_pop_reg(code, REG_RBX); // key tag (assumed STRING)102
+ emit_pop_reg(code, REG_RSI); // map object103
+ 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 tag107
+ emit_load_mem_disp32(code, REG_RAX, REG_RBX, 24); // value payload108
+ 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.h6
+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_H2
+#define KILN_MAPS_H3
+4
+// primary()'s '{' handling: parses `{"k0": v0, "k1": v1, ...}` (keys must5
+// 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 a11
+// 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 already16
+// 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 its20
+// arguments already pushed left to right, matching the normal21
+// call-argument convention. keys()/values() (each needs to build a new22
+// ARRAY sized only-known-at-runtime, real extra complexity for23
+// comparatively rare use) are deferred to milestone 10 alongside the24
+// rest of ashvm's remaining builtins -- not implemented here.25
+void codegen_builtin_has(void); // has(map, key) -> NUMBER 1.0/0.026
+void codegen_builtin_delete(void); // delete(map, key) -> the map (matches ashvm), no-op if key absent27
+28
+#endifkiln/src/codegen/maps_internal.hadded
+14−0@@ -0,0 +1,14 @@1
+#ifndef KILN_MAPS_INTERNAL_H2
+#define KILN_MAPS_INTERNAL_H3
+4
+// Cross-file link between maps.c (literal, find_entry_index, read) and5
+// maps_mutate.c (store/has/delete -- everything that needs to find AND6
+// change an entry), not part of the public API (maps.h).7
+8
+// INPUT: RSI=map object, RDI=target key payload -- both untouched by the9
+// 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
+#endifkiln/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 that19
+// might have clobbered whichever register held it -- same technique20
+// codegen/arrays.c's codegen_builtin_push uses): [0]=val_tag21
+// [8]=val_payload [16]=key_payload [24]=map_object [32]=new_capacity22
+// (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 payload25
+ emit_pop_reg(code, REG_RBX); // value tag26
+ emit_pop_reg(code, REG_RDI); // key payload27
+ emit_pop_reg(code, REG_RCX); // key tag (ignored)28
+ emit_pop_reg(code, REG_RSI); // map object29
+ 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); // capacity54
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 8); // count55
+ 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_capacity59
+ 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 block64
+ 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 capacity69
+ 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 count75
+ 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_capacity81
+ emit_load_mem_disp32(code, REG_RAX, REG_RCX, 40);82
+ emit_store_mem_disp32(code, REG_RSI, 16, REG_RAX); // entries_ptr = new block83
+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); // count89
+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, 16); // entries_ptr90
+ 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 address94
+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_payload98
+ emit_store_mem_disp32(code, REG_RAX, 8, REG_RBX);99
+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 0); // val_tag100
+ emit_store_mem_disp32(code, REG_RAX, 16, REG_RBX);101
+ emit_load_mem_disp32(code, REG_RBX, REG_RCX, 8); // val_payload102
+ 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 payload115
+ emit_pop_reg(code, REG_RBX); // key tag (ignored)116
+ emit_pop_reg(code, REG_RSI); // map object117
+ 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 payload129
+ emit_pop_reg(code, REG_RBX); // key tag (ignored)130
+ emit_pop_reg(code, REG_RSI); // map object131
+ 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 last139
+ // 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); // count143
+ emit_sub_reg_imm8(code, REG_RAX, 1); // last_index144
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // entries_ptr145
+ 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 address149
+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 ashvm163
+ int done = emit_jmp_rel32(code);164
+165
+ // key absent -- no-op, still return the map (matches ashvm's166
+ // 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.h5
+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); // payload9
+ 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); // payload20
+ 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 bit23
+ 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 but32
+// 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 the34
+// truncated result back to a double and checking whether it OVERSHOT the35
+// original value catches exactly that case.36
+void codegen_builtin_floor(void) {37
+ emit_pop_reg(code, REG_RAX); // payload38
+ 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 floor44
+ 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.h4
+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_H2
+#define KILN_MATH_BUILTINS_H3
+4
+// Each assumes its one NUMBER argument already pushed -- codegen/builtins.c's5
+// 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
+#endifkiln/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 end16
+// pointer (payload+length) -- no libc strtod in this freestanding17
+// binary, so this hand-parses the same grammar kiln's own number18
+// literals accept (plus a leading sign and leading whitespace, matching19
+// 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 address22
+ emit_pop_reg(code, REG_RBX); // tag (assumed STRING)23
+24
+ emit_load_mem_disp32(code, REG_RDI, REG_RSI, -8); // length25
+ emit_add_reg_reg(code, REG_RDI, REG_RSI); // RDI = end pointer26
+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.063
+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 digit95
+ 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 * place107
+ emit_addsd(code, XMM0, XMM1);108
+ load_double_const(XMM1, 0.1);109
+ emit_mulsd(code, XMM2, XMM1); // place *= 0.1110
+ 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.h4
+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 caller12
+// is using to hold buf/len across this) survive untouched: both are13
+// 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: write31
+ 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: write38
+ 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: read49
+ 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 unused56
+ // 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 RDI62
+ win_file_call(code, PE_IMPORT_READ_FILE); // RAX already holds the handle63
+ // ReadFile doesn't report "bytes read" in RAX (it's a BOOL success64
+ // flag there) -- read it back out of the out-param scratch this65
+ // call just wrote, so the caller's "did we get a byte" check66
+ // 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: read72
+ 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.h7
+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_H2
+#define KILN_PLATFORM_CONSOLE_H3
+#include "codegen/emit.h"4
+5
+// Console I/O, dispatching on kiln_get_target() internally so call sites6
+// (codegen/print_int.c, codegen/errors.c, codegen/input_builtin.c) don't7
+// 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 to9
+// 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=bytes13
+// actually read (0 or 1) out, matching Linux read(2)'s own return14
+// convention so input_builtin.c's existing "did we get a byte" check15
+// (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 many21
+// bytes in a loop (codegen/input_builtin.c): looking the handle up once22
+// outside the loop instead of once per byte cuts a multi-byte line's23
+// worth of GetStdHandle round-trips down to one, which matters on24
+// 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: in27
+// RAX=handle (as returned above; ignored on Linux), RSI=1-byte buf,28
+// RDX=1; out RAX=bytes read (0 or 1), same convention as29
+// 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
+#endifkiln/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 0x80000000u7
+#define GENERIC_WRITE 0x40000000u8
+#define FILE_APPEND_DATA 0x00000004u // access-right-only open => every write auto-goes to EOF, no seek needed9
+#define FILE_SHARE_READ 1u10
+#define OPEN_EXISTING 3u11
+#define OPEN_ALWAYS 4u12
+#define CREATE_ALWAYS 2u13
+#define FILE_ATTRIBUTE_NORMAL 0x80u14
+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); // path18
+ 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 = NULL21
+ 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 = NULL24
+ 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_RDONLY31
+ emit_mov_reg_imm64(code, REG_RDX, 0);32
+ emit_mov_reg_imm64(code, REG_RAX, 2); // syscall: open33
+ 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 = 108942
+ // (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 064445
+ emit_mov_reg_imm64(code, REG_RAX, 2); // syscall: open46
+ 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: close58
+ 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); // handle64
+ emit_mov_reg_reg(code, REG_RDI, REG_RDX); // len65
+ win_file_call(code, PE_IMPORT_READ_FILE); // RAX=handle, RSI=buf (unchanged), RDI=len66
+ return;67
+ }68
+ emit_mov_reg_imm64(code, REG_RAX, 0); // syscall: read69
+ 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); // handle75
+ emit_mov_reg_reg(code, REG_RDI, REG_RDX); // len76
+ win_file_call(code, PE_IMPORT_WRITE_FILE);77
+ return;78
+ }79
+ emit_mov_reg_imm64(code, REG_RAX, 1); // syscall: write80
+ emit_syscall(code);81
+}82
+83
+// Contract on both targets: returns the size in RAX, leaves the file's84
+// read position untouched. GetFileSize gets this for free; Linux's lseek85
+// doesn't have a "size" query, only "seek and report new position", so86
+// the Linux path pairs a seek-to-end with a seek back to the start it87
+// found the fd at (offset 0, since this is only ever called right after88
+// 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 -- per97
+ // standard x86-64 semantics and every real-world Windows98
+ // 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_END102
+ emit_mov_reg_imm64(code, REG_RAX, 8); // syscall: lseek103
+ emit_syscall(code);104
+ emit_push_reg(code, REG_RAX); // save size -- the rewind below clobbers RAX105
+ emit_mov_reg_imm64(code, REG_RSI, 0);106
+ emit_mov_reg_imm64(code, REG_RDX, 0); // SEEK_SET107
+ 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.h6
+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_H2
+#define KILN_PLATFORM_FILE_H3
+#include "codegen/emit.h"4
+5
+// Real file I/O, dispatching on kiln_get_target() internally so call6
+// 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 is8
+// used the same way on both targets: a signed value, negative means9
+// "open failed" (Linux's -1 and Windows' INVALID_HANDLE_VALUE, which is10
+// also -1, both fail the exact same `cmp rax,0; jge ok` check already in11
+// use -- no call-site change needed there).12
+void platform_emit_open_read(CodeBuf *code); // in: RDI=nullterm path -> out: RAX=fd/handle13
+void platform_emit_open_write(CodeBuf *code, int append); // in: RDI=nullterm path -> out: RAX=fd/handle14
+void platform_emit_close(CodeBuf *code); // in: RDI=fd/handle15
+void platform_emit_read_bytes(CodeBuf *code); // in: RDI=fd/handle, RSI=buf, RDX=len16
+void platform_emit_write_bytes(CodeBuf *code); // in: RDI=fd/handle, RSI=buf, RDX=len17
+// Replaces the lseek(END)+lseek(SET) dance read_file_builtin.c used on18
+// Linux: Windows has a direct GetFileSize call, no seek round-trip19
+// needed. Files over 4GB aren't supported (matches this project's20
+// existing "don't over-engineer for what can't practically happen" scope21
+// limit already documented elsewhere for file I/O).22
+void platform_emit_file_size(CodeBuf *code); // in: RDI=fd/handle -> out: RAX=size23
+24
+#endifkiln/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 a13
+// GP register holding its raw IEEE-754 bit pattern -- same technique14
+// codegen/expr.c uses for user-written number literals (there's no "mov15
+// 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 decrements26
+// before each store so no reversal step is needed.27
+// rsp+32 '.' (only meaningful if frac_len > 0)28
+// [rsp+33, rsp+39) up to 6 fraction digits, built FORWARD29
+// dynamic '\n', placed right after whichever of the above30
+// actually got used, so the whole thing writes in ONE31
+// contiguous write(2) call regardless of whether this32
+// number turned out to be whole or fractional.33
+// RBX holds the sign flag through phase 3, then gets reused as frac_len34
+// (how many of the 6 fraction digits are significant, i.e. up to the35
+// last nonzero one) -- safe, sign has already been written to the36
+// buffer by the time phase 4 starts.37
+// Dispatches on the popped value's tag: a STRING just gets written38
+// directly (it's already bytes -- no digit conversion needed at all,39
+// unlike a number); everything below this point is the NUMBER path,40
+// unchanged in shape from milestone 4 except that it now starts from an41
+// already-popped payload (in RAX) instead of popping XMM0 itself.42
+//43
+// Plan B, phase B1: emitted once (jumped over), called via emit_call_back44
+// from codegen_print_top below -- this is the single biggest deduplication45
+// win of B1, since this routine used to be re-emitted in full at EVERY46
+// print statement in the source (dwarfing heap_emit_alloc/bytes_emit_copy,47
+// which are individually smaller). Takes its value in RAX=payload,48
+// RBX=tag (NOT by popping them itself): a real CALL pushes a return49
+// address on top of whatever the caller already pushed, so a `pop` here50
+// would grab the return address instead of the caller's value -- same51
+// register-based-argument reasoning codegen/heap.c's alloc routine52
+// already uses, not something print specifically needed before this53
+// became a real subroutine. Each of its three paths (string/nil/number)54
+// still balances its own RSP usage back to the entry value, which a real55
+// subroutine does need.56
+static int print_routine_offset = -1;57
+58
+static void emit_print_routine(CodeBuf *code) {59
+ int routine_skip = emit_jmp_rel32(code);60
+ print_routine_offset = code->count;61
+ emit_cmp_reg_imm32(code, REG_RBX, TAG_STRING);62
+ int is_number = emit_jcc_rel32(code, COND_NE);63
+64
+ // ---- STRING path: [payload-8] = length, [payload..] = bytes ----65
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length66
+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // buf67
+ platform_emit_write_stdout(code);68
+ // newline, as its own tiny write (the string's bytes aren't69
+ // guaranteed to be followed by one in memory)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, whose89
+ // load address ld.so chooses at runtime. libkilnrt.dll (Windows)90
+ // is fixed-base like everything else kiln emits (see91
+ // pe/pe_dll_writer.h), so kiln_code_base() already resolves92
+ // correctly there without RIP-relative addressing -- it now93
+ // knows about DLL compilation specifically (see codegen/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 -> double108
+ 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 zero112
+ 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 negative115
+ emit_pxor_xmm_xmm(code, XMM2);116
+ emit_subsd(code, XMM2, XMM0); // XMM2 = 0.0 - XMM0 = -XMM0117
+ 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) routinely150
+ // lands a hair below the "true" decimal value (0.7999999999999999151
+ // instead of 0.8) due to ordinary double rounding, and truncating152
+ // that directly would print "0.799999" instead of "0.8". Adding half153
+ // a unit-in-the-last-place first is the standard fix; it's too small154
+ // to introduce a spurious digit for values that are already exact155
+ // (see the loop comment below).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-9165
+ emit_cvtsi2sd(code, XMM1, REG_RAX);166
+ emit_subsd(code, XMM0, XMM1); // remainder for the next digit167
+ 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_len185
+ 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 fraction188
+ 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 start192
+ emit_add_reg_imm8(code, REG_RDX, 1);193
+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // length = (newline_pos + 1) - buf_start194
+ platform_emit_write_stdout(code);195
+196
+ emit_add_reg_imm8(code, REG_RSP, 40); // restore stack for the next statement197
+ 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); // payload219
+ emit_pop_reg(code, REG_RBX); // tag220
+ 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 needed234
+ }235
+ emit_mov_reg_imm64(code, REG_RDI, 0);236
+ emit_mov_reg_imm64(code, REG_RAX, 60); // syscall: exit237
+ 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.h9
+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_H2
+#define KILN_CODEGEN_PRINT_INT_H3
+#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 standalone10
+// "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) into15
+// XMM0, converts it to decimal ASCII (handles negative values and a16
+// fractional part, trimming trailing fraction zeros so whole numbers17
+// print with no decimal point -- matching ashvm's dual %lld/%g print18
+// format), and writes it + a trailing newline to stdout via a single raw19
+// write(2) syscall. No libc, no external symbols. Plan B, phase B1: a20
+// real CALL to one shared copy (emitted by print_emit_startup) rather21
+// 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 instruction25
+// in the generated program.26
+void codegen_exit0(CodeBuf *code);27
+28
+#endifkiln/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 (matches12
+// ashvm's own single fread call, not a retry loop) -- reasonable for13
+// 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 payload16
+ emit_pop_reg(code, REG_RBX); // tag (ignored, assumed STRING)17
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length18
+ filepath_emit_nullterm(code); // RAX = null-terminated path buffer19
+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); // fd30
+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); // size34
+35
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // size36
+ string_alloc_emit_prefixed(code); // RAX = block, length prefix already written37
+ emit_store_mem_disp32(code, REG_RBP, base + 16, REG_RAX);38
+39
+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 0); // fd40
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 16);41
+ emit_add_reg_imm8(code, REG_RSI, 8); // buf = block+842
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // size43
+ platform_emit_read_bytes(code);44
+45
+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 0); // fd46
+ 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); // payload50
+ 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.h6
+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_payload9
+// [32]=data_ptr [40]=count [48]=i10
+void codegen_builtin_reduce(void) {11
+ codegen_expression(); // array12
+ expect(TOKEN_COMMA, "expected ',' after array argument");13
+ codegen_expression(); // fn14
+ expect(TOKEN_COMMA, "expected ',' after fn argument");15
+ codegen_expression(); // initial16
+ expect(TOKEN_RPAREN, "expected ')' after arguments");17
+18
+ int32_t base = higher_order_scratch_offset();19
+20
+ emit_pop_reg(code, REG_RAX); // initial payload21
+ emit_pop_reg(code, REG_RBX); // initial tag22
+ 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 payload26
+ emit_pop_reg(code, REG_RBX); // fn tag27
+ 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 object31
+ emit_pop_reg(code, REG_RCX); // array tag (ignored)32
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // count33
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // data_ptr34
+ 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 = 039
+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_ptr47
+ 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 address51
+52
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 16); // fn_tag53
+ emit_push_reg(code, REG_RAX);54
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 24); // fn_payload55
+ 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_payload59
+ 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 payload63
+ emit_push_reg(code, REG_RAX);64
+ codegen_call_closure_value(2);65
+66
+ emit_pop_reg(code, REG_RAX); // result payload67
+ emit_pop_reg(code, REG_RBX); // result tag68
+ emit_store_mem_disp32(code, REG_RBP, base + 0, REG_RBX); // acc = result69
+ 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_tag78
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // acc_payload79
+ 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.h5
+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_len12
+// [32]=cur [40]=total_len/occurrences [48]=out_block [56]=cursor13
+// [64]=find_p [72]=find_last_start [80]=find_result14
+// [88]=replacement_ptr [96]=replacement_len15
+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 it18
+// wouldn't simplify anything). Searches for [search_ptr,search_len]19
+// within s starting at [cur]; writes the match position or -1 to20
+// [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_start26
+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 = cur29
+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(); // s73
+ expect(TOKEN_COMMA, "expected ',' after string argument");74
+ codegen_expression(); // search75
+ expect(TOKEN_COMMA, "expected ',' after search argument");76
+ codegen_expression(); // replacement77
+ expect(TOKEN_RPAREN, "expected ')' after arguments");78
+79
+ emit_pop_reg(code, REG_RSI); // replacement payload80
+ emit_pop_reg(code, REG_RBX); // ignored81
+ 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_len85
+86
+ emit_pop_reg(code, REG_RCX); // search payload87
+ emit_pop_reg(code, REG_RBX); // ignored88
+ 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_len91
+92
+ emit_pop_reg(code, REG_RSI); // s payload93
+ emit_pop_reg(code, REG_RBX); // ignored94
+ 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_len97
+98
+ // ---- search is empty: return s unchanged (matches ashvm) ----99
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len100
+ 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_len104
+ string_alloc_emit_prefixed(code); // RAX = block105
+ 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_ptr109
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // len = s_len110
+ 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 = 0123
+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // occurrences = 0124
+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_len135
+ emit_add_reg_reg(code, REG_RAX, REG_RDX);136
+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = match + search_len137
+ 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_len142
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len143
+ emit_sub_reg_reg(code, REG_RAX, REG_RDX); // delta (signed)144
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 40); // occurrences145
+ emit_imul_reg_reg(code, REG_RAX, REG_RCX); // occurrences * delta146
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 8); // s_len147
+ emit_add_reg_reg(code, REG_RAX, REG_RDX); // total_len148
+149
+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);150
+ string_alloc_emit_prefixed(code); // RAX = block151
+ 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); // cursor155
+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 = 0159
+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 cursor167
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 32); // cur168
+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);169
+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // segment length170
+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 0);171
+ emit_add_reg_reg(code, REG_RBX, REG_RCX); // src = s_ptr + cur172
+ emit_load_mem_disp32(code, REG_RDI, REG_RBP, base + 56); // dest = cursor173
+ 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); // match178
+ 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 advanced181
+182
+ // copy replacement to cursor183
+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 88); // replacement_ptr184
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 96); // replacement_len185
+ 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 advanced192
+193
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 80); // match194
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 24); // search_len195
+ emit_add_reg_reg(code, REG_RAX, REG_RDX);196
+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RAX); // cur = match + search_len197
+ 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 block211
+ 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.h6
+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 and2
+// into libkilnrt.so under --link=shared. Single source of truth shared3
+// between the .so writer (which needs to export these by name) and the4
+// executable's codegen (which needs a GOT slot per import) -- same5
+// "adding one means adding one line here, nowhere else" reasoning as6
+// 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_H2
+#define KILN_RUNTIME_IMPORT_ENUM_H3
+4
+// The RuntimeImport enum itself, factored out of elf/elf_dynamic.h so5
+// pe/pe_dll_writer.h can share it too (Windows --link=shared imports the6
+// exact same 5 functions from libkilnrt.dll that Linux imports from7
+// 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_EXPORT13
+ RUNTIME_IMPORT_COUNT_14
+} RuntimeImport;15
+16
+#endifkiln/src/codegen/runtime_layout.hadded
+14−0@@ -0,0 +1,14 @@1
+#ifndef KILN_RUNTIME_LAYOUT_H2
+#define KILN_RUNTIME_LAYOUT_H3
+4
+// OS-agnostic sizes for kiln's own hand-rolled try/catch handler stack5
+// (see codegen/errors.c's raise routine, parser/try_catch.c) -- the byte6
+// layout is a kiln-internal invention, not tied to ELF or PE, so both7
+// elf/elf_writer.h and pe/pe_writer.h include this rather than each8
+// re-declaring the same numbers.9
+#define MAX_KILN_TRY_DEPTH 1610
+#define KILN_TRY_HANDLER_SIZE 3211
+#define KILN_TRY_HANDLERS_SIZE (MAX_KILN_TRY_DEPTH * KILN_TRY_HANDLER_SIZE)12
+#define KILN_TRY_DEPTH_SIZE 813
+14
+#endifkiln/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_len13
+// [32]=cur [40]=count [48]=out_data_ptr [56]=out_index14
+// [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 as19
+// indexof_builtin.c's search, reimplemented here since it's bounded by a20
+// caller-supplied `cur` rather than always starting at 0). Writes the21
+// match position to [base+80], or -1 if none. Called twice -- once to22
+// count pieces, once to extract them -- so the two passes can never23
+// 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_start29
+30
+ emit_load_mem_disp32(code, REG_RBX, REG_RBP, base + 32); // cur31
+ emit_store_mem_disp32(code, REG_RBP, base + 64, REG_RBX); // p = cur32
+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, stores75
+// 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 the77
+// 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); // len80
+ string_alloc_emit_prefixed(code); // RAX = block81
+ 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); // src85
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, base + 96); // len86
+ bytes_emit_copy(code);87
+88
+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);89
+ emit_add_reg_imm8(code, REG_RAX, 8); // payload90
+91
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, base + 56); // out_index92
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, base + 48); // out_data_ptr93
+ 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 = block114
+ emit_store_mem_disp32(code, REG_RBP, base + 48, REG_RAX); // out_data_ptr115
+}116
+117
+void codegen_builtin_split(void) {118
+ codegen_expression(); // s119
+ expect(TOKEN_COMMA, "expected ',' after string argument");120
+ codegen_expression(); // delim121
+ expect(TOKEN_RPAREN, "expected ')' after arguments");122
+123
+ emit_pop_reg(code, REG_RSI); // delim payload124
+ emit_pop_reg(code, REG_RBX); // ignored125
+ emit_pop_reg(code, REG_RCX); // s payload126
+ emit_pop_reg(code, REG_RBX); // ignored127
+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_len132
+ 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_len135
+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_len142
+ 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 address155
+ emit_load_byte_reg(code, REG_RBX, REG_RSI);156
+ emit_store_mem_disp32(code, REG_RBP, base + 112, REG_RBX); // stash char byte157
+158
+ emit_mov_reg_imm64(code, REG_RDX, 1);159
+ string_alloc_emit_prefixed(code); // RAX = new 1-byte block160
+ emit_add_reg_imm8(code, REG_RAX, 8); // payload161
+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); // count182
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);183
+ heap_emit_alloc(code); // RAX = array object184
+ 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 = 0198
+ emit_store_mem_disp32(code, REG_RBP, base + 40, REG_RAX); // count = 0199
+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_len212
+ 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 piece218
+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 = 0223
+ emit_store_mem_disp32(code, REG_RBP, base + 56, REG_RAX); // out_index = 0224
+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); // cur232
+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);233
+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // piece_len234
+ 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_start237
+ 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); // match241
+ 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_len244
+ 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); // cur248
+ emit_load_mem_disp32(code, REG_RAX, REG_RBP, base + 8); // s_len249
+ emit_mov_reg_reg(code, REG_RDX, REG_RAX);250
+ emit_sub_reg_reg(code, REG_RDX, REG_RCX); // trailing piece_len251
+ 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); // count258
+ emit_mov_reg_imm64(code, REG_RDI, ARRAY_OBJECT_SIZE);259
+ heap_emit_alloc(code); // RAX = array object260
+ 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.h6
+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 backward19
+// rsp+32 '.' (only if there's a fraction)20
+// [rsp+33, rsp+39) up to 6 fraction digits, built forward21
+void codegen_builtin_str(void) {22
+ emit_pop_reg(code, REG_RAX); // payload23
+ 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 (see64
+ // 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_len71
+ 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 (no86
+ // 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 length100
+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 RSI105
+106
+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RDX); // length prefix107
+ 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+8110
+ emit_mov_reg_reg(code, REG_RBX, REG_RCX); // src = digit buffer start111
+ bytes_emit_copy(code); // clobbers RAX,RBX,RCX,RDX,RDI; leaves RSI = block addr112
+113
+ emit_add_reg_imm8(code, REG_RSP, 40); // release the digit scratch114
+115
+ emit_mov_reg_reg(code, REG_RAX, REG_RSI);116
+ emit_add_reg_imm8(code, REG_RAX, 8); // payload = block+8117
+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.h5
+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_back8
+// from all 13 call sites -- same pattern as codegen/heap.c's alloc9
+// routine and codegen/bytes.c's copy routine (which this one itself10
+// calls, via heap_emit_alloc -- routines calling other routines nest11
+// exactly like any other CALL/RET, no special handling needed). Note this12
+// stays a LOCAL call even when compiling libkilnrt.so/.dll: the "compile13
+// the runtime" driver sets kiln_set_compiling_so(1), which14
+// heap_emit_alloc's own is_shared() check also looks at, so it correctly15
+// resolves to a same-library relative call rather than calling back out16
+// 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) survives25
+ 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.h7
+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_H2
+#define KILN_STRING_ALLOC_H3
+#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) and7
+// 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 the11
+// 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: input16
+// RDX=content length, output RAX=block address with the length prefix17
+// already written at [RAX+0] (the payload is RAX+8, same layout18
+// codegen/strings.c's literals use). Clobbers RSI, RDI (via19
+// codegen/heap.c's heap_emit_alloc) -- RDX itself survives untouched.20
+// Shared by every milestone-10 string builtin that produces a new string21
+// (upper/lower/trim/substring/join/replace) so the "+8 for the length22
+// prefix" arithmetic exists in exactly one place. Plan B, phase B1: a23
+// real CALL to one shared copy rather than re-emitting inline at each of24
+// its ~13 call sites.25
+void string_alloc_emit_prefixed(CodeBuf *code);26
+27
+#endifkiln/src/codegen/string_builtins.hadded
+19−0@@ -0,0 +1,19 @@1
+#ifndef KILN_STRING_BUILTINS_H2
+#define KILN_STRING_BUILTINS_H3
+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 '(', same12
+// 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
+#endifkiln/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 instructions29
+ 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, since62
+// [rsp+disp] itself needs a SIB byte this project's encoders don't63
+// support -- same technique codegen/print_int.c uses):64
+// [0]=a_payload [8]=b_payload [16]=length_a [24]=length_b [32]=new_block65
+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_b75
+ 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 prefix80
+81
+ emit_push_reg(code, REG_RSI); // heap_emit_alloc clobbers RSI -- save/restore around it82
+ heap_emit_alloc(code); // RAX = new block83
+ 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 length90
+91
+ // copy A right after the length prefix92
+ 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_payload95
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 16); // count = length_a96
+ bytes_emit_copy(code);97
+98
+ // copy B right after A99
+ 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_a102
+ emit_add_reg_reg(code, REG_RDI, REG_RCX);103
+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 8); // src = b_payload104
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, 24); // count = length_b105
+ 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 address109
+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_a119
+ emit_load_mem_disp32(code, REG_RDX, REG_RCX, -8); // length_b120
+ emit_cmp_reg_reg(code, REG_RBX, REG_RDX);121
+ int to_false = emit_jcc_rel32(code, COND_NE); // different lengths -> can't be equal122
+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 -> equal126
+ 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 payload146
+ emit_pop_reg(code, REG_RBX); // index tag (ignored, assumed NUMBER)147
+ emit_pop_reg(code, REG_RSI); // string payload148
+ 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); // index152
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length153
+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 address165
+ 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 written169
+170
+ emit_add_reg_imm8(code, REG_RAX, 8); // payload171
+ 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.h6
+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_H2
+#define KILN_STRINGS_H3
+4
+// primary()'s TOKEN_STRING handling: embeds the literal's escaped bytes5
+// into the code buffer (jumped over, so normal control flow never tries6
+// to execute them as instructions -- same technique fn declarations use7
+// for their bodies), pushes (tag=STRING, payload=<fixed absolute8
+// address>). Kiln generates a fixed, non-PIE executable, so a literal's9
+// 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 bytes13
+// 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_payload19
+// in RCX (a_tag/b_tag, if the caller still needs them, must be read and20
+// 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 combined24
+// 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) or28
+// byte-different (invert=1, i.e. `!=`).29
+void codegen_string_compare(int invert);30
+31
+// Assumes index already pushed (top) and string already pushed (below32
+// it), both (tag, payload) pairs -- postfix()'s `s[i]` read, the33
+// TAG_STRING case (previously missing entirely -- see missing.md #1:34
+// falling through to the array path here would have read the string's35
+// 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 are37
+// immutable, `s[i] = x` is a parse-time-caught statement shape kiln's38
+// grammar doesn't even offer a codegen path for (see parser.c).39
+void codegen_string_index_read(void);40
+41
+#endifkiln/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=end11
+// (matching ashvm's own clamping exactly, not erroring on out-of-range).12
+void codegen_builtin_substring(void) {13
+ codegen_expression(); // str14
+ expect(TOKEN_COMMA, "expected ',' after string argument");15
+ codegen_expression(); // start16
+ expect(TOKEN_COMMA, "expected ',' after start argument");17
+ codegen_expression(); // end18
+ expect(TOKEN_RPAREN, "expected ')' after arguments");19
+20
+ emit_pop_reg(code, REG_RAX); // end payload21
+ 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 payload26
+ 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 payload31
+ emit_pop_reg(code, REG_RBX); // str tag (ignored)32
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // slen33
+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 - start51
+ emit_add_reg_reg(code, REG_RSI, REG_RCX); // src = str payload + start52
+ 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.h6
+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 past9
+// trailing whitespace (RDI starts as the exclusive end pointer), each via10
+// a small 4-way character check ( ' ' '\t' '\n' '\r', matching ashvm's11
+// own set) inlined rather than a shared byte-classifier helper -- it's12
+// 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 payload15
+ emit_pop_reg(code, REG_RBX); // tag (ignored)16
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, -8); // length17
+ emit_mov_reg_reg(code, REG_RSI, REG_RAX); // start cursor18
+ 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); // length65
+ 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); // dest72
+ 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); // payload76
+ 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.h5
+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_H2
+#define KILN_VALUE_H3
+4
+// Milestone 5: an ash value is no longer always a bare double -- it can5
+// also be a string. Every value is now a (tag, payload) pair, 16 bytes6
+// total, moved around generically via GP registers (raw bit-copying7
+// doesn't care what the bits mean -- see codegen/expr.c's header8
+// comment). XMM registers only ever hold the payload transiently, at the9
+// point of actually doing floating-point arithmetic on a NUMBER-tagged10
+// 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 at14
+// compile time) pointing at a length-prefixed byte block: 8 bytes of15
+// length, followed by that many raw bytes. String literals are embedded16
+// directly in the code buffer (see codegen/strings.c); concatenation17
+// results are heap-allocated (see codegen/heap.c) since their size is18
+// only known at runtime.19
+#define TAG_NUMBER 020
+#define TAG_STRING 121
+#define TAG_ARRAY 222
+#define TAG_MAP 323
+#define TAG_FUNCTION 424
+25
+// A nil value's payload is always exactly 0 -- deliberately, so it26
+// coincides with +0.0's bit pattern. That's what lets nil ride the27
+// EXISTING truthy-test and numeric-comparison codegen (both just28
+// ucomisd the payload against 0.0) without needing its own special case29
+// anywhere except print (see codegen/print_int.c's dispatch) and the30
+// tag-equality short-circuit in comparisons (already tag-aware since31
+// M5's `==`/`!=`, see codegen/expr_bool.c -- nil==nil compares equal32
+// tags then falls into the numeric path, 0.0==0.0; nil compared against33
+// any other tag is caught by the tags-differ branch before ever34
+// 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's36
+// "boolean type" note is intentionally only half-addressed this way:37
+// the whole codebase already treats "truthy" as "NUMBER != 0", so a38
+// distinct boolean tag would mean touching that logic everywhere39
+// instead of adding one clean new case).40
+#define TAG_NIL 541
+42
+// An array's payload is the address of a small, fixed-size, STABLE43
+// "array object" (see codegen/strings.h's comment on why absolute44
+// 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 in49
+// [0,capacity)) is what actually moves on growth -- a fresh, bigger50
+// block gets allocated and the old one's contents copied over (the bump51
+// allocator can't grow in place), but only data_ptr's VALUE changes.52
+// The array object's own address -- what every variable/slot holding53
+// this array actually stores -- never moves. This matters: without it,54
+// `push(arr, x)` growing the array would silently leave every existing55
+// reference to `arr` pointing at stale, abandoned memory. (ashvm gets56
+// 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 2460
+61
+// A map uses the exact same stable-object-plus-resizable-data shape as62
+// an array (see above -- same reason: growth must not move the address63
+// every existing reference to the map already holds):64
+// [0] capacity [8] count [16] entries_ptr65
+// Each entry is 32 bytes: [0]=key tag (always TAG_STRING) [8]=key66
+// 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 documented68
+// simplification (O(n) instead of O(1)) for implementation reach, not a69
+// semantic difference a program could observe other than speed.70
+#define MAP_OBJECT_SIZE 2471
+#define MAP_ENTRY_SIZE 3272
+73
+// A function/closure's payload is the address of a small, fixed (NEVER74
+// resized -- closures are immutable once created, no growth concern like75
+// 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 is78
+// a fixed non-PIE executable, see elf/elf_writer.h, so this is79
+// known and stable)80
+// [8] capture_count (int64)81
+// [16+16*i] capture i's tag [16+16*i+8] capture i's payload82
+// A plain named function used as a value (not immediately called) gets83
+// capture_count=0 -- the same shape as any closure with zero captures, so84
+// codegen/closures.c's indirect-call codegen never special-cases "085
+// captures", it just runs a 0-iteration loop.86
+//87
+// Scope limit (documented, not silently pretended away -- same spirit as88
+// the map's linear-scan note above): captures are snapshotted BY VALUE at89
+// closure-creation time, not by reference to a shared mutable cell. This90
+// supports closures that read outer variables (partial application, e.g.91
+// `fn make_adder(n) { return fn(x) { return x + n; }; }`) but NOT closures92
+// that mutate a captured variable and expect that mutation visible on the93
+// next call (the classic mutable "counter" idiom) -- that needs a real94
+// upvalue mechanism (boxed/heap-indirect variables), which conflicts with95
+// kiln's single-pass, fixed-stack-slot variable model and is out of scope96
+// for this milestone.97
+#define FUNCTION_OBJECT_HEADER_SIZE 1698
+99
+#endifkiln/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 kiln7
+// needs a register-immediate AND, so this stays local instead of joining8
+// 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 = AND13
+ emit_byte(code, (uint8_t)imm);14
+}15
+16
+// R8/R9 need REX.B to extend the 3-bit register field in both the17
+// "+reg" immediate-move opcode and a ModRM rm field -- kiln's Reg enum18
+// (emit.h) only covers RAX-RDI (0-7), so these stay as tiny local helpers19
+// parameterized by the raw register number (8 or 9) rather than growing20
+// 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.WB23
+ 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.WB29
+ emit_byte(code, 0x89); // MOV r/m64, r6430
+ 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 byte34
+// (index=100 "none", base=100=RSP, scale=00), unlike RBP which35
+// emit_mem.c's modrm_mem_disp32 already handles directly. Nothing else in36
+// kiln addresses [rsp+disp] (existing code routes through RBP-relative37
+// scratch instead), so this is local to the one place that genuinely38
+// needs it: Windows stack arguments (5th+), which the ABI requires to39
+// 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 pointer86
+ 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 (real94
+// Windows API, needs the full shadow-space dance above), libkilnrt.dll's95
+// exports are kiln's OWN code using kiln's OWN simple per-routine96
+// register conventions (RAX/RBX/RDI/RDX depending on which routine, see97
+// codegen/heap.c etc.) -- exactly like the Linux GOT call98
+// (elf/elf_dynamic_call.c's emit_call_abs32), no register is safe to99
+// clobber loading the IAT address first. Can't reuse emit_call_abs32's100
+// technique directly, though: kiln's PE image base (0x140000000) is101
+// already past the 32-bit absolute-displacement addressing mode's102
+// range, but RIP-relative reaches it fine since the IAT slot and the103
+// 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.h6
+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_H2
+#define KILN_WIN_CALL_H3
+#include "codegen/emit.h"4
+#include "pe/pe_writer.h"5
+6
+// Windows x64 calling convention -- the caller-side sequence for calling7
+// an imported kernel32.dll function. New territory for this codebase:8
+// unlike kiln's own internal `fn` convention (stack-push based, see9
+// parser/parser_control.c), this has to match a REAL external ABI, with10
+// rules kiln has never had to satisfy before:11
+// - first four integer/pointer args in RCX, RDX, R8, R912
+// - a mandatory 32-byte "shadow space" the CALLEE is allowed to spill13
+// its register args into, reserved by the CALLER even if the callee14
+// never uses it15
+// - RSP must be 16-byte aligned at the `call` instruction16
+// - 5th+ args go on the stack, above the shadow space17
+//18
+// RSP alignment is the sharp edge: kiln's codegen never tracks RSP parity19
+// (arbitrary push/pop sequences leave it unpredictable at any given20
+// point, which was fine when the only "call-like" thing was a raw Linux21
+// `syscall` -- syscalls have no alignment requirement at all). So rather22
+// than trying to prove alignment holds at every call site,23
+// win_call_begin ALIGNS DYNAMICALLY at runtime (`and rsp, -16`) and saves24
+// the pre-align RSP to a fixed scratch slot; win_call_end restores that25
+// exact saved value, so the surrounding codegen's own RSP bookkeeping is26
+// completely undisturbed regardless of what it was before the call.27
+//28
+// Argument marshalling note: if a source value already lives in one of29
+// RCX/RDX/R8/R9 from earlier in this same sequence, load higher-indexed30
+// args before lower ones (or route through a scratch register like RAX/31
+// RSI first) -- win_call_arg_* does not detect or resolve register32
+// 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/R935
+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 a39
+// 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 than46
+// reusing win_call_import.47
+void win_call_runtime_import(CodeBuf *code, RuntimeImport which);48
+49
+#endifkiln/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't6
+// accept NULL there for synchronous handles), so it points at a7
+// throwaway RBP-relative scratch DWORD -- computed via a low register8
+// 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); // handle12
+ win_call_arg_reg(code, 1, REG_RSI); // buf13
+ win_call_arg_reg(code, 2, REG_RDI); // len14
+ 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 = NULL18
+ 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.h6
+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_H2
+#define KILN_WIN_FILE_CALL_H3
+#include "pe/pe_writer.h"4
+5
+// Shared shape between ReadFile and WriteFile's argument lists (both are6
+// (handle, buffer, length, &ignored_out, NULL)) -- used by both7
+// codegen/platform_console.c (stdin/stdout/stderr) and8
+// codegen/platform_file.c (real files), so it lives here instead of9
+// being duplicated in each.10
+//11
+// In: RAX=handle, RSI=buf, RDI=len. `which` must be PE_IMPORT_READ_FILE12
+// or PE_IMPORT_WRITE_FILE.13
+void win_file_call(CodeBuf *code, PeImport which);14
+15
+#endifkiln/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(); // path27
+ expect(TOKEN_COMMA, "expected ',' after path argument");28
+ codegen_expression(); // content29
+ expect(TOKEN_RPAREN, "expected ')' after arguments");30
+31
+ emit_pop_reg(code, REG_RSI); // content payload32
+ emit_pop_reg(code, REG_RBX); // ignored33
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // content length34
+35
+ int32_t base = higher_order_scratch_offset();36
+ emit_store_mem_disp32(code, REG_RBP, base + 24, REG_RSI); // content_ptr37
+ emit_store_mem_disp32(code, REG_RBP, base + 32, REG_RDX); // content_len38
+39
+ emit_pop_reg(code, REG_RSI); // path payload40
+ emit_pop_reg(code, REG_RBX); // ignored41
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // path length42
+ filepath_emit_nullterm(code); // RAX = null-terminated path buffer43
+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); // fd47
+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); // fd54
+ 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 payload64
+ emit_pop_reg(code, REG_RBX); // ignored65
+ emit_load_mem_disp32(code, REG_RDX, REG_RSI, -8); // length66
+ filepath_emit_nullterm(code); // RAX = null-terminated path buffer67
+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.h6
+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 200809L2
+#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 from39
+// 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.h3
+src/diagnostics/diagnostics.h:kiln/src/diagnostics/diagnostics.hadded
+26−0@@ -0,0 +1,26 @@1
+#ifndef ASH_DIAGNOSTICS_H2
+#define ASH_DIAGNOSTICS_H3
+4
+// Rust-style colorized error reporting, shared by every stage (compile-time5
+// 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 to10
+// 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 can20
+// point diagnostics at an imported file's own buffer while recursively21
+// parsing it, then restore the importing file's pair afterward -- mirrors22
+// lexer.h's LexerState save/restore.23
+DiagnosticsSource diagnostics_save_source(void);24
+void diagnostics_restore_source(DiagnosticsSource saved);25
+26
+#endifkiln/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 the8
+// kernel hands off to the REAL system dynamic linker, PT_DYNAMIC with9
+// DT_NEEDED=libkilnrt.so, a SysV-hash-format .hash + .dynsym + .dynstr10
+// (no section headers, matching this project's "tiny ELF" philosophy --11
+// ld.so only ever reads PT_DYNAMIC and what it points at, never section12
+// headers), and a GOT filled in by ld.so's own relocation processing13
+// before this program's own code ever runs (eager/non-lazy binding via14
+// R_X86_64_GLOB_DAT relocations -- simpler and just as correct as a lazy15
+// PLT stub sequence for kiln's purposes, and far less to get subtly16
+// wrong). See elf_dynamic.h for why .dynstr specifically is written17
+// 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_EXPORT23
+};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 real43
+// DT_HASH-format .hash table in the wild uses -- ld.so implements this44
+// 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 its58
+ // 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 061
+ 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_EXEC79
+ put16(&f, 62); // e_machine = EM_X86_6480
+ 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, DYNAMIC88
+ put16(&f, 0); put16(&f, 0); put16(&f, 0);89
+90
+ // ---- Elf64_Phdr[0]: PT_LOAD, whole file, RWX (matches this91
+ // 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_R99
+ 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_W105
+ 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 function125
+ // (st_shndx=SHN_UNDEF=0 -- "defined elsewhere", exactly what tells126
+ // ld.so to go resolve it against a NEEDED library rather than treat127
+ // it as locally defined) ----128
+ put32(&f, 0); put8(&f, 0); put8(&f, 0); put16(&f, 0); put64(&f, 0); put64(&f, 0); // null symbol129
+ for (int i = 0; i < RUNTIME_IMPORT_COUNT_; i++) {130
+ put32(&f, dynstr_off[i]); // st_name131
+ put8(&f, (1 << 4) | 2); // st_info: STB_GLOBAL | STT_FUNC132
+ put8(&f, 0); // st_other133
+ put16(&f, 0); // st_shndx = SHN_UNDEF134
+ put64(&f, 0); // st_value135
+ put64(&f, 0); // st_size136
+ }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_RPATH145
+ DT(4, KILN_DYN_HASH_ADDR); // DT_HASH146
+ DT(5, dynstr_addr); // DT_STRTAB (after the code -- depends on its length)147
+ DT(6, KILN_DYN_SYMTAB_ADDR); // DT_SYMTAB148
+ DT(10, dynstr_size); // DT_STRSZ149
+ DT(11, KILN_DYN_SYMENT_SIZE); // DT_SYMENT150
+ DT(7, KILN_DYN_RELA_ADDR); // DT_RELA151
+ DT(8, KILN_DYN_RELA_SIZE); // DT_RELASZ152
+ DT(9, KILN_DYN_RELAENT_SIZE); // DT_RELAENT153
+ DT(0, 0); // DT_NULL154
+ #undef DT155
+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 = 6160
+ put64(&f, KILN_DYN_GOT_ADDR + 8ULL * (uint64_t)i); // r_offset161
+ put64(&f, r_info);162
+ put64(&f, 0); // r_addend163
+ }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 string171
+ 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.h5
+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_H2
+#define KILN_ELF_DYNAMIC_H3
+#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, the8
+// kernel hands off to the system's actual /lib64/ld-linux-x86-64.so.2 (a9
+// PT_INTERP segment says so), which reads PT_DYNAMIC, finds10
+// libkilnrt.so via DT_NEEDED+DT_RPATH, and resolves each imported11
+// runtime function into a GOT slot this file also lays out -- see12
+// elf_dynamic.c's header comment for the full section-by-section layout13
+// and elf_so_writer.h for the library side of the same relationship.14
+//15
+// Design validated against the REAL system dynamic linker with a16
+// hand-written prototype before any of this was ported into kiln itself17
+// (two hand-built ELF files, one ET_DYN "library" and one ET_EXEC18
+// "executable" with PT_INTERP/PT_DYNAMIC/GOT/RELA, loaded and resolved19
+// correctly by /lib64/ld-linux-x86-64.so.2) -- this header encodes that20
+// 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_NSYMS28
+29
+// Section order: everything whose address codegen (or another fixed-size30
+// section) needs as a compile-time constant comes first, in FIXED-SIZE31
+// pieces only (sizes depending on RUNTIME_IMPORT_COUNT_, never on string32
+// lengths); .dynstr -- the one variable-length piece -- goes after the33
+// code instead, exactly like pe_writer.h's hint/name tail, computed at34
+// 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 NUL38
+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 24u43
+#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 this48
+// program's own entry point ever runs. Fixed size/offset regardless of49
+// symbol NAME lengths, exactly like pe_writer.h's IAT -- codegen can50
+// 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 terminator58
+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-known68
+// immediate (kiln's executables are always fixed-base, same reasoning as69
+// 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 at73
+// `path`, expecting libkilnrt.so to be installed at KILN_RPATH by the74
+// 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
+#endifkiln/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.h5
+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_H2
+#define KILN_ELF_DYNAMIC_CALL_H3
+#include "elf/elf_dynamic.h"4
+5
+// Calls `which` through its GOT slot (filled in by ld.so before this6
+// program's own code ever runs) -- a single `call [addr]` touching no7
+// register, so it's safe to insert in front of ANY of the runtime8
+// routines regardless of which registers that specific routine's own9
+// argument convention uses (heap_alloc: RDI/RAX, print_top: RAX/RBX,10
+// bytes_copy: RDI/RBX/RDX, ...). Used for RAISE too even though the11
+// static-link build reaches it via a JMP, not a CALL (it never returns):12
+// the extra return address CALL pushes is simply abandoned, unreachable13
+// dead stack space either way, since nothing after a raise site ever14
+// falls through -- harmless, and not worth a second code path here.15
+void elf_dynamic_call(CodeBuf *code, RuntimeImport which);16
+17
+#endifkiln/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_EXPORT18
+};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 own46
+ // fresh CodeBuf. Link mode STAYS shared (main.c sets it before47
+ // calling this) so codegen/layout.c resolves globals addresses using48
+ // the shared-mode executable layout -- but kiln_set_compiling_so(1)49
+ // makes each routine's OWN call sites (e.g. string_alloc_prefixed50
+ // calling heap_emit_alloc) resolve as ordinary same-buffer relative51
+ // calls instead of GOT calls back out to this very library. These52
+ // are two different questions (see target.h's comment) that a first53
+ // pass at this conflated into one flag, and testing caught it: doing54
+ // 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/addresses76
+ // relative to 0 (ET_DYN: ld.so adds its chosen load bias to77
+ // 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 string83
+ 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,NULL95
+ 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_DYN106
+ 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, DYNAMIC115
+ put16(&f, 0); put16(&f, 0); put16(&f, 0);116
+117
+ put32(&f, 1); put32(&f, 7); // PT_LOAD, RWX118
+ 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_W122
+ 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
+ // .hash126
+ 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 offset140
+ // within this image, st_shndx=1 -- a nonzero placeholder, since this141
+ // 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_FUNC146
+ 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
+ // .dynstr153
+ 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
+ // .dynamic158
+ #define DT(tag, val) do { put64(&f, (uint64_t)(tag)); put64(&f, (uint64_t)(val)); } while (0)159
+ DT(14, soname_off); // DT_SONAME160
+ DT(4, hash_off); // DT_HASH161
+ DT(5, dynstr_file_off); // DT_STRTAB162
+ DT(6, dynsym_off); // DT_SYMTAB163
+ DT(10, dynstr_size); // DT_STRSZ164
+ DT(11, 24); // DT_SYMENT165
+ DT(0, 0); // DT_NULL166
+ #undef DT167
+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.h7
+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_H2
+#define KILN_ELF_SO_WRITER_H3
+4
+// Compiles the runtime (heap/bytes/string_alloc/print/errors-raise) into5
+// a fresh, position-independent CodeBuf and writes it as a real ET_DYN6
+// ELF64 shared object at `path` -- one exported symbol per7
+// codegen/runtime_exports.def entry, loadable by the system's real8
+// dynamic linker (validated structure, see elf/elf_dynamic.h's header9
+// comment). Returns 0 on success.10
+int elf_write_shared_runtime(const char *path);11
+12
+#endifkiln/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); // ELFCLASS6428
+ put8(&file, 1); // ELFDATA2LSB (little-endian)29
+ put8(&file, 1); // EV_CURRENT30
+ put8(&file, 0); // ELFOSABI_SYSV31
+ 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_EXEC34
+ put16(&file, 62); // e_machine = EM_X86_6435
+ put32(&file, 1); // e_version = EV_CURRENT36
+ put64(&file, entry); // e_entry37
+ 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_flags40
+ put16(&file, KILN_EHDR_SIZE); // e_ehsize41
+ put16(&file, KILN_PHDR_SIZE); // e_phentsize42
+ put16(&file, 1); // e_phnum43
+ put16(&file, 0); // e_shentsize44
+ put16(&file, 0); // e_shnum45
+ put16(&file, 0); // e_shstrndx46
+47
+ // ---- Elf64_Phdr: one PT_LOAD segment covering the whole file ----48
+ put32(&file, 1); // p_type = PT_LOAD49
+ // p_flags = PF_R | PF_W | PF_X. Milestone 1-4 only needed R|X (code50
+ // never wrote into its own segment); milestone 5's heap bump pointer51
+ // (see elf_writer.h / codegen/heap.c) is a global living inside this52
+ // same segment, so it needs to be writable too. A real compiler would53
+ // split .text (R|X) from .data (R|W) into separate segments -- kiln54
+ // doesn't do that yet, so the whole thing is RWX for now.55
+ put32(&file, 7);56
+ put64(&file, 0); // p_offset57
+ put64(&file, KILN_LOAD_BASE); // p_vaddr58
+ put64(&file, KILN_LOAD_BASE); // p_paddr59
+ put64(&file, total_size); // p_filesz60
+ put64(&file, total_size); // p_memsz61
+ put64(&file, 0x1000); // p_align62
+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.h3
+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_H2
+#define KILN_ELF_WRITER_H3
+#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 absolute8
+// addresses are known at compile time. Milestone 5 needs one such fixed9
+// address for the heap bump pointer (a "global" -- there's no per-call10
+// frame that's reachable from every function, since each call gets its11
+// own fresh RBP), reserved as a small region between the program headers12
+// and the actual code.13
+#define KILN_LOAD_BASE 0x400000ULL // page-aligned, traditional non-PIE x86-64 base14
+#define KILN_EHDR_SIZE 6415
+#define KILN_PHDR_SIZE 5616
+17
+// Milestone 9: the globals region also holds kiln's own hand-rolled18
+// setjmp/longjmp equivalent for try/catch (see codegen/errors.c's raise19
+// routine and parser/try_catch.c) -- a fixed-depth stack of "handler"20
+// records, since this is a freestanding binary with no libc setjmp to21
+// call. Each handler is {saved_rsp, saved_rbp, target_addr,22
+// error_slot_rbp_offset}, 4 x int64. Bounded the same way every other23
+// table in this project is (MAX_KILN_VARS, MAX_KILN_FUNCS, ...) --24
+// exceeding it is a hard (uncatchable) fatal error, not undefined25
+// behavior, since overflowing past it would silently corrupt the26
+// adjacent code segment.27
+// Milestone: argv(). The kernel hands a freshly-started process its28
+// stack laid out as [argc, argv[0], argv[1], ..., NULL, envp...], with29
+// 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, reachable32
+// from any frame later on, since a called function's own RBP is no33
+// longer the original one by the time argv() might be called from34
+// inside it (see codegen/argv_builtin.c).35
+#define KILN_ARGV_SIZE 836
+37
+#define KILN_GLOBALS_SIZE (8 + KILN_TRY_DEPTH_SIZE + KILN_TRY_HANDLERS_SIZE + KILN_ARGV_SIZE) // heap_ptr, try_depth, handler stack, argv38
+#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-executable45
+// Linux x86-64 ELF64 binary at `path` and marks it executable (chmod46
+// +x). No section headers at all -- the kernel loader only reads the ELF47
+// 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
+#endifkiln/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-static7
+// before) -- shared with lexer_import.c now, and a plain `current` would8
+// 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 EOF48
+ // rather than producing a lex error -- the same49
+ // "good enough, not worth a signature change to50
+ // skip_whitespace for" tradeoff line comments51
+ // 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 the87
+// host, not codegen) does the actual conversion and natively understands88
+// hex ("0x1A") and scientific notation ("1e10", "1.5e-3") already; the89
+// lexer just has to not stop early and hand back a truncated token.90
+// Underscore digit separators (1_000_000) are the one thing strtod91
+// itself doesn't understand -- primary() strips those out of the token92
+// 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 backslash120
+ advance(); // skip the escaped character itself, so a \" doesn't end the string early121
+ 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.h2
+src/lexer/lexer.h:kiln/src/lexer/lexer.hadded
+102−0@@ -0,0 +1,102 @@1
+#ifndef LEXER_H2
+#define LEXER_H3
+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_ERROR68
+} 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 a81
+// different source buffer for an imported file, then resume exactly where82
+// the importing file's tokenizer left off (mirrors vars_save/restore's83
+// 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_LESS94
+// that opened an `@import <...>` path. Scans raw characters (not through95
+// the normal keyword/operator rules -- a path like "./foo.ash" contains96
+// '.' and '/' which aren't otherwise valid outside this context) up to97
+// and INCLUDING the closing '>', and returns a token whose start/length98
+// cover just the path text (excluding both brackets). Errors (via the99
+// TOKEN_ERROR convention) on a newline or EOF before '>'.100
+Token lexer_scan_import_path(void);101
+102
+#endifkiln/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 immediately18
+// after the '<' the parser just confirmed (via current.type==TOKEN_LESS)19
+// WITHOUT calling advance_token() on it -- so `current` here still points20
+// at the first path character, untouched by normal tokenization. Scans21
+// raw bytes up to '>', consuming the '>' too, so the parser's next22
+// 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_H2
+#define KILN_LEXER_IMPORT_H3
+#include "lexer/lexer.h"4
+5
+// Cross-file link between lexer.c (the tokenizer's cursor state and its6
+// low-level scan primitives) and lexer_import.c (state save/restore plus7
+// import-path scanning, split out here to keep lexer.c under the8
+// project's ~200-line guideline) -- not part of the public API. The9
+// public entry points the two files implement together10
+// (lexer_save_state/lexer_restore_state/lexer_scan_import_path) stay11
+// declared in lexer.h exactly as before, so callers outside the lexer12
+// directory don't need to change anything. Mirrors parser.c's split13
+// 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 already17
+// declares its own global `Token current`, and since these now need18
+// external linkage to be shared across the two .c files, a second plain19
+// `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
+#endifkiln/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() exits39
+}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; // unreachable47
+}48
+49
+int main(int argc, char *argv[]) {50
+ // --emit-runtime -o <path> [--target=...]: writes libkilnrt.so/.dll51
+ // (Plan B) and exits, no .ash input at all -- an entirely separate52
+ // 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, since57
+ // that's whose globals this code reads/writes at runtime --58
+ // elf_write_shared_runtime/pe_write_shared_runtime additionally59
+ // set kiln_set_compiling_so to keep the runtime's OWN internal60
+ // calls (e.g. string_alloc calling heap_alloc) local instead of61
+ // calls back into itself.62
+ kiln_set_link_mode(KILN_LINK_SHARED);63
+ return kiln_get_target() == KILN_TARGET_WINDOWS64
+ ? 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 as81
+ // executable by the shell/loader -- match what every other compiler82
+ // 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_SHARED105
+ ? 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_SHARED109
+ ? 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.h8
+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_H2
+#define KILN_CONSTANTS_H3
+4
+#define MAX_KILN_CONSTS 325
+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's10
+ // 64 since it also has to fit a namespace prefix on top11
+ // of a normal identifier12
+ KilnConstantKind kind;13
+ double num_value; // valid when kind == KILN_CONST_NUMBER14
+ char *str_bytes; // valid when kind == KILN_CONST_STRING -- raw15
+ // (still-escaped) bytes from the source token,16
+ // NOT yet processed; codegen_constant_value17
+ // re-runs escape processing at each use site via18
+ // codegen_string_literal_bytes, same as a normal19
+ // string literal would20
+ int str_len;21
+} KilnConstant;22
+23
+// Declares NAME (already namespaced by the caller, e.g. "math.PI") as a24
+// 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 pointer29
+// beyond storing it; the caller (parser.c's let_statement) must pass30
+// bytes from a buffer that outlives the whole compile (the imported31
+// file's source buffer, which imports.c already never frees -- see32
+// 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 have38
+// emitted inline (a NUMBER push or a STRING alloc+push) -- used at every39
+// `ns.CONST` use site.40
+void codegen_constant_value(KilnConstant *k);41
+42
+#endifkiln/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 each18
+// element in turn via the same var-slot mechanism `let` uses, so it's a19
+// perfectly ordinary variable inside the body (readable, reassignable,20
+// though reassigning it doesn't affect iteration -- the next iteration21
+// overwrites it again from the array, matching a plain for-each's usual22
+// semantics). data_ptr/count/i live in this nesting level's fixed23
+// RBP-relative slot (see vars.h's for_level_offset) so they survive24
+// whatever the body's own codegen does, including calls.25
+void for_statement(void) {26
+ VarBlockScope saved_scope = vars_scope_begin();27
+ advance_token(); // consume 'for'28
+ expect(TOKEN_LPAREN, "expected '(' after 'for'");29
+ expect(TOKEN_IDENTIFIER, "expected loop variable name");30
+ const char *var_name = previous.start;31
+ int var_len = previous.length;32
+ expect(TOKEN_IN, "expected 'in' after loop variable");33
+ codegen_expression(); // pushes the array (tag, payload)34
+ expect(TOKEN_RPAREN, "expected ')' after loop expression");35
+36
+ if (for_nesting_depth >= MAX_KILN_FOR_DEPTH) parse_error("too many nested for loops");37
+ int32_t level = for_level_offset(for_nesting_depth);38
+ for_nesting_depth++;39
+40
+ emit_pop_reg(code, REG_RAX); // array payload (object address)41
+ emit_pop_reg(code, REG_RBX); // array tag (ignored, assumed ARRAY)42
+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 8); // count43
+ emit_load_mem_disp32(code, REG_RDX, REG_RAX, 16); // data_ptr44
+ emit_store_mem_disp32(code, REG_RBP, level + 0, REG_RDX);45
+ emit_store_mem_disp32(code, REG_RBP, level + 8, REG_RCX);46
+ emit_mov_reg_imm64(code, REG_RAX, 0);47
+ emit_store_mem_disp32(code, REG_RBP, level + 16, REG_RAX); // i = 048
+49
+ int slot = resolve_var_in_current_scope(var_name, var_len);50
+ if (slot == -1) slot = declare_var(var_name, var_len);51
+52
+ int loop_start = code->count;53
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, level + 16); // i54
+ emit_load_mem_disp32(code, REG_RDX, REG_RBP, level + 8); // count55
+ emit_cmp_reg_reg(code, REG_RCX, REG_RDX);56
+ int loop_exit = emit_jcc_rel32(code, COND_GE);57
+58
+ emit_load_mem_disp32(code, REG_RSI, REG_RBP, level + 0); // data_ptr59
+ emit_mov_reg_reg(code, REG_RAX, REG_RCX);60
+ emit_mov_reg_imm64(code, REG_RDX, 16);61
+ emit_imul_reg_reg(code, REG_RAX, REG_RDX);62
+ emit_add_reg_reg(code, REG_RSI, REG_RAX); // element address63
+64
+ emit_load_mem_disp32(code, REG_RBX, REG_RSI, 0); // element tag65
+ emit_load_mem_disp32(code, REG_RAX, REG_RSI, 8); // element payload66
+ emit_store_mem_disp32(code, REG_RBP, var_slot_tag_offset(slot), REG_RBX);67
+ emit_store_mem_disp32(code, REG_RBP, var_slot_payload_offset(slot), REG_RAX);68
+69
+ loop_push();70
+ block();71
+ loop_patch_continues(); // continue lands here, right before i++72
+73
+ emit_load_mem_disp32(code, REG_RCX, REG_RBP, level + 16);74
+ emit_add_reg_imm8(code, REG_RCX, 1);75
+ emit_store_mem_disp32(code, REG_RBP, level + 16, REG_RCX); // i++76
+ emit_jmp_back(code, loop_start);77
+ emit_patch_jump(code, loop_exit);78
+ loop_pop_and_patch_breaks(); // break lands here, after the loop79
+80
+ for_nesting_depth--;81
+ vars_scope_end(saved_scope);82
+}kiln/src/parser/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.h5
+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_H2
+#define KILN_FOR_LOOP_H3
+4
+// for_statement itself is declared in parser_internal.h alongside5
+// if_statement/while_statement/fn_statement/try_statement (the other6
+// statement-compiler cross-file links) -- this header exists only for7
+// the depth counter below, which fn_statement/lambda.c also need.8
+//9
+// The compile-time for-nesting-depth counter's save/reset/restore, for10
+// fn_statement/lambda.c to wrap around a called body's compilation (same11
+// spirit as parser/vars.h's vars_save/vars_clear/vars_restore): a12
+// function or lambda body gets its own RBP at runtime, so its own `for`13
+// loops can safely reuse the same offsets independently of how deep the14
+// enclosing code's nesting was -- see vars.h's for_level_offset comment.15
+void for_depth_save(int *out);16
+void for_depth_reset(void);17
+void for_depth_restore(int saved);18
+19
+#endifkiln/src/parser/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.h3
+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_H2
+#define KILN_FUNCTIONS_H3
+4
+// Flat name -> {code offset, arity} table, mirroring vars.h's pattern (and5
+// ashvm's vm_functions[]/find_function). Functions can only call ones6
+// already declared earlier in the source -- same single-pass limitation7
+// ashvm's compiler has (see emit_call_back's comment) -- so by the time a8
+// 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 all11
+// -- 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 real13
+// programs room to import a handful of libraries without hitting the cap14
+// immediately; still a hard ceiling, noted as a tuning knob in the design15
+// spec, not solved generally here.16
+#define MAX_KILN_FUNCS 6417
+#define MAX_KILN_PARAMS 818
+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 around35
+// each recursively-parsed imported file; parser_control.c's fn_statement36
+// 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-file41
+// 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, else48
+// 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 own54
+// 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
+#endifkiln/src/parser/import_paths.cadded
+62−0@@ -0,0 +1,62 @@1
+#define _XOPEN_SOURCE 700 // exposes realpath() and PATH_MAX under -std=c112
+#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_H2
+#define KILN_IMPORT_PATHS_H3
+#include <limits.h>4
+5
+// Resolves the raw text inside `@import <...>` to a canonicalized6
+// (realpath()'d) absolute filesystem path.7
+//8
+// raw_path/raw_len: the text between '<' and '>', exactly as scanned by9
+// lexer_scan_import_path (NOT null-terminated -- callers must use len).10
+// importing_file_dir: directory of the file containing this @import11
+// statement (relative imports resolve against this).12
+// project_root_dir: directory of the entry .ash file passed on the13
+// command line (library imports resolve against this, regardless of14
+// 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 failure18
+// (message_out is filled with a short human-readable reason, e.g.19
+// "could not resolve '<path>'" or "library import name must not contain20
+// '.' 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's28
+// own basename with the trailing ".ash" stripped. Works uniformly for29
+// both relative and library imports because library entry files are30
+// required to be named "<libname>/<libname>.ash" -- the folder name and31
+// 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
+#endifkiln/src/parser/imports.cadded
+219−0@@ -0,0 +1,219 @@1
+#define _XOPEN_SOURCE 700 // exposes realpath() and PATH_MAX under -std=c112
+#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 1615
+#define MAX_KILN_IMPORT_DEPTH 816
+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 every87
+ // later use site of its constants (see constants.c), which can be88
+ // arbitrarily far ahead in the importing program. The compiler is a89
+ // one-shot host process; leaking these until it exits is the same90
+ // documented, intentional tradeoff kiln's own runtime heap allocator91
+ // 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 4103
+// Step 6.104
+static void parse_imported_file_body(void) {105
+ // Each imported file gets its OWN fresh import-block window: OPEN106
+ // (not closed), so the imported file's own leading @import107
+ // statements (if any -- e.g. a diamond-shaped or chained import) are108
+ // 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 own111
+ // 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 in135
+ // imported_files but STILL on resolving_stack is mid-parse (its136
+ // registry entry was added before recursing, same as every entry137
+ // here) -- that's a genuine cycle, not a finished diamond import.138
+ // Checking the registry first would silently treat a still-parsing139
+ // file as "already done" and swallow the cycle instead of reporting140
+ // it. Only a registry hit with NO matching resolving_stack entry is141
+ // 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 do146
+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 the153
+ // namespace and surrounding literal text with no truncation, so154
+ // 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 will180
+ // clobber, exactly mirroring vars_save/for_depth_save/loop_depth_save's181
+ // existing save-recurse-restore pattern (the C call stack gives182
+ // 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 parse206
+ // 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_H2
+#define KILN_IMPORTS_H3
+4
+// Called once, at the very start of compile_program, before the main5
+// parse loop -- establishes the entry file's own directory as the6
+// project root that library imports (ash.libs/) resolve against for the7
+// 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()'s11
+// dispatcher has NOT yet consumed the TOKEN_IMPORT (this function does12
+// 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-op16
+// after the first call in a given file's top-level parse -- marks that17
+// file's "imports must be a contiguous block at the top" window as18
+// closed, so a later @import in the SAME file is a compile error. Reset19
+// automatically on entry to each recursively-parsed imported file (their20
+// own import block starts fresh).21
+void imports_close_block(void);22
+23
+#endifkiln/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 165
+#define MAX_LOOP_JUMPS 646
+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.h3
+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_H2
+#define KILN_LOOP_STACK_H3
+4
+// break/continue need to know which loop they belong to, and loops5
+// nest -- but unlike for_loop.h's runtime scratch state, break/continue6
+// need NO runtime state at all: they're pure "emit now, patch later"7
+// jump bookkeeping (same primitive every other forward jump in this8
+// project uses), just tracked per-nesting-level at compile time instead9
+// of resolved immediately.10
+//11
+// continue can't simply backward-jump to a known offset the way a plain12
+// loop back-edge does: `continue`'s target (the per-iteration advance13
+// step -- a for-loop's index increment, or nothing for a while loop) is14
+// only emitted AFTER the body compiles, i.e. after the continue15
+// statement itself. So continue jumps are forward-patched too, resolved16
+// by the loop's own codegen right after block() returns, before17
+// 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-iteration30
+// advance step.31
+void loop_patch_continues(void);32
+33
+// Patches every recorded break jump to the CURRENT code position, then34
+// 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.c38
+// to wrap around a called body's compilation (same spirit as39
+// parser/vars.h's vars_save/vars_clear/vars_restore and for_loop.h's40
+// for_depth_save/reset/restore): a break/continue inside a function or41
+// lambda body must never reach past that body into an ENCLOSING loop it42
+// 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
+#endifkiln/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 own42
+// loop, and imports.c's parse_imported_file_body()) and here, inside a43
+// fn/if/while/for/try body -- block_depth distinguishes the two so the44
+// "only fn/let at an imported file's top level" guard in statement()45
+// only fires for genuinely top-level statements, never for an ordinary46
+// `return`/`if`/etc. nested inside an imported function's own body.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 already60
+// declared instead of erroring, matching ashvm's own let-statement61
+// behavior exactly (ashvm/src/compiler/stmt.c's let_statement).62
+static void let_statement(void) {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); // payload101
+ emit_pop_reg(code, REG_RBX); // tag102
+ 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 map107
+// 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 payload129
+ emit_pop_reg(code, REG_RBX); // value tag130
+ emit_pop_reg(code, REG_RCX); // index payload131
+ emit_pop_reg(code, REG_RDX); // index tag132
+ emit_pop_reg(code, REG_RDI); // base payload133
+ emit_pop_reg(code, REG_RSI); // base tag134
+ 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 fall141
+ // through to the array path (see missing.md #1: that path assumes142
+ // its base is an array object and would corrupt memory given a143
+ // 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 value163
+ 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 payload193
+ emit_pop_reg(code, REG_RDX); // b tag194
+ emit_pop_reg(code, REG_RAX); // a payload195
+ emit_pop_reg(code, REG_RBX); // a tag196
+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_EQUAL205
+ 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 payload212
+ emit_pop_reg(code, REG_RBX); // result tag213
+ 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 (not218
+// a pre/post-increment EXPRESSION with a value of its own -- that needs219
+// 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); // payload261
+ emit_pop_reg(code, REG_RBX); // tag262
+ 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's268
+// 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); // payload275
+ emit_pop_reg(code, REG_RBX); // tag276
+ } else {277
+ advance_token();278
+ emit_mov_reg_imm64(code, REG_RAX, 0); // 0.0's bits are all zero279
+ 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 own287
+// convention: every caught error, built-in or user-thrown, is a plain288
+// string). Reuses codegen/errors.c's raise routine directly -- the exact289
+// same catch/unwind mechanism M9's built-in errors (array bounds, missing290
+// map key) already go through, just with a runtime message instead of a291
+// compile-time-constant one.292
+static void throw_statement(void) {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); // length300
+ errors_emit_die_dynamic(code);301
+}302
+303
+// break ; -- jumps past the innermost loop; the jump is recorded, not304
+// resolved here, since the innermost loop's own codegen (parser_control.c's305
+// while_statement / parser/for_loop.c's for_statement) hasn't emitted the306
+// "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 advance314
+// step (a for-loop's index increment, or straight back to the condition315
+// for a while loop) -- also recorded, not resolved here, for the same316
+// reason as break above.317
+static void continue_statement(void) {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 ")" block328
+// | "fn" IDENT "(" params ")" block329
+// | "return" [expr] ";"330
+// (milestone 5's entire statement grammar -- anything else is a parse331
+// error, per the project rule that malformed input always fails cleanly)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 at371
+ // [argc, argv[0], ..., NULL, envp...] -- this is the ONLY point in372
+ // the whole program where that original RSP is still available373
+ // (nothing has touched it yet), so it's saved to a fixed globals374
+ // address here, before anything else, for argv() to read later from375
+ // any frame (see codegen/argv_builtin.c and elf/elf_writer.h's376
+ // KILN_ARGV_ADDR comment). Windows has no equivalent need: argv()377
+ // there calls GetCommandLineA() directly, callable at any point, so378
+ // 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 variable385
+ // slots. RSP drifts during expression push/pop; RBP never does. Each386
+ // function call below establishes and tears down its own separate387
+ // frame (see parser_control.c's fn_statement/parser.c's388
+ // return_statement) without disturbing this one -- the callee always389
+ // restores RBP before returning. Milestone 5: each slot is now 16390
+ // 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 internally397
+ 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.h9
+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_H2
+#define KILN_PARSER_H3
+#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 small8
+// 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 into12
+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
+#endifkiln/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 a14
+// number too) and compare its payload against 0.0 via ucomisd (not GP15
+// cmp -- see emit_sse.h). COND_E afterward means "was zero, i.e. falsy".16
+static void pop_and_test_truthy(void) {17
+ emit_pop_reg(code, REG_RAX); // payload18
+ 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.021
+ emit_ucomisd(code, XMM0, XMM1);22
+}23
+24
+// The statement kinds involved enough to warrant their own file (mirrors25
+// ashvm/src/compiler/stmt_control.c's split from stmt.c for the same26
+// reason). Each function assumes statement()'s dispatch in parser.c27
+// 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-back61
+ emit_jmp_back(code, loop_start);62
+ emit_patch_jump(code, exit_jump);63
+ loop_pop_and_patch_breaks(); // break lands here, after the loop64
+}65
+66
+// fn NAME ( [IDENT (, IDENT)*] ) block67
+//68
+// Function bodies are compiled inline into the SAME flat CodeBuf as69
+// 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 function72
+// it didn't explicitly call. The jump is patched once the body's end is73
+// known.74
+//75
+// Calling convention (kiln's own -- nothing external ever calls into76
+// generated code, so there's no ABI to match): caller pushes each77
+// argument's (tag, payload) pair -- 16 bytes, milestone 5 -- left to78
+// right via the normal expression codegen, no special reordering needed79
+// (see the offset derivation below), then `call`s the callee's80
+// already-known code offset (functions can only call ones declared81
+// earlier in the source, so the target is always known -- see82
+// emit_call_back). The standard push-rbp/mov-rbp,rsp/sub-rsp prologue83
+// establishes a fresh frame; the FIRST thing the body does is copy each84
+// argument's tag AND payload off the caller's pushed stack values into85
+// its own local slot, so afterward every variable access -- parameter or86
+// `let` -- goes through the exact same var_slot_*_offset() formulas.87
+// Because the last argument is the last one pushed (closest to the88
+// return address), and the first argument is deepest, parameter i's tag89
+// sits at [rbp + 16 + 16*(argc-1-i)] and its payload right after that90
+// (+16 skips the saved rbp and return address). Return value comes back91
+// as RBX=tag, RAX=payload (see parser.c's return_statement).92
+void fn_statement(void) {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 LOWER138
+ // offset: the caller pushed tag-then-payload for each arg, and139
+ // since push grows the stack downward, whatever's pushed SECOND140
+ // (payload) ends up closer to rbp. (Everywhere else this project141
+ // pops in LIFO order, which gets this right automatically --142
+ // here the callee reads the caller's still-live stack region143
+ // directly instead of popping, so the offset had to be derived144
+ // 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, harmless155
+ // (matches ashvm's compile_fn_decl, which does the same156
+ // unconditionally after compiling the body).157
+ emit_mov_reg_imm64(code, REG_RAX, 0); // 0.0's bits are all zero158
+ 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.h6
+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_H2
+#define KILN_PARSER_INTERNAL_H3
+4
+// Cross-file link between parser.c (state, simple statements, the5
+// statement() dispatcher) and parser_control.c (if/while/fn -- the6
+// statement kinds involved enough to warrant their own file), not part of7
+// 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 callable16
+// from imports.c's parse_imported_file_body(), which drives the same17
+// top-level "while not EOF, parse a statement" loop for a recursively18
+// parsed imported file -- so it's declared here rather than staying19
+// static to parser.c.20
+void statement(void);21
+void import_statement(void);22
+23
+#endifkiln/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 (see12
+// elf/elf_writer.h) holds, per active try, the (rsp, rbp) to restore and13
+// the (target address, error-variable rbp-offset) to resume at -- kiln's14
+// own setjmp/longjmp equivalent, hand-rolled since this is a freestanding15
+// binary with no libc to call into (see codegen/errors.c's raise routine16
+// for the runtime half of this).17
+//18
+// The catch variable's slot has to be reserved BEFORE compiling A: its19
+// rbp-relative offset must be embedded into the handler struct at20
+// try-entry, which runs before A's body -- and therefore before `e`'s21
+// real name, which only appears after A -- is even parsed. A placeholder22
+// slot is declared up front and retroactively renamed once `catch (e)`23
+// is actually seen. try/catch is inline (like if/while), not a new call24
+// frame, so this works the same way if/while's own bodies share the25
+// enclosing function's variable table.26
+void try_statement(void) {27
+ VarBlockScope saved_scope = vars_scope_begin();28
+ advance_token(); // consume 'try'29
+30
+ int catch_slot = declare_var("$catch", 6);31
+32
+ // --- push handler (bounds-checked -- overflowing here would33
+ // silently corrupt the code segment right after the handler table) ---34
+ emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());35
+ emit_load_mem_disp32(code, REG_RAX, REG_RAX, 0);36
+ emit_cmp_reg_imm32(code, REG_RAX, MAX_KILN_TRY_DEPTH);37
+ int overflow = emit_jcc_rel32(code, COND_GE);38
+39
+ emit_mov_reg_imm64(code, REG_RCX, KILN_TRY_HANDLER_SIZE);40
+ emit_imul_reg_reg(code, REG_RAX, REG_RCX); // RAX = index * handler size41
+ emit_mov_reg_imm64(code, REG_RDI, kiln_try_handlers_addr());42
+ emit_add_reg_reg(code, REG_RDI, REG_RAX); // RDI = this handler's slot address43
+44
+ emit_store_mem_disp32(code, REG_RDI, 0, REG_RSP);45
+ emit_store_mem_disp32(code, REG_RDI, 8, REG_RBP);46
+ int patch_offset = emit_mov_reg_imm64_patchable(code, REG_RCX); // catch target, patched once known47
+ emit_store_mem_disp32(code, REG_RDI, 16, REG_RCX);48
+ emit_mov_reg_imm64(code, REG_RCX, (uint64_t)(int64_t)var_slot_tag_offset(catch_slot));49
+ emit_store_mem_disp32(code, REG_RDI, 24, REG_RCX);50
+51
+ emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());52
+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 0);53
+ emit_add_reg_imm8(code, REG_RCX, 1);54
+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);55
+56
+ int skip_overflow_die = emit_jmp_rel32(code);57
+ emit_patch_jump(code, overflow);58
+ errors_emit_fatal(code, "runtime error: too many nested try blocks");59
+ emit_patch_jump(code, skip_overflow_die);60
+61
+ block(); // compiles A62
+63
+ // Normal completion (no error was raised): pop the handler -- past64
+ // this point, an error inside the enclosing scope should NOT be65
+ // caught by this try anymore.66
+ emit_mov_reg_imm64(code, REG_RAX, kiln_try_depth_addr());67
+ emit_load_mem_disp32(code, REG_RCX, REG_RAX, 0);68
+ emit_dec_reg(code, REG_RCX);69
+ emit_store_mem_disp32(code, REG_RAX, 0, REG_RCX);70
+71
+ int skip_catch = emit_jmp_rel32(code);72
+ int catch_code_offset = code->count;73
+ uint64_t catch_addr = kiln_code_base() + (uint64_t)catch_code_offset;74
+ emit_patch_imm64(code, patch_offset, catch_addr);75
+76
+ expect(TOKEN_CATCH, "expected 'catch' after try block");77
+ expect(TOKEN_LPAREN, "expected '(' after 'catch'");78
+ expect(TOKEN_IDENTIFIER, "expected error variable name");79
+ rename_var(catch_slot, previous.start, previous.length);80
+ expect(TOKEN_RPAREN, "expected ')' after catch variable");81
+82
+ block(); // compiles B -- `e` already declared at catch_slot above83
+84
+ emit_patch_jump(code, skip_catch);85
+ vars_scope_end(saved_scope);86
+}kiln/src/parser/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.h5
+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 shadowing10
+// semantics -- required for try/catch: parser/try_catch.c reserves each11
+// catch variable's slot with a synthetic placeholder name and renames it12
+// to the real name only after the fact (the slot's rbp-offset has to be13
+// known before the try body compiles, before the real name is even14
+// parsed -- see try_catch.c's header comment), so two SEQUENTIAL (not15
+// nested) `catch (e)` blocks end up with two separate table entries both16
+// named "e". A first-match-wins scan would bind the second catch body's17
+// references to the FIRST catch's stale slot; newest-first fixes that18
+// while leaving `let`'s own reuse-if-already-declared behavior unchanged19
+// (there's normally only one live entry for a given `let` name anyway).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.h3
+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_H2
+#define KILN_VARS_H3
+#include <stdint.h>4
+5
+// Flat name -> stack-slot table for milestone 2's single-scope variable6
+// model (no nested block scoping yet -- see the plan for why that's7
+// deliberately deferred). Mirrors ashvm/src/compiler/state.c's8
+// resolve_local/declare_local, minus the per-function scope stack.9
+#define MAX_KILN_VARS 6410
+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 (same19
+// slot index, new name) -- parser/try_catch.c needs a catch variable's20
+// slot reserved BEFORE its real name (`catch (e)`) is parsed, since the21
+// slot's rbp-relative offset must be embedded into the handler struct at22
+// 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 takes27
+// 16 bytes: tag at [rbp - 16*(i+1)], payload right after it at28
+// [rbp - 16*(i+1) + 8], below the frame pointer set up by parser.c's29
+// prologue (`mov rbp, rsp; sub rsp, MAX_KILN_VARS*16`). Shared here so30
+// 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 each35
+// function body needs its own fresh variable table too -- no visibility36
+// into the caller's (or top-level program's) variables yet (that needs37
+// closures, milestone 8). save/clear/restore lets parser.c's fn-statement38
+// codegen swap tables the same way ashvm's compile_fn_decl swaps39
+// 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 innermost51
+// active block began. vars_scope_begin/end bracket a single block() call52
+// (or, for for/try statements whose own loop/catch variable is declared53
+// outside their nested block() call -- see for_loop.c/try_catch.c -- the54
+// whole statement). Same save-an-int-on-the-C-call-stack pattern as55
+// for_depth_save/restore and loop_depth_save/restore above: block() nests56
+// via ordinary recursive descent, so the C call stack gives correct57
+// nesting for free, no explicit stack structure needed.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 CURRENT64
+// block's own declarations). let_statement/for_statement use this for65
+// their "reuse this slot or declare fresh" decision: a name that only66
+// 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 variable70
+// table, reserved for codegen/closures.c's indirect-call codegen to stash71
+// a runtime-only capture_count across the `call` instruction (GP72
+// registers are all freely clobbered by the callee; RBP is the one thing73
+// provably restored to the caller's own value by the time any call74
+// returns, per the calling convention every milestone since M3 relies75
+// on).76
+#define KILN_CALL_SCRATCH_SIZE 1677
+int32_t call_scratch_offset(void);78
+79
+// Milestone 10: a further 64-byte RBP-relative scratch region for80
+// 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 whichever83
+// of the three is currently running with builtin-specific field meanings84
+// (see codegen/higher_order_*.c) since they're never concurrently active85
+// within the same frame -- source-level arguments are evaluated86
+// left-to-right, so a nested call like `map(map(a,f),g)` has the inner87
+// call's loop fully finish before the outer one's begins. A callback88
+// that itself calls another higher-order builtin is safe for a different89
+// reason: it runs in ITS OWN frame (its own rbp from its own call90
+// prologue), so its scratch use lands at a completely different address.91
+#define KILN_HIGHER_ORDER_SCRATCH_SIZE 12892
+int32_t higher_order_scratch_offset(void);93
+94
+// Milestone 11: `for (x in array)` loop state (data_ptr, count, running95
+// index) also needs to survive across the loop body's own codegen -- but96
+// unlike try/catch's runtime recursion, for-loop NESTING is fully known97
+// at COMPILE TIME (nested `for` statements are literally nested calls in98
+// this single-pass compiler), so each nesting level just gets its own99
+// fixed offset rather than a runtime-tracked stack. Bounded the same way100
+// every other table here is; parser/for_loop.c resets its nesting101
+// counter to 0 when entering a function or lambda body (mirroring102
+// vars_save/vars_clear/vars_restore), since that body gets its own RBP103
+// at runtime and can safely reuse the same offsets independently of how104
+// deep the enclosing code's own `for` nesting was.105
+#define MAX_KILN_FOR_DEPTH 8106
+#define KILN_FOR_LEVEL_SIZE 24107
+#define KILN_FOR_STACK_SIZE (MAX_KILN_FOR_DEPTH * KILN_FOR_LEVEL_SIZE)108
+int32_t for_level_offset(int depth);109
+110
+// Plan A, phase A3: one 8-byte RBP-relative slot for codegen/win_call.c to111
+// stash the original RSP across a Windows API call (dynamic 16-byte align112
+// + shadow space reservation clobbers RSP by an amount only known at113
+// runtime, so it can't be undone with a fixed `add rsp, N` -- see114
+// win_call.c). Same "RBP survives a call, registers don't, and each115
+// function frame gets its own copy" reasoning as every other scratch slot116
+// here; a single slot suffices since one win_call sequence always fully117
+// completes (there's no way for the compiled program to interleave two)118
+// before another can start.119
+#define KILN_WIN_CALL_SCRATCH_SIZE 8120
+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 use124
+// for (bytes actually written/read -- kiln already knows this from the125
+// length it requested, matching the "single read/write call assumed to126
+// fully succeed" simplification already documented in the Linux syscall127
+// call sites). Separate from win_call_scratch_offset because that slot is128
+// 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 8131
+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
+#endifkiln/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 Data13
+// Directory instead of an Import one -- the Windows-side twin of14
+// elf/elf_so_writer.c. The Windows loader resolves imports-by-name via a15
+// BINARY SEARCH over the DLL's Name Pointer Table, which means (unlike16
+// ELF's hash-table symbol lookup) that table must be in sorted order, or17
+// 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_EXPORT24
+};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, matching63
+ // 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) order74
+ 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); // NumberOfSections92
+ put32(&f, 0); put32(&f, 0); put32(&f, 0);93
+ put16(&f, 240);94
+ put16(&f, 0x0002 | 0x2000); // EXECUTABLE_IMAGE | DLL95
+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); // BaseOfCode102
+ 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 emits114
+ 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 Table120
+ 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_DIRECTORY136
+ put32(&f, 0); // Characteristics137
+ put32(&f, 0); // TimeDateStamp138
+ put16(&f, 0); put16(&f, 0); // Major/MinorVersion139
+ put32(&f, dll_name_rva); // Name140
+ put32(&f, 1); // Base (ordinals start at 1)141
+ put32(&f, (uint32_t)RUNTIME_IMPORT_COUNT_); // NumberOfFunctions142
+ put32(&f, (uint32_t)RUNTIME_IMPORT_COUNT_); // NumberOfNames143
+ 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 point148
+ 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 AddressOfFunctions152
+ 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.h7
+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_H2
+#define KILN_PE_DLL_WRITER_H3
+#include "codegen/runtime_import_enum.h"4
+#include "pe/pe_writer.h"5
+6
+// libkilnrt.dll: same fixed-base, no-ASLR, no-relocations philosophy as7
+// every other kiln output (a real DLL would normally get a .reloc8
+// section so the loader can rebase it if its preferred address is9
+// taken; kiln skips that the same way it skips PIC/ASLR support for its10
+// own executables -- consistent, not a shortcut specific to this file).11
+// Distinct base from KILN_PE_IMAGE_BASE (the executable's own) since12
+// both could conceivably be loaded into the same process.13
+#define KILN_PE_DLL_IMAGE_BASE 0x180000000ULL14
+#define KILN_PE_DLL_SECTION_RVA KILN_PE_SECTION_ALIGN15
+16
+// IMAGE_EXPORT_DIRECTORY (40 bytes) + 3 fixed-size arrays (sized only by17
+// RUNTIME_IMPORT_COUNT_, never by name length) -- code starts right after,18
+// so its address is a compile-time constant kiln_code_base() can use19
+// (see codegen/layout.c); the variable-length name strings go in the20
+// 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 40u23
+#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 fresh35
+// CodeBuf and writes it as a PE32+ DLL exporting them by name, loadable36
+// by the real Windows PE loader. Returns 0 on success.37
+int pe_write_shared_runtime(const char *path);38
+39
+#endifkiln/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-section7
+// technique as pe_writer.c's static-mode writer, but importing from TWO8
+// DLLs (kernel32.dll for OS calls, libkilnrt.dll for the runtime -- see9
+// 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_IMPORT19
+};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_EXPORT25
+};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 terminator119
+ 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 descriptor130
+131
+ // Both ILTs, then both IATs (see pe_writer.h: keeps the combined IAT132
+ // 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.def5
+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 codegen2
+// calls. Single source of truth shared between pe_writer.c (which builds3
+// the actual import/IAT tables) and any codegen file that needs to call4
+// one of these (via PE_IMPORT_<NAME> from pe_writer.h) -- adding a new5
+// 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 twin8
+// of elf/elf_writer.c's "tiny ELF" technique: one RWX section holding9
+// everything (globals, import tables, code), a fixed non-relocated image10
+// 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 alone15
+// (never by name string lengths), so codegen can address IAT slots and16
+// the code's own start address as compile-time constants -- the only17
+// things whose size depends on `machine_code->count` or import name18
+// lengths (the hint/name table, the dll name string) are placed AFTER19
+// 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_IMPORT25
+};26
+27
+// --link=shared moves kernel32.dll's IAT to a different offset (there's28
+// 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 at30
+// the wrong slot, which is exactly the kind of bug that surfaces as a31
+// jump to a garbage address rather than a clean error (caught by testing32
+// the Windows shared build under Wine, not assumed correct from the33
+// 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 (PE59
+// 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_lfanew88
+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 = AMD6492
+ put16(&f, 1); // NumberOfSections93
+ put32(&f, 0); // TimeDateStamp94
+ put32(&f, 0); // PointerToSymbolTable95
+ put32(&f, 0); // NumberOfSymbols96
+ put16(&f, 240); // SizeOfOptionalHeader97
+ put16(&f, 0x0002 | 0x0020); // EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE98
+99
+ // ---- IMAGE_OPTIONAL_HEADER64 ----100
+ put16(&f, 0x20B); // Magic = PE32+101
+ put8(&f, 14); put8(&f, 0); // Linker version102
+ put32(&f, section_raw_size); // SizeOfCode103
+ put32(&f, 0); // SizeOfInitializedData104
+ put32(&f, 0); // SizeOfUninitializedData105
+ put32(&f, code_rva); // AddressOfEntryPoint106
+ put32(&f, KILN_PE_SECTION_RVA); // BaseOfCode107
+ 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 version111
+ put16(&f, 0); put16(&f, 0); // Image version112
+ put16(&f, 6); put16(&f, 0); // Subsystem version113
+ put32(&f, 0); // Win32VersionValue114
+ put32(&f, image_size);115
+ put32(&f, KILN_PE_HEADERS_SIZE);116
+ put32(&f, 0); // CheckSum -- not validated by the loader for normal exe launch117
+ put16(&f, 3); // Subsystem = WINDOWS_CUI (console)118
+ put16(&f, 0); // DllCharacteristics -- deliberately no DYNAMIC_BASE: fixed-base, no relocation table119
+ put64(&f, 0x100000); put64(&f, 0x1000); // stack reserve/commit120
+ put64(&f, 0x100000); put64(&f, 0x1000); // heap reserve/commit121
+ put32(&f, 0); // LoaderFlags122
+ put32(&f, 16); // NumberOfRvaAndSizes123
+ 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); // PointerToRawData136
+ put32(&f, 0); put32(&f, 0); // relocations/linenumbers pointers137
+ put16(&f, 0); put16(&f, 0); // relocation/linenumber counts138
+ put32(&f, 0x20u | 0x20000000u | 0x40000000u | 0x80000000u); // CODE|EXECUTE|READ|WRITE139
+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 is147
+ // known before any bytes are emitted), so this is a direct write, no148
+ // patch-later step needed.149
+ put32(&f, KILN_PE_ILT_ADDR - KILN_PE_IMAGE_BASE); // OriginalFirstThunk -> ILT150
+ put32(&f, 0); put32(&f, 0); // TimeDateStamp, ForwarderChain151
+ put32(&f, dll_name_rva); // Name -> DLL name string152
+ put32(&f, KILN_PE_IAT_ADDR - KILN_PE_IMAGE_BASE); // FirstThunk -> IAT153
+ put_zeros(&f, 20); // terminator descriptor154
+155
+ // ILT and IAT: identical initial content, each entry an RVA to this156
+ // 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); // terminator160
+ }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 pad169
+ }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.h5
+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_H2
+#define KILN_PE_WRITER_H3
+#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 "always8
+// loaded at one known address" philosophy as elf/elf_writer.h's9
+// KILN_LOAD_BASE -- absolute addresses for string literals, closure code10
+// addresses, and (this file's addition) imported-function IAT slots are11
+// all known at compile time, no relocation table needed. 0x140000000 is12
+// the address range modern PE tooling (MSVC/link.exe) itself defaults 6413
+// bit executables to, so it's a safe, unsurprising choice for loaders.14
+#define KILN_PE_IMAGE_BASE 0x140000000ULL15
+#define KILN_PE_SECTION_ALIGN 0x1000u16
+#define KILN_PE_FILE_ALIGN 0x200u17
+18
+// DOS header (64) + "PE\0\0" (4) + COFF header (20) + PE32+ optional19
+// 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 368u22
+#define KILN_PE_HEADERS_SIZE 0x200u23
+24
+// The one-and-only section: globals region, then import machinery, then25
+// code, then (past the end of `code`, so its size doesn't affect any of26
+// the fixed offsets below) the variable-length hint/name + DLL-name27
+// tail -- see pe_writer.c's header comment for why that ordering matters.28
+#define KILN_PE_SECTION_RVA KILN_PE_SECTION_ALIGN29
+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, unlike35
+// Linux's argv() which has to snapshot the kernel's original RSP before36
+// 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 list40
+// in pe_imports.def, shared by this enum and pe_writer.c's actual table41
+// 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_IMPORT46
+ PE_IMPORT_COUNT_47
+} PeImport;48
+49
+// Import Directory Table: one IMAGE_IMPORT_DESCRIPTOR (20 bytes) for50
+// 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 40u53
+54
+// Import Lookup Table and Import Address Table: one 8-byte entry per55
+// import plus a zero terminator each. Fixed size regardless of function56
+// NAME lengths (those live in the variable-length tail written after57
+// `code`), which is exactly what lets codegen address IAT slots as58
+// 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 with68
+// the resolved function pointer at load time. codegen/win_call.c loads69
+// this address, dereferences it, and calls through the result (see that70
+// 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 fully75
+// parallel set of constants, never touched by the static-mode ones76
+// 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) instead82
+// of 2 (kernel32.dll, null). Both ILTs are grouped before both IATs83
+// (rather than interleaved per-DLL) so the combined IAT region is84
+// CONTIGUOUS -- the optional header's IAT data directory entry is a85
+// single {RVA,Size} pair, which can only correctly describe one86
+// contiguous range.87
+#define KILN_PE_SHARED_IMPORT_DIR_SIZE 60u88
+#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 from106
+// 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 executable110
+// at `path` (hand-written headers/import tables, same "no external111
+// linker" spirit as elf_write_executable). Returns 0 on success.112
+int pe_write_executable(const char *path, const CodeBuf *machine_code);113
+114
+#endifkiln/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.h2
+src/target.h:kiln/src/target.hadded
+47−0@@ -0,0 +1,47 @@1
+#ifndef KILN_TARGET_H2
+#define KILN_TARGET_H3
+4
+// Compile-time target selector (Plan A, phase A1). Nothing reads this yet5
+// past main.c storing it -- codegen/writer forks land in later phases.6
+// Global, not threaded as a parameter: every codegen/writer call site would7
+// 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 is9
+// avoiding until a phase actually needs to branch on it.10
+typedef enum {11
+ KILN_TARGET_LINUX,12
+ KILN_TARGET_WINDOWS13
+} 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 and20
+// the only mode before Plan B) or left external, calling into21
+// libkilnrt.so via real dynamic linking (KILN_LINK_SHARED, Linux only --22
+// Windows DLL equivalent is a later phase). Same "global, not threaded as23
+// a parameter" reasoning as KilnTarget above.24
+typedef enum {25
+ KILN_LINK_STATIC,26
+ KILN_LINK_SHARED27
+} 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 THIS33
+// routine's own call sites (heap_emit_alloc etc.) reach their callee via34
+// a local call or a GOT call" -- but codegen/layout.c's globals-address35
+// selection needs to answer a DIFFERENT question, "which executable36
+// layout owns the globals this code will read at runtime", and that37
+// answer is the shared-mode layout in BOTH cases: when compiling a38
+// normal --link=shared executable AND when compiling the runtime that39
+// goes into libkilnrt.so (its heap_alloc etc. always run on behalf of40
+// some shared-mode executable's globals, never its own). Conflating41
+// these into one flag was a real bug caught by testing (see42
+// elf/elf_so_writer.c's comment) -- kiln_set_link_mode(STATIC) to get43
+// 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
+#endifkiln/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
+/* multi4
+ line5
+ comment */6
+print 2;7
+print /* inline */ 3;8
+let x = 5; /* trailing */ print x;9
+// still works10
+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; scoped17
+// slot reuse keeps this well under the cap. Verified during planning:18
+// this exact file (with the shadowing let above already showing the19
+// 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 a2
+// block-scoped variable; call it after the block, once its slot has3
+// been reused by a later declaration4
+fn make_from_if() {5
+ let f = 0;6
+ if (true) {7
+ let x = 42;8
+ f = fn() { return x; };9
+ }10
+ let after = 999; // reuses x's old slot11
+ 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 be18
+// untouched afterward19
+fn make_shadow() {20
+ let x = 1;21
+ let f = 0;22
+ if (true) {23
+ let x = 2;24
+ f = fn() { return x; };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 value34
+fn make_list() {35
+ let fns = [];36
+ let i = 0;37
+ while (i < 3) {38
+ let captured = i;39
+ push(fns, fn() { return captured; });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 innermost30
+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 caller39
+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 called2
+let square = fn(x) { return x * x; };3
+print square(5);4
+5
+// named function referenced as a value and called indirectly6
+fn add(a, b) { return a + b; }7
+let op = add;8
+print op(3, 4);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 function21
+fn apply_twice(f, x) {22
+ return f(f(x));23
+}24
+print apply_twice(square, 3);25
+26
+// closure capturing multiple outer variables27
+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 untouched2
+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 -- each9
+// level's binding is independent10
+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 loops7
+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 body16
+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 array23
+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 check30
+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 iteration41
+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.