Skip to content

Commit 1a09463

Browse files
Kenneth McMillanclaude
andcommitted
Pack the cache lines into bv[22] using variadic concat
Now that concat is variadic and interpreted for bit-vectors, the D- and I-caches store the specified packed line directly -- a single bv[22] memory each, [21] full | [20] dirty | [19:16] hi_addr | [15:0] data -- instead of the four parallel field arrays used as a workaround. Lines are built with concat(full, dirty, hi_addr, data) and decoded with bfe (via ln_* accessors shared by the datapath and the invariants). Because the tag now holds only hi_addr, the address a line caches is structurally concat(hi_addr, index), always at its own index, so the direct-mapped "line is at the right index" structural invariant is no longer needed and is removed. The write-back victim address is concat(m_line_hi, m_index) (the variadic-concat fix also makes concat(nib,nib):addr work). ivy_check OK; ivy_to_rtl + yosys OK (each cache is one 22-bit memory). Updates reference_tagging.md (caches/RTL sections) and the reference-tagging skill (packing-with-concat and direct-mapped-geometry notes; bundled example refreshed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9cb5cb9 commit 1a09463

5 files changed

Lines changed: 198 additions & 162 deletions

File tree

.claude/skills/reference-tagging/SKILL.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,19 @@ distilled from — read the one closest to your target before writing code:
150150
store re-derives a *different* `mem_addr`/`target` afterward. Read them via
151151
`old`: `ddirty(old mem_addr) := true`, `pc := old target`.
152152

153-
- **Ivy won't interpret `concat` of two named bit-vector subtypes into an
154-
address.** `concat(hi:nib, lo:nib) : addr` came out unconstrained (the solver
155-
picked garbage — a bogus write-back address). Store the *full* address in a
156-
cache's tag field instead of just the hi bits; the index bits are redundant in
157-
a direct-mapped cache, so it is the same cache with no `concat`. Also, nested
158-
`concat` can't infer intermediate widths — represent a packed line as parallel
159-
field arrays (valid/dirty/tag/data) sharing an index rather than one wide
160-
bit-vector.
153+
- **Packing with `concat`.** `concat` is variadic, so a packed cache line is
154+
built in one shot: `concat(full, dirty, hi_addr, data) : cline` (assign to a
155+
`cline`/`bv[22]`-interpreted type; ascribe the result). Decode fields with
156+
`bfe`. A `concat` is given bit-vector semantics only when every argument sort
157+
*and* the result sort is a bit-vector and the argument widths sum to the result
158+
width; otherwise it is uninterpreted (still sound by congruence) and a width
159+
mismatch warns rather than crashing. Two consequences: (1) each `concat`
160+
argument's sort must be pinned — a bare `bfe[...]` inside a `concat` needs an
161+
ascription like `(bfe[4][7](pc):nib)`, since the `:cline` on the whole `concat`
162+
does not constrain the argument widths; (2) inside an isolate closed
163+
`with addr,opc` (so `word` is not interpreted there), a `concat` returning
164+
`word` is left uninterpreted — which is exactly what you want for the trace's
165+
recorded values.
161166

162167
- **Debugging CTIs: use `shrink=false`.** Counterexample generation can be very
163168
slow; `shrink=false` skips minimization. `trace_dir=<dir>` dumps a CTI for
@@ -213,10 +218,13 @@ weak memory, or any "software must synchronize" contract.
213218
main memory holds the reference value; I-line not dirty ⇒ holds the reference
214219
value. Same reference-tagging style, one fact per cache property.
215220

216-
- **State the direct-mapped geometry.** `valid(I) -> bfe[0][3](tag(I)) = I` (a
217-
line at index `I` caches an address whose index is `I`). Without it the prover
218-
imagines a line filed under the wrong index and writes a victim back to a bogus
219-
address — a classic spurious CTI.
221+
- **Direct-mapped geometry: keep only `hi_addr` in the tag.** If a line stores
222+
only the high address bits (not the full address), the address it caches is
223+
structurally `concat(hi_addr, index)`, always at its own index, so no "line is
224+
filed at the right index" invariant is needed. (If you instead store a full
225+
address in the tag — e.g. to dodge a `concat` — you *do* need the structural
226+
invariant `valid(I) -> bfe[0][3](tag(I)) = I`, or the prover imagines a line
227+
under the wrong index and writes a victim back to a bogus address.)
220228

