python2igcse
Version:
Convert Python code to IGCSE Pseudocode format
1,188 lines • 51.9 kB
JavaScript
"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