Compare commits

...

5 commits
v0.1.4 ... main

Author SHA1 Message Date
0598f8e7c5 Allowed the boot script to check multiple names for the chez executable.
Some checks failed
tests / test (push) Has been cancelled
2026-07-06 17:09:51 +01:00
Dmitri Sotnikov
855fbc4794
Merge pull request #306 from jolt-lang/trace-source-lines
JOLT_TRACE: map tail-frame history to ns/name (file:line)
2026-07-04 21:20:16 +00:00
Yogthos
6c88198115 JOLT_TRACE: map tail-frame history to ns/name (file:line)
The eval path recorded only a frame's munged name, so a JOLT_TRACE backtrace was
a list of bare names. Register source for a runtime-compiled fn def when tracing
is on (keyed by the same munged name the entry push records), reusing the
source-registry the renderer already maps to "ns/name (file:line)". Direct-link
builds already registered via emit-def-cached; this covers the open-world eval
path. trace-off output is byte-identical (returns "" — seed mint / `jolt build`
unchanged), seed re-minted. A name shared across namespaces (e.g. -main) stays
bare, the existing ambiguity guard.

smoke asserts a file-backed project run maps a frame to ns/name (file:line).
2026-07-04 17:09:44 -04:00
Dmitri Sotnikov
e297a74501
Merge pull request #305 from jolt-lang/fix-jolt-trace-aot-binary
JOLT_TRACE: honor the env at runtime in a built joltc
2026-07-04 20:51:03 +00:00
Yogthos
c8167e1c05 JOLT_TRACE: honor the env at runtime in a built joltc
The JOLT_TRACE opt-in was a top-level form in compile-eval.ss, so in a
self-contained joltc it ran at heap-build time — where JOLT_TRACE is always
unset — and never at runtime. `JOLT_TRACE=1 joltc -M:run` therefore produced no
trace from the distributed binary (it worked only under the source-loaded dev
launcher). REPL/nREPL tracing was unaffected (those enable at runtime).

Make it a jolt-trace-init-from-env! fn called from the runtime entrypoints — the
cli.ss dispatch and the built-joltc launcher — before any app namespace compiles,
so the app's own code is traced. While here, drop a redundant trace print in the
joltc launcher (jolt-report-throwable already emits it) that double-printed the
block once tracing actually produced one.

joltc-selfbuild-smoke asserts JOLT_TRACE=1 through the built binary yields exactly
one tail-frame trace.
2026-07-04 16:40:14 -04:00
8 changed files with 172 additions and 96 deletions

View file

@ -14,7 +14,28 @@
# JOLT_PWD. # JOLT_PWD.
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
export JOLT_PWD="${JOLT_PWD:-$PWD}" export JOLT_PWD="${JOLT_PWD:-$PWD}"
# Identify the Chez Scheme executable
while read -r CHEZ
do
if [ `which ${CHEZ}` ]
then
break;
fi
done <<EOF
chez
chezscheme
EOF
# If we failed to find one, whinge and exit.
if [ ! `which ${CHEZ}` ]
then
echo "No valid Chez Scheme executable found: please install Chez Scheme."
exit 1
fi
# Version for --version / banners: git describe of this checkout, else "dev". # Version for --version / banners: git describe of this checkout, else "dev".
export JOLT_VERSION="${JOLT_VERSION:-$(git -C "$root" describe --tags --always --dirty 2>/dev/null || echo dev)}" export JOLT_VERSION="${JOLT_VERSION:-$(git -C "$root" describe --tags --always --dirty 2>/dev/null || echo dev)}"
cd "$root" || exit 1 cd "$root" || exit 1
exec chez --script host/chez/cli.ss "$@" exec ${CHEZ} --script host/chez/cli.ss "$@"

View file