221229
- **`FLUSH` + fetch stall re-establish coherence.** `FLUSH A` writes back the
222230
dirty D-line and evicts `A` from both caches; fetch stalls while a `FLUSH` is

.claude/skills/reference-tagging/references/5stage_cache_cpu_ref.ivy

Lines changed: 77 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,15 @@ interpret reg -> bv[3]
6363
type opc
6464
interpret opc -> bv[3]
6565

66-
# Cache index / hi_addr field type (4 bits): a line is at index bfe[0][3](A)
67-
# and stores hi_addr = bfe[4][7](A).
66+
# Cache line and its field types. A line is a bv[22]:
67+
# [21] full [20] dirty [19:16] hi_addr [15:0] data
68+
# Address A maps to line index bfe[0][3](A); the hi_addr field is bfe[4][7](A).
69+
type cline
70+
interpret cline -> bv[22]
6871
type nib
6972
interpret nib -> bv[4]
73+
type bit
74+
interpret bit -> bv[1]
7075

7176
# The clock: one exported action driven by the environment.
7277

@@ -484,14 +489,11 @@ object cpu = {
484489
# (1) a dirty cache line's address is dirty in the reference;
485490
# (2) a present cache line holds the reference's MEM-stage value;
486491
# (4) a not-dirty address holds the reference value in main memory.
487-
# ((3), the I-cache, arrives in M3.)
492+
# ((3), the I-cache, is below.)
488493
#
489-
# Direct-mapped structural invariant: a valid line at index I caches an
490-
# address whose index is I (maintained by every install, since we install
491-
# at m_index = bfe[0][3](m_addr)). Without this the solver can place an
492-
# address at the wrong line and write a stale victim back to it.
493-
invariant dcf(I) -> bfe[0][3](dct(I)) = I
494-
invariant icf(I) -> bfe[0][3](ict(I)) = I
494+
# (With a packed line the tag holds only hi_addr, so the address cached at
495+
# index I is structurally concat(hi_addr, I), whose index is I -- no
496+
# separate "line is at the right index" invariant is needed.)
495497
invariant ~trace.st(trace.now).error -> (dc_dirty(A) -> trace.st(mcommit).ddirty(A))
496498
invariant ~trace.st(trace.now).error -> (dc_present(A) -> dc_val(A) = trace.st(mcommit).mem(A))
497499
invariant ~trace.st(trace.now).error -> (~dc_dirty(A) -> mem(A) = trace.st(mcommit).mem(A))
@@ -593,34 +595,32 @@ object cpu = {
593595
var mem(A:addr) : word # unified main memory (instructions and data)
594596

595597
# ---- D-cache (direct-mapped, write-back, 16 lines x 1 word) ----
596-
# The specified line format is a 22-bit vector
597-
# [21] full [20] dirty [19:16] hi_addr [15:0] data
598-
# We store the fields as parallel arrays sharing the index bfe[0][3](A); this
599-
# is the same direct-mapped cache (in RTL, parallel narrow memories with a
600-
# shared index) but avoids bit-packing plumbing. hi_addr = bfe[4][7](A).
601-
# Data accesses (LD/ST) go through this cache; instruction fetch in M2 still
602-
# reads main memory directly (the I-cache arrives in M3), so a store that is
603-
# only in the D-cache leaves a stale instruction in main memory -- which is
604-
# exactly the incoherence the ISA's ddirty/error mechanism accounts for.
605-
606-
var dcf(I:nib) : bool # full (valid)
607-
var dcd(I:nib) : bool # dirty
608-
var dct(I:nib) : addr # tag: the full address cached in this line
609-
# (its low nibble is always the line index I;
610-
# storing the full address rather than just the
611-
# hi_addr nibble avoids a concat and is the same
612-
# direct-mapped cache)
613-
var dcw(I:nib) : word # data word
598+
# Each line is a packed bv[22] as described at the top: [21] full, [20] dirty,
599+
# [19:16] hi_addr, [15:0] data. Lines are built with `concat` and their fields
600+
# read with `bfe` (see the ln_* accessors below). Data accesses (LD/ST) go
601+
# through this cache; instruction fetch reads the separate I-cache. A store
602+
# that lives only in the (write-back) D-cache leaves a stale value in main
603+
# memory -- the incoherence the ISA's ddirty/error mechanism accounts for.
604+
605+
var dcache(I:nib) : cline
614606

615607
# ---- I-cache (direct-mapped, read-only, 16 lines x 1 word) ----
616608
# Instruction fetch reads this cache; it is filled from main memory on a miss
617-
# and never written by the CPU (so no dirty bit). FLUSH invalidates a line
618-
# (M3b). Being filled from possibly-stale main memory, it can hold a stale
609+
# and never written (so its dirty bit is always 0). FLUSH invalidates a line.
610+
# Being filled from possibly-stale main memory, it can hold a stale
619611
# instruction; the ISA's ddirty/error accounts for that.
620612

621-
var icf(I:nib) : bool # full (valid)
622-
var ict(I:nib) : addr # tag: the full address cached in this line
623-
var icw(I:nib) : word # cached instruction word
613+
var icache(I:nib) : cline
614+
615+
# ---- packed-line field accessors (used by both datapath and invariants) ----
616+
function ln_full(L:cline) : bool
617+
definition ln_full(L) = (bfe[21][21](L):bit) = 1
618+
function ln_dirty(L:cline) : bool
619+
definition ln_dirty(L) = (bfe[20][20](L):bit) = 1
620+
function ln_hi(L:cline) : nib
621+
definition ln_hi(L) = bfe[16][19](L)
622+
function ln_data(L:cline) : word
623+
definition ln_data(L) = bfe[0][15](L)
624624

625625
# ---- main-memory read-fill controller (multi-cycle) ----
626626
# Read fills take two cycles: an initiating cycle (memory free) and a busy
@@ -722,10 +722,12 @@ object cpu = {
722722

723723
wire f_index : nib
724724
definition f_index = bfe[0][3](pc)
725+
wire f_iline : cline # the I-cache line at the fetch index
726+
definition f_iline = icache(f_index)
725727
wire f_ihit : bool
726-
definition f_ihit = icf(f_index) & (ict(f_index) = pc)
728+
definition f_ihit = ln_full(f_iline) & (ln_hi(f_iline) = bfe[4][7](pc))
727729
wire fetched : word
728-
definition fetched = (icw(f_index) if f_ihit else mem(pc))
730+
definition fetched = (ln_data(f_iline) if f_ihit else mem(pc))
729731

730732
wire f_is_branch : bool
731733
definition f_is_branch = ((bfe[13][15](fetched):opc) = 6)
@@ -754,20 +756,26 @@ object cpu = {
754756

755757
wire m_index : nib
756758
definition m_index = bfe[0][3](m_addr)
759+
wire m_hi : nib # hi_addr of the accessed address
760+
definition m_hi = bfe[4][7](m_addr)
761+
wire m_line : cline # the D-cache line at this index
762+
definition m_line = dcache(m_index)
757763
wire m_line_full : bool
758-
definition m_line_full = dcf(m_index)
764+
definition m_line_full = ln_full(m_line)
759765
wire m_line_dirty : bool
760-
definition m_line_dirty = dcd(m_index)
761-
wire m_line_addr : addr # full address currently cached in this line
762-
definition m_line_addr = dct(m_index)
766+
definition m_line_dirty = ln_dirty(m_line)
767+
wire m_line_hi : nib # hi_addr currently cached in this line
768+
definition m_line_hi = ln_hi(m_line)
763769
wire m_line_data : word
764-
definition m_line_data = dcw(m_index)
770+
definition m_line_data = ln_data(m_line)
765771
wire m_hit : bool
766-
definition m_hit = m_line_full & (m_line_addr = m_addr)
772+
definition m_hit = m_line_full & (m_line_hi = m_hi)
773+
wire m_iline : cline # the I-cache line at this index
774+
definition m_iline = icache(m_index)
767775
wire m_ihit : bool # I-cache holds the (flush) address m_addr
768-
definition m_ihit = icf(m_index) & (ict(m_index) = m_addr)
776+
definition m_ihit = ln_full(m_iline) & (ln_hi(m_iline) = m_hi)
769777
wire m_victim_addr : addr # address to write the victim back to
770-
definition m_victim_addr = m_line_addr
778+
definition m_victim_addr = concat(m_line_hi, m_index)
771779
wire m_fill_data : word # main-memory word for m_addr (pre-clock)
772780
definition m_fill_data = mem(m_addr)
773781

@@ -782,19 +790,27 @@ object cpu = {
782790
wire dmem_stall : bool
783791
definition dmem_stall = d_miss & ~d_fill_go
784792

785-
# ---- D-cache query helpers (used by the invariants, for any address A) ----
793+
# ---- cache query helpers (used by the invariants, for any address A) ----
794+
# A is present in a cache iff the line at its index is full and its hi_addr
795+
# field matches A's hi_addr; the cached address is then structurally
796+
# concat(hi_addr, index(A)) = A, so no separate "right index" invariant is
797+
# needed (unlike a full-address tag).
786798

799+
function dc_line(A:addr) : cline
800+
definition dc_line(A) = dcache(bfe[0][3](A))
787801
function dc_present(A:addr) : bool
788-
definition dc_present(A) = dcf(bfe[0][3](A)) & (dct(bfe[0][3](A)) = A)
802+
definition dc_present(A) = ln_full(dc_line(A)) & (ln_hi(dc_line(A)) = bfe[4][7](A))
789803
function dc_dirty(A:addr) : bool
790-
definition dc_dirty(A) = dc_present(A) & dcd(bfe[0][3](A))
804+
definition dc_dirty(A) = dc_present(A) & ln_dirty(dc_line(A))
791805
function dc_val(A:addr) : word
792-
definition dc_val(A) = dcw(bfe[0][3](A))
806+
definition dc_val(A) = ln_data(dc_line(A))
793807

808+
function ic_line(A:addr) : cline
809+
definition ic_line(A) = icache(bfe[0][3](A))
794810
function ic_present(A:addr) : bool
795-
definition ic_present(A) = icf(bfe[0][3](A)) & (ict(bfe[0][3](A)) = A)
811+
definition ic_present(A) = ln_full(ic_line(A)) & (ln_hi(ic_line(A)) = bfe[4][7](A))
796812
function ic_val(A:addr) : word
797-
definition ic_val(A) = icw(bfe[0][3](A))
813+
definition ic_val(A) = ln_data(ic_line(A))
798814

799815
# ---- reset ----
800816

@@ -806,8 +822,8 @@ object cpu = {
806822
w_valid := false;
807823
rf(R) := 0;
808824
mem(A) := init_mem(A);
809-
dcf(I) := false; # all D-cache lines empty
810-
icf(I) := false; # all I-cache lines empty
825+
dcache(I) := 0; # all D-cache lines empty (full bit = 0)
826+
icache(I) := 0; # all I-cache lines empty
811827
mbusy := false; # no memory fill in progress
812828
}
813829

@@ -836,29 +852,24 @@ object cpu = {
836852
mem(m_victim_addr) := m_line_data
837853
};
838854
if m_opcode = 5 {
839-
# ST: install a dirty line holding the stored word. A 1-word line
840-
# is fully overwritten, so a write miss needs no fetch.
841-
dcf(m_index) := true;
842-
dcd(m_index) := true;
843-
dct(m_index) := m_addr;
844-
dcw(m_index) := m_store
855+
# ST: install a dirty line holding the stored word (full, dirty,
856+
# hi_addr, data). A 1-word line is fully overwritten, so a write
857+
# miss needs no fetch.
858+
dcache(m_index) := (concat(1:bit, 1:bit, m_hi, m_store) : cline)
845859
} else if ~m_hit {
846860
# LD miss: install a clean line with the fetched word.
847-
dcf(m_index) := true;
848-
dcd(m_index) := false;
849-
dct(m_index) := m_addr;
850-
dcw(m_index) := m_fill_data
861+
dcache(m_index) := (concat(1:bit, 0:bit, m_hi, m_fill_data) : cline)
851862
}
852863
};
853864
# FLUSH m_addr: write the D-cache line back if it is present and dirty,
854-
# then evict m_addr from both caches, so a later fetch of m_addr misses
855-
# and refills from the (now up-to-date) main memory.
865+
# then evict m_addr from both caches (full bit -> 0), so a later fetch of
866+
# m_addr misses and refills from the (now up-to-date) main memory.
856867
if ~dmem_stall & m_valid & (m_opcode = 7) {
857868
if m_hit & m_line_dirty {
858869
mem(m_addr) := m_line_data
859870
};
860-
if m_hit { dcf(m_index) := false };
861-
if m_ihit { icf(m_index) := false }
871+
if m_hit { dcache(m_index) := 0 };
872+
if m_ihit { icache(m_index) := 0 }
862873
};
863874

864875
# EX: held while MEM stalls for a D-fill; bubbled on a data hazard;
@@ -903,9 +914,8 @@ object cpu = {
903914
d_pred := f_ptaken;
904915
d_pc := pc;
905916
if ~f_ihit {
906-
icf(f_index) := true;
907-
ict(f_index) := pc;
908-
icw(f_index) := fetched
917+
# install a clean I-cache line (dirty bit unused = 0)
918+
icache(f_index) := (concat(1:bit, 0:bit, (bfe[4][7](pc):nib), fetched) : cline)
909919
};
910920
pc := pred_next_pc
911921
}

.claude/skills/reference-tagging/references/reference_tagging.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,12 @@ same reference-tagging style, all relative to the trace state at the MEM stage
312312

313313
Together these say the *effective* memory -- D-cache where present-and-dirty,
314314
else main memory -- always equals the reference memory (until an error), and the
315-
I-cache agrees wherever it has not gone stale. One structural invariant is
316-
needed to make the direct-mapped geometry sound: a valid line at index `I`
317-
caches an address whose index is `I` (`bfe[0][3](tag) = I`). Without it the
318-
prover can imagine a line filed under the wrong index and write a victim back to
319-
a bogus address.
315+
I-cache agrees wherever it has not gone stale. Each line is a packed bit-vector
316+
`[21] full | [20] dirty | [19:16] hi_addr | [15:0] data`, built with `concat` and
317+
decoded with `bfe`. Because the tag holds only `hi_addr` (not the full address),
318+
the address a line caches is structurally `concat(hi_addr, index)`, so it is
319+
always at its own index -- no extra "line is filed at the right index" invariant
320+
is needed.
320321

321322
`FLUSH A` is what re-establishes coherence: in the MEM stage it writes the
322323
D-cache line for `A` back to main memory if it is dirty, then evicts `A` from
@@ -357,6 +358,9 @@ RTL
357358
All four designs translate to RTL with `ivy_to_rtl`, because the datapath is
358359
kept free of ghost state (tags, shadow and dirty bits used only in the proof
359360
live in `specification` blocks) and uses only point writes to the state arrays.
360-
The caches are stored as parallel field arrays (valid / dirty / tag / data)
361-
sharing one index -- in RTL, narrow memories addressed in lockstep, i.e. a
362-
direct-mapped cache.
361+
Each cache is a single memory of packed 22-bit lines; a line is assembled with
362+
`concat` and its fields read with `bfe`, so in RTL it is one narrow memory per
363+
cache -- a direct-mapped cache. (This uses `concat`'s variadic form,
364+
`concat(full, dirty, hi_addr, data)`; a `concat` is given bit-vector semantics
365+
only when every argument and the result are bit-vectors and the argument widths
366+
sum to the result width, and is otherwise treated as uninterpreted.)

0 commit comments

Comments
 (0)