diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index e016cba..fd7110e 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -7,7 +7,7 @@ import { systemMessageColor, useTerminal, } from "./terminal"; -import { writeOutput } from "./repl"; +import { writeOutput, ReplOutput } from "./repl"; import { useEffect, useState } from "react"; import { useEmbedContext } from "./embedContext"; import { RuntimeLang, useRuntime } from "./runtime"; @@ -46,16 +46,24 @@ export function ExecFile(props: ExecProps) { (async () => { clearTerminal(terminalInstanceRef.current!); terminalInstanceRef.current!.write(systemMessageColor("実行中です...")); - const outputs = await runFiles(props.filenames, files); - clearTerminal(terminalInstanceRef.current!); - writeOutput( - terminalInstanceRef.current!, - outputs, - false, - undefined, - null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない - props.language - ); + const outputs: ReplOutput[] = []; + let isFirstOutput = true; + await runFiles(props.filenames, files, (output) => { + outputs.push(output); + if (isFirstOutput) { + // Clear "実行中です..." message only on first output + clearTerminal(terminalInstanceRef.current!); + isFirstOutput = false; + } + // Append only the new output + writeOutput( + terminalInstanceRef.current!, + output, + undefined, + null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない + props.language + ); + }); // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる setExecResult(props.filenames.join(","), outputs); setExecutionState("idle"); diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index f1a9b46..d34bcf0 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -17,7 +17,13 @@ import { useEmbedContext } from "./embedContext"; import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime"; import clsx from "clsx"; -export type ReplOutputType = "stdout" | "stderr" | "error" | "return" | "trace" | "system"; +export type ReplOutputType = + | "stdout" + | "stderr" + | "error" + | "return" + | "trace" + | "system"; export interface ReplOutput { type: ReplOutputType; // 出力の種類 message: string; // 出力メッセージ @@ -30,47 +36,37 @@ export type SyntaxStatus = "complete" | "incomplete" | "invalid"; // 構文チ export function writeOutput( term: Terminal, - outputs: ReplOutput[], - endNewLine: boolean, + output: ReplOutput, returnPrefix: string | undefined, Prism: typeof import("prismjs") | null, language: RuntimeLang ) { - for (let i = 0; i < outputs.length; i++) { - const output = outputs[i]; - if (i > 0) { - term.writeln(""); - } - // 出力内容に応じて色を変える - const message = String(output.message).replace(/\n/g, "\r\n"); - switch (output.type) { - case "error": - term.write(chalk.red(message)); - break; - case "trace": - term.write(chalk.blue.italic(message)); - break; - case "system": - term.write(systemMessageColor(message)); - break; - case "return": - if (returnPrefix) { - term.write(returnPrefix); - } - if (Prism) { - term.write(highlightCodeToAnsi(Prism, message, language)); - } else { - console.warn("Prism is not loaded, cannot highlight return value"); - term.write(message); - } - break; - default: - term.write(message); - break; - } - } - if (endNewLine && outputs.length > 0) { - term.writeln(""); + // 出力内容に応じて色を変える + const message = String(output.message).replace(/\n/g, "\r\n"); + switch (output.type) { + case "error": + term.writeln(chalk.red(message)); + break; + case "trace": + term.writeln(chalk.blue.italic(message)); + break; + case "system": + term.writeln(systemMessageColor(message)); + break; + case "return": + if (returnPrefix) { + term.write(returnPrefix); + } + if (Prism) { + term.writeln(highlightCodeToAnsi(Prism, message, language)); + } else { + console.warn("Prism is not loaded, cannot highlight return value"); + term.writeln(message); + } + break; + default: + term.writeln(message); + break; } } @@ -176,21 +172,18 @@ export function ReplTerminal({ // ランタイムからのoutputを描画し、inputBufferをリセット const handleOutput = useCallback( - (outputs: ReplOutput[]) => { + (output: ReplOutput) => { if (terminalInstanceRef.current) { writeOutput( terminalInstanceRef.current, - outputs, - true, + output, returnPrefix, Prism, language ); - // 出力が終わったらプロンプトを表示 - updateBuffer(() => [""]); } }, - [Prism, updateBuffer, terminalInstanceRef, returnPrefix, language] + [Prism, terminalInstanceRef, returnPrefix, language] ); const keyHandler = useCallback( @@ -220,11 +213,15 @@ export function ReplTerminal({ terminalInstanceRef.current.writeln(""); const command = inputBuffer.current.join("\n").trim(); inputBuffer.current = []; - const outputs = await runtimeMutex.runExclusive(() => - runCommand(command) - ); - handleOutput(outputs); - addReplOutput?.(terminalId, command, outputs); + const collectedOutputs: ReplOutput[] = []; + await runtimeMutex.runExclusive(async () => { + await runCommand(command, (output) => { + collectedOutputs.push(output); + handleOutput(output); + }); + }); + updateBuffer(() => [""]); + addReplOutput?.(terminalId, command, collectedOutputs); } } else if (code === 127) { // Backspace @@ -301,8 +298,13 @@ export function ReplTerminal({ updateBuffer(() => cmd.command.split("\n")); terminalInstanceRef.current!.writeln(""); inputBuffer.current = []; - handleOutput(cmd.output); + for (const output of cmd.output) { + handleOutput(output); + } + updateBuffer(() => [""]); } + } else { + updateBuffer(() => [""]); } terminalInstanceRef.current!.scrollToTop(); setInitCommandState("idle"); @@ -320,7 +322,10 @@ export function ReplTerminal({ const initCommandResult: ReplCommand[] = []; await runtimeMutex.runExclusive(async () => { for (const cmd of initCommand!) { - const outputs = await runCommand(cmd.command); + const outputs: ReplOutput[] = []; + await runCommand(cmd.command, (output) => { + outputs.push(output); + }); initCommandResult.push({ command: cmd.command, output: outputs, @@ -333,7 +338,10 @@ export function ReplTerminal({ updateBuffer(() => cmd.command.split("\n")); terminalInstanceRef.current!.writeln(""); inputBuffer.current = []; - handleOutput(cmd.output); + for (const output of cmd.output) { + handleOutput(output); + } + updateBuffer(() => [""]); } } updateBuffer(() => [""]); diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 0423a92..d5e649d 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -24,14 +24,18 @@ export interface RuntimeContext { mutex?: MutexInterface; interrupt?: () => void; // repl - runCommand?: (command: string) => Promise; + runCommand?: ( + command: string, + onOutput: (output: ReplOutput) => void + ) => Promise; checkSyntax?: (code: string) => Promise; splitReplExamples?: (content: string) => ReplCommand[]; // file runFiles: ( filenames: string[], - files: Readonly> - ) => Promise; + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => Promise; getCommandlineStr?: (filenames: string[]) => string; } export interface LangConstants { diff --git a/app/terminal/tests.ts b/app/terminal/tests.ts index 6ccfbe0..60a7701 100644 --- a/app/terminal/tests.ts +++ b/app/terminal/tests.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; import { RefObject } from "react"; import { emptyMutex, RuntimeContext, RuntimeLang } from "./runtime"; +import { ReplOutput } from "./repl"; export function defineTests( lang: RuntimeLang, @@ -42,11 +43,14 @@ export function defineTests( if (!printCode) { this.skip(); } - const result = await ( - runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(() => runtimeRef.current[lang].runCommand!(printCode)); - console.log(`${lang} REPL stdout test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: msg }); + const outputs: ReplOutput[] = []; + await (runtimeRef.current[lang].mutex || emptyMutex).runExclusive(() => + runtimeRef.current[lang].runCommand!(printCode, (output) => { + outputs.push(output); + }) + ); + console.log(`${lang} REPL stdout test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: msg }); }); it("should preserve variables across commands", async function () { @@ -68,14 +72,20 @@ export function defineTests( if (!setIntVarCode || !printIntVarCode) { this.skip(); } - const result = await ( - runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(async () => { - await runtimeRef.current[lang].runCommand!(setIntVarCode); - return runtimeRef.current[lang].runCommand!(printIntVarCode); - }); - console.log(`${lang} REPL variable preservation test: `, result); - expect(result).to.be.deep.include({ + const outputs: ReplOutput[] = []; + await (runtimeRef.current[lang].mutex || emptyMutex).runExclusive( + async () => { + await runtimeRef.current[lang].runCommand!(setIntVarCode, () => {}); + await runtimeRef.current[lang].runCommand!( + printIntVarCode, + (output) => { + outputs.push(output); + } + ); + } + ); + console.log(`${lang} REPL variable preservation test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: value.toString(), }); @@ -96,12 +106,15 @@ export function defineTests( if (!errorCode) { this.skip(); } - const result = await ( - runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(() => runtimeRef.current[lang].runCommand!(errorCode)); - console.log(`${lang} REPL error capture test: `, result); + const outputs: ReplOutput[] = []; + await (runtimeRef.current[lang].mutex || emptyMutex).runExclusive(() => + runtimeRef.current[lang].runCommand!(errorCode, (output) => { + outputs.push(output); + }) + ); + console.log(`${lang} REPL error capture test: `, outputs); // eslint-disable-next-line @typescript-eslint/no-unused-expressions - expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be + expect(outputs.filter((r) => r.message.includes(errorMsg))).to.not.be .empty; }); @@ -126,8 +139,8 @@ export function defineTests( const runPromise = ( runtimeRef.current[lang].mutex || emptyMutex ).runExclusive(async () => { - await runtimeRef.current[lang].runCommand!(setIntVarCode); - return runtimeRef.current[lang].runCommand!(infLoopCode); + await runtimeRef.current[lang].runCommand!(setIntVarCode, () => {}); + await runtimeRef.current[lang].runCommand!(infLoopCode, () => {}); }); // Wait a bit to ensure the infinite loop has started await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -137,13 +150,14 @@ export function defineTests( while (!runtimeRef.current[lang].ready) { await new Promise((resolve) => setTimeout(resolve, 100)); } - const result = await ( - runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(() => - runtimeRef.current[lang].runCommand!(printIntVarCode) + const outputs: ReplOutput[] = []; + await (runtimeRef.current[lang].mutex || emptyMutex).runExclusive(() => + runtimeRef.current[lang].runCommand!(printIntVarCode, (output) => { + outputs.push(output); + }) ); - console.log(`${lang} REPL interrupt recovery test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: "42" }); + console.log(`${lang} REPL interrupt recovery test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: "42" }); }); it("should capture files modified by command", async function () { @@ -162,10 +176,9 @@ export function defineTests( if (!writeCode) { this.skip(); } - const result = await ( - runtimeRef.current[lang].mutex || emptyMutex - ).runExclusive(() => runtimeRef.current[lang].runCommand!(writeCode)); - console.log(`${lang} REPL file modify test: `, result); + await (runtimeRef.current[lang].mutex || emptyMutex).runExclusive(() => + runtimeRef.current[lang].runCommand!(writeCode, () => {}) + ); // wait for files to be updated await new Promise((resolve) => setTimeout(resolve, 100)); expect(filesRef.current[targetFile]).to.equal(msg); @@ -191,11 +204,18 @@ export function defineTests( if (!filename || !code) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles([filename], { - [filename]: code, - }); - console.log(`${lang} single file stdout test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: msg }); + const outputs: ReplOutput[] = []; + await runtimeRef.current[lang].runFiles( + [filename], + { + [filename]: code, + }, + (output) => { + outputs.push(output); + } + ); + console.log(`${lang} single file stdout test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: msg }); }); it("should capture errors", async function () { @@ -220,12 +240,19 @@ export function defineTests( if (!filename || !code) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles([filename], { - [filename]: code, - }); - console.log(`${lang} single file error capture test: `, result); + const outputs: ReplOutput[] = []; + await runtimeRef.current[lang].runFiles( + [filename], + { + [filename]: code, + }, + (output) => { + outputs.push(output); + } + ); + console.log(`${lang} single file error capture test: `, outputs); // eslint-disable-next-line @typescript-eslint/no-unused-expressions - expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be + expect(outputs.filter((r) => r.message.includes(errorMsg))).to.not.be .empty; }); @@ -276,12 +303,12 @@ export function defineTests( if (!codes || !execFiles) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles( - execFiles, - codes - ); - console.log(`${lang} multifile stdout test: `, result); - expect(result).to.be.deep.include({ type: "stdout", message: msg }); + const outputs: ReplOutput[] = []; + await runtimeRef.current[lang].runFiles(execFiles, codes, (output) => { + outputs.push(output); + }); + console.log(`${lang} multifile stdout test: `, outputs); + expect(outputs).to.be.deep.include({ type: "stdout", message: msg }); }); it("should capture files modified by script", async function () { @@ -306,10 +333,13 @@ export function defineTests( if (!filename || !code) { this.skip(); } - const result = await runtimeRef.current[lang].runFiles([filename], { - [filename]: code, - }); - console.log(`${lang} file modify test: `, result); + await runtimeRef.current[lang].runFiles( + [filename], + { + [filename]: code, + }, + () => {} + ); // wait for files to be updated await new Promise((resolve) => setTimeout(resolve, 100)); expect(filesRef.current[targetFile]).to.equal(msg); diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx index 57dcc8c..b1a747d 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/app/terminal/typescript/runtime.tsx @@ -91,24 +91,25 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { const { writeFile } = useEmbedContext(); const runFiles = useCallback( - async (filenames: string[], files: Readonly>) => { + async ( + filenames: string[], + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => { if (tsEnv === null || typeof window === "undefined") { - return [ - { type: "error" as const, message: "TypeScript is not ready yet." }, - ]; + onOutput({ type: "error", message: "TypeScript is not ready yet." }); + return; } else { for (const [filename, content] of Object.entries(files)) { tsEnv.createFile(filename, content); } - const outputs: ReplOutput[] = []; - const ts = await import("typescript"); for (const diagnostic of tsEnv.languageService.getSyntacticDiagnostics( filenames[0] )) { - outputs.push({ + onOutput({ type: "error", message: ts.formatDiagnosticsWithColorAndContext([diagnostic], { getCurrentDirectory: () => "", @@ -121,7 +122,7 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { for (const diagnostic of tsEnv.languageService.getSemanticDiagnostics( filenames[0] )) { - outputs.push({ + onOutput({ type: "error", message: ts.formatDiagnosticsWithColorAndContext([diagnostic], { getCurrentDirectory: () => "", @@ -143,12 +144,11 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { } console.log(emitOutput); - const jsOutputs = jsEval.runFiles( + await jsEval.runFiles( [emitOutput.outputFiles[0].name], - files + files, + onOutput ); - - return outputs.concat(await jsOutputs); } }, [tsEnv, writeFile, jsEval] diff --git a/app/terminal/wandbox/api.ts b/app/terminal/wandbox/api.ts index bc4a421..82059f7 100644 --- a/app/terminal/wandbox/api.ts +++ b/app/terminal/wandbox/api.ts @@ -70,6 +70,14 @@ export interface CompileNdjsonResult { data: string; } +/** + * Output event with original NDJSON type and converted ReplOutput + */ +export interface CompileOutputEvent { + ndjsonType: string; + output: ReplOutput; +} + export interface CompileResult { status: string; signal: string; @@ -102,13 +110,75 @@ interface CompileProps { codes: Record; // codes: Code[]; } -export interface CompileResultWithOutput extends CompileResult { - output: ReplOutput[]; -} export async function compileAndRun( - options: CompileProps -): Promise { + options: CompileProps, + onOutput: (event: CompileOutputEvent) => void +): Promise { + // Helper function to process NDJSON result and call onOutput + const processNdjsonResult = (r: CompileNdjsonResult) => { + switch (r.type) { + case "CompilerMessageS": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "stdout", message: line } + }); + } + } + break; + case "CompilerMessageE": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "error", message: line } + }); + } + } + break; + case "StdOut": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "stdout", message: line } + }); + } + } + break; + case "StdErr": + if (r.data.trim()) { + for (const line of r.data.trim().split("\n")) { + onOutput({ + ndjsonType: r.type, + output: { type: "stderr", message: line } + }); + } + } + break; + case "ExitCode": + if(r.data !== "0"){ + onOutput({ + ndjsonType: r.type, + output: { + type: "system", + message: `ステータス ${r.data} で異常終了しました`, + } + }) + } + break; + case "Signal": + // TODO + break; + default: + // Ignore unknown types + console.warn(`Unknown ndjson type: ${r.type}`); + break; + } + }; + // Call the ndjson API instead of json API const response = await fetch(new URL("/api/compile.ndjson", WANDBOX), { method: "post", @@ -158,95 +228,22 @@ export async function compileAndRun( for (const line of lines) { if (line.trim().length > 0) { - ndjsonResults.push(JSON.parse(line) as CompileNdjsonResult); + const r = JSON.parse(line) as CompileNdjsonResult; + ndjsonResults.push(r); + // Call onOutput in real-time as we receive data + processNdjsonResult(r); } } } // Process any remaining data in the buffer if (buffer.trim().length > 0) { - ndjsonResults.push(JSON.parse(buffer) as CompileNdjsonResult); + const r = JSON.parse(buffer) as CompileNdjsonResult; + ndjsonResults.push(r); + // Call onOutput for remaining data + processNdjsonResult(r); } } finally { reader.releaseLock(); } - - // Merge ndjson results into a CompileResult (same logic as Rust merge_compile_result) - const result: CompileResult = { - status: "", - signal: "", - compiler_output: "", - compiler_error: "", - compiler_message: "", - program_output: "", - program_error: "", - program_message: "", - permlink: "", - url: "", - }; - - // Build output array in the order messages are received - const output: ReplOutput[] = []; - - for (const r of ndjsonResults) { - switch (r.type) { - case "Control": - // Ignore control messages - break; - case "CompilerMessageS": - result.compiler_output += r.data; - result.compiler_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "stdout", message: line }); - } - } - break; - case "CompilerMessageE": - result.compiler_error += r.data; - result.compiler_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "error", message: line }); - } - } - break; - case "StdOut": - result.program_output += r.data; - result.program_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "stdout", message: line }); - } - } - break; - case "StdErr": - result.program_error += r.data; - result.program_message += r.data; - // Add to output in order - if (r.data.trim()) { - for (const line of r.data.trim().split("\n")) { - output.push({ type: "stderr", message: line }); - } - } - break; - case "ExitCode": - result.status += r.data; - break; - case "Signal": - result.signal += r.data; - break; - default: - // Ignore unknown types - break; - } - } - - return { - ...result, - output, - }; } diff --git a/app/terminal/wandbox/cpp.ts b/app/terminal/wandbox/cpp.ts index 4c508e1..7b6bdb4 100644 --- a/app/terminal/wandbox/cpp.ts +++ b/app/terminal/wandbox/cpp.ts @@ -84,67 +84,67 @@ export function selectCppCompiler( export async function cppRunFiles( options: SelectedCompiler, files: Record, - filenames: string[] -): Promise { - const result = await compileAndRun({ - ...options, - compilerOptionsRaw: [ - ...options.compilerOptionsRaw, - ...filenames, - "_stacktrace.cpp", - ], - codes: { ...files, "_stacktrace.cpp": _stacktrace_cpp }, - }); + filenames: string[], + onOutput: (output: ReplOutput) => void +): Promise { + // Constants for stack trace processing + const WANDBOX_PATH = "/home/wandbox"; - let outputs = result.output; + // Track state for processing stack traces + let inStackTrace = false; - // Find stack trace in the output - const signalIndex = outputs.findIndex( - (line) => - line.type === "stderr" && line.message.startsWith("#!my_code_signal:") - ); - const traceIndex = outputs.findIndex( - (line) => line.type === "stderr" && line.message === "#!my_code_stacktrace:" - ); + await compileAndRun( + { + ...options, + compilerOptionsRaw: [ + ...options.compilerOptionsRaw, + ...filenames, + "_stacktrace.cpp", + ], + codes: { ...files, "_stacktrace.cpp": _stacktrace_cpp }, + }, + (event) => { + const { ndjsonType, output } = event; - if (signalIndex >= 0) { - outputs[signalIndex] = { - type: "error", - message: outputs[signalIndex].message.slice(17), - } as const; - } - if (traceIndex >= 0) { - // _stacktrace.cpp のコードで出力されるスタックトレースを、js側でパースしていい感じに表示する - const trace = outputs.slice(traceIndex + 1); - const otherOutputs = outputs.slice(0, traceIndex); - const traceOutputs: ReplOutput[] = [{ - type: "trace", - message: "Stack trace (filtered):", - }]; + // Check for signal marker in stderr + if ( + ndjsonType === "StdErr" && + output.message.startsWith("#!my_code_signal:") + ) { + onOutput({ + type: "error", + message: output.message.slice(17), + }); + return; + } + + // Check for stack trace marker + if ( + ndjsonType === "StdErr" && + output.message === "#!my_code_stacktrace:" + ) { + inStackTrace = true; + onOutput({ + type: "trace", + message: "Stack trace (filtered):", + }); + return; + } - for (const line of trace) { - if(line.type === "stderr"){ - // ユーザーのソースコードだけを対象にする - if (line.message.includes("/home/wandbox")) { - traceOutputs.push({ + // Process stack trace lines + if (inStackTrace && ndjsonType === "StdErr") { + // Filter to show only user source code + if (output.message.includes(WANDBOX_PATH)) { + onOutput({ type: "trace", - message: line.message.replace("/home/wandbox/", ""), + message: output.message.replace(`${WANDBOX_PATH}/`, ""), }); } - }else{ - otherOutputs.push(line); + return; } - } - outputs = [...otherOutputs, ...traceOutputs]; - } - - if (result.status !== "0") { - outputs.push({ - type: "system", - message: `ステータス ${result.status} で異常終了しました`, - }); - } - // TODO: result.signal はいつ使われるのか? - return outputs; + // Output normally + onOutput(output); + } + ); } diff --git a/app/terminal/wandbox/runtime.tsx b/app/terminal/wandbox/runtime.tsx index 3199762..9672062 100644 --- a/app/terminal/wandbox/runtime.tsx +++ b/app/terminal/wandbox/runtime.tsx @@ -25,8 +25,9 @@ interface IWandboxContext { lang: WandboxLang ) => ( filenames: string[], - files: Readonly> - ) => Promise; + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => Promise; } const WandboxContext = createContext(null!); @@ -65,17 +66,22 @@ export function WandboxProvider({ children }: { children: ReactNode }) { // Curried function for language-specific file execution const runFilesWithLang = useCallback( (lang: WandboxLang) => - async (filenames: string[], files: Readonly>) => { + async ( + filenames: string[], + files: Readonly>, + onOutput: (output: ReplOutput) => void + ) => { if (!selectedCompiler) { - return [ - { type: "error" as const, message: "Wandbox is not ready yet." }, - ]; + onOutput({ type: "error", message: "Wandbox is not ready yet." }); + return; } switch (lang) { case "cpp": - return cppRunFiles(selectedCompiler.cpp, files, filenames); + await cppRunFiles(selectedCompiler.cpp, files, filenames, onOutput); + break; case "rust": - return rustRunFiles(selectedCompiler.rust, files, filenames); + await rustRunFiles(selectedCompiler.rust, files, filenames, onOutput); + break; default: lang satisfies never; throw new Error(`unsupported language: ${lang}`); diff --git a/app/terminal/wandbox/rust.ts b/app/terminal/wandbox/rust.ts index 01f24ee..d88d30c 100644 --- a/app/terminal/wandbox/rust.ts +++ b/app/terminal/wandbox/rust.ts @@ -30,10 +30,21 @@ export function selectRustCompiler( export async function rustRunFiles( options: SelectedCompiler, files: Record, - filenames: string[] -): Promise { + filenames: string[], + onOutput: (output: ReplOutput) => void +): Promise { + // Regular expressions for parsing stack traces + const STACK_FRAME_PATTERN = /^\s*\d+:/; + const LOCATION_PATTERN = /^\s*at .\//; + const SYSTEM_CODE_PATTERN = /^\s*at .\/prog.rs/; + + // Track state for processing panic traces + let inPanicHook = false; + let foundBacktraceHeader = false; + const traceLines: string[] = []; + const mainModule = filenames[0].replace(/\.rs$/, ""); - const result = await compileAndRun({ + await compileAndRun({ ...options, // メインファイルでmod宣言したものをこちらに移す code: @@ -50,76 +61,59 @@ export async function rustRunFiles( ?.replace(/(?:pub\s+)?(fn\s+main\s*\()/g, "pub $1") .replaceAll(/mod\s+(\w+)\s*;/g, "use super::$1;"), }, - }); - - let outputs = result.output; - - // Find stack trace in the output - const panicIndex = outputs.findIndex( - (line) => line.type === "stderr" && line.message === "#!my_code_panic_hook:" - ); - - if (panicIndex >= 0) { - const traceIndex = - panicIndex + - outputs - .slice(panicIndex) - .findLastIndex( - (line) => - line.type === "stderr" && line.message === "stack backtrace:" - ); - const otherOutputs = outputs.slice(0, panicIndex); - const traceOutputs: ReplOutput[] = [ - { - type: "trace", - message: "Stack trace (filtered):", - }, - ]; - for (const line of outputs.slice(panicIndex + 1, traceIndex)) { - if (line.type === "stderr") { - otherOutputs.push({ - type: "error", - message: line.message, + }, (event) => { + const { ndjsonType, output } = event; + + // Check for panic hook marker + if (ndjsonType === "StdErr" && output.message === "#!my_code_panic_hook:") { + inPanicHook = true; + return; + } + + if (inPanicHook && ndjsonType === "StdErr") { + // Check for stack backtrace header + if (output.message === "stack backtrace:") { + foundBacktraceHeader = true; + onOutput({ + type: "trace", + message: "Stack trace (filtered):", }); - } else { - otherOutputs.push(line); + return; } - } - for (let i = traceIndex + 1; i < outputs.length; i++) { - const line = outputs.at(i)!; - const nextLine = outputs.at(i + 1); - if (line.type === "stderr") { - // ユーザーのソースコードだけを対象にする - if ( - /^\s*\d+:/.test(line.message) && - nextLine && - /^\s*at .\//.test(nextLine.message) && - !/^\s*at .\/prog.rs/.test(nextLine.message) - ) { - traceOutputs.push({ - type: "trace", - message: line.message.replace("prog::", ""), - }); - traceOutputs.push({ - type: "trace", - message: nextLine.message, - }); - i++; // skip next line + + if (foundBacktraceHeader) { + // Process stack trace lines + // Look for pattern: " N: ..." followed by " at ./file.rs:line" + if (STACK_FRAME_PATTERN.test(output.message)) { + traceLines.push(output.message); + } else if (LOCATION_PATTERN.test(output.message)) { + if (traceLines.length > 0) { + // Check if this is user code (not prog.rs) + if (!SYSTEM_CODE_PATTERN.test(output.message)) { + onOutput({ + type: "trace", + message: traceLines[traceLines.length - 1].replace("prog::", ""), + }); + onOutput({ + type: "trace", + message: output.message, + }); + } + traceLines.pop(); // Remove the associated trace line (regardless of match) + } } - } else { - otherOutputs.push(line); + return; } + + // Output panic messages as errors + onOutput({ + type: "error", + message: output.message, + }); + return; } - - outputs = [...otherOutputs, ...traceOutputs]; - } - - if (result.status !== "0") { - outputs.push({ - type: "system", - message: `ステータス ${result.status} で異常終了しました`, - }); - } - - return outputs; + + // Output normally + onOutput(output); + }); } diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index abc0291..6ada852 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -172,17 +172,16 @@ export function WorkerProvider({ }, [initializeWorker, mutex]); const runCommand = useCallback( - async (code: string): Promise => { + async (code: string, onOutput: (output: ReplOutput) => void): Promise => { if (!mutex.isLocked()) { throw new Error(`mutex of context must be locked for runCommand`); } if (!workerApiRef.current || !ready) { - return [ - { - type: "error", - message: `worker runtime is not ready yet.`, - }, - ]; + onOutput({ + type: "error", + message: `worker runtime is not ready yet.`, + }); + return; } if ( @@ -199,6 +198,7 @@ export function WorkerProvider({ code, proxy((item: ReplOutput) => { output.push(item); + onOutput(item); }) ) ); @@ -212,13 +212,12 @@ export function WorkerProvider({ commandHistory.current.push(code); } } - - return output; } catch (error) { if (error instanceof Error) { - return [{ type: "error", message: error.message }]; + onOutput({ type: "error", message: error.message }); + } else { + onOutput({ type: "error", message: String(error) }); } - return [{ type: "error", message: String(error) }]; } }, [ready, writeFile, mutex, trackPromise] @@ -238,23 +237,22 @@ export function WorkerProvider({ const runFiles = useCallback( async ( filenames: string[], - files: Readonly> - ): Promise => { + files: Readonly>, + onOutput: (output: ReplOutput) => void + ): Promise => { if (filenames.length !== 1) { - return [ - { - type: "error", - message: `worker runtime requires exactly one filename.`, - }, - ]; + onOutput({ + type: "error", + message: `worker runtime requires exactly one filename.`, + }); + return; } if (!workerApiRef.current || !ready) { - return [ - { - type: "error", - message: `worker runtime is not ready yet.`, - }, - ]; + onOutput({ + type: "error", + message: `worker runtime is not ready yet.`, + }); + return; } if ( capabilities.current?.interrupt === "buffer" && @@ -263,18 +261,16 @@ export function WorkerProvider({ interruptBuffer.current[0] = 0; } return mutex.runExclusive(async () => { - const output: ReplOutput[] = []; const { updatedFiles } = await trackPromise( workerApiRef.current!.runFile( filenames[0], files, proxy((item: ReplOutput) => { - output.push(item); + onOutput(item); }) ) ); writeFile(updatedFiles); - return output; }); }, [ready, writeFile, mutex, trackPromise]