@ -145,11 +145,10 @@
(scheme-start (scheme-start
(lambda args (lambda args
(set-source-roots! (list \"jolt-core\" \"stdlib\")) (set-source-roots! (list \"jolt-core\" \"stdlib\"))
(guard (v (#t (jolt-report-throwable v (current-error-port)) ;; JOLT_TRACE at RUNTIME (the env is unset at heap-build), before any app ns
(let ((bt (jolt-backtrace-string v))) ;; compiles, so a `-M:run` traces the app's own code.
(when bt (display \" trace:\\n\" (current-error-port)) (jolt-trace-init-from-env!)
(display bt (current-error-port)))) (guard (v (#t (jolt-report-throwable v (current-error-port)) (exit 1)))
(exit 1)))
(cond (cond
((and (= (length args) 2) (string=? (car args) \"-e\")) ((and (= (length args) 2) (string=? (car args) \"-e\"))
(let ((result (jolt-final-str (let ((result (jolt-final-str

View file

@ -66,6 +66,9 @@
(when bt (display " trace:\n" port) (display bt port))) (when bt (display " trace:\n" port) (display bt port)))
(exit 1))) (exit 1)))
;; JOLT_TRACE opt-in, at runtime (before any app ns compiles) so the app is traced.
(jolt-trace-init-from-env!)
(guard (v (#t (jolt-report-uncaught v))) (guard (v (#t (jolt-report-uncaught v)))
(cond (cond
;; -e EXPR — evaluate one expression and print it (blank for nil). Wrapped in ;; -e EXPR — evaluate one expression and print it (blank for nil). Wrapped in

View file

@ -132,11 +132,16 @@
;; push is baked in at compile time, only code compiled after this call is traced — ;; push is baked in at compile time, only code compiled after this call is traced —
;; which is exactly the code you eval / reload in a live session. ;; which is exactly the code you eval / reload in a live session.
(def-var! "jolt.host" "enable-trace!" jolt-enable-trace!) (def-var! "jolt.host" "enable-trace!" jolt-enable-trace!)
;; Explicit opt-in for a whole run (JOLT_TRACE=1): enable at load, BEFORE any app ;; Explicit opt-in for a whole run (JOLT_TRACE=1): turn tracing on BEFORE any app
;; namespace is compiled, so a plain `-M:run` traces the app's own code too. Only an ;; namespace is compiled, so a plain `-M:run` traces the app's own code too. Called
;; affirmative value (set, non-empty, not falsey) forces it on here. ;; from the runtime entrypoints (cli.ss, and the built joltc launcher) — NOT at load
;; time: a built joltc runs top-level forms at heap-build time, where JOLT_TRACE is
;; always unset, so a load-time check would never see the user's runtime env. Only an
;; affirmative value (set, non-empty, not falsey) forces it on.
(define (jolt-trace-init-from-env!)
(let ((e (getenv "JOLT_TRACE"))) (let ((e (getenv "JOLT_TRACE")))
(when (and e (fx>? (string-length e) 0) (not (jolt-trace-env-off? e))) (jolt-enable-trace!))) (when (and e (fx>? (string-length e) 0) (not (jolt-trace-env-off? e)))
(jolt-enable-trace!))))
;; (with-meta sym m) -> sym, else x — an (ns ^:no-doc name …) yields the name with ;; (with-meta sym m) -> sym, else x — an (ns ^:no-doc name …) yields the name with
;; reader metadata as a with-meta form; strip it to read the bare ns symbol. ;; reader metadata as a with-meta form; strip it to read the bare ns symbol.

View file

@ -42,6 +42,20 @@ if [ "$got_e" != "45" ]; then
exit 1 exit 1
fi fi
# 2b. JOLT_TRACE must take effect in the BUILT binary. The env check runs at
# runtime (the launcher), NOT at heap-build where JOLT_TRACE is always unset — so
# an uncaught error shows a tail-frame trace recovering the TCO-elided chain, and
# exactly ONE trace block (the launcher must not double-print it).
got_tr="$(env -i HOME="$HOME" JOLT_TRACE=1 "$joltc" -e '(defn a [x] (+ x 1)) (defn b [x] (a x)) (b :x)' 2>&1)"
if ! printf '%s' "$got_tr" | grep -q ' trace:' || ! printf '%s' "$got_tr" | grep -q 'b'; then
echo " FAIL: JOLT_TRACE=1 in the built joltc produced no tail-frame trace"
echo "--- got ---"; echo "$got_tr"; exit 1
fi
if [ "$(printf '%s' "$got_tr" | grep -c ' trace:')" != "1" ]; then
echo " FAIL: built joltc double-printed the trace block"
echo "--- got ---"; echo "$got_tr"; exit 1
fi
# 3. Build an app through the distributed joltc with an EMPTY environment — no # 3. Build an app through the distributed joltc with an EMPTY environment — no
# PATH at all, so no chez, no cc, no shell tools are reachable. This is the core # PATH at all, so no chez, no cc, no shell tools are reachable. This is the core
# guarantee: joltc compiles apps entirely on its own. # guarantee: joltc compiles apps entirely on its own.

File diff suppressed because one or more lines are too long

View file

@ -124,6 +124,21 @@ if printf '%s' "$err_stale" | grep -q 'h1'; then
else else
pass=$((pass + 1)) pass=$((pass + 1))
fi fi
# A file-backed project run maps each runtime-compiled frame to ns/name (file:line)
# — the eval path registers source in trace mode, so the trace isn't bare names.
tr_proj="$(mktemp -d)"
mkdir -p "$tr_proj/src/tp"
printf '{:paths ["src"] :aliases {:run {:main-opts ["-m" "tp.core"]}}}\n' > "$tr_proj/deps.edn"
printf '(ns tp.core)\n(defn deep [x] (+ x 1))\n(defn mid [x] (inc (deep x)))\n(defn -main [& _] (mid :nan))\n' > "$tr_proj/src/tp/core.clj"
tr_out="$(JOLT_TRACE=1 JOLT_PWD="$tr_proj" bin/joltc -M:run 2>&1)"
if printf '%s' "$tr_out" | grep -Eq 'tp\.core/deep \(.*/tp/core\.clj:2\)'; then
pass=$((pass + 1))
else
echo " FAIL: JOLT_TRACE trace should map a frame to ns/name (file:line)"
printf '%s\n' "$tr_out" | sed 's/^/ | /'
fails=$((fails + 1))
fi
rm -rf "$tr_proj"
# --help prints usage, and lists the nREPL server under its real flag name. # --help prints usage, and lists the nREPL server under its real flag name.
help_out="$(bin/joltc --help 2>/dev/null)" help_out="$(bin/joltc --help 2>/dev/null)"

View file

@ -790,6 +790,21 @@
(returns-scheme-bool? (:body node) bools')) (returns-scheme-bool? (:body node) bools'))
:else false))) :else false)))
;; In trace mode, a fn def also registers its source so the tail-frame history maps
;; the recorded frame-name to "ns/name (file:line)" instead of a bare name. Keyed by
;; the SAME munged name the entry push records (emit-fn's letrec self-binding = the
;; fn's own name). Returns "" when off / not a positioned fn def, so trace-off output
;; (seed mint, `jolt build`) is byte-identical. Direct-link builds already register
;; via emit-def-cached; this covers the open-world eval path.
(defn- trace-source-reg [node]
(let [init (:init node) pos (:pos node)]
(if (and @trace-frames? (= :fn (:op init)) (:name init) pos)
(str " (jolt-register-source! " (chez-str-lit (munge-name (:name init))) " "
(chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " "
(if (:file pos) (chez-str-lit (:file pos)) "jolt-nil") " "
(or (:line pos) 0) ")")
"")))
(defn emit* [node] (defn emit* [node]
(case (:op node) (case (:op node)
:const (emit-const (:val node)) :const (emit-const (:val node))
@ -889,7 +904,8 @@
:fn (emit-fn node) :fn (emit-fn node)
;; (def name) with no init (declare): reserve the cell. A def with non-empty ;; (def name) with no init (declare): reserve the cell. A def with non-empty
;; reader metadata lowers to def-var-with-meta! (ported in a later increment). ;; reader metadata lowers to def-var-with-meta! (ported in a later increment).
:def (cond :def (let [reg (trace-source-reg node)
d (cond
(:no-init node) (:no-init node)
(str "(declare-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) ")") (str "(declare-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) ")")
(jmeta-nonempty? (:meta node)) (jmeta-nonempty? (:meta node))
@ -897,7 +913,8 @@
(emit-with-cells #(emit (:init node))) " " (emit-def-meta node) ")") (emit-with-cells #(emit (:init node))) " " (emit-def-meta node) ")")
:else :else
(str "(def-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " " (str "(def-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " "
(emit-with-cells #(emit (:init node))) ")")) (emit-with-cells #(emit (:init node))) ")"))]
(if (= reg "") d (str "(begin " d reg ")")))
(throw (ex-info (str "emit: op not yet ported / unhandled: " (pr-str (:op node))) {})))) (throw (ex-info (str "emit: op not yet ported / unhandled: " (pr-str (:op node))) {}))))
;; ^:dynamic / ^:redef on a def opts it out of direct-linking: it stays redefinable, ;; ^:dynamic / ^:redef on a def opts it out of direct-linking: it stays redefinable,