Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 66 additions & 20 deletions apps/web/src/lib/why.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<SimResult, Map<number, number[]>>();

/** 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -233,7 +279,7 @@ export function whyNot(rep: Replay, full: string, cycle: number, expected: bigin
};

const seen = new Set<string>();
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;
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/sim/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[];
Expand All @@ -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;
Expand All @@ -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 =
Expand Down
20 changes: 19 additions & 1 deletion apps/web/src/sim/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ self.onmessage = async (ev: MessageEvent<SimRequest>) => {
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",
Expand All @@ -45,9 +54,18 @@ self.onmessage = async (ev: MessageEvent<SimRequest>) => {
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) {
Expand Down
50 changes: 48 additions & 2 deletions crates/wafer-sim-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
self.r.words.iter().map(|s| s.name.clone()).collect()
}

#[wasm_bindgen(getter, js_name = wordWidths)]
pub fn word_widths(&self) -> Vec<u32> {
self.r.words.iter().map(|s| s.width).collect()
}

#[wasm_bindgen(js_name = wordTimes)]
pub fn word_times(&self, i: usize) -> Vec<f64> {
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<u64> {
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<u32> {
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<f64> {
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<u32> {
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<u32> {
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<String> = self
Expand All @@ -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(",")
)
})
Expand Down
46 changes: 46 additions & 0 deletions crates/wafer-sim/src/elab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -351,6 +387,9 @@ pub struct Origin {
pub reads: Vec<String>,
/// 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<String>,
/// The `if` and `case` decisions it is inside, outermost first.
pub conds: Vec<Cond>,
}
Expand Down Expand Up @@ -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<u32>,
/// For each origin: the memory words it reads, as the memory and the index to evaluate.
pub origin_words: Vec<Vec<(usize, CExpr)>>,
}

/// [`Design::cover_decision`] of a coverage point that isn't a branch.
Expand Down Expand Up @@ -850,15 +891,20 @@ 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,
scope: s.path.clone(),
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)
}

Expand Down
Loading