Skip to content

Commit 1fa0d9a

Browse files
authored
Merge pull request #92 from ut-code/refactor-worker-runtime
ruby&javascript追加、再度リファクタ
2 parents 6594b8b + 6644621 commit 1fa0d9a

File tree

18 files changed

+1185
-378
lines changed

18 files changed

+1185
-378
lines changed

app/terminal/README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ
1414
* ランタイムの初期化が完了したか、不要である場合true
1515
* mutex?: `MutexInterface`
1616
* ランタイムに排他制御が必要な場合、MutexInterfaceのインスタンスを返してください。
17-
* interrupt?: `() => Promise<void>`
18-
* 実行中のコマンドを中断します。呼び出し側でmutexのロックはされません
17+
* interrupt?: `() => void`
18+
* 実行中のコマンドを中断します。
19+
* 呼び出し側でmutexのロックはしません。interrupt()を呼ぶ際にはrunCommand()やrunFiles()が実行中であるためmutexはすでにロックされているはずです。
20+
* interrupt()内で実行中の処理のPromiseをrejectしたあと、runtimeを再開する際の処理に必要であればmutexをロックすることも可能です。
1921

2022
### REPL用
2123

@@ -25,7 +27,7 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ
2527
* checkSyntax?: `(code: string) => Promise<SyntaxStatus>`
2628
* コードの構文チェックを行います。行がコマンドとして完結していれば`complete`、次の行に続く場合(if文の条件式の途中など)は`incomplete`を返してください。
2729
* REPLでEnterを押した際の動作に影響します。
28-
* 呼び出し側でmutexのロックはされません
30+
* 呼び出し側でmutexのロックはせず、必要であればcheckSyntax()内でロックします。
2931
* splitReplExamples?: `(code: string) => ReplCommands[]`
3032
* markdown内に記述されているREPLのサンプルコードをパースします。例えば
3133
```
@@ -51,7 +53,7 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ
5153
5254
* runFiles: `(filenames: string[]) => Promise<ReplOutput[]>`
5355
* 指定されたファイルを実行します。ファイルの中身はEmbedContextから取得されます。
54-
* 呼び出し側でmutexのロックはされません
56+
* 呼び出し側でmutexのロックはせず、必要であればrunFiles()内でロックします。
5557
* getCommandlineStr: `(filenames: string[]) => string`
5658
* 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。
5759
@@ -122,11 +124,20 @@ EditorComponent コンポーネントを提供します。
122124
123125
## 各言語の実装
124126
125-
### Pyodide (Python)
127+
### Worker
126128
127-
Pyodide を web worker で動かしています。worker側のスクリプトは /public/python.worker.js にあります。
129+
web worker でコードを実行する実装です。worker側のスクリプトは /public にあります。
130+
workerとの通信部分は言語によらず共通なので、それをworker/runtime.tsxで定義しています。
131+
Contextは言語ごとに分けて(worker/pyodide.ts などで)定義しています。
128132
129-
### Wandbox (C++)
133+
Pythonの実行環境にはPyodideを使用しています。
134+
PyodideにはKeyboardInterruptを送信する機能があるのでinterrupt()でそれを利用しています。
135+
136+
Rubyの実行環境にはruby.wasmを使用しています。
137+
138+
JavaScriptはeval()を使用しています。runFiles()のAPIだけ実装していません。
139+
140+
### Wandbox
130141
131142
wandbox.org のAPIを利用してC++コードを実行しています。C++以外にもいろいろな言語に対応しています。
132143

app/terminal/editor.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ const AceEditor = dynamic(
1111
await import("ace-builds/src-min-noconflict/ext-language_tools");
1212
await import("ace-builds/src-min-noconflict/ext-searchbox");
1313
await import("ace-builds/src-min-noconflict/mode-python");
14+
await import("ace-builds/src-min-noconflict/mode-ruby");
1415
await import("ace-builds/src-min-noconflict/mode-c_cpp");
16+
await import("ace-builds/src-min-noconflict/mode-javascript");
1517
await import("ace-builds/src-min-noconflict/mode-json");
1618
await import("ace-builds/src-min-noconflict/mode-csv");
1719
await import("ace-builds/src-min-noconflict/mode-text");
@@ -28,16 +30,22 @@ import { langConstants } from "./runtime";
2830
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
2931

3032
// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
31-
export type AceLang = "python" | "c_cpp" | "json" | "csv" | "text";
33+
export type AceLang = "python" | "ruby" | "c_cpp" | "javascript" | "json" | "csv" | "text";
3234
export function getAceLang(lang: string | undefined): AceLang {
3335
// Markdownで指定される可能性のある言語名からAceLangを取得
3436
switch (lang) {
3537
case "python":
3638
case "py":
3739
return "python";
40+
case "ruby":
41+
case "rb":
42+
return "ruby";
3843
case "cpp":
3944
case "c++":
4045
return "c_cpp";
46+
case "javascript":
47+
case "js":
48+
return "javascript";
4149
case "json":
4250
return "json";
4351
case "csv":

app/terminal/exec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export function ExecFile(props: ExecProps) {
8383
▶ 実行
8484
</button>
8585
<code className="text-sm ml-4">
86-
{getCommandlineStr(props.filenames)}
86+
{getCommandlineStr?.(props.filenames)}
8787
</code>
8888
</div>
8989
<div className="bg-base-300 p-4 pt-2 rounded-b-lg">

app/terminal/highlight.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import Prism from "prismjs";
22
import chalk from "chalk";
33
import { RuntimeLang } from "./runtime";
4-
// Python言語定義をインポート
4+
// 言語定義をインポート
55
import "prismjs/components/prism-python";
6+
import "prismjs/components/prism-ruby";
7+
import "prismjs/components/prism-javascript";
68

7-
type PrismLang = "python";
9+
type PrismLang = "python" | "ruby" | "javascript";
810

911
function getPrismLanguage(language: RuntimeLang): PrismLang {
1012
switch (language) {
1113
case "python":
1214
return "python";
15+
case "ruby":
16+
return "ruby";
17+
case "javascript":
18+
return "javascript";
1319
case "cpp":
1420
throw new Error(
1521
`highlight for ${language} is disabled because it should not support REPL`

app/terminal/page.tsx

Lines changed: 170 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,144 @@
22
import { Heading } from "@/[docs_id]/markdown";
33
import "mocha/mocha.js";
44
import "mocha/mocha.css";
5-
import { useEffect, useRef, useState } from "react";
6-
import { usePyodide } from "./python/runtime";
5+
import { Fragment, useEffect, useRef, useState } from "react";
76
import { useWandbox } from "./wandbox/runtime";
87
import { RuntimeContext, RuntimeLang } from "./runtime";
98
import { useEmbedContext } from "./embedContext";
109
import { defineTests } from "./tests";
10+
import { usePyodide } from "./worker/pyodide";
11+
import { useRuby } from "./worker/ruby";
12+
import { useJSEval } from "./worker/jsEval";
13+
import { ReplTerminal } from "./repl";
14+
import { EditorComponent, getAceLang } from "./editor";
15+
import { ExecFile } from "./exec";
1116

1217
export default function RuntimeTestPage() {
18+
return (
19+
<div className="p-4 mx-auto w-full max-w-200">
20+
<Heading level={1}>Runtime Test Page</Heading>
21+
22+
<Heading level={2}>REPLとコード実行のサンプル</Heading>
23+
{/* name of each tab group should be unique */}
24+
<div className="tabs tabs-border">
25+
{Object.entries(sampleConfig).map(([lang, config]) => (
26+
<Fragment key={lang}>
27+
<input
28+
type="radio"
29+
name="runtime-sample-tabs"
30+
className="tab"
31+
aria-label={lang}
32+
/>
33+
<div className="tab-content border-base-300 bg-base-100 p-4">
34+
<RuntimeSample lang={lang as RuntimeLang} config={config} />
35+
</div>
36+
</Fragment>
37+
))}
38+
</div>
39+
40+
<Heading level={2}>自動テスト</Heading>
41+
<MochaTest />
42+
</div>
43+
);
44+
}
45+
46+
interface SampleConfig {
47+
repl: boolean;
48+
replInitContent?: string; // ReplOutput[] ではない。stringのパースはruntimeが行う
49+
editor: Record<string, string> | false;
50+
exec: string[] | false;
51+
}
52+
const sampleConfig: Record<RuntimeLang, SampleConfig> = {
53+
python: {
54+
repl: true,
55+
replInitContent: '>>> print("Hello, World!")\nHello, World!',
56+
editor: {
57+
"main.py": 'print("Hello, World!")',
58+
},
59+
exec: ["main.py"],
60+
},
61+
ruby: {
62+
repl: true,
63+
replInitContent: '>> puts "Hello, World!"\nHello, World!',
64+
editor: {
65+
"main.rb": 'puts "Hello, World!"',
66+
},
67+
exec: ["main.rb"],
68+
},
69+
javascript: {
70+
repl: true,
71+
replInitContent: '> console.log("Hello, World!");\nHello, World!',
72+
editor: false,
73+
exec: false,
74+
},
75+
cpp: {
76+
repl: false,
77+
editor: {
78+
"main.cpp": `#include <iostream>
79+
#include "sub.h"
80+
81+
int main() {
82+
std::cout << "Hello, World!" << std::endl;
83+
}`,
84+
"sub.h": ``,
85+
"sub.cpp": ``,
86+
},
87+
exec: ["main.cpp", "sub.cpp"],
88+
},
89+
};
90+
function RuntimeSample({
91+
lang,
92+
config,
93+
}: {
94+
lang: RuntimeLang;
95+
config: SampleConfig;
96+
}) {
97+
return (
98+
<div className="flex flex-col gap-4">
99+
{config.repl && (
100+
<ReplTerminal
101+
terminalId="1"
102+
language={lang}
103+
initContent={config.replInitContent}
104+
/>
105+
)}
106+
{config.editor &&
107+
Object.entries(config.editor).map(([filename, initContent]) => (
108+
<EditorComponent
109+
key={filename}
110+
language={getAceLang(lang)}
111+
filename={filename}
112+
initContent={initContent}
113+
/>
114+
))}
115+
{config.exec && (
116+
<ExecFile filenames={config.exec} language={lang} content="" />
117+
)}
118+
</div>
119+
);
120+
}
121+
122+
function MochaTest() {
13123
const pyodide = usePyodide();
124+
const ruby = useRuby();
125+
const javascript = useJSEval();
14126
const wandboxCpp = useWandbox("cpp");
15127
const runtimeRef = useRef<Record<RuntimeLang, RuntimeContext>>(null!);
16128
runtimeRef.current = {
17129
python: pyodide,
130+
ruby: ruby,
131+
javascript: javascript,
18132
cpp: wandboxCpp,
19133
};
20-
const { files, writeFile } = useEmbedContext();
21-
const filesRef = useRef<Record<string, string>>({});
22-
filesRef.current = files;
23-
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
24-
"idle"
25-
);
134+
26135
const [searchParams, setSearchParams] = useState<string>("");
27136
useEffect(() => {
28137
setSearchParams(window.location.search);
29138
}, []);
139+
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
140+
"idle"
141+
);
142+
const { writeFile } = useEmbedContext();
30143

31144
const runTest = () => {
32145
setMochaState("running");
@@ -44,59 +157,56 @@ export default function RuntimeTestPage() {
44157
};
45158

46159
return (
47-
<div className="p-4 mx-auto w-full max-w-200">
48-
<Heading level={1}>Runtime Test Page</Heading>
49-
<div className="border-1 border-transparent translate-x-0">
50-
{/* margin collapseさせない & fixedの対象をviewportではなくこのdivにする */}
51-
{mochaState === "idle" ? (
52-
<button className="btn btn-primary mt-4" onClick={runTest}>
53-
テストを実行
54-
</button>
55-
) : mochaState === "running" ? (
56-
<div className="alert mt-16 sm:mt-4 w-80">
57-
<svg
58-
xmlns="http://www.w3.org/2000/svg"
59-
className="animate-spin h-5 w-5 mr-3 border-2 border-solid border-current border-t-transparent rounded-full"
60-
fill="none"
61-
viewBox="0 0 24 24"
62-
></svg>
63-
テストを実行中です...
64-
</div>
65-
) : (
66-
<div className="alert mt-16 sm:mt-4 w-80">
67-
<svg
68-
xmlns="http://www.w3.org/2000/svg"
69-
fill="none"
70-
viewBox="0 0 24 24"
71-
className="stroke-info h-6 w-6 shrink-0"
72-
>
73-
<path
74-
strokeLinecap="round"
75-
strokeLinejoin="round"
76-
strokeWidth="2"
77-
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
78-
></path>
79-
</svg>
80-
テストが完了しました
81-
</div>
160+
<div className="border-1 border-transparent translate-x-0">
161+
{/* margin collapseさせない & fixedの対象をviewportではなくこのdivにする */}
162+
{mochaState === "idle" ? (
163+
<button className="btn btn-primary mt-4" onClick={runTest}>
164+
テストを実行
165+
</button>
166+
) : mochaState === "running" ? (
167+
<div className="alert mt-16 sm:mt-4 w-80">
168+
<svg
169+
xmlns="http://www.w3.org/2000/svg"
170+
className="animate-spin h-5 w-5 mr-3 border-2 border-solid border-current border-t-transparent rounded-full"
171+
fill="none"
172+
viewBox="0 0 24 24"
173+
></svg>
174+
テストを実行中です...
175+
</div>
176+
) : (
177+
<div className="alert mt-16 sm:mt-4 w-80">
178+
<svg
179+
xmlns="http://www.w3.org/2000/svg"
180+
fill="none"
181+
viewBox="0 0 24 24"
182+
className="stroke-info h-6 w-6 shrink-0"
183+
>
184+
<path
185+
strokeLinecap="round"
186+
strokeLinejoin="round"
187+
strokeWidth="2"
188+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
189+
></path>
190+
</svg>
191+
テストが完了しました
192+
</div>
193+
)}
194+
<p className="mt-8">
195+
{new URLSearchParams(searchParams).has("grep") && (
196+
<>
197+
一部のテストだけを実行します:
198+
<code className="ml-2 font-mono">
199+
{new URLSearchParams(searchParams).get("grep")}
200+
</code>
201+
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
202+
<a className="ml-4 link link-info" href="/terminal">
203+
{/* aタグでページをリロードしないと動作しない。 */}
204+
フィルタを解除
205+
</a>
206+
</>
82207
)}
83-
<p className="mt-8">
84-
{new URLSearchParams(searchParams).has("grep") && (
85-
<>
86-
一部のテストだけを実行します:
87-
<code className="ml-2 font-mono">
88-
{new URLSearchParams(searchParams).get("grep")}
89-
</code>
90-
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
91-
<a className="ml-4 link link-info" href="/terminal">
92-
{/* aタグでページをリロードしないと動作しない。 */}
93-
フィルタを解除
94-
</a>
95-
</>
96-
)}
97-
</p>
98-
<div className="m-0!" id="mocha" />
99-
</div>
208+
</p>
209+
<div className="m-0!" id="mocha" />
100210
</div>
101211
);
102212
}

0 commit comments

Comments
 (0)