Skip to content

Commit 9aebfa6

Browse files
thomasahleclaude
andcommitted
Fix stdout ordering, dark editor theme, URL deep-linking, and lesson content
- App: ensure stdout always appears before stderr in log output by inserting new stdout entries before any existing stderr entry - App: initialise lessonIndex synchronously from ?lesson=N URL param so the reactive URL-updater does not strip the param before onMount reads it - App: expand the correct sidebar chapter on deep-link load - CodeEditor: add custom dark syntax-highlight palette (was using light-only default colors — comments/strings nearly invisible on dark background) - e2e: add qa-all-lessons.spec.js covering all 54 lessons (solve → run/verify) - e2e: update formal and solutions specs - runtime: circt-adapter improvements - lessons: fix checker and recursive SVA lesson content; add UVM coverage note Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6b037bb commit 9aebfa6

15 files changed

Lines changed: 199 additions & 110 deletions

File tree

e2e/formal.spec.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { test, expect } from '@playwright/test';
22

33
// Helper: expand a chapter in the sidebar and click a lesson.
4+
// Lesson buttons carry data-active; chapter buttons do not — this distinction
5+
// prevents strict-mode violations from partial name matches.
46
async function goToLesson(page, chapterName, lessonName) {
57
await page.goto('/');
6-
await page.getByRole('button', { name: chapterName }).click();
7-
await page.getByRole('button', { name: lessonName }).click();
8+
const lessonBtn = page.locator('button[data-active]').filter({ hasText: lessonName });
9+
if ((await lessonBtn.count()) === 0) {
10+
await page.locator('button:not([data-active])').filter({ hasText: chapterName }).click();
11+
}
12+
await lessonBtn.click();
813
await expect(page.getByRole('heading', { level: 2, name: lessonName })).toBeVisible();
914
}
1015

e2e/qa-all-lessons.spec.js

Lines changed: 70 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,79 @@
11
/**
22
* Full-tutorial QA: every lesson → solve → run/verify → no errors.
3-
* Lessons are visited by URL index so navigation is fast and reliable.
3+
* Lessons are visited by URL index (?lesson=N, 1-based) for reliable navigation.
44
*/
55
import { test, expect } from '@playwright/test';
66

