diff --git a/scratch-gui/competitio_login/assets/gongan-beian.png b/scratch-gui/competitio_login/assets/gongan-beian.png new file mode 100644 index 00000000..6fe667f7 Binary files /dev/null and b/scratch-gui/competitio_login/assets/gongan-beian.png differ diff --git a/scratch-gui/competitio_login/index.html b/scratch-gui/competitio_login/index.html index 02d883f4..c6ae702e 100644 --- a/scratch-gui/competitio_login/index.html +++ b/scratch-gui/competitio_login/index.html @@ -384,7 +384,7 @@ diff --git a/scratch-gui/scripts/test-cpp-regressions.cjs b/scratch-gui/scripts/test-cpp-regressions.cjs index fc5ed6e5..10d9f837 100644 --- a/scratch-gui/scripts/test-cpp-regressions.cjs +++ b/scratch-gui/scripts/test-cpp-regressions.cjs @@ -1,4 +1,4 @@ -// Regression tests for the editor's real interpreter and output callback. +// Regression tests for variable scopes in the editor interpreter. // Run: node scripts/test-cpp-regressions.cjs const assert = require('assert'); const fs = require('fs'); @@ -32,19 +32,13 @@ int main() { } return 0; }`; -// The third screenshot reports cout without supplying a cout example. -// Add observable output to the supplied program to check both fixes together. -const customerEmailWithOutput = customerEmailCode.replace(' playerMove(i);', - ' cout << "i=" << i << endl;\n playerMove(i);'); - async function run(code, expected, configure) { const moves = []; const turns = []; - const output = []; context.window = {isScriptRunOver: false, cppGameAPI: { playerMove: async n => moves.push(n), turnLeft: async n => turns.push(n), - cmdPrint: text => output.push(text) + cmdPrint: () => {} }}; const executor = new CppCodeExecutor(code, null); if (configure) configure(executor, context.window); @@ -53,7 +47,6 @@ async function run(code, expected, configure) { await executor.run(); if (expected.moves) assert.deepStrictEqual(moves, expected.moves); if (expected.turns) assert.deepStrictEqual(turns, expected.turns); - if (expected.output) assert.deepStrictEqual(output, expected.output); assert.strictEqual(executor.variables, executor.globalVariables, 'scope must unwind after execution'); assert.strictEqual(executor.callStack.length, 0); return executor; @@ -61,12 +54,10 @@ async function run(code, expected, configure) { const cases = [ ['customer email complete program (screenshots 1 and 2)', customerEmailCode, {moves: [0], turns: [-1]}], - ['customer email with variable output (screenshot 3)', customerEmailWithOutput, {moves: [0], turns: [-1], output: ['i=0\n']}], ['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]}], ['loop assignment updates existing value', main('int i = 9;\nfor (i = 0; i < 2; i++) {\n}\nplayerMove(i);'), {moves: [2]}], ['block declaration versus assignment', main('int n = 1;\nif (true) {\nint n = 7;\nplayerMove(n);\n}\nif (true) {\nn = 3;\n}\nplayerMove(n);'), {moves: [7, 3]}], - ['string block shadowing', main('string msg = "outer";\nif (true) {\nstring msg = "C++";\ncout << msg;\n}\ncout << msg;'), {output: ['C++', 'outer']}], ['for body scope per iteration', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nplayerMove(x);\nint x = 2;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}], ['while body scope per iteration', main('int x = 9;\nint i = 0;\nwhile (i < 2) {\nplayerMove(x);\nint x = 2;\ni++;\n}\nplayerMove(x);'), {moves: [9, 9, 9]}], ['function locals', 'void f() {\nint n = 7;\nplayerMove(n);\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [7, 2]}], @@ -75,14 +66,6 @@ const cases = [ ['early void return unwinds scope', 'void f() {\nint n = 8;\nreturn;\n}\n' + main('int n = 2;\nf();\nplayerMove(n);'), {moves: [2]}], ['continue unwinds body scope', main('int x = 9;\nfor (int i = 0; i < 2; i++) {\nint x = 2;\ncontinue;\n}\nplayerMove(x);'), {moves: [9]}], ['break does not increment', main('int i = 0;\nfor (i = 0; i < 2; i++) {\nbreak;\n}\nplayerMove(i);'), {moves: [0]}], - ['cout with equals', main('int i = 7;\ncout << "i=" << i << endl;'), {output: ['i=7\n']}], - ['cout with operator text', main('cout << "value=42 C++ -- +=" << endl;'), {output: ['value=42 C++ -- +=\n']}], - ['cout with game API text', main('cout << "playerMove(2)" << endl;'), {moves: [], output: ['playerMove(2)\n']}], - ['qualified cout', main('std::cout << "Hello" << std::endl;'), {output: ['Hello\n']}], - ['cout postfix and prefix', main('int i = 0;\ncout << i++ << endl;\ncout << ++i << endl;\ncout << i-- << endl;\ncout << --i << endl;'), {output: ['0\n', '2\n', '2\n', '0\n']}], - ['cout arithmetic', main('int i = 3;\ncout << "result=" << (i * 2 + 1) << endl;'), {output: ['result=7\n']}], - ['escaped quotes and stream delimiters', main(String.raw`cout << "a\"< { @@ -90,6 +73,15 @@ const cases = [ try { await run(code, expected); } catch (error) {error.message = `${name}: ${error.message}`; throw error;} console.log(`PASS ${name}`); } + const strings = new CppCodeExecutor('', null); + await strings.executeSimpleStatement('string msg = "outer";'); + await strings.withVariableScope(async () => { + await strings.executeSimpleStatement('string msg = "inner";'); + assert.strictEqual(strings.variables.get('msg'), 'inner'); + }); + assert.strictEqual(strings.variables.get('msg'), 'outer'); + console.log('PASS string block shadowing'); + // Seed the interpreter's global environment to verify lexical lookup and writes. const globalExecutor = await run('void f() {\ng += 1;\nplayerMove(g);\n}\n' + main('int g = 9;\nf();\nplayerMove(g);'), {moves: [2, 9]}, executor => executor.variables.set('g', 1)); assert.strictEqual(globalExecutor.variables.get('g'), 2); @@ -112,51 +104,5 @@ const cases = [ console.log(`PASS scope cleanup on ${stop ? 'stop' : 'error'}`); } - // Extract the actual React component callback, avoiding a game-engine dependency. - const uiPath = path.resolve(__dirname, '../src/components/stage-cpp/stage-cpp-clang.jsx'); - const uiSource = fs.readFileSync(uiPath, 'utf8'); - const ast = babel.parseSync(uiSource, {configFile: false, babelrc: false, parserOpts: {plugins: ['jsx']}}); - let callback; - function visit(node) { - if (!node || typeof node !== 'object') return; - if (node.type === 'ObjectProperty' && node.key.name === 'cmdPrint') callback = node.value; - for (const value of Object.values(node)) { - if (Array.isArray(value)) value.forEach(visit); - else if (value && typeof value === 'object') visit(value); - } - } - visit(ast); - assert(callback, 'cmdPrint callback exists'); - for (const engineState of ['absent', 'ready', 'throwing']) { - let displayed = ''; - const sent = []; - const window = {}; - if (engineState !== 'absent') window.unityInstance = {SendMessage: (...args) => { - if (engineState === 'throwing') throw new Error('engine unavailable'); - sent.push(args); - }}; - const cmdPrint = vm.runInNewContext(`(${uiSource.slice(callback.start, callback.end)})`, { - window, console: silentConsole, setCompilationOutput: update => {displayed = update(displayed);} - }); - cmdPrint('A'); - cmdPrint('B\n'); - assert.strictEqual(displayed, 'AB\n'); - assert.strictEqual(window.cmd_print_text, 'AB\n'); - if (engineState === 'ready') assert.deepStrictEqual(sent[1], ['UIMain', 'SetText', 'AB\n']); - console.log(`PASS output callback with ${engineState} engine`); - - // Connect the real interpreter to the real UI callback, including repeated runs. - for (let attempt = 0; attempt < 2; attempt++) { - displayed = ''; - window.cmd_print_text = ''; - await run(customerEmailWithOutput, {moves: [0], turns: [-1]}, (executor, runtimeWindow) => { - runtimeWindow.cppGameAPI.cmdPrint = cmdPrint; - }); - assert.strictEqual(displayed, 'i=0\n'); - assert.strictEqual(window.cmd_print_text, 'i=0\n'); - if (engineState === 'ready') assert.deepStrictEqual(sent[sent.length - 1], ['UIMain', 'SetText', 'i=0\n']); - } - console.log(`PASS customer program through output callback with ${engineState} engine`); - } - console.log(`${cases.length + 9} regression checks passed`); + console.log(`${cases.length + 4} 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 c4b95f5c..9b10e683 100644 --- a/scratch-gui/src/components/stage-cpp/cpp-parser.js +++ b/scratch-gui/src/components/stage-cpp/cpp-parser.js @@ -1373,12 +1373,6 @@ export class CppCodeExecutor { async executeSimpleStatement(content) { debugLog(`🔍 执行简单语句: ${content}`); - // 先识别输出语句,避免把字符串里的 =、++、游戏函数名当作代码执行。 - if (/^\s*(?:std\s*::\s*)?cout\s*< escapes[char]); - } - - // 按字符串和括号边界分割输出流。 + // 智能分割cout的各个部分 splitCoutParts(content) { const parts = []; let current = ''; @@ -2275,12 +2266,6 @@ export class CppCodeExecutor { for (let i = 0; i < content.length; i++) { const char = content[i]; const nextChar = content[i + 1]; - - if (inString && char === '\\' && nextChar !== undefined) { - current += char + nextChar; - i++; - continue; - } if (!inString && inParens === 0 && char === '<' && nextChar === '<') { // 遇到 << 分隔符 @@ -2321,11 +2306,6 @@ export class CppCodeExecutor { expr = expr.trim().replace(/;$/, ''); debugLog(`🔍 计算表达式: ${expr}`); - // 字符串中的 ++、-- 和函数名都是文本,不参与表达式运算。 - if (/^("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')$/.test(expr)) { - return this.decodeStringLiteral(expr); - } - // 布尔常量 if (expr === 'true') { debugLog(`🔍 布尔常量: true = 1`); @@ -2380,7 +2360,7 @@ export class CppCodeExecutor { const newValue = currentValue + 1; this.variables.set(varName, newValue); console.log(`🔄 自增操作: ${expr} (${currentValue} -> ${newValue})`); - return expr.startsWith('++') ? newValue : currentValue; + return newValue; } if (expr.includes('--')) { @@ -2398,7 +2378,7 @@ export class CppCodeExecutor { const newValue = currentValue - 1; this.variables.set(varName, newValue); console.log(`🔄 自减操作: ${expr} (${currentValue} -> ${newValue})`); - return expr.startsWith('--') ? newValue : currentValue; + return newValue; } // 游戏API函数调用 @@ -2428,6 +2408,12 @@ export class CppCodeExecutor { return position; } + // 整个表达式是字符串字面量时才当字符串;含引号的 API 调用走后面的算术解析 + if ((expr.startsWith('"') && expr.endsWith('"')) || + (expr.startsWith("'") && expr.endsWith("'"))) { + return expr.slice(1, -1); + } + return this.parseComplexExpression(expr); } diff --git a/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx b/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx index 835398d8..4aa02668 100644 --- a/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx +++ b/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx @@ -645,21 +645,17 @@ const StageWrapperCppClang = () => { textStr = String(text); } - // cout 自己决定何时换行,连续输出应保持在同一行。 - window.cmd_print_text += textStr; + window.cmd_print_text += textStr + '\n'; const lines = window.cmd_print_text.split('\n'); if (lines.length > 20) { window.cmd_print_text = lines.slice(lines.length - 20).join('\n'); } - setCompilationOutput(prev => prev + textStr); if (window.unityInstance) { - try { - window.unityInstance.SendMessage("UIMain", "SetText", window.cmd_print_text); - } catch (error) { - console.warn('游戏画面输出失败,文本仍可在运行输出中查看', error); - } + window.unityInstance.SendMessage("UIMain", "SetText", window.cmd_print_text); } + + setCompilationOutput(prev => prev + textStr + "\n"); }, player_data: (direction) => { @@ -799,7 +795,6 @@ const StageWrapperCppClang = () => { setCompilationOutput(prev => prev + "语法检查通过!\n"); // 重置停止标志 - window.cmd_print_text = ''; window.isScriptRunOver = false; setIsRunning(true); setOverlayActive(true); @@ -974,7 +969,7 @@ const StageWrapperCppClang = () => {
-

运行输出

+

编译信息

diff --git a/scratch-gui/src/playground/aboutus.ejs b/scratch-gui/src/playground/aboutus.ejs index cac2ff1b..4d291b16 100644 --- a/scratch-gui/src/playground/aboutus.ejs +++ b/scratch-gui/src/playground/aboutus.ejs @@ -1358,7 +1358,7 @@