docs/data: document the LLVM IR backend + knowledge base entries

ober

e7adb8728d06c392110e82469732e1296c1479ce

diff --git a/data/api-signatures.sexp b/data/api-signatures.sexp
index 177c647..26157ed 100644
--- a/data/api-signatures.sexp
+++ b/data/api-signatures.sexp
@@ -1,4 +1,4 @@
-(("errors") ("generated" . "2026-05-27")
+(("errors") ("generated" . "2026-06-03")
   ("modules"
    ("(jerboa build musl)"
      ("exports" "build-musl-binary" "make-musl-cross-target"
@@ -651,6 +651,14 @@
       "typed-ir-var-source" "typed-ir-var-type" "typed-ir-var?")
      ("file" . "lib/jerboa/typed/core.ss")
      ("tier" . "core"))
+   ("(jerboa typed llvmir)"
+     ("exports" "llvm-float-literal" "llvm-function-symbol"
+       "llvm-module-mangle" "llvm-symbol-name"
+       "typed-library-form->llvmir-string"
+       "typed-library-forms->llvmir-module"
+       "typed-module->llvmir-string")
+     ("file" . "lib/jerboa/typed/llvmir.ss")
+     ("tier" . "core"))
    ("(jerboa typed parser)"
      ("exports" "expr->list" "expr-cadddr" "expr-caddr"
       "expr-cadr" "expr-car" "expr-cdddr" "expr-cdr" "expr-length"
@@ -4416,14 +4424,6 @@
        "notebook-name" "notebook?")
      ("file" . "lib/std/notebook.ss")
      ("tier" . "stable"))
-   ("(std nrepl)"
-     ("exports"
-       "nrepl-running?"
-       "nrepl-server-port"
-       "nrepl-start!"
-       "nrepl-stop!")
-     ("file" . "lib/std/nrepl.ss")
-     ("tier" . "stable"))
    ("(std odb)"
      ("exports" "*odb*" ":f64" ":mptr" ":s64" ":string"
       "define-persistent-class" "doclass" "mptr->object"
@@ -6685,13 +6685,13 @@
      ("file" . "lib/std/zipper.ss")
      ("tier" . "stable")))
   ("source_root" . "/Users/user/mine/jerboa")
-  ("stats" ("modules" . 679) ("parse_errors" . 0) ("symbols" . 9907)
+  ("stats" ("modules" . 679) ("parse_errors" . 0) ("symbols" . 9910)
     ("tiers"
       ("compat" . 47)
-      ("core" . 33)
-      ("stable" . 521)
+      ("core" . 34)
+      ("stable" . 520)
       ("unstable" . 78))
-    ("total_exports" . 13077))
+    ("total_exports" . 13080))
   ("symbol_index" ("%chan-enqueue-raw!" "(std csp)")
    ("&actor-dead" "(std error conditions)")
    ("&actor-timeout" "(std error conditions)")
@@ -11988,8 +11988,13 @@
    ("listeners" "(std misc event-emitter)")
    ("listof" "(std contract)") ("literate" "(std lazy)")
    ("live-object-counts" "(std inspect)")
-   ("llist->list" "(std lazy)") ("lmap" "(std lazy)")
-   ("lnull" "(std lazy)") ("lnull?" "(std lazy)")
+   ("llist->list" "(std lazy)")
+   ("llvm-float-literal" "(jerboa typed llvmir)")
+   ("llvm-function-symbol" "(jerboa typed llvmir)")
+   ("llvm-module-mangle" "(jerboa typed llvmir)")
+   ("llvm-symbol-name" "(jerboa typed llvmir)")
+   ("lmap" "(std lazy)") ("lnull" "(std lazy)")
+   ("lnull?" "(std lazy)")
    ("load-c-header" "(std foreign bind)")
    ("load-config" "(std config)")
    ("load-image" "(std persist image)")
@@ -13172,9 +13177,6 @@
    ("notebook?" "(std notebook)" "(std repl notebook)")
    ("nothing" "(std srfi srfi-159)")
    ("notify-change!" "(std dev reload)")
-   ("nrepl-running?" "(std nrepl)")
-   ("nrepl-server-port" "(std nrepl)")
-   ("nrepl-start!" "(std nrepl)") ("nrepl-stop!" "(std nrepl)")
    ("nth-prime" "(std misc prime)") ("nthpath" "(std specter)")
    ("null-list?" "(std srfi srfi-1)")
    ("nullo" "(jerboa clojure)" "(std logic)")
@@ -17531,12 +17533,16 @@
      "(jerboa typed wrapper)")
    ("typed-library-form->jerboa-wrapper-string"
      "(jerboa typed wrapper)")
+   ("typed-library-form->llvmir-string"
+     "(jerboa typed llvmir)")
    ("typed-library-form->rust-string" "(jerboa typed rust)")
    ("typed-library-form?" "(jerboa typed parser)")
    ("typed-library-forms->jerboa-wrapper-files"
      "(jerboa typed wrapper)")
    ("typed-library-forms->jerboa-wrapper-library-files"
      "(jerboa typed wrapper)")
+   ("typed-library-forms->llvmir-module"
+     "(jerboa typed llvmir)")
    ("typed-library-forms->rust-crate-files"
      "(jerboa typed rust)")
    ("typed-library-forms->rust-crate-string"
@@ -17545,6 +17551,7 @@
      "(jerboa typed wrapper)")
    ("typed-module->jerboa-wrapper-string"
      "(jerboa typed wrapper)")
+   ("typed-module->llvmir-string" "(jerboa typed llvmir)")
    ("typed-module->rust-string" "(jerboa typed rust)")
    ("typed-module-declarations" "(jerboa typed parser)")
    ("typed-module-exports" "(jerboa typed parser)")
diff --git a/data/changelog.sexp b/data/changelog.sexp
index 635acc8..5fa12be 100644
--- a/data/changelog.sexp
+++ b/data/changelog.sexp
@@ -2,7 +2,23 @@
    .
    "Machine-readable changelog of Jerboa API drift. Consumers (LLM tooling, lints, jerboa_verify) use this to invalidate stale recommendations and to suggest migrations when a symbol is renamed or relocated.")
   ("entries"
-    (("added" "jerboa_symbol_exists_batch" "jerboa_verify_changes"
+    (("added" "typed-module->llvmir-string" "typed-library-form->llvmir-string"
+        "typed-library-forms->llvmir-module" "llvm-symbol-name"
+        "llvm-module-mangle" "llvm-function-symbol" "llvm-float-literal")
+      ("date" . "2026-06-03")
+      ("modules_added" "(jerboa typed llvmir)")
+      ("moved")
+      ("notes"
+        .
+        "Experimental Typed Jerboa -> textual LLVM IR backend (scalar subset): Nat/Int/Bool/Float plus Unit returns, let/begin/if (phi joins), direct same-module calls, arithmetic/comparisons/equal?/bool, bitwise and shifts, exact->inexact, log2 via llvm.log2.f64, and for/fold over in-range. Deterministic .ll per typed-library form via support/typed-llvmir.ss; Makefile targets typed-llvmir, typed-llvmir-check (llvm-as + opt -passes=verify + opt -O2), typed-llvmir-smoke (llc + cc executable, expected exit status), and typed-llvmir-parity (observed-result comparison against the Rust backend). Rust remains the reference backend. See docs/llvmir-backend.md.")
+      ("removed")
+      ("renamed")
+      ("tier_changes")
+      ("tools_added" "support/typed-llvmir.ss" "make typed-llvmir"
+        "make typed-llvmir-check" "make typed-llvmir-smoke"
+        "make typed-llvmir-parity" "make test-typed-llvmir")
+      ("version" . #f))
+     (("added" "jerboa_symbol_exists_batch" "jerboa_verify_changes"
         "jerboa_module_exists" "jerboa_variadic_ffi_check"
         "tools/gen-api-signatures.ss" "make api-signatures")
       ("date" . "2026-05-27")
diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index 2dfb653..4a74f38 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -4909,4 +4909,16 @@
      "smoke-test" "stdin")
    ("title"
      .
-     "Smoke test a local Jerboa module with jerbuild exec and libdirs")))
+     "Smoke test a local Jerboa module with jerbuild exec and libdirs"))
+ (("code"
+    .
+    ";; Library API: typed-library form -> deterministic textual LLVM IR\n(import (jerboa typed parser) (jerboa typed llvmir))\n\n(def form\n  '(typed-library (sample typed demo)\n     (export main)\n     (def (sum-to (n : Nat)) : Nat\n       (for/fold ((acc 0)) ((i (in-range n)))\n         (+ acc i)))\n     ;; zero-param Nat/Int/Bool main gets a generated @main (exit status)\n     (def (main) : Nat\n       (bitwise-and (sum-to 10) 63))))\n\n(display (typed-library-form->llvmir-string form))\n\n;; Shell pipeline (Makefile targets wrap the same steps):\n;;   scheme --libdirs lib --script support/typed-llvmir.ss build/typed/llvmir file.ss\n;;   make typed-llvmir         # emit .ll for the LLVM fixtures\n;;   make typed-llvmir-check   # llvm-as + opt -passes=verify + opt -O2\n;;   make typed-llvmir-smoke   # llc + cc, run, expect exit 42\n;;   make typed-llvmir-parity  # same source through Rust backend, compare exits") ("id" . "typed-llvmir-emit-and-verify")
+   ("imports" "(jerboa typed llvmir)" "(jerboa typed parser)")
+   ("notes"
+     .
+     "Experimental scalar-only backend (docs/llvmir-backend.md): Nat/Int/Bool/Float, Unit returns, let/begin/if, same-module calls, arith/cmp/equal?/bool, bitwise+shifts, exact->inexact, log2 (llvm.log2.f64), for/fold over in-range. Everything else (String/Bytes/records/variants/match/imports) is rejected with a typed-llvmir error. Signedness lives in operations: Nat -> udiv/icmp ult/lshr, Int -> sdiv/icmp slt/ashr. Float literals emit as IEEE-754 hex (0x4011000000000000 = 4.25). Function symbols: @jt_llvm_<module>__<def>. if branches must agree on one numeric type (positive literals are Nat; compute Int 1 as (/ n n)). Rust backend stays the correctness oracle; LLVM tools found on PATH or Homebrew keg, override with LLVM_BIN=.")
+   ("tags" "typed" "llvmir" "llvm" "backend" "native"
+     "for-fold")
+   ("title"
+     .
+     "Compile Typed Jerboa scalar modules to LLVM IR and verify/run them")))
diff --git a/docs/index.md b/docs/index.md
index e6c77d3..f8ccec7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -38,6 +38,7 @@ New here? Start with [quickstart.md](quickstart.md), then [tutorial.md](tutorial
 
 - [typing.md](typing.md) — gradual typing with runtime assertions, zero production overhead
 - [typed-jerboa.md](typed-jerboa.md) — Typed Jerboa design (vision and non-goals)
+- [llvmir-backend.md](llvmir-backend.md) — experimental Typed Jerboa → textual LLVM IR backend (scalar subset)
 - [jerboa-code-typed-static-plan.md](jerboa-code-typed-static-plan.md) — plan for using Typed Jerboa in the `jerboa-code` project
 - [contracts.md](contracts.md) — runtime contracts system (engineering plan)
 - [gerbil-contracts.md](gerbil-contracts.md) — Gerbil's contract system, as a reference for Jerboa's bridge
diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index 5bdef8c..51bc75a 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -7,7 +7,9 @@ The Rust backend is the first native backend for Typed Jerboa.
 Rust is not the desired surface language. Typed Jerboa is. Rust is the first
 backend because it gives the project a mature compiler, strong memory safety,
 good diagnostics, good cross-compilation, and a practical way to validate typed
-Jerboa semantics before taking on a direct LLVM backend.
+Jerboa semantics before taking on a direct LLVM backend. (An experimental
+direct LLVM IR backend now exists for the scalar subset, with Rust as its
+correctness oracle — see [llvmir-backend.md](llvmir-backend.md).)
 
 The Rust backend should compile typed Jerboa modules into Rust crates, compile
 those crates into native artifacts, and generate Jerboa wrappers so dynamic
diff --git a/docs/llvmir-backend.md b/docs/llvmir-backend.md
new file mode 100644
index 0000000..c0c4ac8
--- /dev/null
+++ b/docs/llvmir-backend.md
@@ -0,0 +1,169 @@
+# Typed Jerboa LLVM IR Backend
+
+Status: experimental, scalar-only. The Rust backend
+([jerboa-to-rust.md](jerboa-to-rust.md)) remains the reference backend and
+correctness oracle. This backend proves Typed Jerboa can lower its checked
+core IR directly to LLVM without going through generated Rust. The original
+design brief is [llvmir-handoff.md](llvmir-handoff.md).
+
+## Pipeline
+
+```text
+typed Jerboa source (.ss, typed-library forms)
+  -> typed parser/checker        (jerboa typed parser) / (jerboa typed checker)
+  -> typed core IR               (jerboa typed core)
+  -> textual LLVM IR (.ll)       (jerboa typed llvmir)
+  -> llvm-as + opt -passes=verify + opt -O2
+  -> llc + cc                    (smoke executable)
+```
+
+The emitter consumes the same elaborated module/def data as the Rust backend
+(`check-and-elaborate-typed-module`), never raw source datums. Generated `.ll`
+is deterministic: emitting the same module twice yields identical text.
+
+## Commands
+
+```bash
+# Emit .ll for the LLVM fixtures into build/typed/llvmir/
+make typed-llvmir
+
+# Assemble + verify + opt -O2 every generated .ll (needs LLVM tools)
+make typed-llvmir-check
+
+# Build the smoke fixture into an executable and require exit status 42
+make typed-llvmir-smoke
+
+# Lower the same typed source through the LLVM and Rust backends and
+# require identical observed results (needs rustc)
+make typed-llvmir-parity
+
+# Emitter unit tests (no LLVM tools required)
+make test-typed-llvmir
+```
+
+Direct CLI, one `.ll` per `typed-library` form:
+
+```bash
+scheme --libdirs lib --script support/typed-llvmir.ss OUT-DIR file.ss ...
+# writes OUT-DIR/<module_mangle>.ll, e.g. sample_typed_llvmir_basic.ll
+```
+
+LLVM tools are discovered on `PATH` first, then the Homebrew keg
+(`/opt/homebrew/opt/llvm/bin`, `/usr/local/opt/llvm/bin`); override with
+`make typed-llvmir-check LLVM_BIN=/path/to/llvm/bin`. When tools are missing,
+unit tests still validate deterministic `.ll` text; the check/smoke targets
+fail with a clear message.
+
+## Supported Subset
+
+Exactly this; everything else is rejected at lowering with a
+`typed-llvmir` error:
+
+- Types: `Nat`, `Int`, `Bool`, `Float`; `Unit` as a return type only.
+- Numeric and boolean literals.
+- `let` (sequential bindings, no stack slots), `begin`, `if`.
+- Direct same-module function calls.
+- Arithmetic `+ - * /`, comparisons `= < <= > >=`, scalar `equal?`.
+- Boolean `not`/`and`/`or` (non-short-circuit `and`/`or` over already
+  evaluated `i1` values).
+- Bitwise `bitwise-and/ior/xor/not`, shifts
+  `bitwise-arithmetic-shift-left/right`.
+- `exact->inexact`, `log2`.
+- `(for/fold ((acc init)) ((i (in-range [start] end))) body)`.
+- A generated `@main` for smoke tests (below).
+
+Not supported (deliberately, this pass): strings, bytes, records, variants,
+Option/Result, resources, match, closures, GC, cross-module imports, the Chez
+FFI boundary, and LLVM C API bindings. Modules containing record/variant/
+resource declarations are rejected whole.
+
+## Representation
+
+```text
+Bool  -> i1
+Nat   -> i64   unsigned ops: udiv, icmp ult/ule/ugt/uge, lshr
+Int   -> i64   signed ops:   sdiv, icmp slt/sle/sgt/sge, ashr
+Float -> double  fadd/fsub/fmul/fdiv, ordered fcmp (oeq/olt/...)
+Unit  -> void  (procedure returns only)
+```
+
+Signedness lives in operations, not types. Mixed int/float operands in one
+operation are rejected at lowering rather than emitted as malformed IR. No
+`nsw`/`nuw` flags are emitted and shift counts are not masked: Typed Jerboa
+overflow/shift semantics are not explicit yet, and wrong flags are
+miscompilation bugs. Float literals are emitted as IEEE-754 bit patterns
+(`0x4011000000000000` for `4.25`), the always-valid LLVM spelling. `log2`
+declares and calls the `llvm.log2.f64` intrinsic.
+
+## Naming
+
+```text
+function symbol  @jt_llvm_<module_mangle>__<def_mangle>
+                 (sample typed llvmir-basic) add-one
+                 -> @jt_llvm_sample_typed_llvmir_basic__add_one
+parameters       %a0, %a1, ...   (positional; user names never appear raw)
+locals           %v0, %v1, ...
+block labels     entry, thenN/elseN/joinN, loop_headN/loop_bodyN/loop_exitN
+```
+
+One mangler is shared by module and def names: `-` becomes `_`, `?` becomes
+`_p`, `!` becomes `_bang`, anything else non-alphanumeric becomes `_`.
+
+## Lowering Shapes
+
+`if` produces real basic blocks with a `phi` at the join; the phi's
+predecessors are the blocks each branch *ends* in, so nested control flow
+stays well-formed. `Unit`-typed `if` emits no phi.
+
+`for/fold` produces a loop header with two phis (accumulator, index), an
+`icmp ult`/`icmp slt` bound check chosen by the range type, a body block
+ending in an index increment, and an exit block whose value is the
+accumulator phi:
+
+```llvm
+entry:
+  br label %loop_head0
+loop_head0:
+  %v0 = phi i64 [ 0, %entry ], [ %v3, %loop_body0 ]
+  %v1 = phi i64 [ 0, %entry ], [ %v4, %loop_body0 ]
+  %v2 = icmp ult i64 %v1, %a0
+  br i1 %v2, label %loop_body0, label %loop_exit0
+loop_body0:
+  %v3 = add i64 %v0, %v1
+  %v4 = add i64 %v1, 1
+  br label %loop_head0
+loop_exit0:
+  ret i64 %v0
+```
+
+## Smoke `@main` Convention
+
+A zero-parameter `main` returning `Nat`, `Int`, or `Bool` gets a generated
+process-entry `@main`:
+
+- `Nat`/`Int`: the i64 result is truncated to the i32 exit status, so smoke
+  fixtures keep results tiny and exact (the canonical fixture exits 42).
+- `Bool`: exit 0 for `#t`, 1 for `#f`.
+- Any other `main` shape lowers as an ordinary function with no `@main`.
+
+This is a smoke-test convention, not runtime integration. Real Jerboa interop
+(C ABI wrappers, linking into the Jerboa binary, or `foreign-procedure`)
+is future work and should follow the Rust backend's ABI experience.
+
+## Parity With the Rust Backend
+
+`make typed-llvmir-parity` lowers `tests/fixtures/typed/llvmir-smoke.ss`
+through both backends and compares observed behavior, not generated source:
+the LLVM executable's exit status must equal the exit status of a
+rustc-compiled harness calling the generated Rust (`lib.rs` built as an rlib
+directly with `rustc`, no cargo), and both must equal the expected constant.
+
+## Files
+
+```text
+lib/jerboa/typed/llvmir.ss       emitter library (jerboa typed llvmir)
+support/typed-llvmir.ss          CLI: typed .ss -> OUT-DIR/<module>.ll
+tests/test-typed-llvmir.ss       deterministic emitter unit tests
+tests/fixtures/typed/llvmir-{basic,if,call,float,bitwise,for-fold,smoke}.ss
+build/typed/llvmir/              disposable generated artifacts (.ll/.bc/.o)
+```
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index 7989d91..54c2315 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -310,6 +310,12 @@ Code that has landed:
   `Bytes`, same-module record/variant handles, Option/Result opaque handles,
   explicit handle drops, stale/double-drop rejection, and guardian-backed
   reaping through `%typed-rust-reap-handles!`.
+- LLVM IR backend (experimental, scalar-only): deterministic textual `.ll`
+  from the same elaborated core IR — `Nat`/`Int`/`Bool`/`Float`, `let`,
+  `if` with phi joins, direct same-module calls, bitwise/shifts,
+  `for/fold`, a smoke `@main` convention, LLVM verifier/`-O2` targets, and
+  an observed-behavior parity check against the Rust backend. See
+  `docs/llvmir-backend.md`.
 - Fixtures/smokes: `tests/fixtures/typed/rust-basic.ss`,
   `tests/fixtures/typed/valid-split-tree.ss`, `make typed-wrapper-smoke`, and
   `make typed-split-tree-smoke`.