typed-rust: topologically sort modules by import graph

ober

02ac22e50f3f8e158f7e788e8a55a144060a6123

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index 0a89380..5bdef8c 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -179,6 +179,13 @@ Done in subsequent phases:
   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.
+- Import-order independence. The crate orchestrator topologically sorts modules
+  by their import graph before checking and emitting, so a module may be listed
+  (or `make typed-rust`'d) before the sibling it imports — the checker's
+  registry, which only sees previously-listed modules, still resolves the
+  import. Cycles bail for the offending node and surface as a check error rather
+  than looping. This is what lets a real multi-module port (e.g. jsecmon's
+  `dga` importing `strbytes`) build regardless of file order.
 
 - Static binary integration via `make binary-typed`. The build sets
   `JERBOA_TYPED_RUST_ARCHIVE` so `support/build-binary.sh` `-force_load`s
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index 48e323f..7989d91 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -256,9 +256,14 @@ Current landing:
   combine, range copy, and lowercase hex encode without in-place mutation
   (`tests/fixtures/typed/rust-bytes-build.ss`). `debug-string` checks one typed
   operand and lowers to Rust `format!("{:?}", ...)`, using the Debug derives on
-  generated records and variants. Imported calls and richer forms are reported
-  as unsupported. Typed core IR is now produced by the checker and consumed by
-  the Rust emitter; imports and LLVM emission are still TODO.
+  generated records and variants. Typed core IR is now produced by the checker
+  and consumed by the Rust emitter. Cross-module imports between typed libraries
+  work: a `(typed-library …)` may `(import (other module name))` and call the
+  other module's exported defs, records, and variants; each module becomes its
+  own Rust file with a precise `use crate::<other>::*;`, and the orchestrator
+  topologically sorts modules by import so source/argument order is irrelevant
+  (a module may be listed before the one it imports). Importing non-typed
+  (stdlib) modules and LLVM emission are still TODO.
 - `Option` and `Result` type expressions now have explicit checked
   constructors in the front end: `(option-some expr)`, `(option-none Type)`,
   `(result-ok expr ErrorType)`, and `(result-err ValueType expr)`. Rust
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index d82ebea..5d8ad82 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -1860,10 +1860,37 @@
     (parameterize ([*rust-ir-env* ir-env])
       (emit-module-declarations module port)))
 
-  (def (typed-modules->rust-crate-files modules)
+  (def (topo-sort-modules modules)
+    ;; Order modules so every module's imported modules that are part of this
+    ;; batch precede it. The checker's registry only sees previously listed
+    ;; modules (check-and-elaborate-typed-modules), so without this a module
+    ;; that imports one later in the list would fail to resolve -- which made
+    ;; correctness depend on file/argument order. Imports referring to modules
+    ;; outside the batch are left alone (the checker reports them). A cycle
+    ;; bails for the offending node, leaving the import unresolved for the
+    ;; checker to flag rather than looping forever.
+    (let ([by-name (map (lambda (m) (cons (typed-module-name m) m)) modules)]
+          [visited '()]
+          [out '()])
+      (define (visit name path)
+        (cond
+          [(member name visited) (void)]
+          [(member name path) (void)] ;; cycle: bail, checker will flag it
+          [else
+           (let ([entry (assoc name by-name)])
+             (when entry
+               (for-each (lambda (imp) (visit imp (cons name path)))
+                         (typed-module-imports (cdr entry)))
+               (set! visited (cons name visited))
+               (set! out (cons (cdr entry) out))))]))
+      (for-each (lambda (m) (visit (typed-module-name m) '())) modules)
+      (reverse out)))
+
+  (def (typed-modules->rust-crate-files modules0)
     ;; Returns list of (relative-path . content) pairs describing the crate's
     ;; src/ tree: a thin src/lib.rs plus one src/<module>.rs per typed module.
-    (let ([ir-envs (elaborate-modules-or-error modules
+    (let* ([modules (topo-sort-modules modules0)]
+           [ir-envs (elaborate-modules-or-error modules
                      'typed-modules->rust-crate-files)])
       (with-rust-env modules
         (lambda ()
diff --git a/tests/test-typed-rust.ss b/tests/test-typed-rust.ss
index b98aa25..fd5478a 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -840,6 +840,22 @@
          (not (substring? consumer "fn jt_return_bytes"))))
   #t)
 
+;; Topological ordering: the consumer imports the provider, so the crate must
+;; build even when the consumer is listed *first* -- the checker's registry
+;; only sees previously listed modules, so the orchestrator has to reorder.
+(define reversed-crate-files
+  (typed-library-forms->rust-crate-files
+    (list import-consumer-form import-provider-form)))
+
+(test "rust crate builds regardless of source order (topo-sorted imports)"
+  (and (substring? (assoc-string "src/rust_import_consumer.rs" reversed-crate-files)
+                   "pub fn bump_pair_x(p: Pt) -> Pt")
+       (substring? (assoc-string "src/rust_import_consumer.rs" reversed-crate-files)
+                   "use crate::rust_import_provider::*;")
+       (substring? (assoc-string "src/rust_import_provider.rs" reversed-crate-files)
+                   "pub fn inc(x: u64) -> u64"))
+  #t)
+
 (printf "~%Typed Rust emitter: ~a passed, ~a failed~%" pass fail)
 (when (> fail 0)
   (exit 1))