From 22ff737f8b4bacf5bf7984df7935b9a7584d387b Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 6 Jun 2026 22:00:21 -0400 Subject: [PATCH] backend: densify phm IR nodes to structs at the emitter boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/jolt/backend.janet | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index 5463087..ab13908 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -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))