-
Notifications
You must be signed in to change notification settings - Fork 1
Add Ruby runtime with ruby.wasm web worker #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 4 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a2f0aee
Initial plan
Copilot 4c57151
Add Ruby runtime implementation with ruby.wasm
Copilot 00f3e85
Improve Ruby worker implementation with proper WASI stdout/stderr cap…
Copilot 0b686e9
Address code review feedback - fix interrupt buffer and error handling
Copilot bb921ef
Implement interrupt with command history and VM restart
Copilot 6fd45b7
Remove unused Ruby npm dependencies (loaded from CDN instead)
Copilot f722cf2
Merge branch 'main' of https://github.com/ut-code/my-code into copilo…
Copilot 43d5db4
Add Ruby runtime tests to test suite
Copilot e0eaed0
Add Ruby worker headers configuration
Copilot bba92bd
ruby runtimeのエラーを修正, wasip1指定を消しdefaultrubyvmに変更
na-trium-144 6dc8c9f
eval行消さなくていいような
na-trium-144 8b04d28
interrupt()のmutexについて
na-trium-144 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| "use client"; | ||
|
|
||
| import { EditorComponent } from "../editor"; | ||
| import { ExecFile } from "../exec"; | ||
| import { ReplTerminal } from "../repl"; | ||
|
|
||
| export default function RubyPage() { | ||
| return ( | ||
| <div className="p-4 flex flex-col gap-4"> | ||
| <ReplTerminal | ||
| terminalId="" | ||
| language="ruby" | ||
| initContent={">> puts 'hello, world!'\nhello, world!"} | ||
| /> | ||
| <EditorComponent | ||
| language="ruby" | ||
| filename="main.rb" | ||
| initContent="puts 'hello, world!'" | ||
| /> | ||
| <ExecFile filenames={["main.rb"]} language="ruby" content="" /> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| "use client"; | ||
|
|
||
| import { | ||
| useState, | ||
| useRef, | ||
| useCallback, | ||
| ReactNode, | ||
| createContext, | ||
| useContext, | ||
| useEffect, | ||
| } from "react"; | ||
| import { SyntaxStatus, ReplOutput, ReplCommand } from "../repl"; | ||
| import { Mutex, MutexInterface } from "async-mutex"; | ||
| import { useEmbedContext } from "../embedContext"; | ||
| import { RuntimeContext } from "../runtime"; | ||
|
|
||
| const RubyContext = createContext<RuntimeContext>(null!); | ||
|
|
||
| export function useRuby(): RuntimeContext { | ||
| const context = useContext(RubyContext); | ||
| if (!context) { | ||
| throw new Error("useRuby must be used within a RubyProvider"); | ||
| } | ||
| return context; | ||
| } | ||
|
|
||
| type MessageToWorker = | ||
| | { | ||
| type: "init"; | ||
| payload: { RUBY_WASM_URL: string }; | ||
| } | ||
| | { | ||
| type: "runRuby"; | ||
| payload: { code: string }; | ||
| } | ||
| | { | ||
| type: "checkSyntax"; | ||
| payload: { code: string }; | ||
| } | ||
| | { | ||
| type: "runFile"; | ||
| payload: { name: string; files: Record<string, string> }; | ||
| }; | ||
|
|
||
| type MessageFromWorker = | ||
| | { id: number; payload: unknown } | ||
| | { id: number; error: string }; | ||
|
|
||
| type InitPayloadFromWorker = { success: boolean }; | ||
| type RunPayloadFromWorker = { | ||
| output: ReplOutput[]; | ||
| updatedFiles: [string, string][]; | ||
| }; | ||
| type StatusPayloadFromWorker = { status: SyntaxStatus }; | ||
|
|
||
| export function RubyProvider({ children }: { children: ReactNode }) { | ||
| const workerRef = useRef<Worker | null>(null); | ||
| const [ready, setReady] = useState<boolean>(false); | ||
| const mutex = useRef<MutexInterface>(new Mutex()); | ||
| const { files, writeFile } = useEmbedContext(); | ||
| const messageCallbacks = useRef< | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| Map<number, [(payload: any) => void, (error: string) => void]> | ||
| >(new Map()); | ||
| const nextMessageId = useRef<number>(0); | ||
|
|
||
| function postMessage<T>({ type, payload }: MessageToWorker) { | ||
| const id = nextMessageId.current++; | ||
| return new Promise<T>((resolve, reject) => { | ||
| messageCallbacks.current.set(id, [resolve, reject]); | ||
| workerRef.current?.postMessage({ id, type, payload }); | ||
| }); | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| const worker = new Worker("/ruby.worker.js"); | ||
| workerRef.current = worker; | ||
|
|
||
| worker.onmessage = (event) => { | ||
| const data = event.data as MessageFromWorker; | ||
| if (messageCallbacks.current.has(data.id)) { | ||
| const [resolve, reject] = messageCallbacks.current.get(data.id)!; | ||
| if ("error" in data) { | ||
| reject(data.error); | ||
| } else { | ||
| resolve(data.payload); | ||
| } | ||
| messageCallbacks.current.delete(data.id); | ||
| } | ||
| }; | ||
|
|
||
| // Use CDN URL for Ruby WASM with stdlib | ||
| const RUBY_WASM_URL = | ||
| "https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/ruby+stdlib.wasm"; | ||
|
|
||
| postMessage<InitPayloadFromWorker>({ | ||
| type: "init", | ||
| payload: { RUBY_WASM_URL }, | ||
| }).then(({ success }) => { | ||
| if (success) { | ||
| setReady(true); | ||
| } | ||
| }); | ||
|
|
||
| return () => { | ||
| workerRef.current?.terminate(); | ||
| }; | ||
| }, []); | ||
|
|
||
| const interrupt = useCallback(() => { | ||
| // TODO: Implement interrupt functionality for Ruby | ||
| // Ruby WASM doesn't currently support interrupts like Pyodide does | ||
| console.warn("Ruby interrupt is not yet implemented"); | ||
| }, []); | ||
|
|
||
| const runCommand = useCallback( | ||
| async (code: string): Promise<ReplOutput[]> => { | ||
| if (!mutex.current.isLocked()) { | ||
| throw new Error("mutex of RubyContext must be locked for runCommand"); | ||
| } | ||
| if (!workerRef.current || !ready) { | ||
| return [{ type: "error", message: "Ruby VM is not ready yet." }]; | ||
| } | ||
|
|
||
| const { output, updatedFiles } = await postMessage<RunPayloadFromWorker>({ | ||
| type: "runRuby", | ||
| payload: { code }, | ||
| }); | ||
| for (const [name, content] of updatedFiles) { | ||
| writeFile(name, content); | ||
| } | ||
| return output; | ||
| }, | ||
| [ready, writeFile] | ||
| ); | ||
|
|
||
| const checkSyntax = useCallback( | ||
| async (code: string): Promise<SyntaxStatus> => { | ||
| if (!workerRef.current || !ready) return "invalid"; | ||
| const { status } = await mutex.current.runExclusive(() => | ||
| postMessage<StatusPayloadFromWorker>({ | ||
| type: "checkSyntax", | ||
| payload: { code }, | ||
| }) | ||
| ); | ||
| return status; | ||
| }, | ||
| [ready] | ||
| ); | ||
|
|
||
| const runFiles = useCallback( | ||
| async (filenames: string[]): Promise<ReplOutput[]> => { | ||
| if (filenames.length !== 1) { | ||
| return [ | ||
| { | ||
| type: "error", | ||
| message: "Ruby execution requires exactly one filename", | ||
| }, | ||
| ]; | ||
| } | ||
| if (!workerRef.current || !ready) { | ||
| return [{ type: "error", message: "Ruby VM is not ready yet." }]; | ||
| } | ||
| return mutex.current.runExclusive(async () => { | ||
| const { output, updatedFiles } = | ||
| await postMessage<RunPayloadFromWorker>({ | ||
| type: "runFile", | ||
| payload: { name: filenames[0], files }, | ||
| }); | ||
| for (const [newName, content] of updatedFiles) { | ||
| writeFile(newName, content); | ||
| } | ||
| return output; | ||
| }); | ||
| }, | ||
| [files, ready, writeFile] | ||
| ); | ||
|
|
||
| const splitReplExamples = useCallback((content: string): ReplCommand[] => { | ||
| const initCommands: { command: string; output: ReplOutput[] }[] = []; | ||
| for (const line of content.split("\n")) { | ||
| if (line.startsWith(">> ")) { | ||
| // Ruby IRB uses >> as the prompt | ||
| initCommands.push({ command: line.slice(3), output: [] }); | ||
| } else if (line.startsWith("?> ")) { | ||
| // Ruby IRB uses ?> for continuation | ||
| if (initCommands.length > 0) { | ||
| initCommands[initCommands.length - 1].command += "\n" + line.slice(3); | ||
| } | ||
| } else { | ||
| // Lines without prompt are output from the previous command | ||
| if (initCommands.length > 0) { | ||
| initCommands[initCommands.length - 1].output.push({ | ||
| type: "stdout", | ||
| message: line, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return initCommands; | ||
| }, []); | ||
|
|
||
| const getCommandlineStr = useCallback( | ||
| (filenames: string[]) => `ruby ${filenames[0]}`, | ||
| [] | ||
| ); | ||
|
|
||
| return ( | ||
| <RubyContext.Provider | ||
| value={{ | ||
| ready, | ||
| runCommand, | ||
| checkSyntax, | ||
| mutex: mutex.current, | ||
| runFiles, | ||
| interrupt, | ||
| splitReplExamples, | ||
| getCommandlineStr, | ||
| }} | ||
| > | ||
| {children} | ||
| </RubyContext.Provider> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.