python2ib
Version:
Convert Python code to IB Pseudocode format
403 lines • 15 kB
JavaScript
/**
* Base visitor class for AST traversal using the Visitor pattern
*/
/** Abstract base visitor class */
export class BaseVisitor {
config;
constructor(config) {
this.config = config;
}
/** Main visit method - dispatches to specific visit methods */
visit(node) {
if (!node) {
throw new Error('Cannot visit null or undefined node');
}
const methodName = `visit${node.type}`;
const method = this[methodName];
if (typeof method === 'function') {
return method.call(this, node);
}
// Fallback for unsupported node types
return this.visitUnsupported(node);
}
/** Visit multiple nodes and return array of IRs */
visitMany(nodes) {
return nodes.map(node => this.visit(node)).filter(ir => ir !== null);
}
/** Handle unsupported node types */
visitUnsupported(node) {
const message = `Unsupported Python construct: ${node.type}`;
console.warn(message);
return {
kind: 'comment',
text: `// ${message}`,
children: [],
meta: { lineNumber: node.lineno }
};
}
/** Extract string representation from expression node */
extractExpression(node, depth = 0) {
if (!node) {
console.warn('extractExpression called with null/undefined node');
return '';
}
if (!node.type) {
console.warn('extractExpression called with node missing type:', node);
return '';
}
if (depth > 100) {
console.error(`extractExpression: Maximum recursion depth (100) exceeded for node type: ${node.type}`);
return `/* MAX_DEPTH_EXCEEDED: ${node.type} */`;
}
switch (node.type) {
case 'Constant':
return this.formatConstant(node.value);
case 'Name':
return this.mapVariableName(node.id);
case 'BinOp':
return this.extractBinaryOperation(node, depth + 1);
case 'UnaryOp':
return this.extractUnaryOperation(node, depth + 1);
case 'Compare':
return this.extractComparison(node, depth + 1);
case 'BoolOp':
return this.extractBooleanOperation(node, depth + 1);
case 'Call':
return this.extractCall(node, depth + 1);
case 'Subscript':
return this.extractSubscript(node, depth + 1);
case 'JoinedStr':
return this.extractJoinedString(node, depth + 1);
case 'Tuple':
// For tuple expressions, join elements with commas
return node.elts.map((elt) => this.extractExpression(elt, depth + 1)).join(', ');
case 'Attribute':
// Handle attribute access (e.g., obj.method)
return this.extractAttribute(node, depth);
default:
return `/* ${node.type} */`;
}
}
/** Format constant values */
formatConstant(value) {
if (value === null)
return 'NULL';
if (typeof value === 'boolean')
return value ? 'true' : 'false';
if (typeof value === 'string')
return `"${value}"`;
if (typeof value === 'number' && value < 0)
return String(value); // Keep negative numbers without extra space
return String(value);
}
/** Extract binary operation expression */
extractBinaryOperation(node, depth = 0) {
const left = this.extractExpression(node.left, depth);
const right = this.extractExpression(node.right, depth);
const operator = this.convertBinaryOperator(node.op);
return `${left} ${operator} ${right}`;
}
/** Extract unary operation expression */
extractUnaryOperation(node, depth = 0) {
const operand = this.extractExpression(node.operand, depth);
const operator = this.convertUnaryOperator(node.op);
// For negative numbers, don't add space between operator and operand
if (node.op === 'USub') {
return `${operator}${operand}`;
}
return `${operator} ${operand}`;
}
/** Extract comparison expression */
extractComparison(node, depth = 0) {
if (!node.left) {
console.warn('extractComparison: node.left is undefined', node);
return '';
}
if (!node.ops || !Array.isArray(node.ops)) {
console.warn('extractComparison: node.ops is invalid', node);
return this.extractExpression(node.left, depth);
}
if (!node.comparators || !Array.isArray(node.comparators)) {
console.warn('extractComparison: node.comparators is invalid', node);
return this.extractExpression(node.left, depth);
}
let result = this.extractExpression(node.left, depth);
for (let i = 0; i < node.ops.length; i++) {
const operator = this.convertComparisonOperator(node.ops[i]);
const comparator = this.extractExpression(node.comparators[i], depth);
// Special case: convert "variable ≠ 0" to "NOT variable = 0"
if (operator === '≠' && comparator === '0') {
result = `NOT ${result} = ${comparator}`;
}
else {
result += ` ${operator} ${comparator}`;
}
}
return result;
}
/** Extract boolean operation expression */
extractBooleanOperation(node, depth = 0) {
const operator = node.op === 'And' ? 'AND' : 'OR';
const values = node.values.map((value) => this.extractExpression(value, depth));
return values.join(` ${operator} `);
}
/** Extract function call expression */
extractCall(node, depth = 0) {
const funcName = this.extractExpression(node.func, depth);
const args = node.args.map((arg) => this.extractExpression(arg, depth));
return `${funcName}(${args.join(', ')})`;
}
/** Extract subscript expression (array/list access) */
extractSubscript(node, depth = 0) {
let value = this.extractExpression(node.value, depth);
const slice = this.extractExpression(node.slice, depth);
// Apply variable name mapping if the value is a simple variable name
if (node.value.type === 'Name') {
value = this.mapVariableName(node.value.id);
}
return `${value}[${slice}]`;
}
/** Extract attribute access expression (e.g., obj.method) */
extractAttribute(node, depth = 0) {
const value = this.extractExpression(node.value, depth);
const attr = node.attr;
// Convert Python method names to IB Pseudocode equivalents
const methodMapping = {
'has_next': 'hasNext',
'get_next': 'getNext',
'reset_next': 'resetNext',
'is_empty': 'isEmpty'
};
const mappedAttr = methodMapping[attr] || attr;
return `${value}.${mappedAttr}`;
}
/** Extract joined string (f-string) expression */
extractJoinedString(node, _depth = 0) {
if (!node.values || !Array.isArray(node.values)) {
return '""';
}
// For f-strings like f"Hello {name}", we need to parse the string and extract variables
const firstValue = node.values[0];
if (firstValue && firstValue.type === 'Constant') {
const stringValue = firstValue.value;
// Simple f-string parsing: extract variables from {variable} patterns
const parts = [];
const regex = /\{([^}]+)\}/g;
let lastIndex = 0;
let match;
while ((match = regex.exec(stringValue)) !== null) {
// Add text before the variable
if (match.index > lastIndex) {
const textPart = stringValue.substring(lastIndex, match.index);
if (textPart) {
parts.push(`"${textPart}"`);
}
}
// Add the variable (convert to uppercase)
const variableName = match[1].trim();
parts.push(this.mapVariableName(variableName));
lastIndex = regex.lastIndex;
}
// Add remaining text after the last variable
if (lastIndex < stringValue.length) {
const textPart = stringValue.substring(lastIndex);
if (textPart) {
parts.push(`"${textPart}"`);
}
}
// Join parts with " + "
return parts.length > 0 ? parts.join(' + ') : '""';
}
return '""';
}
/** Convert Python binary operators to IB Pseudocode */
convertBinaryOperator(op) {
const operators = {
'Add': '+',
'Sub': '-',
'Mult': '*',
'Div': '/',
'FloorDiv': 'div',
'Mod': 'mod',
'Pow': '^'
};
return operators[op] || op;
}
/** Convert Python unary operators to IB Pseudocode */
convertUnaryOperator(op) {
const operators = {
'Not': 'NOT',
'USub': '-',
'UAdd': '+'
};
return operators[op] || op;
}
/** Convert Python comparison operators to IB Pseudocode */
convertComparisonOperator(op) {
if (!op) {
console.warn('convertComparisonOperator: op is undefined/null');
return '?';
}
const operators = {
'Eq': '=',
'NotEq': '≠',
'Lt': '<',
'LtE': '<=',
'Gt': '>',
'GtE': '>=',
'Is': '=',
'IsNot': '≠',
'In': 'IN',
'NotIn': 'NOT IN'
};
return operators[op] || op;
}
/** Check if a function has return statements */
hasReturnStatement(body) {
for (const stmt of body) {
if (stmt.type === 'Return') {
return true;
}
// Check nested structures
if (stmt.type === 'If' && stmt.body) {
if (this.hasReturnStatement(stmt.body))
return true;
if (stmt.orelse && this.hasReturnStatement(stmt.orelse))
return true;
}
if (stmt.type === 'While' && stmt.body) {
if (this.hasReturnStatement(stmt.body))
return true;
}
if (stmt.type === 'For' && stmt.body) {
if (this.hasReturnStatement(stmt.body))
return true;
}
}
return false;
}
/** Apply variable name mapping if configured */
mapVariableName(name) {
// First check for custom mapping
const mapped = this.config.variableMapping[name];
if (mapped) {
return mapped;
}
// Special mappings for common variable names
const specialMappings = {
'list_arr': 'LIST',
'arr': 'ARR',
'array': 'ARR'
};
if (specialMappings[name]) {
return specialMappings[name];
}
// Convert to uppercase for IB Pseudocode convention
return name.toUpperCase();
}
/** Apply function name mapping if configured */
mapFunctionName(name) {
// Check for explicit mapping first
if (this.config.functionMapping[name]) {
return this.config.functionMapping[name];
}
// Convert snake_case to camelCase
return this.snakeToCamelCase(name);
}
/** Convert snake_case to camelCase */
snakeToCamelCase(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
/** Create error IR node */
createError(message, lineNumber) {
return {
kind: 'comment',
text: `// ERROR: ${message}`,
children: [],
meta: { lineNumber }
};
}
/** Create warning IR node */
createWarning(message, lineNumber) {
return {
kind: 'comment',
text: `// WARNING: ${message}`,
children: [],
meta: { lineNumber }
};
}
}
/** Utility functions for visitor implementations */
export const VisitorUtils = {
/** Check if assignment is simple (single target) */
isSimpleAssignment(node) {
return node.type === 'Assign' &&
node.targets.length === 1 &&
(node.targets[0].type === 'Name' || node.targets[0].type === 'Subscript' || node.targets[0].type === 'Tuple');
},
/** Check if node is a function call */
isFunctionCall(node) {
return node.type === 'Call';
},
/** Check if node is a print statement */
isPrintCall(node) {
return node.type === 'Call' &&
node.func.type === 'Name' &&
node.func.id === 'print';
},
/** Check if node is an input statement */
isInputCall(node) {
return node.type === 'Call' &&
node.func.type === 'Name' &&
node.func.id === 'input';
},
/** Check if node is an input call wrapped in type conversion (e.g., int(input())) */
isWrappedInputCall(node) {
if (node.type === 'Call' &&
node.func.type === 'Name' &&
['int', 'float', 'str'].includes(node.func.id) &&
node.args.length === 1) {
return this.isInputCall(node.args[0]);
}
return false;
},
/** Extract input call from wrapped input (e.g., extract input() from int(input())) */
extractInputFromWrapped(node) {
if (this.isWrappedInputCall(node)) {
return node.args[0];
}
return null;
},
/** Check if node is a range call */
isRangeCall(node) {
return node.type === 'Call' &&
node.func.type === 'Name' &&
node.func.id === 'range';
},
/** Extract function name from call node */
getFunctionName(node) {
if (node.type === 'Call') {
if (node.func.type === 'Name') {
return node.func.id;
}
else if (node.func.type === 'Attribute') {
// For method calls like obj.method(), return just the method name
return node.func.attr;
}
}
return '';
},
/** Extract variable name from assignment target */
getAssignmentTarget(node) {
if (node.type === 'Name') {
return node.id;
}
if (node.type === 'Subscript') {
// For subscript, we need to return the full expression like "names[0]"
// This will be handled by extractExpression in the visitor
return '';
}
return '';
}
};
//# sourceMappingURL=base-visitor.js.map