7-
// One entry per lesson in the order they appear in the app.
7+
// One entry per lesson in the exact order they appear in the app (matches index.js).
88
// runner: 'sim' | 'bmc' | 'lec' | 'both' | 'cocotb'
99
const LESSONS = [
10-
{ title: 'Welcome', runner: 'sim' },
11-
{ title: 'Modules and Ports', runner: 'sim' },
12-
{ title: 'always_comb and case', runner: 'sim' },
13-
{ title: 'Priority Encoder', runner: 'sim' },
14-
{ title: 'Flip-Flops with always_ff', runner: 'sim' },
15-
{ title: 'Up-Counter', runner: 'sim' },
16-
{ title: 'Parameters and localparam', runner: 'sim' },
17-
{ title: 'Interfaces and modport', runner: 'sim' },
18-
{ title: 'Tasks and Functions', runner: 'sim' },
19-
{ title: 'typedef enum', runner: 'sim' },
20-
{ title: 'Two-Always Moore FSM', runner: 'sim' },
21-
{ title: 'covergroup and coverpoint', runner: 'sim' },
22-
{ title: 'Bins and ignore_bins', runner: 'sim' },
23-
{ title: 'Cross coverage', runner: 'sim' },
24-
{ title: 'Your First cocotb Test', runner: 'cocotb' },
25-
{ title: 'Clock and Timing', runner: 'cocotb' },
26-
{ title: 'Immediate Assertions', runner: 'bmc' },
27-
{ title: 'Concurrent Assertions in Simulation', runner: 'sim' },
28-
{ title: 'Vacuous Pass', runner: 'sim' },
29-
{ title: '$isunknown — Detecting X and Z', runner: 'sim' },
30-
{ title: 'Sequences and Properties', runner: 'bmc' },
31-
{ title: 'Implication: |-> and |=>', runner: 'bmc' },
32-
{ title: 'Bounded Model Checking', runner: 'bmc' },
33-
{ title: 'Clock Delay ##m and ##[m:n]', runner: 'bmc' },
34-
{ title: '$rose and $fell', runner: 'bmc' },
35-
{ title: 'Request / Acknowledge', runner: 'bmc' },
36-
{ title: 'Consecutive Repetition [*m]', runner: 'bmc' },
37-
{ title: 'Goto Repetition [->m]', runner: 'bmc' },
38-
{ title: 'Non-Consecutive Equal Repetition [=m]', runner: 'bmc' },
39-
{ title: 'throughout — Stability During a Sequence', runner: 'bmc' },
40-
{ title: 'Sequence Composition: intersect, within, and, or', runner: 'bmc' },
41-
{ title: '$stable and $past', runner: 'bmc' },
42-
{ title: '$changed and $sampled', runner: 'bmc' },
43-
{ title: 'disable iff — Reset Handling', runner: 'bmc' },
44-
{ title: 'Aborting Properties: reject_on and accept_on', runner: 'bmc' },
45-
{ title: 'cover property', runner: 'bmc' },
46-
{ title: 'Local Variables in Sequences', runner: 'bmc' },
47-
{ title: '$onehot, $onehot0, $countones', runner: 'bmc' },
48-
{ title: '.triggered — Sequence Endpoint Detection', runner: 'bmc' },
49-
{ title: 'the checker Construct', runner: 'bmc' },
50-
{ title: 'Recursive Properties', runner: 'bmc' },
51-
{ title: 'always and s_eventually', runner: 'bmc' },
52-
{ title: 'until and s_until', runner: 'bmc' },
53-
{ title: 'assume property', runner: 'both' },
54-
{ title: 'Logical Equivalence Checking', runner: 'lec' },
55-
{ title: 'The First UVM Test', runner: 'sim' },
56-
{ title: 'Sequence Items', runner: 'sim' },
57-
{ title: 'Sequences', runner: 'sim' },
58-
{ title: 'The Driver', runner: 'sim' },
59-
{ title: 'Monitor and Scoreboard', runner: 'sim' },
60-
{ title: 'Environment and Test', runner: 'sim' },
61-
{ title: 'Functional Coverage', runner: 'sim' },
62-
{ title: 'Cross Coverage', runner: 'sim' },
63-
{ title: 'Coverage-Driven Verification', runner: 'sim' },
10+
// ── SystemVerilog Basics ──────────────────────────────────────────────────────
11+
{ title: 'Welcome', runner: 'sim' },
12+
{ title: 'Modules and Ports', runner: 'sim' },
13+
{ title: 'always_comb and case', runner: 'sim' },
14+
{ title: 'Priority Encoder', runner: 'sim' },
15+
{ title: 'Flip-Flops with always_ff', runner: 'sim' },
16+
{ title: 'Up-Counter', runner: 'sim' },
17+
{ title: 'Parameters and localparam', runner: 'sim' },
18+
{ title: 'Interfaces and modport', runner: 'sim' },
19+
{ title: 'Tasks and Functions', runner: 'sim' },
20+
{ title: 'typedef enum', runner: 'sim' },
21+
{ title: 'Two-Always Moore FSM', runner: 'sim' },
22+
{ title: 'covergroup and coverpoint', runner: 'sim' },
23+
{ title: 'Bins and ignore_bins', runner: 'sim' },
24+
{ title: 'Cross coverage', runner: 'sim' },
25+
// ── SystemVerilog Assertions ──────────────────────────────────────────────────
26+
{ title: 'Concurrent Assertions in Simulation', runner: 'sim' },
27+
{ title: 'Vacuous Pass', runner: 'sim' },
28+
{ title: '$isunknown — Detecting X and Z', runner: 'sim' },
29+
{ title: 'Immediate Assertions', runner: 'bmc' },
30+
{ title: 'Sequences and Properties', runner: 'bmc' },
31+
{ title: 'Implication: |-> and |=>', runner: 'bmc' },
32+
{ title: 'Bounded Model Checking', runner: 'bmc' },
33+
{ title: 'Clock Delay ##m and ##[m:n]', runner: 'bmc' },
34+
{ title: '$rose and $fell', runner: 'bmc' },
35+
{ title: 'Request / Acknowledge', runner: 'bmc' },
36+
{ title: 'Consecutive Repetition [*m]', runner: 'bmc' },
37+
{ title: 'Goto Repetition [->m]', runner: 'bmc' },
38+
{ title: 'Non-Consecutive Equal Repetition [=m]', runner: 'bmc' },
39+
{ title: 'throughout — Stability During a Sequence', runner: 'bmc' },
40+
{ title: 'Sequence Composition: intersect, within, and, or', runner: 'bmc' },
41+
{ title: '$stable and $past', runner: 'bmc' },
42+
{ title: '$changed and $sampled', runner: 'bmc' },
43+
{ title: 'disable iff — Reset Handling', runner: 'bmc' },
44+
{ title: 'Aborting Properties: reject_on and accept_on', runner: 'bmc' },
45+
{ title: 'cover property', runner: 'bmc' },
46+
{ title: 'Local Variables in Sequences', runner: 'bmc' },
47+
{ title: '$onehot, $onehot0, $countones', runner: 'bmc' },
48+
{ title: '.triggered — Sequence Endpoint Detection', runner: 'bmc' },
49+
{ title: 'The checker Construct', runner: 'bmc' },
50+
{ title: 'Recursive Properties', runner: 'bmc' },
51+
{ title: 'assume property', runner: 'both' },
52+
{ title: 'always and s_eventually', runner: 'bmc' },
53+
{ title: 'until and s_until', runner: 'bmc' },
54+
{ title: 'Logical Equivalence Checking', runner: 'lec' },
55+
// ── UVM ──────────────────────────────────────────────────────────────────────
56+
{ title: 'The First UVM Test', runner: 'sim' },
57+
{ title: 'Sequence Items', runner: 'sim' },
58+
{ title: 'Sequences', runner: 'sim' },
59+
{ title: 'The Driver', runner: 'sim' },
60+
{ title: 'Monitor and Scoreboard', runner: 'sim' },
61+
{ title: 'Environment and Test', runner: 'sim' },
62+
{ title: 'Functional Coverage', runner: 'sim' },
63+
{ title: 'Cross Coverage', runner: 'sim' },
64+
{ title: 'Coverage-Driven Verification', runner: 'sim' },
65+
// ── cocotb ───────────────────────────────────────────────────────────────────
66+
{ title: 'Your First cocotb Test', runner: 'cocotb' },
67+
{ title: 'Clock and Timing', runner: 'cocotb' },
6468
];
6569

