Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions pkg/ir/lisp_type_sidetable_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2026 Norman Nunley, Jr <nnunley@gmail.com>
* Part of the let-go project; see CONTRIBUTORS for full list of authors.
* SPDX-License-Identifier: MIT
*/

package ir_test

import (
"testing"
)

// TestTypeSideTableContract pins the :types side-table invariants introduced
// when inferred types moved off the inst tuple (PR #558). type-of, the
// lattice seed, and clone-inst each implement the same precedence rule —
// side table first, tuple slot 5 (construction-time :unknown) as fallback —
// and the clone carry was a review catch, so lock the contract down:
//
// 1. fallback: an inst with no side-table entry reads :unknown
// 2. set-type! writes are read back through type-of (side table wins)
// 3. overwrite: the last write wins
// 4. sparse growth: setting only a high nid grows the table; untouched
// nids in the gap still read the fallback
// 5. clone-inst carries the side-table entry (typed clone keeps its type,
// untyped clone stays :unknown)
// 6. seed-state-from-inst-types! reads the same merged view
func TestTypeSideTableContract(t *testing.T) {
ensureLoader()
got := runLispString(t, `(pr-str
(let [f (ir.build/build-fn '(defn t558 [x] (+ x 1)))
nid (first (ir/block-insts (ir/entry-block f) f))
last-nid (- (ir/inst-count f) 1)
mid-nid (- last-nid 1)]
[;; 1. fallback: nothing set yet -> construction default
(ir/type-of nid f)
;; 2. side table wins once set
(do (ir/set-type! f nid :int) (ir/type-of nid f))
;; 3. overwrite: last write wins
(do (ir/set-type! f nid :float) (ir/type-of nid f))
;; 4. sparse: only the high nid is set; the gap nid still falls back
(do (ir/set-type! f last-nid :string)
[(ir/type-of last-nid f) (ir/type-of mid-nid f)])
;; 5. clone carry: typed clone keeps the type, untyped stays :unknown
(ir/type-of (ir/clone-inst f nid) f)
(ir/type-of (ir/clone-inst f mid-nid) f)
;; 6. the typeinfra seed reads the merged view (side table first)
(let [s (ir.lattice/seed-state-from-inst-types! (ir.lattice/new-typeinfra-state f) f)]
(ir.lattice/state-type s nid))]))`)
want := `[:unknown :int :float [:string :unknown] :float :unknown :float]`
if got != want {
t.Fatalf("side-table contract mismatch:\n got: %s\nwant: %s", got, want)
}
}
51 changes: 48 additions & 3 deletions pkg/rt/core/ir/data.lg
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@
;; IR mutator by volume) used to copy the whole 6-element inst tuple +
Comment thread
mparrett marked this conversation as resolved.
;; the :insts spine per call; the side table is mutated in place.
:source-info (transient [])
;; Inferred types live OFF the inst tuple too, same shape as
;; :source-info. set-type! was the top typeinfer cost: the epoch
;; flush wrote every inferred type via swap! + assoc-in (both
;; interpreted) — ~94µs/write at 3.5k insts, more than the whole
;; worklist drain. Tuple slot 5 keeps the construction-time
;; :unknown and acts as the read fallback for nids the table
;; doesn't cover.
:types (transient [])
:blocks [{:id 0 :params [] :insts [] :preds [] :term nil}]
:uses-cache nil
:uses-dirty? true}))
Expand Down Expand Up @@ -134,14 +142,25 @@
;; Inst layout: positional vector [op refs aux block source-infos type].
;; Indices 0..5; access via `nth` rather than keyword-as-fn to skip
;; the Keyword.Invoke + hashUnencodedChars hot path.
;; NB slot 5 holds only the construction-time :unknown — inferred types live
;; in the :types side table (see new-fn / set-type!); slot 5 is type-of's
;; fallback for nids the table doesn't cover. Slot 4 (source-infos) is
;; likewise superseded by the :source-info side table.
(defn op [nid f] (nth (nth (:insts @f) nid) 0))
(defn refs [nid f] (nth (nth (:insts @f) nid) 1))
(defn aux [nid f] (nth (nth (:insts @f) nid) 2))
(defn block-of [nid f] (nth (nth (:insts @f) nid) 3))

