(null);
48 |
49 | useEffect(() => {
50 | editorRef.current = monaco.editor.create(containerRef.current!, {
51 | model: state.model,
52 | readOnly: state.readOnly,
53 | padding,
54 | });
55 | actions.forEach(action => editorRef.current?.addAction(action));
56 | const resizeObserver = new ResizeObserver(events => editorRef.current?.layout());
57 | resizeObserver.observe(containerRef.current!);
58 | editorRef.current.restoreViewState(state.viewState);
59 | if (focus)
60 | editorRef.current.focus();
61 | return () => {
62 | state.viewState = editorRef.current!.saveViewState();
63 | resizeObserver.disconnect();
64 | editorRef.current?.dispose();
65 | };
66 | }, [actions]);
67 |
68 | return ;
69 | }
70 |
--------------------------------------------------------------------------------
/src/proto.ts:
--------------------------------------------------------------------------------
1 | export interface LoadPackagesMessage {
2 | type: 'loadPackages',
3 | pkgs: string[]
4 | }
5 |
6 | export interface RunPythonMessage {
7 | type: 'runPython';
8 | code: string;
9 | }
10 |
11 | export type HostToWorkerMessage =
12 | | LoadPackagesMessage
13 | | RunPythonMessage;
14 |
15 | export interface StdoutWriteMessage {
16 | type: 'stdoutWrite',
17 | text: string
18 | }
19 |
20 | export interface StderrWriteMessage {
21 | type: 'stderrWrite',
22 | text: string
23 | }
24 |
25 | export interface ShowRtlilMessage {
26 | type: 'showRtlil',
27 | code: string
28 | }
29 |
30 | export interface ShowVerilogMessage {
31 | type: 'showVerilog',
32 | code: string
33 | }
34 |
35 | export interface ShowWaveformsMessage {
36 | type: 'showWaveforms',
37 | data: object
38 | }
39 |
40 | export interface PythonDoneMessage {
41 | type: 'pythonDone';
42 | error: string | null;
43 | }
44 |
45 | export type WorkerToHostMessage =
46 | | StdoutWriteMessage
47 | | StderrWriteMessage
48 | | ShowRtlilMessage
49 | | ShowVerilogMessage
50 | | ShowWaveformsMessage
51 | | PythonDoneMessage;
52 |
--------------------------------------------------------------------------------
/src/pyodide.ts:
--------------------------------------------------------------------------------
1 | // Slightly cursed wrapper to integrate with esbuild.
2 |
3 | import 'pyodide/pyodide.asm.js';
4 | // @ts-ignore
5 | import pyodideStdLib from 'pyodide/python_stdlib.zip';
6 | import pyodideLockFile from 'pyodide/pyodide-lock.json';
7 | import { loadPyodide as originalLoadPyodide } from 'pyodide';
8 |
9 | export type { PyodideInterface } from 'pyodide';
10 | export type { PyProxy } from 'pyodide/ffi';
11 |
12 | export const loadPyodide: typeof originalLoadPyodide = function(options) {
13 | return originalLoadPyodide({
14 | indexURL: '.',
15 | stdLibURL: pyodideStdLib,
16 | // @ts-ignore
17 | lockFileURL: pyodideLockFile,
18 | ...options
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/runner.ts:
--------------------------------------------------------------------------------
1 | import { WorkerToHostMessage } from './proto';
2 |
3 | const workerURL = `app.worker.js?hash=${globalThis.GIT_COMMIT.substr(0, 8)}`;
4 |
5 | export class PythonError extends Error {}
6 |
7 | export class ToolRunner {
8 | #worker = new Worker(workerURL, { type: 'module' });
9 | #packages: null | string[] = null;
10 |
11 | private initializeWorker(packages: string[]): Worker {
12 | if (JSON.stringify(this.#packages) !== JSON.stringify(packages)) {
13 | if (this.#worker !== null && this.#packages !== null) {
14 | // Terminate and re-initialize if the set of packages has changed.
15 | this.#worker.terminate();
16 | this.#worker = new Worker(workerURL, { type: 'module' });
17 | }
18 | this.#worker.postMessage({ type: 'loadPackages', pkgs: packages });
19 | this.#packages = packages;
20 | }
21 | return this.#worker;
22 | }
23 |
24 | preloadPackages(packages: string[]) {
25 | this.initializeWorker(packages);
26 | }
27 |
28 | runPython(code: string, options: {
29 | packages: string[],
30 | onStdout: (text: string) => void,
31 | onStderr: (text: string) => void,
32 | onShowRtlil: (code: string) => void,
33 | onShowVerilog: (code: string) => void,
34 | onShowWaveforms: (data: object) => void,
35 | }): Promise {
36 | console.log('[Host] Running', { packages: options.packages, code });
37 | const worker = this.initializeWorker(options.packages);
38 | return new Promise((resolve, reject) => {
39 | function onmessage(event: MessageEvent) {
40 | console.log('[Host] Received', event.data);
41 | if (event.data.type === 'stdoutWrite') {
42 | options.onStdout(event.data.text);
43 | } else if (event.data.type === 'stderrWrite') {
44 | options.onStderr(event.data.text);
45 | } else if (event.data.type === 'showRtlil') {
46 | options.onShowRtlil(event.data.code);
47 | } else if (event.data.type === 'showVerilog') {
48 | options.onShowVerilog(event.data.code);
49 | } else if (event.data.type === 'showWaveforms') {
50 | options.onShowWaveforms(event.data.data);
51 | } else if (event.data.type === 'pythonDone') {
52 | worker.removeEventListener('message', onmessage);
53 | worker.removeEventListener('error', onerror);
54 | if (event.data.error === null)
55 | resolve();
56 | else
57 | reject(new PythonError(event.data.error));
58 | } else {
59 | reject(new Error(`[Host] Unexpected message ${(event.data as any).type}`));
60 | }
61 | }
62 | function onerror(event: ErrorEvent) {
63 | console.log('[Host] Failure', event.error);
64 | worker.removeEventListener('message', onmessage);
65 | reject(event.error);
66 | }
67 | worker.addEventListener('message', onmessage);
68 | worker.addEventListener('error', onerror, { once: true });
69 | worker.postMessage({ type: 'runPython', code });
70 | });
71 | }
72 | }
73 |
74 | export let runner = new ToolRunner();
75 |
--------------------------------------------------------------------------------
/src/worker.ts:
--------------------------------------------------------------------------------
1 | import { loadPyodide, PyodideInterface, PyProxy } from './pyodide';
2 | import { runYosys, Exit as YosysExit } from '@yowasp/yosys';
3 |
4 | import { HostToWorkerMessage, WorkerToHostMessage } from './proto';
5 |
6 | function postMessage(data: WorkerToHostMessage, transfer?: Transferable[]) {
7 | console.log('[Worker] Sending', data);
8 | self.postMessage(data, { transfer });
9 | }
10 |
11 | function runInNewContext(pyodide: PyodideInterface, code: string) {
12 | const dict = pyodide.globals.get('dict');
13 | const globals = dict();
14 | try {
15 | return pyodide.runPython(code, { globals, locals: globals });
16 | } finally {
17 | globals.destroy();
18 | dict.destroy();
19 | }
20 | }
21 |
22 | // Start preloading Yosys.
23 | const yosysPromise = (runYosys() as unknown as Promise).then(() => {
24 | console.log('[Worker] Preloaded Yosys');
25 | });
26 |
27 | // Start preloading Pyodide.
28 | let pyodidePromise = loadPyodide({
29 | env: {
30 | HOME: '/',
31 | AMARANTH_USE_YOSYS: 'javascript',
32 | },
33 | stdout: line => postMessage({ type: 'stdoutWrite', text: `${line}\n` }),
34 | stderr: line => postMessage({ type: 'stderrWrite', text: `${line}\n` }),
35 | jsglobals: {
36 | Object,
37 | fetch: fetch.bind(globalThis),
38 | setTimeout: setTimeout.bind(globalThis),
39 | clearTimeout: clearTimeout.bind(globalThis),
40 | runAmaranthYosys: (args: PyProxy, stdinText: string) => {
41 | let stdin = new TextEncoder().encode(stdinText);
42 | const stdout: string[] = [];
43 | const stderr: string[] = [];
44 | try {
45 | runYosys(args.toJs(), {}, {
46 | stdin: (length) => {
47 | if (stdin.length === 0)
48 | return null;
49 | let chunk = stdin.subarray(0, length);
50 | stdin = stdin.subarray(length);
51 | return chunk;
52 | },
53 | stdout: data => data ? stdout.push(new TextDecoder().decode(data)) : null,
54 | stderr: data => data ? stderr.push(new TextDecoder().decode(data)) : null,
55 | synchronously: true,
56 | });
57 | return [0, stdout.join(''), stderr.join('')];
58 | } catch(e) {
59 | if (e instanceof YosysExit) {
60 | return [e.code, stdout.join(''), stderr.join('')];
61 | } else {
62 | throw e;
63 | }
64 | } finally {
65 | args.destroy();
66 | }
67 | }
68 | }
69 | }).then((pyodide) => {
70 | const show_waveforms = runInNewContext(pyodide, `\
71 | import contextlib
72 |
73 | def vcd_to_d3wave(vcd_file):
74 | from vcd.reader import TokenKind, tokenize
75 |
76 | root = {"name": "design", "type": {"name": "struct"}, "children": []}
77 | scope = [root]
78 | by_id = {}
79 | time = 0
80 | for token in tokenize(open(vcd_file, "rb")):
81 | if token.kind == TokenKind.SCOPE:
82 | new_child = {
83 | "name": token.data.ident,
84 | "type": {"name": "struct"},
85 | "children": []
86 | }
87 | scope[-1]["children"].append(new_child)
88 | scope.append(new_child)
89 | elif token.kind == TokenKind.UPSCOPE:
90 | scope.pop()
91 | elif token.kind == TokenKind.VAR:
92 | new_child = {
93 | "name": token.data.reference,
94 | "type": {"name": token.data.type_.value, "width": token.data.size},
95 | "data": []
96 | }
97 | scope[-1]["children"].append(new_child)
98 | by_id[token.data.id_code] = new_child
99 | elif token.kind == TokenKind.CHANGE_TIME:
100 | time = token.data
101 | elif token.kind in (TokenKind.CHANGE_SCALAR, TokenKind.CHANGE_VECTOR):
102 | signal = by_id[token.data.id_code]
103 | value = token.data.value
104 | if isinstance(value, int):
105 | value = f"{value:0{signal['type']['width']}d}"
106 | signal["data"].append([time, value])
107 |
108 | return (root if len(root["children"]) > 1 else root["children"][-1])
109 |
110 |
111 | @contextlib.contextmanager
112 | def show_waveforms(sim):
113 | import amaranth_playground
114 |
115 | vcd_file = "/tmp/waveforms.vcd"
116 | with sim.write_vcd(vcd_file=vcd_file):
117 | yield
118 |
119 | amaranth_playground._show_waveforms(vcd_to_d3wave(vcd_file=vcd_file))
120 |
121 | show_waveforms
122 | `);
123 |
124 | pyodide.registerJsModule('amaranth_playground', {
125 | show_rtlil: (code: string) => postMessage({ type: 'showRtlil', code }),
126 | show_verilog: (code: string) => postMessage({ type: 'showVerilog', code }),
127 | _show_waveforms: (data: PyProxy) => postMessage({
128 | type: 'showWaveforms',
129 | data: data.toJs({ dict_converter: Object.fromEntries })
130 | }),
131 | show_waveforms,
132 | });
133 | console.log('[Worker] Pyodide loaded');
134 | return pyodide;
135 | });
136 |
137 | self.onmessage = async (event: MessageEvent) => {
138 | console.log('[Worker] Received', event.data);
139 | if (event.data.type === 'loadPackages') {
140 | const pyodide = await pyodidePromise;
141 | // Make sure the following call to `loadPackages` or `runPython` waits until it's done.
142 | pyodidePromise = pyodide.loadPackage(event.data.pkgs).then(() => pyodide);
143 | } else if (event.data.type === 'runPython') {
144 | // Wait until all resources are loaded that the final `runYosys` call becomes synchronous.
145 | await yosysPromise;
146 | const pyodide = await pyodidePromise;
147 | try {
148 | runInNewContext(pyodide, event.data.code);
149 | postMessage({ type: 'pythonDone', error: null });
150 | } catch (e) {
151 | postMessage({ type: 'pythonDone', error: e.message });
152 | }
153 | } else {
154 | throw new Error(`[Worker] Unexpected message ${(event.data as any).type}`);
155 | }
156 | }
157 |
158 | self.onerror = (event) => {
159 | console.error('[Worker] Failure', event);
160 | };
161 |
--------------------------------------------------------------------------------