6670
for (const [index, lesson] of LESSONS.entries()) {
67-
test(`[${index}] ${lesson.title}`, async ({ page }) => {
68-
await page.goto(`/?lesson=${index}`);
69-
70-
// Verify we landed on the right lesson
71+
test(`[${String(index + 1).padStart(2)}] ${lesson.title}`, async ({ page }) => {
72+
// URL uses 1-based lesson index
73+
await page.goto(`/?lesson=${index + 1}`);
7174
await expect(page.getByTestId('lesson-title')).toHaveText(lesson.title, { timeout: 10_000 });
7275

73-
// Apply solution
76+
// Apply the solution
7477
const solveBtn = page.getByTestId('solve-button');
7578
if (await solveBtn.count() > 0) {
7679
await solveBtn.click();
@@ -79,27 +82,19 @@ for (const [index, lesson] of LESSONS.entries()) {
7982

8083
const logs = page.getByTestId('runtime-logs');
8184

85+
// Run simulation (sim / cocotb / both)
8286
if (lesson.runner === 'sim' || lesson.runner === 'cocotb' || lesson.runner === 'both') {
8387
await page.getByTestId('run-button').click();
8488
await expect(logs).not.toContainText('exit code: 1', { timeout: 120_000 });
85-
// Confirm a tool actually ran
86-
const toolRan = await Promise.race([
87-
expect(logs).toContainText('$ circt-sim', { timeout: 120_000 }).then(() => true).catch(() => false),
88-
expect(logs).toContainText('$ cocotb', { timeout: 120_000 }).then(() => true).catch(() => false),
89-
]);
90-
// At least one of circt-sim or cocotb must appear
91-
const logText = await logs.innerText();
92-
const ran = logText.includes('$ circt-sim') || logText.includes('$ cocotb') || logText.includes('cocotb');
93-
expect(ran, `No sim tool ran for lesson "${lesson.title}": ${logText.slice(0, 300)}`).toBe(true);
89+
// Confirm a simulation tool actually executed
90+
await expect(logs).toContainText('$ circt', { timeout: 120_000 });
9491
}
9592

93+
// Run formal / LEC (bmc / lec / both)
9694
if (lesson.runner === 'bmc' || lesson.runner === 'lec' || lesson.runner === 'both') {
97-
const verifyBtn = page.getByTestId('verify-button');
98-
await verifyBtn.click();
95+
await page.getByTestId('verify-button').click();
9996
await expect(logs).not.toContainText('exit code: 1', { timeout: 120_000 });
100-
const logText = await logs.innerText();
101-
const ran = logText.includes('$ circt-bmc') || logText.includes('$ circt-lec');
102-
expect(ran, `No formal tool ran for lesson "${lesson.title}": ${logText.slice(0, 300)}`).toBe(true);
97+
await expect(logs).toContainText('$ circt', { timeout: 120_000 });
10398
}
10499
});
105100
}

e2e/solutions.spec.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,23 @@ async function goToLesson(page, chapterName, lessonName) {
3535
if (key.startsWith('svt:')) localStorage.removeItem(key);
3636
}
3737
});
38-
await page.getByRole('button', { name: chapterName }).click();
39-
// Use a plain string (not RegExp) so lesson names with regex-special chars
40-
// like ##[m:n], $rose, [*m] are matched literally. Playwright's name:
41-
// option already does case-insensitive partial matching by default.
42-
await page.getByRole('button', { name: lessonName }).click();
38+
39+
// Lesson buttons carry a data-active attribute; chapter buttons do not.
40+
// Using this selector avoids strict-mode violations when a chapter title
41+
// partially matches a lesson name (e.g. "Core Sequences" vs "Sequences")
42+
// or when a chapter and lesson share the same name ("Functional Coverage").
43+
const lessonBtn = page.locator('button[data-active]').filter({ hasText: lessonName });
44+
45+
// The Introduction chapter is expanded by default (lesson 0). Clicking its
46+
// chapter button would toggle it closed and hide the Welcome button. So we
47+
// only click the chapter button when the lesson button is not yet in the DOM.
48+
if ((await lessonBtn.count()) === 0) {
49+
// Target only chapter buttons (no data-active) to avoid partial-name
50+
// collisions between chapter and lesson button text.
51+
await page.locator('button:not([data-active])').filter({ hasText: chapterName }).click();
52+
}
53+
54+
await lessonBtn.click();
4355
await expect(page.getByRole('heading', { level: 2, name: lessonName })).toBeVisible();
4456
}
4557

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@codemirror/legacy-modes": "^6.5.2",
21+
"@replit/codemirror-vim": "^6.3.0",
2122
"@tailwindcss/vite": "^4.2.0",
2223
"bits-ui": "^2.16.2",
2324
"clsx": "^2.1.1",

src/App.svelte

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,16 @@
1212
let hSplit = 33; // lesson pane % of main section width
1313
let vSplit = 65; // editor pane % of lab section height
1414
15-
let lessonIndex = 0;
16-
let lesson = lessons[0];
15+
// Initialise from the ?lesson=N URL param synchronously so the reactive URL
16+
// updater (which runs before onMount) sees the correct index from the start
17+
// and does not strip the query param before onMount can read it.
18+
function _parseLessonParam() {
19+
if (typeof window === 'undefined') return 0;
20+
const n = Number(new URLSearchParams(window.location.search).get('lesson'));
21+
return (Number.isFinite(n) && n >= 1 && n <= lessons.length) ? n - 1 : 0;
22+
}
23+
let lessonIndex = _parseLessonParam();
24+
let lesson = lessons[lessonIndex];
1725
let starterFiles = cloneFiles(lesson.files.a);
1826
let solutionFiles = mergeFiles(starterFiles, lesson.files.b);
1927
@@ -35,7 +43,7 @@
3543
let showOptions = false;
3644
let completedSlugs = new Set();
3745
let sidebarOpen = true;
38-
let expandedChapters = new Set([lessons[0].chapterTitle]);
46+
let expandedChapters = new Set([lessons[lessonIndex].chapterTitle]);
3947
let sidebarInnerEl;
4048
let copyEnabled = false;
4149
let showCopyModal = false;
@@ -279,6 +287,20 @@
279287
return;
280288
}
281289
290+
// Always place stdout before stderr: if adding stdout and stderr already exists,
291+
// insert the new stdout entry immediately before the first stderr entry.
292+
if (isStdout) {
293+
const stderrIndex = logs.findIndex(isStderrEntry);
294+
if (stderrIndex >= 0) {
295+
logs = trimLogEntries([
296+
...logs.slice(0, stderrIndex),
297+
`${prefix}${trimStreamPayload(payload)}`,
298+
...logs.slice(stderrIndex),
299+
]);
300+
return;
301+
}
302+
}
303+
282304
logs = trimLogEntries([...logs, `${prefix}${trimStreamPayload(payload)}`]);
283305
}
284306

