Emit explicit per-module use declarations in typed Rust

ober

de4279a79529036ff41cda39b50615af0052d52a

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index 59ebcaf..8ca2abf 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -113,20 +113,21 @@ Done in subsequent phases:
   `/* source: path:line:column */` block comment so the compiler-reported Rust
   line can be mapped back to typed source.
 - Per-module file layout. The crate emitter now writes a thin `src/lib.rs`
-  that `pub mod`s each module and `pub use`s its items at the crate root, plus
-  one `src/<module>.rs` per typed module containing that module's records,
-  variants, resources, defs, and ABI wrappers. Runtime helpers
-  (`jt_return_bytes`, `jt_clone_handle`, panic capture, the handle registry)
-  live in `lib.rs` with `pub(crate)` visibility so per-module files can
-  reach them via `use crate::*;`. Cross-module bare-name calls keep working
-  because of the crate-root `pub use` re-exports.
+  that `pub mod`s each module, plus one `src/<module>.rs` per typed module
+  containing that module's records, variants, resources, defs, and ABI
+  wrappers. Runtime helpers (`jt_return_bytes`, `jt_clone_handle`, panic
+  capture, the handle registry) live in `lib.rs` with `pub(crate)` visibility.
+- Explicit per-module `use` declarations driven by the typed `import` graph.
+  Each per-module file imports exactly the runtime helpers it uses
+  (`use crate::{jt_capture_panic, jt_return_bytes, ...};`) and exactly the
+  sibling modules its source declared via `(import (other-mod))`
+  (`use crate::other_mod::*;`). No wildcard `use crate::*;` and no
+  crate-root `pub use module::*;` re-exports.
 
 Still open:
 
 - Direct `rustc` / static binary integration polish and eventual LLVM parity
   tests. Today the build goes through `cargo build` only.
-- Explicit per-module `use` declarations driven by the typed `import` graph
-  (today every module imports `crate::*;`).
 
 ## Output Layout
 
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index a708a14..58d5576 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -302,10 +302,11 @@ Highest-value next steps:
    elaboration, so cross-module record/variant/def references compile into a
    single Rust crate. `typed-modules->rust-crate-files` now writes one
    `src/<module>.rs` per typed module plus a thin `src/lib.rs` that
-   `pub mod`s and `pub use`s each module so cross-module bare-name calls
-   still resolve via the crate root. Explicit per-module `use` declarations
-   driven by the typed `import` graph (instead of `use crate::*;`) remain
-   future work.
+   `pub mod`s each module. Each per-module file emits explicit
+   `use crate::{...helpers...};` for the runtime helpers it actually uses
+   and `use crate::<other_mod>::*;` for every module its source declared via
+   `(import (other-mod))`, so the typed import graph drives the Rust use
+   graph one-for-one.
 3. Improve boundary semantics for Option/Result (Done). `(Option <Scalar>)`
    for Scalar in `{Bool, Char, Int, Nat, Fixnum, Float}` crosses the FFI
    boundary as a tagged Scheme value (`#f` or `(cons 'some V)`): the Rust ABI
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index d8790ba..e7e68b5 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -1706,20 +1706,44 @@
           (string-append "pub mod " (module-abi-prefix module) ";")))
       modules)
     (unless (null? modules) (newline port))
-    (for-each
-      (lambda (module)
-        (write-line port 0
-          (string-append "pub use " (module-abi-prefix module) "::*;")))
-      modules)
-    (unless (null? modules) (newline port))
     (emit-runtime-helpers modules port))
 
+  (def (module-name->prefix name)
+    (join-strings (map rust-symbol-name name) "_"))
+
+  (def (module-runtime-helpers-used module)
+    ;; Returns a sorted list of crate-level helper symbol names that this
+    ;; module's emitted Rust body actually references. Used to drive precise
+    ;; `use crate::{...};` declarations in per-module files.
+    (let ([safe? (module-has-safe-exports? module)]
+          [bytes? (module-needs-byte-buffer-runtime? module)]
+          [handles? (module-needs-handle-registry? module)])
+      (let ([out '()])
+        (when handles?
+          (set! out (cons "jt_clone_handle" out))
+          (set! out (cons "jt_store_handle" out)))
+        (when bytes?
+          (set! out (cons "jt_return_bytes" out)))
+        (when safe?
+          (set! out (cons "jt_capture_panic" out)))
+        out)))
+
   (def (emit-module-file module ir-env port)
     (write-line port 0 "// Generated by Jerboa's typed Rust backend. Do not edit.")
     (write-line port 0 "#![allow(unused_imports)]")
     (newline port)
-    (write-line port 0 "use crate::*;")
-    (newline port)
+    (let ([helpers (module-runtime-helpers-used module)]
+          [imports (typed-module-imports module)])
+      (unless (null? helpers)
+        (write-line port 0
+          (string-append "use crate::{" (join-strings helpers ", ") "};")))
+      (for-each
+        (lambda (import-name)
+          (write-line port 0
+            (string-append "use crate::" (module-name->prefix import-name) "::*;")))
+        imports)
+      (unless (and (null? helpers) (null? imports))
+        (newline port)))
     (parameterize ([*rust-ir-env* ir-env])
       (emit-module-declarations module port)))
 
diff --git a/tests/test-typed-rust.ss b/tests/test-typed-rust.ss
index 9d22ce1..3cd1ec5 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -612,21 +612,26 @@
 (test "rust crate lib.rs declares each module"
   (let ([lib (assoc-string "src/lib.rs" crate-files)])
     (and (substring? lib "pub mod rust_import_provider;")
-         (substring? lib "pub mod rust_import_consumer;")
-         (substring? lib "pub use rust_import_provider::*;")
-         (substring? lib "pub use rust_import_consumer::*;")))
+         (substring? lib "pub mod rust_import_consumer;")))
+  #t)
+
+(test "rust crate lib.rs does not blanket re-export module items"
+  (let ([lib (assoc-string "src/lib.rs" crate-files)])
+    (and (not (substring? lib "pub use rust_import_provider::*;"))
+         (not (substring? lib "pub use rust_import_consumer::*;"))))
   #t)
 
 (test "rust crate provider module file holds provider defs"
   (let ([provider (assoc-string "src/rust_import_provider.rs" crate-files)])
-    (and (substring? provider "use crate::*;")
+    (and (not (substring? provider "use crate::*;"))
          (substring? provider "pub fn inc(x: u64) -> u64")
          (substring? provider "pub struct Pt {")))
   #t)
 
-(test "rust crate consumer module file calls into provider via bare names"
+(test "rust crate consumer module file imports its provider explicitly"
   (let ([consumer (assoc-string "src/rust_import_consumer.rs" crate-files)])
-    (and (substring? consumer "use crate::*;")
+    (and (substring? consumer "use crate::rust_import_provider::*;")
+         (not (substring? consumer "use crate::*;"))
          (substring? consumer "pub fn bump_pair_x(p: Pt) -> Pt")
          (substring? consumer "inc(")))
   #t)