Bake Typed Jerboa Rust staticlib into jerboa-bin

ober

34000b220f38cda641ff7bc078d258648f7c0cf3

diff --git a/Makefile b/Makefile
index 1e28e97..5a01744 100644
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,7 @@ TYPED_RUST_SOURCES ?= $(TYPED_SOURCES)
 TYPED_RUST_DIR ?= build/typed/rust
 TYPED_WRAPPER_DIR ?= build/typed/jerboa
 
-.PHONY: help chez chez-cross build binary binary-cross native-cross pure-audit typecheck typed-rust typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit-native clean security security-production security-profile fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push
+.PHONY: help chez chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit-native clean security security-production security-profile fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push
 
 help:
 	@echo "Usage: make <target>"
@@ -344,6 +344,34 @@ typed-split-tree-smoke:
 
 typed-test: test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-wrappers test-typed-fuzz typecheck
 
+# Build a jerboa-bin that bakes in a Typed Jerboa Rust .a archive. The
+# generated wrapper resolves its `jt_*` symbols via dlsym(RTLD_DEFAULT) — no
+# JERBOA_TYPED_RUST_LIB env var needed at runtime.
+#
+# Override:
+#   TYPED_RUST_SOURCES   — typed .ss source files (default: rust-basic fixture)
+#   TYPED_WRAPPER_DIR    — where the generated `.ss` wrapper is written
+#   BINARY_OUTPUT        — path of the resulting jerboa-bin binary
+binary-typed: chez build
+	@$(MAKE) --no-print-directory typed-build \
+	  TYPED_RUST_SOURCES="$(TYPED_RUST_SOURCES)" \
+	  TYPED_WRAPPER_DIR="$(TYPED_WRAPPER_DIR)"
+	@archive="$(TYPED_RUST_DIR)/target/debug/libjerboa_typed_generated.a"; \
+	  test -f "$$archive" || { echo "ERROR: typed Rust .a not found at $$archive" >&2; exit 1; }; \
+	  JERBOA_TYPED_RUST_ARCHIVE="$$archive" \
+	  SCHEME=$(SCHEME) JERBOA_CHEZ_PREFIX=$(CHEZ_PREFIX) \
+	  support/build-binary.sh $(BINARY_ENTRY) $(BINARY_OUTPUT)
+
+# Smoke test: prove the static-link path resolves `jt_*` symbols without
+# JERBOA_TYPED_RUST_LIB by running a tiny caller through the freshly built
+# binary.
+binary-typed-smoke:
+	@$(MAKE) --no-print-directory binary-typed \
+	  TYPED_RUST_SOURCES=tests/fixtures/typed/rust-basic.ss \
+	  TYPED_WRAPPER_DIR=build/typed/jerboa-binary-smoke \
+	  BINARY_OUTPUT=build/typed/jerboa-bin-typed
+	@unset JERBOA_TYPED_RUST_LIB; ./build/typed/jerboa-bin-typed tests/test-typed-binary-smoke.ss
+
 typed-clean:
 	@rm -rf build/typed
 
diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index 8ca2abf..cebd8b7 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -124,9 +124,17 @@ Done in subsequent phases:
   (`use crate::other_mod::*;`). No wildcard `use crate::*;` and no
   crate-root `pub use module::*;` re-exports.
 
+- Static binary integration via `make binary-typed`. The build sets
+  `JERBOA_TYPED_RUST_ARCHIVE` so `support/build-binary.sh` `-force_load`s
+  (Darwin) / `--whole-archive`s (Linux/BSD) the `.a` into the final
+  executable, and emits an autogenerated `typed_symbols.h` that registers
+  each `jt_*` export through Chez's `Sregister_symbol`. The wrapper then
+  resolves its foreign-procedure names from the binary's own symbol table —
+  no `JERBOA_TYPED_RUST_LIB` is needed at runtime.
+
 Still open:
 
-- Direct `rustc` / static binary integration polish and eventual LLVM parity
+- Direct `rustc` invocation (skipping `cargo build`) and eventual LLVM parity
   tests. Today the build goes through `cargo build` only.
 
 ## Output Layout
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index 58d5576..d96f2bb 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -1033,7 +1033,11 @@ Minimum excluded features:
 - Generate a Cargo crate. Initial `make typed-rust` / `make typed-build`
   targets landed for a primitive typed fixture.
 - Compile generated Rust as a static or dynamic library. Initial disposable
