diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3c7a1..fa81fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to Wafersmith are recorded here. The format follows ### Added +- **The example: use what the design did**: when an example fails and the design is right, one + click rewrites the failing check from the simulation (moved a few cycles, a longer wait-until, a + shorter hold, or the value the design gave), re-simulates until it passes, and lists the changes + before applying them. No AI and no cost. - **What a proposal does**: agent proposals open on a behaviour tab. **Compare behaviour** runs every example before and after the change (in the browser) and describes each output that behaves differently ("`led` now changes 1 cycle earlier"), and compares examples passing, diff --git a/apps/web/src/components/Design.tsx b/apps/web/src/components/Design.tsx index eb1596c..6238198 100644 --- a/apps/web/src/components/Design.tsx +++ b/apps/web/src/components/Design.tsx @@ -14,6 +14,7 @@ import { type StepOp, } from "../api"; import { extendTestbench, fmt as fmtValue, replay, targetSpec, type Replay } from "../lib/replay"; +import { findFix, type Fix } from "../lib/fixexample"; import { StateMachine } from "./StateMachine"; import { TryIt } from "./TryIt"; import { SimClient } from "../sim/client"; @@ -830,6 +831,8 @@ function Examples({ busy={busy} onFixDesign={(story) => fixDesign(sel, story)} onFixExample={(story) => fixExample(sel, story)} + baseSpec={spec} + onReplaceSteps={(steps) => setEx({ steps })} /> )}
@@ -939,16 +942,46 @@ function Happened({ busy, onFixDesign, onFixExample, + baseSpec, + onReplaceSteps, }: { projectId: string; + /** The spec as this example sees it (the chip's pins for a chip example). */ spec: Spec; + /** The project's spec, from which the server compiles a changed example. */ + baseSpec: Spec; ex: SpecExample; canEdit: boolean; busy: boolean; onFixDesign: (story: string) => void; onFixExample: (story: string) => void; + onReplaceSteps: (steps: SpecExample["steps"]) => void; }) { const [rep, setRep] = useState(null); + // "The design is right": a change to the example that makes it pass, worked out and re-checked. + const [fix, setFix] = useState(null); + async function acceptDesign() { + if (!rep) return; + setFix("working"); + const client = new SimClient(); + try { + const snap = await api.snapshot(projectId); + const design = Object.entries(snap.files) + .filter(([p]) => /\.s?v$/.test(p) && !p.startsWith("tb/")) + .map(([name, source]) => ({ name, source })); + const simulate = async (changed: SpecExample) => { + const tb = await api.exampleTestbench(projectId, { spec: baseSpec, example: changed }); + const sim = await client.run([...design, { name: tb.path, source: extendTestbench(tb.source, spec) }], tb.top); + return replay(spec, changed, sim); + }; + setFix((await findFix(ex, rep, simulate)) ?? "none"); + } catch (e) { + setError((e as Error).message); + setFix(null); + } finally { + client.dispose(); + } + } const [error, setError] = useState(null); // Fit the cycles to the space available; long examples scroll. const box = useRef(null); @@ -1033,11 +1066,47 @@ function Happened({ +
)} + {fix === "working" &&

Working out the change and checking it…

} + {fix === "none" && ( +

+ There's no simple change (a check a few cycles off, a wait too short, a different value) that makes this example pass. + Ask the assistant, or the AI to fix the design. +

+ )} + {fix && typeof fix === "object" && ( +
+ {fix.passes ? "This change makes the example pass:" : "This change gets closer, but the example still fails:"} + +
+ + + {fix.passes && Then save the spec to keep it.} +
+
+ )} ); } diff --git a/apps/web/src/lib/fixexample.ts b/apps/web/src/lib/fixexample.ts new file mode 100644 index 0000000..7e00053 --- /dev/null +++ b/apps/web/src/lib/fixexample.ts @@ -0,0 +1,112 @@ +// "The design is right": when an example fails because the example is off (a check a cycle too +// early, a wait too short, a wrong value), change the example to what the design did, without the +// AI. Each fix is the smallest that matches the simulation; the fixed example is simulated again +// (with the testbench the server generates) until it passes, so what is offered is known to pass. + +import type { SpecExample, SpecStep } from "../api"; +import type { Replay } from "./replay"; + +export interface Fix { + steps: SpecStep[]; + /** What changed, step by step, in words. */ + edits: string[]; + /** Whether the example passes with all the edits. */ + passes: boolean; +} + +/** How far a value may have moved in time and still count as "a few cycles off". */ +const MAX_SHIFT = 5; + +const wait = (cycles: number, note = ""): SpecStep => ({ op: "wait", signal: "", value: "", cycles, note }); + +/** `v` written the way `like` was (0x.., 0b.. or decimal). */ +function writeLike(like: string, v: bigint): string { + const t = like.trim().toLowerCase(); + if (t.startsWith("0x") || /'h/.test(t)) return `0x${v.toString(16).toUpperCase()}`; + if (t.startsWith("0b") || /'b/.test(t)) return `0b${v.toString(2)}`; + return v.toString(); +} + +const plural = (n: number, w: string) => `${n} ${w}${n === 1 ? "" : "s"}`; + +/** The smallest change to `steps` that makes the first failing check match the simulation. */ +export function oneFix(steps: SpecStep[], rep: Replay): { steps: SpecStep[]; edit: string } | null { + const f = rep.firstFail; + if (!f) return null; + const i = f.step - 1; + const st = steps[i]; + if (!st) return null; + const at = `Step ${f.step} (${st.op.replace("_", " ")} ${st.signal} = ${st.value})`; + const read = (c: number) => (c >= 0 ? rep.read(f.signal, c) : null); + const next = steps.map((s) => ({ ...s })); + + if (f.op === "expect" || (f.op === "expect_for" && f.failCycle === f.from)) { + const n = f.op === "expect_for" ? f.to - f.from + 1 : 1; + const holds = (c: number) => Array.from({ length: n }, (_, k) => read(c + k)).every((v) => v === f.expected); + for (let d = 1; d <= MAX_SHIFT; d++) { + // A little later: wait before the check. + if (holds(f.from + d)) { + next.splice(i, 0, wait(d, "the design is this much slower")); + return { steps: next, edit: `${at}: checked ${plural(d, "cycle")} later` }; + } + // A little earlier: shorten the wait just before it. + const prev = next[i - 1]; + if (prev?.op === "wait" && prev.cycles > d && holds(f.from - d)) { + prev.cycles -= d; + return { steps: next, edit: `${at}: checked ${plural(d, "cycle")} earlier (the wait before it is shorter)` }; + } + } + } + if (f.op === "expect_for" && f.failCycle !== undefined && f.failCycle > f.from) { + // It holds, just not for as long: keep what holds, and keep the rest of the example's timing. + const kept = f.failCycle - f.from; + const total = f.to - f.from + 1; + next[i].cycles = kept; + next.splice(i + 1, 0, wait(total - kept)); + return { steps: next, edit: `${at}: must stay ${st.value} for ${plural(kept, "cycle")} instead of ${total}` }; + } + if (f.op === "wait_until") { + // It does become the value, just later than the limit. + for (let c = f.from; c <= f.from + st.cycles + 20; c++) { + if (read(c) === f.expected) { + next[i].cycles = Math.max(1, c - f.from); + return { steps: next, edit: `${at}: waits up to ${plural(next[i].cycles, "cycle")} instead of ${st.cycles}` }; + } + } + return null; + } + // Otherwise the value itself: what the design had. + const seen = f.op === "expect_for" ? read(f.from) : f.actual; + if (seen === null || seen === undefined) return null; + if (f.op === "expect_for") { + const n = f.to - f.from + 1; + if (!Array.from({ length: n }, (_, k) => read(f.from + k)).every((v) => v === seen)) return null; + } + next[i].value = writeLike(st.value, seen); + return { steps: next, edit: `${at}: expects ${st.signal} = ${next[i].value} instead of ${st.value}` }; +} + +/** + * Fixes the example one failing check at a time, simulating it again after each fix, until it + * passes or no simple fix is left (null if there wasn't even one). + */ +export async function findFix( + ex: SpecExample, + first: Replay, + simulate: (ex: SpecExample) => Promise, + maxRounds = 6, +): Promise { + let steps = ex.steps; + let rep = first; + const edits: string[] = []; + for (let round = 0; round < maxRounds; round++) { + if (!rep.firstFail && !rep.problem) return { steps, edits, passes: true }; + const f = oneFix(steps, rep); + if (!f) break; + steps = f.steps; + edits.push(f.edit); + rep = await simulate({ ...ex, steps }); + } + if (!edits.length) return null; + return { steps, edits, passes: !rep.firstFail && !rep.problem }; +} diff --git a/apps/web/src/lib/replay.ts b/apps/web/src/lib/replay.ts index 5163690..da3269c 100644 --- a/apps/web/src/lib/replay.ts +++ b/apps/web/src/lib/replay.ts @@ -45,6 +45,8 @@ export interface Replay { story: string; /** Set when the replay couldn't follow the run (e.g. it stopped early). */ problem: string | null; + /** A connection's value at a cycle's check point, including the cycles after the example. */ + read: (port: string, cycle: number) => bigint | null; } const MAX_CYCLES = 4000; @@ -173,7 +175,7 @@ export function replay(spec: Spec, ex: SpecExample, sim: SimResult, internal: { ...internal.map((p): Row => ({ name: p.name, dir: "internal", width: p.width, values: Array.from({ length: cycles }, (_, k) => read(p.name, k)) })), ]; const firstFail = checks.find((ch) => !ch.ok) ?? null; - return { cycles, exampleCycles, rows, checks, sets, firstFail, story: explain(spec, read, firstFail, checks.length), problem }; + return { cycles, exampleCycles, rows, checks, sets, firstFail, story: explain(spec, read, firstFail, checks.length), problem, read }; } function explain(spec: Spec, read: (port: string, c: number) => bigint | null, f: Check | null, total: number): string { diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index ddea342..a85c004 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -2645,3 +2645,9 @@ a.button { .behaviour-list > li { margin-bottom: 8px; } + +/* "Use what the design did": the proposed change to an example. */ +.fix-preview ul { + margin: 4px 0 8px; + padding-left: 20px; +} diff --git a/docs/user-guide/design-view.md b/docs/user-guide/design-view.md index 1493823..5a2156f 100644 --- a/docs/user-guide/design-view.md +++ b/docs/user-guide/design-view.md @@ -83,11 +83,20 @@ Selecting an example simulates it in your browser and replays its steps against by cycle: each connection's value, the example's checks laid over it (amber), any difference in red, and the inputs it sets. The first difference is explained in plain words, including when the expected value did appear ("it became 1 one cycle later, so the design may be one cycle slower -than the example says"). Two buttons answer the one question to decide: - -- **The design is wrong**: ask the AI to fix the design, with this explanation; -- **The example is wrong**: ask the assistant to re-check the example against the spec and - correct it. +than the example says"). Three buttons answer the one question to decide, **Which is wrong?**: + +- **The design: ask the AI to fix it**, with this explanation; +- **The example: use what the design did**: rewrites the failing check from the simulation, + without AI. If the expected value appears a few cycles later or earlier, the check moves + ("Step 3 (expect led = 1): checked 1 cycle later", by adding a wait or shortening the one + before it); a wait-until gets more cycles; a hold check covers only the cycles it held; + otherwise the check expects the value the design gave, written the same way (`0x`, `0b` or + decimal). The changed example is simulated again (repeating for further failing checks), and the + changes are listed first: **Change the example** applies them only if the example then passes, + and saving the spec keeps them. Use it when the design is right and the example's timing or + value was a guess; +- **The example: ask the assistant to correct it**: re-checks the example against the spec in + words and corrects it. ## Health