Emit Typed Jerboa source-span comments in Rust output

ober

0d267a6126e1604164ae81816126578c6f64592d

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index a1f5f98..5ebadd6 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -79,26 +79,47 @@ Landed:
   wrong-type handles, expose explicit handle drops, and use a Chez guardian
   reaper as a cleanup safety net.
 
-Still open:
+Done in subsequent phases:
 
-- (Done) Typed core IR adoption. The IR record types live in
-  `(jerboa typed core)`, the checker elaborates def bodies into IR via
+- Typed core IR adoption. The IR record types live in `(jerboa typed core)`,
+  the checker elaborates def bodies into IR via
   `check-and-elaborate-typed-module`, and the Rust emitter
   (`typed-module->rust-string`, `typed-modules->rust-crate-string`, and their
-  library-form siblings) now consumes the IR through `*rust-ir-env*`. Future
-  rust.ss work can prune the surface fall-back paths once the wrapper
-  generator follows.
-- Import resolution between typed modules.
-- Structured Rust-to-Scheme error returns instead of conservative panic
-  defaults.
-- Direct Scheme conversions for Option/Result instead of opaque handles.
-- Resource `#:close` lowering and Rust `Drop` integration.
-- Borrow-aware lowering. Current generated Rust clones liberally.
-- Generated Rust source comments tied to typed source spans. Checker
-  diagnostics now carry expression-level `(path,line,column)` triples through
-  `typed-check-error-source`; the Rust emitter does not yet thread that into
-  generated `// source: ...` comments.
-- Direct `rustc`/static integration polish and eventual LLVM parity tests.
+  library-form siblings) consumes the IR through `*rust-ir-env*`.
+- Import resolution between typed modules. The parser accepts
+  `(import (module name) ...)` and the checker walks topologically ordered
+  module lists; `typed-modules->rust-crate-string` /
+  `typed-library-forms->rust-crate-string` emit a single multi-module crate.
+- Structured Rust-to-Scheme error returns. Every safe export wraps its body in
+  `catch_unwind`; panic payloads route through `jt_capture_panic` into a
+  thread-local that wrappers drain via `%typed-rust-check-panic!`, surfacing
+  Rust panics as Scheme errors whose irritants carry the panic message.
+- Direct Scheme conversions for Option and Result instead of opaque handles.
+  `(Option <Scalar>)`, `(Option String)`, `(Option Bytes)`, and `(Result T E)`
+  across every scalar/buffer combination now cross the boundary as tagged
+  Scheme values (`#f` / `(cons 'some V)` / `(cons 'ok V)` / `(cons 'err E)`).
+- Resource `#:close` lowering and Rust `Drop` integration. Resource
+  declarations lower to opaque `pub struct Name { pub _opaque: () }` placeholders
+  and `(resource Name #:close fn)` additionally emits an `impl Drop` that
+  delegates to the close hook.
+- Borrow-aware lowering for explicit `(Borrow T)` / `(MutBorrow T)` parameters.
+  Borrow params lower to `&T` / `&mut T` and call sites emit `&(expr)` /
+  `&mut (expr)` instead of `.clone()`. Owned values are still cloned at
+  ordinary `T` parameter positions (conservative).
+- Generated Rust source comments tied to typed source spans. Records, variants,
+  resources, and defs emit `// source: path:line:column` comments above each
+  declaration when the parsed datum carries a source-location.
+
+Still open:
+
+- Per-module file layout (`src/modules/<module>.rs`). The current emitter
+  inlines all modules into a single `lib.rs`.
+- Direct `rustc` / static binary integration polish and eventual LLVM parity
+  tests. Today the build goes through `cargo build` only.
+- Expression-level source comments inside function bodies (only the
+  declaration headers carry source spans today).
+- Cross-module `use` declarations and per-module Rust files for richer
+  workspaces.
 
 ## Output Layout
 