-  `rlib`/`staticlib`/`cdylib` builds landed.
+  `rlib`/`staticlib`/`cdylib` builds landed. The `make binary-typed` target
+  links the `staticlib` into the standalone Jerboa binary so generated
+  wrappers resolve their `jt_*` foreign-procedure names directly from the
+  binary's own symbol table — no `JERBOA_TYPED_RUST_LIB` env var needed at
+  runtime.
 - Generate conversion functions. Initial scalar argument checks, `Char`
   code-point conversions, `String` argument/return UTF-8 bytevector
   conversions, and `Bytes` argument/return bytevector conversions landed at the
diff --git a/support/build-binary.sh b/support/build-binary.sh
index 6993ea8..a4c6c5e 100755
--- a/support/build-binary.sh
+++ b/support/build-binary.sh
@@ -155,7 +155,7 @@ echo ""
 # ── Step 1: WPO-compile entry script -> program.so ───────────────────────────
 WPO_SO="${OUTPUT}.wp.so"
 OBJ_DIR=$(mktemp -d "/tmp/jerboa-bin-obj.XXXXXX")
-trap 'rm -rf "$OBJ_DIR" "$WPO_SO" petite_boot.h scheme_boot.h program_boot.h "${OUTPUT}-main.c"' EXIT
+trap 'rm -rf "$OBJ_DIR" "$WPO_SO" petite_boot.h scheme_boot.h program_boot.h typed_symbols.h "${OUTPUT}-main.c"' EXIT
 
 echo "==> [1/4] WPO compile"
 JERBOA_XPATCH="$JERBOA_XPATCH" "$SCHEME" --libdirs "$BINARY_LIBDIRS" \
@@ -180,6 +180,36 @@ embed() {
 embed "$CSV_DIR/petite.boot"  petite_boot
 embed "$CSV_DIR/scheme.boot"  scheme_boot
 embed "$WPO_SO"               program_boot
+
+# When baking a Typed Jerboa Rust staticlib, extract its jt_* exports and
+# generate a typed_symbols.h that pre-registers each one through Chez's
+# Sregister_symbol. Without this, Chez's foreign-procedure lookup only
+# scans objects added via (load-shared-object PATH) — which the generated
+# wrapper skips when JERBOA_TYPED_RUST_LIB is unset.
+TYPED_SYMBOLS_CFLAGS=""
+if [ -n "${JERBOA_TYPED_RUST_ARCHIVE:-}" ]; then
+    JT_SYMS=$(nm -g "$JERBOA_TYPED_RUST_ARCHIVE" 2>/dev/null \
+        | awk '/ T _?jt_[A-Za-z0-9_]+$/ {
+                   sym = $NF; sub(/^_/, "", sym);
+                   if (!seen[sym]++) print sym;
+               }' \
+        | sort)
+    if [ -n "$JT_SYMS" ]; then
+        {
+            echo '/* Generated by support/build-binary.sh — Sregister_symbol entries'
+            echo ' * for the Typed Jerboa Rust staticlib baked into this binary. */'
+            for s in $JT_SYMS; do
+                echo "extern int $s();"
+            done
+            echo 'static void jerboa_register_typed_symbols(void) {'
+            for s in $JT_SYMS; do
+                echo "    Sregister_symbol(\"$s\", (void *)$s);"
+            done
+            echo '}'
+        } > typed_symbols.h
+        TYPED_SYMBOLS_CFLAGS="-DJERBOA_HAS_TYPED_SYMBOLS=1"
+    fi
+fi
 echo ""
 
 # ── Step 3: Generate main.c ──────────────────────────────────────────────────
@@ -198,6 +228,9 @@ cat > "${OUTPUT}-main.c" <<'CMAIN'
 #include "petite_boot.h"
 #include "scheme_boot.h"
 #include "program_boot.h"
+#ifdef JERBOA_HAS_TYPED_SYMBOLS
+#include "typed_symbols.h"
+#endif
 
 static const char *write_program_tmpfile(void) {
     static char path[] = "/tmp/jerboa-prog-XXXXXX";
@@ -222,6 +255,9 @@ int main(int argc, const char *argv[]) {
     Sregister_boot_file_bytes("scheme",
         (void *)scheme_boot_data, scheme_boot_size);
     Sbuild_heap(NULL, NULL);
+#ifdef JERBOA_HAS_TYPED_SYMBOLS
+    jerboa_register_typed_symbols();
+#endif
 
     const char *prog_path = write_program_tmpfile();
     int status = Sscheme_program(prog_path, argc, argv);
@@ -243,12 +279,36 @@ for a in liblz4.a libz.a; do
     fi
 done
 
+# Optional: bake a Typed Jerboa Rust staticlib into the binary so generated
+# wrappers can resolve their `jt_*` symbols via dlsym(RTLD_DEFAULT) without
+# needing JERBOA_TYPED_RUST_LIB set at runtime.
+TYPED_RUST_LINK=""
+if [ -n "${JERBOA_TYPED_RUST_ARCHIVE:-}" ]; then
+    [ -f "$JERBOA_TYPED_RUST_ARCHIVE" ] || {
+        echo "ERROR: JERBOA_TYPED_RUST_ARCHIVE not found: $JERBOA_TYPED_RUST_ARCHIVE" >&2
+        exit 1
+    }
+    # The jt_* exports are made visible to Chez at runtime by way of
+    # Sregister_symbol calls emitted into main.c (see typed_symbols.h
+    # generation below); the linker just needs to actually drag the
+    # archive's object files into the executable.
+    case "$TARGET_OS" in
+        Darwin)
+            TYPED_RUST_LINK="-Wl,-force_load,$JERBOA_TYPED_RUST_ARCHIVE" ;;
+        Linux|FreeBSD|OpenBSD|NetBSD)
+            TYPED_RUST_LINK="-Wl,--whole-archive $JERBOA_TYPED_RUST_ARCHIVE -Wl,--no-whole-archive" ;;
+        *)
+            TYPED_RUST_LINK="$JERBOA_TYPED_RUST_ARCHIVE" ;;
+    esac
+fi
+
 # shellcheck disable=SC2086
