python2ib
Version:
Convert Python code to IB Pseudocode format
534 lines • 22.2 kB
JavaScript
/**
* Main visitor for converting Python AST to IR
*/
import { BaseVisitor, VisitorUtils } from './base-visitor.js';
import { IRFactory } from '../../types/ir.js';
import { OperatorConverter } from '../../utils/operators.js';
import { KeywordConverter, SPECIAL_CONSTRUCTS } from '../../utils/keywords.js';
/** Main Python to IR visitor */
export class PythonToIRVisitor extends BaseVisitor {
constructor(config) {
super(config);
}
/** Visit Module (root node) */
visitModule(node) {
const program = IRFactory.program();
for (const stmt of node.body) {
const ir = this.visit(stmt);
if (ir) {
program.children.push(ir);
}
}
return program;
}
/** Visit Assignment statement */
visitAssign(node) {
if (!VisitorUtils.isSimpleAssignment(node)) {
return this.createError('Complex assignments not supported', node.lineno);
}
const targetNode = node.targets[0];
// Handle tuple assignment (multiple assignment)
if (targetNode.type === 'Tuple') {
return this.visitTupleAssignment(node);
}
let mappedTarget;
if (targetNode.type === 'Name') {
const target = VisitorUtils.getAssignmentTarget(targetNode);
mappedTarget = this.mapVariableName(target);
}
else if (targetNode.type === 'Subscript') {
// Handle array/list assignment like names[0] = "Alice"
mappedTarget = this.extractExpression(targetNode);
}
else {
return this.createError('Unsupported assignment target', node.lineno);
}
// Handle input() function calls in assignments
if (VisitorUtils.isInputCall(node.value)) {
return this.visitInputCallAssignment(node.value, mappedTarget);
}
// Handle wrapped input() calls like int(input())
if (VisitorUtils.isWrappedInputCall(node.value)) {
const inputCall = VisitorUtils.extractInputFromWrapped(node.value);
if (inputCall) {
return this.visitInputCallAssignment(inputCall, mappedTarget);
}
}
const value = this.extractExpression(node.value);
// Handle compound assignment operators
if (node.operator && node.operator !== '=') {
const expandedAssignment = OperatorConverter.expandCompoundAssignment(mappedTarget, node.operator, value);
return {
kind: 'assign',
text: expandedAssignment,
children: [],
meta: { variable: mappedTarget, lineNumber: node.lineno }
};
}
return IRFactory.assign(mappedTarget, value, node.lineno);
}
/** Visit tuple assignment (multiple assignment) */
visitTupleAssignment(node) {
const targetNode = node.targets[0];
const valueNode = node.value;
// Handle tuple swap: a, b = b, a or arr[i], arr[j] = arr[j], arr[i]
if (valueNode.type === 'Tuple' && targetNode.elts.length === 2 && valueNode.elts.length === 2) {
const leftTarget = this.extractExpression(targetNode.elts[0]);
const rightTarget = this.extractExpression(targetNode.elts[1]);
const leftValue = this.extractExpression(valueNode.elts[0]);
// Create a sequence of assignments using TEMP variable
// For a, b = b, a: TEMP = a; a = b; b = TEMP
const tempAssign = IRFactory.assign('TEMP', leftTarget, node.lineno);
const firstAssign = IRFactory.assign(leftTarget, leftValue, node.lineno);
const secondAssign = IRFactory.assign(rightTarget, 'TEMP', node.lineno);
return {
kind: 'sequence',
text: '',
children: [tempAssign, firstAssign, secondAssign],
meta: { lineNumber: node.lineno }
};
}
return this.createError('Complex tuple assignments not supported', node.lineno);
}
/** Visit Expression statement */
visitExpr(node) {
const value = node.value;
// Handle pass statement (which comes as Name node with id='pass')
if (value.type === 'Name' && value.id === 'pass') {
return {
kind: 'comment',
text: '// empty block',
children: [],
meta: { lineNumber: node.lineno }
};
}
// Handle special function calls
if (VisitorUtils.isPrintCall(value)) {
return this.visitPrintCall(value);
}
if (VisitorUtils.isInputCall(value)) {
return this.visitInputCall(value);
}
// Handle function calls (including method calls)
if (VisitorUtils.isFunctionCall(value)) {
// Use extractCall to handle both regular function calls and method calls
const callExpression = this.extractCall(value);
return {
kind: 'expression',
text: callExpression,
children: [],
meta: { lineNumber: node.lineno }
};
}
// Generic expression
const expression = this.extractExpression(value);
return {
kind: 'expression',
text: expression,
children: [],
meta: { lineNumber: node.lineno }
};
}
/** Visit Print function call */
visitPrintCall(node) {
const args = node.args.map((arg) => this.extractExpression(arg));
if (args.length === 0) {
return IRFactory.output('""', node.lineno);
}
if (args.length === 1) {
return IRFactory.output(args[0], node.lineno);
}
// Multiple arguments - join with commas
const joinedArgs = args.join(', ');
return IRFactory.output(joinedArgs, node.lineno);
}
/** Visit Input function call */
visitInputCall(node) {
// For input() calls in expressions, we need to handle them specially
// This is typically used in assignments like: x = input("prompt")
const args = node.args.map((arg) => this.extractExpression(arg));
if (args.length > 0) {
// Has prompt - create OUTPUT followed by INPUT
const prompt = args[0];
return {
kind: 'block',
text: '',
children: [
IRFactory.output(prompt, node.lineno),
{
kind: 'input',
text: 'INPUT',
children: [],
meta: { lineNumber: node.lineno }
}
],
meta: { lineNumber: node.lineno }
};
}
return {
kind: 'input',
text: 'INPUT',
children: [],
meta: { lineNumber: node.lineno }
};
}
/** Visit Input function call in assignment */
visitInputCallAssignment(node, variable) {
const args = node.args.map((arg) => this.extractExpression(arg));
if (args.length > 0) {
// Has prompt - create OUTPUT followed by INPUT with variable
const prompt = args[0];
return {
kind: 'block',
text: '',
children: [
IRFactory.output(prompt, node.lineno),
{
kind: 'input',
text: `INPUT ${variable}`,
children: [],
meta: { variable, lineNumber: node.lineno }
}
],
meta: { lineNumber: node.lineno }
};
}
return {
kind: 'input',
text: `INPUT ${variable}`,
children: [],
meta: { variable, lineNumber: node.lineno }
};
}
/** Visit If statement */
visitIf(node) {
if (!node.test) {
console.warn('visitIf: node.test is undefined', node);
return IRFactory.comment('Invalid IF statement', node.lineno);
}
const condition = this.extractExpression(node.test);
const ifNode = IRFactory.if(condition, node.lineno);
// Process if body
if (node.body && Array.isArray(node.body)) {
for (const stmt of node.body) {
const childNode = this.visit(stmt);
if (childNode) {
ifNode.children.push(childNode);
}
}
}
const children = [ifNode];
// Process elif/else (orelse)
if (node.orelse && Array.isArray(node.orelse)) {
let i = 0;
while (i < node.orelse.length) {
const elseStmt = node.orelse[i];
if (elseStmt.type === 'If') {
// This is an elif
const elifCondition = this.extractExpression(elseStmt.test);
const elifNode = IRFactory.elseif(elifCondition, elseStmt.lineno);
// Process elif body
if (elseStmt.body && Array.isArray(elseStmt.body)) {
for (const stmt of elseStmt.body) {
const childNode = this.visit(stmt);
if (childNode) {
elifNode.children.push(childNode);
}
}
}
children.push(elifNode);
// Handle nested elif/else by continuing the loop
if (elseStmt.orelse && Array.isArray(elseStmt.orelse)) {
// Add the nested orelse to the current processing queue
node.orelse.splice(i + 1, 0, ...elseStmt.orelse);
}
}
else if (elseStmt.type === 'Else') {
// This is an else block
const elseNode = IRFactory.else(elseStmt.lineno);
// Process else body
if (elseStmt.body && Array.isArray(elseStmt.body)) {
for (const stmt of elseStmt.body) {
const childNode = this.visit(stmt);
if (childNode) {
elseNode.children.push(childNode);
}
}
}
children.push(elseNode);
}
else {
// This is a direct statement (old format compatibility)
const elseNode = IRFactory.else(elseStmt.lineno);
// Process all remaining statements as else body
for (let j = i; j < node.orelse.length; j++) {
const childNode = this.visit(node.orelse[j]);
if (childNode) {
elseNode.children.push(childNode);
}
}
children.push(elseNode);
break; // Processed all remaining statements
}
i++;
}
}
// Add ENDIF
const endifNode = IRFactory.endif(node.lineno);
children.push(endifNode);
return {
kind: 'block',
text: '',
children,
meta: { lineNumber: node.lineno }
};
}
/** Visit While statement */
visitWhile(node) {
const condition = this.extractExpression(node.test);
// Use while condition directly (no negation needed)
const loopNode = IRFactory.while(condition, node.lineno);
// Process body
if (node.body && Array.isArray(node.body)) {
for (const stmt of node.body) {
const childNode = this.visit(stmt);
if (childNode) {
loopNode.children.push(childNode);
}
}
}
// Add ENDWHILE
const endwhileNode = IRFactory.endwhile(node.lineno);
return {
kind: 'block',
text: '',
children: [loopNode, endwhileNode],
meta: { lineNumber: node.lineno }
};
}
/** Visit For statement */
visitFor(node) {
const target = this.extractExpression(node.target);
const mappedTarget = this.mapVariableName(target);
// Check if it's a range() call
if (VisitorUtils.isRangeCall(node.iter)) {
const rangeArgs = node.iter.args.map((arg) => this.extractExpression(arg));
const range = SPECIAL_CONSTRUCTS.convertRange(rangeArgs);
const forNode = IRFactory.for(mappedTarget, range.start, range.end, range.step, node.lineno);
// Process body
if (node.body && Array.isArray(node.body)) {
for (const stmt of node.body) {
const childNode = this.visit(stmt);
if (childNode) {
forNode.children.push(childNode);
}
}
}
// Add NEXT
const nextNode = IRFactory.next(mappedTarget, node.lineno);
return {
kind: 'block',
text: '',
children: [forNode, nextNode],
meta: { lineNumber: node.lineno }
};
}
// Non-range iteration (not fully supported)
return this.createWarning(`For loop over ${this.extractExpression(node.iter)} not fully supported`, node.lineno);
}
/** Visit Function definition */
visitFunctionDef(node) {
const name = this.mapFunctionName(node.name);
const params = node.args.args.map((arg) => this.mapVariableName(arg.arg || arg.id || String(arg)));
// Check if function has return statements
const hasReturn = this.hasReturnStatement(node.body || []);
let funcNode;
let endNode;
if (hasReturn) {
// Function with return value
funcNode = IRFactory.function(name, params, 'value', node.lineno);
endNode = IRFactory.endfunction(node.lineno);
}
else {
// Procedure without return value
funcNode = IRFactory.procedure(name, params, node.lineno);
endNode = IRFactory.endprocedure(node.lineno);
}
// Process body
console.log('Processing function body:', node.body);
if (node.body && node.body.length > 0) {
for (const stmt of node.body) {
console.log('Processing statement:', stmt.type, stmt);
const childIR = this.visit(stmt);
console.log('Generated IR:', childIR);
if (childIR) {
funcNode.children.push(childIR);
}
}
}
console.log('Final funcNode children:', funcNode.children.length);
return {
kind: 'block',
text: '',
children: [funcNode, endNode],
meta: { lineNumber: node.lineno }
};
}
/** Visit Return statement */
visitReturn(node) {
if (node.value) {
const value = this.extractExpression(node.value);
return IRFactory.return(value, node.lineno);
}
return IRFactory.return('', node.lineno);
}
/** Visit Pass statement */
visitPass(node) {
return {
kind: 'comment',
text: '// empty block',
children: [],
meta: { lineNumber: node.lineno }
};
}
/** Visit Comment */
visitComment(node) {
return IRFactory.comment(node.value, node.lineno);
}
/** Visit Call expression */
visitCall(node) {
const funcName = VisitorUtils.getFunctionName(node);
// Handle built-in functions
if (KeywordConverter.isBuiltinSupported(funcName)) {
const ibFuncName = KeywordConverter.convertBuiltinFunction(funcName);
const args = node.args.map((arg) => this.extractExpression(arg));
// Special handling for specific functions
switch (funcName) {
case 'print':
return this.visitPrintCall(node);
case 'input':
return this.visitInputCall(node);
case 'len':
if (args.length === 1) {
return {
kind: 'expression',
text: `SIZE(${args[0]})`,
children: [],
meta: { lineNumber: node.lineno }
};
}
break;
default:
return {
kind: 'expression',
text: `${ibFuncName}(${args.join(', ')})`,
children: [],
meta: { lineNumber: node.lineno }
};
}
}
// User-defined function call
const mappedName = this.mapFunctionName(funcName);
const args = node.args.map((arg) => this.extractExpression(arg));
return {
kind: 'expression',
text: `${mappedName}(${args.join(', ')})`,
children: [],
meta: { lineNumber: node.lineno }
};
}
/** Override extractBinaryOperation to handle operator precedence */
extractBinaryOperation(node, depth = 0) {
const left = this.extractExpression(node.left, depth + 1);
const right = this.extractExpression(node.right, depth + 1);
const operator = this.convertBinaryOperator(node.op);
// Special case: if this is an addition/subtraction that contains variables
// that match the pattern (left + right), add parentheses
if ((node.op === 'Add' || node.op === 'Sub') &&
node.left.type === 'Name' && node.right.type === 'Name' &&
(node.left.id === 'left' || node.left.id === 'right') &&
(node.right.id === 'left' || node.right.id === 'right')) {
return `(${left} ${operator} ${right})`;
}
return `${left} ${operator} ${right}`;
}
/** Override extractCall to handle special cases */
extractCall(node) {
// Handle method calls (obj.method())
if (node.func.type === 'Attribute') {
let obj = this.extractExpression(node.func.value);
const method = node.func.attr;
const args = node.args.map((arg) => this.extractExpression(arg));
// Apply variable name mapping if the object is a simple variable name
if (node.func.value.type === 'Name') {
obj = this.mapVariableName(node.func.value.id);
}
// Convert Python method names to IB Pseudocode equivalents
const methodMapping = {
'is_empty': 'isEmpty',
'has_next': 'hasNext',
'get_next': 'getNext',
'reset_next': 'resetNext'
};
const mappedMethod = methodMapping[method] || method;
return `${obj}.${mappedMethod}(${args.join(', ')})`;
}
const funcName = VisitorUtils.getFunctionName(node);
// Handle built-in functions
if (KeywordConverter.isBuiltinSupported(funcName)) {
const ibFuncName = KeywordConverter.convertBuiltinFunction(funcName);
const args = node.args.map((arg) => this.extractExpression(arg));
switch (funcName) {
case 'len':
return args.length === 1 ? `SIZE(${args[0]})` : `SIZE(${args.join(', ')})`;
case 'str':
return args.length === 1 ? `STRING(${args[0]})` : `STRING(${args.join(', ')})`;
case 'int':
return args.length === 1 ? `INTEGER(${args[0]})` : `INTEGER(${args.join(', ')})`;
case 'float':
return args.length === 1 ? `REAL(${args[0]})` : `REAL(${args.join(', ')})`;
default:
return `${ibFuncName}(${args.join(', ')})`;
}
}
// User-defined function
const mappedName = this.mapFunctionName(funcName);
const args = node.args.map((arg) => this.extractExpression(arg));
return `${mappedName}(${args.join(', ')})`;
}
/** Visit List node (array literal) */
visitList(node) {
if (!node.elts || !Array.isArray(node.elts)) {
throw new Error('Invalid List node: missing or invalid elements');
}
const elements = node.elts.map((elt) => this.extractExpression(elt));
return {
kind: 'expression',
text: `[${elements.join(', ')}]`,
children: [],
meta: { lineNumber: node.lineno }
};
}
/** Handle unsupported constructs with more specific messages */
visitUnsupported(node) {
const specificMessages = {
'ListComp': 'List comprehensions are not supported in IB Pseudocode',
'Lambda': 'Lambda functions are not supported in IB Pseudocode',
'ClassDef': 'Class definitions are not supported in basic IB Pseudocode',
'With': 'Context managers (with statements) are not supported',
'Try': 'Exception handling is partially supported',
'Import': 'Import statements are not supported in IB Pseudocode',
'ImportFrom': 'Import statements are not supported in IB Pseudocode',
'Global': 'Global declarations are not supported',
'Nonlocal': 'Nonlocal declarations are not supported'
};
const message = specificMessages[node.type] || `Unsupported Python construct: ${node.type}`;
return {
kind: 'comment',
text: `// ${message}`,
children: [],
meta: { lineNumber: node.lineno }
};
}
}
//# sourceMappingURL=python-to-ir-visitor.js.map