Refactor phase 3b: keep :try IR nodes structs, not phms (jolt-26dm)
analyze-try assoc'd :catch-sym/:catch-body/:finally nil-when-absent, so a try
with no catch (or no finally) carried a nil-valued key — which makes the node a
phm in jolt's map representation and forces the back end to densify it
(norm-node) before reading :op. That's the map-nil-representation trap Phase 2
already cleaned up for def/fn/arity nodes. Add those keys only when the clause is
present, matching the arity :rest discipline; a try node stays a fast struct.
Behavior-invisible: emit-try reads each key with a nil-safe (node :k) and gates
on it, so an absent key and a present-nil key are indistinguishable to every
consumer. Adds ir-try-shape-test asserting the node shape across all four
try/catch/finally combinations plus end-to-end eval.
Note on scope: the plan's "delete the defensive norm-node calls" is NOT done — it
can't be. {:op :const :val nil} (e.g. (def x nil)) and nil map keys are
inherently phm, so the emit-dispatch norm-node guards a real case, not a
present-or-absent artifact. This PR removes a source of gratuitous phm nodes
rather than the densification itself. Full gate green.
This commit is contained in:
parent
61e4bbf2de
commit
130a6c5c4d
2 changed files with 59 additions and 6 deletions
|
|
@ -167,12 +167,22 @@
|
|||
(= hname "finally")
|
||||
(reset! finally-body (rest (vec (form-elements c))))
|
||||
:else (swap! body conj c))))
|
||||
{:op :try
|
||||
:body (analyze-seq ctx @body env)
|
||||
:catch-sym @catch-sym
|
||||
:catch-body (when @catch-body
|
||||
(analyze-seq ctx @catch-body (add-locals env [@catch-sym])))
|
||||
:finally (when @finally-body (analyze-seq ctx @finally-body env))}))
|
||||
;; Add :catch-sym/:catch-body/:finally ONLY when present (same discipline as
|
||||
;; the arity :rest key above). Assoc'ing them nil-when-absent would give the
|
||||
;; node a nil-valued key, which makes it a phm in jolt's map representation
|
||||
;; and forces the back end to densify it (norm-node) before reading :op — the
|
||||
;; map-nil-representation trap Phase 2 cleaned up for def/fn/arity nodes. The
|
||||
;; back end reads each key with a nil-safe (node :k) and gates on it, so an
|
||||
;; absent key is indistinguishable from a present-nil one.
|
||||
(let [n {:op :try :body (analyze-seq ctx @body env)}
|
||||
n (if @catch-body
|
||||
(assoc n :catch-sym @catch-sym
|
||||
:catch-body (analyze-seq ctx @catch-body (add-locals env [@catch-sym])))
|
||||
n)
|
||||
n (if @finally-body
|
||||
(assoc n :finally (analyze-seq ctx @finally-body env))
|
||||
n)]
|
||||
n)))
|
||||
|
||||
(defn- analyze-special [ctx op items env]
|
||||
(case op
|
||||
|
|
|
|||
43
test/integration/ir-try-shape-test.janet
Normal file
43
test/integration/ir-try-shape-test.janet
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# IR shape hygiene for :try nodes (phase 3b, jolt-26dm). analyze-try used to
|
||||
# assoc :catch-sym/:catch-body/:finally nil-when-absent, which made the node a
|
||||
# phm (jolt's nil-valued-key map representation) and forced backend densification
|
||||
# before every :op read. It now adds those keys only when the clause is present
|
||||
# — same discipline as the arity :rest key — so a try node stays a fast struct.
|
||||
# The change is behavior-invisible (the back end reads each key nil-safely and
|
||||
# gates on it), so we also pin that tries still evaluate correctly.
|
||||
(import ../../src/jolt/api :as api)
|
||||
(import ../../src/jolt/backend :as backend)
|
||||
(import ../../src/jolt/reader :as reader)
|
||||
|
||||
(print "IR :try shape...")
|
||||
(def ctx (api/init-cached {:compile? true}))
|
||||
(defn try-node [src] (backend/analyze-form ctx (reader/parse-string src)))
|
||||
|
||||
(defn check-struct [src]
|
||||
(def n (try-node src))
|
||||
(assert (= :try (n :op)) (string src " is a :try node"))
|
||||
# a struct (nil-free) — NOT a phm; phm nodes carry a :jolt/deftype tag
|
||||
(assert (struct? n) (string src " analyzes to a struct, not a phm"))
|
||||
(assert (nil? (get n :jolt/deftype)) (string src " is not a phm")))
|
||||
|
||||
# no catch, no finally — neither optional key should appear
|
||||
(check-struct "(try 1)")
|
||||
# finally only — :catch-sym/:catch-body must be ABSENT, not nil
|
||||
(def fin (try-node "(try 1 (finally 2))"))
|
||||
(assert (struct? fin) "(try .. finally) is a struct")
|
||||
(assert (nil? (get fin :catch-body)) "no catch-body when there is no catch")
|
||||
(assert (get fin :finally) "finally present")
|
||||
# catch only
|
||||
(def cat (try-node "(try 1 (catch Throwable e 2))"))
|
||||
(assert (struct? cat) "(try .. catch) is a struct")
|
||||
(assert (get cat :catch-sym) "catch-sym present")
|
||||
(assert (nil? (get cat :finally)) "no finally when there is none")
|
||||
# catch + finally
|
||||
(check-struct "(try 1 (catch Throwable e 2) (finally 3))")
|
||||
|
||||
# behavior unchanged across all shapes
|
||||
(assert (= 1 (api/eval-string ctx "(try 1)")) "try value")
|
||||
(assert (= 2 (api/eval-string ctx "(try (throw (ex-info \"x\" {})) (catch Throwable e 2))")) "catch value")
|
||||
(assert (= 7 (api/eval-string ctx "(let [a (atom 0)] (try (reset! a 7) (finally nil)) @a)")) "finally runs")
|
||||
(assert (= 2 (api/eval-string ctx "(try (throw (ex-info \"x\" {})) (catch Throwable e 2) (finally 9))")) "catch+finally")
|
||||
(print "IR :try shape passed!")
|
||||
Loading…
Add table
Add a link
Reference in a new issue