-$CC -I"$CSV_DIR" -O2 $STATIC_CFLAGS \
+$CC -I. -I"$CSV_DIR" -O2 $STATIC_CFLAGS $TYPED_SYMBOLS_CFLAGS \
     -o "$OUTPUT" \
     "${OUTPUT}-main.c" \
     "$CSV_DIR/libkernel.a" \
     $EXTRA_ARCHIVES \
+    $TYPED_RUST_LINK \
     $OS_LIBS
 
 echo ""
diff --git a/tests/test-typed-binary-smoke.ss b/tests/test-typed-binary-smoke.ss
new file mode 100644
index 0000000..12a7809
--- /dev/null
+++ b/tests/test-typed-binary-smoke.ss
@@ -0,0 +1,59 @@
+#!chezscheme
+;;; Smoke test for the statically-baked Typed Jerboa Rust path.
+;;;
+;;; Invoked through `./build/typed/jerboa-bin-typed tests/test-typed-binary-smoke.ss`
+;;; by `make binary-typed-smoke`. The binary embeds the Typed Jerboa Rust .a, so
+;;; the generated wrapper must resolve its `jt_*` foreign-procedure bindings
+;;; through the binary's own symbol table — no JERBOA_TYPED_RUST_LIB needed.
+
+(import (chezscheme))
+
+;; The wrapper script imports (jerboa prelude), so the static binary needs to
+;; know where source libraries live. The binary itself has no baked-in libdirs
+;; — that's a deliberate design: users either set CHEZSCHEMELIBDIRS or have
+;; the entry script call (library-directories ...). For this smoke test we
+;; just point at the in-repo lib/ tree.
+(library-directories '("lib"))
+
+
+(define pass 0)
+(define fail 0)
+
+(define (check name ok?)
+  (cond
+    [ok?
+     (set! pass (+ pass 1))
+     (printf "  ok ~a~%" name)]
+    [else
+     (set! fail (+ fail 1))
+     (printf "FAIL ~a~%" name)]))
+
+(define wrapper-path
+  "build/typed/jerboa-binary-smoke/sample_typed_rust_basic.ss")
+
+(unless (file-exists? wrapper-path)
+  (printf "ERROR: wrapper not found at ~a~%" wrapper-path)
+  (exit 2))
+
+(check "JERBOA_TYPED_RUST_LIB is unset for this run"
+  (let ([env (getenv "JERBOA_TYPED_RUST_LIB")])
+    (or (not env) (string=? env ""))))
+
+(load wrapper-path)
+
+;; The wrapper exposes a `zero` (-> Nat) and an `add-one` (Nat -> Nat).
+;; Calling either proves dlsym(RTLD_DEFAULT) resolved the static `jt_*`
+;; symbols from the binary's own symbol table.
+(check "zero resolves and returns 0"
+  (= (zero) 0))
+
+(check "add-one resolves and adds"
+  (= (add-one 41) 42))
+
+(check "greeting resolves and returns a non-empty string"
+  (let ([g (greeting)])
+    (and (string? g) (> (string-length g) 0))))
+
+(printf "~%Typed binary static-link smoke: ~a passed, ~a failed~%" pass fail)
+(when (> fail 0)
+  (exit 1))