feat: precise file:line:col source locations for type errors (jolt-fqy)

RFC 0006 error reporting wanted file:line:col but IR nodes carried no
position, so diagnostics read only "type error in <ns>: <msg>". Now:

    type error /tmp/scene.clj:5:5: `inc` requires a number, but argument 1 is a string

The reader records each LIST form's absolute start offset in a table keyed
by form identity (lists are fresh arrays, never interned), gated behind a
flag the loaders enable only when JOLT_TYPE_CHECK is on — zero cost off.
Keying by identity makes positions survive macroexpansion exactly when the
user's own sub-form is spliced through, and absent for macro-synthesized
structure: a `(inc :k)` written inside `(when c ...)` reports at its own
line, never at the expansion's generated if/do.

The analyzer stamps the offset onto :invoke nodes (form-position host
contract fn); the checker carries it into each diagnostic as :pos; the
loaders stash the file's source + path on the env (save/restored across
nested requires); backend/type-check! converts offset -> line:col via the
reader's line-col and renders the RFC format. Falls back to the ns when no
position is available (synthetic forms), so it is never worse than before.

Gate green, conformance 335/335, suite 4718, runtime bench even (positions
are compile-time only; off by default).
This commit is contained in:
Yogthos 2026-06-13 12:18:53 -04:00
parent 824b30defd
commit f2d65addc8
9 changed files with 106 additions and 17 deletions

View file

@ -23,7 +23,7 @@
form-map-pairs form-set-items form-special? compile-ns
form-macro? form-expand-1 resolve-global
form-sym-meta host-intern! form-syntax-quote-lower
record-type?]]))
record-type? form-position]]))
(declare analyze)
@ -250,8 +250,13 @@
(and (form-sym? head) (not shadowed) (form-macro? ctx head))
(analyze ctx (form-expand-1 ctx form) env)
:else
(invoke (analyze ctx head env)
(mapv #(analyze ctx % env) (rest items))))))))
;; stamp the list form's source offset onto the :invoke (jolt-fqy)
;; so the success checker can report file:line:col. nil when the
;; reader did not record it (synthetic/macro-built forms).
(let [n (invoke (analyze ctx head env)
(mapv #(analyze ctx % env) (rest items)))
p (form-position form)]
(if p (assoc n :pos p) n)))))))
(defn analyze
([ctx form] (analyze ctx form (empty-env)))