backend: densify phm IR nodes to structs at the emitter boundary

The back end reads IR node fields with raw (node :key) access, which assumes a
Janet struct. Once user maps preserve nil (coming), a node that carries a
nil-valued field — anonymous fn :name, nil const :val, def with no :meta, arity
with no :rest — is built as a phm instead, whose fields live under :buckets and
read back nil. norm-node densifies such a node via phm-to-struct, which drops
exactly those nil fields — precisely what the back end wants, since it already
treats an absent field as nil. Applied where a node first reaches the emitter
(emit entry, fn arities, invoke callee); structs pass through untouched.

No representation change yet, so this is a no-op today: full suite, conformance
218/218 x3, fixpoint, fib all unchanged. Keeps IR access a host-layer concern
so jolt.ir/jolt.analyzer stay portable.
This commit is contained in:
Yogthos 2026-06-06 22:00:21 -04:00
parent bd76eddb74
commit 22ff737f8b

View file

@ -13,6 +13,19 @@
(import ./compiler :as comp)
(use ./evaluator)
(import ./reader :as r)
(import ./phm :as phm)
# The IR is portable data; reading its representation is a host-layer concern.
# Most nodes are Janet structs (raw-readable), but a node carrying a nil-valued
# field — an anonymous fn's :name, a nil const's :val, a def with no :meta, an
# arity with no :rest — is a phm, whose fields live under :buckets, not as direct
# keys. Densify such a node to a struct: phm-to-struct drops exactly those
# nil-valued fields, which is what the back end wants (it already treats an absent
# field as nil). Structs (the common case) pass through untouched. Applied at the
# few points where a node first reaches the emitter, so the rest of the back end
# keeps using plain (node :key) access and the portable front end never sees this.
(defn- norm-node [n]
(if (phm/phm? n) (phm/phm-to-struct n) n))
# Var late-binding: reads go through `(var-get cell)` with the cell embedded as a
# constant, so compiled code sees redefinition (Janet early-binds plain symbols)
@ -120,7 +133,7 @@
core))
(defn- emit-fn-body [ctx node]
(def arities (vview (node :arities)))
(def arities (map norm-node (vview (node :arities))))
(def multi (> (length arities) 1))
(cond
# Single fixed arity (the hot case): emit the arity fn directly — its name is
@ -194,15 +207,16 @@
op))
(defn- emit-invoke [ctx node]
(def fnode (norm-node (node :fn)))
(def args (map |(emit ctx $) (vview (node :args))))
(def nop (native-op (node :fn) (length args)))
(def nop (native-op fnode (length args)))
(cond
nop (case nop
'++ ['+ (in args 0) 1]
'-- ['- (in args 0) 1]
(tuple nop ;args))
(direct-call? ctx (node :fn)) (tuple (emit ctx (node :fn)) ;args)
(tuple jolt-call (emit ctx (node :fn)) ;args)))
(direct-call? ctx fnode) (tuple (emit ctx fnode) ;args)
(tuple jolt-call (emit ctx fnode) ;args)))
(defn- emit-vector [ctx node]
(def items (map |(emit ctx $) (vview (node :items))))
@ -217,7 +231,8 @@
(tuple/slice args))
(set emit
(fn emit [ctx node]
(fn emit [ctx raw]
(def node (norm-node raw))
(case (node :op)
:const (node :val)
:local (symbol (node :name))