Source locations: reader positions, error locations, native stack traces (#218)
* Reader records source line/column on list forms The reader stamps 1-based :line/:column metadata on every list form (plus :file when load-jolt-file is reading a file), and jolt.host/form-position reads it back so the analyzer's :pos scaffold finally gets real data. A left-to-right cursor counts newlines over the delta between successive forms, so it stays O(n). Vector/map/set literals are untouched (their metadata is a runtime value the analyzer would have to wrap in with-meta); empty () can't carry meta. ^meta now merges onto the position keys instead of clobbering them. Re-mint is byte-identical (the backend doesn't emit :pos), so this is a pure scaffold for the error-location work that follows. * Report source location on uncaught errors Each top-level form records its source position (thread-local) before it compiles+evals, and cli.ss jolt-report-uncaught appends 'at file:line:col' when an error propagates out. Covers joltc -e, joltc run <file>, and load-string — every interpreted path. Top-level granularity, one set per form; deeper frames come from the Phase 2 frame walk. Runtime .ss only, no re-mint. * Clojure stack traces via source registry + native frame walk A direct-link build emits (jolt-register-source! short-name ns name file line) once per fn def — at definition time, so zero per-call cost. On an uncaught error the reporter walks Chez's native continuation frames (jolt-throw captures the live continuation via call/cc; host conditions carry their own &continuation), maps each frame's procedure name through the registry, and prints a Clojure backtrace 'ns/name (file:line)'. Wired into both the cli and a built binary's launcher. Frames are keyed by the short munged fn name Chez actually reports (emit-fn's letrec self-binding), not jv$ns$name; a cross-namespace collision degrades to the bare frame name rather than a wrong attribution. The analyzer carries the original form's position through defn macroexpansion onto the def node. Calling a non-fn now throws a catchable ClassCastException (via jolt-throw) naming the operator, instead of a raw Chez error. Caveats (documented in source-registry.ss): names map only in direct-link/AOT closed-world builds — the open-world -e/repl/run path falls back to the top-level location; and pervasive TCO erases tail-call frames, so a mapped trace shows only the non-tail spine. JOLT_DEBUG_FRAMES dumps raw frame names. Re-mint (analyzer + backend); prelude byte-identical (direct-link off during mint). Corpus rows certified, build-smoke asserts the trace. * Propagate source position through macroexpansion hc-expand-1 now carries the macro call form's :line/:column onto the top of a list expansion that has none of its own (merged under any meta the macro set), so errors and stack traces in macro-generated code point at the call site — Clojure parity. The analyze recursion re-expands inner macros, so each level's top form picks it up, matching the reference compiler. (meta (macroexpand-1 '(when x y))) now reports the call-site line. A direct-link fn defined through a user macro (build-app's defguarded) registers with a real line, so build-smoke's trace assertion covers macro-defined fns. Runtime .ss (host-contract.ss) — no re-mint; selfhost holds. Phase 3's optional items are deferred: :line-in-ex-data has no clean consumer (it would pollute ex-data, break = and printing, and positions already surface via the trace + top-level location), and Chez source-object emission is a large backend change the jv$-name registry already sidesteps. * Review fixes: registration key, thread-locals, debug flag timing - Register a fn under the name Chez actually reports for its frame, not the def name: a named fn literal whose name differs from the def (def foo (fn bar …)) is framed as 'bar', and an anonymous fn def (def foo (fn …)) as jv$ns$foo. Both previously registered under the def name and so never appeared in traces. - rdr-source-file / rdr-pos-cursor are thread parameters, so concurrent compiles (futures, core.async) don't clobber each other's file/line attribution. - Read JOLT_DEBUG_FRAMES at call time: a built binary evaluates top-level forms at heap-build time, where a load-time getenv is always unset. Re-mint (backend + reader); prelude byte-identical, selfhost holds. --------- Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
parent
bdf436e242
commit
8180c85393
18 changed files with 449 additions and 122 deletions
|
|
@ -119,8 +119,22 @@
|
|||
(define (hc-inst-source x) (jolt-get x hc-kw-form))
|
||||
(define (hc-uuid-source x) (jolt-get x hc-kw-form))
|
||||
|
||||
;; The Chez reader does not record source offsets yet.
|
||||
(define (hc-form-position x) jolt-nil)
|
||||
;; Source position for a list form: the reader stamps :line/:column (+ :file when
|
||||
;; compiling a file) into the form's metadata. Return a clean {:line :column
|
||||
;; :file?} map, or nil for a synthetic/macro-built form that carries none.
|
||||
(define hc-kw-line (keyword #f "line"))
|
||||
(define hc-kw-column (keyword #f "column"))
|
||||
(define hc-kw-file (keyword #f "file"))
|
||||
(define (hc-form-position x)
|
||||
(let ((m (jolt-meta x)))
|
||||
(if (and (pmap? m) (not (jolt-nil? (jolt-get m hc-kw-line))))
|
||||
(let ((line (jolt-get m hc-kw-line))
|
||||
(col (jolt-get m hc-kw-column))
|
||||
(file (jolt-get m hc-kw-file)))
|
||||
(if (jolt-nil? file)
|
||||
(jolt-hash-map hc-kw-line line hc-kw-column col)
|
||||
(jolt-hash-map hc-kw-line line hc-kw-column col hc-kw-file file)))
|
||||
jolt-nil)))
|
||||
|
||||
;; --- special forms ----------------------------------------------------------
|
||||
;; Mirrors host_iface special-names + interop-head? — forms the analyzer marks
|
||||
|
|
@ -170,12 +184,31 @@
|
|||
;; of the list), and the analyzer re-analyzes the returned form.
|
||||
(define (hc-macro? ctx sym)
|
||||
(macro-var? (hc-resolve-cell ctx sym)))
|
||||
;; Clojure parity: a macro expansion inherits the call form's source position, so
|
||||
;; errors/traces in macro-generated code point at the macro call site. Carry it
|
||||
;; onto the top of a LIST expansion (code) that has none of its own — merged under
|
||||
;; any meta the macro set, leaving collection literals (runtime data) alone. The
|
||||
;; recursion through analyze re-expands inner macros, so each level's top form
|
||||
;; picks up the position the same way (as the reference compiler does).
|
||||
(define (hc-propagate-pos src dst)
|
||||
(if (and (cseq? dst) (cseq-list? dst))
|
||||
(let ((sp (hc-form-position src))
|
||||
(dm (jolt-meta dst)))
|
||||
(if (and (pmap? sp)
|
||||
(or (jolt-nil? dm) (jolt-nil? (jolt-get dm hc-kw-line))))
|
||||
(jolt-with-meta dst
|
||||
(if (pmap? dm)
|
||||
(pmap-fold sp (lambda (k v acc) (jolt-assoc1 acc k v)) dm)
|
||||
sp))
|
||||
dst))
|
||||
dst))
|
||||
|
||||
(define (hc-expand-1 ctx form)
|
||||
(let* ((items (seq->list form))
|
||||
(head (car items))
|
||||
(args (cdr items))
|
||||
(expander (var-cell-root (hc-resolve-cell ctx head))))
|
||||
(apply jolt-invoke expander args)))
|
||||
(hc-propagate-pos form (apply jolt-invoke expander args))))
|
||||
|
||||
;; Classify a global (non-local) symbol reference against the var registry:
|
||||
;; {:kind :var :ns NS :name NAME} — a defined var (compile ns / clojure.core)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue