UNPKG

@onlabsorg/swan-js

Version:

A simple yet powerful expression language written in JavaScript

945 lines (759 loc) 28.6 kB
// ============================================================================= // INTERPRETER // // This module exports a `parser` function that takes an expression string as // input and returns an asynchronous `evaluate` function. The `evaluate` // function takes a context object, evaluate the expression AST and returns // the expression value. // // The parser function is a `Parser` instance (see parser module). The parser // is configured to wrap the AST nodes in custom nodes. Each custom node // has an `evaluate` function that takes a `context` as input and returns // the node value. // // The node values returned by the `parse` function are wrapped in a type // object (see types module). // // ============================================================================= const {matchIdentifier} = require("./lexer"); const {Parser, ASTNode} = require('./parser'); const types = require('./types'); const Undefined = (position, type, ...args) => { const undef = new types.Undefined(type, ...args); undef.position = position; return undef; } // ----------------------------------------------------------------------------- // AST Node Wrappers // // These are the basic type of AST nodes: each node is either a binary // operation, a unary operatio or a leaf. // ----------------------------------------------------------------------------- // Generic binary operation used as a base class for actual AST nodes. class BinaryOperation extends ASTNode { // Operation name get name () { return "BinaryOperation"; } // left-hand node of this binary operation get leftHandOperand () { return this.children[0]; } // right-hand node of this binary operation get rightHandOperand () { return this.children[1]; } // shortcut function to create a `types.Undefined` value when the binary // operation is not defined undefined (leftHandOperand, rightHandOperand) { return Undefined(this.position, this.name, leftHandOperand, rightHandOperand); } // this is inernally used to evaluate name definitions, when identifiers // should not be resolved to their mapped values. async evaluateInNameDomain (context) { const id1 = await this.leftHandOperand.evaluateInNameDomain(context); const id2 = await this.rightHandOperand.evaluateInNameDomain(context); return this.undefined(id1, id2); } } // Generic unary operation used as a base class for actual AST nodes. class UnaryOperation extends ASTNode { // Operation name get name () { return "UnaryOperation"; } // the operand node of this unary operation get operand () { return this.children[0]; } // shortcut function to create a `types.Undefined` value when the unary // operation is not defined undefined (operand) { return Undefined(this.position, this.name, operand); } // this is inernally used to evaluate name definitions, when identifiers // should not be resolved to their mapped values. async evaluateInNameDomain (context) { const id = await this.operand.evaluateInNameDomain(context); return this.undefined(id); } } // Generic leaf used as a base class for actual AST terminal nodes. class Leaf extends ASTNode { // Operation name get name () { return "Leaf"; } // shortcut function to create a `types.Undefined` value when the leaf // value is not defined undefined (value) { return Undefined(this.position, this.name, value); } // this is inernally used to evaluate name definitions, when identifiers // should not be resolved to their mapped values. async evaluateInNameDomain (context) { const value = await this.evaluate(context); return this.undefined(value); } // by default, a leaf node evaluates to the node value async evaluate (context={}) { return types.wrap(this.value); } } // ----------------------------------------------------------------------------- // Core Operations // // These are the foundamental operations of the swan language. // ----------------------------------------------------------------------------- // Node representing a `()` literal, which is an empty tuple (aka Nothing). class VoidLiteral extends Leaf { // Operation name get name () { return "VoidLiteral"; } async evaluate (context) { return new types.Tuple(); } } // Node representing a numeric literal, such as `123.45`. class NumberLiteral extends Leaf { // Operation name get name () { return "NumberLiteral"; } async evaluate (context={}) { return new types.Numb(this.value); } } // Node representing a string literal, such as `"abc"` or `'abc'`. class StringLiteral extends Leaf { // Operation name get name () { return "StringLiteral"; } async evaluate (context={}) { return new types.Text(this.value); } } // Node representing a string template, which is a text enclosed between // accent quotes "`" and containing inline swan expression enclosed between // `${` and `}`. class StringTemplate extends UnaryOperation { // Operation name get name () { return "StringTemplate"; } async evaluate (context) { // retrieve the raw template text let text = this.value; // replace all the inline expressions with a placeholder const expressions = []; text = text.replace(/\{%([\s\S]+?)%}/g, (match, expressionSource) => { const i = expressions.length; expressions.push( parser.parse(expressionSource) ); return "${" + i + "}"; }); // evaluate the inline expression and push their serialized value back // to the template text for (let i=0; i<expressions.length; i++) { const xpTerm = await expressions[i].evaluate(context); text = text.replace("${" + i + "}", xpTerm.toString()); } // wrap and return the resolved template text return new types.Text(text); } } // Node representing a pairing operation `X , Y` which defines a tuple. class PairingOperation extends BinaryOperation { // Operation name get name () { return "PairingOperation"; } // Pairs the names on the left-hand side of an assignment operation such // as `a,b = 1,2` or of a function definition such as `(a,b) -> a+b`. async evaluateInNameDomain (context) { const id1 = await this.leftHandOperand.evaluateInNameDomain(context); const id2 = await this.rightHandOperand.evaluateInNameDomain(context); if ((id1 instanceof types.Text || id1 instanceof types.Tuple) && (id2 instanceof types.Text || id2 instanceof types.Tuple)) { return new types.Tuple(id1, id2); } else { return this.undefined(id1, id2); } } // Create a tuple given two terms. async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); const term2 = await this.rightHandOperand.evaluate(context); return new types.Tuple(term1, term2); } } // Node representing a list literal `[T]`, where `T` is a tuple. class ListDefinition extends UnaryOperation { // Operation name get name () { return "ListDefinition"; } // Turns the child node into a list object async evaluate (context) { const term = await this.operand.evaluate(context); return new types.List( Array.from(term) ); } } // Node representing an identifier, which will be evaluated to its mapped value. class NameReference extends Leaf { // Operation name get name () { return "NameReference"; } // Returns the identifier value as a Text object async evaluateInNameDomain (context) { return new types.Text(this.value); } // Returns the name mapped to the identifier value in the given context async evaluate (context) { const name = this.value; if (!matchIdentifier(name)) return this.undefined(name); const value = context[name]; if (value === Object.prototype[name]) return this.undefined(name); return types.wrap(value); } } // Node representing a labelling operation `name : value`. class LabellingOperation extends BinaryOperation { // Operation name get name () { return "LabellingOperation"; } // Maps the left-hand names to the right-hand values and returns the values async evaluate (context) { const term1 = await this.leftHandOperand.evaluateInNameDomain(context); const term2 = await this.rightHandOperand.evaluate(context); if (term1 instanceof types.Undefined) { return this.undefined(term1, term2); } this.constructor.defineNames(context, term1, term2); //new types.Namespace(context).assign(term1, term2); return term2; } static defineNames (context, term1, term2) { const names = Array.from(term1).map(types.unwrap); const values = Array.from(term2).map(types.unwrap); if (values.length > names.length) { values[names.length-1] = new types.Tuple(...values.slice(names.length-1)); } for (let i=0; i<names.length; i++) { context[ names[i] ] = i < values.length ? types.unwrap(values[i]) : null; } } } // Node representing an assignment operation `name = value`. class AssignmentOperation extends LabellingOperation { // Operation name get name () { return "AssignmentOperation"; } // Maps the left-hand names to the right-hand values and returns Nothing async evaluate (context) { const term2 = await super.evaluate(context); if (term2 instanceof types.Undefined && term2.position === this.position) { return term2; } else { return new types.Tuple(); } } } // Node representing a namespace literal `{T}`, where `T` is a tuple. class NamespaceDefinition extends UnaryOperation { // Operation name get name () { return "NamespaceDefinition"; } // Evaluates the operand in a child context and returns the resulting namespace async evaluate (context) { const subContext = Object.assign(Object.create(context), {}); await this.operand.evaluate(subContext); return new types.Namespace(subContext); } } // Node representing a function definition `names -> expression`. class FunctionDefinition extends BinaryOperation { // Operation name get name () { return "FunctionDefinition"; } // Creates a new Func object async evaluate (context) { const params = await this.leftHandOperand.evaluateInNameDomain(context); if (params instanceof types.Undefined) { return this.undefined(params, this.rightHandOperand); } const func = async (...args) => { const functionContext = Object.create(context); functionContext.self = func; LabellingOperation.defineNames(functionContext, params, args); return await this.rightHandOperand.evaluate(functionContext); } return new types.Func(func); } } // Node representing an application operation `Y X`. class ApplyOperation extends BinaryOperation { // Operation name get name () { return "ApplyOperation"; } // Returns LHO(RHO), where LHO can be a Func or a Mapping async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); const term2 = await this.rightHandOperand.evaluate(context); return await this.apply(term1, term2); } apply (term1, term2) { return term1.imapAsync(async item1 => { if (item1 instanceof types.Applicable) { try { return types.wrap( await item1.apply(...term2) ); } catch (error) { return Undefined(this.position, "Term", error); } } else { return this.undefined(term1, term2); } }); } } // Node representing a map operation `X => Y`. class MapOperation extends ApplyOperation { // Operation name get name () { return "MapOperation"; } // (a,b,c) => F returns (F a, F b, F c) async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); const term2 = await this.rightHandOperand.evaluate(context); return term1.imapAsync(item1 => this.apply(term2, item1)); } } // Node representing a function piping operation `X >> Y`. class PipeOperation extends ApplyOperation { // Operation name get name () { return "PipeOperation"; } // (f >> g) x returns g(f x) async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); const term2 = await this.rightHandOperand.evaluate(context); return new types.Func(async (...args) => { const args1 = new types.Tuple(...args); const args2 = await this.apply(term1, args1); return await this.apply(term2, args2); }); } } // Node representing a function composition operation `X << Y`. class ComposeOperation extends ApplyOperation { // Operation name get name () { return "ComposeOperation"; } // (f << g) x returns f(g x) async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); const term2 = await this.rightHandOperand.evaluate(context); return new types.Func(async (...args) => { const args2 = new types.Tuple(...args); const args1 = await this.apply(term2, args2); return await this.apply(term1, args1); }); } } // Node representing a sub-contexting operation `X . Y`. class SubcontextingOperation extends BinaryOperation { // Operation name get name () { return "SubcontextingOperation"; } // Evaluates the RHO in a new child-context augumented with the LHO async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); return await term1.imapAsync(async item1 => { if (item1 instanceof types.Namespace) { return await item1.vmapAsync(async namespace => { return await this.rightHandOperand.evaluate(namespace); }) } else { return this.undefined(item1, this.rightHandOperand); } }); } } // ----------------------------------------------------------------------------- // Unary Operators +x and -x // // There are only two unary operations is swan: `+X` and `-X`. // ----------------------------------------------------------------------------- // Node representing a `+X` unary operation. class IdentityOperation extends UnaryOperation { // Operation name get name () { return "IdentityOperation"; } async evaluate (context) { return this.operand.evaluate(context); } } // Node representing a `-X` unary operation. class NegationOperation extends UnaryOperation { // Operation name get name () { return "NegationOperation"; } async evaluate (context) { const term = await this.operand.evaluate(context); return term.imapSync(item => { if (typeof item.negate === "function") { return item.negate(); } else { return this.undefined(item); } }); } } // ----------------------------------------------------------------------------- // Logic operations // // Nodes representing the swan logic operations `AND`, `OR`, `IF` and `ELSE`. // ----------------------------------------------------------------------------- // Node representing the OR operation `X | Y`. class OrOperation extends BinaryOperation { // Operation name get name () { return "OrOperation"; } async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); if (term1.toBoolean()) { return term1; } else { return await this.rightHandOperand.evaluate(context); } } } // Node representing the AND operation `X & Y`. class AndOperation extends BinaryOperation { // Operation name get name () { return "AndOperation"; } async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); if (term1.toBoolean()) { return await this.rightHandOperand.evaluate(context); } else { return term1; } } } // Node representing the IF operation `X ? Y`. class ConditionalOperation extends BinaryOperation { // Operation name get name () { return "ConditionalOperation"; } async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); if (term1.toBoolean()) { return await this.rightHandOperand.evaluate(context); } else { return Undefined(this.position, 'Term'); } } } // Node representing the ELSE operation `X ; Y`. class AlternativeOperation extends BinaryOperation { // Operation name get name () { return "AlternativeOperation"; } async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); if (term1 instanceof types.Undefined) { return await this.rightHandOperand.evaluate(context); } else { return term1; } } } // ----------------------------------------------------------------------------- // Arithmetic Binary Operations // // These nodes represnt the swan binary operations `+`, `-`, `*`, `/` and `**`. // ----------------------------------------------------------------------------- // Generic arithmetic operation used as base for the actual operations. class ArithmeticOperation extends BinaryOperation { // Operation name get name () { return "ArithmeticOperation"; } async evaluate (context, dunderName, op) { const term1 = await this.leftHandOperand.evaluate(context); const term2 = await this.rightHandOperand.evaluate(context); const pairs = Array.from(term1.iterPairs(term2)); const items = []; for (let [item1, item2] of pairs) { const dunder = (item1 instanceof types.Namespace) ? item1.apply(dunderName) : null; items.push(dunder instanceof types.Func ? await dunder.apply(item1, item2) : op(item1, item2)); } return items.length === 1 ? items[0] : new types.Tuple(...items); } } // Node representing a sum operation `X + Y`. class SumOperation extends ArithmeticOperation { // Operation name get name () { return "SumOperation"; } async evaluate (context) { return await super.evaluate(context, '__add__', (item1, item2) => { if (item1.typeName === item2.typeName && typeof item1.sum === "function") { return item1.sum(item2); } else { return this.undefined(item1, item2); } }); } } // Node representing a subtraction operation `X - Y`. class SubOperation extends ArithmeticOperation { // Operation name get name () { return "SubOperation"; } async evaluate (context) { return await super.evaluate(context, '__sub__', (item1, item2) => { if (item1.typeName === item2.typeName && typeof item1.sum === "function" && typeof item2.negate === "function") { return item1.sum( item2.negate() ); } else { return this.undefined(item1, item2); } }); } } // Node representing a product operation `X * Y`. class MulOperation extends ArithmeticOperation { // Operation name get name () { return "MulOperation"; } async evaluate (context) { return await super.evaluate(context, '__mul__', (item1, item2) => { if (item1.typeName === item2.typeName && typeof item1.mul === "function") { return item1.mul(item2); } else { return this.undefined(item1, item2); } }); } } // Node representing a division operation `X / Y`. class DivOperation extends ArithmeticOperation { // Operation name get name () { return "DivOperation"; } async evaluate (context) { return await super.evaluate(context, '__div__', (item1, item2) => { if (item1.typeName === item2.typeName && typeof item1.mul === "function" && typeof item2.invert === "function") { return item1.mul( item2.invert() ); } else { return this.undefined(item1, item2); } }); } } // Node representing a modulo operation `X % Y`. class ModOperation extends ArithmeticOperation { // Operation name get name () { return "ModOperation"; } async evaluate (context) { return await super.evaluate(context, '__mod__', (item1, item2) => { if (item1 instanceof types.Numb && item2 instanceof types.Numb) { return types.wrap( types.unwrap(item1) % types.unwrap(item2) ); } else { return this.undefined(item1, item2); } }); } } // Node representing a power operation `X ** Y`. class PowOperation extends ArithmeticOperation { // Operation name get name () { return "PowOperation"; } async evaluate (context) { return await super.evaluate(context, '__pow__', (item1, item2) => { if (item1.typeName === item2.typeName && typeof item1.pow === "function") { return item1.pow(item2); } else { return this.undefined(item1, item2); } }); } } // ----------------------------------------------------------------------------- // Comparison Binary Operations // // Nodes representing the comparison operations `==`, `!=`, `>`, `>=`, `<` and // `<=`. // ----------------------------------------------------------------------------- // Generic comparison operation used as base for the actual operations. class ComparisonOperation extends BinaryOperation { // Operation name get name () { return "ComparisonOperation"; } async evaluate (context) { const term1 = await this.leftHandOperand.evaluate(context); const term2 = await this.rightHandOperand.evaluate(context); if (term1 instanceof types.Namespace) { let __cmp__ = await term1.apply('__cmp__'); if (__cmp__ instanceof types.Func) { return types.unwrap(await __cmp__.apply(term1, term2)); } } return term1.compare(term2); } } // Node representing an equality check operation `X == Y`. class EqOperation extends ComparisonOperation { // Operation name get name () { return "EqOperation"; } async evaluate (context) { const cmp = await super.evaluate(context); return new types.Bool(cmp === 0); } } // Node representing an non-equality check operation `X != Y`. class NeOperation extends ComparisonOperation { // Operation name get name () { return "NeOperation"; } async evaluate (context) { const cmp = await super.evaluate(context); return new types.Bool(cmp !== 0); } } // Node representing a less-than check operation `X < Y`. class LtOperation extends ComparisonOperation { // Operation name get name () { return "LtOperation"; } async evaluate (context) { const cmp = await super.evaluate(context); return new types.Bool(cmp < 0); } } // Node representing a less-than-or-equal-to check operation `X <= Y`. class LeOperation extends ComparisonOperation { // Operation name get name () { return "LeOperation"; } async evaluate (context) { const cmp = await super.evaluate(context); return new types.Bool(cmp <= 0); } } // Node representing a greater-than check operation `X > Y`. class GtOperation extends ComparisonOperation { // Operation name get name () { return "GtOperation"; } async evaluate (context) { const cmp = await super.evaluate(context); return new types.Bool(cmp > 0); } } // Node representing a greater-than-or-equal-to check operation `X >= Y`. class GeOperation extends ComparisonOperation { // Operation name get name () { return "GeOperation"; } async evaluate (context) { const cmp = await super.evaluate(context); return new types.Bool(cmp >= 0); } } // ----------------------------------------------------------------------------- // Parser // ----------------------------------------------------------------------------- const parser = new Parser({ binaryOperations: { "," : {precedence:10, Node: PairingOperation }, ":" : {precedence:12, Node: LabellingOperation }, "=" : {precedence:12, Node: AssignmentOperation }, "=>" : {precedence:13, Node: MapOperation }, ">>" : {precedence:14, Node: PipeOperation }, "<<" : {precedence:14, Node: ComposeOperation , right:true}, "->" : {precedence:15, Node: FunctionDefinition, right:true}, ";" : {precedence:21, Node: AlternativeOperation }, "?" : {precedence:22, Node: ConditionalOperation }, "|" : {precedence:23, Node: OrOperation }, "&" : {precedence:23, Node: AndOperation }, "==" : {precedence:24, Node: EqOperation }, "!=" : {precedence:24, Node: NeOperation }, "<" : {precedence:24, Node: LtOperation }, "<=" : {precedence:24, Node: LeOperation }, ">" : {precedence:24, Node: GtOperation }, ">=" : {precedence:24, Node: GeOperation }, "+" : {precedence:25, Node: SumOperation }, "-" : {precedence:25, Node: SubOperation }, "*" : {precedence:26, Node: MulOperation }, "/" : {precedence:26, Node: DivOperation }, "%" : {precedence:26, Node: ModOperation }, "^" : {precedence:27, Node: PowOperation }, "." : {precedence:30, Node: SubcontextingOperation }, "" : {precedence:30, Node: ApplyOperation }, }, unaryOperations: { "+": { Node: IdentityOperation }, "-": { Node: NegationOperation }, }, groupingOperations: { "[]" : { Node: ListDefinition }, "{}" : { Node: NamespaceDefinition }, }, literals: { void : { Node: VoidLiteral }, identifier : { Node: NameReference }, string1 : { Node: StringLiteral }, string2 : { Node: StringLiteral }, string3 : { Node: StringTemplate }, number : { Node: NumberLiteral }, } }); module.exports = source => { const ast = parser.parse(source); return (context={}) => ast.evaluate(context); }