Skip to content

Commit 3daa040

Browse files
thomasahleclaude
andcommitted
Add cancel button: click spinner to abort a running simulation
- Add signal parameter to runToolInWorker / runCocotbInWorker so an AbortController can terminate the worker mid-run - Add _runController and cancel() to CirctWasmAdapter; each run* method creates a fresh AbortController and passes its signal to _invokeTool - AbortError is caught silently in all four run* catch blocks (no "runtime unavailable" noise in logs) - Run/Verify buttons now call cancelRun() instead of being disabled while their mode is active; Ctrl+Enter also cancels when running - cancelRun() appends "# run cancelled" to the log before aborting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c65ed7b commit 3daa040

2 files changed

Lines changed: 67 additions & 16 deletions

File tree

src/routes/lesson/[part]/[name]/+page.svelte

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -428,10 +428,16 @@
428428
}
429429
}
430430
431+
function cancelRun() {
432+
if (!running || !circt) return;
433+
circt.cancel();
434+
appendLogEntry('# run cancelled');
435+
}
436+
431437
function onKeydown(e) {
432438
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
433439
e.preventDefault();
434-
runSim();
440+
if (running) cancelRun(); else runSim();
435441
}
436442
}
437443
@@ -585,11 +591,11 @@
585591
<div class="absolute bottom-3 right-3 z-10 flex gap-[0.4rem]">
586592
{#if lesson.runner !== 'bmc' && lesson.runner !== 'lec'}
587593
<button
588-
onclick={() => runSim('sim')}
589-
disabled={running}
594+
onclick={() => (running && runMode === 'sim') ? cancelRun() : runSim('sim')}
595+
disabled={running && runMode !== 'sim'}
590596
data-testid="run-button"
591-
aria-label="{lesson.runner === 'cocotb' ? 'Test' : 'Run'} (Ctrl+Enter)"
592-
title="{lesson.runner === 'cocotb' ? 'Test' : 'Run'} (Ctrl+Enter)"
597+
aria-label="{(running && runMode === 'sim') ? 'Cancel' : (lesson.runner === 'cocotb' ? 'Test' : 'Run')} (Ctrl+Enter)"
598+
title="{(running && runMode === 'sim') ? 'Cancel' : (lesson.runner === 'cocotb' ? 'Test' : 'Run')} (Ctrl+Enter)"
593599
class="w-10 h-10 rounded-full bg-teal text-white flex items-center justify-center [box-shadow:0_4px_16px_rgba(0,0,0,0.25),0_2px_6px_rgba(0,0,0,0.15)] hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
594600
>
595601
{#if running && runMode === 'sim'}
@@ -605,11 +611,11 @@
605611
{/if}
606612
{#if lesson.runner === 'bmc' || lesson.runner === 'both'}
607613
<button
608-
onclick={() => runSim('bmc')}
609-
disabled={running}
614+
onclick={() => (running && runMode === 'bmc') ? cancelRun() : runSim('bmc')}
615+
disabled={running && runMode !== 'bmc'}
610616
data-testid="verify-button"
611-
aria-label="Verify"
612-
title="Verify"
617+
aria-label="{running && runMode === 'bmc' ? 'Cancel' : 'Verify'}"
618+
title="{running && runMode === 'bmc' ? 'Cancel' : 'Verify'}"
613619
class="w-10 h-10 rounded-full bg-teal text-white flex items-center justify-center [box-shadow:0_4px_16px_rgba(0,0,0,0.25),0_2px_6px_rgba(0,0,0,0.15)] hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
614620
>
615621
{#if running && runMode === 'bmc'}
@@ -625,11 +631,11 @@
625631
{/if}
626632
{#if lesson.runner === 'lec'}
627633
<button
628-
onclick={() => runSim('lec')}
629-
disabled={running}
634+
onclick={() => (running && runMode === 'lec') ? cancelRun() : runSim('lec')}
635+
disabled={running && runMode !== 'lec'}
630636
data-testid="verify-button"
631-
aria-label="Verify (LEC)"
632-
title="Verify (LEC)"
637+
aria-label="{running && runMode === 'lec' ? 'Cancel' : 'Verify (LEC)'}"
638+
title="{running && runMode === 'lec' ? 'Cancel' : 'Verify (LEC)'}"
633639
class="w-10 h-10 rounded-full bg-teal text-white flex items-center justify-center [box-shadow:0_4px_16px_rgba(0,0,0,0.25),0_2px_6px_rgba(0,0,0,0.15)] hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
634640
>
635641
{#if running && runMode === 'lec'}

src/runtime/circt-adapter.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,8 @@ function runToolInWorker({
915915
readFiles,
916916
createDirs = [],
917917
uvmManifestUrl = null,
918-
onOutput = null
918+
onOutput = null,
919+
signal = null
919920
}) {
920921
return new Promise((resolve, reject) => {
921922
const worker = new Worker(getWorkerBlobUrl());
@@ -930,6 +931,20 @@ function runToolInWorker({
930931
};
931932
resetTimeout();
932933

934+
if (signal) {
935+
if (signal.aborted) {
936+
clearTimeout(timeout);
937+
worker.terminate();
938+
reject(new DOMException('Run cancelled', 'AbortError'));
939+
return;
940+
}
941+
signal.addEventListener('abort', () => {
942+
clearTimeout(timeout);
943+
worker.terminate();
944+
reject(new DOMException('Run cancelled', 'AbortError'));
945+
}, { once: true });
946+
}
947+
933948
worker.onmessage = (event) => {
934949
const payload = event.data || {};
935950
if (payload.type === 'stream') {
@@ -1084,7 +1099,8 @@ function runCocotbInWorker({
10841099
pyCode,
10851100
shimCode,
10861101
maxTimeNs,
1087-
onLogLine = null
1102+
onLogLine = null,
1103+
signal = null
10881104
}) {
10891105
return new Promise((resolve, reject) => {
10901106
const worker = new Worker(getCocotbWorkerBlobUrl());
@@ -1099,6 +1115,20 @@ function runCocotbInWorker({
10991115
};
11001116
resetTimeout();
11011117

1118+
if (signal) {
1119+
if (signal.aborted) {
1120+
clearTimeout(timeout);
1121+
worker.terminate();
1122+
reject(new DOMException('Run cancelled', 'AbortError'));
1123+
return;
1124+
}
1125+
signal.addEventListener('abort', () => {
1126+
clearTimeout(timeout);
1127+
worker.terminate();
1128+
reject(new DOMException('Run cancelled', 'AbortError'));
1129+
}, { once: true });
1130+
}
1131+
11021132
worker.onmessage = (event) => {
11031133
const payload = event.data || {};
11041134
if (payload.type === 'log') {
@@ -1137,6 +1167,11 @@ export class CirctWasmAdapter {
11371167
this.repo = CIRCT_FORK_REPO;
11381168
this.config = getCirctRuntimeConfig();
11391169
this.ready = false;
1170+
this._runController = null;
1171+
}
1172+
1173+
cancel() {
1174+
this._runController?.abort();
11401175
}
11411176

11421177
async init() {
@@ -1170,7 +1205,8 @@ export class CirctWasmAdapter {
11701205
readFiles,
11711206
createDirs,
11721207
uvmManifestUrl,
1173-
onOutput
1208+
onOutput,
1209+
signal: this._runController?.signal ?? null
11741210
});
11751211
}
11761212

@@ -1206,6 +1242,7 @@ export class CirctWasmAdapter {
12061242
}
12071243

12081244
async run({ files, top, simulate = true, onStatus = null, onLog = null }) {
1245+
this._runController = new AbortController();
12091246
try {
12101247
await this.init();
12111248

@@ -1442,6 +1479,7 @@ export class CirctWasmAdapter {
14421479
};
14431480
} catch (error) {
14441481
if (typeof onStatus === 'function') onStatus('done');
1482+
if (error.name === 'AbortError') return { ok: false, logs: [], waveform: null };
14451483
return {
14461484
ok: false,
14471485
logs: [
@@ -1458,6 +1496,7 @@ export class CirctWasmAdapter {
14581496
}
14591497

14601498
async runBmc({ files, top, onStatus = null, onLog = null }) {
1499+
this._runController = new AbortController();
14611500
try {
14621501
await this.init();
14631502

@@ -1612,6 +1651,7 @@ export class CirctWasmAdapter {
16121651

16131652
} catch (error) {
16141653
if (typeof onStatus === 'function') onStatus('done');
1654+
if (error.name === 'AbortError') return { ok: false, logs: [] };
16151655
return {
16161656
ok: false,
16171657
logs: [
@@ -1625,6 +1665,7 @@ export class CirctWasmAdapter {
16251665
}
16261666

16271667
async runCocotb({ files, top, onStatus = null, onLog = null }) {
1668+
this._runController = new AbortController();
16281669
try {
16291670
await this.init();
16301671

@@ -1730,6 +1771,7 @@ export class CirctWasmAdapter {
17301771
pyCode,
17311772
shimCode: COCOTB_SHIM,
17321773
maxTimeNs: 1_000_000,
1774+
signal: this._runController?.signal ?? null,
17331775
onLogLine: (line) => {
17341776
sawWorkerStream = true;
17351777
emitLog(cocotbWorkerLogToStreamEntry(line));
@@ -1747,6 +1789,7 @@ export class CirctWasmAdapter {
17471789

17481790
} catch (error) {
17491791
if (typeof onStatus === 'function') onStatus('done');
1792+
if (error.name === 'AbortError') return { ok: false, logs: [] };
17501793
return {
17511794
ok: false,
17521795
logs: [String(error?.message || error)]
@@ -1755,6 +1798,7 @@ export class CirctWasmAdapter {
17551798
}
17561799

17571800
async runLec({ files, module1, module2, onStatus = null, onLog = null }) {
1801+
this._runController = new AbortController();
17581802
try {
17591803
await this.init();
17601804

@@ -1927,6 +1971,7 @@ export class CirctWasmAdapter {
19271971

19281972
} catch (error) {
19291973
if (typeof onStatus === 'function') onStatus('done');
1974+
if (error.name === 'AbortError') return { ok: false, logs: [] };
19301975
return {
19311976
ok: false,
19321977
logs: [

0 commit comments

Comments
 (0)