@@ -517,15 +538,22 @@ unsafe {
 
 ## Source Locations
 
-Generated Rust should preserve source locations in comments:
+Generated Rust preserves source locations in comments above each declaration
+when the typed parser was given an annotated datum (typically from
+`jerboa-read-file`):
 
 ```rust
 // source: src/jerboa-emacs/typed/rope.tss:42:3
 pub fn rope_insert(...) -> Rope { ... }
 ```
 
-When Rust compilation fails, the typed compiler should try to map errors back
-to Typed Jerboa source. In the MVP, readable generated Rust is enough.
+Current landing: records, variants, resources, and defs each emit a
+`// source: path:line:column` comment above their generated declaration. The
+checker already threads spans through `typed-check-error-source`, so future
+work can wire expression-level spans into intra-body comments as well as
+richer source maps. When Rust compilation fails, a developer can map the
+reported file to the typed source through these comments. Mapping cargo
+errors directly back to typed source line and column remains future work.
 
 ## Build Integration
 
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index 6abb2eb..cf09e91 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -18,6 +18,8 @@
 
   (import (chezscheme) ; jerboa-security: suppress direct-chezscheme-import-user-code -- trusted typed compiler Rust emitter
           (only (jerboa core) def)
+          (only (jerboa reader) source-location? source-location-path
+                source-location-line source-location-column)
           (jerboa typed parser)
           (jerboa typed checker)
           (jerboa typed core))
@@ -69,6 +71,21 @@
     (display text port)
     (newline port))
 
+  (def (source-location->rust-comment source)
+    (and (source-location? source)
+         (string-append
+           "// source: "
+           (or (source-location-path source) "<unknown>")
+           ":"
+           (number->string (source-location-line source))
+           ":"
+           (number->string (source-location-column source)))))
+
+  (def (write-source-comment port level source)
+    (let ([text (source-location->rust-comment source)])
+      (when text
+        (write-line port level text))))
+
   (def rust-keywords
     '("as" "break" "const" "continue" "crate" "else" "enum" "extern"
       "false" "fn" "for" "if" "impl" "in" "let" "loop" "match" "mod"
@@ -313,6 +330,7 @@
       (thunk)))
 
   (def (emit-record record port)
+    (write-source-comment port 0 (typed-record-source record))
     (write-line port 0 "#[derive(Clone, Debug, PartialEq)]")
     (write-line port 0
       (string-append "pub struct " (rust-symbol-name (typed-record-name record)) " {"))
@@ -354,6 +372,7 @@
          (write-line port 1 "},")])))
 
   (def (emit-variant variant port)
+    (write-source-comment port 0 (typed-variant-source variant))
     (write-line port 0 "#[derive(Clone, Debug, PartialEq)]")
     (write-line port 0
       (string-append "pub enum " (rust-symbol-name (typed-variant-name variant)) " {"))
@@ -1557,6 +1576,7 @@
             (cond
               [ir-entry (emit-expression (cdr ir-entry))]
               [else (emit-begin (strip-source-annotations (typed-def-body def)))])])
+      (write-source-comment port 0 (typed-def-source def))
       (write-line port 0
         (string-append
           "pub fn "
@@ -1573,6 +1593,7 @@
   (def (emit-resource resource port)
     (let ([name (rust-symbol-name (typed-resource-name resource))]
           [close (typed-resource-close resource)])
+      (write-source-comment port 0 (typed-resource-source resource))
       (write-line port 0 "#[derive(Debug)]")
       (write-line port 0 (string-append "pub struct " name " {"))
       (write-line port 1 "pub _opaque: (),")
diff --git a/tests/test-typed-rust.ss b/tests/test-typed-rust.ss
index a1efdaf..e00fe8f 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -2,6 +2,7 @@
 ;;; Tests for (jerboa typed rust)
 
 (import (chezscheme)
+        (jerboa reader)
         (jerboa typed rust))
 
 (define pass 0)
@@ -551,6 +552,29 @@
        (substring? import-crate-rust "(p).y"))
   #t)
 
+(define annotated-form
+  (car (jerboa-read-file "tests/fixtures/typed/rust-basic.ss")))
+
+(define annotated-rust
+  (typed-library-form->rust-string annotated-form))
+
+(test "rust emits source-span comments above defs"
+  (and (substring? annotated-rust "// source: tests/fixtures/typed/rust-basic.ss:")
+       (substring? annotated-rust ":15:")
+       (substring? annotated-rust "pub fn zero() -> u64"))
+  #t)
+
+(test "rust emits source-span comments above records and variants"
+  (and (substring? annotated-rust "// source: tests/fixtures/typed/rust-basic.ss:8:")
+       (substring? annotated-rust "pub struct Box {")
+       (substring? annotated-rust "// source: tests/fixtures/typed/rust-basic.ss:11:")
+       (substring? annotated-rust "pub enum Token {"))
+  #t)
+
+(test "rust omits source-span comment when datum has no source"
+  (not (substring? calc-rust "// source:"))
+  #t)
+
 (printf "~%Typed Rust emitter: ~a passed, ~a failed~%" pass fail)
 (when (> fail 0)
   (exit 1))