diff --git a/source_1.pdf b/source_1.pdf new file mode 100644 index 0000000..b9a75d7 Binary files /dev/null and b/source_1.pdf differ diff --git a/src/cse-machine/dict.ts b/src/cse-machine/dict.ts new file mode 100644 index 0000000..a905e31 --- /dev/null +++ b/src/cse-machine/dict.ts @@ -0,0 +1,112 @@ +import * as es from 'estree' +import { isImportDeclaration, getModuleDeclarationSource } from './utils'; + +/** + * Python style dictionary + */ +export default class Dict { + constructor(private readonly internalMap = new Map()) {} + + public get size() { + return this.internalMap.size + } + + public [Symbol.iterator]() { + return this.internalMap[Symbol.iterator]() + } + + public get(key: K) { + return this.internalMap.get(key) + } + + public set(key: K, value: V) { + return this.internalMap.set(key, value) + } + + public has(key: K) { + return this.internalMap.has(key) + } + + /** + * Similar to how the python dictionary's setdefault function works: + * If the key is not present, it is set to the given value, then that value is returned + * Otherwise, `setdefault` returns the value stored in the dictionary without + * modifying it + */ + public setdefault(key: K, value: V) { + if (!this.has(key)) { + this.set(key, value) + } + + return this.get(key)! + } + + public update(key: K, defaultVal: V, updater: (oldV: V) => V) { + const value = this.setdefault(key, defaultVal) + const newValue = updater(value) + this.set(key, newValue) + return newValue + } + + public entries() { + return [...this.internalMap.entries()] + } + + public forEach(func: (key: K, value: V) => void) { + this.internalMap.forEach((v, k) => func(k, v)) + } + + /** + * Similar to `mapAsync`, but for an async mapping function that does not return any value + */ + public async forEachAsync(func: (k: K, v: V, index: number) => Promise): Promise { + await Promise.all(this.map((key, value, i) => func(key, value, i))) + } + + public map(func: (key: K, value: V, index: number) => T) { + return this.entries().map(([k, v], i) => func(k, v, i)) + } + + /** + * Using a mapping function that returns a promise, transform a map + * to another map with different keys and values. All calls to the mapping function + * execute asynchronously + */ + public mapAsync(func: (key: K, value: V, index: number) => Promise) { + return Promise.all(this.map((key, value, i) => func(key, value, i))) + } + + public flatMap(func: (key: K, value: V, index: number) => U[]) { + return this.entries().flatMap(([k, v], i) => func(k, v, i)) + } +} + +/** + * Convenience class for maps that store an array of values + */ +export class ArrayMap extends Dict { + public add(key: K, item: V) { + this.setdefault(key, []).push(item) + } + } + + export function filterImportDeclarations({ + body + }: es.Program): [ + ArrayMap, + Exclude[] + ] { + return body.reduce( + ([importNodes, otherNodes], node) => { + if (!isImportDeclaration(node)) return [importNodes, [...otherNodes, node]] + + const moduleName = getModuleDeclarationSource(node) + importNodes.add(moduleName, node) + return [importNodes, otherNodes] + }, + [new ArrayMap(), []] as [ + ArrayMap, + Exclude[] + ] + ) +} diff --git a/src/cse-machine/interpreter.ts b/src/cse-machine/interpreter.ts new file mode 100644 index 0000000..2273a75 --- /dev/null +++ b/src/cse-machine/interpreter.ts @@ -0,0 +1,843 @@ +/** + * This interpreter implements an explicit-control evaluator. + * + * Heavily adapted from https://github.com/source-academy/JSpike/ + */ + +/* tslint:disable:max-classes-per-file */ + +import * as es from 'estree' +import { Stack } from './stack' +import { Control, ControlItem } from './control'; +import { Stash, Value } from './stash'; +import { Environment, createBlockEnvironment, createEnvironment, createProgramEnvironment, currentEnvironment, popEnvironment, pushEnvironment } from './environment'; +import { Context } from './context'; +import { isNode, isBlockStatement, hasDeclarations, statementSequence, blockArrowFunction, constantDeclaration, pyVariableDeclaration, identifier, literal } from './ast-helper'; +import { envChanging,declareFunctionsAndVariables, handleSequence, defineVariable, getVariable, checkStackOverFlow, checkNumberOfArguments, isInstr, isSimpleFunction, isIdentifier, reduceConditional, valueProducing, handleRuntimeError, hasImportDeclarations, declareIdentifier } from './utils'; +import { AppInstr, AssmtInstr, BinOpInstr, BranchInstr, EnvInstr, Instr, InstrType, StatementSequence, UnOpInstr } from './types'; +import * as instr from './instrCreator' +import { Closure } from './closure'; +import { evaluateBinaryExpression, evaluateUnaryExpression } from './operators'; +import { conditionalExpression } from './instrCreator'; +import * as error from "../errors/errors" +import { ComplexLiteral, CSEBreak, None, PyComplexNumber, RecursivePartial, Representation, Result } from '../types'; +import { builtIns, builtInConstants } from '../stdlib'; +import { IOptions } from '..'; +import { CseError } from './error'; +import { filterImportDeclarations } from './dict'; +import { RuntimeSourceError } from '../errors/runtimeSourceError'; + +type CmdEvaluator = ( + command: ControlItem, + context: Context, + control: Control, + stash: Stash, + isPrelude: boolean +) => void + +let cseFinalPrint = ""; +export function addPrint(str: string) { + cseFinalPrint = cseFinalPrint + str + "\n"; +} + +/** + * 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 CSEResultPromise(context: Context, value: Value): Promise { + return new Promise((resolve, reject) => { + if (value instanceof CSEBreak) { + resolve({ status: 'suspended-cse-eval', context }); + } else if (value instanceof CseError) { + resolve({ status: 'error' } as unknown as Result ); + } else { + //const rep: Value = { type: "string", value: cseFinalPrint }; + const representation = new Representation(value); + resolve({ status: 'finished', context, value, representation }) + } + }) +} + +/** + * Function to be called when a program is to be interpreted using + * the explicit control evaluator. + * + * @param program The program to evaluate. + * @param context The context to evaluate the program in. + * @param options Evaluation options. + * @returns The result of running the CSE machine. + */ +export function evaluate(program: es.Program, context: Context, options: RecursivePartial = {}): Value { + try { + // TODO: is undefined variables check necessary for Python? + // checkProgramForUndefinedVariables(program, context) + } catch (error: any) { + context.errors.push(new CseError(error.message)); + return { type: 'error', message: error.message }; + } + // TODO: should call transformer like in js-slang + // seq.transform(program) + + try { + context.runtime.isRunning = true + context.control = new Control(program); + context.stash = new Stash(); + // Adaptation for new feature + const result = runCSEMachine( + context, + context.control, + context.stash, + options.envSteps!, + options.stepLimit!, + options.isPrelude + ); + const rep: Value = { type: "string", value: cseFinalPrint }; + return rep; + } catch (error: any) { + context.errors.push(new CseError(error.message)); + return { type: 'error', message: error.message }; + } finally { + context.runtime.isRunning = false + } +} + +function evaluateImports(program: es.Program, context: Context) { + try { + const [importNodeMap] = filterImportDeclarations(program) + const environment = currentEnvironment(context) + for (const [moduleName, nodes] of importNodeMap) { + const functions = context.nativeStorage.loadedModules[moduleName] + for (const node of nodes) { + for (const spec of node.specifiers) { + declareIdentifier(context, spec.local.name, node, environment) + let obj: any + + switch (spec.type) { + case 'ImportSpecifier': { + if (spec.imported.type === 'Identifier') { + obj = functions[spec.imported.name]; + } else { + throw new Error(`Unexpected literal import: ${spec.imported.value}`); + } + //obj = functions[(spec.imported).name] + break + } + case 'ImportDefaultSpecifier': { + obj = functions.default + break + } + case 'ImportNamespaceSpecifier': { + obj = functions + break + } + } + + defineVariable(context, spec.local.name, obj, true, node) + } + } + } + } catch (error) { + handleRuntimeError(context, error as RuntimeSourceError) + } +} + +/** + * The primary runner/loop of the explicit control evaluator. + * + * @param context The context to evaluate the program in. + * @param control Points to the current Control stack. + * @param stash Points to the current Stash. + * @param envSteps Number of environment steps to run. + * @param stepLimit Maximum number of steps to execute. + * @param isPrelude Whether the program is the prelude. + * @returns The top value of the stash after execution. + */ +function runCSEMachine( + context: Context, + control: Control, + stash: Stash, + envSteps: number, + stepLimit: number, + isPrelude: boolean = false +): Value { + const eceState = generateCSEMachineStateStream( + context, + control, + stash, + envSteps, + stepLimit, + isPrelude + ); + + // Execute the generator until it completes + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const value of eceState) { + } + + // Return the value at the top of the storage as the result + const result = stash.peek(); + return result !== undefined ? result : { type: 'undefined' }; +} + +/** + * Generator function that yields the state of the CSE Machine at each step. + * + * @param context The context of the program. + * @param control The control stack. + * @param stash The stash storage. + * @param envSteps Number of environment steps to run. + * @param stepLimit Maximum number of steps to execute. + * @param isPrelude Whether the program is the prelude. + * @yields The current state of the stash, control stack, and step count. + */ +export function* generateCSEMachineStateStream( + context: Context, + control: Control, + stash: Stash, + envSteps: number, + stepLimit: number, + isPrelude: boolean = false +) { + + // steps: number of steps completed + let steps = 0 + + let command = control.peek() + + // Push first node to be evaluated into context. + // The typeguard is there to guarantee that we are pushing a node (which should always be the case) + if (command && isNode(command)) { + context.runtime.nodes.unshift(command) + } + + while (command) { + // For local debug only + // console.info('next command to be evaluated'); + // console.info(command); + + // Return to capture a snapshot of the control and stash after the target step count is reached + if (!isPrelude && steps === envSteps) { + yield { stash, control, steps } + return + } + // Step limit reached, stop further evaluation + if (!isPrelude && steps === stepLimit) { + break + } + + if (!isPrelude && envChanging(command)) { + // command is evaluated on the next step + // Hence, next step will change the environment + context.runtime.changepointSteps.push(steps + 1) + } + + control.pop() + if (isNode(command)) { + context.runtime.nodes.shift() + context.runtime.nodes.unshift(command) + //checkEditorBreakpoints(context, command) + cmdEvaluators[command.type](command, context, control, stash, isPrelude) + if (context.runtime.break && context.runtime.debuggerOn) { + // TODO + // We can put this under isNode since context.runtime.break + // will only be updated after a debugger statement and so we will + // run into a node immediately after. + // With the new evaluator, we don't return a break + // return new CSEBreak() + } + } else { + // Command is an instruction + cmdEvaluators[(command as Instr).instrType](command, context, control, stash, isPrelude) + } + + // Push undefined into the stack if both control and stash is empty + if (control.isEmpty() && stash.isEmpty()) { + //stash.push(undefined) + } + command = control.peek() + + steps += 1 + if (!isPrelude) { + context.runtime.envStepsTotal = steps + } + + // printEnvironmentVariables(context.runtime.environments); + + yield { stash, control, steps } + } +} + +function printEnvironmentVariables(environments: Environment[]): void { + console.info('----------------------------------------'); + environments.forEach(env => { + console.info(`Env: ${env.name} (ID: ${env.id})`); + + const variables = env.head; + const variableNames = Object.keys(variables); + + if (variableNames.length > 0) { + variableNames.forEach(varName => { + const descriptor = Object.getOwnPropertyDescriptor(env.head, varName); + if (descriptor) { + const value = descriptor.value.value; + console.info('value: ', value); + const valueStr = (typeof value === 'object' && value !== null) + ? JSON.stringify(value, null, 2) + : String(value); + console.info(` ${varName}: ${valueStr}`); + } else { + console.info(` ${varName}: None`); + } + }); + } else { + console.info(' no defined variables'); + } + }); +} + +const cmdEvaluators: { [type: string]: CmdEvaluator } = { + /** + * AST Nodes + */ + + Program: function ( + command: ControlItem, + context: Context, + control: Control, + stash: Stash, + isPrelude: boolean + ) { + // Clean up non-global, non-program, and non-preparation environments + while ( + currentEnvironment(context).name !== 'global' && + currentEnvironment(context).name !== 'programEnvironment' && + currentEnvironment(context).name !== 'prelude' + ) { + popEnvironment(context) + } + + if (hasDeclarations(command as es.BlockStatement) || hasImportDeclarations(command as es.BlockStatement)) { + if (currentEnvironment(context).name != 'programEnvironment') { + const programEnv = createProgramEnvironment(context, isPrelude) + pushEnvironment(context, programEnv) + } + const environment = currentEnvironment(context) + evaluateImports(command as unknown as es.Program, context) + declareFunctionsAndVariables(context, command as es.BlockStatement, environment) + } + + if ((command as es.Program).body.length === 1) { + // If the program contains only a single statement, execute it immediately + const next = (command as es.Program).body[0]; + cmdEvaluators[next.type](next, context, control, stash, isPrelude); + } else { + // Push the block body as a sequence of statements onto the control stack + const seq: StatementSequence = statementSequence( + (command as es.Program).body as es.Statement[], + (command as es.Program).loc + ) as unknown as StatementSequence + control.push(seq); + } + }, + + BlockStatement: function ( + command: ControlItem, + context: Context, + control: Control + ) { + const next = control.peek(); + + // for some of the block statements, such as if, for, + // no need to create a new environment + + if(!command.skipEnv){ + // If environment instructions need to be pushed + if ( + next && + !(isInstr(next) && next.instrType === InstrType.ENVIRONMENT) && + !control.canAvoidEnvInstr() + ) { + control.push(instr.envInstr(currentEnvironment(context), command as es.BlockStatement)); + } + + // create new block environment (for function) + const environment = createBlockEnvironment(context, 'blockEnvironment'); + declareFunctionsAndVariables(context, command as es.BlockStatement, environment); + pushEnvironment(context, environment); + } + + // Push the block body onto the control stack as a sequence of statements + const seq: StatementSequence = statementSequence((command as es.BlockStatement).body, (command as es.BlockStatement).loc); + control.push(seq); + }, + + StatementSequence: function ( + command: ControlItem, + context: Context, + control: Control, + stash: Stash, + isPrelude: boolean + ) { + if ((command as StatementSequence).body.length === 1) { + // If the program contains only a single statement, execute it immediately + const next = (command as StatementSequence).body[0]; + cmdEvaluators[next.type](next, context, control, stash, isPrelude); + } else { + // Split and push individual nodes + control.push(...handleSequence((command as StatementSequence).body)); + } + }, + + IfStatement: function ( + command: ControlItem, //es.IfStatement, + context: Context, + control: Control, + stash: Stash + ) { + control.push(...reduceConditional(command as es.IfStatement)); + }, + + ExpressionStatement: function ( + command: ControlItem,//es.ExpressionStatement, + context: Context, + control: Control, + stash: Stash, + isPrelude: boolean + ) { + cmdEvaluators[(command as es.ExpressionStatement).expression.type]((command as es.ExpressionStatement).expression, context, control, stash, isPrelude); + }, + + VariableDeclaration: function ( + command: ControlItem, + context: Context, + control: Control + ) { + const declaration: es.VariableDeclarator = (command as es.VariableDeclaration).declarations[0]; + const id = declaration.id as es.Identifier; + const init = declaration.init!; + + control.push(instr.popInstr(command as es.VariableDeclaration)); + control.push(instr.assmtInstr(id.name, (command as es.VariableDeclaration).kind === 'const', true, command as es.VariableDeclaration)); + control.push(init); + }, + + FunctionDeclaration: function ( + command: ControlItem, //es.FunctionDeclaration, + context: Context, + control: Control + ) { + const lambdaExpression: es.ArrowFunctionExpression = blockArrowFunction( + (command as es.FunctionDeclaration).params as es.Identifier[], + (command as es.FunctionDeclaration).body, + (command as es.FunctionDeclaration).loc + ); + const lambdaDeclaration: pyVariableDeclaration = constantDeclaration( + (command as es.FunctionDeclaration).id!.name, + lambdaExpression, + (command as es.FunctionDeclaration).loc + ); + control.push(lambdaDeclaration as ControlItem); + }, + + ReturnStatement: function ( + command: ControlItem, //as es.ReturnStatement, + context: Context, + control: Control + ) { + const next = control.peek(); + if (next && isInstr(next) && next.instrType === InstrType.MARKER) { + control.pop(); + } else { + control.push(instr.resetInstr(command as es.ReturnStatement)); + } + if ((command as es.ReturnStatement).argument) { + control.push((command as es.ReturnStatement).argument!); + } + }, + + ImportDeclaration: function () {}, + + /** + * Expressions + */ + Literal: function ( + command: ControlItem, //es.Literal + context: Context, + control: Control, + stash: Stash + ) { + const literalValue = (command as es.Literal).value; + const bigintValue = (command as es.BigIntLiteral).bigint; + const complexValue = ((command as unknown) as ComplexLiteral).complex; + + if (literalValue !== undefined) { + let value: Value; + if (typeof literalValue === 'number') { + value = { type: 'number', value: literalValue }; + } else if (typeof literalValue === 'string') { + value = { type: 'string', value: literalValue }; + } else if (typeof literalValue === 'boolean') { + value = { type: 'bool', value: literalValue }; + //value = literalValue; + } else { + //handleRuntimeError(context, new CseError('Unsupported literal type')); + return; + } + stash.push(value); + } else if (bigintValue !== undefined) { + let fixedBigintValue = bigintValue.toString().replace(/_/g, ""); + let value: Value; + try { + value = { type: 'bigint', value: BigInt(fixedBigintValue) }; + } catch (e) { + //handleRuntimeError(context, new CseError('Invalid BigInt literal')); + return; + } + stash.push(value); + } else if (complexValue !== undefined) { + let value: Value; + let pyComplexNumber = new PyComplexNumber(complexValue.real, complexValue.imag); + try { + value = { type: 'complex', value: pyComplexNumber }; + } catch (e) { + //handleRuntimeError(context, new CseError('Invalid BigInt literal')); + return; + } + stash.push(value); + } else { + // TODO + // Error + } + + }, + + NoneType: function ( + command: ControlItem, //es.Literal + context: Context, + control: Control, + stash: Stash + ) { + stash.push({ type: 'NoneType', value: undefined }); + }, + + ConditionalExpression: function ( + command: ControlItem, //es.ConditionalExpression, + context: Context, + control: Control, + stash: Stash + ) { + control.push(...reduceConditional(command as es.ConditionalExpression)); + }, + + Identifier: function ( + command: ControlItem,//es.Identifier, + context: Context, + control: Control, + stash: Stash + ) { + if (builtInConstants.has((command as es.Identifier).name)) { + const builtinCons = builtInConstants.get((command as es.Identifier).name)!; + try { + stash.push(builtinCons); + return; + } catch (error) { + // Error + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error(); + } + // if (error instanceof RuntimeSourceError) { + // throw error; + // } else { + // throw new RuntimeSourceError(`Error in builtin function ${funcName}: ${error}`); + // } + } + } else { + stash.push(getVariable(context, (command as es.Identifier).name, (command as es.Identifier))); + } + }, + + UnaryExpression: function ( + command: ControlItem, //es.UnaryExpression, + context: Context, + control: Control + ) { + control.push(instr.unOpInstr((command as es.UnaryExpression).operator, command as es.UnaryExpression)); + control.push((command as es.UnaryExpression).argument); + }, + + BinaryExpression: function ( + command: ControlItem, //es.BinaryExpression, + context: Context, + control: Control + ) { + // currently for if statement + + control.push(instr.binOpInstr((command as es.BinaryExpression).operator, command as es.Node)); + control.push((command as es.BinaryExpression).right); + control.push((command as es.BinaryExpression).left); + }, + + LogicalExpression: function ( + command: ControlItem, //es.LogicalExpression, + context: Context, + control: Control + ) { + if ((command as es.LogicalExpression).operator === '&&') { + control.push( + conditionalExpression((command as es.LogicalExpression).left, (command as es.LogicalExpression).right, literal(false), (command as es.LogicalExpression).loc) + ); + } else { + control.push( + conditionalExpression((command as es.LogicalExpression).left, literal(true), (command as es.LogicalExpression).right, (command as es.LogicalExpression).loc) + ); + } + }, + + ArrowFunctionExpression: function ( + command: ControlItem,//es.ArrowFunctionExpression, + context: Context, + control: Control, + stash: Stash, + isPrelude: boolean + ) { + const closure: Closure = Closure.makeFromArrowFunction( + command as es.ArrowFunctionExpression, + currentEnvironment(context), + context, + true, + isPrelude + ); + stash.push(closure); + }, + + CallExpression: function ( + command: ControlItem,//es.CallExpression, + context: Context, + control: Control + ) { + // add + if (isIdentifier((command as es.CallExpression).callee)) { + let name = ((command as es.CallExpression).callee as es.Identifier).name; + if (name === '__py_adder' || name === '__py_minuser' || + name === '__py_multiplier' || name === '__py_divider' || + name === '__py_modder' || name === '__py_floorer' || + name === '__py_powerer') { + control.push(instr.binOpInstr((command as es.CallExpression).callee as es.Identifier, command as es.Node)) + control.push((command as es.CallExpression).arguments[1]) + control.push((command as es.CallExpression).arguments[0]) + return; + } + } + + control.push(instr.appInstr((command as es.CallExpression).arguments.length, command as es.CallExpression)); + for (let index = (command as es.CallExpression).arguments.length - 1; index >= 0; index--) { + control.push((command as es.CallExpression).arguments[index]); + } + control.push((command as es.CallExpression).callee); + }, + + // /** + // * Instructions + // */ + [InstrType.RESET]: function ( + command: ControlItem, //Instr, + context: Context, + control: Control, + stash: Stash + ) { + const cmdNext: ControlItem | undefined = control.pop(); + if (cmdNext && (isNode(cmdNext) || (cmdNext as Instr).instrType !== InstrType.MARKER)) { + control.push(instr.resetInstr((command as Instr).srcNode)); + } + }, + + [InstrType.ASSIGNMENT]: function ( + command: ControlItem, //AssmtInstr, + context: Context, + control: Control, + stash: Stash + ) { + if ((command as AssmtInstr).declaration) { + //if () + defineVariable( + context, + (command as AssmtInstr).symbol, + stash.peek()!, + (command as AssmtInstr).constant, + (command as AssmtInstr).srcNode as es.VariableDeclaration + ); + } else { + // second time definition + // setVariable( + // context, + // command.symbol, + // stash.peek(), + // command.srcNode as es.AssignmentExpression + // ); + } + }, + + [InstrType.UNARY_OP]: function ( + command: ControlItem, //UnOpInstr, + context: Context, + control: Control, + stash: Stash + ) { + const argument = stash.pop(); + stash.push(evaluateUnaryExpression((command as UnOpInstr).symbol, argument)); + }, + + [InstrType.BINARY_OP]: function ( + command: ControlItem, //BinOpInstr, + context: Context, + control: Control, + stash: Stash + ) { + const right = stash.pop(); + const left = stash.pop(); + + if ((left.type === 'string' && right.type !== 'string') || + (left.type !== 'string' && right.type === 'string')){ + handleRuntimeError(context, new error.TypeConcatenateError(command as es.Node)); + } + + + stash.push(evaluateBinaryExpression(context, (command as BinOpInstr).symbol, left, right)); + + }, + + [InstrType.POP]: function ( + command: ControlItem,//Instr, + context: Context, + control: Control, + stash: Stash + ) { + stash.pop(); + }, + + [InstrType.APPLICATION]: function ( + command: ControlItem, //AppInstr, + context: Context, + control: Control, + stash: Stash + ) { + checkStackOverFlow(context, control); + const args: Value[] = []; + for (let index = 0; index < (command as AppInstr).numOfArgs; index++) { + args.unshift(stash.pop()!); + } + + const func: Closure = stash.pop(); + + if (!(func instanceof Closure)) { + //error + //handleRuntimeError(context, new errors.CallingNonFunctionValue(func, command.srcNode)) + } + + // continuation in python? + + // func instanceof Closure + if (func instanceof Closure) { + // Check for number of arguments mismatch error + checkNumberOfArguments(command, context, func, args, (command as AppInstr).srcNode) + + const next = control.peek() + + // Push ENVIRONMENT instruction if needed - if next control stack item + // exists and is not an environment instruction, OR the control only contains + // environment indepedent items + if ( + next && + !(isInstr(next) && next.instrType === InstrType.ENVIRONMENT) && + !control.canAvoidEnvInstr() + ) { + control.push(instr.envInstr(currentEnvironment(context), (command as AppInstr).srcNode)) + } + + // Create environment for function parameters if the function isn't nullary. + // Name the environment if the function call expression is not anonymous + if (args.length > 0) { + const environment = createEnvironment(context, (func as Closure), args, (command as AppInstr).srcNode) + pushEnvironment(context, environment) + } else { + context.runtime.environments.unshift((func as Closure).environment) + } + + // Handle special case if function is simple + if (isSimpleFunction((func as Closure).node)) { + // Closures convert ArrowExpressionStatements to BlockStatements + const block = (func as Closure).node.body as es.BlockStatement + const returnStatement = block.body[0] as es.ReturnStatement + control.push(returnStatement.argument ?? identifier('undefined', returnStatement.loc)) + } else { + if (control.peek()) { + // push marker if control not empty + control.push(instr.markerInstr((command as AppInstr).srcNode)) + } + control.push((func as Closure).node.body) + + // console.info((func as Closure).node.body); + } + + return + } + + // Value is a built-in function + let function_name = (((command as AppInstr).srcNode as es.CallExpression).callee as es.Identifier).name; + + if (builtIns.has(function_name)) { + const builtinFunc = builtIns.get(function_name)!; + + try { + stash.push(builtinFunc(args)); + return; + } catch (error) { + // Error + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error(); + } + } + } + }, + + [InstrType.BRANCH]: function ( + command: ControlItem,//BranchInstr, + context: Context, + control: Control, + stash: Stash + ) { + const test = stash.pop(); + + if (test.value) { + if (!valueProducing((command as BranchInstr).consequent)) { + control.push(identifier('undefined', (command as BranchInstr).consequent.loc)); + } + ((command as BranchInstr).consequent as ControlItem).skipEnv = true; + control.push((command as BranchInstr).consequent); + } else if ((command as BranchInstr).alternate) { + if (!valueProducing((command as BranchInstr).alternate!)) { + control.push(identifier('undefined', (command as BranchInstr).alternate!.loc)); + } + ((command as BranchInstr).alternate as ControlItem).skipEnv = true; + control.push((command as BranchInstr).alternate!); + } else { + control.push(identifier('undefined', (command as BranchInstr).srcNode.loc)); + } + }, + + [InstrType.ENVIRONMENT]: function ( + command: ControlItem, //EnvInstr, + context: Context + ) { + while (currentEnvironment(context).id !== (command as EnvInstr).env.id) { + popEnvironment(context); + } + }, +}; diff --git a/src/cse-machine/operators.ts b/src/cse-machine/operators.ts new file mode 100644 index 0000000..7353a27 --- /dev/null +++ b/src/cse-machine/operators.ts @@ -0,0 +1,447 @@ +import * as es from "estree"; +import { handleRuntimeError, isIdentifier, pythonMod } from "./utils"; +import { Context } from "./context"; +import { PyComplexNumber } from "../types"; + +export type BinaryOperator = + | "==" + | "!=" + | "===" + | "!==" + | "<" + | "<=" + | ">" + | ">=" + | "<<" + | ">>" + | ">>>" + | "+" + | "-" + | "*" + | "/" + | "%" + | "**" + | "|" + | "^" + | "&" + | "in" + | "instanceof"; + +export function evaluateUnaryExpression(operator: es.UnaryOperator, value: any) { + if (operator === '!') { + if (value.type === 'bool') { + return { + type: 'bool', + value: !(Boolean(value.value)) + }; + } else { + // TODO: error + } + } else if (operator === '-') { + 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 + } + // else if (value.type === 'bool') { + // return { + // type: 'bigint', + // value: Boolean(value.value)?BigInt(-1):BigInt(0) + // }; + // } + } else if (operator === 'typeof') { + // todo + return { + type: String, + value: typeof value.value + }; + } else { + return value; + } +} + +export function evaluateBinaryExpression(context: Context, identifier: any, left: any, right: any) { + //if(isIdentifier(identifier)){ + //if(identifier.name === '__py_adder') { + if (left.type === 'string' && right.type === 'string' && identifier.name === '__py_adder') { + if(isIdentifier(identifier) && identifier.name === '__py_adder') { + return { + type: 'string', + value: left.value + right.value + }; + } else { + let ret_type : any; + let ret_value : any; + if (identifier === '>') { + ret_value = left.value > right.value; + } else if(identifier === '>=') { + ret_value = left.value >= right.value; + } else if(identifier === '<') { + ret_value = left.value < right.value; + } else if(identifier === '<=') { + ret_value = left.value <= right.value; + } else if(identifier === '===') { + ret_value = left.value === right.value; + } else if(identifier === '!==') { + ret_value = left.value !== right.value; + } else { + // TODO: error + } + + return { + type: 'bool', + value: ret_value + }; + } + } else { + // numbers: only int and float, not bool + const numericTypes = ['number', 'bigint', 'complex']; //, 'bool' + if (!numericTypes.includes(left.type) || !numericTypes.includes(right.type)) { + // TODO: + //throw new Error('Placeholder: invalid operand types for addition'); + // console.info('not num or bigint', left.type, right.type); + } + + // if (left.type === 'bool') { + // left.type = 'bigint'; + // left.value = left.value?BigInt(1):BigInt(0); + // } + // if (right.type === 'bool') { + // right.type = 'bigint'; + // right.value = right.value?BigInt(1):BigInt(0); + // } + + let originalLeft = { type : left.type, value : left.value }; + let originalRight = { type : right.type, value : right.value }; + + if (left.type !== right.type) { + // left.type = 'number'; + // left.value = Number(left.value); + // right.type = 'number'; + // right.value = Number(right.value); + + 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(isIdentifier(identifier)) { + if(identifier.name === '__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(identifier.name === '__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(identifier.name === '__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(identifier.name === '__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.value !== 0) { + ret_type = 'number'; + ret_value = Number(left.value) / Number(right.value); + } else { + // TODO: divide by 0 error + } + } + } else if(identifier.name === '__py_modder') { + if (left.type === 'complex') { + // TODO: error + } + ret_value = pythonMod(left.value, right.value); + } else if(identifier.name === '__py_floorer') { + // TODO: floorer not in python now + ret_value = 0; + } else if(identifier.name === '__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 { + // TODO: throw an error + } + } 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 (identifier === '===') { + ret_value = leftComplex.equals(rightComplex); + } else if (identifier === '!==') { + ret_value = !leftComplex.equals(rightComplex); + } else { + // TODO: error + } + } 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 (identifier === '>') { + ret_value = compare_res > 0; + } else if(identifier === '>=') { + ret_value = compare_res >= 0; + } else if(identifier === '<') { + ret_value = compare_res < 0; + } else if(identifier === '<=') { + ret_value = compare_res <= 0; + } else if(identifier === '===') { + ret_value = compare_res === 0; + } else if(identifier === '!==') { + ret_value = compare_res !== 0; + } else { + // TODO: error + } + + + } else { + if (identifier === '>') { + ret_value = left.value > right.value; + } else if(identifier === '>=') { + ret_value = left.value >= right.value; + } else if(identifier === '<') { + ret_value = left.value < right.value; + } else if(identifier === '<=') { + ret_value = left.value <= right.value; + } else if(identifier === '===') { + ret_value = left.value === right.value; + } else if(identifier === '!==') { + ret_value = left.value !== right.value; + } else { + // TODO: error + } + } + + + } + + 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); +} diff --git a/src/index.ts b/src/index.ts index bc9598d..251660c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -184,3 +184,9 @@ export * from './errors'; // } // } // } + +export interface IOptions { + isPrelude: boolean, + envSteps: number, + stepLimit: number +}; diff --git a/src/stdlib.ts b/src/stdlib.ts index f1b2dd4..12c4c22 100644 --- a/src/stdlib.ts +++ b/src/stdlib.ts @@ -2,6 +2,17 @@ import { ArrowFunctionExpression } from "estree"; import { Closure } from "./cse-machine/closure"; import { Value } from "./cse-machine/stash"; +/* + Create a map to hold built-in constants. + Each constant is stored with a string key and its corresponding value object. +*/ +export const builtInConstants = new Map(); +/* + Create a map to hold built-in functions. + The keys are strings (function names) and the values are functions that can take any arguments. +*/ +export const builtIns = new Map any>(); + /** * Converts a number to a string that mimics Python's float formatting behavior. *