src/lessons/sva/checker/description.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
handshake_check c1(clk, req1, ack1);
1212
handshake_check c2(clk, req2, ack2);
1313
endmodule</pre>
14-
<p>Open <code>top.sv</code> and complete the property inside the <code>handshake_check</code> checker.</p>
14+
<p>The WASM tool does not yet lower the <code>checker</code>/<code>endchecker</code> keywords, so this exercise uses a regular <code>module</code> instead — the semantics are identical for verification purposes. Open <code>top.sv</code> and complete the property inside <code>handshake_check</code>.</p>
1515
<blockquote><p>Checkers can also accept <code>sequence</code>, <code>property</code>, and <code>event</code> typed formal arguments, making them fully parameterisable assertion bundles — the SVA equivalent of generic verification IP.</p></blockquote>

src/lessons/sva/checker/top.sol.sv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
checker handshake_check(input logic clk, req, ack);
1+
module handshake_check(input logic clk, req, ack);
22
property p;
33
@(posedge clk) req |=> ##[1:3] ack;
44
endproperty
55
req_ack_a: assert property (p);
6-
endchecker
6+
endmodule
77

88
module top(input logic clk,
99
input logic req1, ack1,

src/lessons/sva/checker/top.sv

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
// A checker is an encapsulated, reusable assertion block
2-
checker handshake_check(input logic clk, req, ack);
1+
// A checker is an encapsulated, reusable assertion block.
2+
// (The tool represents it as a module; semantics are identical.)
3+
module handshake_check(input logic clk, req, ack);
34
property p;
45
@(posedge clk)
56
// TODO: req |=> ##[1:3] ack;
67
;
78
endproperty
89
req_ack_a: assert property (p);
9-
endchecker
10+
endmodule
1011

1112
// Instantiate the checker for two independent channels
1213
module top(input logic clk,

0 commit comments

Comments
 (0)