// Regression tests for the editor's real interpreter and output callback. // Run: node scripts/test-cpp-regressions.cjs const assert = require('assert'); const fs = require('fs'); const path = require('path'); const vm = require('vm'); const babel = require('@babel/core'); const parserPath = path.resolve(__dirname, '../src/components/stage-cpp/cpp-parser.js'); const source = fs.readFileSync(parserPath, 'utf8'); const compiled = babel.transformSync(source, { filename: parserPath, configFile: false, babelrc: false, plugins: ['@babel/plugin-transform-modules-commonjs'], presets: process.argv.includes('--transpiled') ? ['@babel/preset-env'] : [] }).code; 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 main = body => `int main() {\n${body}\n}`; // Complete program from the first email screenshot; the second repeats this issue. const customerEmailCode = `void test() { for (int i = 0; i < 2; i++) { } } int main() { // 在这里编写你的C++代码来控制角色 turnLeft(-1); for (int i = 0; i < 1; i++) { test(); playerMove(i); } 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) }}; const executor = new CppCodeExecutor(code, null); if (configure) configure(executor, context.window); const validation = await validateCppSyntax(code); assert.strictEqual(validation.isValid, true, JSON.stringify(validation.errors)); 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; } 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]}], ['all arguments evaluated before binding', 'void f(int a, int b) {\nplayerMove(a);\nplayerMove(b);\n}\n' + main('int a = 2;\nf(7, a);\nplayerMove(a);'), {moves: [7, 2, 2]}], ['recursive locals and parameters', 'void f(int n) {\nint saved = n;\nif (n > 0) {\nf(n - 1);\n}\nplayerMove(saved);\n}\n' + main('f(2);'), {moves: [0, 1, 2]}], ['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\"< { for (const [name, code, expected] of cases) { try { await run(code, expected); } catch (error) {error.message = `${name}: ${error.message}`; throw error;} console.log(`PASS ${name}`); } // 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); console.log('PASS global writes survive function calls; caller locals are not visible'); for (const stop of [false, true]) { const executor = new CppCodeExecutor('void f(int n) {\nfor (int i = 0; i < 2; i++) {\nplayerMove(i);\n}\n}\n' + main('f(2);'), null); executor.variables.set('n', 9); context.window = {isScriptRunOver: false, cppGameAPI: {playerMove: async () => { if (stop) context.window.isScriptRunOver = true; else throw new Error('game failure'); }}}; if (stop) await executor.run(); else await assert.rejects(executor.run(), /game failure/); assert.strictEqual(executor.variables, executor.globalVariables); assert.strictEqual(executor.variables.get('n'), 9); assert.strictEqual(executor.variables.has('i'), false); assert.strictEqual(executor.callStack.length, 0); assert.strictEqual(executor.currentExecutionContext, null); 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`); })().catch(error => {console.error(error); process.exitCode = 1;});