diff --git a/Makefile b/Makefile index 3873183..2cd029b 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # build step. `make test` is the full gate. `make remint` rebuilds the seed after a # source change. -.PHONY: test ci values corpus unit smoke buildsmoke selfhost sci certify ffi transient infer directlink numeric inline remint +.PHONY: test ci values corpus unit smoke buildsmoke selfhost sci certify ffi transient infer directlink numeric inline shakesmoke remint # Full gate (dev machine). Includes the self-host byte-fixpoint, which only holds # on the same Chez that minted the seed. @@ -77,6 +77,12 @@ numeric: inline: @chez --script test/chez/inline-test.ss +# Tree-shake soundness: build example apps (incl. deps.edn git-lib apps) default vs +# --tree-shake and require identical output. Slow (two builds per app); not in the +# default gate. Skips without the examples repo / Chez kernel dev files. +shakesmoke: + @sh host/chez/tree-shake-smoke.sh + # JVM oracle: certify the corpus against reference Clojure. Skips if clojure absent. certify: @if command -v clojure >/dev/null 2>&1; then \ diff --git a/host/chez/emit-image.ss b/host/chez/emit-image.ss index 2fece8f..0080a89 100644 --- a/host/chez/emit-image.ss +++ b/host/chez/emit-image.ss @@ -93,17 +93,22 @@ ;; only the re-emitted namespaces are shaken. (define dce-kw-op (keyword #f "op")) (define dce-kw-var (keyword #f "var")) +(define dce-kw-the-var (keyword #f "the-var")) (define dce-kw-def (keyword #f "def")) (define dce-kw-ns (keyword #f "ns")) (define dce-kw-name (keyword #f "name")) (define dce-reduce-children (var-deref "jolt.ir" "reduce-ir-children")) -;; "ns/name" of every :var reference anywhere in node, prepended to acc. Arg order -;; (acc node) matches reduce-ir-children's fold fn, so it nests directly. +;; "ns/name" of every var reference anywhere in node, prepended to acc. Counts BOTH +;; a :var (call head or value) and a :the-var (#'x / (var x)) — per Stalin's rule, +;; any reference (not just a direct call) keeps the target live, so a var passed as a +;; value or referenced as #'x is reachable. Arg order (acc node) matches +;; reduce-ir-children's fold fn, so it nests directly. (define (dce-collect-refs acc node) - (if (eq? (jolt-get node dce-kw-op) dce-kw-var) - (cons (string-append (jolt-get node dce-kw-ns) "/" (jolt-get node dce-kw-name)) acc) - (dce-reduce-children dce-collect-refs acc node))) + (let ((op (jolt-get node dce-kw-op))) + (if (or (eq? op dce-kw-var) (eq? op dce-kw-the-var)) + (cons (string-append (jolt-get node dce-kw-ns) "/" (jolt-get node dce-kw-name)) acc) + (dce-reduce-children dce-collect-refs acc node)))) ;; The fqn of a bare top-level def (the only prunable form), else #f. (define (dce-def-fqn node) diff --git a/host/chez/tree-shake-smoke.sh b/host/chez/tree-shake-smoke.sh new file mode 100644 index 0000000..e8102ed --- /dev/null +++ b/host/chez/tree-shake-smoke.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# Tree-shake soundness smoke: build each example app twice — default and +# --tree-shake — run both, and require identical output. A wrongly-dropped def +# (incl. a core fn once core-shaking lands) shows up as a diff or a crash. Covers a +# pure-compute app and several that pull libraries via deps.edn (the key risk). +# +# Skips (like build-smoke) when the example repo or the Chez kernel dev files / +# C compiler aren't available. Slow (two full binary builds per app); not in the +# default gate — run with `make shakesmoke`. +root="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" +cd "$root" +examples="$root/../examples" +[ -d "$examples" ] || examples="$HOME/src/jolt-lang/examples" +if [ ! -d "$examples" ]; then echo "shake smoke: skipped (examples repo not found)"; exit 0; fi + +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" ]; then + echo "shake smoke: skipped (Chez kernel dev files or C compiler not available)"; exit 0 +fi +export JOLT_CHEZ_CSV="$csv" + +tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT +fail=0 + +# app-dir | main-ns | args +run_case() { + app="$examples/$1"; ns="$2"; args="$3" + [ -d "$app" ] || { echo " - $1: skipped (not present)"; return; } + b0="$tmp/$1-plain"; b1="$tmp/$1-shake" + if ! JOLT_PWD="$app" bin/joltc build -m "$ns" -o "$b0" >/dev/null 2>&1; then + echo " - $1: FAIL (default build)"; fail=1; return; fi + if ! JOLT_PWD="$app" bin/joltc build -m "$ns" -o "$b1" --tree-shake >/dev/null 2>&1; then + echo " - $1: FAIL (--tree-shake build)"; fail=1; return; fi + o0="$(cd "$app" && "$b0" $args 2>&1)" + o1="$(cd "$app" && "$b1" $args 2>&1)" + if [ "$o0" != "$o1" ]; then + echo " - $1: FAIL (output differs default vs --tree-shake)" + echo " --- default ---"; echo "$o0" | head -5 + echo " --- shake -----"; echo "$o1" | head -5 + fail=1; return + fi + s0="$(wc -c < "$b0")"; s1="$(wc -c < "$b1")" + echo " - $1: ok (output identical; $((s0/1024))K -> $((s1/1024))K)" +} + +# Library apps (deps.edn git deps) with deterministic stdout — the key risk is that +# tree-shaking a binary that pulled libraries drops a reachable lib (or, later, core) +# fn. A timing/benchmark app (e.g. ray-tracer) is unsuitable: its output varies. +echo "shake smoke: building each app default vs --tree-shake (output must match)" +run_case markdown-app app.core "" +run_case malli-app app.core "" +run_case commonmark-app app.core "" +run_case hiccup-app app.core "" + +[ "$fail" = 0 ] && echo "shake smoke: passed" || echo "shake smoke: FAILED" +exit $fail