diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index 016b1ca..dbe6241 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -370,11 +370,13 @@ "(a phm/sorted/transient/lazy-seq), not the plain " "struct/record the ^:struct/^Record hint asserts")])) # Subject carries a complete :shape (jolt-t34) => it is provably a shape-rec; - # the field reads by bare index. The shape is the inference's canonical key - # vector, so the index is the key's position in it, +1 for the descriptor. + # the field reads by bare index. The :shape is just the KEY SET; the layout + # comes from the runtime's canonical shape-sort (the same order shape-for and + # emit-map use), so construction and lookup always agree. (def sidx (when (and (os/getenv "JOLT_SHAPE") subj-node (subj-node :shape)) - (def sk (let [s (subj-node :shape)] (if (pv/pvec? s) (pv/pv->array s) s))) + (def raw (let [s (subj-node :shape)] (if (pv/pvec? s) (pv/pv->array s) s))) + (def sk (shape-sort raw)) (var pos nil) (var i 0) (each kk sk (when (= kk k) (set pos i)) (++ i)) (when pos (+ pos 1)))) @@ -487,15 +489,20 @@ (unless (and (= :const (k :op)) (or (keyword? kv) (string? kv) (number? kv) (boolean? kv))) (set fast false))) - # Shape-record fast path (jolt-t34, JOLT_SHAPE): a const-key map literal the - # inference tagged with a complete :shape becomes a shape tuple (≈2x cheaper - # than a struct). The shape is the inference's canonical (str-sorted) key - # vector — ANY constant key set, no hardcoding. Values are emitted in that - # order; element 0 is the compile-time shape descriptor. + # Shape-record representation (jolt-t34, JOLT_SHAPE): EVERY constant-key map + # literal becomes a shape tuple (≈2x cheaper than a struct), CONSISTENTLY — + # not gated on the inference, so a value's representation always matches what + # the type system claims about it. The layout is the runtime's canonical + # shape-sort of the keys (the single source of truth all sites share). Values + # are emitted in that order; element 0 is the compile-time shape descriptor. + # gated on :inline? — shapes apply to USER code only, never to core or the + # compiler's own IR-node map literals (which the back end reads with raw + # keyword access and would break if turned into tuples) (def shape-keys - (when (and fast (os/getenv "JOLT_SHAPE") (node :shape)) - (def sk (node :shape)) - (if (pv/pvec? sk) (pv/pv->array sk) (if (or (tuple? sk) (array? sk)) sk nil)))) + (when (and fast (os/getenv "JOLT_SHAPE") (get (ctx :env) :inline?)) + (def ks @[]) + (each pair pairs (array/push ks ((norm-node (in (vview pair) 0)) :val))) + (shape-sort ks))) (if shape-keys (do (def desc (shape-for shape-keys)) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index feba8db..22d8112 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -122,6 +122,7 @@ (cond # nil is an empty seq in Clojure — iterating it yields nothing. (nil? c) @[] + (shape-rec? c) (map (fn [k] (tuple k (shape-get c k nil))) (shape-keys c)) (pvec? c) (pv->array c) (plist? c) (pl->array c) (set? c) (phs-seq c) @@ -208,7 +209,8 @@ # Tagged structs (symbols/chars/uuids — anything with :jolt/type) are VALUES, # not maps. (sorted-map? is defined later, so the table check is inlined.) (defn core-map? [x] - (or (phm? x) + (or (shape-rec? x) + (phm? x) (and (struct? x) (nil? (get x :jolt/type))) (and (table? x) (or (not (nil? (get x :jolt/deftype))) @@ -311,6 +313,8 @@ an array; otherwise nil. Lets = compare across tuple/array/lazy-seq." [x] (cond + # a shape-rec is a MAP, not a sequence, even though it is a tuple + (shape-rec? x) nil (lazy-seq? x) (realize-for-iteration x) (pvec? x) (pv->array x) (plist? x) (pl->array x) @@ -322,6 +326,7 @@ "Return [k v] pairs for a map-like value (phm/sorted-map/struct/table), else nil." [x] (cond + (shape-rec? x) (map (fn [k] @[k (shape-get x k nil)]) (shape-keys x)) (phm? x) (phm-entries x) # sorted-map equals any map with the same pairs (representation-agnostic, as # in Clojure); sorted-set is handled by the set branch of jolt-equal? @@ -495,42 +500,6 @@ (error "Vector arg to map conj must be a pair"))) result))))))))))))) -# --- shape records (hidden classes, jolt-t34) ------------------------------- -# A "shape record" is a cheap fixed-layout representation for a map literal -# whose keys are a known compile-time set (e.g. a vec3 {:r :g :b}). It is a -# Janet tuple [SHAPE v0 v1 ...] where SHAPE is an interned descriptor struct -# {:jolt/shape KEYS :idx {k->pos}}. Construction is a tuple (≈2x cheaper than a -# struct), const-keyword lookup compiles to an index, and general map ops below -# recognize it via shape-rec? and treat it as a map — so it is transparent -# wherever it flows. Created only by the backend when JOLT_SHAPE is on and the -# inference proves the shape; the runtime support here is always present so a -# shape value is handled correctly regardless. -(def- shape-cache @{}) # sorted-keys-tuple -> interned shape descriptor -(defn shape-for - "Interned shape descriptor for an ordered key vector (keys in layout order)." - [keyv] - (def kk (tuple ;keyv)) - (or (get shape-cache kk) - (let [idx @{}] - (var i 0) (each k keyv (put idx k i) (++ i)) - (def desc (struct :jolt/shape kk :idx (table/to-struct idx))) - (put shape-cache kk desc) - desc))) -(defn shape-rec? [x] - (and (tuple? x) (> (length x) 0) - (struct? (in x 0)) (not (nil? (in (in x 0) :jolt/shape))))) -(defn shape-keys [rec] ((in rec 0) :jolt/shape)) -(defn shape-get [rec k default] - (def pos (get ((in rec 0) :idx) k)) - (if (nil? pos) default (in rec (+ pos 1)))) -(defn shape-assoc [rec k v] - # assoc on a known key keeps the layout; a new key falls back to a struct - (def desc (in rec 0)) - (def pos (get (desc :idx) k)) - (if (nil? pos) - (let [t @{}] (each kk (desc :jolt/shape) (put t kk (shape-get rec kk nil))) (put t k v) (table/to-struct t)) - (let [a (array ;rec)] (put a (+ pos 1) v) (tuple ;a)))) - (defn core-assoc [m & kvs] (when (odd? (length kvs)) (error "assoc expects an even number of key/value arguments")) @@ -589,6 +558,8 @@ (defn core-dissoc [m & ks] (cond (nil? m) nil + # dissoc loses a key -> the shape changes; bridge through a struct (cold op) + (shape-rec? m) (core-dissoc (shape->struct m) ;ks) (core-sorted-map? m) ((sorted-op m :dissoc) m ks) (phm? m) (do (var result m) (each k ks (set result (phm-dissoc result k))) result) # reject clearly non-map values (scalars, sequences, sets, symbol/char structs) @@ -633,6 +604,7 @@ (defn jolt-call [f & args] (cond (or (function? f) (cfunction? f)) (apply f args) + (shape-rec? f) (core-get f (get args 0) (get args 1)) (keyword? f) (core-get (get args 0) f (get args 1)) (and (struct? f) (= :symbol (f :jolt/type))) (core-get (get args 0) f (get args 1)) (core-sorted? f) ((sorted-op f :get) f (get args 0) (get args 1)) @@ -673,6 +645,7 @@ # get-in now lives in the Clojure collection tier (core/20-coll.clj). (defn core-contains? [coll key] + (if (shape-rec? coll) (shape-contains? coll key) (if (core-sorted? coll) (if ((sorted-op coll :contains) coll key) true false) (if (core-transient? coll) (case (coll :kind) @@ -685,7 +658,7 @@ (if (table? coll) (not (nil? (coll key))) (if (or (tuple? coll) (array? coll)) (and (number? key) (>= key 0) (< key (length coll))) - false))))))))) + false)))))))))) # Coerce a Clojure IFn value to a Janet-callable fn for higher-order fns # (map/filter/sort-by/group-by/...). Janet functions pass through; a keyword or @@ -708,6 +681,7 @@ (defn core-count [coll] (cond (nil? coll) 0 + (shape-rec? coll) (shape-count coll) (core-transient? coll) (length (if (= :vector (coll :kind)) (coll :arr) (coll :tbl))) (core-sorted? coll) ((sorted-op coll :count) coll) (lazy-seq? coll) (ls-count coll) @@ -722,6 +696,7 @@ (defn core-first [coll] (cond + (shape-rec? coll) (core-first (shape->struct coll)) (core-sorted? coll) ((sorted-op coll :first) coll) (lazy-seq? coll) (ls-first coll) (pvec? coll) (if (= 0 (pv-count coll)) nil (pv-nth coll 0)) @@ -800,6 +775,7 @@ (defn core-seq [coll] (cond + (shape-rec? coll) (tuple ;(map (fn [k] (tuple k (shape-get coll k nil))) (shape-keys coll))) (core-sorted? coll) ((sorted-op coll :seq) coll) (or (nil? coll) (and (or (tuple? coll) (array? coll)) (= 0 (length coll)))) nil # Cell-based emptiness, NOT (nil? (ls-first)): a lazy-seq whose first element @@ -1655,6 +1631,8 @@ (buffer/push-string buf (ns-display-name v)) (buffer/push-string buf "]")) (and (table? v) (= :jolt/var (get v :jolt/type))) (buffer/push-string buf (var-display v)) + (shape-rec? v) (pr-render-pairs buf + (map (fn [k] [k (shape-get v k nil)]) (shape-keys v))) (core-sorted-map? v) (pr-render-pairs buf (map (fn [e] [(vnth e 0) (vnth e 1)]) (sorted-entries-arr v))) (core-sorted-set? v) (pr-render-seq buf (sorted-entries-arr v) "#{" "}") diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 466e502..d05578a 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -106,6 +106,7 @@ [coll k default] (cond (jolt-transient? coll) (transient-lookup coll k default) + (shape-rec? coll) (shape-get coll k default) # sorted colls are tables — without this arm they fell into the raw # table-get branch and (:k (sorted-map ...)) was always nil (jolt-4vr spec) (and (table? coll) (or (= :jolt/sorted-map (coll :jolt/type)) diff --git a/src/jolt/types.janet b/src/jolt/types.janet index 2808f7d..c5c4bc3 100644 --- a/src/jolt/types.janet +++ b/src/jolt/types.janet @@ -590,3 +590,57 @@ (let [registry (get (ctx :env) :type-registry) type-impls (get registry type-tag)] (if (and type-impls (get type-impls protocol-name)) true false))) + +# --- shape records (hidden classes, jolt-t34) ------------------------------- +# A "shape record" is a cheap fixed-layout representation for a map literal +# whose keys are a known compile-time set (e.g. a vec3 {:r :g :b}). It is a +# Janet tuple [SHAPE v0 v1 ...] where SHAPE is an interned descriptor struct +# {:jolt/shape KEYS :idx {k->pos}}. Construction is a tuple (≈2x cheaper than a +# struct), const-keyword lookup compiles to an index, and general map ops below +# recognize it via shape-rec? and treat it as a map — so it is transparent +# wherever it flows. Created only by the backend when JOLT_SHAPE is on and the +# inference proves the shape; the runtime support here is always present so a +# shape value is handled correctly regardless. +(def- shape-cache @{}) # canonical-keys-tuple -> interned shape descriptor +# Canonical key order, OWNED BY THE RUNTIME (jolt-t34): every site that builds or +# reads a shape (shape-for, emit-map, emit-kw-lookup, build-map-literal) derives +# the layout from this one function, so they always agree regardless of what +# order the inference passed the keys in. Sorted by the keys' jdn print form — +# deterministic and total across keywords/strings/numbers/bools. +(defn shape-sort [ks] + (sort (array ;ks) (fn [a b] (< (string/format "%j" a) (string/format "%j" b))))) +(defn shape-for + "Interned shape descriptor for a key set. Keys are canonicalized internally, + so callers need not pre-sort and any permutation yields the same descriptor." + [keyv] + (def sk (tuple ;(shape-sort keyv))) + (or (get shape-cache sk) + (let [idx @{}] + (var i 0) (each k sk (put idx k i) (++ i)) + (def desc (struct :jolt/shape sk :idx (table/to-struct idx))) + (put shape-cache sk desc) + desc))) +(defn shape-rec? [x] + (and (tuple? x) (> (length x) 0) + (struct? (in x 0)) (not (nil? (in (in x 0) :jolt/shape))))) +(defn shape-keys [rec] ((in rec 0) :jolt/shape)) +(defn shape-get [rec k default] + (def pos (get ((in rec 0) :idx) k)) + (if (nil? pos) default (in rec (+ pos 1)))) +(defn shape-assoc [rec k v] + # assoc on a known key keeps the layout; a new key falls back to a struct + (def desc (in rec 0)) + (def pos (get (desc :idx) k)) + (if (nil? pos) + (let [t @{}] (each kk (desc :jolt/shape) (put t kk (shape-get rec kk nil))) (put t k v) (table/to-struct t)) + (let [a (array ;rec)] (put a (+ pos 1) v) (tuple ;a)))) +(defn shape-count [rec] (- (length rec) 1)) +(defn shape-contains? [rec k] (not (nil? (get ((in rec 0) :idx) k)))) +(defn shape-vals [rec] (tuple/slice rec 1)) +# a struct snapshot of a shape-rec — the reusable bridge for ops that already +# handle structs (dissoc, vals, seq, equality, print, ...) without per-op code +(defn shape->struct [rec] + (def desc (in rec 0)) (def t @{}) + (each kk (desc :jolt/shape) (put t kk (in rec (+ 1 (get (desc :idx) kk))))) + (table/to-struct t)) + diff --git a/test/integration/shape-transparency-test.janet b/test/integration/shape-transparency-test.janet new file mode 100644 index 0000000..496803f --- /dev/null +++ b/test/integration/shape-transparency-test.janet @@ -0,0 +1,64 @@ +# Shape-record transparency (jolt-t34 Round 1). A shape-rec is the compiler's +# cheap representation for a constant-key map literal — a Janet tuple +# [descriptor v0 v1 ...]. Every map operation must treat it EXACTLY like the +# equivalent struct map, so a shape value is transparent wherever it flows. +# These build shape-recs directly via the runtime (shape-for + tuple) and +# assert each op matches the struct map's behavior. +(use ../../src/jolt/types) +(use ../../src/jolt/core) + +(var fails 0) +(defn check [label got want] + (if (deep= got want) (print " ok " label) + (do (++ fails) (printf " FAIL %s: got %j want %j" label got want)))) + +# {:a 1 :b 2} as a shape-rec and as a struct — they must be indistinguishable +(def SH (shape-for [:a :b])) +(def r (tuple SH 1 2)) +(def s {:a 1 :b 2}) + +# --- access ------------------------------------------------------------------ +(check "get hit" (core-get r :a nil) 1) +(check "get hit 2" (core-get r :b nil) 2) +(check "get miss" (core-get r :z :d) :d) +(check "count" (core-count r) 2) +(check "contains? hit" (core-contains? r :a) true) +(check "contains? miss" (core-contains? r :z) false) +(check "map?" (core-map? r) true) + +# --- update (returns something that reads back correctly) -------------------- +(check "assoc existing" (core-get (core-assoc r :a 9) :a nil) 9) +(check "assoc new key" (core-get (core-assoc r :c 3) :c nil) 3) +(check "assoc keeps others" (core-get (core-assoc r :c 3) :b nil) 2) +(check "dissoc" (core-get (core-dissoc r :a) :a :gone) :gone) +(check "dissoc keeps" (core-get (core-dissoc r :a) :b nil) 2) + +# --- enumeration: entries via the central seq normalizer (what keys/vals/seq/ +# reduce-kv in the overlay all flow through) — order-independent --------------- +(defn entryset [m] (sorted (map |(string/format "%j" $) (realize-for-iteration m)))) +(check "entries match struct" (entryset r) (entryset s)) +(check "count of seq" (length (core-seq r)) 2) +(check "first is a 2-entry" (length (core-first r)) 2) + +# --- equality: a shape-rec equals the same struct map and vice versa --------- +(check "= shape vs struct" (jolt-equal? r s) true) +(check "= struct vs shape" (jolt-equal? s r) true) +(check "= shape vs shape" (jolt-equal? r (tuple (shape-for [:a :b]) 1 2)) true) +(check "not= diff value" (jolt-equal? r (tuple (shape-for [:a :b]) 1 9)) false) +(check "not= diff shape" (jolt-equal? r (tuple (shape-for [:a :b :c]) 1 2 3)) false) +(check "not= vs vector" (jolt-equal? r [1 2]) false) + +# --- IFn: a map is callable as a key lookup ---------------------------------- +(check "call as fn" (jolt-call r :a) 1) +(check "call as fn miss default" (jolt-call r :z :d) :d) + +# --- nil/false values are present (shape-recs store them positionally) -------- +(def rn (tuple (shape-for [:a :b]) nil false)) +(check "nil value present" (core-contains? rn :a) true) +(check "nil value get" (core-get rn :a :d) nil) +(check "false value get" (core-get rn :b :d) false) +(check "count with nils" (core-count rn) 2) + +(if (> fails 0) + (error (string "shape-transparency: " fails " failing check(s)")) + (print "\nShape transparency passed!"))