UNPKG

python2igcse

Version:

Convert Python code to IGCSE Pseudocode format

1,188 lines 51.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StatementVisitor = void 0; const ir_1 = require("../types/ir"); const expression_visitor_1 = require("./expression-visitor"); const base_parser_1 = require("./base-parser"); /** * Visitor class responsible for processing statements */ class StatementVisitor extends base_parser_1.BaseParser { /** * Execute parsing (not used in StatementVisitor) */ parse(_source) { throw new Error('StatementVisitor.parse() should not be called directly'); } constructor() { super(); this.expressionVisitor = new expression_visitor_1.ExpressionVisitor(); } /** * Set context */ setContext(context) { this.context = context; } /** * Process assignment statements */ visitAssign(node) { // Detect array multiplication initialization ([0] * 5, etc.) if (this.isArrayMultiplication(node.value)) { return this.handleArrayMultiplication(node); } // Detect array initialization first if (this.expressionVisitor.isArrayInitialization(node.value)) { return this.handleArrayInitialization(node); } // Special handling for input() function assignment (including nested function calls) if (this.containsInputCall(node.value)) { return this.handleInputAssignment(node); } // Detect class instantiation if (node.value.type === 'Call') { if (node.value.func.type === 'Name') { const funcName = node.value.func.id; const isClass = this.context.isClass(funcName); if (isClass) { return this.handleClassInstantiation(node); } } } const targetNode = node.targets[0]; // Process array element assignment (data[1] = 100) if (targetNode.type === 'Subscript') { return this.handleElementAssign(targetNode, node.value); } // Process attribute assignment (obj.field = value) if (targetNode.type === 'Attribute') { return this.handleAttributeAssign(targetNode, node.value); } // For class instantiation, process directly without going through expression-visitor if (node.value.type === 'Call') { const func = this.expressionVisitor.visitExpression(node.value.func); const args = node.value.args.map((arg) => this.expressionVisitor.visitExpression(arg)); // If not a built-in function, process as class instantiation const builtinResult = this.expressionVisitor.convertBuiltinFunction(func, args); if (!builtinResult && this.isClassInstantiation(node.value)) { return this.handleClassInstantiation(node); } } const target = this.expressionVisitor.visitExpression(targetNode); const value = this.expressionVisitor.visitExpression(node.value); let text = `${target} ← ${value}`; // Add inline comment if present if (node.inlineComment) { text += ` // ${node.inlineComment}`; } // Infer and register variable type const dataType = this.expressionVisitor.inferTypeFromValue(node.value); if (targetNode.type === 'Name') { this.registerVariable(targetNode.id, dataType, node.lineno); } return this.createIRNode('assign', text); } /** * Process input() function assignment */ handleInputAssignment(node) { const targetNode = node.targets[0]; const target = this.expressionVisitor.visitExpression(targetNode); // Find input() function (considering nested function calls) const inputCall = this.findInputCall(node.value); if (!inputCall) { // If input() is not found, process as normal assignment const value = this.expressionVisitor.visitExpression(node.value); const text = `${target} ← ${value}`; return this.createIRNode('assign', text); } const args = inputCall.args.map((arg) => this.expressionVisitor.visitExpression(arg)); // If there's a prompt, create two IR nodes: OUTPUT and INPUT statements if (args.length > 0) { const outputText = `OUTPUT ${args[0]}`; const inputText = `INPUT ${target}`; // Create compound IR node (containing OUTPUT and INPUT statements) const outputIR = this.createIRNode('output', outputText); const inputIR = this.createIRNode('input', inputText); // Infer and register variable type if (targetNode.type === 'Name') { // For int(input(...)), register as INTEGER type if (node.value.type === 'Call' && node.value.func.type === 'Name' && node.value.func.id === 'int') { this.registerVariable(targetNode.id, 'INTEGER', node.lineno); } else { this.registerVariable(targetNode.id, 'STRING', node.lineno); } } // Return as compound node return this.createIRNode('compound', '', [outputIR, inputIR]); } else { // If no prompt, only INPUT statement const inputText = `INPUT ${target}`; // Infer and register variable type if (targetNode.type === 'Name') { // For int(input(...)), register as INTEGER type if (node.value.type === 'Call' && node.value.func.type === 'Name' && node.value.func.id === 'int') { this.registerVariable(targetNode.id, 'INTEGER', node.lineno); } else { this.registerVariable(targetNode.id, 'STRING', node.lineno); } } return this.createIRNode('input', inputText); } } /** * Find input() from nested function calls */ findInputCall(node) { if (node.type === 'Call' && node.func.type === 'Name' && node.func.id === 'input') { return node; } // Recursively search nested function calls if (node.type === 'Call' && node.args) { for (const arg of node.args) { const result = this.findInputCall(arg); if (result) return result; } } return null; } /** * Check if node contains input() call */ containsInputCall(node) { return this.findInputCall(node) !== null; } /** * Process augmented assignment statements */ visitAugAssign(node) { const target = this.expressionVisitor.visitExpression(node.target); const value = this.expressionVisitor.visitExpression(node.value); const op = this.convertOperator(node.op); const text = `${target} ← ${target} ${op} ${value}`; return this.createIRNode('assign', text); } /** * Process type-annotated assignment statements (items: list[str] = []) */ visitAnnAssign(node) { const targetName = node.target.id; // Detect array type from type annotation if (this.isListTypeAnnotation(node.annotation)) { const elementType = this.extractListElementType(node.annotation); // For empty list, generate array declaration with default size if (node.value && node.value.type === 'List' && node.value.elts.length === 0) { const text = `DECLARE ${targetName} : ARRAY[1:100] OF ${elementType}`; this.registerVariable(targetName, 'ARRAY', node.lineno); return this.createIRNode('array', text); } // If there's a value, process as normal array initialization if (node.value) { const fakeAssignNode = { type: 'Assign', targets: [node.target], value: node.value, lineno: node.lineno || 0 }; return this.handleArrayInitialization(fakeAssignNode); } } // Normal type-annotated assignment const target = this.expressionVisitor.visitExpression(node.target); const dataType = this.convertAnnotationToIGCSEType(node.annotation); if (node.value) { // If there's a value, generate both declaration and assignment const value = this.expressionVisitor.visitExpression(node.value); const declText = `DECLARE ${target} : ${dataType}`; const assignText = `${target} ← ${value}`; this.registerVariable(targetName, dataType, node.lineno); const declIR = this.createIRNode('statement', declText); const assignIR = this.createIRNode('assign', assignText); return this.createIRNode('statement', '', [declIR, assignIR]); } else { // If no value, only declaration const text = `DECLARE ${target} : ${dataType}`; this.registerVariable(targetName, dataType, node.lineno); return this.createIRNode('statement', text); } } /** * Process IF statements */ visitIf(node) { // Determine if if-elif-else can be converted to CASE statement const caseResult = this.tryConvertToCase(node); if (caseResult) { return caseResult; } const variable = this.expressionVisitor.visitExpression(node.test); const ifText = `IF ${variable} THEN`; this.enterScope('if', 'block'); this.increaseIndent(); const bodyChildren = node.body.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); this.exitScope(); let children = bodyChildren; // Process ELSE clause if (node.orelse && node.orelse.length > 0) { const firstElse = node.orelse[0]; // If the first element is an IF statement, process as ELSE IF if (firstElse.type === 'If') { const condition = this.expressionVisitor.visitExpression(firstElse.test); const elseIfText = `ELSE IF ${condition} THEN`; const elseIfIR = this.createIRNode('elseif', elseIfText); this.enterScope('elseif', 'block'); this.increaseIndent(); const elseIfBodyChildren = firstElse.body.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); this.exitScope(); children = [...bodyChildren, elseIfIR, ...elseIfBodyChildren]; // Recursively process the orelse clause of ELSE IF statements if (firstElse.orelse && firstElse.orelse.length > 0) { const nestedElseResult = this.visitIf({ ...firstElse, body: [], // Empty body, process only orelse test: null // test is also unnecessary }); // Add child elements of nested ELSE/ELSE IF statements if (nestedElseResult.children) { children = [...children, ...nestedElseResult.children]; } } } else { // Normal ELSE clause const elseIR = this.createIRNode('else', 'ELSE'); this.enterScope('else', 'block'); this.increaseIndent(); const elseChildren = node.orelse.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); this.exitScope(); children = [...bodyChildren, elseIR, ...elseChildren]; } } return this.createIRNode('if', ifText, children); } /** * Process FOR statements */ visitFor(node) { const target = this.expressionVisitor.visitExpression(node.target); // Process for statements using range() function if (node.iter.type === 'Call' && node.iter.func.id === 'range') { return this.handleRangeFor(node, target); } // Direct iteration over arrays or lists if (node.iter.type === 'Name') { const arrayName = node.iter.id; const indexVar = 'i'; // Get array size (from context) let arraySize = '3'; // Default size // Get array size from context if (this.context && this.context.arrayInfo && this.context.arrayInfo[arrayName]) { arraySize = this.context.arrayInfo[arrayName].size.toString(); } // FOR i ← 1 TO size format const forText = `FOR ${indexVar} ← 1 TO ${arraySize}`; this.enterScope('for', 'block'); this.increaseIndent(); // Process body (directly reference array elements without using target variable) const bodyChildren = node.body.map((child) => { // Convert print(target) to OUTPUT array[i] // Convert print(target) to OUTPUT array[i] if (child.type === 'Expr' && child.value.type === 'Call' && child.value.func && child.value.func.type === 'Name' && child.value.func.id === 'print' && child.value.args.length === 1 && ((child.value.args[0].type === 'Name' && child.value.args[0].id === target) || (child.value.args[0].type === 'Str' && child.value.args[0].s === target))) { return this.createIRNode('output', `OUTPUT ${arrayName}[${indexVar}]`); } return this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node'); }); this.decreaseIndent(); this.exitScope(); const nextIR = this.createIRNode('statement', `NEXT ${indexVar}`); bodyChildren.push(nextIR); return this.createIRNode('for', forText, bodyChildren); } // Normal for statements (other iterable objects) const iterable = this.expressionVisitor.visitExpression(node.iter); const forText = `FOR ${target} IN ${iterable}`; this.enterScope('for', 'block'); this.increaseIndent(); const bodyChildren = node.body.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); this.exitScope(); const nextIR = this.createIRNode('statement', `NEXT ${target}`); bodyChildren.push(nextIR); return this.createIRNode('for', forText, bodyChildren); } /** * Process WHILE statements */ visitWhile(node) { const condition = this.expressionVisitor.visitExpression(node.test); // Convert while True + break pattern to REPEAT-UNTIL if (condition === 'TRUE' || condition === 'True' || condition.toUpperCase() === 'TRUE') { const repeatUntilResult = this.tryConvertToRepeatUntil(node); if (repeatUntilResult) { return repeatUntilResult; } } const whileText = `WHILE ${condition}`; this.enterScope('while', 'block'); this.increaseIndent(); const bodyChildren = node.body.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); this.exitScope(); const endwhileIR = this.createIRNode('endwhile', 'ENDWHILE'); bodyChildren.push(endwhileIR); return this.createIRNode('while', whileText, bodyChildren); } /** * Try to convert while True + break pattern to REPEAT-UNTIL */ tryConvertToRepeatUntil(node) { // Check if there's a break in the last IF statement of the body const body = node.body; if (body.length === 0) return null; // Don't convert if there are continue statements const hasContinue = this.hasStatementType(body, 'Continue'); if (hasContinue) return null; // Check if the last statement is an IF statement containing a break const lastStatement = body[body.length - 1]; if (lastStatement.type === 'If' && lastStatement.body && lastStatement.body.length === 1 && lastStatement.body[0].type === 'Break' && (!lastStatement.orelse || lastStatement.orelse.length === 0)) { // Convert to REPEAT-UNTIL structure const condition = this.expressionVisitor.visitExpression(lastStatement.test); this.enterScope('repeat', 'block'); this.increaseIndent(); // Process statements other than break IF statements const repeatBodyChildren = body.slice(0, -1).map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); this.exitScope(); const untilIR = this.createIRNode('until', `UNTIL ${condition}`); repeatBodyChildren.push(untilIR); return this.createIRNode('repeat', 'REPEAT', repeatBodyChildren); } return null; } /** * Check if specified statement type exists in body */ hasStatementType(body, statementType) { for (const stmt of body) { if (stmt.type === statementType) { return true; } // Check nested structures as well if (stmt.body && Array.isArray(stmt.body)) { if (this.hasStatementType(stmt.body, statementType)) { return true; } } if (stmt.orelse && Array.isArray(stmt.orelse)) { if (this.hasStatementType(stmt.orelse, statementType)) { return true; } } } return false; } /** * Get array size */ getArraySize(arrayName) { if (this.context.arrayInfo && this.context.arrayInfo[arrayName]) { return this.context.arrayInfo[arrayName].size; } return null; } /** * Set array size */ setArraySize(arrayName, size) { if (!this.context.arrayInfo) { this.context.arrayInfo = {}; } if (!this.context.arrayInfo[arrayName]) { this.context.arrayInfo[arrayName] = { size: 0, elementType: 'STRING', currentIndex: 0 }; } this.context.arrayInfo[arrayName].size = size; } /** * Process function call statements */ visitCall(node) { // For attribute method calls (obj.method()) if (node.func.type === 'Attribute') { const objectName = this.expressionVisitor.visitExpression(node.func.value); const methodName = node.func.attr; const args = node.args.map((arg) => this.expressionVisitor.visitExpression(arg)); // Special handling for append method if (methodName === 'append') { if (args.length > 0) { // Update array size const currentSize = this.getArraySize(objectName) || 0; const newSize = currentSize + 1; this.setArraySize(objectName, newSize); // Process as array element assignment const text = `${objectName}[${newSize}] ← ${args[0]}`; return this.createIRNode('assign', text); } return this.createIRNode('comment', `// ${objectName}.append() with no arguments`); } // Other attribute method calls const text = `${objectName}.${methodName}(${args.join(', ')})`; return this.createIRNode('statement', text); } const func = this.expressionVisitor.visitExpression(node.func); const args = node.args.map((arg) => this.expressionVisitor.visitExpression(arg)); // Record function call information (infer argument types) if (node.func.type === 'Name') { const functionName = node.func.id; const argumentTypes = node.args.map((arg) => this.expressionVisitor.inferTypeFromValue(arg)); this.recordFunctionCall(functionName, argumentTypes); } // Detect if func is an append method call if (func.includes('.append(')) { const match = func.match(/^(.+)\.append\((.+)\)$/); if (match) { const objectName = match[1]; const argValue = match[2]; // Update array size const currentSize = this.getArraySize(objectName) || 0; const newSize = currentSize + 1; this.setArraySize(objectName, newSize); // Process as array element assignment const text = `${objectName}[${newSize}] ← ${argValue}`; return this.createIRNode('assign', text); } } // Built-in function conversion if (func === 'print') { // Special handling for single f-string argument if (args.length === 1 && node.args[0].type === 'JoinedStr') { const fstringResult = this.expressionVisitor.visitExpression(node.args[0]); const text = `OUTPUT ${fstringResult}`; return this.createIRNode('output', text); } const text = `OUTPUT ${args.join(', ')}`; return this.createIRNode('output', text); } if (func === 'input') { const prompt = args.length > 0 ? args[0] : ''; const text = prompt ? `INPUT ${prompt}` : 'INPUT'; return this.createIRNode('input', text); } // Regular function call (add CALL keyword) const capitalizedFunc = this.capitalizeFirstLetter(func); const text = `CALL ${capitalizedFunc}(${args.join(', ')})`; return this.createIRNode('statement', text); } /** * Process RETURN statements */ visitReturn(node) { if (node.value) { const value = this.expressionVisitor.visitExpression(node.value); return this.createIRNode('return', `RETURN ${value}`); } return this.createIRNode('return', 'RETURN'); } /** * Process expression statements */ visitExpr(node) { // Special handling for function calls if (node.value && node.value.type === 'Call') { return this.visitCall(node.value); } const expr = this.expressionVisitor.visitExpression(node.value); return this.createIRNode('statement', expr); } /** * Process comments */ visitComment(node) { return this.createIRNode('comment', `// ${node.value}`); } /** * Process PASS statements */ visitPass(_node) { return this.createIRNode('comment', '// pass'); } /** * Process BREAK statements */ visitBreak(_node) { return this.createIRNode('break', 'BREAK'); } /** * Process CONTINUE statements */ visitContinue(_node) { return this.createIRNode('statement', 'CONTINUE'); } /** * Process IMPORT statements */ visitImport(_node) { // In IGCSE, import is usually not used, so output as comment return this.createIRNode('comment', `// import statement`); } /** * Process TRY statements */ visitTry(_node) { // Exception handling is not commonly used in IGCSE, so output as comment return this.createIRNode('comment', `// try-except statement`); } /** * Process RAISE statement */ visitRaise(_node) { return this.createIRNode('comment', `// raise statement`); } /** * Process WITH statement */ visitWith(_node) { return this.createIRNode('comment', `// with statement`); } /** * Process ASSERT statement */ visitAssert(_node) { return this.createIRNode('comment', `// assert statement`); } /** * Process GLOBAL statement */ visitGlobal(_node) { return this.createIRNode('comment', `// global statement`); } /** * Process MATCH statement (Python 3.10+) */ visitMatch(node) { const subject = this.expressionVisitor.visitExpression(node.subject); const caseText = `CASE OF ${subject}`; const children = []; // Process each case for (const caseNode of node.cases) { if (caseNode.pattern.type === 'MatchValue') { // Value pattern const value = this.expressionVisitor.visitExpression(caseNode.pattern.value); // If case body is a single statement, output on the same line if (caseNode.body.length === 1) { const bodyIR = this.visitNode ? this.visitNode(caseNode.body[0]) : this.createIRNode('comment', '// Unprocessed node'); const caseItemText = ` ${value} : ${bodyIR.text}`; const caseItemIR = this.createIRNode('statement', caseItemText); children.push(caseItemIR); } else { // For multiple statements, use traditional format const caseItemText = ` ${value} :`; const caseItemIR = this.createIRNode('statement', caseItemText); children.push(caseItemIR); this.increaseIndent(); const bodyChildren = caseNode.body.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); children.push(...bodyChildren); } } else if (caseNode.pattern.type === 'MatchAs' && !caseNode.pattern.pattern) { // Default case (_) if (caseNode.body.length === 1) { const bodyIR = this.visitNode ? this.visitNode(caseNode.body[0]) : this.createIRNode('comment', '// Unprocessed node'); const otherwiseText = ` OTHERWISE : ${bodyIR.text}`; const otherwiseIR = this.createIRNode('statement', otherwiseText); children.push(otherwiseIR); } else { const otherwiseIR = this.createIRNode('statement', ' OTHERWISE :'); children.push(otherwiseIR); this.increaseIndent(); const bodyChildren = caseNode.body.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); children.push(...bodyChildren); } } } const endCaseIR = this.createIRNode('statement', 'ENDCASE'); children.push(endCaseIR); return this.createIRNode('case', caseText, children); } /** * Process DELETE statement */ visitDelete(_node) { return this.createIRNode('comment', `// delete statement`); } // Helper methods handleRangeFor(node, target) { const args = node.iter.args; let startValue = '0'; let endValue = '0'; let stepValue = '1'; if (args.length === 1) { // range(n) endValue = this.expressionVisitor.visitExpression(args[0]); } else if (args.length === 2) { // range(start, end) startValue = this.expressionVisitor.visitExpression(args[0]); endValue = this.expressionVisitor.visitExpression(args[1]); } else if (args.length === 3) { // range(start, end, step) startValue = this.expressionVisitor.visitExpression(args[0]); endValue = this.expressionVisitor.visitExpression(args[1]); stepValue = this.expressionVisitor.visitExpression(args[2]); } // Optimize for numeric constants // Only subtract 1 from end value when step is 1 if (stepValue === '1') { // Special handling for LENGTH() function and other function calls if (endValue.startsWith('LENGTH(') || endValue.includes('(')) { endValue = `${endValue} - 1`; } else if (this.expressionVisitor.isNumericConstant(args[args.length - 1])) { const endNum = this.expressionVisitor.getNumericValue(args[args.length - 1]); endValue = (endNum - 1).toString(); } else { endValue = `${endValue} - 1`; } } else { // When step is not 1, calculate the last value reached if (args.length === 3 && this.expressionVisitor.isNumericConstant(args[0]) && this.expressionVisitor.isNumericConstant(args[1]) && this.expressionVisitor.isNumericConstant(args[2])) { const start = this.expressionVisitor.getNumericValue(args[0]); const end = this.expressionVisitor.getNumericValue(args[1]); const step = this.expressionVisitor.getNumericValue(args[2]); // Calculate the last value reached let lastValue = start; if (step > 0) { while (lastValue + step < end) { lastValue += step; } } else { while (lastValue + step > end) { lastValue += step; } } endValue = lastValue.toString(); } } const forText = stepValue === '1' ? `FOR ${target} ← ${startValue} TO ${endValue}` : `FOR ${target} ← ${startValue} TO ${endValue} STEP ${stepValue}`; this.enterScope('for', 'block'); this.increaseIndent(); const bodyChildren = node.body.map((child) => this.visitNode ? this.visitNode(child) : this.createIRNode('comment', '// Unprocessed node')); this.decreaseIndent(); this.exitScope(); const nextIR = this.createIRNode('statement', `NEXT ${target}`); bodyChildren.push(nextIR); return this.createIRNode('for', forText, bodyChildren); } /** * Check if it's array multiplication initialization ([0] * 5, etc.) */ isArrayMultiplication(node) { return node.type === 'BinOp' && node.op.type === 'Mult' && (this.expressionVisitor.isArrayInitialization(node.left) || this.expressionVisitor.isArrayInitialization(node.right)); } /** * Process array multiplication initialization ([0] * 5, etc.) */ handleArrayMultiplication(node) { const target = this.expressionVisitor.visitExpression(node.targets[0]); let arrayNode; let sizeNode; // Determine format: [0] * 5 or 5 * [0] if (this.expressionVisitor.isArrayInitialization(node.value.left)) { arrayNode = node.value.left; sizeNode = node.value.right; } else { arrayNode = node.value.right; sizeNode = node.value.left; } // Get size let size; if (sizeNode.type === 'Constant' && typeof sizeNode.value === 'number') { size = sizeNode.value; } else if (sizeNode.type === 'Num') { size = sizeNode.n; } else { // Use default value for dynamic size size = 100; } // Get element type let elementType; if (arrayNode.elts && arrayNode.elts.length > 0) { // For normal List/Tuple types const firstElement = arrayNode.elts[0]; elementType = this.expressionVisitor.inferTypeFromValue(firstElement); } else if (arrayNode.type === 'Name' && arrayNode.id) { // For Name type with array literal format (e.g., "[0]") const arrayLiteral = arrayNode.id; // Extract elements from array literal const match = arrayLiteral.match(/^\[(.+)\]$/); if (match) { const elementStr = match[1].trim(); // Check if it's a number if (/^\d+$/.test(elementStr)) { elementType = 'INTEGER'; } else if (/^\d+\.\d+$/.test(elementStr)) { elementType = 'REAL'; } else if (elementStr === 'True' || elementStr === 'False') { elementType = 'BOOLEAN'; } else { elementType = 'STRING'; } } else { elementType = 'INTEGER'; } } else { elementType = 'INTEGER'; } // Generate DECLARE statement for array multiplication const declText = `DECLARE ${target} : ARRAY[1:${size}] OF ${elementType}`; // Record array size information in context if (this.context && this.context.arrayInfo) { this.context.arrayInfo[target] = { size: size, elementType: elementType, currentIndex: 0 }; } // Infer and register variable type const targetNode = node.targets[0]; if (targetNode.type === 'Name') { this.registerVariable(targetNode.id, 'ARRAY', node.lineno); } return this.createIRNode('array', declText); } handleArrayInitialization(node) { const target = this.expressionVisitor.visitExpression(node.targets[0]); const elements = node.value.elts; const size = elements.length; // Reconstruct elements split as strings and detect object calls const reconstructedElements = this.reconstructObjectCalls(elements); // Check if it's an object array const isObjectArray = reconstructedElements.length > 0 && this.isObjectCall(reconstructedElements[0]); if (isObjectArray) { // For object arrays const firstCall = reconstructedElements[0]; const className = this.extractClassName(firstCall); const recordTypeName = `${className}Record`; const children = []; // Array declaration (using actual element count) const actualSize = reconstructedElements.length; const declText = `DECLARE ${target} : ARRAY[1:${actualSize}] OF ${recordTypeName}`; children.push(this.createIRNode('statement', declText)); // Record array size information in context if (this.context && this.context.arrayInfo) { this.context.arrayInfo[target] = { size: actualSize, elementType: recordTypeName, currentIndex: 0 }; } // Process each element reconstructedElements.forEach((elementStr, index) => { const args = this.extractArguments(elementStr); // Need to get actual field names from class definition, // but currently simplified to handle Point class as x, y if (className === 'Point' && args.length >= 2) { children.push(this.createIRNode('assign', `${target}[${index + 1}].x ← ${args[0]}`)); children.push(this.createIRNode('assign', `${target}[${index + 1}].y ← ${args[1]}`)); } else { // Generic handling for other classes args.forEach((arg, argIndex) => { const fieldName = `field${argIndex + 1}`; // Temporary field name children.push(this.createIRNode('assign', `${target}[${index + 1}].${fieldName} ← ${arg}`)); }); } }); return this.createIRNode('statement', '', children); } else { // For normal arrays const elementType = elements.length > 0 ? this.expressionVisitor.inferTypeFromValue(elements[0]) : 'STRING'; // Array declaration const declText = `DECLARE ${target} : ARRAY[1:${size}] OF ${elementType}`; const declIR = this.createIRNode('array', declText); // Record array size information in context if (this.context && this.context.arrayInfo) { this.context.arrayInfo[target] = { size: size, elementType: elementType, currentIndex: 0 }; } // Element assignment const assignments = []; elements.forEach((element, index) => { const value = this.expressionVisitor.visitExpression(element); const assignText = `${target}[${index + 1}] ← ${value}`; assignments.push(this.createIRNode('assign', assignText)); }); return this.createIRNode('statement', '', [declIR, ...assignments]); } } reconstructObjectCalls(elements) { const result = []; let i = 0; while (i < elements.length) { const element = elements[i]; if (element.type === 'Name' && element.id) { const elementStr = element.id; // Detect class name pattern (starts with uppercase and ends with parenthesis) if (/^[A-Z]\w*\(/.test(elementStr)) { // Combine with next elements to reconstruct object call let objectCall = elementStr; i++; // Combine elements until closing parenthesis is found while (i < elements.length && !objectCall.includes(')')) { const nextElement = elements[i]; if (nextElement.type === 'Name' && nextElement.id) { objectCall += ', ' + nextElement.id; } i++; } result.push(objectCall); } else { result.push(elementStr); i++; } } else { result.push(this.expressionVisitor.visitExpression(element)); i++; } } return result; } isObjectCall(elementStr) { // Detect class name(arguments) pattern return /^[A-Z]\w*\(.+\)$/.test(elementStr); } extractClassName(objectCall) { const match = objectCall.match(/^([A-Z]\w*)\(/); return match ? match[1] : 'Unknown'; } extractArguments(objectCall) { const match = objectCall.match(/\((.+)\)$/); if (match) { return match[1].split(',').map(arg => arg.trim()); } return []; } isClassInstantiation(node) { // Simple check: treat as class if function name starts with uppercase if (node.func.type === 'Name') { return /^[A-Z]/.test(node.func.id); } return false; } handleClassInstantiation(node) { const className = this.expressionVisitor.visitExpression(node.value.func); const target = this.expressionVisitor.visitExpression(node.targets[0]); const args = node.value.args.map((arg) => this.expressionVisitor.visitExpression(arg)); // When treating as record type, generate variable declaration and field assignments const recordTypeName = `${className}Record`; const children = []; // Variable declaration const declareText = `DECLARE ${target} : ${recordTypeName}`; children.push(this.createIRNode('statement', declareText)); // Get attribute names from class definition const classAttributes = this.getClassAttributes(className); // Field assignment (based on argument order) for (let i = 0; i < Math.min(args.length, classAttributes.length); i++) { const attrName = classAttributes[i]; const assignText = `${target}.${attrName} ← ${args[i]}`; children.push(this.createIRNode('assign', assignText)); } return this.createIRNode('block', '', children); } convertOperator(op) { switch (op.type) { case 'Add': return '+'; case 'Sub': return '-'; case 'Mult': return '*'; case 'Div': return '/'; case 'FloorDiv': return 'DIV'; case 'Mod': return 'MOD'; case 'Pow': return '^'; default: return '+'; } } /** * Process array element assignment (data[1] = 100) */ handleElementAssign(targetNode, valueNode) { const arrayName = this.expressionVisitor.visitExpression(targetNode.value); const value = this.expressionVisitor.visitExpression(valueNode); // Process index directly to avoid comments let adjustedIndex; let sliceNode = targetNode.slice; // Get the inner value if wrapped in Index node if (sliceNode.type === 'Index') { sliceNode = sliceNode.value; } if (sliceNode.type === 'Constant' && typeof sliceNode.value === 'number') { // For numeric literals, add 1 adjustedIndex = String(sliceNode.value + 1); } else if (sliceNode.type === 'Num') { // For old Python AST numeric nodes adjustedIndex = String(sliceNode.n + 1); } else if (sliceNode.type === 'Name') { // For variables, add +1 adjustedIndex = `${sliceNode.id} + 1`; } else { // For other cases, process as expression const index = this.expressionVisitor.visitExpression(targetNode.slice); adjustedIndex = this.convertIndexToOneBased(index); } const text = `${arrayName}[${adjustedIndex}] ← ${value}`; return this.createIRNode('element_assign', text); } /** * Process attribute assignment (obj.field = value) */ handleAttributeAssign(targetNode, valueNode) { const objectName = this.expressionVisitor.visitExpression(targetNode.value); const attributeName = targetNode.attr; const value = this.expressionVisitor.visitExpression(valueNode); const text = `${objectName}.${attributeName} ← ${value}`; return this.createIRNode('attribute_assign', text); } /** * Convert index from 0-based to 1-based */ convertIndexToOneBased(index) { // For numeric literals, add 1 if (/^\d+$/.test(index)) { return String(parseInt(index) + 1); } // For variables, add +1 return `${index} + 1`; } /** * Check if type annotation is a list type */ isListTypeAnnotation(annotation) { if (!annotation) return false; // list[type] format if (annotation.type === 'Subscript' && annotation.value.type === 'Name' && annotation.value.id === 'list') { return true; } // List[type] format (typing.List) if (annotation.type === 'Subscript' && annotation.value.type === 'Name' && annotation.value.id === 'List') { return true; } return false; } /** * Extract element type from list type annotation */ extractListElementType(annotation) { if (annotation.type === 'Subscript' && annotation.slice) { const elementType = annotation.slice; if (elementType.type === 'Name') { return this.convertPythonTypeToIGCSE(elementType.id); } } return 'STRING'; // Default } /** * Convert type annotation to IGCSE type */ convertAnnotationToIGCSEType(annotation) { if (!annotation) return 'STRING'; if (annotation.type === 'Name') { return this.convertPythonTypeToIGCSE(annotation.id); } if (this.isListTypeAnnotation(annotation)) { const elementType = this.extractListElementType(annotation); return `ARRAY[1:100] OF ${elementType}`; } return 'STRING'; } /** * Convert Python type name to IGCSE type */ convertPythonTypeToIGCSE(typeName) { switch (typeName) { case 'int': return 'INTEGER'; case 'str': return 'STRING'; case 'bool': return 'BOOLEAN'; case 'float': return 'REAL'; default: return 'STRING'; } } /** * Get attribute names from class definition */ getClassAttributes(className) { // Search for class definition from context if (this.context && this.context.classDefinitions) { const classDef = this.context.classDefinitions[className]; if (classDef && classDef.attributes) { return classDef.attributes.map((attr) => attr.split(' : ')[0]); } } // Default attribute names (for Student class) if (className === 'Student') { return ['name', 'age']; } // Default for other classes return ['x', 'y']; } /** * Capitalize the first letter of a string */ capitalizeFirstLetter(str) { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1); } /** * Check if IF-ELIF-ELSE statement can be converted to CASE statement, and convert if possible */ tryConvertToCase(node) { // Convert to CASE statement only for chains of equality comparisons on the same variable (if x == 1: elif x == 2: ...) if (!this.canConvertToCase(node)) { return null; } const variable = this.extractCaseVariable(node); if (!variable) { return null; } const children = []; const caseText = `CASE OF ${variable}`; // Process each condition this.processCaseConditions(node, variable, children); children.push(this.createIRNode('statement', 'ENDCASE')); return this.createIRNode('case', caseText, children); } /** * Check if IF-ELIF-ELSE statement can be converted to CASE statement */ canConvertToCase(node) { let current = node; let variable = null; while (current) { // Cannot convert if condition is not equality comparison (==) if (!current.test || current.test.type !== 'Compare') { return false; } const compare = current.test; if (!compare.ops || compare.ops.length !== 1 || compare.ops[0].type !== 'Eq') { return false; } // Cannot convert if left side is not a variable if (!compare.left || compare.left.type !== 'Name') { return false; } const currentVar = compare.left.id; if (variable === null) { variable = currentVar; } else if (variable !== currentVar) { // Cannot convert if different variables return false; } // Cannot convert if right side is not a constant const comparator = compare.comparators[0]; if (!comparator || (comparator.type !== 'Constant' && comparator.type !== 'Num' && comparator.type !== 'Str')) { return false; } // Move to next elif/else if (current.orelse && current.orelse.length === 1 && current.orelse[0].type === 'If') { current = current.orelse[0]; } else { break; } } return true; } /** * Extract variable used in CASE statement */ extractCaseVariable(node) { if (node.test && node.test.type === 'Compare' && node.test.left && node.test.left.type === 'Name') { return node.test.left.id; } return null; } /** * Process each condition in CASE statement */ processCaseConditions(node, _variable, children) { let current = node; while (current) { if (current.test && current.test.type === 'Compare') { const comparator = current.test.comparators[0]; let value; if (comparator.type === 'Constant') { value = typeof comparator.value === 'string' ? `"${comparator.value}"` : String(comparator.value); } else if (comparator.type === 'Num') { value = String(comparator.n); } else if (comparator.type === 'Str') { value = `"${comparator.s}"`; } else { value = this.expressionVisitor.visitExpression(comparator); } // Process statements in condition and place them on the same line as the condition const