diff --git a/scratch-gui/scripts/patch-config.js b/scratch-gui/scripts/patch-config.js index 33082d89..8eebdce5 100644 --- a/scratch-gui/scripts/patch-config.js +++ b/scratch-gui/scripts/patch-config.js @@ -21,7 +21,7 @@ function getConfigForEnv(env) { case 'test': return { url: 'https://test-service.001code.cn', disabledev: false }; case 'prod': - return { url: 'https://service.001code.cn', disabledev: true }; + return { url: 'https://service.001code.com', disabledev: true }; case 'start': default: return { url: 'http://127.0.0.1:8000', disabledev: false }; diff --git a/scratch-gui/scripts/test-cpp-regressions.cjs b/scratch-gui/scripts/test-cpp-regressions.cjs index 10d9f837..a8ac45f6 100644 --- a/scratch-gui/scripts/test-cpp-regressions.cjs +++ b/scratch-gui/scripts/test-cpp-regressions.cjs @@ -1,4 +1,4 @@ -// Regression tests for variable scopes in the editor interpreter. +// Regression tests for variable scopes and return control flow in the editor interpreter. // Run: node scripts/test-cpp-regressions.cjs const assert = require('assert'); const fs = require('fs'); @@ -15,7 +15,7 @@ const compiled = babel.transformSync(source, { const silentConsole = {log() {}, warn() {}, error() {}}; const context = {exports: {}, require, setTimeout, console: silentConsole, window: {}}; vm.runInNewContext(compiled, context, {filename: parserPath}); -const {CppCodeExecutor, validateCppSyntax} = context.exports; +const {CppCodeExecutor, validateCppSyntax, createCppExecutor} = context.exports; const main = body => `int main() {\n${body}\n}`; // Complete program from the first email screenshot; the second repeats this issue. const customerEmailCode = `void test() { @@ -44,15 +44,30 @@ async function run(code, expected, configure) { if (configure) configure(executor, context.window); const validation = await validateCppSyntax(code); assert.strictEqual(validation.isValid, true, JSON.stringify(validation.errors)); - await executor.run(); + const exitCode = await executor.run(); + if (Object.prototype.hasOwnProperty.call(expected, 'exitCode')) assert.strictEqual(exitCode, expected.exitCode); if (expected.moves) assert.deepStrictEqual(moves, expected.moves); if (expected.turns) assert.deepStrictEqual(turns, expected.turns); assert.strictEqual(executor.variables, executor.globalVariables, 'scope must unwind after execution'); assert.strictEqual(executor.callStack.length, 0); + assert.strictEqual(executor.currentExecutionContext, null); + assert.strictEqual(executor.isRunning, false); return executor; } const cases = [ + ['screenshot: main return 1 is a normal exit', main('playerMove(4);\nreturn 1;'), {moves: [4], exitCode: 1}], + ['return 1 skips remaining main statements', main('return 1;\nplayerMove(9);'), {moves: [], exitCode: 1}], + ['return 0 also stops main early', main('return 0;\nplayerMove(9);'), {moves: [], exitCode: 0}], + ['negative exit code', main('return -1;'), {moves: [], exitCode: -1}], + ['parenthesized return expression', main('int status = 2;\nreturn (status + 1);'), {moves: [], exitCode: 3}], + ['main fallthrough returns zero', main('playerMove(1);'), {moves: [1], exitCode: 0}], + ['return from for loop exits main', main('for (int i = 0; i < 3; i++) {\nplayerMove(i);\nreturn 1;\n}\nplayerMove(9);'), {moves: [0], exitCode: 1}], + ['return zero inside loop is not ignored', main('for (int i = 0; i < 3; i++) {\nreturn 0;\n}\nplayerMove(9);'), {moves: [], exitCode: 0}], + ['return through while and if exits main', main('while (true) {\nif (true) {\nreturn 2;\n}\nplayerMove(9);\n}\nplayerMove(8);'), {moves: [], exitCode: 2}], + ['function return 0 skips only its own remaining statements', 'int f() {\nreturn 0;\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);\nreturn 1;'), {moves: [1], exitCode: 1}], + ['function return inside loop resumes caller', 'int f() {\nfor (int i = 0; i < 3; i++) {\nreturn 1;\n}\nplayerMove(9);\n}\n' + main('f();\nplayerMove(1);'), {moves: [1], exitCode: 0}], + ['identifier starting with return is not a return statement', main('int returnValue = 1;\nreturnValue = 2;\nplayerMove(returnValue);\nreturn 0;'), {moves: [2], exitCode: 0}], ['customer email complete program (screenshots 1 and 2)', customerEmailCode, {moves: [0], turns: [-1]}], ['nested loops', main('for (int i = 0; i < 2; i++) {\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);\n}'), {moves: [0, 1]}], ['loop initializer restores shadowed value', main('int i = 9;\nfor (int i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [9]}], @@ -104,5 +119,21 @@ const cases = [ console.log(`PASS scope cleanup on ${stop ? 'stop' : 'error'}`); } - console.log(`${cases.length + 4} regression checks passed`); + const highlighted = []; + context.window = {isScriptRunOver: false, setHighlightedLine: line => highlighted.push(line)}; + const manager = createCppExecutor(); + for (const status of [1, 0]) { + assert.strictEqual(await manager.execute(main(`return ${status};`)), status); + assert.strictEqual(manager.currentExecutor, null); + assert.strictEqual(highlighted[highlighted.length - 1], null); + } + console.log('PASS editor executor manager resolves nonzero exit and can run again'); + + context.window.cppGameAPI = {playerMove: async () => {throw new Error('RETURN_INTERRUPT');}}; + await assert.rejects(manager.execute('void f() {\nplayerMove(1);\n}\n' + main('f();')), /RETURN_INTERRUPT/); + assert.strictEqual(manager.currentExecutor, null); + assert.strictEqual(highlighted[highlighted.length - 1], null); + console.log('PASS real game errors are not mistaken for a return signal'); + + console.log(`${cases.length + 6} regression checks passed`); })().catch(error => {console.error(error); process.exitCode = 1;}); diff --git a/scratch-gui/src/components/stage-cpp/cpp-parser.js b/scratch-gui/src/components/stage-cpp/cpp-parser.js index 9b10e683..22594d75 100644 --- a/scratch-gui/src/components/stage-cpp/cpp-parser.js +++ b/scratch-gui/src/components/stage-cpp/cpp-parser.js @@ -548,6 +548,14 @@ class CppVariableScope extends Map { } } +// return 是跨越循环/条件块的控制信号,只在所属函数边界消费。 +// 使用独立类型,避免吞掉恰好带有相同消息的真实运行错误。 +class CppReturnSignal { + constructor(value) { + this.value = value; + } +} + export class CppCodeExecutor { constructor(code, setHighlightedLine) { this.code = code; @@ -566,7 +574,7 @@ export class CppCodeExecutor { try { this.isRunning = true; this.parseCode(); - await this.withVariableScope(() => this.executeMain(), this.globalVariables); + return await this.withVariableScope(() => this.executeMain(), this.globalVariables); } catch (error) { console.error('C++执行错误:', error); throw error; @@ -814,12 +822,26 @@ export class CppCodeExecutor { while (i < lines.length) { const line = lines[i].trim(); - if (!line || line.startsWith('//') || line === '{' || line === '}' || line === 'return 0;' || + if (!line || line.startsWith('//') || line === '{' || line === '}' || line.match(/^\s*}\s*else\s*\{\s*$/) || line.startsWith('else')) { i++; continue; } + // return 不能标记成 continue,否则循环会直接跳过它。 + if (/^return\b/.test(line)) { + statements.push({ + type: 'control', + subtype: 'return', + content: line, + lineNumber: isMainFunction ? this.getActualLineNumber(i) : i + 1, + originalContent: line, + contextPath: isMainFunction ? 'main' : 'function' + }); + i++; + continue; + } + // 检查是否是自定义函数调用(包括带括号的实参,如 f((n + 1));) const customCall = this.parseCustomFunctionCall(line); if (customCall) { @@ -848,8 +870,8 @@ export class CppCodeExecutor { statements.push(whileLoop); i = whileLoop.endIndex; } else { - // 检查是否是break或continue或return语句 - if (line === 'break;' || line === 'continue;' || line.startsWith('return')) { + // 检查是否是break或continue语句 + if (line === 'break;' || line === 'continue;') { statements.push({ type: 'control', subtype: line === 'break;' ? 'break' : 'continue', @@ -1327,10 +1349,17 @@ export class CppCodeExecutor { } async executeMain() { - for (const statement of this.statements) { - if (this.shouldStop()) break; - await this.executeStatement(statement); + try { + for (const statement of this.statements) { + if (this.shouldStop()) break; + await this.executeStatement(statement); + } + } catch (signal) { + if (!(signal instanceof CppReturnSignal)) throw signal; + return signal.value; } + // main 正常执行到末尾,相当于 return 0。 + return 0; } async executeStatement(statement) { @@ -1361,8 +1390,11 @@ export class CppCodeExecutor { await this.executeFunctionCall(statement); break; case 'control': - if (statement.content.startsWith('return')) { - throw new Error('RETURN_INTERRUPT'); + if (statement.subtype === 'return') { + const match = statement.content.match(/^return\b([\s\S]*?);(?:\s*\/\/.*)?$/); + if (!match) throw new Error(`无法解析return语句: ${statement.content}`); + const expression = match[1].trim(); + throw new CppReturnSignal(expression ? this.evaluateExpression(expression) : undefined); } // break和continue在各自的循环中处理,这里只记录 debugLog(`🔄 控制语句: ${statement.subtype}`); @@ -1887,7 +1919,8 @@ export class CppCodeExecutor { await this.executeStatement(adjustedStatement); } } catch (e) { - if (e.message !== 'RETURN_INTERRUPT') throw e; + if (!(e instanceof CppReturnSignal)) throw e; + return e.value; } console.log(`✅ 函数${functionName}执行完成`); @@ -2719,7 +2752,7 @@ export const createCppExecutor = () => { const executor = new CppCodeExecutor(code, window.setHighlightedLine); this.currentExecutor = executor; try { - await executor.run(); + return await executor.run(); } finally { this.currentExecutor = null; } diff --git a/scratch-gui/src/playground/config.js b/scratch-gui/src/playground/config.js index a41635c9..cc48a4da 100644 --- a/scratch-gui/src/playground/config.js +++ b/scratch-gui/src/playground/config.js @@ -1,12 +1,12 @@ // config.js // const CONFIG = { -// DataServerBaseUrl: "https://service.001code.cn", // 在这里定义服务器地址 -// // DataServerBaseUrl: "https://service.001code.cn", // 在这里定义服务器地址 -// // DataServerBaseUrl: "https://service.001code.cn", -// // DataServerBaseUrl: "https://service.001code.cn", -// // DataServerBaseUrl: "https://service.001code.cn", -// // DataServerBaseUrl: "https://service.001code.cn", +// DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址 +// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址 +// // DataServerBaseUrl: "https://service.001code.com", +// // DataServerBaseUrl: "https://service.001code.com", +// // DataServerBaseUrl: "https://service.001code.com", +// // DataServerBaseUrl: "https://service.001code.com", // version : "2025010301", // unitycdndir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826', // unitycdnbuilddir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826/Build', @@ -78,12 +78,12 @@ // const CONFIG = { -// // DataServerBaseUrl: "https://service.001code.cn", // 在这里定义服务器地址 -// // DataServerBaseUrl: "https://service.001code.cn", // 在这里定义服务器地址 -// // DataServerBaseUrl: "https://service.001code.cn", -// DataServerBaseUrl: "https://service.001code.cn", -// // DataServerBaseUrl: "https://service.001code.cn", -// // DataServerBaseUrl: "https://service.001code.cn", +// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址 +// // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址 +// // DataServerBaseUrl: "https://service.001code.com", +// DataServerBaseUrl: "https://service.001code.com", +// // DataServerBaseUrl: "https://service.001code.com", +// // DataServerBaseUrl: "https://service.001code.com", // version : "2025011001", // unitycdndir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826', // unitycdnbuilddir : 'https://oss-rpwqy.jjwlcdn.com/001_code_unity_res_20250826/Build', @@ -157,12 +157,12 @@ const CONFIG = { - // DataServerBaseUrl: "https://service.001code.cn", // 在这里定义服务器地址 - // DataServerBaseUrl: "https://service.001code.cn", // 在这里定义服务器地址 - // DataServerBaseUrl: "https://service.001code.cn", - DataServerBaseUrl: "https://service.001code.cn", - // DataServerBaseUrl: "https://service.001code.cn", - // DataServerBaseUrl: "https://service.001code.cn", + // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址 + // DataServerBaseUrl: "https://service.001code.com", // 在这里定义服务器地址 + // DataServerBaseUrl: "https://service.001code.com", + DataServerBaseUrl: "https://service.001code.com", + // DataServerBaseUrl: "https://service.001code.com", + // DataServerBaseUrl: "https://service.001code.com", version: "2026071701", /** OSS 真实地址(生产环境浏览器直连) */