From e3eb952b2dec27da0a44ac6a2e49805c0a2dc8d3 Mon Sep 17 00:00:00 2001 From: Rahul Tomar Date: Fri, 6 Nov 2026 20:51:16 +0100 Subject: [PATCH] Why? through memories: follow a value read from a memory to the word and its writer "Why is it this value?" used to stop at a memory read with "mem = ?". The simulator now records each memory word's changes like a signal's (SimResult::words, named like tb.dut.mem[2]) and, each time an assignment that reads memory words runs, which words it read (SimResult::word_reads, Origin::words). The web app shows the word read ("It used mem[0] = 17, rptr = 0") and follows it back to the line that stored it. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++ apps/web/src/lib/why.ts | 86 ++++++++++++++++++++++++-------- apps/web/src/sim/protocol.ts | 14 ++++++ apps/web/src/sim/worker.ts | 20 +++++++- crates/wafer-sim-wasm/src/lib.rs | 50 ++++++++++++++++++- crates/wafer-sim/src/elab.rs | 46 +++++++++++++++++ crates/wafer-sim/src/sim.rs | 79 ++++++++++++++++++++++++++++- crates/wafer-sim/src/tests.rs | 47 +++++++++++++++++ docs/architecture.md | 2 +- docs/user-guide/design-view.md | 6 ++- 10 files changed, 329 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7785e8a..43d9557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to Wafersmith are recorded here. The format follows ### Added +- **Why? through memories**: in "Why is it this value?", a value read from a memory (`dout = + mem[rptr]`) now names the word it was (`mem[0] = 17`), and that word can be clicked like any + value, back to the line that stored it (`mem[wptr] <= din`) and why it ran. The simulator records + each memory word's changes like a signal's (`SimResult::words`) and, each time an assignment that + reads memory words runs, which words it read (`SimResult::word_reads`, `Origin::words`). Runs in + the browser; no AI and no cost. - **Try a small fix**: a failing example offers "The design: try a small fix". Without AI, the server tries the small repairs that undo the most common mistakes (a comparison turned around, a limit one off in either direction, a condition reversed, a missing or extra `!`/`~`, `&&` for diff --git a/apps/web/src/lib/why.ts b/apps/web/src/lib/why.ts index 7400651..9f1acba 100644 --- a/apps/web/src/lib/why.ts +++ b/apps/web/src/lib/why.ts @@ -1,8 +1,9 @@ // "Why is it this value?": the assignment in the design that wrote a value, the if/case decisions // that led to it and the values it read, from the simulator's record of what wrote each change. -// Port connections are followed through; the example's own inputs end the chain. +// Port connections are followed through, and a memory read to the word read and what wrote it; +// the example's own inputs end the chain. -import { NO_ORIGIN, type SimOrigin, type SimSignal } from "../sim/protocol"; +import { NO_ORIGIN, type SimOrigin, type SimResult, type SimSignal } from "../sim/protocol"; import type { Replay } from "./replay"; /** A value an explanation mentions, which can be explained in turn. */ @@ -12,7 +13,7 @@ export interface Read { full: string; /** In decimal, as examples write values. */ value: string; - /** The moment to explain it at; null if it isn't recorded (a memory). */ + /** The moment to explain it at; null if it isn't recorded (a memory word the run didn't). */ t: number | null; } @@ -55,9 +56,46 @@ function changeAt(s: SimSignal, t: number): number { /** A nonblocking assignment (`q <= d`), as opposed to `=` or a comparison inside one. */ const nonblocking = (code: string) => /^[^=<]*<=/.test(code); -/** Why `full` (a signal's full name) has the value it has at time `t`. */ -export function why(rep: Replay, full: string, t: number): Because | null { - const signals = new Map(rep.sim.signals.map((s) => [s.name, s])); +/** For each origin that reads memory words: the entries of `wordReads` it made, in order. */ +const readsByOrigin = new WeakMap>(); + +/** The memory words origin `by` read the last time it ran at or before `time`, one per entry of + * its `words`; null if the run didn't record it. */ +function wordsRead(sim: SimResult, by: number, time: number): number[] | null { + const n = sim.origins[by]?.words.length ?? 0; + if (!n) return null; + let index = readsByOrigin.get(sim); + if (!index) { + index = new Map(); + sim.wordReads.origins.forEach((o, j) => { + const list = index!.get(o); + if (list) list.push(j); + else index!.set(o, [j]); + }); + readsByOrigin.set(sim, index); + } + const list = index.get(by) ?? []; + const { times, words } = sim.wordReads; + let lo = 0; + let hi = list.length - 1; + let end = -1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (times[list[mid]] <= time) { + end = mid; + lo = mid + 1; + } else hi = mid - 1; + } + // Each run records one entry per word, so the last `n` entries are its words. + if (end + 1 < n) return null; + const group = list.slice(end + 1 - n, end + 1).map((j) => words[j]); + return group.includes(NO_ORIGIN) ? null : group; +} + +/** How to look values up in a run: its signals and memory words by full name, and how a person + * names them (`count`, `u_rx.state`, `mem[2]`). */ +function lookup(rep: Replay) { + const signals = new Map([...rep.sim.signals, ...rep.sim.words].map((s) => [s.name, s])); const name = (f: string) => { const rest = f.startsWith(`${rep.top}.`) ? f.slice(rep.top.length + 1) : f; return rest.startsWith("dut.") ? rest.slice(4) : rest; @@ -67,6 +105,23 @@ export function why(rep: Replay, full: string, t: number): Because | null { const i = s ? changeAt(s, at) : -1; return { name: name(f), full: f, value: s && i >= 0 ? s.values[i].toString() : "?", t: s && i >= 0 ? at : null }; }; + /** The values origin `by`, run at `ran`, computed from (as they were at `at`); a memory it + * read is the words it read. */ + const reads = (by: number, ran: number, at: number): Read[] => { + const o = rep.sim.origins[by]; + const got = wordsRead(rep.sim, by, ran); + return o.reads.flatMap((r) => { + if (!got || !o.words.includes(r)) return [read(r, at)]; + const words = [...new Set(got.filter((_, i) => o.words[i] === r))]; + return words.map((k) => read(rep.sim.words[k].name, at)); + }); + }; + return { signals, name, read, reads }; +} + +/** Why `full` (a signal's or memory word's full name) has the value it has at time `t`. */ +export function why(rep: Replay, full: string, t: number): Because | null { + const { signals, name, read, reads } = lookup(rep); const shown = full; let cur = full; let time = t; @@ -98,7 +153,7 @@ export function why(rep: Replay, full: string, t: number): Because | null { line: o.line, code: o.text, conds: o.conds.map((c) => ({ line: c.line, code: c.text, taken: c.taken, reads: c.reads.map((r) => read(r, at)) })), - reads: o.reads.map((r) => read(r, at)), + reads: reads(by, s.times[i], at), }; } return null; @@ -177,16 +232,7 @@ function constant(s: string): bigint | null { export function whyNot(rep: Replay, full: string, cycle: number, expected: bigint): WhyNot | null { const t = rep.at(cycle); if (t === undefined) return null; - const signals = new Map(rep.sim.signals.map((s) => [s.name, s])); - const name = (f: string) => { - const rest = f.startsWith(`${rep.top}.`) ? f.slice(rep.top.length + 1) : f; - return rest.startsWith("dut.") ? rest.slice(4) : rest; - }; - const read = (f: string, at: number): Read => { - const s = signals.get(f); - const i = s ? changeAt(s, at) : -1; - return { name: name(f), full: f, value: s && i >= 0 ? s.values[i].toString() : "?", t: s && i >= 0 ? at : null }; - }; + const { signals, name, read, reads } = lookup(rep); // Through port connections to the signal the design writes. let cur = full; for (let hop = 0; hop < 32; hop++) { @@ -233,7 +279,7 @@ export function whyNot(rep: Replay, full: string, cycle: number, expected: bigin }; const seen = new Set(); - for (const o of rep.sim.origins) { + for (const [by, o] of rep.sim.origins.entries()) { if (o.writes !== cur || /^(input|output) \S+ of /.test(o.text) || o.text.endsWith("(its starting value)")) continue; const key = `${o.file}:${o.line}:${o.text}`; if (seen.has(key)) continue; @@ -265,9 +311,9 @@ export function whyNot(rep: Replay, full: string, cycle: number, expected: bigin } cand.passed.push(cond(c, times[j])); } - if (cand.verdict === "ran") cand.reads = o.reads.map((r) => read(r, valuesAt(times[j]))); + if (cand.verdict === "ran") cand.reads = reads(by, times[j], valuesAt(times[j])); } - } else cand.reads = o.reads.map((r) => read(r, valuesAt(t))); + } else cand.reads = reads(by, t, valuesAt(t)); out.candidates.push(cand); } return out; diff --git a/apps/web/src/sim/protocol.ts b/apps/web/src/sim/protocol.ts index a3645d0..f49674b 100644 --- a/apps/web/src/sim/protocol.ts +++ b/apps/web/src/sim/protocol.ts @@ -24,6 +24,8 @@ export interface SimOrigin { reads: string[]; /** Full name of the signal it writes; empty when it writes several. */ writes: string; + /** The memories it reads a word of, in the order read; `SimWordReads` says which words. */ + words: string[]; /** The `if`/`case` decisions it is inside, outermost first, and which way each went for it to * run; `point` is that branch, as in `SimDecisions`. */ conds: { line: number; text: string; reads: string[]; taken: string; point: number }[]; @@ -40,6 +42,15 @@ export interface SimDecisions { cut: boolean; } +/** Each time an assignment that reads memory words ran, one entry per word it read. */ +export interface SimWordReads { + times: Float64Array; + /** The assignment (an index into `origins`). */ + origins: Uint32Array; + /** The word read: an index into `SimResult.words`, or NO_ORIGIN for one outside the memory. */ + words: Uint32Array; +} + export interface SimResult { log: string[]; time: number; @@ -52,6 +63,9 @@ export interface SimResult { signals: SimSignal[]; origins: SimOrigin[]; decisions: SimDecisions; + /** Memory words written or read, each like a signal: `tb.dut.mem[2]`. */ + words: SimSignal[]; + wordReads: SimWordReads; } export type SimRequest = diff --git a/apps/web/src/sim/worker.ts b/apps/web/src/sim/worker.ts index afc12b9..aad54f7 100644 --- a/apps/web/src/sim/worker.ts +++ b/apps/web/src/sim/worker.ts @@ -29,6 +29,15 @@ self.onmessage = async (ev: MessageEvent) => { cut: last.decisionsCut, }; const signals = names.map((name, i) => ({ name, width: widths[i], times: last!.times(i), values: last!.values(i), by: last!.by(i) })); + const wordWidths = last.wordWidths; + const words = last.wordNames.map((name, i) => ({ + name, + width: wordWidths[i], + times: last!.wordTimes(i), + values: last!.wordValues(i), + by: last!.wordBy(i), + })); + const wordReads = { times: last.wordReadTimes(), origins: last.wordReadOrigins(), words: last.wordReadWords() }; reply( { type: "result", @@ -45,9 +54,18 @@ self.onmessage = async (ev: MessageEvent) => { signals, origins: JSON.parse(last.origins()), decisions, + words, + wordReads, }, }, - [...signals.flatMap((s) => [s.times.buffer, s.values.buffer, s.by.buffer]), decisions.times.buffer, decisions.branches.buffer], + [ + ...[...signals, ...words].flatMap((s) => [s.times.buffer, s.values.buffer, s.by.buffer]), + decisions.times.buffer, + decisions.branches.buffer, + wordReads.times.buffer, + wordReads.origins.buffer, + wordReads.words.buffer, + ], ); } } catch (e) { diff --git a/crates/wafer-sim-wasm/src/lib.rs b/crates/wafer-sim-wasm/src/lib.rs index 900e363..7fb2cb2 100644 --- a/crates/wafer-sim-wasm/src/lib.rs +++ b/crates/wafer-sim-wasm/src/lib.rs @@ -119,7 +119,52 @@ impl Simulation { self.r.decisions_cut } - /// Where written values come from, as JSON: `[{file, line, scope, text, reads, writes, conds: + /// The memory words written or read (`tb.dut.mem[2]`); `wordTimes`, `wordValues` and `wordBy` + /// give each one's changes as for signals. + #[wasm_bindgen(getter, js_name = wordNames)] + pub fn word_names(&self) -> Vec { + self.r.words.iter().map(|s| s.name.clone()).collect() + } + + #[wasm_bindgen(getter, js_name = wordWidths)] + pub fn word_widths(&self) -> Vec { + self.r.words.iter().map(|s| s.width).collect() + } + + #[wasm_bindgen(js_name = wordTimes)] + pub fn word_times(&self, i: usize) -> Vec { + self.r.words.get(i).map(|s| s.times.iter().map(|&t| t as f64).collect()).unwrap_or_default() + } + + #[wasm_bindgen(js_name = wordValues)] + pub fn word_values(&self, i: usize) -> Vec { + self.r.words.get(i).map(|s| s.values.clone()).unwrap_or_default() + } + + #[wasm_bindgen(js_name = wordBy)] + pub fn word_by(&self, i: usize) -> Vec { + self.r.words.get(i).map(|s| s.by.clone()).unwrap_or_default() + } + + /// When each recorded memory word read happened, in order. + #[wasm_bindgen(js_name = wordReadTimes)] + pub fn word_read_times(&self) -> Vec { + self.r.word_reads.iter().map(|&(t, _, _)| t as f64).collect() + } + + /// The origin (assignment) that read it. + #[wasm_bindgen(js_name = wordReadOrigins)] + pub fn word_read_origins(&self) -> Vec { + self.r.word_reads.iter().map(|&(_, o, _)| o).collect() + } + + /// The word read: an index into `wordNames`, 0xFFFFFFFF for one outside the memory. + #[wasm_bindgen(js_name = wordReadWords)] + pub fn word_read_words(&self) -> Vec { + self.r.word_reads.iter().map(|&(_, _, k)| k).collect() + } + + /// Where written values come from, as JSON: `[{file, line, scope, text, reads, writes, words, conds: /// [{line, text, reads, taken, point}]}]`. pub fn origins(&self) -> String { let list: Vec = self @@ -142,13 +187,14 @@ impl Simulation { }) .collect(); format!( - r#"{{"file":{},"line":{},"scope":{},"text":{},"reads":{},"writes":{},"conds":[{}]}}"#, + r#"{{"file":{},"line":{},"scope":{},"text":{},"reads":{},"writes":{},"words":{},"conds":[{}]}}"#, json(&o.file), o.line, json(&o.scope), json(&o.text), strings(&o.reads), json(&o.writes), + strings(&o.words), conds.join(",") ) }) diff --git a/crates/wafer-sim/src/elab.rs b/crates/wafer-sim/src/elab.rs index d8c971c..cdcd587 100644 --- a/crates/wafer-sim/src/elab.rs +++ b/crates/wafer-sim/src/elab.rs @@ -222,6 +222,42 @@ impl CExpr { CExpr::Const { .. } | CExpr::Str(_) | CExpr::Time | CExpr::Random(_) => {} } } + + /// The memory words this expression reads (`mem[i]`, `mem[i][3:0]`): the memory and the index. + pub fn words(&self, out: &mut Vec<(usize, CExpr)>) { + match self { + CExpr::Word { sig, idx } => { + idx.words(out); + out.push((*sig, (**idx).clone())); + } + CExpr::Bit { sig, word, idx } => { + idx.words(out); + if let Some(w) = word { + w.words(out); + out.push((*sig, (**w).clone())); + } + } + CExpr::Part { sig, word, .. } => { + if let Some(w) = word { + w.words(out); + out.push((*sig, (**w).clone())); + } + } + CExpr::IPart { start, .. } => start.words(out), + CExpr::Concat(v) => v.iter().for_each(|e| e.words(out)), + CExpr::Repl(_, e) | CExpr::Unary(_, e) | CExpr::Signed(e) | CExpr::Unsigned(e) => e.words(out), + CExpr::Binary(_, a, b) => { + a.words(out); + b.words(out); + } + CExpr::Ternary(c, t, f, _) => { + c.words(out); + t.words(out); + f.words(out); + } + CExpr::Sig { .. } | CExpr::Const { .. } | CExpr::Str(_) | CExpr::Time | CExpr::Random(_) => {} + } + } } #[derive(Debug, Clone)] @@ -351,6 +387,9 @@ pub struct Origin { pub reads: Vec, /// The signal it writes (full name), or empty when it writes several (`{a, b} = ...`). pub writes: String, + /// The memories it reads a word of, in the order the words are read: each time it runs, the + /// simulation records which words those were ([`crate::SimResult::word_reads`]). + pub words: Vec, /// The `if` and `case` decisions it is inside, outermost first. pub conds: Vec, } @@ -394,6 +433,8 @@ pub struct Design { /// For each coverage point that is a branch of an `if` or `case`: the decision it belongs to /// (numbered from 0); [`NO_DECISION`] for the others. pub cover_decision: Vec, + /// For each origin: the memory words it reads, as the memory and the index to evaluate. + pub origin_words: Vec>, } /// [`Design::cover_decision`] of a coverage point that isn't a branch. @@ -850,6 +891,9 @@ impl<'a> Elab<'a> { | LVal::Part { sig, .. } | LVal::IPart { sig, .. } => self.d.signals[*sig].name.clone(), }; + let mut words = Vec::new(); + from.words(&mut words); + words.retain(|&(sig, _)| !self.d.signals[sig].hidden); self.d.origins.push(Origin { file: s.module.file.clone(), line, @@ -857,8 +901,10 @@ impl<'a> Elab<'a> { text, reads, writes, + words: words.iter().map(|&(sig, _)| self.d.signals[sig].name.clone()).collect(), conds: self.conds.clone(), }); + self.d.origin_words.push(words); Op::From((self.d.origins.len() - 1) as u32) } diff --git a/crates/wafer-sim/src/sim.rs b/crates/wafer-sim/src/sim.rs index 0966b7d..da450af 100644 --- a/crates/wafer-sim/src/sim.rs +++ b/crates/wafer-sim/src/sim.rs @@ -4,7 +4,7 @@ //! nonblocking assignments, repeating until nothing changes; then `$strobe` and //! `$monitor` output, then time advances to the next scheduled event. -use std::collections::{BTreeMap, VecDeque}; +use std::collections::{BTreeMap, HashMap, VecDeque}; use crate::ast::Edge; use crate::elab::*; @@ -73,11 +73,20 @@ pub struct SimResult { pub cover_decision: Vec, /// Set when the run made more than [`MAX_DECISIONS`] decisions: only the first are recorded. pub decisions_cut: bool, + /// The memory words written or read, each like a signal (`tb.dut.mem[2]`), in no set order. + pub words: Vec, + /// Each time an assignment that reads memory words ran: when, the origin, and the words it + /// read (indexes into [`SimResult::words`]), one entry per word in [`crate::elab::Origin::words`] + /// order. Only the first [`MAX_WORD_READS`] are recorded. + pub word_reads: Vec<(u64, u32, u32)>, } /// Decisions recorded per run, at most. pub const MAX_DECISIONS: usize = 1 << 21; +/// The most memory word reads [`SimResult::word_reads`] keeps. +pub const MAX_WORD_READS: usize = 1 << 20; + #[derive(Clone, Copy, PartialEq, Eq)] enum State { Ready, @@ -139,6 +148,10 @@ pub struct Sim<'d> { cover_hits: Vec>, decisions: Vec<(u64, u32)>, decisions_cut: bool, + words: Vec, + /// The entry in `words` of a memory word's slot. + word_of: HashMap, + word_reads: Vec<(u64, u32, u32)>, } const fn mask(w: u32) -> u64 { @@ -206,6 +219,9 @@ impl<'d> Sim<'d> { cover_hits: (0..d.cover.len()).map(|_| std::cell::Cell::new(0)).collect(), decisions: Vec::new(), decisions_cut: false, + words: Vec::new(), + word_of: HashMap::new(), + word_reads: Vec::new(), } } @@ -230,6 +246,8 @@ impl<'d> Sim<'d> { decisions: std::mem::take(&mut self.decisions), cover_decision: self.d.cover_decision.clone(), decisions_cut: self.decisions_cut, + words: self.words, + word_reads: self.word_reads, }) } @@ -339,6 +357,7 @@ impl<'d> Sim<'d> { match op { Op::From(n) => self.procs[p].from = *n, Op::Assign(lv, e) => { + self.note_word_reads(p); let w = lv.width(self.d); let v = self.eval_ctx(e, w); let writes = self.resolve(lv, v, self.procs[p].from); @@ -347,6 +366,7 @@ impl<'d> Sim<'d> { } } Op::NbAssign(lv, e, delay) => { + self.note_word_reads(p); let w = lv.width(self.d); let v = self.eval_ctx(e, w); let writes = self.resolve(lv, v, self.procs[p].from); @@ -501,6 +521,7 @@ impl<'d> Sim<'d> { fn write(&mut self, w: &Write) { let old = self.vals[w.slot]; + let word = if self.d.signals[w.sig].array.is_some() { self.word(w.sig, w.slot) } else { None }; let m = mask(w.w) << w.lo; let new = ((old & !m) | ((w.value & mask(w.w)) << w.lo)) & mask(self.d.signals[w.sig].width); if new == old { @@ -511,9 +532,29 @@ impl<'d> Sim<'d> { { *wave.by.last_mut().unwrap() = w.by; } + if let Some(k) = word { + let wave = &mut self.words[k as usize]; + if wave.by.last() == Some(&NO_ORIGIN) { + *wave.by.last_mut().unwrap() = w.by; + } + } return; } self.vals[w.slot] = new; + if let Some(k) = word + && self.changes < self.limits.max_changes + { + let wave = &mut self.words[k as usize]; + if wave.times.last() == Some(&self.now) { + *wave.values.last_mut().unwrap() = new; + *wave.by.last_mut().unwrap() = w.by; + } else { + wave.times.push(self.now); + wave.values.push(new); + wave.by.push(w.by); + self.changes += 1; + } + } if let Some(wave) = self.waves[w.sig].as_mut() && self.changes < self.limits.max_changes { @@ -533,6 +574,42 @@ impl<'d> Sim<'d> { self.notify(w.sig); } + /// The entry in `words` of the memory word at `slot` (of memory `sig`), made on first use; + /// `None` for a hidden memory. + fn word(&mut self, sig: usize, slot: usize) -> Option { + let s = &self.d.signals[sig]; + if s.hidden { + return None; + } + if let Some(&k) = self.word_of.get(&slot) { + return Some(k); + } + let lo = s.array.map(|(lo, _)| lo).unwrap_or(0); + let k = self.words.len() as u32; + self.words.push(WaveSignal { + name: format!("{}[{}]", s.name, lo + (slot - s.slot) as i64), + width: s.width, + times: vec![0], + values: vec![self.vals[slot]], + by: vec![NO_ORIGIN], + }); + self.word_of.insert(slot, k); + Some(k) + } + + /// Records the memory words the assignment process `p` is about to run reads. + fn note_word_reads(&mut self, p: usize) { + let from = self.procs[p].from; + let Some(list) = self.d.origin_words.get(from as usize) else { return }; + if list.is_empty() || self.word_reads.len() + list.len() > MAX_WORD_READS { + return; + } + for (sig, idx) in list { + let k = self.word_slot(*sig, Some(idx)).and_then(|slot| self.word(*sig, slot)); + self.word_reads.push((self.now, from, k.unwrap_or(NO_ORIGIN))); + } + } + /// An index or part-select start: unsigned unless the expression is signed, so a sized /// index such as `4'd8` (or a 4-bit `b + 4'd1` that reaches 8) isn't read as negative. fn index(&self, e: &CExpr) -> i64 { diff --git a/crates/wafer-sim/src/tests.rs b/crates/wafer-sim/src/tests.rs index 1cbd3f1..74522fb 100644 --- a/crates/wafer-sim/src/tests.rs +++ b/crates/wafer-sim/src/tests.rs @@ -509,3 +509,50 @@ endmodule assert!(r.decisions.windows(2).all(|w| w[0].0 <= w[1].0), "in time order"); assert!(!r.decisions_cut); } + +#[test] +fn memory_words_are_recorded_like_signals() { + let src = r#" +module fifo (input wire clk, input wire wr, input wire [7:0] din, input wire [1:0] rptr, output wire [7:0] dout); + reg [7:0] mem [1:4]; + reg [1:0] wptr = 0; + always @(posedge clk) if (wr) begin + mem[wptr + 1] <= din; + wptr <= wptr + 1; + end + assign dout = mem[rptr + 1] + mem[1][3:0]; +endmodule + +module tb; + reg clk = 0, wr = 0; + reg [7:0] din = 0; + reg [1:0] rptr = 0; + wire [7:0] dout; + fifo dut (.clk(clk), .wr(wr), .din(din), .rptr(rptr), .dout(dout)); + always #5 clk = ~clk; + initial begin + #1 wr = 1; din = 7; + #10 din = 9; + #10 wr = 0; rptr = 1; + #10 $finish; + end +endmodule +"#; + let r = run(src); + let word = |n: &str| r.words.iter().position(|s| s.name == n).unwrap_or_else(|| panic!("{n}")); + let (w1, w2) = (word("tb.dut.mem[1]"), word("tb.dut.mem[2]")); + assert_eq!((r.words[w1].times.as_slice(), r.words[w1].values.as_slice()), (&[0, 5][..], &[0, 7][..])); + assert_eq!((r.words[w2].times.as_slice(), r.words[w2].values.as_slice()), (&[0, 15][..], &[0, 9][..])); + let store = &r.origins[r.words[w2].by[1] as usize]; + assert_eq!((store.text.as_str(), store.line), ("mem[wptr + 1] <= din", 6)); + assert!(store.words.is_empty(), "writing a word isn't reading one"); + + // `dout` reads two words each time it is computed, in the order written. + let dout = r.signals.iter().find(|s| s.name == "tb.dut.dout").unwrap(); + let by = *dout.by.last().unwrap(); + assert_eq!(r.origins[by as usize].words, ["tb.dut.mem", "tb.dut.mem"]); + let last: Vec = r.word_reads.iter().filter(|&&(_, o, _)| o == by).map(|&(_, _, k)| k).rev().take(2).collect(); + assert_eq!(last, [w1 as u32, w2 as u32], "after rptr = 1: mem[2], then mem[1]"); + assert_eq!(*dout.values.last().unwrap(), 9 + 7); + assert!(r.word_reads.windows(2).all(|w| w[0].0 <= w[1].0), "in time order"); +} diff --git a/docs/architecture.md b/docs/architecture.md index 74b551a..bc16d08 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -30,7 +30,7 @@ | `crates/wafer-api` | The only server process: HTTP and WebSocket API, background workers for flow runs and agent proposals, database migrations (`migrations/`). | | `crates/wafer-git` | Bare Git repositories via libgit2: commits without a worktree, optimistic parents, path normalisation. | | `crates/wafer-jobs` | The `Backend` trait for flow runs and its Docker implementation; LibreLane metrics and report parsing; safe reading of run outputs. | -| `crates/wafer-sim` | Event-driven Verilog simulator (pure Rust), checked line by line against Icarus Verilog (`tests/conformance`). Besides waveforms it records branch coverage and, for every value change, the assignment that made it (`origins`: line, text, the signals read and the `if`/`case` decisions around it) and every decision made with the branch taken (`decisions`), which the web app uses to explain values and why a value wasn't the expected one. | +| `crates/wafer-sim` | Event-driven Verilog simulator (pure Rust), checked line by line against Icarus Verilog (`tests/conformance`). Besides waveforms it records branch coverage and, for every value change, the assignment that made it (`origins`: line, text, the signals read and the `if`/`case` decisions around it) every decision made with the branch taken (`decisions`), and each memory word like a signal (`words`) with the words every assignment read (`word_reads`), which the web app uses to explain values and why a value wasn't the expected one. | | `crates/wafer-gds` | GDSII reader: parse, flatten hierarchy, tessellate for the GPU. | | `crates/wafer-sim-wasm`, `crates/wafer-gds-wasm` | WebAssembly bindings used by the web app. | | `apps/web` | React + TypeScript app (Vite): Design view, Code view (Monaco), runs, layout viewer, agent, IP. | diff --git a/docs/user-guide/design-view.md b/docs/user-guide/design-view.md index fe11525..1d7d84c 100644 --- a/docs/user-guide/design-view.md +++ b/docs/user-guide/design-view.md @@ -91,8 +91,10 @@ it ("line 20 of blinker.v wrote it: `led <= ~led`"), why that line ran (each `if around it and which way it went: "`cnt == N - 1` was true (cnt = 3)"), and the values it used. Every value mentioned can be clicked in turn, so you can follow a wrong output back, one step at a time, to the decision or the example input that caused it. Values passed through a port of a -block are followed into the block; an input the example sets ends the chain ("set by step 1 of -the example"). When you then ask the AI to fix the design or the assistant to correct the +block are followed into the block, and a value read from a memory names the word it was +("It used `mem[0]` = 17, `rptr` = 0"), which leads to the line that stored it there +("`mem[wptr] <= din`"). An input the example sets ends the chain ("set by step 1 of the +example"). When you then ask the AI to fix the design or the assistant to correct the example, the explanation you followed goes with the request. **Why wasn't it the expected value?** When an example fails, the account of the difference ends