Tree-shake the clojure.core prelude too (whole-program DCE)
--tree-shake now shakes the clojure.core/stdlib prelude in the same reachability graph as the app + libraries — only core fns actually reached from -main ship. dce-blob-records reads prelude.ss with Chez `read`, unwraps each (guard ... (def-var! "ns" "name" V)) and builds the core->core call graph from the (var-deref/jolt-var "ns" "name") refs in V — the real emitted edges, no re-analysis. bld-shake-all merges core + app records into one BFS; roots are -main, side-effecting forms, and the clojure.core fns the runtime .ss shims reference by name (enumerated in dce-runtime-core-roots). The shaken core is spliced where the prelude.ss blob was, in original (tier) order, so load-time deps are preserved. Bail (reachable runtime resolve) keeps the prelude whole. Soundness follows Stalin's rule (any reference, value or call, keeps a def live): dce-collect-refs counts :var and :the-var; non-def registration forms are always kept (covers protocol/multimethod dispatch). Validated by make shakesmoke: the four deps.edn git-lib apps build byte-identical output shaken vs not. Wins (binary): build-app 9.84MB -> 8.12MB (dropped 403/457 core defs); malli-app 10.0MB -> 8.1MB; markdown 9.9 -> 8.3; commonmark 9.8 -> 8.1 — all output-identical. build-smoke asserts an unused core fn (group-by) is dropped; full make test green.
This commit is contained in:
parent
f7fd73c448
commit
298ac66fb1
3 changed files with 96 additions and 38 deletions
|
|
@ -122,6 +122,50 @@
|
|||
"clojure.core/load-string" "clojure.core/load-file" "clojure.core/load-reader"
|
||||
"clojure.core/load"))
|
||||
|
||||
;; clojure.core fns the runtime .ss shims reference by name (via var-deref) — they
|
||||
;; aren't visible in the IR call graph, so seed them as roots. (Found by grepping the
|
||||
;; runtime shims; the smoke harness catches a miss as a diff/crash.)
|
||||
(define dce-runtime-core-roots
|
||||
'("clojure.core/identity" "clojure.core/isa?" "clojure.core/line-seq"
|
||||
"clojure.core/make-hierarchy" "clojure.core/read" "clojure.core/read-string"
|
||||
"clojure.core/read+string" "clojure.core/realized?" "clojure.core/reset!"))
|
||||
|
||||
;; --- reading a minted blob (prelude.ss) into DCE records --------------------
|
||||
;; The prelude is a flat list of (guard CLAUSE (def-var! "ns" "name" V)) forms (+ the
|
||||
;; occasional side-effecting init). Read each with Chez `read` so it joins the
|
||||
;; reachability graph instead of being baked wholesale: a def-var! is a prunable node
|
||||
;; whose core->core edges are the (var-deref/jolt-var "ns" "name") calls in V; any
|
||||
;; other form is non-prunable (kept, refs are roots).
|
||||
(define (dce-unwrap form)
|
||||
(if (and (pair? form) (eq? (car form) 'guard) (pair? (cddr form))) (caddr form) form))
|
||||
|
||||
(define (dce-sexp-refs form acc)
|
||||
(cond
|
||||
((and (pair? form) (memq (car form) '(var-deref jolt-var))
|
||||
(pair? (cdr form)) (string? (cadr form)) (pair? (cddr form)) (string? (caddr form)))
|
||||
(cons (string-append (cadr form) "/" (caddr form)) acc))
|
||||
((pair? form) (dce-sexp-refs (cdr form) (dce-sexp-refs (car form) acc)))
|
||||
(else acc)))
|
||||
|
||||
;; Records (same shape as ei-emit-ns-records) for a minted blob file. str re-serializes
|
||||
;; the read form (compiled identically; comments/whitespace are irrelevant).
|
||||
(define (dce-blob-records path)
|
||||
(call-with-input-file path
|
||||
(lambda (p)
|
||||
(let loop ((acc '()))
|
||||
(let ((form (read p)))
|
||||
(if (eof-object? form)
|
||||
(reverse acc)
|
||||
(let ((b (dce-unwrap form))
|
||||
(str (with-output-to-string (lambda () (write form))))
|
||||
(refs (dce-sexp-refs form '())))
|
||||
(loop (cons
|
||||
(if (and (pair? b) (eq? (car b) 'def-var!) (pair? (cdr b)) (string? (cadr b))
|
||||
(pair? (cddr b)) (string? (caddr b)))
|
||||
(vector #f (string-append (cadr b) "/" (caddr b)) refs str)
|
||||
(vector #t #f refs str))
|
||||
acc)))))))))
|
||||
|
||||
;; A reference that needs the analyzer/back end at runtime (compile-from-source). If
|
||||
;; reachable code uses none of these, the compiler image can be dropped from the
|
||||
;; binary — the AOT app is fully compiled. (resolve/require don't need it: resolve is
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue