From 5a3f1acc3ab0fdc4f671ccf530a93af9c8f84050 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 4 Sep 2025 17:16:33 +0800 Subject: [PATCH 01/16] =?UTF-8?q?Setup=20new=20execution=20path=20for=20Py?= =?UTF-8?q?thon=20AST=20[Refs=C2=A0#51][1]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated index.ts with runPyAST (Python AST -> CSE) * py_interpreter.ts in CSE (to replace interpreter.ts in the future) * PyRunCSEMachine in pyRunner.ts * added local.test.ts to gitignore * naming convention for files/functions to replace old logic will start with "Py" -- to be renamed after full migration --- .gitignore | 5 ++++- src/cse-machine/py_interpreter.ts | 27 +++++++++++++++++++++++++++ src/index.ts | 22 +++++++++++++++++++++- src/runner/pyRunner.ts | 10 +++++++++- 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/cse-machine/py_interpreter.ts diff --git a/.gitignore b/.gitignore index 196a12d..5281521 100644 --- a/.gitignore +++ b/.gitignore @@ -107,4 +107,7 @@ dist .idea # TypeScript build files -build/ \ No newline at end of file +build/ + +# Ignore personal test files +*.local.test.ts \ No newline at end of file diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts new file mode 100644 index 0000000..d76d701 --- /dev/null +++ b/src/cse-machine/py_interpreter.ts @@ -0,0 +1,27 @@ +import { Context } from "./context"; +import { CSEBreak, Representation, Result, Finished } from "../types"; +import { StmtNS } from "../ast-types"; +import { Value, ErrorValue } from "./stash"; + +type Stmt = StmtNS.Stmt; + +export function PyCSEResultPromise(context: Context, value: Value): Promise { + return new Promise((resolve, reject) => { + if (value instanceof CSEBreak) { + resolve({ status: 'suspended-cse-eval', context }); + } else if (value.type === 'error') { + const errorValue = value as ErrorValue; + const representation = new Representation(errorValue.message); + resolve({ status: 'finished', context, value, representation } as Finished); + } else { + const representation = new Representation(value); + resolve({ status: 'finished', context, value, representation } as Finished); + } + }); +} + +export function PyEvaluate(code: string, program: Stmt, context: Context): Promise { + // dummy for now, just to test getting AST from parser + const dummyValue = { type: 'NoneType', value: undefined }; + return PyCSEResultPromise(context, dummyValue); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 48eaf32..cdd87cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -141,6 +141,10 @@ import { runCSEMachine } from "./runner/pyRunner"; import { initialise } from "./conductor/runner/util/initialise"; import { PyEvaluator } from "./conductor/runner/types/PyEvaluator"; export * from './errors'; +import { PyRunCSEMachine } from "./runner/pyRunner"; +import { StmtNS } from "./ast-types"; + +type Stmt = StmtNS.Stmt; export function parsePythonToEstreeAst(code: string, variant: number = 1, @@ -205,4 +209,20 @@ export async function runInContext( return result; } -const {runnerPlugin, conduit} = initialise(PyEvaluator); + + +export async function runPyAST( + code: string, + context: Context, + options: RecursivePartial = {} +): Promise { + const script = code + "\n"; + const tokenizer = new Tokenizer(script); + const tokens = tokenizer.scanEverything(); + const pyParser = new Parser(script, tokens); + const ast = pyParser.parse(); + const result = PyRunCSEMachine(code, ast, context, options); + return result; +}; + +const {runnerPlugin, conduit} = initialise(PyEvaluator); \ No newline at end of file diff --git a/src/runner/pyRunner.ts b/src/runner/pyRunner.ts index 4e6ac0f..0210b64 100644 --- a/src/runner/pyRunner.ts +++ b/src/runner/pyRunner.ts @@ -3,8 +3,16 @@ import { Context } from "../cse-machine/context" import { CSEResultPromise, evaluate } from "../cse-machine/interpreter" import { RecursivePartial, Result } from "../types" import * as es from 'estree' +import { PyEvaluate } from "../cse-machine/py_interpreter" +import { StmtNS } from "../ast-types"; export function runCSEMachine(code: string, program: es.Program, context: Context, options: RecursivePartial = {}): Promise { const result = evaluate(code, program, context, options); return CSEResultPromise(context, result); -} \ No newline at end of file +} + +type Stmt = StmtNS.Stmt; + +export function PyRunCSEMachine(code: string, program: Stmt, context: Context, options: RecursivePartial = {}): Promise { + return PyEvaluate(code, program, context); +} From b9226ca2efcdeb46bbf1e13f62c2bab70d1eff82 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 5 Sep 2025 11:26:11 +0800 Subject: [PATCH 02/16] Evaluating expression and operators (Refs #51)[2] * new files: py_visitor, py_utils, py_operators - to support needs for py_visitor for now, usage of any for loose type checks for now * py-interpreter - replaced dummy with PyVisitor * py_visitor - rework visit*expr to work with ExprNS and FileInputStmt * py_utils - reworked operandTranslator to work with TokenType * py_operators - reworked to change es.UnaryOperator to TokenType --- src/cse-machine/py_interpreter.ts | 8 +- src/cse-machine/py_operators.ts | 442 ++++++++++++++++++++++++++++++ src/cse-machine/py_utils.ts | 75 +++++ src/cse-machine/py_visitor.ts | 138 ++++++++++ 4 files changed, 660 insertions(+), 3 deletions(-) create mode 100644 src/cse-machine/py_operators.ts create mode 100644 src/cse-machine/py_utils.ts create mode 100644 src/cse-machine/py_visitor.ts diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index d76d701..bac3bfa 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -2,6 +2,7 @@ import { Context } from "./context"; import { CSEBreak, Representation, Result, Finished } from "../types"; import { StmtNS } from "../ast-types"; import { Value, ErrorValue } from "./stash"; +import { PyVisitor } from "./py_visitor"; type Stmt = StmtNS.Stmt; @@ -9,7 +10,7 @@ export function PyCSEResultPromise(context: Context, value: Value): Promise { if (value instanceof CSEBreak) { resolve({ status: 'suspended-cse-eval', context }); - } else if (value.type === 'error') { + } else if (value && value.type === 'error') { const errorValue = value as ErrorValue; const representation = new Representation(errorValue.message); resolve({ status: 'finished', context, value, representation } as Finished); @@ -22,6 +23,7 @@ export function PyCSEResultPromise(context: Context, value: Value): Promise { // dummy for now, just to test getting AST from parser - const dummyValue = { type: 'NoneType', value: undefined }; - return PyCSEResultPromise(context, dummyValue); + const visitor = new PyVisitor(code, context); + const result = visitor.visit(program); + return PyCSEResultPromise(context, result); } \ No newline at end of file diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts new file mode 100644 index 0000000..5b00c86 --- /dev/null +++ b/src/cse-machine/py_operators.ts @@ -0,0 +1,442 @@ +import { Context } from "./context"; +import { PyComplexNumber } from "../types"; +import { TypeConcatenateError, UnsupportedOperandTypeError, ZeroDivisionError } from "../errors/errors"; +import { ExprNS } from "../ast-types"; +import { TokenType } from "../tokens"; +import { handleRuntimeError, operandTranslator, pythonMod, typeTranslator } from "./py_utils"; +import { Token } from "../tokenizer"; + +export type BinaryOperator = + | "==" + | "!=" + | "===" + | "!==" + | "<" + | "<=" + | ">" + | ">=" + | "<<" + | ">>" + | ">>>" + | "+" + | "-" + | "*" + | "/" + | "%" + | "**" + | "|" + | "^" + | "&" + | "in" + | "instanceof"; + +// Changed operator from es.UnaryOperator to TokenType +// Added parameters command and context for error handling +// no changes to logic +export function evaluateUnaryExpression(operator: TokenType, value: any, command: ExprNS.Expr, context: Context) { + if (operator === TokenType.NOT) { + if (value.type === 'bool') { + return { + type: 'bool', + value: !(Boolean(value.value)) + }; + } else { + // TODO: error handling for unsupported type + // TODO: command is currently passed as "any", to be adjusted in future commits[from #2] + // handleRuntimeError(context, new UnsupportedOperandTypeError("", command as any, typeTranslator(value.type), "", operandTranslator(operator))) + } + } else if (operator === TokenType.MINUS) { + if (value.type === 'bigint') { + return { + type: 'bigint', + value: -value.value + }; + } else if (value.type === 'number') { + return { + type: 'number', + value: -Number(value.value) + }; + } else { + // TODO: error handling for unsupported type + // TODO: command is currently passed as "any", to be adjusted in future commits[from #2] + // handleRuntimeError(context, new UnsupportedOperandTypeError("", command as any, typeTranslator(value.type), "", operandTranslator(operator))) + } + } else if (operator === TokenType.PLUS) { + if (value.type === 'complex' || value.type === 'number' || value.type === 'bigint') { + return value + } else { + // TODO: error handling for unsupported type + // TODO: command is currently passed as "any", to be adjusted in future commits[from #2] + // handleRuntimeError(context, new UnsupportedOperandTypeError("", command as any, typeTranslator(value.type), "", operandTranslator(operator))) + } + } else { + // Fallback for other unary operators if they exist in the future + return value; + } +} + +// Change command from "ControlItem" to "ExprNS.Expr", "identifier" to "operator" for type safety +export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: Context, operator: TokenType | string, left: any, right: any) { + + const operand = operandTranslator(operator); + const originalLeftType = typeTranslator(left.type); + const originalRightType = typeTranslator(right.type); + + if (left.type === 'string' && right.type === 'string') { + if(operator === '__py_adder') { + return { + type: 'string', + value: left.value + right.value + }; + } else { + let ret_type : any; + let ret_value : any; + if (operator === '>') { + ret_value = left.value > right.value; + } else if(operator === '>=') { + ret_value = left.value >= right.value; + } else if(operator === '<') { + ret_value = left.value < right.value; + } else if(operator === '<=') { + ret_value = left.value <= right.value; + } else if(operator === '===') { + ret_value = left.value === right.value; + } else if(operator === '!==') { + ret_value = left.value !== right.value; + } else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + } + + return { + type: 'bool', + value: ret_value + }; + } + } else { + // numbers: only int and float, not bool + const numericTypes = ['number', 'bigint', 'complex']; + if (!numericTypes.includes(left.type) || !numericTypes.includes(right.type)) { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + } + + let originalLeft = { type : left.type, value : left.value }; + let originalRight = { type : right.type, value : right.value }; + + if (left.type !== right.type) { + if (left.type === 'complex' || right.type === 'complex') { + left.type = 'complex'; + right.type = 'complex'; + left.value = PyComplexNumber.fromValue(left.value); + right.value = PyComplexNumber.fromValue(right.value); + } else if (left.type === 'number' || right.type === 'number') { + left.type = 'number'; + right.type = 'number'; + left.value = Number(left.value); + right.value = Number(right.value); + } + } + + let ret_value : any; + let ret_type : any = left.type; + + if(typeof operator === 'string') { + if(operator === '__py_adder') { + if (left.type === 'complex' || right.type === 'complex') { + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + ret_value = leftComplex.add(rightComplex); + } else { + ret_value = left.value + right.value; + } + } else if(operator === '__py_minuser') { + if (left.type === 'complex' || right.type === 'complex') { + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + ret_value = leftComplex.sub(rightComplex); + } else { + ret_value = left.value - right.value; + } + } else if(operator === '__py_multiplier') { + if (left.type === 'complex' || right.type === 'complex') { + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + ret_value = leftComplex.mul(rightComplex); + } else { + ret_value = left.value * right.value; + } + } else if(operator === '__py_divider') { + if (left.type === 'complex' || right.type === 'complex') { + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + ret_value = leftComplex.div(rightComplex); + } else { + if((right.type === 'bigint' && Number(right.value) !== 0) || + (right.type === 'number' && right.value !== 0)) { + ret_type = 'number'; + ret_value = Number(left.value) / Number(right.value); + } else { + // handleRuntimeError(context, new ZeroDivisionError(code, command as any, context)); + } + } + } else if(operator === '__py_modder') { + if (left.type === 'complex') { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + } + ret_value = pythonMod(left.value, right.value); + } else if(operator === '__py_floorer') { + // TODO: floorer not in python now + // see math_floor in stdlib.ts + ret_value = 0; + } else if(operator === '__py_powerer') { + if (left.type === 'complex') { + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + ret_value = leftComplex.pow(rightComplex); + } else { + if (left.type === 'bigint' && right.value < 0) { + ret_value = Number(left.value) ** Number(right.value); + ret_type = 'number'; + } else { + ret_value = left.value ** right.value; + } + } + } else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + } + } else { + ret_type = 'bool'; + // one of them is complex, convert all to complex then compare + // for complex, only '==' and '!=' valid + if (left.type === 'complex') { + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + + if (operator === TokenType.DOUBLEEQUAL) { + ret_value = leftComplex.equals(rightComplex); + } else if (operator === TokenType.NOTEQUAL) { + ret_value = !leftComplex.equals(rightComplex); + } else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + } + } else if (originalLeft.type !== originalRight.type) { + let int_num : any; + let floatNum : any; + let compare_res; + if (originalLeft.type === 'bigint') { + int_num = originalLeft; + floatNum = originalRight; + compare_res = pyCompare(int_num, floatNum); + } else { + int_num = originalRight; + floatNum = originalLeft; + compare_res = -pyCompare(int_num, floatNum); + } + + if (operator === TokenType.GREATER) { + ret_value = compare_res > 0; + } else if(operator === TokenType.GREATEREQUAL) { + ret_value = compare_res >= 0; + } else if(operator === TokenType.LESS) { + ret_value = compare_res < 0; + } else if(operator === TokenType.LESSEQUAL) { + ret_value = compare_res <= 0; + } else if(operator === TokenType.DOUBLEEQUAL) { + ret_value = compare_res === 0; + } else if(operator === TokenType.NOTEQUAL) { + ret_value = compare_res !== 0; + } else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + } + } else { + if (operator === TokenType.GREATER) { + ret_value = left.value > right.value; + } else if(operator === TokenType.GREATEREQUAL) { + ret_value = left.value >= right.value; + } else if(operator === TokenType.LESS) { + ret_value = left.value < right.value; + } else if(operator === TokenType.LESSEQUAL) { + ret_value = left.value <= right.value; + } else if(operator === TokenType.DOUBLEEQUAL) { + ret_value = left.value === right.value; + } else if(operator === TokenType.NOTEQUAL) { + ret_value = left.value !== right.value; + } else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + } + } + + } + + return { + type: ret_type, + value: ret_value + }; + } +} + +/** + * TEMPORARY IMPLEMENTATION + * This function is a simplified comparison between int and float + * to mimic Python-like ordering semantics. + * + * TODO: In future, replace this with proper method dispatch to + * __eq__, __lt__, __gt__, etc., according to Python's object model. + * + * pyCompare: Compares a Python-style big integer (int_num) with a float (float_num), + * returning -1, 0, or 1 for less-than, equal, or greater-than. + * + * This logic follows CPython's approach in floatobject.c, ensuring Python-like semantics: + * + * 1. Special Values: + * - If float_num is inf, any finite int_num is smaller (returns -1). + * - If float_num is -inf, any finite int_num is larger (returns 1). + * + * 2. Compare by Sign: + * - Determine each number’s sign (negative, zero, or positive). If they differ, return based on sign. + * - If both are zero, treat them as equal. + * + * 3. Safe Conversion: + * - If |int_num| <= 2^53, safely convert it to a double and do a normal floating comparison. + * + * 4. Handling Large Integers: + * - For int_num beyond 2^53, approximate the magnitudes via exponent/bit length. + * - Compare the integer’s digit count with float_num’s order of magnitude. + * + * 5. Close Cases: + * - If both integer and float have the same digit count, convert float_num to a “big-int-like” string + * (approximateBigIntString) and compare lexicographically to int_num’s string. + * + * By layering sign checks, safe numeric range checks, and approximate comparisons, + * we achieve a Python-like ordering of large integers vs floats. + */ +function pyCompare(int_num : any, float_num : any) { + // int_num.value < float_num.value => -1 + // int_num.value = float_num.value => 0 + // int_num.value > float_num.value => 1 + + // If float_num is positive Infinity, then int_num is considered smaller. + if (float_num.value === Infinity) { + return -1; + } + if (float_num.value === -Infinity) { + return 1; + } + + const signInt = (int_num.value < 0) ? -1 : (int_num.value > 0 ? 1 : 0); + const signFlt = Math.sign(float_num.value); // -1, 0, or 1 + + if (signInt < signFlt) return -1; // e.g. int<0, float>=0 => int < float + if (signInt > signFlt) return 1; // e.g. int>=0, float<0 => int > float + + // Both have the same sign (including 0). + // If both are zero, treat them as equal. + if (signInt === 0 && signFlt === 0) { + return 0; + } + + // Both are either positive or negative. + // If |int_num.value| is within 2^53, it can be safely converted to a JS number for an exact comparison. + const absInt = int_num.value < 0 ? -int_num.value : int_num.value; + const MAX_SAFE = 9007199254740991; // 2^53 - 1 + + if (absInt <= MAX_SAFE) { + // Safe conversion to double. + const intAsNum = Number(int_num.value); + const diff = intAsNum - float_num.value; + if (diff === 0) return 0; + return diff < 0 ? -1 : 1; + } + + // For large integers exceeding 2^53, need to distinguish more carefully. + // Determine the order of magnitude of float_num.value (via log10) and compare it with + // the number of digits of int_num.value. An approximate comparison can indicate whether + // int_num.value is greater or less than float_num.value. + + // First, check if float_num.value is nearly zero (but not zero). + if (float_num.value === 0) { + // Although signFlt would be 0 and handled above, just to be safe: + return signInt; + } + + const absFlt = Math.abs(float_num.value); + // Determine the order of magnitude. + const exponent = Math.floor(Math.log10(absFlt)); + + // Get the decimal string representation of the absolute integer. + const intStr = absInt.toString(); + const intDigits = intStr.length; + + // If exponent + 1 is less than intDigits, then |int_num.value| has more digits + // and is larger (if positive) or smaller (if negative) than float_num.value. + // Conversely, if exponent + 1 is greater than intDigits, int_num.value has fewer digits. + const integerPartLen = exponent + 1; + if (integerPartLen < intDigits) { + // length of int_num.value is larger => all positive => int_num.value > float_num.value + // => all negative => int_num.value < float_num.value + return (signInt > 0) ? 1 : -1; + } else if (integerPartLen > intDigits) { + // length of int_num.value is smaller => all positive => int_num.value < float_num.value + // => all negative => int_num.value > float_num.value + return (signInt > 0) ? -1 : 1; + } else { + // If the number of digits is the same, they may be extremely close. + // Method: Convert float_num.value into an approximate BigInt string and perform a lexicographical comparison. + const floatApproxStr = approximateBigIntString(absFlt, 30); + + const aTrim = intStr.replace(/^0+/, ''); + const bTrim = floatApproxStr.replace(/^0+/, ''); + + // If lengths differ after trimming, the one with more digits is larger. + if (aTrim.length > bTrim.length) { + return (signInt > 0) ? 1 : -1; + } else if (aTrim.length < bTrim.length) { + return (signInt > 0) ? -1 : 1; + } else { + // Same length: use lexicographical comparison. + const cmp = aTrim.localeCompare(bTrim); + if (cmp === 0) { + return 0; + } + // cmp>0 => aTrim > bTrim => aVal > bVal + return (cmp > 0) ? (signInt > 0 ? 1 : -1) + : (signInt > 0 ? -1 : 1); + } + } +} + +function approximateBigIntString(num: number, precision: number): string { + // Use scientific notation to obtain a string in the form "3.333333333333333e+49" + const s = num.toExponential(precision); + // Split into mantissa and exponent parts. + // The regular expression matches strings of the form: /^([\d.]+)e([+\-]\d+)$/ + const match = s.match(/^([\d.]+)e([+\-]\d+)$/); + if (!match) { + // For extremely small or extremely large numbers, toExponential() should follow this format. + // As a fallback, return Math.floor(num).toString() + return Math.floor(num).toString(); + } + let mantissaStr = match[1]; // "3.3333333333..." + const exp = parseInt(match[2], 10); // e.g. +49 + + // Remove the decimal point + mantissaStr = mantissaStr.replace('.', ''); + // Get the current length of the mantissa string + const len = mantissaStr.length; + // Calculate the required integer length: for exp ≥ 0, we want the integer part + // to have (1 + exp) digits. + const integerLen = 1 + exp; + if (integerLen <= 0) { + // This indicates num < 1 (e.g., exponent = -1, mantissa = "3" results in 0.xxx) + // For big integer comparison, such a number is very small, so simply return "0" + return "0"; + } + + if (len < integerLen) { + // The mantissa is not long enough; pad with zeros at the end. + return mantissaStr.padEnd(integerLen, '0'); + } + // If the mantissa is too long, truncate it (this is equivalent to taking the floor). + // Rounding could be applied if necessary, but truncation is sufficient for comparison. + return mantissaStr.slice(0, integerLen); +} + \ No newline at end of file diff --git a/src/cse-machine/py_utils.ts b/src/cse-machine/py_utils.ts new file mode 100644 index 0000000..241b4d2 --- /dev/null +++ b/src/cse-machine/py_utils.ts @@ -0,0 +1,75 @@ +import { Context } from "./context"; +import { ExprNS } from "../ast-types"; +import { TokenType } from "../tokens"; +import { UnsupportedOperandTypeError } from "../errors/errors"; + +export function handleRuntimeError (context: Context, error: any) { + throw error; +} + +export function typeTranslator(type: string): string { + switch (type) { + case "bigint": + return "int"; + case "number": + return "float"; + case "boolean": + return "bool"; + case "bool": + return "bool"; + case "string": + return "string"; + case "complex": + return "complex"; + default: + return "unknown"; + } +} + +// TODO: properly adapt for the rest, string is passed in to cater for __py_adder etc... +export function operandTranslator(operand: TokenType | string) { + if (typeof operand === 'string') { + return operand; + } + switch (operand) { + case TokenType.PLUS: + return '+'; + case TokenType.MINUS: + return '-'; + case TokenType.STAR: + return '*'; + case TokenType.SLASH: + return '/'; + case TokenType.LESS: + return '<'; + case TokenType.GREATER: + return '>'; + case TokenType.PERCENT: + return '%'; + case TokenType.DOUBLEEQUAL: + return '=='; + case TokenType.NOTEQUAL: + return '!=' + case TokenType.LESSEQUAL: + return '<='; + case TokenType.GREATEREQUAL: + return '>='; + case TokenType.DOUBLESTAR: + return '**'; + case TokenType.NOT: + return 'not'; + case TokenType.DOUBLESLASH: + return '//'; + default: + return String(operand); + } +} + +export function pythonMod(a: any, b: any): any { + const mod = a % b; + if ((mod >= 0 && b > 0) || (mod <= 0 && b < 0)) { + return mod; + } else { + return mod + b; + } +} diff --git a/src/cse-machine/py_visitor.ts b/src/cse-machine/py_visitor.ts new file mode 100644 index 0000000..16b86aa --- /dev/null +++ b/src/cse-machine/py_visitor.ts @@ -0,0 +1,138 @@ +import { ExprNS, StmtNS } from "../ast-types"; +import { Context } from "./context"; +// TODO: setup py_operators +import { evaluateBinaryExpression, evaluateUnaryExpression } from "./py_operators"; +import { TokenType } from "../tokens"; +import { Token } from "../tokenizer"; + +type Stmt = StmtNS.Stmt; +type Expr = ExprNS.Expr; + +// TODO: type 'any' to be changed to node type for replacement of es.Node +export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { + private code: string; + private context: Context; + + constructor(code: string, context: Context) { + this.code = code; + this.context = context; + } + + // Main entry point + visit(node: Stmt | Expr): any { + return node.accept(this); + } + + private mapOperatorToPyOperator(operatorToken: Token): TokenType | string { + switch (operatorToken.type) { + // return string names that py_operators expect + case TokenType.PLUS: + return '__py_adder'; + case TokenType.MINUS: + return '__py_minuser'; + case TokenType.STAR: + return '__py_multiplier'; + case TokenType.SLASH: + return '__py_divider'; + case TokenType.PERCENT: + return '__py_modder'; + case TokenType.DOUBLESTAR: + return '__py_powerer'; + case TokenType.DOUBLESLASH: + return '__py_floorer'; + + // pass TokenType for comparisons and unary + case TokenType.GREATER: return TokenType.GREATER; + case TokenType.GREATEREQUAL: return TokenType.GREATEREQUAL; + case TokenType.LESS: return TokenType.LESS; + case TokenType.LESSEQUAL: return TokenType.LESSEQUAL; + case TokenType.DOUBLEEQUAL: return TokenType.DOUBLEEQUAL; + case TokenType.NOTEQUAL: return TokenType.NOTEQUAL; + case TokenType.NOT: return TokenType.NOT; + default: + throw new Error(`Unsupported operator token type for mapping: ${TokenType[operatorToken.type]}`); + } + } + + // Expression Visitors + visitLiteralExpr(expr: ExprNS.Literal): any { + return { + type: typeof expr.value, + value: expr.value, + }; + } + + visitUnaryExpr(expr: ExprNS.Unary): any { + const argumentValue = this.visit(expr.right); + return evaluateUnaryExpression(expr.operator.type, argumentValue, expr, this.context); + } + + visitBinaryExpr(expr: ExprNS.Binary): any { + const leftValue = this.visit(expr.left); + const rightValue = this.visit(expr.right); + const operatorForPyOperators = this.mapOperatorToPyOperator(expr.operator); + + return evaluateBinaryExpression( + this.code, + expr, + this.context, + operatorForPyOperators, + leftValue, + rightValue, + ) + } + + visitBigIntLiteralExpr(expr: ExprNS.BigIntLiteral): any { + return { + type: 'bigint', + value: BigInt(expr.value) + }; + } + + // Placeholder for TODO expr visitors + visitCompareExpr(expr: ExprNS.Compare): any { /* TODO */ } + visitBoolOpExpr(expr: ExprNS.BoolOp): any { /* TODO */ } + visitGroupingExpr(expr: ExprNS.Grouping): any { return this.visit(expr.expression);} + visitTernaryExpr(expr: ExprNS.Ternary): any { /* TODO */ } + visitLambdaExpr(expr: ExprNS.Lambda): any { /* TODO */ } + visitMultiLambdaExpr(expr: ExprNS.MultiLambda): any { /* TODO */ } + visitVariableExpr(expr: ExprNS.Variable): any { /* TODO */ } + visitCallExpr(expr: ExprNS.Call): any { /* TODO */ } + visitComplexExpr(expr: ExprNS.Complex): any { /* TODO */ } + visitNoneExpr(expr: ExprNS.None): any { /* TODO */ } + + // Statement Visitors + visitFileInputStmt(stmt: StmtNS.FileInput): any { + let lastValue: any; + for (const statement of stmt.statements) { + lastValue = this.visit(statement); + } + return lastValue; + } + + visitSimpleExprStmt(stmt: StmtNS.SimpleExpr): any { + return this.visit(stmt.expression); + } + + + // Placeholder for TODO stmt visitors + visitIndentCreation(stmt: StmtNS.Indent): any { /* TODO */ } + visitDedentCreation(stmt: StmtNS.Dedent): any { /* TODO */ } + visitPassStmt(stmt: StmtNS.Pass): any { /* TODO */ } + visitAssignStmt(stmt: StmtNS.Assign): any { /* TODO */ } + visitAnnAssignStmt(stmt: StmtNS.AnnAssign): any { /* TODO */ } + visitBreakStmt(stmt: StmtNS.Break): any { /* TODO */ } + visitContinueStmt(stmt: StmtNS.Continue): any { /* TODO */ } + visitReturnStmt(stmt: StmtNS.Return): any { /* TODO */ } + visitFromImportStmt(stmt: StmtNS.FromImport): any { /* TODO */ } + visitGlobalStmt(stmt: StmtNS.Global): any { /* TODO */ } + visitNonLocalStmt(stmt: StmtNS.NonLocal): any { /* TODO */ } + visitAssertStmt(stmt: StmtNS.Assert): any { /* TODO */ } + visitIfStmt(stmt: StmtNS.If): any { /* TODO */ } + visitWhileStmt(stmt: StmtNS.While): any { /* TODO */ } + visitForStmt(stmt: StmtNS.For): any { /* TODO */ } + visitFunctionDefStmt(stmt: StmtNS.FunctionDef): any { /* TODO */ } + + + +} \ No newline at end of file From d9ffc84e36ac0343ce22d0545f7f9bf609c9b7e0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 11 Sep 2025 18:14:38 +0800 Subject: [PATCH 03/16] Attempt to reinstate Value [failed](Refs #51)[3] * Updated compilerOptions in tsconfig.json - target: es2016 -> es2020 Reason: support for bigInt(0n) * stash.ts - Reintroduce Value to be passed to visitors in pyvisitor Updates: Failed lol, major changes that affects stdlib in src Temp solution: fallback to Value : any in /cse-machine/stash.ts for now * py_operators.ts - introduce bool for integers, string, None (evaluteUnaryExpression) * py_operator.ts refactored {evaluateBinaryExpression} - cleaner sequence Operands: String -> Complex -> Mixed Float&/Int (arithmetic -> comparisons) -> Integers only * py_visitor.ts: added visitComplexExpr --- src/cse-machine/py_interpreter.ts | 3 +- src/cse-machine/py_operators.ts | 501 +++++++++++++++--------------- src/cse-machine/py_visitor.ts | 27 +- src/cse-machine/stash.ts | 30 +- src/index.ts | 34 +- tsconfig.json | 2 +- 6 files changed, 320 insertions(+), 277 deletions(-) diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index bac3bfa..db58053 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -3,6 +3,7 @@ import { CSEBreak, Representation, Result, Finished } from "../types"; import { StmtNS } from "../ast-types"; import { Value, ErrorValue } from "./stash"; import { PyVisitor } from "./py_visitor"; +import { toPythonString } from "../stdlib"; type Stmt = StmtNS.Stmt; @@ -15,7 +16,7 @@ export function PyCSEResultPromise(context: Context, value: Value): Promise') { - ret_value = left.value > right.value; - } else if(operator === '>=') { - ret_value = left.value >= right.value; - } else if(operator === '<') { - ret_value = left.value < right.value; - } else if(operator === '<=') { - ret_value = left.value <= right.value; - } else if(operator === '===') { - ret_value = left.value === right.value; - } else if(operator === '!==') { - ret_value = left.value !== right.value; - } else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - } - - return { - type: 'bool', - value: ret_value - }; - } - } else { - // numbers: only int and float, not bool - const numericTypes = ['number', 'bigint', 'complex']; - if (!numericTypes.includes(left.type) || !numericTypes.includes(right.type)) { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - } - - let originalLeft = { type : left.type, value : left.value }; - let originalRight = { type : right.type, value : right.value }; - - if (left.type !== right.type) { - if (left.type === 'complex' || right.type === 'complex') { - left.type = 'complex'; - right.type = 'complex'; - left.value = PyComplexNumber.fromValue(left.value); - right.value = PyComplexNumber.fromValue(right.value); - } else if (left.type === 'number' || right.type === 'number') { - left.type = 'number'; - right.type = 'number'; - left.value = Number(left.value); - right.value = Number(right.value); - } - } - - let ret_value : any; - let ret_type : any = left.type; - - if(typeof operator === 'string') { - if(operator === '__py_adder') { - if (left.type === 'complex' || right.type === 'complex') { - const leftComplex = PyComplexNumber.fromValue(left.value); - const rightComplex = PyComplexNumber.fromValue(right.value); - ret_value = leftComplex.add(rightComplex); - } else { - ret_value = left.value + right.value; - } - } else if(operator === '__py_minuser') { - if (left.type === 'complex' || right.type === 'complex') { - const leftComplex = PyComplexNumber.fromValue(left.value); - const rightComplex = PyComplexNumber.fromValue(right.value); - ret_value = leftComplex.sub(rightComplex); - } else { - ret_value = left.value - right.value; - } - } else if(operator === '__py_multiplier') { - if (left.type === 'complex' || right.type === 'complex') { - const leftComplex = PyComplexNumber.fromValue(left.value); - const rightComplex = PyComplexNumber.fromValue(right.value); - ret_value = leftComplex.mul(rightComplex); - } else { - ret_value = left.value * right.value; - } - } else if(operator === '__py_divider') { - if (left.type === 'complex' || right.type === 'complex') { - const leftComplex = PyComplexNumber.fromValue(left.value); - const rightComplex = PyComplexNumber.fromValue(right.value); - ret_value = leftComplex.div(rightComplex); - } else { - if((right.type === 'bigint' && Number(right.value) !== 0) || - (right.type === 'number' && right.value !== 0)) { - ret_type = 'number'; - ret_value = Number(left.value) / Number(right.value); - } else { - // handleRuntimeError(context, new ZeroDivisionError(code, command as any, context)); - } - } - } else if(operator === '__py_modder') { - if (left.type === 'complex') { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - } - ret_value = pythonMod(left.value, right.value); - } else if(operator === '__py_floorer') { - // TODO: floorer not in python now - // see math_floor in stdlib.ts - ret_value = 0; - } else if(operator === '__py_powerer') { - if (left.type === 'complex') { - const leftComplex = PyComplexNumber.fromValue(left.value); - const rightComplex = PyComplexNumber.fromValue(right.value); - ret_value = leftComplex.pow(rightComplex); - } else { - if (left.type === 'bigint' && right.value < 0) { - ret_value = Number(left.value) ** Number(right.value); - ret_type = 'number'; - } else { - ret_value = left.value ** right.value; - } - } - } else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - } - } else { - ret_type = 'bool'; - // one of them is complex, convert all to complex then compare - // for complex, only '==' and '!=' valid - if (left.type === 'complex') { - const leftComplex = PyComplexNumber.fromValue(left.value); - const rightComplex = PyComplexNumber.fromValue(right.value); - - if (operator === TokenType.DOUBLEEQUAL) { - ret_value = leftComplex.equals(rightComplex); - } else if (operator === TokenType.NOTEQUAL) { - ret_value = !leftComplex.equals(rightComplex); - } else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - } - } else if (originalLeft.type !== originalRight.type) { - let int_num : any; - let floatNum : any; - let compare_res; - if (originalLeft.type === 'bigint') { - int_num = originalLeft; - floatNum = originalRight; - compare_res = pyCompare(int_num, floatNum); - } else { - int_num = originalRight; - floatNum = originalLeft; - compare_res = -pyCompare(int_num, floatNum); - } - - if (operator === TokenType.GREATER) { - ret_value = compare_res > 0; - } else if(operator === TokenType.GREATEREQUAL) { - ret_value = compare_res >= 0; - } else if(operator === TokenType.LESS) { - ret_value = compare_res < 0; - } else if(operator === TokenType.LESSEQUAL) { - ret_value = compare_res <= 0; - } else if(operator === TokenType.DOUBLEEQUAL) { - ret_value = compare_res === 0; - } else if(operator === TokenType.NOTEQUAL) { - ret_value = compare_res !== 0; - } else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - } - } else { - if (operator === TokenType.GREATER) { - ret_value = left.value > right.value; - } else if(operator === TokenType.GREATEREQUAL) { - ret_value = left.value >= right.value; - } else if(operator === TokenType.LESS) { - ret_value = left.value < right.value; - } else if(operator === TokenType.LESSEQUAL) { - ret_value = left.value <= right.value; - } else if(operator === TokenType.DOUBLEEQUAL) { - ret_value = left.value === right.value; - } else if(operator === TokenType.NOTEQUAL) { - ret_value = left.value !== right.value; - } else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - } - } - - } - - return { - type: ret_type, - value: ret_value - }; - } -} - /** * TEMPORARY IMPLEMENTATION * This function is a simplified comparison between int and float @@ -309,21 +66,36 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con * By layering sign checks, safe numeric range checks, and approximate comparisons, * we achieve a Python-like ordering of large integers vs floats. */ -function pyCompare(int_num : any, float_num : any) { + +function pyCompare(val1 : Value, val2 : Value): number { // int_num.value < float_num.value => -1 // int_num.value = float_num.value => 0 // int_num.value > float_num.value => 1 + let int_val: bigint; + let float_val: number; + + if (val1.type === 'bigint' && val2.type === 'number') { + int_val = val1.value; + float_val = val2.value; + } else if (val1.type === 'number' && val2.type === 'bigint') { + int_val = val2.value; + float_val = val1.value; + // for swapped order, swap the result of comparison here + return -pyCompare(val2, val1); + } else { + return 0; + } // If float_num is positive Infinity, then int_num is considered smaller. - if (float_num.value === Infinity) { + if (float_val === Infinity) { return -1; } - if (float_num.value === -Infinity) { + if (float_val === -Infinity) { return 1; } - const signInt = (int_num.value < 0) ? -1 : (int_num.value > 0 ? 1 : 0); - const signFlt = Math.sign(float_num.value); // -1, 0, or 1 + const signInt = (int_val < 0n) ? -1 : (int_val > 0n ? 1 : 0); + const signFlt = Math.sign(float_val); // -1, 0, or 1 if (signInt < signFlt) return -1; // e.g. int<0, float>=0 => int < float if (signInt > signFlt) return 1; // e.g. int>=0, float<0 => int > float @@ -336,13 +108,13 @@ function pyCompare(int_num : any, float_num : any) { // Both are either positive or negative. // If |int_num.value| is within 2^53, it can be safely converted to a JS number for an exact comparison. - const absInt = int_num.value < 0 ? -int_num.value : int_num.value; + const absInt = int_val < 0n ? -int_val : int_val; const MAX_SAFE = 9007199254740991; // 2^53 - 1 if (absInt <= MAX_SAFE) { // Safe conversion to double. - const intAsNum = Number(int_num.value); - const diff = intAsNum - float_num.value; + const intAsNum = Number(int_val); + const diff = intAsNum - float_val; if (diff === 0) return 0; return diff < 0 ? -1 : 1; } @@ -353,12 +125,12 @@ function pyCompare(int_num : any, float_num : any) { // int_num.value is greater or less than float_num.value. // First, check if float_num.value is nearly zero (but not zero). - if (float_num.value === 0) { + if (float_val === 0) { // Although signFlt would be 0 and handled above, just to be safe: return signInt; } - const absFlt = Math.abs(float_num.value); + const absFlt = Math.abs(float_val); // Determine the order of magnitude. const exponent = Math.floor(Math.log10(absFlt)); @@ -439,4 +211,223 @@ function approximateBigIntString(num: number, precision: number): string { // Rounding could be applied if necessary, but truncation is sufficient for comparison. return mantissaStr.slice(0, integerLen); } - \ No newline at end of file + +// Changed operator from es.UnaryOperator to TokenType +// Added parameters command and context for error handling +// updated logics for TokenType.NOT [3] +export function evaluateUnaryExpression(operator: TokenType, value: any, command: ExprNS.Expr, context: Context): Value { + if (operator === TokenType.NOT) { + let isFalsy: boolean; + switch (value.type) { + case 'bigint': + isFalsy = value.value === 0n; + break; + case 'number': + isFalsy = value.value === 0; + break; + case 'bool': + isFalsy = !value.value; + break; + case 'string': + isFalsy = value.value === ''; + break; + case 'undefined': + isFalsy = true; + break; + default: + // TODO: consider strings, list as truthy if exists + // Implement falsy for empty strings..etc + isFalsy = false; + }return {type: 'bool', value: isFalsy} + + } else if (operator === TokenType.MINUS) { + if (value.type === 'bigint') { + return { + type: 'bigint', + value: -1n * value.value + }; + } else if (value.type === 'number') { + return { + type: 'number', + value: -value.value + }; + } else { + // TODO: error handling for unsupported type + // TODO: command is currently passed as "any", to be adjusted in future commits[from #2] + // handleRuntimeError(context, new UnsupportedOperandTypeError("", command as any, typeTranslator(value.type), "", operandTranslator(operator))) + } + } else if (operator === TokenType.PLUS) { + if (value.type === 'complex' || value.type === 'number' || value.type === 'bigint') { + return value; + } else { + // TODO: error handling for unsupported type + // TODO: command is currently passed as "any", to be adjusted in future commits[from #2] + // handleRuntimeError(context, new UnsupportedOperandTypeError("", command as any, typeTranslator(value.type), "", operandTranslator(operator))) + } + } + // final fallback + // handleRuntimeError + return { type: "error", message: 'unreachable' }; +} + +// Change command from "ControlItem" to "ExprNS.Expr", "identifier" to "operator" for type safety +export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: Context, operator: TokenType | string, left: Value, right: Value): Value { + + const operand = operandTranslator(operator); + const originalLeftType = typeTranslator(left.type); + const originalRightType = typeTranslator(right.type); + + // String Operations + if (left.type === 'string' && right.type === 'string') { + if(operator === '__py_adder') { + return { type: 'string', value: left.value + right.value }; + } else if (operator === TokenType.GREATER) { + return { type: 'bool', value: left.value > right.value }; + } else if (operator === TokenType.GREATEREQUAL) { + return { type: 'bool', value: left.value >= right.value }; + } else if (operator === TokenType.LESS) { + return { type: 'bool', value: left.value < right.value }; + } else if (operator === TokenType.LESSEQUAL) { + return { type: 'bool', value: left.value <= right.value }; + } else if (operator === TokenType.DOUBLEEQUAL) { + return { type: 'bool', value: left.value === right.value }; + } else if (operator === TokenType.NOTEQUAL) { + return { type: 'bool', value: left.value !== right.value }; + } else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); + return { type: 'error', message: 'Unsupported string operation' } + } + } + // Complex Operations + else if (left.type === 'complex' || right.type ==='complex'){ + let leftForComplex: number | bigint | string | PyComplexNumber; + let rightForComplex: number | bigint | string | PyComplexNumber; + + if (left.type === 'complex' || left.type === 'number' || left.type === 'bigint' || left.type === 'string') { + leftForComplex = left.value; + } else { + // handleRuntimeError + return { type: 'error', message: 'Invalid operand for complex operation' }; + } + + if (right.type === 'complex' || right.type === 'number' || right.type === 'bigint' || right.type === 'string') { + rightForComplex = right.value; + } else { + // handleRuntimeError + return { type: 'error', message: 'Invalid operand for complex operation' }; + } + + const leftComplex = PyComplexNumber.fromValue(leftForComplex); + const rightComplex = PyComplexNumber.fromValue(rightForComplex); + let result: PyComplexNumber; + + if (operator === '__py_adder') { + result = leftComplex.add(rightComplex); + } else if (operator === '__py_minuser') { + result = leftComplex.sub(rightComplex); + } else if (operator === '__py_multiplier') { + result = leftComplex.mul(rightComplex); + } else if (operator === '__py_divider') { + result = leftComplex.div(rightComplex); + } else if (operator === '__py_powerer') { + result = leftComplex.pow(rightComplex); + } else if (operator === TokenType.DOUBLEEQUAL) { + return { type: 'bool', value: leftComplex.equals(rightComplex)}; + } else if (operator === TokenType.NOTEQUAL) { + return { type: 'bool', value: !leftComplex.equals(rightComplex)}; + } else { + // handleRuntimeError + return {type: 'error', message: 'Unsupported complex operation'}; + } return {type: 'complex', value: result}; + } + // Float and or Int Operations + else if ((left.type === 'number' || left.type === 'bigint') && (right.type === 'number' || right.type === 'bigint')) { + if (left.type === 'number' || right.type === 'number' || operator === '__py_divider') { + const leftFloat = Number(left.value); + const rightFloat = Number(right.value); + let result: number | boolean; + + // Arithmetic + if (typeof operator === 'string') { + if (operator === '__py_adder') result = leftFloat + rightFloat; + else if (operator === '__py_minuser') result = leftFloat - rightFloat; + else if (operator === '__py_multiplier') result = leftFloat * rightFloat; + else if (operator === '__py_divider') { + if (rightFloat === 0) { + // handleRuntimeError(context, new ZeroDivisionError(code, command, context)); + return { type: 'error', message: 'Division by zero' }; + } + result = leftFloat / rightFloat; + } + else if (operator === '__py_powerer') result = leftFloat ** rightFloat; + else if (operator === '__py_modder') result = pythonMod(leftFloat, rightFloat); + else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); + return { type: 'error', message: 'Unsupported float operation' }; + } + return { type: 'number', value: result }; + } + // Comparisons + else { + const compare_res = pyCompare(left, right); + if (operator === TokenType.GREATER) result = compare_res > 0; + else if (operator === TokenType.GREATEREQUAL) result = compare_res >= 0; + else if (operator === TokenType.LESS) result = compare_res < 0; + else if (operator === TokenType.LESSEQUAL) result = compare_res <= 0; + else if (operator === TokenType.DOUBLEEQUAL) result = compare_res === 0; + else if (operator === TokenType.NOTEQUAL) result = compare_res !== 0; + else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); + return { type: 'error', message: 'Unsupported float comparison' }; + } + return { type: 'bool', value: result }; + } + } + // Same type Integer Operations + else { + const leftBigInt = left.value as bigint; + const rightBigInt = right.value as bigint; + let result: bigint | boolean; + + if (operator === '__py_adder') { + return { type: 'bigint', value: leftBigInt + rightBigInt }; + } else if (operator === '__py_minuser') { + return { type: 'bigint', value: leftBigInt - rightBigInt }; + } else if (operator === '__py_multiplier') { + return { type: 'bigint', value: leftBigInt * rightBigInt }; + } else if (operator === '__py_divider') { + if (rightBigInt === 0n) { + // handleRunTimeError - ZeroDivisionError + return { type: 'error', message: 'Division by zero' } ; + } + return { type: 'number', value: Number(leftBigInt) / Number(rightBigInt) }; + } else if (operator === '__py_powerer') { + if (leftBigInt === 0n && rightBigInt < 0) { + // handleRunTimeError, zerodivision error + return { type: 'error', message: '0.0 cannot be raised to a negative pwoer'} + } + if (rightBigInt < 0) { + return {type: 'number', value: Number(leftBigInt) ** Number(rightBigInt) }; + } + return { type: 'bigint', value: leftBigInt ** rightBigInt }; + } else if (operator === '__py_modder') { + return { type: 'bigint', value: pythonMod(leftBigInt, rightBigInt) }; + } else if (operator === TokenType.GREATER) { + return { type: 'bool', value: leftBigInt > rightBigInt }; + } else if (operator === TokenType.GREATEREQUAL) { + return { type: 'bool', value: leftBigInt >= rightBigInt }; + } else if (operator === TokenType.LESS) { + return { type: 'bool', value: leftBigInt < rightBigInt }; + } else if (operator === TokenType.LESSEQUAL) { + return { type: 'bool', value: leftBigInt <= rightBigInt }; + } else if (operator === TokenType.DOUBLEEQUAL) { + return { type: 'bool', value: leftBigInt === rightBigInt }; + } else if (operator === TokenType.NOTEQUAL) { + return { type: 'bool', value: leftBigInt !== rightBigInt }; + } else { + // handleRuntimeError + return { type: 'error', message: 'Unsupported integer operation' }; + } + } + } +} \ No newline at end of file diff --git a/src/cse-machine/py_visitor.ts b/src/cse-machine/py_visitor.ts index 16b86aa..21c321c 100644 --- a/src/cse-machine/py_visitor.ts +++ b/src/cse-machine/py_visitor.ts @@ -4,12 +4,14 @@ import { Context } from "./context"; import { evaluateBinaryExpression, evaluateUnaryExpression } from "./py_operators"; import { TokenType } from "../tokens"; import { Token } from "../tokenizer"; +import { Value } from "./stash"; +import { PyComplexNumber } from "../types"; type Stmt = StmtNS.Stmt; type Expr = ExprNS.Expr; // TODO: type 'any' to be changed to node type for replacement of es.Node -export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { +export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { private code: string; private context: Context; @@ -55,11 +57,17 @@ export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { } // Expression Visitors - visitLiteralExpr(expr: ExprNS.Literal): any { - return { - type: typeof expr.value, - value: expr.value, - }; + visitLiteralExpr(expr: ExprNS.Literal): Value { + const value = expr.value; + if (typeof value == 'number') { + return { type: 'number', value: value }; + } else if (typeof value == 'boolean') { + return { type: 'bool', value: value}; + } else if (typeof value == 'string') { + return { type: 'string', value: value}; + } + // TODO to handle null, representing null as UndefinedValue + return { type: 'undefined'}; } visitUnaryExpr(expr: ExprNS.Unary): any { @@ -98,7 +106,12 @@ export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { visitMultiLambdaExpr(expr: ExprNS.MultiLambda): any { /* TODO */ } visitVariableExpr(expr: ExprNS.Variable): any { /* TODO */ } visitCallExpr(expr: ExprNS.Call): any { /* TODO */ } - visitComplexExpr(expr: ExprNS.Complex): any { /* TODO */ } + visitComplexExpr(expr: ExprNS.Complex): Value { + return { + type: 'complex', + value: new PyComplexNumber(expr.value.real, expr.value.imag) + }; + } visitNoneExpr(expr: ExprNS.None): any { /* TODO */ } // Statement Visitors diff --git a/src/cse-machine/stash.ts b/src/cse-machine/stash.ts index e28e496..e1ed38b 100644 --- a/src/cse-machine/stash.ts +++ b/src/cse-machine/stash.ts @@ -3,22 +3,23 @@ import { ExprNS, StmtNS } from '../ast-types'; import { Closure } from './closure'; import { Environment } from './environment'; import { Stack } from './stack'; +import { PyComplexNumber } from "../types"; /** * Value represents various runtime values in Python. */ -export type Value = any -// | NumberValue -// | BoolValue -// | StringValue -// | FunctionValue -// | LambdaValue -// | MultiLambdaValue -// | ErrorValue -// | UndefinedValue -// | string -// | BigIntValue -// | pyClosureValue; +export type Value = any + // | NumberValue + // | BoolValue + // | StringValue + // | ComplexValue + // | FunctionValue + // | LambdaValue + // | MultiLambdaValue + // | ErrorValue + // | UndefinedValue + // | BigIntValue + // | pyClosureValue; export interface pyClosureValue { type: "closure"; @@ -45,6 +46,11 @@ export interface StringValue { value: string; } +export interface ComplexValue { + type: 'complex'; + value: PyComplexNumber; +} + export interface FunctionValue { type: 'function'; name: string; diff --git a/src/index.ts b/src/index.ts index cdd87cd..d892651 100644 --- a/src/index.ts +++ b/src/index.ts @@ -225,4 +225,36 @@ export async function runPyAST( return result; }; -const {runnerPlugin, conduit} = initialise(PyEvaluator); \ No newline at end of file +// const {runnerPlugin, conduit} = initialise(PyEvaluator); +export * from "./errors"; +import * as fs from "fs"; + + +if (require.main === module) { + (async () => { + if (process.argv.length < 3) { + console.error("Usage: npm run start:dev -- "); + process.exit(1); + } + const options = {}; + const context = new Context(); + + const filePath = process.argv[2]; + + try { + //await loadModulesFromServer(context, "http://localhost:8022"); + + const code = fs.readFileSync(filePath, "utf8") + "\n"; + console.log(`Parsing Python file: ${filePath}`); + + const result = await runInContext(code, context, options); + console.info(result); + console.info((result as Finished).value); + console.info((result as Finished).representation.toString((result as Finished).value)); + + } catch (e) { + console.error("Error:", e); + } + + })(); +} diff --git a/tsconfig.json b/tsconfig.json index 9fd404c..1ceeca8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "lib": ["es6", "dom"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ From ed3529c4c10bb70d6cb80beeb185a656f32dee4c Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 12 Sep 2025 10:45:34 +0800 Subject: [PATCH 04/16] Extend visitors to implement expressions for evaluation (Refs #51)[4] * tokenizer.ts - bug fix for TokenType.NOT to prevent crash * py-visitor.ts - Added visitCompareExpr, visitBoolOpExpr, visitVariableExpr and visitNoneExpr * py-operators.ts - refined not oprator logic to handle truthiness - added specific errors for module and power operators with to handle lhs or rhs operand of 0 --- src/cse-machine/py_operators.ts | 74 +++++++++++++++++++++++---- src/cse-machine/py_visitor.ts | 91 +++++++++++++++++++++++++++++---- src/tokenizer.ts | 2 +- 3 files changed, 145 insertions(+), 22 deletions(-) diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts index 69842d3..64079b6 100644 --- a/src/cse-machine/py_operators.ts +++ b/src/cse-machine/py_operators.ts @@ -339,6 +339,50 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con // handleRuntimeError return {type: 'error', message: 'Unsupported complex operation'}; } return {type: 'complex', value: result}; + } + // bool and numeric operations + else if ((left.type === 'bool' && (right.type === 'number' || right.type === 'bigint' || right.type === 'bool')) || + (right.type === 'bool' && (left.type === 'number' || left.type === 'bigint' || left.type === 'bool'))) { + + const leftNum = left.type === 'bool' ? (left.value ? 1 : 0) : Number(left.value); + const rightNum = right.type === 'bool' ? (right.value ? 1 : 0) : Number(right.value); + let result: number | boolean; + + // Arithmetic + if (typeof operator === 'string') { + if (operator === '__py_adder') result = leftNum + rightNum; + else if (operator === '__py_minuser') result = leftNum - rightNum; + else if (operator === '__py_multiplier') result = leftNum * rightNum; + else if (operator === '__py_divider') { + if (rightNum === 0) { + // handleRuntimeError(context, new ZeroDivisionError(code, command, context)); + return { type: 'error', message: 'Division by zero' }; + } + result = leftNum / rightNum; + } + else if (operator === '__py_powerer') result = leftNum ** rightNum; + else if (operator === '__py_modder') result = pythonMod(leftNum, rightNum); + else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); + return { type: 'error', message: 'Unsupported boolean/numeric operation' }; + } + const resultType = (left.type === 'number' || right.type === 'number') ? 'number' : 'bigint'; + return { type: resultType, value: resultType === 'bigint' ? BigInt(result) : result }; + } + // Comparisons + else { + if (operator === TokenType.GREATER) result = leftNum > rightNum; + else if (operator === TokenType.GREATEREQUAL) result = leftNum >= rightNum; + else if (operator === TokenType.LESS) result = leftNum < rightNum; + else if (operator === TokenType.LESSEQUAL) result = leftNum <= rightNum; + else if (operator === TokenType.DOUBLEEQUAL) result = leftNum === rightNum; + else if (operator === TokenType.NOTEQUAL) result = leftNum !== rightNum; + else { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); + return { type: 'error', message: 'Unsupported boolean/numeric comparison' }; + } + return { type: 'bool', value: result }; + } } // Float and or Int Operations else if ((left.type === 'number' || left.type === 'bigint') && (right.type === 'number' || right.type === 'bigint')) { @@ -360,10 +404,15 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con result = leftFloat / rightFloat; } else if (operator === '__py_powerer') result = leftFloat ** rightFloat; - else if (operator === '__py_modder') result = pythonMod(leftFloat, rightFloat); - else { + else if (operator === '__py_modder') { + if (rightFloat === 0) { + // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); + return { type: 'error', message: 'Division by zero' }; + } + result = pythonMod(leftFloat, rightFloat); + } else { // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); - return { type: 'error', message: 'Unsupported float operation' }; + return { type: 'error', message: 'Unsupported float comparison' }; } return { type: 'number', value: result }; } @@ -381,10 +430,10 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con return { type: 'error', message: 'Unsupported float comparison' }; } return { type: 'bool', value: result }; - } + } } // Same type Integer Operations - else { + else if (left.type === 'bigint' && right.type ==='bigint') { const leftBigInt = left.value as bigint; const rightBigInt = right.value as bigint; let result: bigint | boolean; @@ -404,13 +453,17 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con } else if (operator === '__py_powerer') { if (leftBigInt === 0n && rightBigInt < 0) { // handleRunTimeError, zerodivision error - return { type: 'error', message: '0.0 cannot be raised to a negative pwoer'} + return { type: 'error', message: '0.0 cannot be raised to a negative power'} } if (rightBigInt < 0) { return {type: 'number', value: Number(leftBigInt) ** Number(rightBigInt) }; } return { type: 'bigint', value: leftBigInt ** rightBigInt }; } else if (operator === '__py_modder') { + if (rightBigInt === 0n) { + // handleRunTimeError - ZeroDivisionError + return { type: 'error', message: 'integer modulo by zero' } ; + } return { type: 'bigint', value: pythonMod(leftBigInt, rightBigInt) }; } else if (operator === TokenType.GREATER) { return { type: 'bool', value: leftBigInt > rightBigInt }; @@ -424,10 +477,9 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con return { type: 'bool', value: leftBigInt === rightBigInt }; } else if (operator === TokenType.NOTEQUAL) { return { type: 'bool', value: leftBigInt !== rightBigInt }; - } else { - // handleRuntimeError - return { type: 'error', message: 'Unsupported integer operation' }; + } + // handleRuntimeError + return { type: 'error', message: 'Unsupported operation' }; } } - } -} \ No newline at end of file +} diff --git a/src/cse-machine/py_visitor.ts b/src/cse-machine/py_visitor.ts index 21c321c..e2f98ea 100644 --- a/src/cse-machine/py_visitor.ts +++ b/src/cse-machine/py_visitor.ts @@ -70,12 +70,12 @@ export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { return { type: 'undefined'}; } - visitUnaryExpr(expr: ExprNS.Unary): any { + visitUnaryExpr(expr: ExprNS.Unary): Value { const argumentValue = this.visit(expr.right); return evaluateUnaryExpression(expr.operator.type, argumentValue, expr, this.context); } - visitBinaryExpr(expr: ExprNS.Binary): any { + visitBinaryExpr(expr: ExprNS.Binary): Value { const leftValue = this.visit(expr.left); const rightValue = this.visit(expr.right); const operatorForPyOperators = this.mapOperatorToPyOperator(expr.operator); @@ -90,7 +90,7 @@ export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { ) } - visitBigIntLiteralExpr(expr: ExprNS.BigIntLiteral): any { + visitBigIntLiteralExpr(expr: ExprNS.BigIntLiteral): Value { return { type: 'bigint', value: BigInt(expr.value) @@ -98,25 +98,96 @@ export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { } // Placeholder for TODO expr visitors - visitCompareExpr(expr: ExprNS.Compare): any { /* TODO */ } - visitBoolOpExpr(expr: ExprNS.BoolOp): any { /* TODO */ } - visitGroupingExpr(expr: ExprNS.Grouping): any { return this.visit(expr.expression);} + // To test on multiple comparisons, eg, a < b < c + visitCompareExpr(expr: ExprNS.Compare): Value { + const leftValue = this.visit(expr.left); + const rightValue = this.visit(expr.right); + const operatorToken = expr.operator; + + const operatorForEval = this.mapOperatorToPyOperator(operatorToken); + + return evaluateBinaryExpression( + this.code, + expr, + this.context, + operatorForEval, + leftValue, + rightValue, + ); + } + + visitBoolOpExpr(expr: ExprNS.BoolOp): Value { + const leftValue = this.visit(expr.left); + // Handle 'or' short-circuiting + if (expr.operator.type === TokenType.OR) { + let isTruthy = true; + if (leftValue.type === 'bool' && !leftValue.value) isTruthy = false; + if (leftValue.type === 'bigint' && leftValue.value === 0n) isTruthy = false; + if (leftValue.type === 'number' && leftValue.value === 0) isTruthy = false; + if (leftValue.type === 'string' && leftValue.value === '') isTruthy = false; + if (leftValue.type === 'undefined') isTruthy = false; + + if (isTruthy) { + return leftValue; + } else { + return this.visit(expr.right); + } + } + // Handle 'and' short-circuiting + if (expr.operator.type === TokenType.AND) { + let isFalsy = false; + if (leftValue.type === 'bool' && !leftValue.value) isFalsy = true; + if (leftValue.type === 'bigint' && leftValue.value === 0n) isFalsy = true; + if (leftValue.type === 'number' && leftValue.value === 0) isFalsy = true; + if (leftValue.type === 'string' && leftValue.value === '') isFalsy = true; + if (leftValue.type === 'undefined') isFalsy = true; + + if (isFalsy) { + return leftValue; + } else { + return this.visit(expr.right); + } + } + return { type: 'error', message: 'Unsupported boolean operator' }; + } + + visitGroupingExpr(expr: ExprNS.Grouping): any { + return this.visit(expr.expression); + } + visitTernaryExpr(expr: ExprNS.Ternary): any { /* TODO */ } visitLambdaExpr(expr: ExprNS.Lambda): any { /* TODO */ } visitMultiLambdaExpr(expr: ExprNS.MultiLambda): any { /* TODO */ } - visitVariableExpr(expr: ExprNS.Variable): any { /* TODO */ } + + visitVariableExpr(expr: ExprNS.Variable): Value { + const name = expr.name.lexeme; + if (name === 'True') { + return { type: 'bool', value: true }; + } else if (name === 'False') { + return { type: 'bool', value: false }; + } else if (name === 'None') { + return { type: 'undefined' }; + } + // TODO: add user defined variables, for now all variables are caught as error + return { type: 'error', message: `name '${name}' is not defined` }; + } + visitCallExpr(expr: ExprNS.Call): any { /* TODO */ } + visitComplexExpr(expr: ExprNS.Complex): Value { return { type: 'complex', value: new PyComplexNumber(expr.value.real, expr.value.imag) }; } - visitNoneExpr(expr: ExprNS.None): any { /* TODO */ } + + visitNoneExpr(expr: ExprNS.None): Value { + return { type: 'undefined' }; + } // Statement Visitors - visitFileInputStmt(stmt: StmtNS.FileInput): any { - let lastValue: any; + visitFileInputStmt(stmt: StmtNS.FileInput): Value { + let lastValue: Value = { type: 'undefined' }; for (const statement of stmt.statements) { lastValue = this.visit(statement); } diff --git a/src/tokenizer.ts b/src/tokenizer.ts index 6600dba..8b14183 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -465,7 +465,7 @@ export class Tokenizer { const previousToken = this.tokens[this.tokens.length - 1]; switch (specialIdent) { case TokenType.NOT: - if (previousToken.type === TokenType.IS) { + if (previousToken && previousToken.type === TokenType.IS) { this.overwriteToken(TokenType.ISNOT); } else { this.addToken(specialIdent); From 75e754821b531e0a09abfd44f652e1fdf67ca98c Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 12 Sep 2025 17:03:13 +0800 Subject: [PATCH 05/16] Edited runner path from index.ts to Conductor (Refs #51)[5] * super small commit haha * conductor/runner/types/PyEvaluator.ts - Updated with PyRunInContext, no change to the rest * index.ts - Introduced PyRunInContext that is replacement for runInContext * a small reminder: frigging initialise the PyEvaluator (UNCOMMENT) for local testingssssss --- src/conductor/runner/types/PyEvaluator.ts | 9 +++++---- src/index.ts | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/conductor/runner/types/PyEvaluator.ts b/src/conductor/runner/types/PyEvaluator.ts index 2c08d6a..5642cc6 100644 --- a/src/conductor/runner/types/PyEvaluator.ts +++ b/src/conductor/runner/types/PyEvaluator.ts @@ -2,12 +2,11 @@ // https://github.com/source-academy/conductor // Original author(s): Source Academy Team -import { runInContext } from "../../../"; +import { PyRunInContext } from "../../../"; import { Context } from "../../../cse-machine/context"; import { BasicEvaluator } from "../BasicEvaluator"; import { IRunnerPlugin } from "./IRunnerPlugin"; import { IOptions } from "../../../"; -import { Finished } from "../../../types"; const defaultContext = new Context(); const defaultOptions: IOptions = { @@ -28,12 +27,14 @@ export class PyEvaluator extends BasicEvaluator { async evaluateChunk(chunk: string): Promise { try { - const result = await runInContext( + const result = await PyRunInContext( chunk, // Code this.context, this.options ); - this.conductor.sendOutput(`${(result as Finished).representation.toString((result as Finished).value)}`); + if ('status' in result && result.status === 'finished') { + this.conductor.sendOutput(`${result.representation.toString(result.value)}`); + } } catch (error) { this.conductor.sendOutput(`Error: ${error instanceof Error ? error.message : error}`); } diff --git a/src/index.ts b/src/index.ts index d892651..0eec5f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -215,21 +215,28 @@ export async function runPyAST( code: string, context: Context, options: RecursivePartial = {} -): Promise { +): Promise { const script = code + "\n"; const tokenizer = new Tokenizer(script); const tokens = tokenizer.scanEverything(); const pyParser = new Parser(script, tokens); const ast = pyParser.parse(); + return ast; +}; + +export async function PyRunInContext( + code: string, + context: Context, + options: RecursivePartial = {} +): Promise { + const ast = await runPyAST(code, context, options); const result = PyRunCSEMachine(code, ast, context, options); return result; -}; +} -// const {runnerPlugin, conduit} = initialise(PyEvaluator); export * from "./errors"; import * as fs from "fs"; - if (require.main === module) { (async () => { if (process.argv.length < 3) { @@ -258,3 +265,4 @@ if (require.main === module) { })(); } +// const {runnerPlugin, conduit} = initialise(PyEvaluator); From 7b311232a6a5b732e4f931ee7ce4eff81680dea3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 15 Sep 2025 18:07:42 +0800 Subject: [PATCH 06/16] Major change to interpreter, context, control, instrCreator (Refs #51)[6] * New Files: py_context.ts, py_control.ts, py_instrCreator.ts, py_types.ts Note: No logic change, only adaptation of pynodes and instr * index.ts, pyrunner - Adjusted execution path and adaptaion of pycontext, pycontrol * py_interpreter - Overhauled to support control and instr * changes 1 --- src/cse-machine/py_context.ts | 169 +++++++++++++++++++++++++++ src/cse-machine/py_control.ts | 54 +++++++++ src/cse-machine/py_instrCreator.ts | 66 +++++++++++ src/cse-machine/py_interpreter.ts | 177 ++++++++++++++++++++++++++--- src/cse-machine/py_operators.ts | 6 +- src/cse-machine/py_types.ts | 103 +++++++++++++++++ src/index.ts | 5 +- src/runner/pyRunner.ts | 10 +- src/types.ts | 6 +- 9 files changed, 568 insertions(+), 28 deletions(-) create mode 100644 src/cse-machine/py_context.ts create mode 100644 src/cse-machine/py_control.ts create mode 100644 src/cse-machine/py_instrCreator.ts create mode 100644 src/cse-machine/py_types.ts diff --git a/src/cse-machine/py_context.ts b/src/cse-machine/py_context.ts new file mode 100644 index 0000000..4841ec4 --- /dev/null +++ b/src/cse-machine/py_context.ts @@ -0,0 +1,169 @@ +import { Stash, Value } from './stash'; +import { PyControl, PyControlItem } from './py_control'; +import { createSimpleEnvironment, createProgramEnvironment, Environment } from './environment'; +import { CseError } from './error'; +import { Heap } from './heap'; +import { PyNode } from './py_types'; +import { NativeStorage } from '../types'; +import { StmtNS } from '../ast-types'; + +export class PyContext { + public control: PyControl; + public stash: Stash; + //public environment: Environment; + public errors: CseError[] = []; + + runtime: { + break: boolean + debuggerOn: boolean + isRunning: boolean + environmentTree: EnvTree + environments: Environment[] + nodes: PyNode[] + control: PyControl | null + stash: Stash | null + objectCount: number + envStepsTotal: number + breakpointSteps: number[] + changepointSteps: number[] + } + + /** + * Used for storing the native context and other values + */ + nativeStorage: NativeStorage + + constructor(program?: StmtNS.Stmt, context?: PyContext) { + this.control = new PyControl(program); + this.stash = new Stash(); + this.runtime = this.createEmptyRuntime(); + //this.environment = createProgramEnvironment(context || this, false); + if (this.runtime.environments.length === 0) { + const globalEnvironment = this.createGlobalEnvironment() + this.runtime.environments.push(globalEnvironment) + this.runtime.environmentTree.insert(globalEnvironment) + } + this.nativeStorage = { + builtins: new Map(), + previousProgramsIdentifiers: new Set(), + operators: new Map Value>(), + maxExecTime: 1000, + //evaller: null, + loadedModules: {}, + loadedModuleTypes: {} + } + } + + createGlobalEnvironment = (): Environment => ({ + tail: null, + name: 'global', + head: {}, + heap: new Heap(), + id: '-1' + }) + + createEmptyRuntime = () => ({ + break: false, + debuggerOn: true, + isRunning: false, + environmentTree: new EnvTree(), + environments: [], + value: undefined, + nodes: [], + control: null, + stash: null, + objectCount: 0, + envSteps: -1, + envStepsTotal: 0, + breakpointSteps: [], + changepointSteps: [] + }) + + public reset(program?: StmtNS.Stmt): void { + this.control = new PyControl(program); + this.stash = new Stash(); + //this.environment = createProgramEnvironment(this, false); + this.errors = []; + } + + public copy(): PyContext { + const newContext = new PyContext(); + newContext.control = this.control.copy(); + newContext.stash = this.stash.copy(); + //newContext.environments = this.copyEnvironment(this.environments); + return newContext; + } + + private copyEnvironment(env: Environment): Environment { + const newTail = env.tail ? this.copyEnvironment(env.tail) : null; + const newEnv: Environment = { + id: env.id, + name: env.name, + tail: newTail, + head: { ...env.head }, + heap: new Heap(), + callExpression: env.callExpression, + thisContext: env.thisContext + }; + return newEnv; + } +} + +export class EnvTree { + private _root: EnvTreeNode | null = null + private map = new Map() + + get root(): EnvTreeNode | null { + return this._root + } + + public insert(environment: Environment): void { + const tailEnvironment = environment.tail + if (tailEnvironment === null) { + if (this._root === null) { + this._root = new EnvTreeNode(environment, null) + this.map.set(environment, this._root) + } + } else { + const parentNode = this.map.get(tailEnvironment) + if (parentNode) { + const childNode = new EnvTreeNode(environment, parentNode) + parentNode.addChild(childNode) + this.map.set(environment, childNode) + } + } + } + + public getTreeNode(environment: Environment): EnvTreeNode | undefined { + return this.map.get(environment) + } +} + +export class EnvTreeNode { + private _children: EnvTreeNode[] = [] + + constructor(readonly environment: Environment, public parent: EnvTreeNode | null) {} + + get children(): EnvTreeNode[] { + return this._children + } + + public resetChildren(newChildren: EnvTreeNode[]): void { + this.clearChildren() + this.addChildren(newChildren) + newChildren.forEach(c => (c.parent = this)) + } + + private clearChildren(): void { + this._children = [] + } + + private addChildren(newChildren: EnvTreeNode[]): void { + this._children.push(...newChildren) + } + + public addChild(newChild: EnvTreeNode): EnvTreeNode { + this._children.push(newChild) + return newChild + } +} diff --git a/src/cse-machine/py_control.ts b/src/cse-machine/py_control.ts new file mode 100644 index 0000000..0e86385 --- /dev/null +++ b/src/cse-machine/py_control.ts @@ -0,0 +1,54 @@ +import { Stack } from "./stack"; +import { PyNode, Instr } from "./py_types"; +import { StmtNS } from "../ast-types"; +import { isEnvDependent } from './utils'; // TODO + +export type PyControlItem = (PyNode | Instr) & { + isEnvDependent?: boolean; + skipEnv?: boolean; +}; + + +export class PyControl extends Stack { + private numEnvDependentItems: number; + public constructor(program?: StmtNS.Stmt) { + super() + this.numEnvDependentItems = 0 + // Load program into control stack + program ? this.push(program) : null + } + + public canAvoidEnvInstr(): boolean { + return this.numEnvDependentItems === 0 + } + + // For testing purposes + public getNumEnvDependentItems(): number { + return this.numEnvDependentItems + } + +// TODO in the future +// public pop(): PyControlItem | undefined { +// const item = super.pop(); +// if (item !== undefined && isEnvDependent(item)) { +// this.numEnvDependentItems--; +// } +// return item; +// } +// public push(...items: PyControlItem[]): void { +// items.forEach((item: PyControlItem) => { +// // We keep this logic for future use with the stepper. +// if (isEnvDependent(item)) { +// this.numEnvDependentItems++; +// } +// }); +// super.push(...items); +// } + + public copy(): PyControl { + const newControl = new PyControl(); + const stackCopy = super.getStack(); + newControl.push(...stackCopy); + return newControl; + } +} \ No newline at end of file diff --git a/src/cse-machine/py_instrCreator.ts b/src/cse-machine/py_instrCreator.ts new file mode 100644 index 0000000..94d5bde --- /dev/null +++ b/src/cse-machine/py_instrCreator.ts @@ -0,0 +1,66 @@ +import { Environment } from "./environment"; +import { AppInstr, AssmtInstr, BinOpInstr, BranchInstr, EnvInstr, Instr, InstrType, PyNode, UnOpInstr } from "./py_types"; +import { TokenType } from "../tokens"; + +export const popInstr = (srcNode: PyNode): Instr => ({ + instrType: InstrType.POP, + srcNode +}) + +export const assmtInstr = ( + symbol: string, + constant: boolean, + declaration: boolean, + srcNode: PyNode +): AssmtInstr => ({ + instrType: InstrType.ASSIGNMENT, + symbol, + constant, + declaration, + srcNode +}) + +export const appInstr = (numOfArgs: number, srcNode: PyNode): AppInstr => ({ + instrType: InstrType.APPLICATION, + numOfArgs, + srcNode +}) + +export const envInstr = (env: Environment, srcNode: PyNode): EnvInstr => ({ + instrType: InstrType.ENVIRONMENT, + env, + srcNode +}) + +export const markerInstr = (srcNode: PyNode): Instr => ({ + instrType: InstrType.MARKER, + srcNode +}) + +export const binOpInstr = (symbol: any, srcNode: PyNode): BinOpInstr => ({ + instrType: InstrType.BINARY_OP, + symbol, + srcNode +}) + +export const resetInstr = (srcNode: PyNode): Instr => ({ + instrType: InstrType.RESET, + srcNode +}) + +export const branchInstr = ( + consequent: PyNode, + alternate: PyNode | null | undefined, + srcNode: PyNode +): BranchInstr => ({ + instrType: InstrType.BRANCH, + consequent, + alternate, + srcNode +}) + +export const unOpInstr = (symbol: TokenType, srcNode: PyNode): UnOpInstr => ({ + instrType: InstrType.UNARY_OP, + symbol, + srcNode +}) diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index db58053..0f533a2 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -1,30 +1,173 @@ -import { Context } from "./context"; -import { CSEBreak, Representation, Result, Finished } from "../types"; -import { StmtNS } from "../ast-types"; -import { Value, ErrorValue } from "./stash"; -import { PyVisitor } from "./py_visitor"; -import { toPythonString } from "../stdlib"; +import { StmtNS, ExprNS } from '../ast-types'; +import { PyContext } from './py_context'; +import { PyControl, PyControlItem } from './py_control'; +import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr } from './py_types'; +import { Stash, Value, ErrorValue } from './stash'; +import { IOptions } from '..'; +import * as instr from './py_instrCreator'; +import { evaluateUnaryExpression, evaluateBinaryExpression } from './py_operators'; +import { TokenType } from '../tokens'; +import { Token } from '../tokenizer'; +import { Result, Finished, CSEBreak, Representation} from '../types'; +import { toPythonString } from '../stdlib' -type Stmt = StmtNS.Stmt; +type CmdEvaluator = ( + command: PyControlItem, + context: PyContext, + control: PyControl, + stash: Stash, + isPrelude: boolean +) => void -export function PyCSEResultPromise(context: Context, value: Value): Promise { +export function PyCSEResultPromise(context: PyContext, value: Value): Promise { return new Promise((resolve, reject) => { if (value instanceof CSEBreak) { resolve({ status: 'suspended-cse-eval', context }); - } else if (value && value.type === 'error') { + } else if (value && (value as any).type === 'error') { const errorValue = value as ErrorValue; const representation = new Representation(errorValue.message); - resolve({ status: 'finished', context, value, representation } as Finished); + resolve({ status: 'finished', context, value, representation }); } else { const representation = new Representation(toPythonString(value)); - resolve({ status: 'finished', context, value, representation } as Finished); + resolve({ status: 'finished', context, value, representation }); } }); } -export function PyEvaluate(code: string, program: Stmt, context: Context): Promise { - // dummy for now, just to test getting AST from parser - const visitor = new PyVisitor(code, context); - const result = visitor.visit(program); - return PyCSEResultPromise(context, result); -} \ No newline at end of file +function mapOperatorToPyOperator(operatorToken: Token): TokenType | string { + switch (operatorToken.type) { + case TokenType.PLUS: return '__py_adder'; + case TokenType.MINUS: return '__py_minuser'; + case TokenType.STAR: return '__py_multiplier'; + case TokenType.SLASH: return '__py_divider'; + case TokenType.PERCENT: return '__py_modder'; + case TokenType.DOUBLESTAR: return '__py_powerer'; + // Add other arithmetic operators as needed + default: return operatorToken.type; // For comparison and unary operators + } +} + +export function PyEvaluate(code: string, program: StmtNS.Stmt, context: PyContext, options: IOptions): Value { + context.control = new PyControl(program); + context.runtime.isRunning = true; + + const result = pyRunCSEMachine(code, context, context.control, context.stash, options.isPrelude || false); + + context.runtime.isRunning = false; + return result; +} + +function pyRunCSEMachine(code: string, context: PyContext, control: PyControl, stash: Stash, isPrelude: boolean): Value { + let command = control.peek(); + + while (command) { + control.pop(); + + if ('instrType' in command) { + const instr = command as Instr; + if (pyCmdEvaluators[instr.instrType]) { + pyCmdEvaluators[instr.instrType](instr, context, control, stash, isPrelude); + } else { + throw new Error(`Unknown instruction type: ${instr.instrType}`); + } + } else { + const node = command as PyNode; + const nodeType = node.constructor.name; + if (pyCmdEvaluators[nodeType]) { + pyCmdEvaluators[nodeType](node, context, control, stash, isPrelude); + } else { + throw new Error(`Unknown Python AST node type: ${nodeType}`); + } + } + + command = control.peek(); + } + + const result = stash.peek(); + return result !== undefined ? result : { type: 'undefined' }; +} + +const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { + /** + * AST Node Handlers + */ + + 'FileInput': (command, context, control) => { + const fileInput = command as StmtNS.FileInput; + const statements = fileInput.statements.slice().reverse(); + control.push(...statements); + }, + + 'SimpleExpr': (command, context, control) => { + const simpleExpr = command as StmtNS.SimpleExpr; + control.push(simpleExpr.expression); + }, + + 'Literal': (command, context, control, stash) => { + const literal = command as ExprNS.Literal; + if (typeof literal.value === 'number') { + stash.push({ type: 'number', value: literal.value }); + } else if (typeof literal.value === 'boolean') { + stash.push({ type: 'bool', value: literal.value }); + } else if (typeof literal.value === 'string') { + stash.push({ type: 'string', value: literal.value }); + } else { + stash.push({ type: 'undefined' }); // For null + } + }, + + 'BigIntLiteral': (command, context, control, stash) => { + const literal = command as ExprNS.BigIntLiteral; + stash.push({ type: 'bigint', value: BigInt(literal.value) }); + }, + + 'Unary': (command, context, control) => { + const unary = command as ExprNS.Unary; + const op_instr = instr.unOpInstr(unary.operator.type, unary); + control.push(op_instr); + control.push(unary.right); + }, + + 'Binary': (command, context, control) => { + const binary = command as ExprNS.Binary; + const opStr = mapOperatorToPyOperator(binary.operator); + const op_instr = instr.binOpInstr(opStr, binary); + control.push(op_instr); + control.push(binary.right); + control.push(binary.left); + }, + + /** + * Instruction Handlers + */ + [InstrType.UNARY_OP]: function (command: PyControlItem, context: PyContext, control: PyControl, stash: Stash, isPrelude: boolean) { + const instr = command as UnOpInstr; + const argument = stash.pop(); + if (argument) { + const result = evaluateUnaryExpression( + instr.symbol, + argument, + instr.srcNode as ExprNS.Expr, + context + ); + stash.push(result); + } + }, + + [InstrType.BINARY_OP]: function (command: PyControlItem, context: PyContext, control: PyControl, stash: Stash, isPrelude: boolean) { + const instr = command as BinOpInstr; + const right = stash.pop(); + const left = stash.pop(); + if (left && right) { + const result = evaluateBinaryExpression( + "", // source string code + instr.srcNode as ExprNS.Expr, + context, + instr.symbol, + left, + right + ); + stash.push(result); + } + }, +}; \ No newline at end of file diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts index 64079b6..918aa30 100644 --- a/src/cse-machine/py_operators.ts +++ b/src/cse-machine/py_operators.ts @@ -1,5 +1,5 @@ import { Value } from "./stash"; -import { Context } from "./context"; +import { PyContext } from "./py_context"; import { PyComplexNumber } from "../types"; import { TypeConcatenateError, UnsupportedOperandTypeError, ZeroDivisionError } from "../errors/errors"; import { ExprNS } from "../ast-types"; @@ -215,7 +215,7 @@ function approximateBigIntString(num: number, precision: number): string { // Changed operator from es.UnaryOperator to TokenType // Added parameters command and context for error handling // updated logics for TokenType.NOT [3] -export function evaluateUnaryExpression(operator: TokenType, value: any, command: ExprNS.Expr, context: Context): Value { +export function evaluateUnaryExpression(operator: TokenType, value: Value, command: ExprNS.Expr, context: PyContext): Value { if (operator === TokenType.NOT) { let isFalsy: boolean; switch (value.type) { @@ -271,7 +271,7 @@ export function evaluateUnaryExpression(operator: TokenType, value: any, command } // Change command from "ControlItem" to "ExprNS.Expr", "identifier" to "operator" for type safety -export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: Context, operator: TokenType | string, left: Value, right: Value): Value { +export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: PyContext, operator: TokenType | string, left: Value, right: Value): Value { const operand = operandTranslator(operator); const originalLeftType = typeTranslator(left.type); diff --git a/src/cse-machine/py_types.ts b/src/cse-machine/py_types.ts new file mode 100644 index 0000000..069199b --- /dev/null +++ b/src/cse-machine/py_types.ts @@ -0,0 +1,103 @@ +import { Environment } from './environment'; +import { StmtNS, ExprNS } from '../ast-types'; +import { TokenType } from '../tokens'; + +export type PyNode = StmtNS.Stmt | ExprNS.Expr; + +export enum InstrType { + RESET = 'Reset', + WHILE = 'While', + FOR = 'For', + ASSIGNMENT = 'Assignment', + ANN_ASSIGNMENT = 'AnnAssignment', + APPLICATION = 'Application', + UNARY_OP = 'UnaryOperation', + BINARY_OP = 'BinaryOperation', + BOOL_OP = 'BoolOperation', + COMPARE = 'Compare', + CALL = 'Call', + RETURN = 'Return', + BREAK = 'Break', + CONTINUE = 'Continue', + IF = 'If', + FUNCTION_DEF = 'FunctionDef', + LAMBDA = 'Lambda', + MULTI_LAMBDA = 'MultiLambda', + GROUPING = 'Grouping', + LITERAL = 'Literal', + VARIABLE = 'Variable', + TERNARY = 'Ternary', + PASS = 'Pass', + ASSERT = 'Assert', + IMPORT = 'Import', + GLOBAL = 'Global', + NONLOCAL = 'NonLocal', + Program = 'Program', + BRANCH = 'Branch', + POP = 'Pop', + ENVIRONMENT = 'environment', + MARKER = 'marker', +} + +interface BaseInstr { + instrType: InstrType + srcNode: PyNode + isEnvDependent?: boolean +} + +export interface WhileInstr extends BaseInstr { + test: PyNode + body: PyNode +} + +// TODO: more strict type in the future +export interface ForInstr extends BaseInstr { + init: PyNode + test: PyNode + update: PyNode + body: PyNode +} + +export interface AssmtInstr extends BaseInstr { + symbol: string + constant: boolean + declaration: boolean +} + +export interface UnOpInstr extends BaseInstr { + symbol: TokenType +} + +export interface BinOpInstr extends BaseInstr { + symbol: string | TokenType +} + +export interface AppInstr extends BaseInstr { + numOfArgs: number + srcNode: PyNode +} + +export interface BranchInstr extends BaseInstr { + consequent: PyNode + alternate: PyNode | null | undefined +} + +export interface EnvInstr extends BaseInstr { + env: Environment +} + +export interface ArrLitInstr extends BaseInstr { + arity: number +} + +export type Instr = + | BaseInstr + | WhileInstr + | ForInstr + | AssmtInstr + | AppInstr + | BranchInstr + | EnvInstr + | ArrLitInstr + | UnOpInstr + | BinOpInstr \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 0eec5f6..c262863 100644 --- a/src/index.ts +++ b/src/index.ts @@ -143,6 +143,7 @@ import { PyEvaluator } from "./conductor/runner/types/PyEvaluator"; export * from './errors'; import { PyRunCSEMachine } from "./runner/pyRunner"; import { StmtNS } from "./ast-types"; +import { PyContext } from "./cse-machine/py_context"; type Stmt = StmtNS.Stmt; @@ -213,7 +214,7 @@ export async function runInContext( export async function runPyAST( code: string, - context: Context, + context: PyContext, options: RecursivePartial = {} ): Promise { const script = code + "\n"; @@ -226,7 +227,7 @@ export async function runPyAST( export async function PyRunInContext( code: string, - context: Context, + context: PyContext, options: RecursivePartial = {} ): Promise { const ast = await runPyAST(code, context, options); diff --git a/src/runner/pyRunner.ts b/src/runner/pyRunner.ts index 0210b64..208eeb7 100644 --- a/src/runner/pyRunner.ts +++ b/src/runner/pyRunner.ts @@ -1,10 +1,11 @@ import { IOptions } from ".." -import { Context } from "../cse-machine/context" import { CSEResultPromise, evaluate } from "../cse-machine/interpreter" import { RecursivePartial, Result } from "../types" import * as es from 'estree' -import { PyEvaluate } from "../cse-machine/py_interpreter" +import { PyEvaluate, PyCSEResultPromise } from "../cse-machine/py_interpreter" +import { Context } from "../cse-machine/context" import { StmtNS } from "../ast-types"; +import { PyContext } from "../cse-machine/py_context" export function runCSEMachine(code: string, program: es.Program, context: Context, options: RecursivePartial = {}): Promise { const result = evaluate(code, program, context, options); @@ -13,6 +14,7 @@ export function runCSEMachine(code: string, program: es.Program, context: Contex type Stmt = StmtNS.Stmt; -export function PyRunCSEMachine(code: string, program: Stmt, context: Context, options: RecursivePartial = {}): Promise { - return PyEvaluate(code, program, context); +export function PyRunCSEMachine(code: string, program: Stmt, context: PyContext, options: RecursivePartial = {}): Promise< Result> { + const value = PyEvaluate(code, program, context, options as IOptions); + return PyCSEResultPromise(context, value); } diff --git a/src/types.ts b/src/types.ts index cf63963..7c95284 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,8 @@ import { toPythonString } from './stdlib' import { Value } from './cse-machine/stash' import { Context } from './cse-machine/context' import { ModuleFunctions } from './modules/moduleTypes' +import { PyContext } from './cse-machine/py_context' +import { PyControl } from './cse-machine/py_control' export class CSEBreak {} @@ -281,12 +283,12 @@ export type Result = Finished | Error | SuspendedCseEval // | Suspended export interface SuspendedCseEval { status: 'suspended-cse-eval' - context: Context + context: Context | PyContext } export interface Finished { status: 'finished' - context: Context + context: Context | PyContext value: Value representation: Representation // if the returned value needs a unique representation, // (for example if the language used is not JS), From 63a35ed41d9ffc4d5c6d4bca6433c6e165482428 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 15 Sep 2025 18:35:03 +0800 Subject: [PATCH 07/16] Added BoolOp, None Variable (Refs #51)[7] * py_types -added BoolOpInstr * py_instrcreator - added boolOpInstr * py_interpreter - added handler for boolop, none, variable, --- src/cse-machine/py_instrCreator.ts | 8 +++- src/cse-machine/py_interpreter.ts | 74 +++++++++++++++++++++++++++++- src/cse-machine/py_types.ts | 7 ++- 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/cse-machine/py_instrCreator.ts b/src/cse-machine/py_instrCreator.ts index 94d5bde..1e711b4 100644 --- a/src/cse-machine/py_instrCreator.ts +++ b/src/cse-machine/py_instrCreator.ts @@ -1,5 +1,5 @@ import { Environment } from "./environment"; -import { AppInstr, AssmtInstr, BinOpInstr, BranchInstr, EnvInstr, Instr, InstrType, PyNode, UnOpInstr } from "./py_types"; +import { AppInstr, AssmtInstr, BinOpInstr, BranchInstr, EnvInstr, Instr, InstrType, PyNode, UnOpInstr, BoolOpInstr } from "./py_types"; import { TokenType } from "../tokens"; export const popInstr = (srcNode: PyNode): Instr => ({ @@ -64,3 +64,9 @@ export const unOpInstr = (symbol: TokenType, srcNode: PyNode): UnOpInstr => ({ symbol, srcNode }) + +export const boolOpInstr = (symbol: TokenType, srcNode: PyNode): BoolOpInstr => ({ + instrType: InstrType.BOOL_OP, + symbol, + srcNode +}); \ No newline at end of file diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index 0f533a2..47babc2 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -1,7 +1,7 @@ import { StmtNS, ExprNS } from '../ast-types'; import { PyContext } from './py_context'; import { PyControl, PyControlItem } from './py_control'; -import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr } from './py_types'; +import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr } from './py_types'; import { Stash, Value, ErrorValue } from './stash'; import { IOptions } from '..'; import * as instr from './py_instrCreator'; @@ -137,6 +137,32 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(binary.left); }, + 'BoolOp': (command, context, control, stash, isPrelude) => { + const boolOp = command as ExprNS.BoolOp; + control.push(instr.boolOpInstr(boolOp.operator.type, boolOp)); + control.push(boolOp.right); + control.push(boolOp.left); + }, + + 'None': (command, context, control, stash, isPrelude) => { + stash.push({ type: 'undefined' }); + }, + + 'Variable': (command, context, control, stash, isPrelude) => { + const variable = command as ExprNS.Variable; + const name = variable.name.lexeme; + // For now, we only handle built in constants. + // In a future commit, we will look up variables in the environment. + if (name === 'True') { + stash.push({ type: 'bool', value: true }); + } else if (name === 'False') { + stash.push({ type: 'bool', value: false }); + } else { + // Throw an error for undefined variables for now + throw new Error(`NameError: name '${name}' is not defined`); + } + }, + /** * Instruction Handlers */ @@ -170,4 +196,50 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { stash.push(result); } }, + + [InstrType.BOOL_OP]: function (command: PyControlItem, context: PyContext, control: PyControl, stash: Stash, isPrelude: + boolean) { + const instr = command as BoolOpInstr; + const rightValue = stash.pop(); + const leftValue = stash.pop(); + + if (!leftValue || !rightValue) { + throw new Error("RuntimeError: Boolean operation requires two operands."); + } + + // Implement Python's short-circuiting logic + if (instr.symbol === TokenType.OR) { + // If left is truthy, return left. Otherwise, return right. + let isLeftTruthy = false; + if (leftValue.type === 'bool') isLeftTruthy = leftValue.value; + else if (leftValue.type === 'bigint') isLeftTruthy = leftValue.value !== 0n; + else if (leftValue.type === 'number') isLeftTruthy = leftValue.value !== 0; + else if (leftValue.type === 'string') isLeftTruthy = leftValue.value !== ''; + else if (leftValue.type === 'undefined') isLeftTruthy = false; + else isLeftTruthy = true; // Other types are generally truthy + + if (isLeftTruthy) { + stash.push(leftValue); + } else { + stash.push(rightValue); + } + } else if (instr.symbol === TokenType.AND) { + // If left is falsy, return left. Otherwise, return right. + let isLeftFalsy = false; + if (leftValue.type === 'bool') isLeftFalsy = !leftValue.value; + else if (leftValue.type === 'bigint') isLeftFalsy = leftValue.value === 0n; + else if (leftValue.type === 'number') isLeftFalsy = leftValue.value === 0; + else if (leftValue.type === 'string') isLeftFalsy = leftValue.value === ''; + else if (leftValue.type === 'undefined') isLeftFalsy = true; + else isLeftFalsy = false; // Other types are generally truthy + + if (isLeftFalsy) { + stash.push(leftValue); + } else { + stash.push(rightValue); + } + } else { + throw new Error(`Unsupported boolean operator: ${instr.symbol}`); + } + }, }; \ No newline at end of file diff --git a/src/cse-machine/py_types.ts b/src/cse-machine/py_types.ts index 069199b..b67385d 100644 --- a/src/cse-machine/py_types.ts +++ b/src/cse-machine/py_types.ts @@ -72,6 +72,10 @@ export interface BinOpInstr extends BaseInstr { symbol: string | TokenType } +export interface BoolOpInstr extends BaseInstr { + symbol: TokenType; +} + export interface AppInstr extends BaseInstr { numOfArgs: number srcNode: PyNode @@ -100,4 +104,5 @@ export type Instr = | EnvInstr | ArrLitInstr | UnOpInstr - | BinOpInstr \ No newline at end of file + | BinOpInstr + | BoolOpInstr \ No newline at end of file From 6bd1db6f5e9de465954ec2608ab5abd39a7cc8c0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 16 Sep 2025 10:25:11 +0800 Subject: [PATCH 08/16] gdded variable assignment and compare (Refs #51)[8] * tsconfig.json: added ignoreDeprecations to for backward compatibility with moduleresolution * new files: py_environment * Introduced for simple handling of variable declarations TODO: undeclared variable error --- src/cse-machine/py_context.ts | 2 +- src/cse-machine/py_environment.ts | 119 ++++++++++++++++++++++++++++++ src/cse-machine/py_interpreter.ts | 48 ++++++++++-- src/cse-machine/py_operators.ts | 2 +- src/cse-machine/py_utils.ts | 61 ++++++++++----- src/errors/errors.ts | 3 +- tsconfig.json | 1 + 7 files changed, 207 insertions(+), 29 deletions(-) create mode 100644 src/cse-machine/py_environment.ts diff --git a/src/cse-machine/py_context.ts b/src/cse-machine/py_context.ts index 4841ec4..bc6fae9 100644 --- a/src/cse-machine/py_context.ts +++ b/src/cse-machine/py_context.ts @@ -1,6 +1,6 @@ import { Stash, Value } from './stash'; import { PyControl, PyControlItem } from './py_control'; -import { createSimpleEnvironment, createProgramEnvironment, Environment } from './environment'; +import { createSimpleEnvironment, createProgramEnvironment, Environment } from './py_environment'; import { CseError } from './error'; import { Heap } from './heap'; import { PyNode } from './py_types'; diff --git a/src/cse-machine/py_environment.ts b/src/cse-machine/py_environment.ts new file mode 100644 index 0000000..b691c29 --- /dev/null +++ b/src/cse-machine/py_environment.ts @@ -0,0 +1,119 @@ +import { Value } from './stash'; +import { Heap } from './heap'; +// import { Closure } from './closure'; +import { PyContext } from './py_context'; +import { PyNode } from './py_types'; +import { ExprNS } from '../ast-types'; + + +export interface Frame { + [name: string]: any +} + +export interface Environment { + readonly id: string + name: string + tail: Environment | null + callExpression?: ExprNS.Call; + head: Frame + heap: Heap + thisContext?: Value +} + +export const uniqueId = (context: PyContext): string => { + return `${context.runtime.objectCount++}` +} + +// export const createEnvironment = ( +// context: PyContext, +// closure: Closure, +// args: Value[], +// callExpression: ExprNS.Call +// ): Environment => { +// const environment: Environment = { +// // TODO: name +// name: '', +// tail: closure.environment, +// head: {}, +// heap: new Heap(), +// id: uniqueId(context), +// callExpression: { +// ...callExpression, +// //arguments: args.map(ast.primitive) +// } +// } + +// // console.info('closure.node.params:', closure.node.params); +// // console.info('Number of params:', closure.node.params.length); + +// closure.node.params.forEach((param, index) => { +// if (isRestElement(param)) { +// const array = args.slice(index) +// handleArrayCreation(context, array, environment) +// environment.head[(param.argument as es.Identifier).name] = array +// } else { +// environment.head[(param as es.Identifier).name] = args[index] +// } +// }) +// return environment +// } + +export const createSimpleEnvironment = ( + context: PyContext, + name: string, + tail: Environment | null = null +): Environment => { + return { + id: uniqueId(context), + name, + tail, + head: {}, + heap: new Heap(), + // TODO: callExpression and thisContext are optional and can be provided as needed. + }; +}; + +export const createProgramEnvironment = (context: PyContext, isPrelude: boolean): Environment => { + return createSimpleEnvironment(context, isPrelude ? 'prelude' : 'programEnvironment'); +}; + +export const createBlockEnvironment = ( + context: PyContext, + name = 'blockEnvironment' +): Environment => { + return { + name, + tail: currentEnvironment(context), + head: {}, + heap: new Heap(), + id: uniqueId(context) + } +} + +// export const isRestElement = (node: Node): node is es.RestElement => { +// return (node as es.RestElement).type === 'RestElement'; +// }; + +// export const handleArrayCreation = ( +// context: PyContext, +// array: any[], +// envOverride?: Environment +// ): void => { +// const environment = envOverride ?? currentEnvironment(context); +// Object.defineProperties(array, { +// id: { value: uniqueId(context) }, +// environment: { value: environment, writable: true } +// }); +// environment.heap.add(array as any); +// }; + +export const currentEnvironment = (context: PyContext): Environment => { + return context.runtime.environments[0]; +}; + +export const popEnvironment = (context: PyContext) => context.runtime.environments.shift() + +export const pushEnvironment = (context: PyContext, environment: Environment) => { + context.runtime.environments.unshift(environment) + context.runtime.environmentTree.insert(environment) +} diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index 47babc2..118d295 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -1,7 +1,7 @@ import { StmtNS, ExprNS } from '../ast-types'; import { PyContext } from './py_context'; import { PyControl, PyControlItem } from './py_control'; -import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr } from './py_types'; +import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInstr } from './py_types'; import { Stash, Value, ErrorValue } from './stash'; import { IOptions } from '..'; import * as instr from './py_instrCreator'; @@ -10,6 +10,7 @@ import { TokenType } from '../tokens'; import { Token } from '../tokenizer'; import { Result, Finished, CSEBreak, Representation} from '../types'; import { toPythonString } from '../stdlib' +import { pyGetVariable, pyDefineVariable } from './py_utils'; type CmdEvaluator = ( command: PyControlItem, @@ -149,20 +150,44 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { }, 'Variable': (command, context, control, stash, isPrelude) => { - const variable = command as ExprNS.Variable; - const name = variable.name.lexeme; - // For now, we only handle built in constants. - // In a future commit, we will look up variables in the environment. + const variableNode = command as ExprNS.Variable; + const name = variableNode.name.lexeme; + if (name === 'True') { stash.push({ type: 'bool', value: true }); } else if (name === 'False') { stash.push({ type: 'bool', value: false }); } else { - // Throw an error for undefined variables for now - throw new Error(`NameError: name '${name}' is not defined`); + // if not built in, look up in environment + const value = pyGetVariable(context, name, variableNode) + stash.push(value); } }, + 'Compare': (command, context, control, stash, isPrelude) => { + const compareNode = command as ExprNS.Compare; + // For now, we only handle simple, single comparisons. + const opStr = mapOperatorToPyOperator(compareNode.operator); + const op_instr = instr.binOpInstr(opStr, compareNode); + control.push(op_instr); + control.push(compareNode.right); + control.push(compareNode.left); + }, + + 'Assign': (command, context, control, stash, isPrelude) => { + const assignNode = command as StmtNS.Assign; + + const assmtInstr = instr.assmtInstr( + assignNode.name.lexeme, + false, + true, + assignNode + ); + + control.push(assmtInstr); + control.push(assignNode.value); + }, + /** * Instruction Handlers */ @@ -242,4 +267,13 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { throw new Error(`Unsupported boolean operator: ${instr.symbol}`); } }, + + [InstrType.ASSIGNMENT]: (command, context, control, stash, isPrelude) => { + const instr = command as AssmtInstr; + const value = stash.pop(); // Get the evaluated value from the stash + + if (value) { + pyDefineVariable(context, instr.symbol, value); + } + }, }; \ No newline at end of file diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts index 918aa30..f343e36 100644 --- a/src/cse-machine/py_operators.ts +++ b/src/cse-machine/py_operators.ts @@ -433,7 +433,7 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con } } // Same type Integer Operations - else if (left.type === 'bigint' && right.type ==='bigint') { + else { const leftBigInt = left.value as bigint; const rightBigInt = right.value as bigint; let result: bigint | boolean; diff --git a/src/cse-machine/py_utils.ts b/src/cse-machine/py_utils.ts index 241b4d2..daedc87 100644 --- a/src/cse-machine/py_utils.ts +++ b/src/cse-machine/py_utils.ts @@ -1,9 +1,15 @@ -import { Context } from "./context"; -import { ExprNS } from "../ast-types"; +import { PyContext } from "./py_context"; +import { Value } from "./stash"; +import { PyNode } from "./py_types"; import { TokenType } from "../tokens"; -import { UnsupportedOperandTypeError } from "../errors/errors"; +import { RuntimeSourceError } from "../errors/runtimeSourceError"; +import { currentEnvironment, Environment } from "./py_environment"; +import { TypeError } from "../errors/errors"; -export function handleRuntimeError (context: Context, error: any) { + + +export function handleRuntimeError (context: PyContext, error: RuntimeSourceError) { + context.errors.push(error); throw error; } @@ -29,23 +35,21 @@ export function typeTranslator(type: string): string { // TODO: properly adapt for the rest, string is passed in to cater for __py_adder etc... export function operandTranslator(operand: TokenType | string) { if (typeof operand === 'string') { - return operand; + switch (operand) { + case "__py_adder": return "+"; + case "__py_minuser": return "-"; + case "__py_multiplier": return "*"; + case "__py_divider": return "/"; + case "__py_modder": return "%"; + case "__py_powerer": return "**"; + default: return operand; + } } switch (operand) { - case TokenType.PLUS: - return '+'; - case TokenType.MINUS: - return '-'; - case TokenType.STAR: - return '*'; - case TokenType.SLASH: - return '/'; case TokenType.LESS: return '<'; case TokenType.GREATER: return '>'; - case TokenType.PERCENT: - return '%'; case TokenType.DOUBLEEQUAL: return '=='; case TokenType.NOTEQUAL: @@ -54,12 +58,8 @@ export function operandTranslator(operand: TokenType | string) { return '<='; case TokenType.GREATEREQUAL: return '>='; - case TokenType.DOUBLESTAR: - return '**'; case TokenType.NOT: return 'not'; - case TokenType.DOUBLESLASH: - return '//'; default: return String(operand); } @@ -73,3 +73,26 @@ export function pythonMod(a: any, b: any): any { return mod + b; } } + +export function pyDefineVariable(context: PyContext, name: string, value: Value) { + const environment = currentEnvironment(context); + Object.defineProperty(environment.head, name, { + value: value, + writable: true, + enumerable: true + }); +} + +export function pyGetVariable(context: PyContext, name: string, node: PyNode): Value { + let environment: Environment | null = currentEnvironment(context); + while (environment) { + if (Object.prototype.hasOwnProperty.call(environment.head, name)) { + return environment.head[name]; + } else { + environment = environment.tail; + } + } + // For now, we throw an error. We can change this to return undefined if needed. + handleRuntimeError(context, new TypeError(`name '${name} is not defined`, node as any, context as any, '', '')); + return { type: 'error', message: 'unreachable' }; +} diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 3341b73..9cefa87 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -261,10 +261,11 @@ export class TypeError extends RuntimeSourceError { ?? ''; let hint = "TypeError: '" + originalType + "' cannot be interpreted as an '" + targetType + "'."; const offset = fullLine.indexOf(snippet); + const adjustedOffset = offset >= 0 ? offset : 0; const indicator = createErrorIndicator(snippet, '@'); const name = "TypeError"; const suggestion = ' Make sure the value you are passing is compatible with the expected type.'; - const msg = name + " at line " + line + "\n\n " + fullLine + "\n " + " ".repeat(offset) + indicator + "\n" + hint + suggestion; + const msg = name + " at line " + line + "\n\n " + fullLine + "\n " + " ".repeat(adjustedOffset) + indicator + "\n" + hint + suggestion; this.message = msg; } } diff --git a/tsconfig.json b/tsconfig.json index 1ceeca8..429607f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,7 @@ "module": "commonjs",//"ESNext",// /* Specify what module code is generated. */ "rootDir": "src", /* Specify the root folder within your source files. */ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "ignoreDeprecations": "6.0", // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ From aaee0a78204db8b7e45a171dab6d65dc3694ea0a Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 17 Sep 2025 13:48:05 +0800 Subject: [PATCH 09/16] Removed __py_{operators} and updated operators (Refs #51)[9] * src/types.ts - updated Representation as call toPythonString is redundant * py_interpreter - removed mapOperatorToPyOperator, update binary and compare * py_operators - updated evaluateUnaryExpression, evaluateBinaryExpression, PyCompare * minor fix for the pythonMod, pyGetVariable --- src/cse-machine/py_interpreter.ts | 19 +- src/cse-machine/py_operators.ts | 447 +++++++++++++----------------- src/cse-machine/py_types.ts | 2 +- src/cse-machine/py_utils.ts | 26 +- src/types.ts | 5 +- 5 files changed, 224 insertions(+), 275 deletions(-) diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index 118d295..7bfa7c6 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -35,19 +35,6 @@ export function PyCSEResultPromise(context: PyContext, value: Value): Promise { const binary = command as ExprNS.Binary; - const opStr = mapOperatorToPyOperator(binary.operator); - const op_instr = instr.binOpInstr(opStr, binary); + const op_instr = instr.binOpInstr(binary.operator.type, binary); control.push(op_instr); control.push(binary.right); control.push(binary.left); @@ -167,8 +153,7 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { 'Compare': (command, context, control, stash, isPrelude) => { const compareNode = command as ExprNS.Compare; // For now, we only handle simple, single comparisons. - const opStr = mapOperatorToPyOperator(compareNode.operator); - const op_instr = instr.binOpInstr(opStr, compareNode); + const op_instr = instr.binOpInstr(compareNode.operator.type, compareNode); control.push(op_instr); control.push(compareNode.right); control.push(compareNode.left); diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts index f343e36..136c9da 100644 --- a/src/cse-machine/py_operators.ts +++ b/src/cse-machine/py_operators.ts @@ -68,6 +68,18 @@ export type BinaryOperator = */ function pyCompare(val1 : Value, val2 : Value): number { + // Handle same type comparisons first + if (val1.type === 'bigint' && val2.type === 'bigint') { + if (val1.value < val2.value) return -1; + if (val1.value > val2.value) return 1; + return 0; + } + if (val1.type === 'number' && val2.type === 'number') { + if (val1.value < val2.value) return -1; + if (val1.value > val2.value) return 1; + return 0; + } + // int_num.value < float_num.value => -1 // int_num.value = float_num.value => 0 // int_num.value > float_num.value => 1 @@ -212,274 +224,215 @@ function approximateBigIntString(num: number, precision: number): string { return mantissaStr.slice(0, integerLen); } -// Changed operator from es.UnaryOperator to TokenType -// Added parameters command and context for error handling -// updated logics for TokenType.NOT [3] +// Helper function for truthiness based on Python rules +function isFalsy(value: Value): boolean { + switch (value.type) { + case 'bigint': + return value.value === 0n; + case 'number': + return value.value === 0; + case 'bool': + return !value.value; + case 'string': + return value.value === ''; + case 'complex': + return value.value.real === 0 && value.value.imag == 0; + case 'undefined': // Represents None + return true; + default: + // All other objects are considered truthy + return false; + } +} + export function evaluateUnaryExpression(operator: TokenType, value: Value, command: ExprNS.Expr, context: PyContext): Value { - if (operator === TokenType.NOT) { - let isFalsy: boolean; - switch (value.type) { - case 'bigint': - isFalsy = value.value === 0n; - break; - case 'number': - isFalsy = value.value === 0; - break; - case 'bool': - isFalsy = !value.value; - break; - case 'string': - isFalsy = value.value === ''; - break; - case 'undefined': - isFalsy = true; - break; - default: - // TODO: consider strings, list as truthy if exists - // Implement falsy for empty strings..etc - isFalsy = false; - }return {type: 'bool', value: isFalsy} + switch (operator) { + case TokenType.NOT: + return { type: 'bool', value: isFalsy(value) }; - } else if (operator === TokenType.MINUS) { - if (value.type === 'bigint') { - return { - type: 'bigint', - value: -1n * value.value - }; - } else if (value.type === 'number') { - return { - type: 'number', - value: -value.value - }; - } else { - // TODO: error handling for unsupported type - // TODO: command is currently passed as "any", to be adjusted in future commits[from #2] - // handleRuntimeError(context, new UnsupportedOperandTypeError("", command as any, typeTranslator(value.type), "", operandTranslator(operator))) - } - } else if (operator === TokenType.PLUS) { - if (value.type === 'complex' || value.type === 'number' || value.type === 'bigint') { - return value; - } else { - // TODO: error handling for unsupported type - // TODO: command is currently passed as "any", to be adjusted in future commits[from #2] - // handleRuntimeError(context, new UnsupportedOperandTypeError("", command as any, typeTranslator(value.type), "", operandTranslator(operator))) - } + case TokenType.MINUS: + switch (value.type) { + case 'number': + return { type: 'number', value: -value.value }; + case 'bigint': + return { type: 'bigint', value: -value.value }; + case 'bool': + return { type: 'bigint', value: value.value ? -1n : 0n }; + case 'complex': + return { + type: 'complex', + value: new PyComplexNumber(-value.value.real, -value.value.imag) + } + default: + // handleRuntimeError(context, new UnsupportedOperandTypeError(...)); + return { type: 'error', message: `Unsupported operand for -: '${value.type}'` }; + } + + case TokenType.PLUS: + switch (value.type) { + case 'number': + case 'bigint': + case 'complex': + return value; + case 'bool': + return { type: 'bigint', value: value.value ? 1n : 0n }; + default: + // handleRuntimeError(context, new UnsupportedOperandTypeError(...)); + return { type: 'error', message: `Unsupported operand for +: '${value.type}'` }; + } } - // final fallback - // handleRuntimeError - return { type: "error", message: 'unreachable' }; + return { type: 'error', message: 'Unreachable unary operator' }; } -// Change command from "ControlItem" to "ExprNS.Expr", "identifier" to "operator" for type safety -export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: PyContext, operator: TokenType | string, left: Value, right: Value): Value { - - const operand = operandTranslator(operator); - const originalLeftType = typeTranslator(left.type); - const originalRightType = typeTranslator(right.type); - - // String Operations - if (left.type === 'string' && right.type === 'string') { - if(operator === '__py_adder') { - return { type: 'string', value: left.value + right.value }; - } else if (operator === TokenType.GREATER) { - return { type: 'bool', value: left.value > right.value }; - } else if (operator === TokenType.GREATEREQUAL) { - return { type: 'bool', value: left.value >= right.value }; - } else if (operator === TokenType.LESS) { - return { type: 'bool', value: left.value < right.value }; - } else if (operator === TokenType.LESSEQUAL) { - return { type: 'bool', value: left.value <= right.value }; - } else if (operator === TokenType.DOUBLEEQUAL) { - return { type: 'bool', value: left.value === right.value }; - } else if (operator === TokenType.NOTEQUAL) { - return { type: 'bool', value: left.value !== right.value }; - } else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command as any, originalLeftType, originalRightType, operand)); - return { type: 'error', message: 'Unsupported string operation' } - } +// Remove __py_{operators} translation stage and switch case for readability +export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: PyContext, operator: TokenType, left: Value, right: Value): Value { + // Helper to generate a specific TypeError message string + // TODO: remove for proper error message after ../errors/error.ts is migrated + function unsupportedOperandsMessage(): string { + const leftTypeStr = typeTranslator(left.type); + const rightTypeStr = typeTranslator(right.type); + const opStr = operandTranslator(operator); + return `TypeError: unsupported operand type(s) for ${opStr}: '${leftTypeStr}' and '${rightTypeStr}'`; } - // Complex Operations - else if (left.type === 'complex' || right.type ==='complex'){ - let leftForComplex: number | bigint | string | PyComplexNumber; - let rightForComplex: number | bigint | string | PyComplexNumber; - if (left.type === 'complex' || left.type === 'number' || left.type === 'bigint' || left.type === 'string') { - leftForComplex = left.value; - } else { - // handleRuntimeError - return { type: 'error', message: 'Invalid operand for complex operation' }; + // Handle Complex numbers + if (left.type === 'complex' || right.type === 'complex') { + if (right.type !== 'complex' && right.type !== 'number' && right.type !== 'bigint' && right.type !== 'bool') { + return { type: 'error', message: unsupportedOperandsMessage() }; } + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + let result: PyComplexNumber; - if (right.type === 'complex' || right.type === 'number' || right.type === 'bigint' || right.type === 'string') { - rightForComplex = right.value; - } else { - // handleRuntimeError - return { type: 'error', message: 'Invalid operand for complex operation' }; + switch (operator) { + case TokenType.PLUS: result = leftComplex.add(rightComplex); break; + case TokenType.MINUS: result = leftComplex.sub(rightComplex); break; + case TokenType.STAR: result = leftComplex.mul(rightComplex); break; + case TokenType.SLASH: result = leftComplex.div(rightComplex); break; + case TokenType.DOUBLESTAR: result = leftComplex.pow(rightComplex); break; + case TokenType.DOUBLEEQUAL: return { type: 'bool', value: leftComplex.equals(rightComplex) }; + case TokenType.NOTEQUAL: return { type: 'bool', value: !leftComplex.equals(rightComplex) }; + default: return { type: 'error', message: `TypeError: unsupported operator for complex numbers: ${operator}` }; } - - const leftComplex = PyComplexNumber.fromValue(leftForComplex); - const rightComplex = PyComplexNumber.fromValue(rightForComplex); - let result: PyComplexNumber; + return { type: 'complex', value: result }; + } - if (operator === '__py_adder') { - result = leftComplex.add(rightComplex); - } else if (operator === '__py_minuser') { - result = leftComplex.sub(rightComplex); - } else if (operator === '__py_multiplier') { - result = leftComplex.mul(rightComplex); - } else if (operator === '__py_divider') { - result = leftComplex.div(rightComplex); - } else if (operator === '__py_powerer') { - result = leftComplex.pow(rightComplex); - } else if (operator === TokenType.DOUBLEEQUAL) { - return { type: 'bool', value: leftComplex.equals(rightComplex)}; - } else if (operator === TokenType.NOTEQUAL) { - return { type: 'bool', value: !leftComplex.equals(rightComplex)}; - } else { - // handleRuntimeError - return {type: 'error', message: 'Unsupported complex operation'}; - } return {type: 'complex', value: result}; - } - // bool and numeric operations - else if ((left.type === 'bool' && (right.type === 'number' || right.type === 'bigint' || right.type === 'bool')) || - (right.type === 'bool' && (left.type === 'number' || left.type === 'bigint' || left.type === 'bool'))) { + // Handle comparisons with None (represented as 'undefined' type) + if (left.type === 'undefined' || right.type === 'undefined') { + switch (operator) { + case TokenType.DOUBLEEQUAL: + // True only if both are None + return { type: 'bool', value: left.type === right.type }; + case TokenType.NOTEQUAL: + return { type: 'bool', value: left.type !== right.type }; + default: + return { type: 'error', message: unsupportedOperandsMessage() }; + } + } - const leftNum = left.type === 'bool' ? (left.value ? 1 : 0) : Number(left.value); - const rightNum = right.type === 'bool' ? (right.value ? 1 : 0) : Number(right.value); - let result: number | boolean; - - // Arithmetic - if (typeof operator === 'string') { - if (operator === '__py_adder') result = leftNum + rightNum; - else if (operator === '__py_minuser') result = leftNum - rightNum; - else if (operator === '__py_multiplier') result = leftNum * rightNum; - else if (operator === '__py_divider') { - if (rightNum === 0) { - // handleRuntimeError(context, new ZeroDivisionError(code, command, context)); - return { type: 'error', message: 'Division by zero' }; - } - result = leftNum / rightNum; + // Handle string operations + if (left.type === 'string' || right.type === 'string') { + if (left.type === 'string' && right.type === 'string') { + switch (operator) { + case TokenType.PLUS: + return { type: 'string', value: left.value + right.value }; + case TokenType.DOUBLEEQUAL: + return { type: 'bool', value: left.value === right.value }; + case TokenType.NOTEQUAL: + return { type: 'bool', value: left.value !== right.value }; + case TokenType.LESS: + return { type: 'bool', value: left.value < right.value }; + case TokenType.LESSEQUAL: + return { type: 'bool', value: left.value <= right.value }; + case TokenType.GREATER: + return { type: 'bool', value: left.value > right.value }; + case TokenType.GREATEREQUAL: + return { type: 'bool', value: left.value >= right.value }; } - else if (operator === '__py_powerer') result = leftNum ** rightNum; - else if (operator === '__py_modder') result = pythonMod(leftNum, rightNum); - else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); - return { type: 'error', message: 'Unsupported boolean/numeric operation' }; - } - const resultType = (left.type === 'number' || right.type === 'number') ? 'number' : 'bigint'; - return { type: resultType, value: resultType === 'bigint' ? BigInt(result) : result }; - } - // Comparisons - else { - if (operator === TokenType.GREATER) result = leftNum > rightNum; - else if (operator === TokenType.GREATEREQUAL) result = leftNum >= rightNum; - else if (operator === TokenType.LESS) result = leftNum < rightNum; - else if (operator === TokenType.LESSEQUAL) result = leftNum <= rightNum; - else if (operator === TokenType.DOUBLEEQUAL) result = leftNum === rightNum; - else if (operator === TokenType.NOTEQUAL) result = leftNum !== rightNum; - else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); - return { type: 'error', message: 'Unsupported boolean/numeric comparison' }; - } - return { type: 'bool', value: result }; } + return { type: 'error', message: unsupportedOperandsMessage() }; } - // Float and or Int Operations - else if ((left.type === 'number' || left.type === 'bigint') && (right.type === 'number' || right.type === 'bigint')) { - if (left.type === 'number' || right.type === 'number' || operator === '__py_divider') { - const leftFloat = Number(left.value); - const rightFloat = Number(right.value); - let result: number | boolean; - // Arithmetic - if (typeof operator === 'string') { - if (operator === '__py_adder') result = leftFloat + rightFloat; - else if (operator === '__py_minuser') result = leftFloat - rightFloat; - else if (operator === '__py_multiplier') result = leftFloat * rightFloat; - else if (operator === '__py_divider') { - if (rightFloat === 0) { - // handleRuntimeError(context, new ZeroDivisionError(code, command, context)); - return { type: 'error', message: 'Division by zero' }; - } - result = leftFloat / rightFloat; - } - else if (operator === '__py_powerer') result = leftFloat ** rightFloat; - else if (operator === '__py_modder') { - if (rightFloat === 0) { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); - return { type: 'error', message: 'Division by zero' }; - } - result = pythonMod(leftFloat, rightFloat); - } else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); - return { type: 'error', message: 'Unsupported float comparison' }; + // Coerce boolean to a numeric value for all other arithmetic + const leftNum = left.type === 'bool' ? (left.value ? 1 : 0) : left.value; + const rightNum = right.type === 'bool' ? (right.value ? 1 : 0) : right.value; + const leftType = left.type === 'bool' ? 'number' : left.type; + const rightType = right.type === 'bool' ? 'number' : right.type; + + // Numeric Operations (number or bigint) + switch (operator) { + case TokenType.PLUS: + case TokenType.MINUS: + case TokenType.STAR: + case TokenType.SLASH: + case TokenType.DOUBLESLASH: + case TokenType.PERCENT: + case TokenType.DOUBLESTAR: + if (leftType === 'number' || rightType === 'number') { + const l = Number(leftNum); + const r = Number(rightNum); + switch (operator) { + case TokenType.PLUS: return { type: 'number', value: l + r }; + case TokenType.MINUS: return { type: 'number', value: l - r }; + case TokenType.STAR: return { type: 'number', value: l * r }; + case TokenType.SLASH: + if (r === 0) return { type: 'error', message: 'ZeroDivisionError: division by zero' }; + return { type: 'number', value: l / r }; + case TokenType.DOUBLESLASH: + if (r === 0) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; + return { type: 'number', value: Math.floor(l / r) }; + case TokenType.PERCENT: + if (r === 0) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; + return { type: 'number', value: pythonMod(l, r) }; + case TokenType.DOUBLESTAR: return { type: 'number', value: l ** r }; } - return { type: 'number', value: result }; } - // Comparisons - else { - const compare_res = pyCompare(left, right); - if (operator === TokenType.GREATER) result = compare_res > 0; - else if (operator === TokenType.GREATEREQUAL) result = compare_res >= 0; - else if (operator === TokenType.LESS) result = compare_res < 0; - else if (operator === TokenType.LESSEQUAL) result = compare_res <= 0; - else if (operator === TokenType.DOUBLEEQUAL) result = compare_res === 0; - else if (operator === TokenType.NOTEQUAL) result = compare_res !== 0; - else { - // handleRuntimeError(context, new UnsupportedOperandTypeError(code, command, originalLeftType, originalRightType, operand)); - return { type: 'error', message: 'Unsupported float comparison' }; - } - return { type: 'bool', value: result }; + if (leftType === 'bigint' && rightType === 'bigint') { + const l = leftNum as bigint; + const r = rightNum as bigint; + switch (operator) { + case TokenType.PLUS: return { type: 'bigint', value: l + r }; + case TokenType.MINUS: return { type: 'bigint', value: l - r }; + case TokenType.STAR: return { type: 'bigint', value: l * r }; + case TokenType.SLASH: + if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: division by zero' }; + return { type: 'number', value: Number(l) / Number(r) }; + case TokenType.DOUBLESLASH: + if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; + return { type: 'bigint', value: l / r }; + case TokenType.PERCENT: + if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; + return { type: 'bigint', value: pythonMod(l, r) }; + case TokenType.DOUBLESTAR: + if (l === 0n && r < 0n) { + return { type: 'error', message: 'ZeroDivisionError: 0.0 cannot be raised to a negative power' }; + } + if (r < 0n) return { type: 'number', value: Number(l) ** Number(r)} + return { type: 'bigint', value: l ** r }; + } } - } - // Same type Integer Operations - else { - const leftBigInt = left.value as bigint; - const rightBigInt = right.value as bigint; - let result: bigint | boolean; + break; - if (operator === '__py_adder') { - return { type: 'bigint', value: leftBigInt + rightBigInt }; - } else if (operator === '__py_minuser') { - return { type: 'bigint', value: leftBigInt - rightBigInt }; - } else if (operator === '__py_multiplier') { - return { type: 'bigint', value: leftBigInt * rightBigInt }; - } else if (operator === '__py_divider') { - if (rightBigInt === 0n) { - // handleRunTimeError - ZeroDivisionError - return { type: 'error', message: 'Division by zero' } ; - } - return { type: 'number', value: Number(leftBigInt) / Number(rightBigInt) }; - } else if (operator === '__py_powerer') { - if (leftBigInt === 0n && rightBigInt < 0) { - // handleRunTimeError, zerodivision error - return { type: 'error', message: '0.0 cannot be raised to a negative power'} - } - if (rightBigInt < 0) { - return {type: 'number', value: Number(leftBigInt) ** Number(rightBigInt) }; + // Comparison Operators + case TokenType.DOUBLEEQUAL: + case TokenType.NOTEQUAL: + case TokenType.LESS: + case TokenType.LESSEQUAL: + case TokenType.GREATER: + case TokenType.GREATEREQUAL: { + const cmp = pyCompare(left, right); + let result: boolean; + switch (operator) { + case TokenType.DOUBLEEQUAL: result = cmp === 0; break; + case TokenType.NOTEQUAL: result = cmp !== 0; break; + case TokenType.LESS: result = cmp < 0; break; + case TokenType.LESSEQUAL: result = cmp <= 0; break; + case TokenType.GREATER: result = cmp > 0; break; + case TokenType.GREATEREQUAL:result = cmp >= 0; break; + default: return { type: 'error', message: 'Unreachable code path in comparison' }; } - return { type: 'bigint', value: leftBigInt ** rightBigInt }; - } else if (operator === '__py_modder') { - if (rightBigInt === 0n) { - // handleRunTimeError - ZeroDivisionError - return { type: 'error', message: 'integer modulo by zero' } ; - } - return { type: 'bigint', value: pythonMod(leftBigInt, rightBigInt) }; - } else if (operator === TokenType.GREATER) { - return { type: 'bool', value: leftBigInt > rightBigInt }; - } else if (operator === TokenType.GREATEREQUAL) { - return { type: 'bool', value: leftBigInt >= rightBigInt }; - } else if (operator === TokenType.LESS) { - return { type: 'bool', value: leftBigInt < rightBigInt }; - } else if (operator === TokenType.LESSEQUAL) { - return { type: 'bool', value: leftBigInt <= rightBigInt }; - } else if (operator === TokenType.DOUBLEEQUAL) { - return { type: 'bool', value: leftBigInt === rightBigInt }; - } else if (operator === TokenType.NOTEQUAL) { - return { type: 'bool', value: leftBigInt !== rightBigInt }; + return { type: 'bool', value: result }; } - // handleRuntimeError - return { type: 'error', message: 'Unsupported operation' }; - } } + return { type: 'error', message: unsupportedOperandsMessage() }; } diff --git a/src/cse-machine/py_types.ts b/src/cse-machine/py_types.ts index b67385d..5656cc4 100644 --- a/src/cse-machine/py_types.ts +++ b/src/cse-machine/py_types.ts @@ -69,7 +69,7 @@ export interface UnOpInstr extends BaseInstr { } export interface BinOpInstr extends BaseInstr { - symbol: string | TokenType + symbol: TokenType } export interface BoolOpInstr extends BaseInstr { diff --git a/src/cse-machine/py_utils.ts b/src/cse-machine/py_utils.ts index daedc87..4c28a59 100644 --- a/src/cse-machine/py_utils.ts +++ b/src/cse-machine/py_utils.ts @@ -65,12 +65,24 @@ export function operandTranslator(operand: TokenType | string) { } } -export function pythonMod(a: any, b: any): any { +export function pythonMod(a: number | bigint, b: number | bigint): number | bigint { + if (typeof a === 'bigint' || typeof b === 'bigint') { + const big_a = BigInt(a); + const big_b = BigInt(b); + const mod = big_a % big_b; + + if ((mod < 0n && big_b > 0n) || (mod > 0n && big_b < 0n)) { + return mod + big_b; + } else { + return mod; + } + } + // both are numbers const mod = a % b; - if ((mod >= 0 && b > 0) || (mod <= 0 && b < 0)) { - return mod; - } else { + if ((mod < 0 && b > 0) || (mod > 0 && b < 0)) { return mod + b; + } else { + return mod; } } @@ -93,6 +105,6 @@ export function pyGetVariable(context: PyContext, name: string, node: PyNode): V } } // For now, we throw an error. We can change this to return undefined if needed. - handleRuntimeError(context, new TypeError(`name '${name} is not defined`, node as any, context as any, '', '')); - return { type: 'error', message: 'unreachable' }; -} + // handleRuntimeError(context, new TypeError(`name '${name} is not defined`, node as any, context as any, '', '')); + return { type: 'error', message: `NameError: name '${name}' is not defined` }; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 7c95284..a58fb51 100644 --- a/src/types.ts +++ b/src/types.ts @@ -306,11 +306,10 @@ export interface Finished { export class Representation { constructor(public representation: string) {} - toString(value: any): string { + toString(): string { // call str(value) in stdlib // TODO: mapping - const result = toPythonString(value); - return result; + return this.representation; } } From c148d73650b6f29355c0ab3d333392445cce71ac Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 17 Sep 2025 14:36:02 +0800 Subject: [PATCH 10/16] Added Grouping and Complex (Refs #51)[9] * minor fix for integer division on bigints --- src/cse-machine/py_interpreter.ts | 10 ++++++++++ src/cse-machine/py_operators.ts | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index 7bfa7c6..a4151ae 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -131,6 +131,16 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(boolOp.left); }, + 'Grouping': (command, context, control) => { + const groupingNode = command as ExprNS.Grouping; + control.push(groupingNode.expression); + }, + + 'Complex': (command, context, control, stash) => { + const complexNode = command as ExprNS.Complex; + stash.push({ type: 'complex', value: complexNode.value }); + }, + 'None': (command, context, control, stash, isPrelude) => { stash.push({ type: 'undefined' }); }, diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts index 136c9da..6b9e623 100644 --- a/src/cse-machine/py_operators.ts +++ b/src/cse-machine/py_operators.ts @@ -265,7 +265,7 @@ export function evaluateUnaryExpression(operator: TokenType, value: Value, comma } default: // handleRuntimeError(context, new UnsupportedOperandTypeError(...)); - return { type: 'error', message: `Unsupported operand for -: '${value.type}'` }; + return { type: 'error', message: `TypeError: Unsupported operand for -: '${value.type}'` }; } case TokenType.PLUS: @@ -278,7 +278,7 @@ export function evaluateUnaryExpression(operator: TokenType, value: Value, comma return { type: 'bigint', value: value.value ? 1n : 0n }; default: // handleRuntimeError(context, new UnsupportedOperandTypeError(...)); - return { type: 'error', message: `Unsupported operand for +: '${value.type}'` }; + return { type: 'error', message: `TypeError: Unsupported operand for +: '${value.type}'` }; } } return { type: 'error', message: 'Unreachable unary operator' }; @@ -399,7 +399,7 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con return { type: 'number', value: Number(l) / Number(r) }; case TokenType.DOUBLESLASH: if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; - return { type: 'bigint', value: l / r }; + return { type: 'bigint', value: (l - (pythonMod(l, r) as bigint)) / r }; case TokenType.PERCENT: if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; return { type: 'bigint', value: pythonMod(l, r) }; From 801e807343a999c70c1ec2ec6f8be42c78bcc513 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 18 Sep 2025 19:21:23 +0800 Subject: [PATCH 11/16] Migrated errors and interpreter (Refs #51)[10] * new files: errors/ - py_errors, py_runtimeSourceError * replace temporary error handlign with propery runtimeHandler * moved interpreter core functions * TODO: other errors as needed when new handlers are introduced * --- src/cse-machine/py_interpreter.ts | 319 +++++++++++------ src/cse-machine/py_operators.ts | 509 ++++++++++++++++------------ src/cse-machine/py_utils.ts | 45 +-- src/errors/py_errors.ts | 339 ++++++++++++++++++ src/errors/py_runtimeSourceError.ts | 54 +++ src/index.ts | 2 +- src/py_stdlib.ts | 59 ++++ src/runner/pyRunner.ts | 14 +- src/types.ts | 35 +- 9 files changed, 1020 insertions(+), 356 deletions(-) create mode 100644 src/errors/py_errors.ts create mode 100644 src/errors/py_runtimeSourceError.ts create mode 100644 src/py_stdlib.ts diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index a4151ae..42de5fc 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -1,3 +1,11 @@ +/** + * This interpreter implements an explicit-control evaluator. + * + * Heavily adapted from https://github.com/source-academy/JSpike/ + */ + +/* tslint:disable:max-classes-per-file */ + import { StmtNS, ExprNS } from '../ast-types'; import { PyContext } from './py_context'; import { PyControl, PyControlItem } from './py_control'; @@ -5,14 +13,15 @@ import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInst import { Stash, Value, ErrorValue } from './stash'; import { IOptions } from '..'; import * as instr from './py_instrCreator'; -import { evaluateUnaryExpression, evaluateBinaryExpression } from './py_operators'; +import { evaluateUnaryExpression, evaluateBinaryExpression, evaluateBoolExpression } from './py_operators'; import { TokenType } from '../tokens'; import { Token } from '../tokenizer'; import { Result, Finished, CSEBreak, Representation} from '../types'; -import { toPythonString } from '../stdlib' +import { toPythonString } from '../py_stdlib' import { pyGetVariable, pyDefineVariable } from './py_utils'; type CmdEvaluator = ( + code: string, command: PyControlItem, context: PyContext, control: PyControl, @@ -20,6 +29,13 @@ type CmdEvaluator = ( isPrelude: boolean ) => void +/** + * Function that returns the appropriate Promise given the output of CSE machine evaluating, depending + * on whether the program is finished evaluating, ran into a breakpoint or ran into an error. + * @param context The context of the program. + * @param value The value of CSE machine evaluating the program. + * @returns The corresponding promise. + */ export function PyCSEResultPromise(context: PyContext, value: Value): Promise { return new Promise((resolve, reject) => { if (value instanceof CSEBreak) { @@ -35,44 +51,175 @@ export function PyCSEResultPromise(context: PyContext, value: Value): Promise { + 'FileInput': (code, command, context, control, stash, isPrelude) => { const fileInput = command as StmtNS.FileInput; const statements = fileInput.statements.slice().reverse(); control.push(...statements); }, - 'SimpleExpr': (command, context, control) => { + 'SimpleExpr': (code, command, context, control, stash, isPrelude) => { const simpleExpr = command as StmtNS.SimpleExpr; control.push(simpleExpr.expression); }, - 'Literal': (command, context, control, stash) => { + 'Literal': (code, command, context, control, stash, isPrelude) => { const literal = command as ExprNS.Literal; if (typeof literal.value === 'number') { stash.push({ type: 'number', value: literal.value }); @@ -104,19 +251,19 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { } }, - 'BigIntLiteral': (command, context, control, stash) => { + 'BigIntLiteral': (code, command, context, control, stash, isPrelude) => { const literal = command as ExprNS.BigIntLiteral; stash.push({ type: 'bigint', value: BigInt(literal.value) }); }, - 'Unary': (command, context, control) => { + 'Unary': (code, command, context, control, stash, isPrelude) => { const unary = command as ExprNS.Unary; const op_instr = instr.unOpInstr(unary.operator.type, unary); control.push(op_instr); control.push(unary.right); }, - 'Binary': (command, context, control) => { + 'Binary': (code, command, context, control, stash, isPrelude) => { const binary = command as ExprNS.Binary; const op_instr = instr.binOpInstr(binary.operator.type, binary); control.push(op_instr); @@ -124,43 +271,37 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(binary.left); }, - 'BoolOp': (command, context, control, stash, isPrelude) => { + 'BoolOp': (code, command, context, control, stash, isPrelude) => { const boolOp = command as ExprNS.BoolOp; control.push(instr.boolOpInstr(boolOp.operator.type, boolOp)); control.push(boolOp.right); control.push(boolOp.left); }, - 'Grouping': (command, context, control) => { + 'Grouping': (code, command, context, control, stash, isPrelude) => { const groupingNode = command as ExprNS.Grouping; control.push(groupingNode.expression); }, - 'Complex': (command, context, control, stash) => { + 'Complex': (code, command, context, control, stash, isPrelude) => { const complexNode = command as ExprNS.Complex; stash.push({ type: 'complex', value: complexNode.value }); }, - 'None': (command, context, control, stash, isPrelude) => { + 'None': (code, command, context, control, stash, isPrelude) => { stash.push({ type: 'undefined' }); }, - 'Variable': (command, context, control, stash, isPrelude) => { + 'Variable': (code, command, context, control, stash, isPrelude) => { const variableNode = command as ExprNS.Variable; const name = variableNode.name.lexeme; - if (name === 'True') { - stash.push({ type: 'bool', value: true }); - } else if (name === 'False') { - stash.push({ type: 'bool', value: false }); - } else { - // if not built in, look up in environment - const value = pyGetVariable(context, name, variableNode) - stash.push(value); - } + // if not built in, look up in environment + const value = pyGetVariable(context, name, variableNode) + stash.push(value); }, - 'Compare': (command, context, control, stash, isPrelude) => { + 'Compare': (code, command, context, control, stash, isPrelude) => { const compareNode = command as ExprNS.Compare; // For now, we only handle simple, single comparisons. const op_instr = instr.binOpInstr(compareNode.operator.type, compareNode); @@ -169,7 +310,7 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(compareNode.left); }, - 'Assign': (command, context, control, stash, isPrelude) => { + 'Assign': (code, command, context, control, stash, isPrelude) => { const assignNode = command as StmtNS.Assign; const assmtInstr = instr.assmtInstr( @@ -186,27 +327,29 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { /** * Instruction Handlers */ - [InstrType.UNARY_OP]: function (command: PyControlItem, context: PyContext, control: PyControl, stash: Stash, isPrelude: boolean) { + [InstrType.UNARY_OP]: function (code, command, context, control, stash, isPrelude) { const instr = command as UnOpInstr; const argument = stash.pop(); if (argument) { const result = evaluateUnaryExpression( - instr.symbol, - argument, + code, instr.srcNode as ExprNS.Expr, - context + context, + instr.symbol, + argument + ); stash.push(result); } }, - [InstrType.BINARY_OP]: function (command: PyControlItem, context: PyContext, control: PyControl, stash: Stash, isPrelude: boolean) { + [InstrType.BINARY_OP]: function (code, command, context, control, stash, isPrelude) { const instr = command as BinOpInstr; const right = stash.pop(); const left = stash.pop(); if (left && right) { const result = evaluateBinaryExpression( - "", // source string code + code, instr.srcNode as ExprNS.Expr, context, instr.symbol, @@ -217,53 +360,25 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { } }, - [InstrType.BOOL_OP]: function (command: PyControlItem, context: PyContext, control: PyControl, stash: Stash, isPrelude: - boolean) { + [InstrType.BOOL_OP]: function (code, command, context, control, stash, isPrelude) { const instr = command as BoolOpInstr; - const rightValue = stash.pop(); - const leftValue = stash.pop(); - - if (!leftValue || !rightValue) { - throw new Error("RuntimeError: Boolean operation requires two operands."); - } + const right = stash.pop(); + const left = stash.pop(); - // Implement Python's short-circuiting logic - if (instr.symbol === TokenType.OR) { - // If left is truthy, return left. Otherwise, return right. - let isLeftTruthy = false; - if (leftValue.type === 'bool') isLeftTruthy = leftValue.value; - else if (leftValue.type === 'bigint') isLeftTruthy = leftValue.value !== 0n; - else if (leftValue.type === 'number') isLeftTruthy = leftValue.value !== 0; - else if (leftValue.type === 'string') isLeftTruthy = leftValue.value !== ''; - else if (leftValue.type === 'undefined') isLeftTruthy = false; - else isLeftTruthy = true; // Other types are generally truthy - - if (isLeftTruthy) { - stash.push(leftValue); - } else { - stash.push(rightValue); - } - } else if (instr.symbol === TokenType.AND) { - // If left is falsy, return left. Otherwise, return right. - let isLeftFalsy = false; - if (leftValue.type === 'bool') isLeftFalsy = !leftValue.value; - else if (leftValue.type === 'bigint') isLeftFalsy = leftValue.value === 0n; - else if (leftValue.type === 'number') isLeftFalsy = leftValue.value === 0; - else if (leftValue.type === 'string') isLeftFalsy = leftValue.value === ''; - else if (leftValue.type === 'undefined') isLeftFalsy = true; - else isLeftFalsy = false; // Other types are generally truthy - - if (isLeftFalsy) { - stash.push(leftValue); - } else { - stash.push(rightValue); - } - } else { - throw new Error(`Unsupported boolean operator: ${instr.symbol}`); + if (left && right) { + const result = evaluateBoolExpression( + code, + instr.srcNode as ExprNS.Expr, + context, + instr.symbol, + left, + right + ) + stash.push(result); } }, - [InstrType.ASSIGNMENT]: (command, context, control, stash, isPrelude) => { + [InstrType.ASSIGNMENT]: (code, command, context, control, stash, isPrelude) => { const instr = command as AssmtInstr; const value = stash.pop(); // Get the evaluated value from the stash diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts index 6b9e623..7dae99b 100644 --- a/src/cse-machine/py_operators.ts +++ b/src/cse-machine/py_operators.ts @@ -1,11 +1,12 @@ import { Value } from "./stash"; import { PyContext } from "./py_context"; -import { PyComplexNumber } from "../types"; -import { TypeConcatenateError, UnsupportedOperandTypeError, ZeroDivisionError } from "../errors/errors"; +import { PyComplexNumber} from "../types"; +import { UnsupportedOperandTypeError, ZeroDivisionError, TypeConcatenateError } from "../errors/py_errors"; import { ExprNS } from "../ast-types"; import { TokenType } from "../tokens"; -import { handleRuntimeError, operandTranslator, pythonMod, typeTranslator } from "./py_utils"; +import { pyHandleRuntimeError, operatorTranslator, pythonMod, typeTranslator } from "./py_utils"; import { Token } from "../tokenizer"; +import { operandTranslator } from "./utils"; export type BinaryOperator = | "==" @@ -31,6 +32,293 @@ export type BinaryOperator = | "in" | "instanceof"; +// Helper function for truthiness based on Python rules +function isFalsy(value: Value): boolean { + switch (value.type) { + case 'bigint': + return value.value === 0n; + case 'number': + return value.value === 0; + case 'bool': + return !value.value; + case 'string': + return value.value === ''; + case 'complex': + return value.value.real === 0 && value.value.imag == 0; + case 'undefined': // Represents None + return true; + default: + // All other objects are considered truthy + return false; + } +} + +export function evaluateBoolExpression(code: string, command: ExprNS.Expr, context: PyContext, operator: TokenType, left: Value, right: Value): Value { + if (operator === TokenType.OR) { + // Python 'or': if the first value is truthy, return it. Otherwise, evaluate and return the second value. + return !isFalsy(left) ? left : right; + } else if (operator === TokenType.AND) { + // Python 'and': if the first value is falsy, return it. Otherwise, evaluate and return the second value. + return isFalsy(left) ? left : right; + } else { + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( + code, command, typeTranslator(left.type), typeTranslator(right.type), operatorTranslator(operator) + )); + return { type: 'error', message: `Unreachable in evaluateBoolExpression}` }; + } +} + +export function evaluateUnaryExpression(code: string, command: ExprNS.Expr, context: PyContext, operator: TokenType, value: Value): Value { + switch (operator) { + case TokenType.NOT: + return { type: 'bool', value: isFalsy(value) }; + + case TokenType.MINUS: + switch (value.type) { + case 'number': + return { type: 'number', value: -value.value }; + case 'bigint': + return { type: 'bigint', value: -value.value }; + case 'bool': + return { type: 'bigint', value: value.value ? -1n : 0n }; + case 'complex': + return { + type: 'complex', + value: new PyComplexNumber(-value.value.real, -value.value.imag) + } + default: + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( + code, command, value.type, "", operatorTranslator(operator) + )); + return { type: 'error', message: 'Unreachable in evaluateUnaryExpression - MINUS' }; + } + + case TokenType.PLUS: + switch (value.type) { + case 'number': + case 'bigint': + case 'complex': + return value; + case 'bool': + return { type: 'bigint', value: value.value ? 1n : 0n }; + default: + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( + code, + command, + value.type, + "", + operatorTranslator(operator) + )); + return { type: 'error', message: 'Unreachable in evaluateUnaryExpression - PLUS' }; + } + } + return { type: 'error', message: 'Unreachable in evaluateUnaryExpression' }; +} + +// Remove __py_{operators} translation stage and switch case for readability +// TODO: do we need to string repetition like 'a' * 10? +export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: PyContext, operator: TokenType, left: Value, right: Value): Value { + + // Handle Complex numbers + if (left.type === 'complex' || right.type === 'complex') { + if (right.type !== 'complex' && right.type !== 'number' && right.type !== 'bigint' && right.type !== 'bool') { + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( + code, + command, + left.type, + right.type, + operatorTranslator(operator))); + return { type: 'error', message: 'Unreachable in evaluateBinaryExpression - complex | complex (start)' }; + } + const leftComplex = PyComplexNumber.fromValue(left.value); + const rightComplex = PyComplexNumber.fromValue(right.value); + let result: PyComplexNumber; + + switch (operator) { + case TokenType.PLUS: result = leftComplex.add(rightComplex); break; + case TokenType.MINUS: result = leftComplex.sub(rightComplex); break; + case TokenType.STAR: result = leftComplex.mul(rightComplex); break; + case TokenType.SLASH: result = leftComplex.div(rightComplex); break; + case TokenType.DOUBLESTAR: result = leftComplex.pow(rightComplex); break; + case TokenType.DOUBLEEQUAL: return { type: 'bool', value: leftComplex.equals(rightComplex) }; + case TokenType.NOTEQUAL: return { type: 'bool', value: !leftComplex.equals(rightComplex) }; + default: + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( + code, + command, + left.type, + right.type, + operatorTranslator(operator))); + return { type: 'error', message: 'Unreachable in evaluateBinaryExpression - complex | complex (end)' }; + } + return { type: 'complex', value: result }; + } + + // Handle comparisons with None (represented as 'undefined' type) + if (left.type === 'undefined' || right.type === 'undefined') { + switch (operator) { + case TokenType.DOUBLEEQUAL: + // True only if both are None + return { type: 'bool', value: left.type === right.type }; + case TokenType.NOTEQUAL: + return { type: 'bool', value: left.type !== right.type }; + default: + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( + code, + command, + left.type, + right.type, + operatorTranslator(operator))); + return { type: 'error', message: 'Unreachable in evaluateBinaryExpression - undefined | undefined' }; + } + } + + // Handle string operations + if (left.type === 'string' || right.type === 'string') { + if (operator === TokenType.PLUS) { + if (left.type === 'string' && right.type === 'string') { + return { type: 'string', value: left.value + right.value }; + } else { + const wrongType = left.type === 'string' ? right.type : left.type; + pyHandleRuntimeError(context, new TypeConcatenateError( + code, + command, + typeTranslator(wrongType) + )); + } + } + if (left.type === 'string' && right.type === 'string') { + switch (operator) { + case TokenType.DOUBLEEQUAL: + return { type: 'bool', value: left.value === right.value }; + case TokenType.NOTEQUAL: + return { type: 'bool', value: left.value !== right.value }; + case TokenType.LESS: + return { type: 'bool', value: left.value < right.value }; + case TokenType.LESSEQUAL: + return { type: 'bool', value: left.value <= right.value }; + case TokenType.GREATER: + return { type: 'bool', value: left.value > right.value }; + case TokenType.GREATEREQUAL: + return { type: 'bool', value: left.value >= right.value }; + } + } + // TypeError: Reached if one is a string and the other is not + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( + code, + command, + left.type, + right.type, + operatorTranslator(operator))); + return { type: 'error', message: 'Unreachable in evaluateBinaryExpression - string | string' }; + } + + /** + * Coerce boolean to a numeric value for all other arithmetic + * Support for True - 1 or False + 1 + */ + const leftNum = left.type === 'bool' ? (left.value ? 1 : 0) : left.value; + const rightNum = right.type === 'bool' ? (right.value ? 1 : 0) : right.value; + const leftType = left.type === 'bool' ? 'number' : left.type; + const rightType = right.type === 'bool' ? 'number' : right.type; + + // Numeric Operations (number or bigint) + switch (operator) { + case TokenType.PLUS: + case TokenType.MINUS: + case TokenType.STAR: + case TokenType.SLASH: + case TokenType.DOUBLESLASH: + case TokenType.PERCENT: + case TokenType.DOUBLESTAR: + if (leftType === 'number' || rightType === 'number') { + const l = Number(leftNum); + const r = Number(rightNum); + switch (operator) { + case TokenType.PLUS: + return { type: 'number', value: l + r }; + case TokenType.MINUS: + return { type: 'number', value: l - r }; + case TokenType.STAR: + return { type: 'number', value: l * r }; + case TokenType.SLASH: + if (r === 0) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + return { type: 'number', value: l / r }; + case TokenType.DOUBLESLASH: + if (r === 0) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + return { type: 'number', value: Math.floor(l / r) }; + case TokenType.PERCENT: + if (r === 0) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + return { type: 'number', value: pythonMod(l, r) }; + case TokenType.DOUBLESTAR: + if (r === 0) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + return { type: 'number', value: l ** r }; + } + } + if (leftType === 'bigint' && rightType === 'bigint') { + const l = leftNum as bigint; + const r = rightNum as bigint; + switch (operator) { + case TokenType.PLUS: return { type: 'bigint', value: l + r }; + case TokenType.MINUS: return { type: 'bigint', value: l - r }; + case TokenType.STAR: return { type: 'bigint', value: l * r }; + case TokenType.SLASH: + if (r === 0n) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + return { type: 'number', value: Number(l) / Number(r) }; + case TokenType.DOUBLESLASH: + if (r === 0n) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + return { type: 'bigint', value: (l - (pythonMod(l, r) as bigint)) / r }; + case TokenType.PERCENT: + if (r === 0n) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + return { type: 'bigint', value: pythonMod(l, r) }; + case TokenType.DOUBLESTAR: + if (l === 0n && r < 0n) { + pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); + } + if (r < 0n) return { type: 'number', value: Number(l) ** Number(r)} + return { type: 'bigint', value: l ** r }; + } + } + break; + + // Comparison Operators + case TokenType.DOUBLEEQUAL: + case TokenType.NOTEQUAL: + case TokenType.LESS: + case TokenType.LESSEQUAL: + case TokenType.GREATER: + case TokenType.GREATEREQUAL: { + const cmp = pyCompare(left, right); + let result: boolean; + switch (operator) { + case TokenType.DOUBLEEQUAL: result = cmp === 0; break; + case TokenType.NOTEQUAL: result = cmp !== 0; break; + case TokenType.LESS: result = cmp < 0; break; + case TokenType.LESSEQUAL: result = cmp <= 0; break; + case TokenType.GREATER: result = cmp > 0; break; + case TokenType.GREATEREQUAL:result = cmp >= 0; break; + default: return { type: 'error', message: 'Unreachable in evaluateBinaryExpression - comparison' }; + } + return { type: 'bool', value: result }; + } + } + return { type: 'error', message: 'todo error' }; +} + /** * TEMPORARY IMPLEMENTATION * This function is a simplified comparison between int and float @@ -222,217 +510,4 @@ function approximateBigIntString(num: number, precision: number): string { // If the mantissa is too long, truncate it (this is equivalent to taking the floor). // Rounding could be applied if necessary, but truncation is sufficient for comparison. return mantissaStr.slice(0, integerLen); -} - -// Helper function for truthiness based on Python rules -function isFalsy(value: Value): boolean { - switch (value.type) { - case 'bigint': - return value.value === 0n; - case 'number': - return value.value === 0; - case 'bool': - return !value.value; - case 'string': - return value.value === ''; - case 'complex': - return value.value.real === 0 && value.value.imag == 0; - case 'undefined': // Represents None - return true; - default: - // All other objects are considered truthy - return false; - } -} - -export function evaluateUnaryExpression(operator: TokenType, value: Value, command: ExprNS.Expr, context: PyContext): Value { - switch (operator) { - case TokenType.NOT: - return { type: 'bool', value: isFalsy(value) }; - - case TokenType.MINUS: - switch (value.type) { - case 'number': - return { type: 'number', value: -value.value }; - case 'bigint': - return { type: 'bigint', value: -value.value }; - case 'bool': - return { type: 'bigint', value: value.value ? -1n : 0n }; - case 'complex': - return { - type: 'complex', - value: new PyComplexNumber(-value.value.real, -value.value.imag) - } - default: - // handleRuntimeError(context, new UnsupportedOperandTypeError(...)); - return { type: 'error', message: `TypeError: Unsupported operand for -: '${value.type}'` }; - } - - case TokenType.PLUS: - switch (value.type) { - case 'number': - case 'bigint': - case 'complex': - return value; - case 'bool': - return { type: 'bigint', value: value.value ? 1n : 0n }; - default: - // handleRuntimeError(context, new UnsupportedOperandTypeError(...)); - return { type: 'error', message: `TypeError: Unsupported operand for +: '${value.type}'` }; - } - } - return { type: 'error', message: 'Unreachable unary operator' }; -} - -// Remove __py_{operators} translation stage and switch case for readability -export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, context: PyContext, operator: TokenType, left: Value, right: Value): Value { - // Helper to generate a specific TypeError message string - // TODO: remove for proper error message after ../errors/error.ts is migrated - function unsupportedOperandsMessage(): string { - const leftTypeStr = typeTranslator(left.type); - const rightTypeStr = typeTranslator(right.type); - const opStr = operandTranslator(operator); - return `TypeError: unsupported operand type(s) for ${opStr}: '${leftTypeStr}' and '${rightTypeStr}'`; - } - - // Handle Complex numbers - if (left.type === 'complex' || right.type === 'complex') { - if (right.type !== 'complex' && right.type !== 'number' && right.type !== 'bigint' && right.type !== 'bool') { - return { type: 'error', message: unsupportedOperandsMessage() }; - } - const leftComplex = PyComplexNumber.fromValue(left.value); - const rightComplex = PyComplexNumber.fromValue(right.value); - let result: PyComplexNumber; - - switch (operator) { - case TokenType.PLUS: result = leftComplex.add(rightComplex); break; - case TokenType.MINUS: result = leftComplex.sub(rightComplex); break; - case TokenType.STAR: result = leftComplex.mul(rightComplex); break; - case TokenType.SLASH: result = leftComplex.div(rightComplex); break; - case TokenType.DOUBLESTAR: result = leftComplex.pow(rightComplex); break; - case TokenType.DOUBLEEQUAL: return { type: 'bool', value: leftComplex.equals(rightComplex) }; - case TokenType.NOTEQUAL: return { type: 'bool', value: !leftComplex.equals(rightComplex) }; - default: return { type: 'error', message: `TypeError: unsupported operator for complex numbers: ${operator}` }; - } - return { type: 'complex', value: result }; - } - - // Handle comparisons with None (represented as 'undefined' type) - if (left.type === 'undefined' || right.type === 'undefined') { - switch (operator) { - case TokenType.DOUBLEEQUAL: - // True only if both are None - return { type: 'bool', value: left.type === right.type }; - case TokenType.NOTEQUAL: - return { type: 'bool', value: left.type !== right.type }; - default: - return { type: 'error', message: unsupportedOperandsMessage() }; - } - } - - // Handle string operations - if (left.type === 'string' || right.type === 'string') { - if (left.type === 'string' && right.type === 'string') { - switch (operator) { - case TokenType.PLUS: - return { type: 'string', value: left.value + right.value }; - case TokenType.DOUBLEEQUAL: - return { type: 'bool', value: left.value === right.value }; - case TokenType.NOTEQUAL: - return { type: 'bool', value: left.value !== right.value }; - case TokenType.LESS: - return { type: 'bool', value: left.value < right.value }; - case TokenType.LESSEQUAL: - return { type: 'bool', value: left.value <= right.value }; - case TokenType.GREATER: - return { type: 'bool', value: left.value > right.value }; - case TokenType.GREATEREQUAL: - return { type: 'bool', value: left.value >= right.value }; - } - } - return { type: 'error', message: unsupportedOperandsMessage() }; - } - - // Coerce boolean to a numeric value for all other arithmetic - const leftNum = left.type === 'bool' ? (left.value ? 1 : 0) : left.value; - const rightNum = right.type === 'bool' ? (right.value ? 1 : 0) : right.value; - const leftType = left.type === 'bool' ? 'number' : left.type; - const rightType = right.type === 'bool' ? 'number' : right.type; - - // Numeric Operations (number or bigint) - switch (operator) { - case TokenType.PLUS: - case TokenType.MINUS: - case TokenType.STAR: - case TokenType.SLASH: - case TokenType.DOUBLESLASH: - case TokenType.PERCENT: - case TokenType.DOUBLESTAR: - if (leftType === 'number' || rightType === 'number') { - const l = Number(leftNum); - const r = Number(rightNum); - switch (operator) { - case TokenType.PLUS: return { type: 'number', value: l + r }; - case TokenType.MINUS: return { type: 'number', value: l - r }; - case TokenType.STAR: return { type: 'number', value: l * r }; - case TokenType.SLASH: - if (r === 0) return { type: 'error', message: 'ZeroDivisionError: division by zero' }; - return { type: 'number', value: l / r }; - case TokenType.DOUBLESLASH: - if (r === 0) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; - return { type: 'number', value: Math.floor(l / r) }; - case TokenType.PERCENT: - if (r === 0) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; - return { type: 'number', value: pythonMod(l, r) }; - case TokenType.DOUBLESTAR: return { type: 'number', value: l ** r }; - } - } - if (leftType === 'bigint' && rightType === 'bigint') { - const l = leftNum as bigint; - const r = rightNum as bigint; - switch (operator) { - case TokenType.PLUS: return { type: 'bigint', value: l + r }; - case TokenType.MINUS: return { type: 'bigint', value: l - r }; - case TokenType.STAR: return { type: 'bigint', value: l * r }; - case TokenType.SLASH: - if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: division by zero' }; - return { type: 'number', value: Number(l) / Number(r) }; - case TokenType.DOUBLESLASH: - if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; - return { type: 'bigint', value: (l - (pythonMod(l, r) as bigint)) / r }; - case TokenType.PERCENT: - if (r === 0n) return { type: 'error', message: 'ZeroDivisionError: integer division or modulo by zero' }; - return { type: 'bigint', value: pythonMod(l, r) }; - case TokenType.DOUBLESTAR: - if (l === 0n && r < 0n) { - return { type: 'error', message: 'ZeroDivisionError: 0.0 cannot be raised to a negative power' }; - } - if (r < 0n) return { type: 'number', value: Number(l) ** Number(r)} - return { type: 'bigint', value: l ** r }; - } - } - break; - - // Comparison Operators - case TokenType.DOUBLEEQUAL: - case TokenType.NOTEQUAL: - case TokenType.LESS: - case TokenType.LESSEQUAL: - case TokenType.GREATER: - case TokenType.GREATEREQUAL: { - const cmp = pyCompare(left, right); - let result: boolean; - switch (operator) { - case TokenType.DOUBLEEQUAL: result = cmp === 0; break; - case TokenType.NOTEQUAL: result = cmp !== 0; break; - case TokenType.LESS: result = cmp < 0; break; - case TokenType.LESSEQUAL: result = cmp <= 0; break; - case TokenType.GREATER: result = cmp > 0; break; - case TokenType.GREATEREQUAL:result = cmp >= 0; break; - default: return { type: 'error', message: 'Unreachable code path in comparison' }; - } - return { type: 'bool', value: result }; - } - } - return { type: 'error', message: unsupportedOperandsMessage() }; -} +} \ No newline at end of file diff --git a/src/cse-machine/py_utils.ts b/src/cse-machine/py_utils.ts index 4c28a59..1e95228 100644 --- a/src/cse-machine/py_utils.ts +++ b/src/cse-machine/py_utils.ts @@ -2,13 +2,11 @@ import { PyContext } from "./py_context"; import { Value } from "./stash"; import { PyNode } from "./py_types"; import { TokenType } from "../tokens"; -import { RuntimeSourceError } from "../errors/runtimeSourceError"; +import { PyRuntimeSourceError } from "../errors/py_runtimeSourceError"; import { currentEnvironment, Environment } from "./py_environment"; -import { TypeError } from "../errors/errors"; - -export function handleRuntimeError (context: PyContext, error: RuntimeSourceError) { +export function pyHandleRuntimeError (context: PyContext, error: PyRuntimeSourceError) { context.errors.push(error); throw error; } @@ -24,28 +22,33 @@ export function typeTranslator(type: string): string { case "bool": return "bool"; case "string": - return "string"; + return "str"; case "complex": return "complex"; + case "undefined": + return "NoneType"; default: return "unknown"; } } // TODO: properly adapt for the rest, string is passed in to cater for __py_adder etc... -export function operandTranslator(operand: TokenType | string) { - if (typeof operand === 'string') { - switch (operand) { - case "__py_adder": return "+"; - case "__py_minuser": return "-"; - case "__py_multiplier": return "*"; - case "__py_divider": return "/"; - case "__py_modder": return "%"; - case "__py_powerer": return "**"; - default: return operand; - } - } - switch (operand) { +export function operatorTranslator(operator: TokenType | string) { + switch (operator) { + case TokenType.PLUS: + return '+'; + case TokenType.MINUS: + return '-'; + case TokenType.STAR: + return '*'; + case TokenType.SLASH: + return '/'; + case TokenType.DOUBLESLASH: + return '//'; + case TokenType.PERCENT: + return '%'; + case TokenType.DOUBLESTAR: + return '**'; case TokenType.LESS: return '<'; case TokenType.GREATER: @@ -60,8 +63,12 @@ export function operandTranslator(operand: TokenType | string) { return '>='; case TokenType.NOT: return 'not'; + case TokenType.AND: + return 'and'; + case TokenType.OR: + return 'or'; default: - return String(operand); + return String(operator); } } diff --git a/src/errors/py_errors.ts b/src/errors/py_errors.ts new file mode 100644 index 0000000..8f0e51d --- /dev/null +++ b/src/errors/py_errors.ts @@ -0,0 +1,339 @@ +import { ExprNS } from '../ast-types'; +import { ErrorType, SourceError, SourceLocation} from '../types' +import { PyRuntimeSourceError } from './py_runtimeSourceError'; +import { PyContext } from '../cse-machine/py_context'; +import { column } from 'mathjs'; +import { typeTranslator, operatorTranslator } from '../cse-machine/py_utils'; + +/* Searches backwards and forwards till it hits a newline */ +function getFullLine(source: string, current: number): { line: number; fullLine: string } { + let back: number = current; + let forward: number = current; + + while (back > 0 && source[back] != '\n') { + back--; + } + if (source[back] === '\n') { + back++; + } + while (forward < source.length && source[forward] != '\n') { + forward++; + } + + const line = source.slice(0, back).split('\n').length; + const fullLine = source.slice(back, forward); + + return {line, fullLine}; +} + +export function createErrorIndicator(snippet: string, errorPos: number): string { + let indicator = ""; + for (let i = 0; i < snippet.length; i++) { + indicator += (i === errorPos ? "^" : "~"); + } + return indicator; +} + +export class TypeConcatenateError extends PyRuntimeSourceError { + constructor(source: string, node: ExprNS.Expr, wrongType: string) { + super(node); + this.type = ErrorType.TYPE; + + const { line, fullLine } = getFullLine(source, node.startToken.indexInSource); + + const snippet = source.substring(node.startToken.indexInSource, node.endToken.indexInSource + node.endToken.lexeme.length); + const offset = fullLine.indexOf(snippet); + const adjustedOffset = offset >= 0 ? offset : 0; + + const errorPos = (node as any).operator.indexInSource - node.startToken.indexInSource; + const indicator = createErrorIndicator(snippet, errorPos); + const name = "TypeError"; + let hint = 'TypeError: can only concatenate str (not "' + wrongType + '") to str.'; + const suggestion = "You are trying to concatenate a string with an " + wrongType + ". To fix this, convert the " + wrongType + " to a string using str(), or ensure both operands are of the same type."; + const msg = `${name} at line ${line}\n\n ${fullLine}\n ${' '.repeat(adjustedOffset)}${indicator}\n${hint}\n${suggestion}`; + this.message = msg; + } +} + +export class UnsupportedOperandTypeError extends PyRuntimeSourceError { + constructor(source: string, node: ExprNS.Expr, wrongType1: string, wrongType2: string, operand: string) { + super(node); + this.type = ErrorType.TYPE; + + const operatorStr = operatorTranslator(operand); + const typeStr1 = typeTranslator(wrongType1); + + const { line, fullLine } = getFullLine(source, node.startToken.indexInSource); + + const snippet = source.substring(node.startToken.indexInSource, node.endToken.indexInSource + node.endToken.lexeme.length); + + const offset = fullLine.indexOf(snippet); + const adjustedOffset = offset >= 0 ? offset : 0; + + const errorPos = (node as any).operator.indexInSource - node.startToken.indexInSource; + const indicator = createErrorIndicator(snippet, errorPos); + + let hint: string; + let suggestion: string; + + if (wrongType2 === '') { + // Format for Unary operators + hint = `TypeError: bad operand type for unary ${operatorStr}: '${typeStr1}'`; + suggestion = `You are using the unary '${operatorStr}' operator on '${typeStr1}', which is not a supported type for this operation.\nMake sure the operator is of the correct type.\n`; + } else { + // Format for Binary operators + const typeStr2 = typeTranslator(wrongType2); + hint = `TypeError: unsupported operand type(s) for ${operatorStr}: '${typeStr1}' and '${typeStr2}'`; + suggestion = `You are using the '${operatorStr}' operator between '${typeStr1}' and '${typeStr2}', which are not compatible types for this operation.\nMake sure both operands are of the correct type.\n`; + } + + // Assemble the final multi-line message + this.message = `TypeError at line ${line}\n\n ${fullLine}\n ${' '.repeat(adjustedOffset)}${indicator}\n${hint}\n${suggestion}`; + } +} + +export class MissingRequiredPositionalError extends PyRuntimeSourceError { + private functionName: string; + private missingParamCnt: number; + private missingParamName: string; + + constructor(source: string, node: ExprNS.Expr, functionName: string, params: any, args: any, variadic: boolean) { + super(node); + this.type = ErrorType.TYPE; + this.functionName = functionName; + let adverb: string = "exactly"; + if (variadic) { + adverb = "at least"; + } + const index = (node as any).loc?.start?.index + ?? (node as any).srcNode?.loc?.start?.index + ?? 0; + const { line, fullLine } = getFullLine(source, index); + this.message = 'TypeError at line ' + line + '\n\n ' + fullLine + '\n'; + + if (typeof params === 'number') { + this.missingParamCnt = params; + this.missingParamName = ''; + const givenParamCnt = args.length; + if (this.missingParamCnt === 1 || this.missingParamCnt === 0) {} + const msg = `TypeError: ${this.functionName}() takes ${adverb} ${this.missingParamCnt} argument (${givenParamCnt} given) +Check the function definition of '${this.functionName}' and make sure to provide all required positional arguments in the correct order.`; + this.message += msg; + } else { + this.missingParamCnt = params.length - args.length; + const missingNames: string[] = []; + for (let i = args.length; i < params.length; i++) { + const param = params[i].name; + missingNames.push("\'"+param+"\'"); + } + this.missingParamName = this.joinWithCommasAndAnd(missingNames); + const msg = `TypeError: ${this.functionName}() missing ${this.missingParamCnt} required positional argument(s): ${this.missingParamName} +You called ${this.functionName}() without providing the required positional argument ${this.missingParamName}. Make sure to pass all required arguments when calling ${this.functionName}.`; + this.message += msg; + } + } + + private joinWithCommasAndAnd(names: string[]): string { + if (names.length === 0) { + return ''; + } else if (names.length === 1) { + return names[0]; + } else if (names.length === 2) { + return `${names[0]} and ${names[1]}`; + } else { + const last = names.pop(); + return `${names.join(', ')} and ${last}`; + } + } +} + +export class TooManyPositionalArgumentsError extends PyRuntimeSourceError { + private functionName: string; + private expectedCount: number; + private givenCount: number; + + constructor(source: string, node: ExprNS.Expr, functionName: string, params: any, args: any, variadic: boolean) { + super(node); + this.type = ErrorType.TYPE; + this.functionName = functionName; + let adverb: string = "exactly"; + if (variadic) { + adverb = "at most"; + } + + const index = (node as any).loc?.start?.index + ?? (node as any).srcNode?.loc?.start?.index + ?? 0; + const { line, fullLine } = getFullLine(source, index); + this.message = 'TypeError at line ' + line + '\n\n ' + fullLine + '\n'; + + if (typeof params === 'number') { + this.expectedCount = params; + this.givenCount = args.length; + if (this.expectedCount === 1 || this.expectedCount === 0) { + this.message += `TypeError: ${this.functionName}() takes ${adverb} ${this.expectedCount} argument (${this.givenCount} given)`; + } else { + this.message += `TypeError: ${this.functionName}() takes ${adverb} ${this.expectedCount} arguments (${this.givenCount} given)`; + } + } else { + this.expectedCount = params.length; + this.givenCount = args.length; + if (this.expectedCount === 1 || this.expectedCount === 0) { + this.message += `TypeError: ${this.functionName}() takes ${this.expectedCount} positional argument but ${this.givenCount} were given`; + } else { + this.message += `TypeError: ${this.functionName}() takes ${this.expectedCount} positional arguments but ${this.givenCount} were given`; + } + } + + this.message += `\nRemove the extra argument(s) when calling '${this.functionName}', or check if the function definition accepts more arguments.`; + } +} + +export class ZeroDivisionError extends PyRuntimeSourceError { + constructor(source: string, node: ExprNS.Expr, context: PyContext) { + super(node); + this.type = ErrorType.TYPE; + + const { line, fullLine } = getFullLine(source, node.startToken.indexInSource); + + const snippet = source.substring(node.startToken.indexInSource, node.endToken.indexInSource + node.endToken.lexeme.length); + const offset = fullLine.indexOf(snippet); + const adjustedOffset = offset >= 0 ? offset : 0; + + const errorPos = (node as any).operator.indexInSource - node.startToken.indexInSource; + const indicator = createErrorIndicator(snippet, errorPos); + const name = "ZeroDivisionError"; + const operator = (node as any).operator.lexeme; + let hint: string; + + switch (operator) { + case '/': + hint = 'ZeroDivisionError: division by zero.'; + break; + case '//': + hint = 'ZeroDivisionError: integer division or modulo by zero.'; + break; + case '%': + hint = 'ZeroDivisionError: integer modulo by zero.'; + break; + case '**': + hint = 'ZeroDivisionError: 0.0 cannot be raised to a negative power.'; + break; + default: + hint = 'ZeroDivisionError: division by zero.'; + } + const suggestion = "You attempted to divide by zero. Division or modulo operations cannot be performed with a divisor of zero. Please ensure that the divisor is non-zero before performing the operation."; + const msg = `${name} at line ${line}\n\n ${fullLine}\n ${' '.repeat(adjustedOffset)}${indicator}\n${hint}\n${suggestion}`; + this.message = msg; + } +} + +// export class StepLimitExceededError extends PyRuntimeSourceError { +// constructor(source: string, node: ExprNS.Expr, context: PyContext) { +// super(node); +// this.type = ErrorType.RUNTIME; + +// const index = (node as any).loc?.start?.index +// ?? (node as any).srcNode?.loc?.start?.index +// ?? 0; + +// const { line, fullLine } = getFullLine(source, index); + +// const snippet = (node as any).loc?.source +// ?? (node as any).srcNode?.loc?.source +// ?? ''; + +// const indicator = createErrorIndicator(fullLine, '@'); // no target symbol + +// const name = 'StepLimitExceededError'; +// const hint = 'The evaluation has exceeded the maximum step limit.'; + +// const offset = fullLine.indexOf(fullLine); +// const adjustedOffset = offset >= 0 ? offset : 0; + +// const msg = [ +// `${name} at line ${line}`, +// '', +// ' ' + fullLine, +// ' ' + ' '.repeat(adjustedOffset) + indicator, +// hint +// ].join('\n'); + +// this.message = msg; +// } +// } + +// export class ValueError extends PyRuntimeSourceError { +// constructor(source: string, node: ExprNS.Expr, context: PyContext, functionName: string) { +// super(node); +// this.type = ErrorType.TYPE; +// const index = (node as any).loc?.start?.index +// ?? (node as any).srcNode?.loc?.start?.index +// ?? 0; +// const { line, fullLine } = getFullLine(source, index); +// const snippet = (node as any).loc?.source +// ?? (node as any).srcNode?.loc?.source +// ?? ''; +// let hint = 'ValueError: math domain error. '; +// const offset = fullLine.indexOf(snippet); +// const indicator = createErrorIndicator(snippet, '@'); +// const name = "ValueError"; +// const suggestion = `Ensure that the input value(s) passed to '${functionName}' satisfy the mathematical requirements`; +// const msg = name + " at line " + line + "\n\n " + fullLine + "\n " + " ".repeat(offset) + indicator + "\n" + hint + suggestion; +// this.message = msg; +// } +// } + +// export class TypeError extends PyRuntimeSourceError { +// constructor(source: string, node: ExprNS.Expr, context: PyContext, originalType: string, targetType: string) { +// super(node); +// originalType = typeTranslator(originalType); +// this.type = ErrorType.TYPE; +// const index = (node as any).loc?.start?.index +// ?? (node as any).srcNode?.loc?.start?.index +// ?? 0; +// const { line, fullLine } = getFullLine(source, index); +// const snippet = (node as any).loc?.source +// ?? (node as any).srcNode?.loc?.source +// ?? ''; +// let hint = "TypeError: '" + originalType + "' cannot be interpreted as an '" + targetType + "'."; +// const offset = fullLine.indexOf(snippet); +// const adjustedOffset = offset >= 0 ? offset : 0; +// const indicator = createErrorIndicator(snippet, '@'); +// const name = "TypeError"; +// const suggestion = ' Make sure the value you are passing is compatible with the expected type.'; +// const msg = name + " at line " + line + "\n\n " + fullLine + "\n " + " ".repeat(adjustedOffset) + indicator + "\n" + hint + suggestion; +// this.message = msg; +// } +// } + +// export class SublanguageError extends PyRuntimeSourceError { +// constructor ( +// source: string, +// node: ExprNS.Expr, +// context: PyContext, +// functionName: string, +// chapter: string, +// details?: string +// ) { +// super(node) + +// this.type = ErrorType.TYPE + +// const index = (node as any).loc?.start?.index +// ?? (node as any).srcNode?.loc?.start?.index +// ?? 0 +// const { line, fullLine } = getFullLine(source, index) +// const snippet = (node as any).loc?.source +// ?? (node as any).srcNode?.loc?.source +// ?? '' +// const offset = fullLine.indexOf(snippet) +// const indicator = createErrorIndicator(snippet, '@') + +// const name = 'SublanguageError' +// const hint = 'Feature not supported in Python §' + chapter + '. ' +// const suggestion = `The call to '${functionName}()' relies on behaviour that is valid in full Python but outside the Python §1 sublanguage${details ? ': ' + details : ''}.` + +// this.message = `${name} at line ${line}\n\n ${fullLine}\n ${' '.repeat(offset)}${indicator}\n${hint}${suggestion}` +// } +// } diff --git a/src/errors/py_runtimeSourceError.ts b/src/errors/py_runtimeSourceError.ts new file mode 100644 index 0000000..43f5139 --- /dev/null +++ b/src/errors/py_runtimeSourceError.ts @@ -0,0 +1,54 @@ +import { ErrorSeverity, ErrorType, SourceError, SourceLocation } from '../types' +import { Token } from '../tokenizer'; + +// todo +// just put on here temporarily +export const UNKNOWN_LOCATION: SourceLocation = { + start: { + line: -1, + column: -1 + }, + end: { + line: -1, + column: -1 + } +} + +interface Locatable { + startToken: Token; + endToken: Token; +} + +export abstract class PyRuntimeSourceError implements SourceError { + public type: ErrorType = ErrorType.RUNTIME; + public severity: ErrorSeverity = ErrorSeverity.ERROR; + public location: SourceLocation; + public message = 'Unknown runtime error has occured'; + + constructor(node?: Locatable) { + if (node) { + this.location = { + start: { + line: node.startToken.line, + column: node.startToken.col, + }, + end: { + line: node.startToken.line, + column: node.startToken.col, + } + }; + }else { + this.location = UNKNOWN_LOCATION; + } + } + + public explain() { + return '' + } + + public elaborate() { + return this.explain() + } +} + + diff --git a/src/index.ts b/src/index.ts index c262863..9a690d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -137,7 +137,7 @@ import { Resolver } from "./resolver"; import { Context } from './cse-machine/context'; export * from './errors'; import { Finished, RecursivePartial, Result } from "./types"; -import { runCSEMachine } from "./runner/pyRunner"; +// import { runCSEMachine } from "./runner/pyRunner"; import { initialise } from "./conductor/runner/util/initialise"; import { PyEvaluator } from "./conductor/runner/types/PyEvaluator"; export * from './errors'; diff --git a/src/py_stdlib.ts b/src/py_stdlib.ts new file mode 100644 index 0000000..d8b4930 --- /dev/null +++ b/src/py_stdlib.ts @@ -0,0 +1,59 @@ +import { Closure } from "./cse-machine/closure"; +import { Value } from "./cse-machine/stash"; + + +export function toPythonFloat(num: number): string { + if (Object.is(num, -0)) { + return "-0.0"; + } + if (num === 0) { + return "0.0"; + } + + if (num === Infinity) { + return "inf"; + } + if (num === -Infinity) { + return "-inf"; + } + + if (Number.isNaN(num)) { + return "nan"; + } + + if (Math.abs(num) >= 1e16 || (num !== 0 && Math.abs(num) < 1e-4)) { + return num.toExponential().replace(/e([+-])(\d)$/, 'e$10$2'); + } + if (Number.isInteger(num)) { + return num.toFixed(1).toString(); + } + return num.toString(); +} + +export function toPythonString(obj: Value): string { + let ret: any; + if ((obj as Value).type === 'bigint' || (obj as Value).type === 'complex') { + ret = (obj as Value).value.toString(); + } else if ((obj as Value).type === 'number') { + ret = toPythonFloat((obj as Value).value); + } else if ((obj as Value).type === 'bool') { + if ((obj as Value).value === true) { + return "True"; + } else { + return "False"; + } + } else if ((obj as Value).type === 'error') { + return (obj as Value).message; + } else if ((obj as unknown as Closure).node) { + for (let name in (obj as unknown as Closure).environment!.head) { + if ((obj as unknown as Closure).environment!.head[name] === obj) { + return ''; + } + } + } else if ((obj as Value) === undefined || (obj as Value).value === undefined) { + ret = 'None'; + } else { + ret = (obj as Value).value.toString(); + } + return ret; + } \ No newline at end of file diff --git a/src/runner/pyRunner.ts b/src/runner/pyRunner.ts index 208eeb7..104d11b 100644 --- a/src/runner/pyRunner.ts +++ b/src/runner/pyRunner.ts @@ -1,16 +1,16 @@ import { IOptions } from ".." -import { CSEResultPromise, evaluate } from "../cse-machine/interpreter" +// import { CSEResultPromise, evaluate } from "../cse-machine/interpreter" import { RecursivePartial, Result } from "../types" -import * as es from 'estree' +// import * as es from 'estree' import { PyEvaluate, PyCSEResultPromise } from "../cse-machine/py_interpreter" -import { Context } from "../cse-machine/context" +// import { Context } from "../cse-machine/context" import { StmtNS } from "../ast-types"; import { PyContext } from "../cse-machine/py_context" -export function runCSEMachine(code: string, program: es.Program, context: Context, options: RecursivePartial = {}): Promise { - const result = evaluate(code, program, context, options); - return CSEResultPromise(context, result); -} +// export function runCSEMachine(code: string, program: es.Program, context: Context, options: RecursivePartial = {}): Promise { +// const result = evaluate(code, program, context, options); +// return CSEResultPromise(context, result); +// } type Stmt = StmtNS.Stmt; diff --git a/src/types.ts b/src/types.ts index a58fb51..624ea3a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,3 @@ -import * as es from 'estree' import { toPythonString } from './stdlib' import { Value } from './cse-machine/stash' import { Context } from './cse-machine/context' @@ -12,6 +11,25 @@ export class CSEBreak {} // constructor(public readonly error: any) {} // } +/** + * Represents a specific position in source code + * Line is 1-based, Column is 0-based + */ +export interface SourcePosition { + line: number; + column: number; +} + +/** + * Represents the span of code within source code from start to end + * Can be null if source code is not available + */ +export interface SourceLocation { + source?: string | null; + start: SourcePosition; + end: SourcePosition; +} + export enum ErrorType { IMPORT = 'Import', RUNTIME = 'Runtime', @@ -28,7 +46,7 @@ export enum ErrorSeverity { export interface SourceError { type: ErrorType severity: ErrorSeverity - location: es.SourceLocation + location: SourceLocation explain(): string elaborate(): string } @@ -240,18 +258,15 @@ export class PyComplexNumber { } } -export interface None extends es.BaseNode { +export interface None{ type: 'NoneType'; - loc?: es.SourceLocation; + loc?: SourceLocation | null; } -export interface ComplexLiteral extends es.BaseNode { +export interface ComplexLiteral{ type: 'Literal'; - complex: { - real: number; - imag: number; - } - loc?: es.SourceLocation; + complex?: PyComplexNumber; + loc?: SourceLocation | null; } From c8cb9fb90426229e9b1202cbb85e8441cec0aa98 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 18 Sep 2025 19:49:04 +0800 Subject: [PATCH 12/16] Removed translator, py_visitor and regression.test (Refs #51) [11] * removed translator.ts, not used anymore * removed regression.test, the test is mainly for translator * removed py_visitor - failed attempt for migration --- src/cse-machine/py_visitor.ts | 222 ------------ src/index.ts | 64 ++-- src/tests/regression.test.ts | 52 --- src/tests/utils.ts | 20 +- src/translator.ts | 631 ---------------------------------- 5 files changed, 43 insertions(+), 946 deletions(-) delete mode 100644 src/cse-machine/py_visitor.ts delete mode 100644 src/tests/regression.test.ts delete mode 100644 src/translator.ts diff --git a/src/cse-machine/py_visitor.ts b/src/cse-machine/py_visitor.ts deleted file mode 100644 index e2f98ea..0000000 --- a/src/cse-machine/py_visitor.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { ExprNS, StmtNS } from "../ast-types"; -import { Context } from "./context"; -// TODO: setup py_operators -import { evaluateBinaryExpression, evaluateUnaryExpression } from "./py_operators"; -import { TokenType } from "../tokens"; -import { Token } from "../tokenizer"; -import { Value } from "./stash"; -import { PyComplexNumber } from "../types"; - -type Stmt = StmtNS.Stmt; -type Expr = ExprNS.Expr; - -// TODO: type 'any' to be changed to node type for replacement of es.Node -export class PyVisitor implements ExprNS.Visitor, StmtNS.Visitor { - private code: string; - private context: Context; - - constructor(code: string, context: Context) { - this.code = code; - this.context = context; - } - - // Main entry point - visit(node: Stmt | Expr): any { - return node.accept(this); - } - - private mapOperatorToPyOperator(operatorToken: Token): TokenType | string { - switch (operatorToken.type) { - // return string names that py_operators expect - case TokenType.PLUS: - return '__py_adder'; - case TokenType.MINUS: - return '__py_minuser'; - case TokenType.STAR: - return '__py_multiplier'; - case TokenType.SLASH: - return '__py_divider'; - case TokenType.PERCENT: - return '__py_modder'; - case TokenType.DOUBLESTAR: - return '__py_powerer'; - case TokenType.DOUBLESLASH: - return '__py_floorer'; - - // pass TokenType for comparisons and unary - case TokenType.GREATER: return TokenType.GREATER; - case TokenType.GREATEREQUAL: return TokenType.GREATEREQUAL; - case TokenType.LESS: return TokenType.LESS; - case TokenType.LESSEQUAL: return TokenType.LESSEQUAL; - case TokenType.DOUBLEEQUAL: return TokenType.DOUBLEEQUAL; - case TokenType.NOTEQUAL: return TokenType.NOTEQUAL; - case TokenType.NOT: return TokenType.NOT; - default: - throw new Error(`Unsupported operator token type for mapping: ${TokenType[operatorToken.type]}`); - } - } - - // Expression Visitors - visitLiteralExpr(expr: ExprNS.Literal): Value { - const value = expr.value; - if (typeof value == 'number') { - return { type: 'number', value: value }; - } else if (typeof value == 'boolean') { - return { type: 'bool', value: value}; - } else if (typeof value == 'string') { - return { type: 'string', value: value}; - } - // TODO to handle null, representing null as UndefinedValue - return { type: 'undefined'}; - } - - visitUnaryExpr(expr: ExprNS.Unary): Value { - const argumentValue = this.visit(expr.right); - return evaluateUnaryExpression(expr.operator.type, argumentValue, expr, this.context); - } - - visitBinaryExpr(expr: ExprNS.Binary): Value { - const leftValue = this.visit(expr.left); - const rightValue = this.visit(expr.right); - const operatorForPyOperators = this.mapOperatorToPyOperator(expr.operator); - - return evaluateBinaryExpression( - this.code, - expr, - this.context, - operatorForPyOperators, - leftValue, - rightValue, - ) - } - - visitBigIntLiteralExpr(expr: ExprNS.BigIntLiteral): Value { - return { - type: 'bigint', - value: BigInt(expr.value) - }; - } - - // Placeholder for TODO expr visitors - // To test on multiple comparisons, eg, a < b < c - visitCompareExpr(expr: ExprNS.Compare): Value { - const leftValue = this.visit(expr.left); - const rightValue = this.visit(expr.right); - const operatorToken = expr.operator; - - const operatorForEval = this.mapOperatorToPyOperator(operatorToken); - - return evaluateBinaryExpression( - this.code, - expr, - this.context, - operatorForEval, - leftValue, - rightValue, - ); - } - - visitBoolOpExpr(expr: ExprNS.BoolOp): Value { - const leftValue = this.visit(expr.left); - // Handle 'or' short-circuiting - if (expr.operator.type === TokenType.OR) { - let isTruthy = true; - if (leftValue.type === 'bool' && !leftValue.value) isTruthy = false; - if (leftValue.type === 'bigint' && leftValue.value === 0n) isTruthy = false; - if (leftValue.type === 'number' && leftValue.value === 0) isTruthy = false; - if (leftValue.type === 'string' && leftValue.value === '') isTruthy = false; - if (leftValue.type === 'undefined') isTruthy = false; - - if (isTruthy) { - return leftValue; - } else { - return this.visit(expr.right); - } - } - // Handle 'and' short-circuiting - if (expr.operator.type === TokenType.AND) { - let isFalsy = false; - if (leftValue.type === 'bool' && !leftValue.value) isFalsy = true; - if (leftValue.type === 'bigint' && leftValue.value === 0n) isFalsy = true; - if (leftValue.type === 'number' && leftValue.value === 0) isFalsy = true; - if (leftValue.type === 'string' && leftValue.value === '') isFalsy = true; - if (leftValue.type === 'undefined') isFalsy = true; - - if (isFalsy) { - return leftValue; - } else { - return this.visit(expr.right); - } - } - return { type: 'error', message: 'Unsupported boolean operator' }; - } - - visitGroupingExpr(expr: ExprNS.Grouping): any { - return this.visit(expr.expression); - } - - visitTernaryExpr(expr: ExprNS.Ternary): any { /* TODO */ } - visitLambdaExpr(expr: ExprNS.Lambda): any { /* TODO */ } - visitMultiLambdaExpr(expr: ExprNS.MultiLambda): any { /* TODO */ } - - visitVariableExpr(expr: ExprNS.Variable): Value { - const name = expr.name.lexeme; - if (name === 'True') { - return { type: 'bool', value: true }; - } else if (name === 'False') { - return { type: 'bool', value: false }; - } else if (name === 'None') { - return { type: 'undefined' }; - } - // TODO: add user defined variables, for now all variables are caught as error - return { type: 'error', message: `name '${name}' is not defined` }; - } - - visitCallExpr(expr: ExprNS.Call): any { /* TODO */ } - - visitComplexExpr(expr: ExprNS.Complex): Value { - return { - type: 'complex', - value: new PyComplexNumber(expr.value.real, expr.value.imag) - }; - } - - visitNoneExpr(expr: ExprNS.None): Value { - return { type: 'undefined' }; - } - - // Statement Visitors - visitFileInputStmt(stmt: StmtNS.FileInput): Value { - let lastValue: Value = { type: 'undefined' }; - for (const statement of stmt.statements) { - lastValue = this.visit(statement); - } - return lastValue; - } - - visitSimpleExprStmt(stmt: StmtNS.SimpleExpr): any { - return this.visit(stmt.expression); - } - - - // Placeholder for TODO stmt visitors - visitIndentCreation(stmt: StmtNS.Indent): any { /* TODO */ } - visitDedentCreation(stmt: StmtNS.Dedent): any { /* TODO */ } - visitPassStmt(stmt: StmtNS.Pass): any { /* TODO */ } - visitAssignStmt(stmt: StmtNS.Assign): any { /* TODO */ } - visitAnnAssignStmt(stmt: StmtNS.AnnAssign): any { /* TODO */ } - visitBreakStmt(stmt: StmtNS.Break): any { /* TODO */ } - visitContinueStmt(stmt: StmtNS.Continue): any { /* TODO */ } - visitReturnStmt(stmt: StmtNS.Return): any { /* TODO */ } - visitFromImportStmt(stmt: StmtNS.FromImport): any { /* TODO */ } - visitGlobalStmt(stmt: StmtNS.Global): any { /* TODO */ } - visitNonLocalStmt(stmt: StmtNS.NonLocal): any { /* TODO */ } - visitAssertStmt(stmt: StmtNS.Assert): any { /* TODO */ } - visitIfStmt(stmt: StmtNS.If): any { /* TODO */ } - visitWhileStmt(stmt: StmtNS.While): any { /* TODO */ } - visitForStmt(stmt: StmtNS.For): any { /* TODO */ } - visitFunctionDefStmt(stmt: StmtNS.FunctionDef): any { /* TODO */ } - - - -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9a690d4..082e532 100644 --- a/src/index.ts +++ b/src/index.ts @@ -129,15 +129,17 @@ /* Use as a command line script */ /* npm run start:dev -- test.py */ +// import { Translator } from "./translator"; +// import { Program } from "estree"; +// import { Resolver } from "./resolver"; +// import { Context } from './cse-machine/context'; +// import { runCSEMachine } from "./runner/pyRunner"; + import { Tokenizer } from "./tokenizer"; import { Parser } from "./parser"; -import { Translator } from "./translator"; -import { Program } from "estree"; -import { Resolver } from "./resolver"; -import { Context } from './cse-machine/context'; + export * from './errors'; import { Finished, RecursivePartial, Result } from "./types"; -// import { runCSEMachine } from "./runner/pyRunner"; import { initialise } from "./conductor/runner/util/initialise"; import { PyEvaluator } from "./conductor/runner/types/PyEvaluator"; export * from './errors'; @@ -147,20 +149,20 @@ import { PyContext } from "./cse-machine/py_context"; type Stmt = StmtNS.Stmt; -export function parsePythonToEstreeAst(code: string, - variant: number = 1, - doValidate: boolean = false): Program { - const script = code + '\n' - const tokenizer = new Tokenizer(script) - const tokens = tokenizer.scanEverything() - const pyParser = new Parser(script, tokens) - const ast = pyParser.parse() - if (doValidate) { - new Resolver(script, ast).resolve(ast); - } - const translator = new Translator(script) - return translator.resolve(ast) as unknown as Program -} +// export function parsePythonToEstreeAst(code: string, +// variant: number = 1, +// doValidate: boolean = false): Program { +// const script = code + '\n' +// const tokenizer = new Tokenizer(script) +// const tokens = tokenizer.scanEverything() +// const pyParser = new Parser(script, tokens) +// const ast = pyParser.parse() +// if (doValidate) { +// new Resolver(script, ast).resolve(ast); +// } +// const translator = new Translator(script) +// return translator.resolve(ast) as unknown as Program +// } // import {ParserErrors, ResolverErrors, TokenizerErrors} from "./errors"; // import fs from "fs"; @@ -200,15 +202,15 @@ export interface IOptions { stepLimit: number }; -export async function runInContext( - code: string, - context: Context, - options: RecursivePartial = {} -): Promise { - const estreeAst = parsePythonToEstreeAst(code, 1, true); - const result = runCSEMachine(code, estreeAst, context, options); - return result; -} +// export async function runInContext( +// code: string, +// context: Context, +// options: RecursivePartial = {} +// ): Promise { +// const estreeAst = parsePythonToEstreeAst(code, 1, true); +// const result = runCSEMachine(code, estreeAst, context, options); +// return result; +// } @@ -245,7 +247,7 @@ if (require.main === module) { process.exit(1); } const options = {}; - const context = new Context(); + const context = new PyContext(); const filePath = process.argv[2]; @@ -255,10 +257,10 @@ if (require.main === module) { const code = fs.readFileSync(filePath, "utf8") + "\n"; console.log(`Parsing Python file: ${filePath}`); - const result = await runInContext(code, context, options); + const result = await PyRunInContext(code, context, options); console.info(result); console.info((result as Finished).value); - console.info((result as Finished).representation.toString((result as Finished).value)); + console.info((result as Finished).representation.toString()); } catch (e) { console.error("Error:", e); diff --git a/src/tests/regression.test.ts b/src/tests/regression.test.ts deleted file mode 100644 index 8107c05..0000000 --- a/src/tests/regression.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { toEstreeAST, toEstreeAstAndResolve } from "./utils"; - -describe('Regression tests for py-slang', () => { - test('Issue #2', () => { - const text = ` -def foo(): - pass - - pass -`; - toEstreeAST(text); - }) - test('Issue #5', () => { - const text = ` -print("hi") - -print("world") -`; - toEstreeAST(text); - }) - test('Issue #3', () => { - const text = ` -def foo( - a, - b -): - pass - - pass -`; - toEstreeAST(text); - }) - test('Issue #9', () => { - const text = ` -add_one = lambda : None -add_one = lambda : True -add_one = lambda : False -`; - toEstreeAST(text); - }) - - test('Issue #35', () => { - const text = ` -def f(): - return g() - -def g(): - return 3 -`; - toEstreeAstAndResolve(text); - }) -}) \ No newline at end of file diff --git a/src/tests/utils.ts b/src/tests/utils.ts index 8850ddf..ab23bd4 100644 --- a/src/tests/utils.ts +++ b/src/tests/utils.ts @@ -6,7 +6,7 @@ import { import {Tokenizer} from '../tokenizer'; import {Parser} from '../parser'; import {Resolver} from '../resolver'; -import {Translator} from '../translator'; +// import {Translator} from '../translator'; import {StmtNS} from "../ast-types"; import Stmt = StmtNS.Stmt; @@ -26,13 +26,13 @@ export function toPythonAstAndResolve(text: string): Stmt { return ast; } -export function toEstreeAST(text: string): Expression | Statement { - const ast = toPythonAst(text); - return new Translator(text).resolve(ast); -} +// export function toEstreeAST(text: string): Expression | Statement { +// const ast = toPythonAst(text); +// return new Translator(text).resolve(ast); +// } -export function toEstreeAstAndResolve(text: string): Expression | Statement { - const ast = toPythonAst(text); - new Resolver(text, ast).resolve(ast); - return new Translator(text).resolve(ast); -} \ No newline at end of file +// export function toEstreeAstAndResolve(text: string): Expression | Statement { +// const ast = toPythonAst(text); +// new Resolver(text, ast).resolve(ast); +// return new Translator(text).resolve(ast); +// } \ No newline at end of file diff --git a/src/translator.ts b/src/translator.ts deleted file mode 100644 index f7a7d24..0000000 --- a/src/translator.ts +++ /dev/null @@ -1,631 +0,0 @@ -/* -* Translate our AST to estree AST (Source's AST) -* */ - -import { StmtNS, ExprNS } from "./ast-types"; - -type Expr = ExprNS.Expr; -type Stmt = StmtNS.Stmt; -import { Token } from "./tokenizer"; -import { TokenType } from "./tokens"; - -import { - ArrowFunctionExpression, - AssignmentExpression, - BaseNode, - BigIntLiteral, - BinaryExpression, - BinaryOperator, - BlockStatement, - BreakStatement, - CallExpression, - ConditionalExpression, - ContinueStatement, - EmptyStatement, - Expression, - ExpressionStatement, - FunctionDeclaration, - Identifier, - IfStatement, - ImportDeclaration, ImportSpecifier, - LogicalExpression, - LogicalOperator, - Program, - ReturnStatement, - SimpleLiteral, - Statement, - UnaryExpression, - UnaryOperator, - VariableDeclaration, - VariableDeclarator, - WhileStatement -} from "estree"; -import { TranslatorErrors } from "./errors"; -import { ComplexLiteral, None } from "./types"; -// import { isEmpty } from "lodash"; - -export interface EstreePosition { - line: number; - column: number; - index: number; -} - -export interface EstreeLocation { - source: string, - start: EstreePosition; - end: EstreePosition; -} - -export class Translator implements StmtNS.Visitor, ExprNS.Visitor { - private readonly source: string - - constructor(source: string) { - this.source = source; - } - - private tokenToEstreeLocation(token: Token): EstreeLocation { - // Convert zero-based to one-based. - const line = token.line + 1; - const start: EstreePosition = { - line, - column: token.col - token.lexeme.length, - index: token.indexInSource - }; - const end: EstreePosition = { - line, - column: token.col, - index: token.indexInSource - } - const source: string = token.lexeme; - return { source, start, end }; - } - - private toEstreeLocation(stmt: Stmt | Expr): EstreeLocation { - const start: EstreePosition = { - // Convert zero-based to one-based. - line: stmt.startToken.line + 1, - column: stmt.startToken.col - stmt.startToken.lexeme.length, - index: stmt.startToken.indexInSource - }; - const end: EstreePosition = { - // Convert zero-based to one-based. - line: stmt.endToken.line + 1, - column: stmt.endToken.col, - index: stmt.startToken.indexInSource - } - const source: string = this.source.slice(stmt.startToken.indexInSource, - stmt.endToken.indexInSource + stmt.endToken.lexeme.length); - return { source, start, end }; - } - - resolve(stmt: Stmt | Expr): Statement | Expression { - return stmt.accept(this); - } - - // Ugly, but just to support proper typing - resolveStmt(stmt: Stmt) { - return stmt.accept(this); - } - - resolveManyStmt(stmts: Stmt[]): Statement[] { - const res = []; - for (const stmt of stmts) { - res.push(this.resolveStmt(stmt)) - } - return res; - } - - resolveExpr(expr: Expr) { - return expr.accept(this); - } - - resolveManyExpr(exprs: Expr[]) { - const res = []; - for (const expr of exprs) { - res.push(this.resolveExpr(expr)) - } - return res; - } - - - // Converts our internal identifier to estree identifier. - private rawStringToIdentifier(name: string, stmtOrExpr: Stmt | Expr): Identifier { - const keywords = new Set(['abstract', 'arguments', 'await', 'boolean', 'byte', - 'case', 'catch', 'char', 'const', 'debugger', 'default', 'delete', 'do', 'double', 'enum', - 'eval', 'export', 'extends', 'false', 'final', 'float', 'function', 'goto', 'implements', - 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 'null', 'package', - 'private', 'protected', 'public', 'short', 'static', 'super', 'switch', 'synchronized', 'this', - 'throw', 'throws', 'transient', 'true', 'typeof', 'var', 'void', 'volatile']) - - return { - type: 'Identifier', - name: keywords.has(name) ? '$' + name : name, - loc: this.toEstreeLocation(stmtOrExpr), - }; - } - - // Token to estree identifier. - private convertToIdentifier(name: Token): Identifier { - const keywords = new Set(['abstract', 'arguments', 'await', 'boolean', 'byte', - 'case', 'catch', 'char', 'const', 'debugger', 'default', 'delete', 'do', 'double', 'enum', - 'eval', 'export', 'extends', 'false', 'final', 'float', 'function', 'goto', 'implements', - 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 'null', 'package', - 'private', 'protected', 'public', 'short', 'static', 'super', 'switch', 'synchronized', 'this', - 'throw', 'throws', 'transient', 'true', 'typeof', 'var', 'void', 'volatile']) - - return { - type: 'Identifier', - name: keywords.has(name.lexeme) ? '$' + name.lexeme : name.lexeme, - loc: this.tokenToEstreeLocation(name), - }; - } - - private convertToIdentifiers(names: Token[]): Identifier[] { - return names.map(name => this.convertToIdentifier(name)); - } - - // private convertToExpressionStatement(expr: Expression): ExpressionStatement { - // return { - // type: 'ExpressionStatement', - // expression: expr, - // // loc: this.toEstreeLocation(), - // } - // } - - // private converTokenstoDecls(varDecls: Token[]): VariableDeclaration { - // return { - // type: 'VariableDeclaration', - // declarations: varDecls?.map((token): VariableDeclarator => { - // return { - // type: 'VariableDeclarator', - // id: this.convertToIdentifier(token), - // loc: this.tokenToEstreeLocation(token), - // } - // }), - // kind: 'var', - // loc: this.toEstreeLocation(), - // }; - // } - - // Wraps an array of statements to a block. - // WARNING: THIS CREATES A NEW BLOCK IN - // JS AST. THIS ALSO MEANS A NEW NAMESPACE. BE CAREFUL! - private wrapInBlock(stmt: Stmt, stmts: StmtNS.Stmt[]): BlockStatement { - return { - type: 'BlockStatement', - body: this.resolveManyStmt(stmts), - loc: this.toEstreeLocation(stmt), - }; - } - - //// STATEMENTS - - visitFileInputStmt(stmt: StmtNS.FileInput): Program { - const newBody = this.resolveManyStmt(stmt.statements); - // if (stmt.varDecls !== null && stmt.varDecls.length > 0) { - // const decls = this.converTokenstoDecls(stmt.varDecls); - // newBody.unshift(decls); - // } - return { - type: 'Program', - sourceType: 'module', - body: newBody, - loc: this.toEstreeLocation(stmt), - }; - } - - visitIndentCreation(stmt: StmtNS.Indent): EmptyStatement { - return { - type: 'EmptyStatement', - loc: this.toEstreeLocation(stmt), - }; - } - - visitDedentCreation(stmt: StmtNS.Dedent): EmptyStatement { - return { - type: 'EmptyStatement', - loc: this.toEstreeLocation(stmt), - }; - } - - visitFunctionDefStmt(stmt: StmtNS.FunctionDef): FunctionDeclaration { - const newBody = this.resolveManyStmt(stmt.body); - // if (stmt.varDecls !== null && stmt.varDecls.length > 0) { - // const decls = this.converTokenstoDecls(stmt.varDecls); - // newBody.unshift(decls); - // } - return { - type: 'FunctionDeclaration', - id: this.convertToIdentifier(stmt.name), - params: this.convertToIdentifiers(stmt.parameters), - body: { - type: 'BlockStatement', - body: newBody, - }, - loc: this.toEstreeLocation(stmt), - }; - } - - visitAnnAssignStmt(stmt: StmtNS.AnnAssign): AssignmentExpression { - return { - type: 'AssignmentExpression', - // We only have one type of assignment in restricted Python. - operator: '=', - left: this.convertToIdentifier(stmt.name), - right: this.resolveExpr(stmt.value), - loc: this.toEstreeLocation(stmt), - }; - } - - // Note: assignments are expressions in JS. - visitAssignStmt(stmt: StmtNS.Assign): VariableDeclaration { - // return this.convertToExpressionStatement({ - // type: 'AssignmentExpression', - // // We only have one type of assignment in restricted Python. - // operator: '=', - // left: this.convertToIdentifier(stmt.name), - // right: this.resolveExpr(stmt.value), - // loc: this.toEstreeLocation(stmt), - // }) - const declaration: VariableDeclarator = { - type: 'VariableDeclarator', - id: this.convertToIdentifier(stmt.name), - loc: this.tokenToEstreeLocation(stmt.name), - init: this.resolveExpr(stmt.value), - } - return { - type: 'VariableDeclaration', - declarations: [declaration], - // Note: we abuse the fact that var is function and module scoped - // which is exactly the same as how Python assignments are scoped! - kind: 'var', - loc: this.toEstreeLocation(stmt), - }; - } - - // Convert to source's built-in assert function. - visitAssertStmt(stmt: StmtNS.Assert): CallExpression { - return { - type: 'CallExpression', - optional: false, - callee: this.rawStringToIdentifier('assert', stmt), - arguments: [this.resolveExpr(stmt.value)], - // @TODO, this needs to come after callee - loc: this.toEstreeLocation(stmt), - } - } - - // @TODO decide how to do for loops - // For now, empty block - visitForStmt(stmt: StmtNS.For): EmptyStatement { - return { - type: 'EmptyStatement', - loc: this.toEstreeLocation(stmt), - }; - } - - visitIfStmt(stmt: StmtNS.If): IfStatement { - return { - type: 'IfStatement', - test: this.resolveExpr(stmt.condition), - consequent: this.wrapInBlock(stmt, stmt.body), - alternate: stmt.elseBlock !== null ? this.wrapInBlock(stmt, stmt.elseBlock) : null, - loc: this.toEstreeLocation(stmt), - }; - } - - visitGlobalStmt(stmt: StmtNS.Global): EmptyStatement { - return { - type: 'EmptyStatement', - loc: this.toEstreeLocation(stmt), - }; - } - - visitNonLocalStmt(stmt: StmtNS.NonLocal): EmptyStatement { - return { - type: 'EmptyStatement', - loc: this.toEstreeLocation(stmt), - }; - } - - visitReturnStmt(stmt: StmtNS.Return): ReturnStatement { - return { - type: 'ReturnStatement', - argument: stmt.value == null ? null : this.resolveExpr(stmt.value), - loc: this.toEstreeLocation(stmt), - }; - } - - visitWhileStmt(stmt: StmtNS.While): WhileStatement { - return { - type: 'WhileStatement', - test: this.resolveExpr(stmt.condition), - body: this.wrapInBlock(stmt, stmt.body), - loc: this.toEstreeLocation(stmt), - } - } - - visitSimpleExprStmt(stmt: StmtNS.SimpleExpr): ExpressionStatement { - return { - type: 'ExpressionStatement', - expression: this.resolveExpr(stmt.expression), - loc: this.toEstreeLocation(stmt), - } - } - - // @TODO - visitFromImportStmt(stmt: StmtNS.FromImport): ImportDeclaration { - const specifiers: ImportSpecifier[] = stmt.names.map(name => { - const ident = this.convertToIdentifier(name); - return { - type: 'ImportSpecifier', - imported: ident, - local: ident, - } - }); - return { - type: 'ImportDeclaration', - specifiers: specifiers, - source: { - type: 'Literal', - value: stmt.module.lexeme, - loc: this.tokenToEstreeLocation(stmt.module) - }, - attributes: [] - } - } - - visitContinueStmt(stmt: StmtNS.Continue): ContinueStatement { - return { - type: 'ContinueStatement', - loc: this.toEstreeLocation(stmt), - } - } - - visitBreakStmt(stmt: StmtNS.Break): BreakStatement { - return { - type: 'BreakStatement', - loc: this.toEstreeLocation(stmt), - } - } - - visitPassStmt(stmt: StmtNS.Pass): EmptyStatement { - return { - type: 'EmptyStatement', - loc: this.toEstreeLocation(stmt), - } - } - - - //// EXPRESSIONS - visitVariableExpr(expr: ExprNS.Variable): Identifier { - return this.convertToIdentifier(expr.name); - } - - visitLambdaExpr(expr: ExprNS.Lambda): ArrowFunctionExpression { - return { - type: 'ArrowFunctionExpression', - expression: true, - params: this.convertToIdentifiers(expr.parameters), - body: this.resolveExpr(expr.body), - loc: this.toEstreeLocation(expr), - } - } - - // disabled for now - visitMultiLambdaExpr(expr: ExprNS.MultiLambda): EmptyStatement { - return { - type: 'EmptyStatement', - loc: this.toEstreeLocation(expr), - } - } - - visitUnaryExpr(expr: ExprNS.Unary): UnaryExpression | CallExpression { - const op = expr.operator.type; - let res: UnaryOperator = '-'; - let plus = false; - switch (op) { - case TokenType.NOT: - res = '!' - break; - case TokenType.PLUS: - res = '+'; - plus = true; - break; - case TokenType.MINUS: - res = '-' - break; - default: - throw new Error("Unreachable code path in translator"); - } - if (plus) { - return { - type: 'CallExpression', - optional: false, - callee: { - type: 'Identifier', - name: '__py_unary_plus', - loc: this.toEstreeLocation(expr), - }, - arguments: [this.resolveExpr(expr.right)], - loc: this.toEstreeLocation(expr), - } - } - return { - type: 'UnaryExpression', - // To satisfy the type checker. - operator: res, - prefix: true, - argument: this.resolveExpr(expr.right), - loc: this.toEstreeLocation(expr), - } - } - - visitGroupingExpr(expr: ExprNS.Grouping): Expression { - return this.resolveExpr(expr.expression); - } - - visitBinaryExpr(expr: ExprNS.Binary): CallExpression { - const op = expr.operator.type; - let res = ''; - // To make the type checker happy. - switch (op) { - case TokenType.PLUS: - res = '__py_adder'; - break; - case TokenType.MINUS: - res = '__py_minuser'; - break; - case TokenType.STAR: - res = '__py_multiplier'; - break; - case TokenType.SLASH: - res = '__py_divider'; - break; - case TokenType.PERCENT: - res = '__py_modder'; - break; - // @TODO double slash and power needs to convert to math exponent/floor divide - case TokenType.DOUBLESLASH: - res = '__py_floorer'; - break; - case TokenType.DOUBLESTAR: - res = '__py_powerer'; - break; - default: - throw new Error("Unreachable binary code path in translator"); - } - return { - type: 'CallExpression', - optional: false, - callee: { - type: 'Identifier', - name: res, - loc: this.toEstreeLocation(expr), - }, - arguments: [this.resolveExpr(expr.left), this.resolveExpr(expr.right)], - loc: this.toEstreeLocation(expr), - } - } - - visitCompareExpr(expr: ExprNS.Compare): BinaryExpression { - const op = expr.operator.type; - let res: BinaryOperator = '+'; - // To make the type checker happy. - switch (op) { - case TokenType.LESS: - res = '<'; - break; - case TokenType.GREATER: - res = '>'; - break; - case TokenType.DOUBLEEQUAL: - res = '==='; - break; - case TokenType.GREATEREQUAL: - res = '>='; - break; - case TokenType.LESSEQUAL: - res = '<='; - break; - case TokenType.NOTEQUAL: - res = '!=='; - break; - // @TODO we need to convert these to builtin function applications. - case TokenType.IS: - case TokenType.ISNOT: - case TokenType.IN: - case TokenType.NOTIN: - throw new TranslatorErrors.UnsupportedOperator(expr.operator.line, expr.operator.col, this.source, expr.operator.indexInSource); - default: - throw new Error("Unreachable binary code path in translator"); - } - return { - type: 'BinaryExpression', - operator: res, - left: this.resolveExpr(expr.left), - right: this.resolveExpr(expr.right), - loc: this.toEstreeLocation(expr), - } - } - - visitBoolOpExpr(expr: ExprNS.BoolOp): LogicalExpression { - const op = expr.operator.type; - let res: LogicalOperator = '||'; - // To make the type checker happy. - switch (op) { - case TokenType.AND: - res = '&&'; - break; - case TokenType.OR: - res = '||'; - break; - default: - throw new Error("Unreachable binary code path in translator"); - } - return { - type: 'LogicalExpression', - operator: res, - left: this.resolveExpr(expr.left), - right: this.resolveExpr(expr.right), - loc: this.toEstreeLocation(expr), - } - } - - visitCallExpr(expr: ExprNS.Call): CallExpression { - return { - type: 'CallExpression', - optional: false, - callee: this.resolveExpr(expr.callee), - arguments: this.resolveManyExpr(expr.args), - loc: this.toEstreeLocation(expr), - } - } - - visitTernaryExpr(expr: ExprNS.Ternary): ConditionalExpression { - return { - type: 'ConditionalExpression', - test: this.resolveExpr(expr.predicate), - alternate: this.resolveExpr(expr.alternative), - consequent: this.resolveExpr(expr.consequent), - loc: this.toEstreeLocation(expr), - } - } - - visitLiteralExpr(expr: ExprNS.Literal): SimpleLiteral { - return { - type: 'Literal', - value: expr.value, - loc: this.toEstreeLocation(expr), - } - } - - visitBigIntLiteralExpr(expr: ExprNS.BigIntLiteral): BigIntLiteral { - return { - type: 'Literal', - bigint: expr.value, - loc: this.toEstreeLocation(expr), - } - } - - visitNoneExpr(expr: ExprNS.None): None { - return { - type: 'NoneType', - loc: this.toEstreeLocation(expr) - } - } - - visitComplexExpr(expr: ExprNS.Complex): ComplexLiteral { - return { - type: 'Literal', - - complex: { - real: expr.value.real, - imag: expr.value.imag - }, - - loc: this.toEstreeLocation(expr), - } - } - -} \ No newline at end of file From 37944cb20202c112348709a5aad9f73d9c735772 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 19 Sep 2025 11:38:01 +0800 Subject: [PATCH 13/16] Migrated environment and closure (Refs #51)[12] * new files: py_closure * migrated environment and closure * added instructions for functiondef and lambda --- src/cse-machine/py_closure.ts | 66 ++++++++++++++++ src/cse-machine/py_context.ts | 18 ++--- src/cse-machine/py_control.ts | 1 + src/cse-machine/py_environment.ts | 74 ++++++++---------- src/cse-machine/py_instrCreator.ts | 9 ++- src/cse-machine/py_interpreter.ts | 121 +++++++++++++++++++++++++++-- src/cse-machine/py_types.ts | 41 ++++++++-- src/cse-machine/py_utils.ts | 4 +- 8 files changed, 262 insertions(+), 72 deletions(-) create mode 100644 src/cse-machine/py_closure.ts diff --git a/src/cse-machine/py_closure.ts b/src/cse-machine/py_closure.ts new file mode 100644 index 0000000..dd4eecf --- /dev/null +++ b/src/cse-machine/py_closure.ts @@ -0,0 +1,66 @@ +import { StmtNS, ExprNS } from '../ast-types' +import { PyEnvironment, uniqueId } from './py_environment' +import { PyContext } from './py_context' +import { StatementSequence } from './py_types' +import { PyControlItem } from './py_control' + +/** + * Represents a python closure, the class is a runtime representation of a function. + * Bundles the function's code (AST node) with environment in which its defined. + * When Closure is called, a new environment will be created whose parent is the 'Environment' captured + */ +export class PyClosure { + public readonly id: string + /** AST node for function, either a 'def' or 'lambda' */ + public node: StmtNS.FunctionDef | ExprNS.Lambda; + /** Environment captures at time of function's definition, key for lexical scoping */ + public environment: PyEnvironment; + public context: PyContext; + public readonly predefined: boolean; + public originalNode?: StmtNS.FunctionDef | ExprNS.Lambda; + + constructor( + node: StmtNS.FunctionDef | ExprNS.Lambda, + environment: PyEnvironment, + context: PyContext, + predefined: boolean = false +) { + this.id = uniqueId(context); + this.node = node; + this.environment = environment; + this.context = context; + this.predefined = predefined; + this.originalNode = node; + } + + /** + * Creates closure for FunctionDef + */ + static makeFromFunctionDef ( + node: StmtNS.FunctionDef, + environment: PyEnvironment, + context: PyContext + ): PyClosure { + const closure = new PyClosure(node, environment, context); + return closure; + } + + /** + * Creates closure for Lambda + */ + static makeFromLambda( + node: ExprNS.Lambda, + environment: PyEnvironment, + context: PyContext + ): PyClosure { + const closure = new PyClosure(node, environment, context); + return closure; + } + } + +/** +* Type guard to check if a control item is a StatementSequence. +*/ +export const isStatementSequence = (node: PyControlItem): node is StatementSequence => { + return (node as StatementSequence).type === 'StatementSequence'; +}; \ No newline at end of file diff --git a/src/cse-machine/py_context.ts b/src/cse-machine/py_context.ts index bc6fae9..1e10975 100644 --- a/src/cse-machine/py_context.ts +++ b/src/cse-machine/py_context.ts @@ -1,6 +1,6 @@ import { Stash, Value } from './stash'; import { PyControl, PyControlItem } from './py_control'; -import { createSimpleEnvironment, createProgramEnvironment, Environment } from './py_environment'; +import { createSimpleEnvironment, createProgramEnvironment, PyEnvironment } from './py_environment'; import { CseError } from './error'; import { Heap } from './heap'; import { PyNode } from './py_types'; @@ -18,7 +18,7 @@ export class PyContext { debuggerOn: boolean isRunning: boolean environmentTree: EnvTree - environments: Environment[] + environments: PyEnvironment[] nodes: PyNode[] control: PyControl | null stash: Stash | null @@ -54,7 +54,7 @@ export class PyContext { } } - createGlobalEnvironment = (): Environment => ({ + createGlobalEnvironment = (): PyEnvironment => ({ tail: null, name: 'global', head: {}, @@ -94,9 +94,9 @@ export class PyContext { return newContext; } - private copyEnvironment(env: Environment): Environment { + private copyEnvironment(env: PyEnvironment): PyEnvironment { const newTail = env.tail ? this.copyEnvironment(env.tail) : null; - const newEnv: Environment = { + const newEnv: PyEnvironment = { id: env.id, name: env.name, tail: newTail, @@ -111,13 +111,13 @@ export class PyContext { export class EnvTree { private _root: EnvTreeNode | null = null - private map = new Map() + private map = new Map() get root(): EnvTreeNode | null { return this._root } - public insert(environment: Environment): void { + public insert(environment: PyEnvironment): void { const tailEnvironment = environment.tail if (tailEnvironment === null) { if (this._root === null) { @@ -134,7 +134,7 @@ export class EnvTree { } } - public getTreeNode(environment: Environment): EnvTreeNode | undefined { + public getTreeNode(environment: PyEnvironment): EnvTreeNode | undefined { return this.map.get(environment) } } @@ -142,7 +142,7 @@ export class EnvTree { export class EnvTreeNode { private _children: EnvTreeNode[] = [] - constructor(readonly environment: Environment, public parent: EnvTreeNode | null) {} + constructor(readonly environment: PyEnvironment, public parent: EnvTreeNode | null) {} get children(): EnvTreeNode[] { return this._children diff --git a/src/cse-machine/py_control.ts b/src/cse-machine/py_control.ts index 0e86385..9c6a4eb 100644 --- a/src/cse-machine/py_control.ts +++ b/src/cse-machine/py_control.ts @@ -2,6 +2,7 @@ import { Stack } from "./stack"; import { PyNode, Instr } from "./py_types"; import { StmtNS } from "../ast-types"; import { isEnvDependent } from './utils'; // TODO +import { StatementSequence } from "./py_types"; export type PyControlItem = (PyNode | Instr) & { isEnvDependent?: boolean; diff --git a/src/cse-machine/py_environment.ts b/src/cse-machine/py_environment.ts index b691c29..1bbb056 100644 --- a/src/cse-machine/py_environment.ts +++ b/src/cse-machine/py_environment.ts @@ -1,19 +1,19 @@ import { Value } from './stash'; import { Heap } from './heap'; -// import { Closure } from './closure'; +import { PyClosure } from './py_closure'; import { PyContext } from './py_context'; import { PyNode } from './py_types'; -import { ExprNS } from '../ast-types'; +import { ExprNS, StmtNS } from '../ast-types'; export interface Frame { [name: string]: any } -export interface Environment { +export interface PyEnvironment { readonly id: string name: string - tail: Environment | null + tail: PyEnvironment | null callExpression?: ExprNS.Call; head: Frame heap: Heap @@ -24,45 +24,33 @@ export const uniqueId = (context: PyContext): string => { return `${context.runtime.objectCount++}` } -// export const createEnvironment = ( -// context: PyContext, -// closure: Closure, -// args: Value[], -// callExpression: ExprNS.Call -// ): Environment => { -// const environment: Environment = { -// // TODO: name -// name: '', -// tail: closure.environment, -// head: {}, -// heap: new Heap(), -// id: uniqueId(context), -// callExpression: { -// ...callExpression, -// //arguments: args.map(ast.primitive) -// } -// } +export const createEnvironment = ( + context: PyContext, + closure: PyClosure, + args: Value[], + callExpression: ExprNS.Call +): PyEnvironment => { + const environment: PyEnvironment = { + name: closure.node.constructor.name === 'FunctionDef' ? (closure.node as StmtNS.FunctionDef).name.lexeme: 'lambda', + tail: closure.environment, + head: {}, + heap: new Heap(), + id: uniqueId(context), + callExpression: callExpression, + } -// // console.info('closure.node.params:', closure.node.params); -// // console.info('Number of params:', closure.node.params.length); - -// closure.node.params.forEach((param, index) => { -// if (isRestElement(param)) { -// const array = args.slice(index) -// handleArrayCreation(context, array, environment) -// environment.head[(param.argument as es.Identifier).name] = array -// } else { -// environment.head[(param as es.Identifier).name] = args[index] -// } -// }) -// return environment -// } + closure.node.parameters.forEach((paramToken, index) => { + const paramName = paramToken.lexeme; + environment.head[paramName] = args[index]; + }); + return environment +} export const createSimpleEnvironment = ( context: PyContext, name: string, - tail: Environment | null = null -): Environment => { + tail: PyEnvironment | null = null +): PyEnvironment => { return { id: uniqueId(context), name, @@ -73,14 +61,14 @@ export const createSimpleEnvironment = ( }; }; -export const createProgramEnvironment = (context: PyContext, isPrelude: boolean): Environment => { +export const createProgramEnvironment = (context: PyContext, isPrelude: boolean): PyEnvironment => { return createSimpleEnvironment(context, isPrelude ? 'prelude' : 'programEnvironment'); }; export const createBlockEnvironment = ( context: PyContext, name = 'blockEnvironment' -): Environment => { +): PyEnvironment => { return { name, tail: currentEnvironment(context), @@ -97,7 +85,7 @@ export const createBlockEnvironment = ( // export const handleArrayCreation = ( // context: PyContext, // array: any[], -// envOverride?: Environment +// envOverride?: PyEnvironment // ): void => { // const environment = envOverride ?? currentEnvironment(context); // Object.defineProperties(array, { @@ -107,13 +95,13 @@ export const createBlockEnvironment = ( // environment.heap.add(array as any); // }; -export const currentEnvironment = (context: PyContext): Environment => { +export const currentEnvironment = (context: PyContext): PyEnvironment => { return context.runtime.environments[0]; }; export const popEnvironment = (context: PyContext) => context.runtime.environments.shift() -export const pushEnvironment = (context: PyContext, environment: Environment) => { +export const pushEnvironment = (context: PyContext, environment: PyEnvironment) => { context.runtime.environments.unshift(environment) context.runtime.environmentTree.insert(environment) } diff --git a/src/cse-machine/py_instrCreator.ts b/src/cse-machine/py_instrCreator.ts index 1e711b4..416f2c1 100644 --- a/src/cse-machine/py_instrCreator.ts +++ b/src/cse-machine/py_instrCreator.ts @@ -1,5 +1,5 @@ import { Environment } from "./environment"; -import { AppInstr, AssmtInstr, BinOpInstr, BranchInstr, EnvInstr, Instr, InstrType, PyNode, UnOpInstr, BoolOpInstr } from "./py_types"; +import { AppInstr, AssmtInstr, BinOpInstr, BranchInstr, EnvInstr, Instr, InstrType, PyNode, UnOpInstr, BoolOpInstr, EndOfFunctionBodyInstr } from "./py_types"; import { TokenType } from "../tokens"; export const popInstr = (srcNode: PyNode): Instr => ({ @@ -69,4 +69,9 @@ export const boolOpInstr = (symbol: TokenType, srcNode: PyNode): BoolOpInstr => instrType: InstrType.BOOL_OP, symbol, srcNode -}); \ No newline at end of file +}); + +export const endOfFunctionBodyInstr = (srcNode: PyNode): EndOfFunctionBodyInstr => ({ + instrType: InstrType.END_OF_FUNCTION_BODY, + srcNode +}) \ No newline at end of file diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index 42de5fc..fa1cfce 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -7,12 +7,14 @@ /* tslint:disable:max-classes-per-file */ import { StmtNS, ExprNS } from '../ast-types'; +import { PyClosure } from './py_closure'; import { PyContext } from './py_context'; import { PyControl, PyControlItem } from './py_control'; -import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInstr } from './py_types'; +import { createEnvironment, currentEnvironment, pushEnvironment, popEnvironment } from './py_environment'; +import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInstr, AppInstr } from './py_types'; import { Stash, Value, ErrorValue } from './stash'; import { IOptions } from '..'; -import * as instr from './py_instrCreator'; +import * as instrCreator from './py_instrCreator'; import { evaluateUnaryExpression, evaluateBinaryExpression, evaluateBoolExpression } from './py_operators'; import { TokenType } from '../tokens'; import { Token } from '../tokenizer'; @@ -20,6 +22,7 @@ import { Result, Finished, CSEBreak, Representation} from '../types'; import { toPythonString } from '../py_stdlib' import { pyGetVariable, pyDefineVariable } from './py_utils'; + type CmdEvaluator = ( code: string, command: PyControlItem, @@ -258,14 +261,14 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { 'Unary': (code, command, context, control, stash, isPrelude) => { const unary = command as ExprNS.Unary; - const op_instr = instr.unOpInstr(unary.operator.type, unary); + const op_instr = instrCreator.unOpInstr(unary.operator.type, unary); control.push(op_instr); control.push(unary.right); }, 'Binary': (code, command, context, control, stash, isPrelude) => { const binary = command as ExprNS.Binary; - const op_instr = instr.binOpInstr(binary.operator.type, binary); + const op_instr = instrCreator.binOpInstr(binary.operator.type, binary); control.push(op_instr); control.push(binary.right); control.push(binary.left); @@ -273,7 +276,7 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { 'BoolOp': (code, command, context, control, stash, isPrelude) => { const boolOp = command as ExprNS.BoolOp; - control.push(instr.boolOpInstr(boolOp.operator.type, boolOp)); + control.push(instrCreator.boolOpInstr(boolOp.operator.type, boolOp)); control.push(boolOp.right); control.push(boolOp.left); }, @@ -304,7 +307,7 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { 'Compare': (code, command, context, control, stash, isPrelude) => { const compareNode = command as ExprNS.Compare; // For now, we only handle simple, single comparisons. - const op_instr = instr.binOpInstr(compareNode.operator.type, compareNode); + const op_instr = instrCreator.binOpInstr(compareNode.operator.type, compareNode); control.push(op_instr); control.push(compareNode.right); control.push(compareNode.left); @@ -313,7 +316,7 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { 'Assign': (code, command, context, control, stash, isPrelude) => { const assignNode = command as StmtNS.Assign; - const assmtInstr = instr.assmtInstr( + const assmtInstr = instrCreator.assmtInstr( assignNode.name.lexeme, false, true, @@ -324,6 +327,63 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(assignNode.value); }, + 'Call': (code, command, context, control, stash, isPrelude) => { + const callNode = command as ExprNS.Call; + + // push application instruction, track number of arguments + control.push(instrCreator.appInstr(callNode.args.length, callNode)); + + // push arguments onto stacks in reverse order + for (let i = callNode.args.length - 1; i >= 0; i--) { + control.push(callNode.args[i]); + } + + // push function expression itself + control.push(callNode.callee); + }, + + 'FunctionDef': (code, command, context, control, stash, isPrelude) => { + const functionDefNode = command as StmtNS.FunctionDef; + + // create closure, capture function code and environment + const closure = PyClosure.makeFromFunctionDef( + functionDefNode, + currentEnvironment(context), + context + ); + // define function name in current environment and bind to new closure + pyDefineVariable(context, functionDefNode.name.lexeme, closure); + }, + + /** + * Only handles explicit return for now + * To handle implicit return None next + */ + 'Return': (code, command, context, control, stash, isPrelude) => { + const returnNode = command as StmtNS.Return; + + let head; + + while (true) { + head = control.pop(); + + // if stack is empty before RESET, break + if (!head || (('instrType' in head) && head.instrType === InstrType.RESET)) { + break; + } + } + if (head) { + control.push(head); + } + // explicit return + if (returnNode.value) { + control.push(returnNode.value); + } else { + // if just return, returns None like implicit return + stash.push({ type: 'undefined' }); + } + }, + /** * Instruction Handlers */ @@ -380,10 +440,55 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.ASSIGNMENT]: (code, command, context, control, stash, isPrelude) => { const instr = command as AssmtInstr; - const value = stash.pop(); // Get the evaluated value from the stash + // Get the evaluated value from the stash + const value = stash.pop(); if (value) { pyDefineVariable(context, instr.symbol, value); } }, + + [InstrType.APPLICATION]: (code, command, context, control, stash, isPrelude) => { + const instr = command as AppInstr; + const numOfArgs = instr.numOfArgs; + + // pop evaluated arguments from stash + const args = []; + for (let i = 0; i < numOfArgs; i++) { + args.unshift(stash.pop()); + } + + // pop closure from stash + const closure = stash.pop() as PyClosure; + + // push reset and implicit return for cleanup at end of function + control.push(instrCreator.resetInstr(instr.srcNode)); + control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode)); + + // create new function environment + const newEnv = createEnvironment(context, closure, args, instr.srcNode as ExprNS.Call); + pushEnvironment(context, newEnv); + + // push function body onto control stack + const closureNode = closure.node; + if (closureNode.constructor.name === 'FunctionDef') { + // 'def' has a body of statements (an array) + const bodyStmts = (closureNode as StmtNS.FunctionDef).body.slice().reverse(); + control.push(...bodyStmts); + } else { + // 'lambda' has a body with a single expression + const bodyExpr = (closureNode as ExprNS.Lambda).body; + control.push(bodyExpr); + } + }, + + [InstrType.RESET]: (code, command, context, control, stash, isPrelude) => { + popEnvironment(context); + }, + + [InstrType.END_OF_FUNCTION_BODY]: (code, command, context, control, stash, isPrelude) => { + // this is only reached if function runs to completion without explicit return + stash.push({ type: 'undefined' }); + }, + }; \ No newline at end of file diff --git a/src/cse-machine/py_types.ts b/src/cse-machine/py_types.ts index 5656cc4..2f9ded5 100644 --- a/src/cse-machine/py_types.ts +++ b/src/cse-machine/py_types.ts @@ -2,9 +2,19 @@ import { Environment } from './environment'; import { StmtNS, ExprNS } from '../ast-types'; import { TokenType } from '../tokens'; -export type PyNode = StmtNS.Stmt | ExprNS.Expr; +export type PyNode = StmtNS.Stmt | ExprNS.Expr | StatementSequence; + +export interface StatementSequence { + type: 'StatementSequence'; + body: StmtNS.Stmt[]; + loc?: { + start: { line: number; column: number }; + end: { line: number; column: number }; + }; + } export enum InstrType { + END_OF_FUNCTION_BODY = "EndOfFunctionBody", RESET = 'Reset', WHILE = 'While', FOR = 'For', @@ -59,26 +69,39 @@ export interface ForInstr extends BaseInstr { } export interface AssmtInstr extends BaseInstr { - symbol: string - constant: boolean - declaration: boolean + instrType: InstrType.ASSIGNMENT; + symbol: string; + constant: boolean; + declaration: boolean; } export interface UnOpInstr extends BaseInstr { - symbol: TokenType + instrType: InstrType.UNARY_OP; + symbol: TokenType; } export interface BinOpInstr extends BaseInstr { - symbol: TokenType + instrType: InstrType.BINARY_OP; + symbol: TokenType; } export interface BoolOpInstr extends BaseInstr { + instrType: InstrType.BOOL_OP; symbol: TokenType; } export interface AppInstr extends BaseInstr { - numOfArgs: number - srcNode: PyNode + instrType: InstrType.APPLICATION; + numOfArgs: number; + srcNode: PyNode; +} + +export interface EndOfFunctionBodyInstr extends BaseInstr { + instrType: InstrType.END_OF_FUNCTION_BODY; +} + +export interface ResetInstr extends BaseInstr { + instrType: InstrType.RESET; } export interface BranchInstr extends BaseInstr { @@ -100,6 +123,8 @@ export type Instr = | ForInstr | AssmtInstr | AppInstr + | EndOfFunctionBodyInstr + | ResetInstr | BranchInstr | EnvInstr | ArrLitInstr diff --git a/src/cse-machine/py_utils.ts b/src/cse-machine/py_utils.ts index 1e95228..3dd17e0 100644 --- a/src/cse-machine/py_utils.ts +++ b/src/cse-machine/py_utils.ts @@ -3,7 +3,7 @@ import { Value } from "./stash"; import { PyNode } from "./py_types"; import { TokenType } from "../tokens"; import { PyRuntimeSourceError } from "../errors/py_runtimeSourceError"; -import { currentEnvironment, Environment } from "./py_environment"; +import { currentEnvironment, PyEnvironment } from "./py_environment"; export function pyHandleRuntimeError (context: PyContext, error: PyRuntimeSourceError) { @@ -103,7 +103,7 @@ export function pyDefineVariable(context: PyContext, name: string, value: Value) } export function pyGetVariable(context: PyContext, name: string, node: PyNode): Value { - let environment: Environment | null = currentEnvironment(context); + let environment: PyEnvironment | null = currentEnvironment(context); while (environment) { if (Object.prototype.hasOwnProperty.call(environment.head, name)) { return environment.head[name]; From ae36bb9a05225caf80a0cf21a4960a7e7b38ff9e Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 19 Sep 2025 12:08:40 +0800 Subject: [PATCH 14/16] Migrated environment, closure && added functionDef, Lambda (Refs #51)[12] * New Files: py_closure.ts * Added necessary parts for functionDef and Lambda such as explicit and implicit return, reset --- src/cse-machine/py_interpreter.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index fa1cfce..d1cbde3 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -355,6 +355,19 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { pyDefineVariable(context, functionDefNode.name.lexeme, closure); }, + 'Lambda': (code, command, context, control, stash, isPrelude) => { + const lambdaNode = command as ExprNS.Lambda; + + //create closure, capturing current environment + const closure = PyClosure.makeFromLambda( + lambdaNode, + currentEnvironment(context), + context + ); + // lambda is expression, just push value onto stash + stash.push(closure); + }, + /** * Only handles explicit return for now * To handle implicit return None next @@ -463,7 +476,11 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { // push reset and implicit return for cleanup at end of function control.push(instrCreator.resetInstr(instr.srcNode)); - control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode)); + + // Only push endOfFunctionBodyInstr for functionDef + if (closure.node.constructor.name === 'FunctionDef') { + control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode)); + } // create new function environment const newEnv = createEnvironment(context, closure, args, instr.srcNode as ExprNS.Call); From e391c4f341f009a14629668cb0f1125e4a33e375 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 19 Sep 2025 18:19:32 +0800 Subject: [PATCH 15/16] Added If, ternary, print (Refs #51)[13] * added print() for testing on front end * reworked interpreter to be more script runner and not repl like output * changes 1 --- src/cse-machine/py_context.ts | 1 + src/cse-machine/py_interpreter.ts | 124 +++++++++++++++++++++++------- src/cse-machine/py_operators.ts | 13 ++-- src/cse-machine/py_types.ts | 5 ++ src/cse-machine/py_utils.ts | 4 + src/errors/py_errors.ts | 38 +++++---- src/py_stdlib.ts | 53 ++++++++++--- 7 files changed, 176 insertions(+), 62 deletions(-) diff --git a/src/cse-machine/py_context.ts b/src/cse-machine/py_context.ts index 1e10975..2501a53 100644 --- a/src/cse-machine/py_context.ts +++ b/src/cse-machine/py_context.ts @@ -10,6 +10,7 @@ import { StmtNS } from '../ast-types'; export class PyContext { public control: PyControl; public stash: Stash; + public output: string = ''; //public environment: Environment; public errors: CseError[] = []; diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index d1cbde3..acef024 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -11,11 +11,11 @@ import { PyClosure } from './py_closure'; import { PyContext } from './py_context'; import { PyControl, PyControlItem } from './py_control'; import { createEnvironment, currentEnvironment, pushEnvironment, popEnvironment } from './py_environment'; -import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInstr, AppInstr } from './py_types'; +import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInstr, AppInstr, BranchInstr } from './py_types'; import { Stash, Value, ErrorValue } from './stash'; import { IOptions } from '..'; import * as instrCreator from './py_instrCreator'; -import { evaluateUnaryExpression, evaluateBinaryExpression, evaluateBoolExpression } from './py_operators'; +import { evaluateUnaryExpression, evaluateBinaryExpression, evaluateBoolExpression, isFalsy } from './py_operators'; import { TokenType } from '../tokens'; import { Token } from '../tokenizer'; import { Result, Finished, CSEBreak, Representation} from '../types'; @@ -66,9 +66,9 @@ export function PyCSEResultPromise(context: PyContext, value: Value): Promise { + const ifNode = command as StmtNS.If; + + // create branch instruction, wrap statement arrays in 'StatementSequence' objects + const branch = instrCreator.branchInstr( + { type: 'StatementSequence', body: ifNode.body }, + ifNode.elseBlock + ? (Array.isArray(ifNode.elseBlock) + // 'else' block + ? {type: 'StatementSequence', body: ifNode.elseBlock } + // 'elif' block + : ifNode.elseBlock) + // 'else' block dont exist + : null, + ifNode + ); + control.push(branch); + control.push(ifNode.condition); + }, + + 'Ternary': (code, command, context, control, stash, isPrelude) => { + const ternaryNode = command as ExprNS.Ternary; + const branch = instrCreator.branchInstr( + ternaryNode.consequent, + ternaryNode.alternative, + ternaryNode + ); + control.push(branch); + control.push(ternaryNode.predicate); + }, + /** * Instruction Handlers */ @@ -471,31 +502,39 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { args.unshift(stash.pop()); } - // pop closure from stash - const closure = stash.pop() as PyClosure; - - // push reset and implicit return for cleanup at end of function - control.push(instrCreator.resetInstr(instr.srcNode)); + // pop callable from stash + const callable = stash.pop(); - // Only push endOfFunctionBodyInstr for functionDef - if (closure.node.constructor.name === 'FunctionDef') { - control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode)); - } + if (callable instanceof PyClosure) { + // User-defined function + const closure = callable as PyClosure; + // push reset and implicit return for cleanup at end of function + control.push(instrCreator.resetInstr(instr.srcNode)); - // create new function environment - const newEnv = createEnvironment(context, closure, args, instr.srcNode as ExprNS.Call); - pushEnvironment(context, newEnv); + // Only push endOfFunctionBodyInstr for functionDef + if (closure.node.constructor.name === 'FunctionDef') { + control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode)); + } - // push function body onto control stack - const closureNode = closure.node; - if (closureNode.constructor.name === 'FunctionDef') { - // 'def' has a body of statements (an array) - const bodyStmts = (closureNode as StmtNS.FunctionDef).body.slice().reverse(); - control.push(...bodyStmts); + // create new function environment + const newEnv = createEnvironment(context, closure, args, instr.srcNode as ExprNS.Call); + pushEnvironment(context, newEnv); + + // push function body onto control stack + const closureNode = closure.node; + if (closureNode.constructor.name === 'FunctionDef') { + // 'def' has a body of statements (an array) + const bodyStmts = (closureNode as StmtNS.FunctionDef).body.slice().reverse(); + control.push(...bodyStmts); + } else { + // 'lambda' has a body with a single expression + const bodyExpr = (closureNode as ExprNS.Lambda).body; + control.push(bodyExpr); + } } else { - // 'lambda' has a body with a single expression - const bodyExpr = (closureNode as ExprNS.Lambda).body; - control.push(bodyExpr); + // Built-in function from stdlib / constants + const result = (callable as any)(context, ...args); + stash.push(result); } }, @@ -508,4 +547,35 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { stash.push({ type: 'undefined' }); }, + [InstrType.BRANCH]: (code, command, context, control, stash, isPrelude) => { + const instr = command as BranchInstr; + const condition = stash.pop(); + + if (!isFalsy(condition)) { + // Condition is truthy, execute the consequent + const consequent = instr.consequent; + if (consequent && 'type' in consequent && consequent.type === 'StatementSequence') { + control.push(...(consequent as any).body.slice().reverse()); + } else if (consequent) { + // consequent of ternary or single statement + control.push(consequent); + } + } else if (instr.alternate) { + // Condition is falsy, execute the alternate + const alternate = instr.alternate; + if (alternate && 'type' in alternate && alternate.type === 'StatementSequence') { + // 'else' block + control.push(...(alternate as any).body.slice().reverse()); + } else if (alternate) { + // 'elif' or ternary alternative + control.push(alternate); + } + } + // If condition is falsy and there's no alternate, do nothing + }, + + [InstrType.POP]: (code, command, context, control, stash, isPrelude) => { + stash.pop(); + }, + }; \ No newline at end of file diff --git a/src/cse-machine/py_operators.ts b/src/cse-machine/py_operators.ts index 7dae99b..afd6669 100644 --- a/src/cse-machine/py_operators.ts +++ b/src/cse-machine/py_operators.ts @@ -1,7 +1,7 @@ import { Value } from "./stash"; import { PyContext } from "./py_context"; import { PyComplexNumber} from "../types"; -import { UnsupportedOperandTypeError, ZeroDivisionError, TypeConcatenateError } from "../errors/py_errors"; +import { UnsupportedOperandTypeError, ZeroDivisionError } from "../errors/py_errors"; import { ExprNS } from "../ast-types"; import { TokenType } from "../tokens"; import { pyHandleRuntimeError, operatorTranslator, pythonMod, typeTranslator } from "./py_utils"; @@ -33,7 +33,7 @@ export type BinaryOperator = | "instanceof"; // Helper function for truthiness based on Python rules -function isFalsy(value: Value): boolean { +export function isFalsy(value: Value): boolean { switch (value.type) { case 'bigint': return value.value === 0n; @@ -179,11 +179,12 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con if (left.type === 'string' && right.type === 'string') { return { type: 'string', value: left.value + right.value }; } else { - const wrongType = left.type === 'string' ? right.type : left.type; - pyHandleRuntimeError(context, new TypeConcatenateError( + pyHandleRuntimeError(context, new UnsupportedOperandTypeError( code, command, - typeTranslator(wrongType) + left.type, + right.type, + operatorTranslator(operator) )); } } @@ -257,7 +258,7 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con } return { type: 'number', value: pythonMod(l, r) }; case TokenType.DOUBLESTAR: - if (r === 0) { + if (l === 0 && r < 0) { pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context)); } return { type: 'number', value: l ** r }; diff --git a/src/cse-machine/py_types.ts b/src/cse-machine/py_types.ts index 2f9ded5..e93e652 100644 --- a/src/cse-machine/py_types.ts +++ b/src/cse-machine/py_types.ts @@ -109,6 +109,10 @@ export interface BranchInstr extends BaseInstr { alternate: PyNode | null | undefined } +export interface PopInstr extends BaseInstr { + instrType: InstrType.POP; +} + export interface EnvInstr extends BaseInstr { env: Environment } @@ -126,6 +130,7 @@ export type Instr = | EndOfFunctionBodyInstr | ResetInstr | BranchInstr + | PopInstr | EnvInstr | ArrLitInstr | UnOpInstr diff --git a/src/cse-machine/py_utils.ts b/src/cse-machine/py_utils.ts index 3dd17e0..9ca53ad 100644 --- a/src/cse-machine/py_utils.ts +++ b/src/cse-machine/py_utils.ts @@ -4,6 +4,7 @@ import { PyNode } from "./py_types"; import { TokenType } from "../tokens"; import { PyRuntimeSourceError } from "../errors/py_runtimeSourceError"; import { currentEnvironment, PyEnvironment } from "./py_environment"; +import { builtIns } from "../py_stdlib"; export function pyHandleRuntimeError (context: PyContext, error: PyRuntimeSourceError) { @@ -111,6 +112,9 @@ export function pyGetVariable(context: PyContext, name: string, node: PyNode): V environment = environment.tail; } } + if (builtIns.has(name)) { + return builtIns.get(name)!; + } // For now, we throw an error. We can change this to return undefined if needed. // handleRuntimeError(context, new TypeError(`name '${name} is not defined`, node as any, context as any, '', '')); return { type: 'error', message: `NameError: name '${name}' is not defined` }; diff --git a/src/errors/py_errors.ts b/src/errors/py_errors.ts index 8f0e51d..981e251 100644 --- a/src/errors/py_errors.ts +++ b/src/errors/py_errors.ts @@ -34,26 +34,24 @@ export function createErrorIndicator(snippet: string, errorPos: number): string return indicator; } -export class TypeConcatenateError extends PyRuntimeSourceError { - constructor(source: string, node: ExprNS.Expr, wrongType: string) { - super(node); - this.type = ErrorType.TYPE; - - const { line, fullLine } = getFullLine(source, node.startToken.indexInSource); - - const snippet = source.substring(node.startToken.indexInSource, node.endToken.indexInSource + node.endToken.lexeme.length); - const offset = fullLine.indexOf(snippet); - const adjustedOffset = offset >= 0 ? offset : 0; - - const errorPos = (node as any).operator.indexInSource - node.startToken.indexInSource; - const indicator = createErrorIndicator(snippet, errorPos); - const name = "TypeError"; - let hint = 'TypeError: can only concatenate str (not "' + wrongType + '") to str.'; - const suggestion = "You are trying to concatenate a string with an " + wrongType + ". To fix this, convert the " + wrongType + " to a string using str(), or ensure both operands are of the same type."; - const msg = `${name} at line ${line}\n\n ${fullLine}\n ${' '.repeat(adjustedOffset)}${indicator}\n${hint}\n${suggestion}`; - this.message = msg; - } -} +// export class TypeConcatenateError extends PyRuntimeSourceError { +// constructor(source: string, node: ExprNS.Expr, wrongType: string) { +// super(node); +// this.type = ErrorType.TYPE; + +// let index = (node as any).symbol?.loc?.start?.index; +// const { line, fullLine } = getFullLine(source, index); +// const snippet = (node as any).symbol?.loc?.source ?? ''; + +// let hint = 'TypeError: can only concatenate str (not "' + wrongType + '") to str.'; +// const offset = fullLine.indexOf(snippet); +// const indicator = createErrorIndicator(snippet, '+'); +// const name = "TypeError"; +// const suggestion = "You are trying to concatenate a string with an " + wrongType + ". To fix this, convert the " + wrongType + " to a string using str(), or ensure both operands are of the same type."; +// const msg = name + " at line " + line + "\n\n " + fullLine + "\n " + " ".repeat(offset) + indicator + "\n" + hint + "\n" + suggestion; +// this.message = msg; +// } +// } export class UnsupportedOperandTypeError extends PyRuntimeSourceError { constructor(source: string, node: ExprNS.Expr, wrongType1: string, wrongType2: string, operand: string) { diff --git a/src/py_stdlib.ts b/src/py_stdlib.ts index d8b4930..33766ea 100644 --- a/src/py_stdlib.ts +++ b/src/py_stdlib.ts @@ -1,6 +1,8 @@ -import { Closure } from "./cse-machine/closure"; +import { PyContext } from './cse-machine/py_context'; +import { PyClosure } from './cse-machine/py_closure'; import { Value } from "./cse-machine/stash"; - +import { pyHandleRuntimeError } from "./cse-machine/py_utils"; +import { UnsupportedOperandTypeError } from "./errors/py_errors"; export function toPythonFloat(num: number): string { if (Object.is(num, -0)) { @@ -32,6 +34,9 @@ export function toPythonFloat(num: number): string { export function toPythonString(obj: Value): string { let ret: any; + if (!obj) { + return 'None'; + } if ((obj as Value).type === 'bigint' || (obj as Value).type === 'complex') { ret = (obj as Value).value.toString(); } else if ((obj as Value).type === 'number') { @@ -44,16 +49,46 @@ export function toPythonString(obj: Value): string { } } else if ((obj as Value).type === 'error') { return (obj as Value).message; - } else if ((obj as unknown as Closure).node) { - for (let name in (obj as unknown as Closure).environment!.head) { - if ((obj as unknown as Closure).environment!.head[name] === obj) { - return ''; - } + } else if (obj instanceof PyClosure) { + if (obj.node) { + const funcName = (obj.node as any).name?.lexeme || '(anonymous)'; + return ``; } - } else if ((obj as Value) === undefined || (obj as Value).value === undefined) { + } else if ((obj as Value).value === undefined) { ret = 'None'; } else { ret = (obj as Value).value.toString(); } return ret; - } \ No newline at end of file +} + +export class BuiltInFunctions { + static print(context: PyContext, ...args: Value[]): Value { + const output = args.map(arg => toPythonString(arg)).join(' '); + context.output += output + '\n'; + return { type: 'undefined' }; + } + + static _int(context: PyContext, ...args: Value[]): Value { + if (args.length === 0) { + return { type: 'bigint', value: BigInt(0) }; + } + + const arg = args[0]; + if (arg.type === 'number') { + const truncated = Math.trunc(arg.value); + return { type: 'bigint', value: BigInt(truncated) }; + } + if (arg.type === 'bigint') { + return { type: 'bigint', value: arg.value }; + } + + // TODO: Use proper TypeError class once node is passed to built-ins + return { type: 'error', message: `TypeError: int() argument must be a string, a bytes-like object or a real number, not '${arg.type}'` }; + } +} + +// Load only the functions we have implemented +export const builtIns = new Map any>(); +builtIns.set('print', BuiltInFunctions.print); +builtIns.set('_int', BuiltInFunctions._int); \ No newline at end of file From ac3df3c97c8e15dd3909a0b25700524b2b183267 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 30 Sep 2025 20:41:50 +0800 Subject: [PATCH 16/16] Fixed Scope Error in functions (Refs #51)[14] * modifed closure, environment and interpreter to have proper checks of variable scope * added error messages UnboundLocalError and NameError for illegal usage of variables --- src/cse-machine/py_closure.ts | 16 +++++--- src/cse-machine/py_environment.ts | 2 + src/cse-machine/py_interpreter.ts | 16 +++++--- src/cse-machine/py_utils.ts | 64 ++++++++++++++++++++++++++----- src/errors/py_errors.ts | 42 ++++++++++++++++++++ 5 files changed, 121 insertions(+), 19 deletions(-) diff --git a/src/cse-machine/py_closure.ts b/src/cse-machine/py_closure.ts index dd4eecf..b1cfb76 100644 --- a/src/cse-machine/py_closure.ts +++ b/src/cse-machine/py_closure.ts @@ -18,12 +18,15 @@ export class PyClosure { public context: PyContext; public readonly predefined: boolean; public originalNode?: StmtNS.FunctionDef | ExprNS.Lambda; + /** Stores local variables for scope check */ + public localVariables: Set; constructor( node: StmtNS.FunctionDef | ExprNS.Lambda, environment: PyEnvironment, context: PyContext, - predefined: boolean = false + predefined: boolean = false, + localVariables: Set = new Set() ) { this.id = uniqueId(context); this.node = node; @@ -31,6 +34,7 @@ export class PyClosure { this.context = context; this.predefined = predefined; this.originalNode = node; + this.localVariables = localVariables; } /** @@ -39,9 +43,10 @@ export class PyClosure { static makeFromFunctionDef ( node: StmtNS.FunctionDef, environment: PyEnvironment, - context: PyContext + context: PyContext, + localVariables: Set ): PyClosure { - const closure = new PyClosure(node, environment, context); + const closure = new PyClosure(node, environment, context, false, localVariables); return closure; } @@ -51,9 +56,10 @@ export class PyClosure { static makeFromLambda( node: ExprNS.Lambda, environment: PyEnvironment, - context: PyContext + context: PyContext, + localVariables: Set ): PyClosure { - const closure = new PyClosure(node, environment, context); + const closure = new PyClosure(node, environment, context, false, localVariables); return closure; } } diff --git a/src/cse-machine/py_environment.ts b/src/cse-machine/py_environment.ts index 1bbb056..05f7465 100644 --- a/src/cse-machine/py_environment.ts +++ b/src/cse-machine/py_environment.ts @@ -18,6 +18,7 @@ export interface PyEnvironment { head: Frame heap: Heap thisContext?: Value + closure?: PyClosure } export const uniqueId = (context: PyContext): string => { @@ -37,6 +38,7 @@ export const createEnvironment = ( heap: new Heap(), id: uniqueId(context), callExpression: callExpression, + closure: closure } closure.node.parameters.forEach((paramToken, index) => { diff --git a/src/cse-machine/py_interpreter.ts b/src/cse-machine/py_interpreter.ts index acef024..3cbd303 100644 --- a/src/cse-machine/py_interpreter.ts +++ b/src/cse-machine/py_interpreter.ts @@ -20,7 +20,7 @@ import { TokenType } from '../tokens'; import { Token } from '../tokenizer'; import { Result, Finished, CSEBreak, Representation} from '../types'; import { toPythonString } from '../py_stdlib' -import { pyGetVariable, pyDefineVariable } from './py_utils'; +import { pyGetVariable, pyDefineVariable, scanForAssignments } from './py_utils'; type CmdEvaluator = ( @@ -78,7 +78,7 @@ export function PyEvaluate(code: string, program: StmtNS.Stmt, context: PyContex options.stepLimit, options.isPrelude || false, ); - return context.output ? { type: "string", value: context.output} : { type: 'undefined' }; + return context.output ? { type: "string", value: context.output} : { type: 'string', value: '' }; } catch(error: any) { return { type: 'error', message: error.message}; } finally { @@ -300,7 +300,7 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { const name = variableNode.name.lexeme; // if not built in, look up in environment - const value = pyGetVariable(context, name, variableNode) + const value = pyGetVariable(code, context, name, variableNode) stash.push(value); }, @@ -345,11 +345,14 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { 'FunctionDef': (code, command, context, control, stash, isPrelude) => { const functionDefNode = command as StmtNS.FunctionDef; + // find all local variables defined in function body + const localVariables = scanForAssignments(functionDefNode.body); // create closure, capture function code and environment const closure = PyClosure.makeFromFunctionDef( functionDefNode, currentEnvironment(context), - context + context, + localVariables ); // define function name in current environment and bind to new closure pyDefineVariable(context, functionDefNode.name.lexeme, closure); @@ -358,11 +361,14 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = { 'Lambda': (code, command, context, control, stash, isPrelude) => { const lambdaNode = command as ExprNS.Lambda; + // find all local variables defined in function body + const localVariables = scanForAssignments(lambdaNode.body); //create closure, capturing current environment const closure = PyClosure.makeFromLambda( lambdaNode, currentEnvironment(context), - context + context, + localVariables ); // lambda is expression, just push value onto stash stash.push(closure); diff --git a/src/cse-machine/py_utils.ts b/src/cse-machine/py_utils.ts index 9ca53ad..affd60c 100644 --- a/src/cse-machine/py_utils.ts +++ b/src/cse-machine/py_utils.ts @@ -5,6 +5,8 @@ import { TokenType } from "../tokens"; import { PyRuntimeSourceError } from "../errors/py_runtimeSourceError"; import { currentEnvironment, PyEnvironment } from "./py_environment"; import { builtIns } from "../py_stdlib"; +import { StmtNS, ExprNS } from "../ast-types"; +import { UnboundLocalError, NameError } from "../errors/py_errors"; export function pyHandleRuntimeError (context: PyContext, error: PyRuntimeSourceError) { @@ -103,19 +105,63 @@ export function pyDefineVariable(context: PyContext, name: string, value: Value) }); } -export function pyGetVariable(context: PyContext, name: string, node: PyNode): Value { - let environment: PyEnvironment | null = currentEnvironment(context); - while (environment) { - if (Object.prototype.hasOwnProperty.call(environment.head, name)) { - return environment.head[name]; +export function pyGetVariable(code: string, context: PyContext, name: string, node: PyNode): Value { + const env = currentEnvironment(context); + // UnboundLocalError check + if (env.closure && env.closure.localVariables.has(name)) { + if (!env.head.hasOwnProperty(name)) { + throw new UnboundLocalError(code, name, node as ExprNS.Variable); + } + } + + let currentEnv: PyEnvironment | null = env; + while (currentEnv) { + if (Object.prototype.hasOwnProperty.call(currentEnv.head, name)) { + return currentEnv.head[name]; } else { - environment = environment.tail; + currentEnv = currentEnv.tail; } } if (builtIns.has(name)) { return builtIns.get(name)!; } - // For now, we throw an error. We can change this to return undefined if needed. - // handleRuntimeError(context, new TypeError(`name '${name} is not defined`, node as any, context as any, '', '')); - return { type: 'error', message: `NameError: name '${name}' is not defined` }; + throw new NameError(code, name, node as ExprNS.Variable); +} + +export function scanForAssignments(node: PyNode | PyNode[]): Set { + const assignments = new Set(); + const visitor = (curNode: PyNode) => { + if (!curNode || typeof curNode !== 'object') { + return; + } + + const nodeType = curNode.constructor.name; + + if (nodeType === 'Assign') { + assignments.add((curNode as StmtNS.Assign).name.lexeme); + } else if (nodeType === 'FunctionDef' || nodeType === 'Lambda') { + // detach here, nested functions have their own scope + return; + } + + // Recurse through all other properties of the node + for (const key in curNode) { + if (Object.prototype.hasOwnProperty.call(curNode, key)) { + const child = (curNode as any)[key]; + if (Array.isArray(child)) { + child.forEach(visitor); + } else if (child && typeof child === 'object' && child.hasOwnProperty('type')) { + visitor(child); + } + } + } + }; + + if (Array.isArray(node)) { + node.forEach(visitor); + } else { + visitor(node); + } + + return assignments; } \ No newline at end of file diff --git a/src/errors/py_errors.ts b/src/errors/py_errors.ts index 981e251..60031b3 100644 --- a/src/errors/py_errors.ts +++ b/src/errors/py_errors.ts @@ -226,6 +226,48 @@ export class ZeroDivisionError extends PyRuntimeSourceError { } } +export class UnboundLocalError extends PyRuntimeSourceError { + constructor(source: string, name: string, node: ExprNS.Expr) { + super(node); + this.type = ErrorType.TYPE; + + const { line, fullLine } = getFullLine(source, node.startToken.indexInSource); + const snippet = source.substring(node.startToken.indexInSource, node.endToken.indexInSource + node.endToken.lexeme.length); + const offset = fullLine.indexOf(snippet); + const adjustedOffset = offset >= 0 ? offset : 0; + + const errorPos = 0; + const indicator = createErrorIndicator(snippet, errorPos); + + const hint = `UnboundLocalError: cannot access local variable '${name}' where it is not associated with a value` + const suggestion = `The variable '${name}' is used in the current function, so it's considered a local variable. However, you tried to access it before a value was assigned to it in the local scope. Assign a value to '${name}' before you use it.`; + const msg = `UnboundLocalError at line ${line}\n\n ${fullLine}\n ${' '.repeat(adjustedOffset)}${indicator}\n${hint}\n${suggestion}` + this.message = msg; + } +} + +export class NameError extends PyRuntimeSourceError { + constructor(source: string, name: string, node: ExprNS.Variable) { + super(node); + this.type = ErrorType.TYPE; + + const { line, fullLine } = getFullLine(source, node.startToken.indexInSource); + + const snippet = source.substring(node.startToken.indexInSource, node.endToken.indexInSource + node.endToken.lexeme.length); + + const offset = fullLine.indexOf(snippet); + const adjustedOffset = offset >= 0 ? offset : 0; + + const errorPos = 0; + const indicator = createErrorIndicator(snippet, errorPos); + + const hint = `NameError: name '${name}' is not defined`; + const suggestion = `The name '${name}' is not defined in the current scope. Check for typos or make sure the variable is assigned a value before being used.`; + + this.message = `NameError at line ${line}\n\n ${fullLine}\n ${' '.repeat(adjustedOffset)}${indicator}\n${hint}\n${suggestion}`; + } +} + // export class StepLimitExceededError extends PyRuntimeSourceError { // constructor(source: string, node: ExprNS.Expr, context: PyContext) { // super(node);