typed-rust: topologically sort modules by import graph
ober
02ac22e50f3f8e158f7e788e8a55a144060a6123
--- 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 --- 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 --- 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 () --- 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))