UNPKG

swahili-lang

Version:

A new programming language with semantics borrowed from the Swahili language to help teach programming concepts to Swahili speaking students.

968 lines (832 loc) 28.5 kB
const TT = require('../lexer/tokenTypes'); const SWValue = require('./types/value'); const SWNull = require('./types/null'); const SWNumber = require('./types/number'); const SWString = require('./types/string'); const SWList = require('./types/list'); const SWBaseFunction = require('./types/base-function'); const SWBuiltInFunction = require('./types/built-in-function'); const SWFunction = require('./types/function'); const SWObject = require('./types/object'); const SWPackage = require('./types/_package'); const SWExecutable = require('./types/_executable'); const Context = require('./context'); const SymbolTable = require('./symbolTable'); const RTResult = require('./runtimeResult'); const { RTError, UncaughtException } = require('./error'); /** Analyzes abstract syntax trees from the parser and executes programs */ class Interpreter { /** instantiates the interpreter */ constructor() { this.callbackQueue = []; this.exportValue = null; } /** * Runs code to get the export value (if any) * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type */ copyHeadless(node, context, caller = null) { let res = new RTResult(); let currentExportValue = this.exportValue; res.register(this.visit(node, context, caller)); if (res.error) return res; let output = this.exportValue; this.exportValue = currentExportValue; return res.success(output); } /** * Evaluates an AST node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type */ visit(node, context, caller = null) { let methodName = `visit${node.constructor.name}`; let method = this[methodName] || this.noVisitMethod; return method(node, context, caller); } /** * Occurs when no visit method is defined for the current AST node * @param {Node} node the AST node to visit * @param {Context} context the calling context */ noVisitMethod = (node, context) => { throw new Error(`No visit${node.constructor.name} method defined`); }; /** * Evaluates a number node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @returns {RTResult} */ visitNumberNode = (node, context) => { let res = new RTResult(); return res.success( new SWNumber(node.tok.value) .setContext(context) .setPosition(node.posStart, node.posEnd) ); }; /** * Evaluates a string node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @returns {RTResult} */ visitStringNode = (node, context) => { let res = new RTResult(); return res.success( new SWString(node.tok.value) .setContext(context) .setPosition(node.posStart, node.posEnd) ); }; /** * Evaluates an object node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitObjectNode = (node, context, caller = null) => { let res = new RTResult(); let properties = []; for (let propNode of node.propertyNodes) { propNode.varNameTok = propNode.nodeChain[0]; properties.push(res.register(this.visit(propNode, context, caller))); if (res.shouldReturn()) return res; } return res.success( new SWObject(properties) .setContext(context) .setPosition(node.posStart, node.posEnd) ); }; /** * Evaluates a list node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitListNode = (node, context, caller = null) => { let res = new RTResult(); let elements = []; for (let elementNode of node.elementNodes) { let el = res.register(this.visit(elementNode, context, caller)); if (Array.isArray(el)) { el = el[0]; } if (el) elements.push(el); if (res.shouldReturn()) return res; } return res.success( new SWList(elements) .setContext(context) .setPosition(node.posStart, node.posEnd) ); }; /** * Returns a property value from the associated context's symbol table * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitPropAccessNode = (node, context, caller = null) => { let res = new RTResult(); let value = null; let propChain = []; let currentNode = node; if (node.parent === null) return res.success(this.visitVarAccessNode(node, context, caller)); while (currentNode.parent !== null) { propChain.push(currentNode.varNameTok.value); currentNode = currentNode.parent; } let obj = res.register( this.visitVarAccessNode(currentNode, context, caller) ); if (res.shouldReturn()) return res; // if not an object, see if any typed methods support this value if (!(obj instanceof SWObject) && !(obj instanceof SWPackage)) { try { if (propChain.length > 1) throw 0; let methodName = propChain[0]; let typeMethod = context.symbolTable.get('$' + methodName); // type methods are hidden with a $ in the global context if (!typeMethod) throw 0; let supportedTypes = typeMethod.types || []; for (let i = 0; i < supportedTypes.length; i++) { let type = supportedTypes[i]; if (obj instanceof type) { return res.success([typeMethod, obj]); } } return res.failure( new RTError( currentNode.posStart, currentNode.posEnd, `'${methodName}' not supported on type '${obj.typeName}'`, context ) ); } catch (err) { return res.failure( new RTError( currentNode.posStart, currentNode.posEnd, `'${currentNode.varNameTok.value}' is not an object`, context ) ); } } let chainLength = propChain.length; let props = [...propChain].reverse(); for (let propName of props) { const newValue = obj.symbolTable.get(propName); value = newValue || value; if (newValue) chainLength--; if ( (newValue instanceof SWObject || newValue instanceof SWPackage) && !(newValue instanceof SWFunction) ) { obj = newValue; } else { const propIndex = props.indexOf(propName); if (!newValue && propIndex !== props.length - 1) { return res.failure( new RTError( node.posStart, node.posEnd, `Cannot get property '${propChain[0]}' of undefined`, context ) ); } } } if (chainLength) { try { let methodName = propChain[chainLength - 1]; let typeMethod = context.symbolTable.get('$' + methodName); // type methods are hidden with a $ in the global context if (!typeMethod) { if (!(obj instanceof SWPackage)) throw 0; return res.success(SWNull.NULL); } if (!value) value = obj; let supportedTypes = typeMethod.types; for (let i = 0; i < supportedTypes.length; i++) { let type = supportedTypes[i]; if (value instanceof type) { return res.success([typeMethod, value]); } } return res.failure( new RTError( currentNode.posStart, currentNode.posEnd, `'${methodName}' not supported on type '${value.typeName}'`, context ) ); } catch (err) { if (value ? value instanceof SWObject : obj instanceof SWObject) return res.success(SWNull.NULL); return res.failure( new RTError( node.posStart, node.posEnd, `Cannot get property '${propChain[0]}' ${ value ? `on type ${value.typeName}` : `of undefined` }`, context ) ); } } return res.success(value || SWNull.NULL); }; /** * Creates a property node for an object's symbol table * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitPropAssignNode = (node, context, caller = null) => { let res = new RTResult(); let value = res.register(this.visit(node.valueNode, context, caller)); if (node.varNameTok) { let name = node.varNameTok.value; if (res.shouldReturn()) return res; return res.success({ name, value }); } let currentNode = node.nodeChain[0].value; let obj = context.symbolTable.get(currentNode); if (!obj) { let self = caller.symbolTable.get(currentNode); if (self.name) obj = context.symbolTable.get(self.name); if (!obj) obj = self; } // cannot mutate properties on packages if (obj instanceof SWPackage) return res.failure( new RTError( node.posStart, node.posEnd, `Cannot modify package properties`, context ) ); let valueNode; for (let i = 1; i < node.nodeChain.length; i++) { if (valueNode instanceof SWObject) { obj = valueNode; } if (valueNode instanceof SWPackage) return res.failure( new RTError( node.posStart, node.posEnd, `Cannot modify package properties`, context ) ); currentNode = node.nodeChain[i].value; valueNode = obj.symbolTable.get(currentNode); } if (value instanceof SWObject && !(value instanceof SWBuiltInFunction)) { if (!value.name) value.name = currentNode; if (!(value instanceof SWFunction)) { value.parent = obj.symbolTable.symbols; } } obj.symbolTable.set(currentNode, value); if (caller) caller.symbolTable.set('hii', obj); if (obj.parent) obj.parent[obj.name] = obj; return res.success(value); }; /** * Returns a variable value from the associated context's symbol table * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitVarAccessNode = (node, context, caller = null) => { let res = new RTResult(); let varName = node.varNameTok.value; let value = context.symbolTable.get(varName) || (caller ? caller.symbolTable.get(varName) || caller.defContext.symbolTable.get(varName) : null); if (!value) return res.failure( new RTError( node.posStart, node.posEnd, `'${varName}' is not defined`, context ) ); value = value .copy() .setPosition(node.posStart, node.posEnd) .setContext(context); return res.success(value); }; /** * Updates a variable into the associated context's symbol table * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitVarAssignNode = (node, context, caller = null) => { let res = new RTResult(); let varName = node.varNameTok.value; let value = res.register(this.visit(node.valueNode, context, caller)); if (res.shouldReturn()) return res; if (!context.symbolTable.get(varName)) return res.failure( new RTError( node.posStart, node.posEnd, `'${varName}' is not defined`, context ) ); if (value instanceof SWObject && !(value instanceof SWBuiltInFunction)) { if (!value.name) value.name = varName; if (!(value instanceof SWFunction)) { value.parent = context.symbolTable.symbols; } } let isSet = context.symbolTable.set(varName, value, true); if (!isSet) return res.failure( new RTError( node.posStart, node.posEnd, `Cannot change value of constant '${varName}'`, context ) ); return res.success(value); }; /** * Creates a variable into the associated context's symbol table * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitVarDefNode = (node, context, caller = null) => { let res = new RTResult(); let varName = node.varNameTok.value; let value = res.register(this.visit(node.valueNode, context, caller)); if (res.shouldReturn()) return res; if (context.symbolTable.get(varName, true)) return res.failure( new RTError( node.posStart, node.posEnd, `Cannot re-declare '${varName}'`, context ) ); if (value instanceof SWObject && !(value instanceof SWBuiltInFunction)) { if (!value.name) value.name = varName; if (!(value instanceof SWFunction)) { value.parent = context.symbolTable.symbols; } } context.symbolTable.set(varName, value); return res.success(value); }; /** * Evaluates a binary operation node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitBinOpNode = (node, context, caller = null) => { let res = new RTResult(); let left = res.register(this.visit(node.leftNode, context, caller)); if (res.shouldReturn()) return res; let right = res.register(this.visit(node.rightNode, context, caller)); if (res.shouldReturn()) return res; let result = new SWValue(); let error = null; if (node.opTok.type === TT.PLUS) { [result, error] = left.addedTo(right); } else if (node.opTok.type === TT.MINUS) { [result, error] = left.subbedBy(right); } else if (node.opTok.type === TT.MUL) { [result, error] = left.multedBy(right); } else if (node.opTok.type === TT.DIV) { [result, error] = left.divvedBy(right); } else if (node.opTok.type === TT.POW) { [result, error] = left.powedBy(right); } else if (node.opTok.type === TT.MOD) { [result, error] = left.moddedBy(right); } else if (node.opTok.type === TT.EE) { [result, error] = left.getComparisonEQ(right); } else if (node.opTok.type == TT.NE) { [result, error] = left.getComparisonNE(right); } else if (node.opTok.type === TT.LT) { [result, error] = left.getComparisonLT(right); } else if (node.opTok.type === TT.GT) { [result, error] = left.getComparisonGT(right); } else if (node.opTok.type === TT.LTE) { [result, error] = left.getComparisonLTE(right); } else if (node.opTok.type === TT.GTE) { [result, error] = left.getComparisonGTE(right); } else if (node.opTok.type === TT.AND) { [result, error] = left.andedBy(right); } else if (node.opTok.type === TT.OR) { [result, error] = left.oredBy(right); } if (error) { return res.failure(error); } else { return res.success(result.setPosition(node.posStart, node.posEnd)); } }; /** * Evaluates a unary operation node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitUnaryOpNode = (node, context, caller = null) => { let res = new RTResult(); let number = res.register(this.visit(node.node, context, caller)); if (res.shouldReturn()) return res; let error = null; if (node.opTok.type === TT.MINUS) { [number, error] = number.multedBy(new SWNumber(-1)); } else if (node.opTok.type === TT.NOT) { [number, error] = number.notted(); } if (error) { return res.failure(error); } else { return res.success(number.setPosition(node.posStart, node.posEnd)); } }; /** * Evaluates an if node and returns the value from the case that evaluated to true * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitIfNode = (node, context, caller = null) => { let res = new RTResult(); let originalScope = context.symbolTable; context.symbolTable = new SymbolTable(context.symbolTable); for (let [condition, expr, shouldReturnNull] of node.cases) { let conditionValue = res.register(this.visit(condition, context, caller)); if (res.shouldReturn()) return res; if (conditionValue.isTrue()) { let exprValue = res.register(this.visit(expr, context, caller)); if (res.shouldReturn()) return res; return res.success( shouldReturnNull ? SWNull.NULL : exprValue.elements[0] ); } } if (node.elseCase) { let [expr, shouldReturnNull] = node.elseCase; let elseValue = res.register(this.visit(expr, context, caller)); if (res.shouldReturn()) return res; return res.success( shouldReturnNull ? SWNull.NULL : elseValue.elements[0] ); } context.symbolTable = originalScope; return res.success(SWNull.NULL); }; /** * Evaluates a for node and returns the value of the expression while the iterator meets given conditions * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitForNode = (node, context, caller = null) => { let res = new RTResult(); let elements = []; let stepValue = new SWNumber(1); let condition = null; let startValue = res.register( this.visit(node.startValueNode, context, caller) ); if (res.shouldReturn()) return res; let endValue = res.register(this.visit(node.endValueNode, context, caller)); if (res.shouldReturn()) return res; if (node.stepValueNode) { stepValue = res.register(this.visit(node.stepValueNode, context, caller)); } let i = startValue.value; condition = stepValue.value >= 0 ? () => i < endValue.value : () => i > endValue.value; let originalScope = context.symbolTable; let blockScope = new SymbolTable(context.symbolTable); context.symbolTable = blockScope; context.symbolTable.set(node.varNameTok.value, new SWNumber(i)); while (condition()) { context.symbolTable = new SymbolTable(context.symbolTable); context.symbolTable.set(node.varNameTok.value, new SWNumber(i), true); i += stepValue.value; let value = res.register(this.visit(node.bodyNode, context, caller)); if (res.shouldReturn() && !res.loopShouldContinue && !res.loopShouldBreak) return res; if (res.loopShouldContinue) continue; if (res.loopShouldBreak) break; elements.push(value); // restore original context context.symbolTable = blockScope; } context.symbolTable = originalScope; return res.success(null); }; /** * Evaluates a for each node and returns the value of the expression for the number of times in the result * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitForEachNode = (node, context, caller = null) => { let res = new RTResult(); let elements = []; let iteratorValue = res.register( this.visit(node.iterationNode, context, caller) ); if (res.shouldReturn()) return res; if (!iteratorValue instanceof SWList && !iteratorValue instanceof SWString) return res.failure( new RTError( node.iteratorValue.posStart, node.iteratorValue.posEnd, `Cannot iterate over non-iterable '${iteratorValue.toString(false)}'`, context ) ); let iterable = iteratorValue.value ? iteratorValue.value .toString() .split('') .map((s) => new SWString(s)) : iteratorValue.elements; if (!iterable) return res.failure( new RTError( node.iteratorValue.posStart, node.iteratorValue.posEnd, `Cannot iterate over non-iterable '${iteratorValue.toString(false)}'`, context ) ); let originalScope = context.symbolTable; let blockScope = new SymbolTable(context.symbolTable); context.symbolTable = blockScope; context.symbolTable.set(node.varNameTok.value, iterable[0]); for (let i = 0; i < iterable.length; i++) { context.symbolTable = new SymbolTable(context.symbolTable); context.symbolTable.set(node.varNameTok.value, iterable[i], true); let value = res.register(this.visit(node.bodyNode, context, caller)); if (res.shouldReturn() && !res.loopShouldContinue && !res.loopShouldBreak) return res; if (res.loopShouldContinue) continue; if (res.loopShouldBreak) break; elements.push(value); // restore original context context.symbolTable = blockScope; } context.symbolTable = originalScope; return res.success(null); }; /** * Evaluates a while node and returns the value of the expression while condition is true * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitWhileNode = (node, context, caller = null) => { let res = new RTResult(); let elements = []; while (true) { let condition = res.register( this.visit(node.conditionNode, context, caller) ); if (res.shouldReturn()) return res; if (!condition.isTrue()) break; let value = res.register(this.visit(node.bodyNode, context, caller)); if (res.shouldReturn() && !res.loopShouldContinue && !res.loopShouldBreak) return res; if (res.loopShouldContinue) continue; if (res.loopShouldBreak) break; elements.push(value); } return res.success(null); }; /** * Evaluates a function definition node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @returns {RTResult} */ visitFuncDefNode = (node, context) => { let res = new RTResult(); let funcName = node.varNameTok ? node.varNameTok.value : null; let bodyNode = node.bodyNode; let argNames = node.argNameToks.map((argName) => argName.value); let funcValue = new SWFunction(funcName, bodyNode, argNames, this) .setContext(context) .setPosition(node.posStart, node.posEnd); if (node.varNameTok) { if (context.symbolTable.get(funcName, true)) return res.failure( new RTError( node.posStart, node.posEnd, `Cannot re-declare '${funcName}'`, context ) ); let isSet = context.symbolTable.set(funcName, funcValue); if (!isSet) return res.failure( new RTError( node.posStart, node.posEnd, `Cannot change value of constant '${funcName}'`, context ) ); } return res.success(funcValue); }; /** * Evaluates a function call node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitCallNode = (node, context, caller = null) => { let res = new RTResult(); let args = []; let valueToCall = res.register( this.visit(node.nodeToCall, context, caller) ); if (res.shouldReturn()) return res; if (Array.isArray(valueToCall)) { args.push(valueToCall[1]); valueToCall = valueToCall[0]; } if ( !( valueToCall instanceof SWBaseFunction || valueToCall instanceof SWExecutable ) ) return res.failure( new RTError( node.posStart, node.posEnd, `'${valueToCall}' is not a method`, context ) ); valueToCall = valueToCall .copy() .setContext(context) .setPosition(node.posStart, node.posEnd); // add the interpreter to it just in case we need it if (!valueToCall.interpreter) valueToCall.interpreter = this; for (let argNode of node.argNodes) { let val = res.register(this.visit(argNode, context, caller)); if (Array.isArray(val)) val = val[0]; args.push(val); if (res.shouldReturn()) return res; } let returnValue = res.register(valueToCall.execute(args)); if (res.shouldReturn()) return res; if (returnValue) returnValue = returnValue .copy() .setPosition(node.posStart, node.posEnd) .setContext(context); return res.success(returnValue); }; /** * Evaluates a try catch node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitTryCatchNode = (node, context, caller = null) => { let res = new RTResult(); let err = null; const tryContext = new Context('<try>', context, node.posStart); tryContext.symbolTable = new SymbolTable(context.symbolTable); for (let line of node.tryBodyNode.elementNodes) { res.register(this.visit(line, tryContext, caller)); if (res.shouldReturn()) { err = res.error; res.reset(); break; } } if (err) { let errVarName = node.errVarNameTok.value; tryContext.symbolTable.set(errVarName, new SWString(err.details)); for (let line of node.catchBodyNode.elementNodes) { res.register(this.visit(line, tryContext, caller)); if (res.shouldReturn()) return res; } } if (node.finallyBodyNode) { for (let line of node.finallyBodyNode.elementNodes) { res.register(this.visit(line, tryContext, caller)); if (res.shouldReturn()) return res; } } return res.success(SWNull.NULL); }; /** * Evaluates a return node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitReturnNode = (node, context, caller = null) => { let res = new RTResult(); let value = SWNull.NULL; if (node.nodeToReturn) { value = res.register(this.visit(node.nodeToReturn, context, caller)); if (res.shouldReturn()) return res; } if (value instanceof SWFunction && caller) value.symbolTable = caller.symbolTable; return res.successReturn(value); }; /** * Evaluates a throw node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @param {*} caller the calling type * @returns {RTResult} */ visitThrowNode = (node, context, caller = null) => { let res = new RTResult(); let value = SWNull.NULL; if (node.nodeToThrow) { value = res.register(this.visit(node.nodeToThrow, context, caller)); if (res.shouldReturn()) return res; } if (value instanceof SWFunction && caller) value.symbolTable = caller.symbolTable; let err = new UncaughtException( node.posStart, node.posEnd, value.toString(false) ); return res.successThrow(err); }; /** * Evaluates a continue node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @returns {RTResult} */ visitContinueNode = (node, context) => { return new RTResult().successContinue(); }; /** * Evaluates a break node * @param {Node} node the AST node to visit * @param {Context} context the calling context * @returns {RTResult} */ visitBreakNode = (node, context) => { return new RTResult().successBreak(); }; } module.exports = Interpreter;