* 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>
119 lines
4.9 KiB
Bash
Executable file
119 lines
4.9 KiB
Bash
Executable file
#!/bin/sh
|
|
# build smoke: `jolt build` compiles a multi-namespace app (macro + cross-ns +
|
|
# clojure.string) into a standalone binary, which then runs with no jolt source
|
|
# or Chez install on the path — args reach -main, output matches.
|
|
root="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
|
cd "$root"
|
|
|
|
# Preflight: a standalone build needs Chez's kernel dev files (libkernel.a +
|
|
# scheme.h) and a C compiler. A distro chezscheme package ships neither, so on
|
|
# such hosts (CI included) skip — like `certify` skips without Clojure. Pin the
|
|
# csv dir we validate so the build uses exactly it.
|
|
csv="$JOLT_CHEZ_CSV"
|
|
if [ -z "$csv" ]; then
|
|
chez_bin="$(command -v chez || command -v scheme || command -v petite || true)"
|
|
if [ -n "$chez_bin" ]; then
|
|
base="$(cd "$(dirname "$chez_bin")/.." 2>/dev/null && pwd)"
|
|
for d in "$base"/lib/csv*/*/; do
|
|
[ -f "${d}libkernel.a" ] && csv="${d%/}" && break
|
|
done
|
|
fi
|
|
fi
|
|
if ! command -v cc >/dev/null 2>&1 || [ -z "$csv" ] || [ ! -f "$csv/scheme.h" ] || [ ! -f "$csv/libkernel.a" ]; then
|
|
echo "build smoke: skipped (Chez kernel dev files or C compiler not available)"
|
|
exit 0
|
|
fi
|
|
export JOLT_CHEZ_CSV="$csv"
|
|
|
|
app="$root/test/chez/build-app"
|
|
out="$(mktemp -d)/app-bin"
|
|
trap 'rm -rf "$(dirname "$out")"' EXIT
|
|
|
|
echo "build smoke: compiling app.core -> $out"
|
|
if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" >/dev/null 2>&1; then
|
|
echo " FAIL: jolt build exited non-zero"
|
|
exit 1
|
|
fi
|
|
[ -x "$out" ] || { echo " FAIL: no executable produced"; exit 1; }
|
|
|
|
# Run from a neutral cwd with args. The first line is an embedded resource
|
|
# (deps.edn :jolt/build :embed), proving io/resource resolves from the binary with
|
|
# no resources/ dir on disk; the rest exercise a macro, cross-ns, and args.
|
|
got="$(cd / && "$out" alpha bb ccc 2>&1)"
|
|
want='embedded resource ok
|
|
HELLO FROM A BUILT BINARY!
|
|
HELLO FROM A BUILT BINARY!
|
|
args: [alpha bb ccc]
|
|
sum: 10
|
|
greet-default: greet:default
|
|
greet-loud: greet:loud
|
|
greet-soft: greet:soft'
|
|
if [ "$got" != "$want" ]; then
|
|
echo " FAIL: binary output mismatch"
|
|
echo "--- want ---"; echo "$want"
|
|
echo "--- got ----"; echo "$got"
|
|
exit 1
|
|
fi
|
|
|
|
# Optimized mode (inference + flatten + scalar-replace) must produce the same
|
|
# result — a sanity check that the passes don't miscompile this app.
|
|
if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" --opt >/dev/null 2>&1; then
|
|
echo " FAIL: jolt build --opt exited non-zero"; exit 1
|
|
fi
|
|
got_opt="$(cd / && "$out" alpha bb ccc 2>&1)"
|
|
if [ "$got_opt" != "$want" ]; then
|
|
echo " FAIL: --opt binary output mismatch"
|
|
echo "--- got ----"; echo "$got_opt"
|
|
exit 1
|
|
fi
|
|
|
|
# Closed-world direct-linking (opt-in): same result, and the cross-namespace call
|
|
# (app.core -> app.util/shout) must lower to a direct jv$ binding, not var-deref.
|
|
if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" --direct-link >/dev/null 2>&1; then
|
|
echo " FAIL: jolt build --direct-link exited non-zero"; exit 1
|
|
fi
|
|
got_dl="$(cd / && "$out" alpha bb ccc 2>&1)"
|
|
if [ "$got_dl" != "$want" ]; then
|
|
echo " FAIL: --direct-link binary output mismatch"
|
|
echo "--- got ----"; echo "$got_dl"
|
|
exit 1
|
|
fi
|
|
if ! grep -q '(jv\$app.util\$shout' "$out.build/flat.ss"; then
|
|
echo " FAIL: --direct-link did not emit a direct app->app call"; exit 1
|
|
fi
|
|
# A direct-link build registers fn sources, so an uncaught throw prints a Clojure
|
|
# stack trace mapping each native frame back to ns/name (file:line).
|
|
if ! grep -q 'jolt-register-source!' "$out.build/flat.ss"; then
|
|
echo " FAIL: --direct-link did not emit source registrations"; exit 1
|
|
fi
|
|
boom_err="$(cd / && "$out" --boom 2>&1 >/dev/null)"
|
|
for frame in 'app.util/deep-boom' 'app.util/mid-boom' 'app.core/-main'; do
|
|
if ! printf '%s' "$boom_err" | grep -q "$frame"; then
|
|
echo " FAIL: stack trace missing frame $frame"
|
|
echo "--- got ----"; echo "$boom_err"
|
|
exit 1
|
|
fi
|
|
done
|
|
# Tree-shaking (opt-in): same result, and an unreachable def (the `twice` macro,
|
|
# expanded at AOT and never called at runtime) is dropped.
|
|
if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" --tree-shake >/dev/null 2>&1; then
|
|
echo " FAIL: jolt build --tree-shake exited non-zero"; exit 1
|
|
fi
|
|
got_ts="$(cd / && "$out" alpha bb ccc 2>&1)"
|
|
if [ "$got_ts" != "$want" ]; then
|
|
echo " FAIL: --tree-shake binary output mismatch"
|
|
echo "--- got ----"; echo "$got_ts"
|
|
exit 1
|
|
fi
|
|
if grep -q 'def-var! "app.util" "twice"' "$out.build/flat.ss"; then
|
|
echo " FAIL: --tree-shake did not drop the unreachable twice macro"; exit 1
|
|
fi
|
|
# The app never evals, so the compiler image (analyzer/back end) is dropped.
|
|
if grep -q 'def-var! "jolt.analyzer"' "$out.build/flat.ss"; then
|
|
echo " FAIL: --tree-shake kept the compiler image in a no-eval app"; exit 1
|
|
fi
|
|
# Core is shaken: a clojure.core overlay fn this app never uses is dropped.
|
|
if grep -q 'def-var! "clojure.core" "group-by"' "$out.build/flat.ss"; then
|
|
echo " FAIL: --tree-shake kept an unreachable clojure.core fn (group-by)"; exit 1
|
|
fi
|
|
echo "build smoke: passed (release + optimized + direct-link + tree-shake + compiler+core shake)"
|