Skip to content

Provide configurable tree shaking - #891

Draft
statonjr wants to merge 9 commits into
jolt-lang:mainfrom
statonjr:feat/tree-shake-allow-dynamic
Draft

Provide configurable tree shaking#891
statonjr wants to merge 9 commits into
jolt-lang:mainfrom
statonjr:feat/tree-shake-allow-dynamic

Conversation

@statonjr

@statonjr statonjr commented Sep 8, 2026

Copy link
Copy Markdown

deps.edn can now vouch for a runtime var lookup so jolt build --tree-shake proceeds past it:

:jolt/tree-shake {:allow-dynamic [clojure.spec.alpha/res
                                  clojure.spec.gen.alpha/dynaload]}

The key is read from the app's deps.edn and every library's, where :jolt/native is read, and unioned. A def in the set is skipped by the bail scan. Nothing is kept on its behalf. With no key, the shake bails exactly as before.

When the shake still bails, the message lists each site once and ends with the paste-ready key:

jolt build: tree-shake skipped (reachable code resolves vars at runtime):
  clojure.spec.alpha/res -> clojure.core/resolve
to proceed, if these never run in the built binary, add to deps.edn:
  :jolt/tree-shake {:allow-dynamic [clojure.spec.alpha/res]}

Real-world check, the hello world from the issue with spec.alpha 0.5.238: bails with the two sites the issue names; with the pasted key it shakes, drops the compiler image, and s/describe still prints int?. Binary 30.7MB -> 17.4MB (13.3MB, 43%).

Fixes #890

…aped fixture

The spec.alpha hello world that motivates the key needs a git dep, and the
shakelocal fixtures run in CI without one, so the fixture reproduces spec's
two shapes in miniature: an app's res behind a path -main never takes and a
:local/root library's dynaload behind a delay, each vouched for by its own
deps.edn. The partial fixture adds one caller nothing vouches for and must
still bail with a hint naming that caller alone, which is the only way to
show an allowed site was honored without letting a non-allowed one through.
Both callers are ^:redef because the inline pass would otherwise splice them
into their callers and the bail would name the caller instead.
…ast it

A reachable resolve/eval-family reference bails the tree-shake, and it must:
the static graph cannot follow a runtime name. But some of those sites are
dead in a built binary and the author can prove it — spec.alpha's res and
spec.gen's dynaload are the motivating pair, and one s/def is enough to lose
the whole shake. dce-bail-scan now takes an allow set of def fqns and skips
those defs in both the bail scan and the compiler-needed scan: a site vouched
never to run needs no compiler, and skipping only the bail scan would let an
allowed eval caller shake without dropping the image, breaking the no-bail-
drops-compiler invariant shakelocal asserts. Nothing is kept on an allowed
def's behalf. The bail diagnostic lists each site once (the IR+text ref union
had it printing every line twice) and ends with the paste-ready deps.edn key
for every def it named, so the path from skipped to kept is one paste.
jolt.host/build-binary already carries its cross-compile target as trailing
optional arguments, so the allow list rides the same way (index 2) rather
than widening the positional contract every caller would have to learn. An
absent or nil option is the empty list, which is today's behaviour exactly.
… path

A library knows its own dynamic sites; the app that pulls it in does not. So
the key is read where :jolt/native is read — each dependency's deps.edn in
resolve-deps, the project's own in resolve-project — and unioned, project
first, so a library ships its list once and an app can still vouch for a
library that has not. Entries travel as ns/name strings because that is the
shape the Scheme bail scan keys on.
The driver takes it as the third trailing option, after the cross-compile
target and pack, so an app that declares nothing passes an empty vector and
builds exactly as before. This turns the allow-dynamic fixtures green:
verified by mutation that dropping either the app's or the library's
declaration bails the shaking fixture again.
…nit level

The README says when the key is safe to add, that a library can ship it, and
that the name to paste is the one the bail prints. The changelog records why
an allowed def skips the compiler scan too, because that is the one choice a
reader could reasonably have made the other way. The deps unit gate now
resolves the committed shake fixture and asserts both declarations arrive,
project first: the build-level gate can show the shake ran, not which
declaration got it there.
The header said bail scan only, which is the half of the decision a reader
landing here first would take as the whole: an allowed eval caller also
skips the compiler-needed scan, and dce-bail-scan explains why.
@statonjr
statonjr marked this pull request as draft September 8, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add configurable tree shake

1 participant