(defn type-of [nid f]
(let [t (nth (nth (:insts @f) nid) 5)]
(if (nil? t) :unknown t)))
;; Side table first (where set-type! writes), tuple slot 5 as fallback
;; (construction-time default). A nil in either place reads as :unknown.
(let [s @f
Comment thread
mparrett marked this conversation as resolved.
tys (:types s)
side (when (< nid (count tys)) (nth tys nid))]
(if (nil? side)
(let [t (nth (nth (:insts s) nid) 5)]
(if (nil? t) :unknown t))
side)))

(defn source-infos [nid f]
;; Positional read from the side table (see new-fn). Bounds-checked: a nid
Expand Down Expand Up @@ -422,8 +441,26 @@
(defn set-aux! [f nid aux-val]
(swap! f assoc-in [:insts nid 2] aux-val))

(defn- ensure-types! [f nid]
;; Grow the types side table so index nid is addressable, then return the
;; transient. Same shape as ensure-si!; typeinfer's flush walks nids in
;; order, so this appends ~1 slot amortized.
(let [tys (:types @f)]
(loop []
(when (<= (count tys) nid)
Comment thread
mparrett marked this conversation as resolved.
(conj! tys nil)
(recur)))
tys))

(defn set-type! [f nid t]
(swap! f assoc-in [:insts nid 5] t))
;; In-place positional update — no inst-tuple copy, no :insts spine copy,
;; no swap! (mirrors add-source-info!; this was typeinfer's #1 cost: the
;; flush paid ~94µs per interpreted swap!+assoc-in write).
;; Contract: this bypasses the Function atom — no CAS, and atom watches
;; will NOT fire. Safe because compilation is single-threaded per Function
;; (the same assumption :source-info's add-source-info! already relies on);
;; concurrent mutation of one Function was never supported.
(assoc! (ensure-types! f nid) nid t))

(defn cond-target-set-true! [ct bt]
(assoc ct :true-target bt))
Expand Down Expand Up @@ -516,6 +553,14 @@
(let [src-sis (source-infos nid f)]
(when (seq src-sis)
(assoc! (ensure-si! f new-id) new-id src-sis)))
;; Same for the inferred type: it lives in the :types side table now, and
;; the copied tuple only carries the construction-time :unknown in slot 5.
;; Without this carry a clone of a typed inst would silently read :unknown
;; (conservative de-optimization, not a miscompile — but a perf trap).
(let [tys (:types @f)
src-t (when (< nid (count tys)) (nth tys nid))]
Comment thread
mparrett marked this conversation as resolved.
(when (some? src-t)
(assoc! (ensure-types! f new-id) new-id src-t)))
(invalidate-uses-state! f)
new-id))

Expand Down
10 changes: 8 additions & 2 deletions pkg/rt/core/ir/lattice.lg
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,17 @@
(let [s @state
facts (:inst-facts s)
tch (:touched s)
insts (:insts @f)
fv @f
insts (:insts fv)
;; types side table (see ir.data new-fn) — read it first, tuple slot 5
;; is the construction-time fallback, mirroring ir/type-of.
tys (:types fv)
tn (count tys)
n (count insts)]
(loop [i 0]
(if (< i n)
(let [raw-type (nth (nth insts i) 5)
(let [side (when (< i tn) (nth tys i))
raw-type (if (nil? side) (nth (nth insts i) 5) side)
seeded (if (or (nil? raw-type) (= raw-type :unknown)) :bottom raw-type)]
(conj! facts (fact-from-type seeded))
(conj! tch false)
Comment thread
mparrett marked this conversation as resolved.
Expand Down
Binary file modified pkg/rt/core_compiled.lgb
Binary file not shown.
2 changes: 1 addition & 1 deletion pkg/rt/generated.sums
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Content digest of all .lg + lgbgen sources that feed the .lgb
# bundle and the lowered Go tree. The genmanifest staleness test
# fails if this no longer matches the sources on disk.
a7dc9e523b36e733b9d9e4e92b60e45badf58bed0187c491f47f5abcfb03a0bc
6d48dd1c077adff47aa9060a9e15e372f52690b1cb9b742e8d267e892f645ee4
Loading