@onlabsorg/swan-js
Version:
A simple yet powerful expression language written in JavaScript
1,069 lines (778 loc) • 29.3 kB
JavaScript
// =============================================================================
// This module contains the Swan data types.
//
// The most generic data type is a `Term`: everything is a Term in swan. There
// are two type of Terms:
//
// - `Item` is either Bool, Numb, Text, List, Namespace, Func or Undefined.
// - `Tuple` is the swan product type (a sequence of items)
//
// Each Item behaves also as a Tuple of one element and every tuple made of
// only one element behaves like an Item.
//
// An empty Tuple represents nothingness.
//
// Each Term wraps a javascript object (the term `value`) and exposes a standard
// interface to interact with it. The term interface is documented below
// inside the Term class definition.
// =============================================================================
const {matchIdentifier} = require("./lexer");
class Term {
// ### TUPLE ITERATORS ###################################################
// This methods iterates over the tuple items. If the term is an Item, it
// will yield the item itself.
*items () {}
// This method iterates over the tuple item values (the javascript wrapped
// objects). If the term is an Iterm, it will yield a single value.
*values () {
for (let item of this.items()) yield unwrap(item);
}
// When iterating over a term, it yields the tuple item values.
*[Symbol.iterator] () {
for (let value of this.values()) yield value;
}
// Given two terms, it pairs their corresponding iterm and yields them as
// a pair. For example (item11, item12, item13) paired with (item21, item22,
// item23) yields [item11, item21], [item12, item22], [item13, item23].
*iterPairs (other) {
const iterator1 = this.items();
const iterator2 = other.items();
while (true) {
let iterItem1 = iterator1.next();
let iterItem2 = iterator2.next();
if (iterItem1.done && iterItem2.done) break;
yield [wrap(iterItem1.value), wrap(iterItem2.value)];
}
}
// ### TUPLE MAPPING #####################################################
// Maps each item via the synchronous function fn
imapSync (fn) {}
// Maps each value via the synchronous function fn
vmapSync (fn) {}
// Maps each item via the asynchronous function fn
async imapAsync (fn) {}
// Maps each value via the asynchronous function fn
async vmapAsync (fn) {}
// ### TYPE CASTING ######################################################
// Converts this term to a JavaScript Boolean
toBoolean () {}
// Converts this term to a JavaScript String
toString () {}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Term';
}
// Returns true if this term is an empty tuple
isNothing () {}
// Coverts a tuple containing only one item to an item. In any other case,
// returns this term as it is.
normalize () {}
// Returns the JavaScript value wrapped in this term
unwrap () {}
// ### ALGEBRA ###########################################################
// The following methods define the agebraic behavior of this term. Not all
// terms define all the algebraic methods. The method not being defined
// means that the corresponding operation is not defined.
// Internal sum operation
// sum (other) {}
// Additive inverse
// negate () {}
// Additive neutral element check
// isNull () {}
// Additive neutral element
// static get null () {}
// Internal product operation
// mul (other) {}
// Multiplicative inverse
// invert () {}
// Multiplicative neutral element check
// isUnit () {}
// Multiplicative neutral element
// static get unit () {}
// Power operation
// pow (other) {}
// Compare this term with another term and it returns
// "=" if the two terms are equal
// ">" if this term is greather than the other term
// "<" if this term is less than the other term
// "#" if no order is defined for this term but the two terms are not equal
// compare (other) {}
}
class Item extends Term {
constructor (value) {
super();
this.$value = value;
}
// ### TUPLE ITERATORS ###################################################
// Yields this item as if it was a tuple made of one item only.
*items () {
yield this;
}
// ### TUPLE MAPPING #####################################################
// Maps this item as if it was a tuple made of one item only.
imapSync (f) {
return wrap( f(this) );
}
// Maps this item value as if it was a tuple made of one item only.
vmapSync (f) {
return wrap( f( unwrap(this) ) );
}
// Maps this item as if it was a tuple made of one item only.
async imapAsync (f) {
return wrap( await f(this) );
}
// Maps this item value as if it was a tuple made of one item only.
async vmapAsync (f) {
return wrap( await f( unwrap(this) ) );
}
// ### TYPE CASTING ######################################################
// By default, an item is always true.
toBoolean () {
return true;
}
// By default, an item stringifies to the type name between `[[` and `]]`.
toString () {
return `[[${this.typeName}]]`;
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Item';
}
// An item is never an empty tuple
isNothing () {
return false;
}
// An item is already in its normalized form
normalize () {
return this;
}
// By default, it returns the argument passed to the constructor
unwrap () {
return this.$value;
}
// ### ALGEBRA ###########################################################
// This compare method is generally used by specific Items. It takes care
// of some common boilerplate and delegates the actual comparison to the
// function passed as second parameters.
// If called directly without the second parameter, it performs a
// JavaScript `===` comparison and returns either `=` or `#`.
// It also makes sure that Nothing is lower than anything elese.
compare (other, _compareSameTypeValues) {
if (other instanceof this.constructor) {
if (typeof _compareSameTypeValues === 'function') {
return _compareSameTypeValues(this.unwrap(), other.unwrap());
} else {
return this.unwrap() === other.unwrap() ? 0 : NaN;
}
} else {
return other.isNothing() ? +1 : NaN;
}
}
}
class Tuple extends Term {
// Creates an Tuple instance given a sequence of values. The passed values
// can be items and/or tuples. The iterator function will take care of flattening
// the tuple, so that `new Tuple(a, new TupleObject(b, c))` is
// equivalent to `new TupleObject(a, b, c)`.
constructor (...items) {
super();
this._items = items.map(wrap);
}
// ### TUPLE ITERATORS ###################################################
// Yields all the items of this tuple, flattening nested tuples.
*items () {
for (let item of this._items) {
for (let subItem of item.items()) yield subItem;
}
}
// ### TUPLE MAPPING #####################################################
// Maps each item of this tuple through the passed synchronous function
// and returns a new tuple.
imapSync (f) {
const values = Array.from( this.items() ).map(f);
return new this.constructor(...values.map(wrap));
}
// Maps each item value of this tuple through the passed synchronous
// function and returns a new tuple.
vmapSync (f) {
const values = Array.from( this.values() ).map(f);
return new this.constructor(...values.map(wrap));
}
// Maps each item of this tuple through the passed asynchronous function
// and returns a new tuple.
async imapAsync (f) {
const values = await Promise.all(Array.from( this.items() ).map(f));
return new this.constructor(...values.map(wrap));
}
// Maps each item value of this tuple through the passed asynchronous
// function and returns a new tuple.
async vmapAsync (f) {
const values = await Promise.all(Array.from( this.values() ).map(f));
return new this.constructor(...values.map(wrap));
}
// ### TYPE CASTING ######################################################
// A tuple booleanizes to false only if all its items booleanize to false.
toBoolean () {
for (let item of this.items()) {
if (item.toBoolean()) return true;
}
return false;
}
// A tuple serializes to the concatenation of all the serialized items
toString () {
let text = "";
for (let item of this.items()) {
text += item.toString();
}
return text;
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
const iterator = this.items();
const first = iterator.next();
// If the tuple is empty return null
if (first.done) return 'Nothing';
// If the tuple contains only one iterm, return that item
if (iterator.next().done) return first.value.typeName;
// If the tuple contains more than one iterm, return the tuple itself
return 'Tuple';
}
// A tuple is nothing if it contains no items.
isNothing () {
let iterator = this.items();
return iterator.next().done;
}
// A tuple normalizes to itself if it contains none or more than one item.
// If it contains only one item, it normalizes to that item.
normalize () {
const iterator = this.items();
const first = iterator.next();
// If the tuple is empty return null
if (first.done) return this;
// If the tuple contains only one iterm, return that item
if (iterator.next().done) return first.value;
// If the tuple contains more than one iterm, return the tuple itself
return this;
}
// A tuple unwraps to itself: there is no equivalent javascript object for
// a tuple.
unwrap () {
const iterator = this.values();
const first = iterator.next();
// If the tuple is empty return null
if (first.done) return null;
// If the tuple contains only one iterm, return that item
if (iterator.next().done) return first.value;
// If the tuple contains more than one iterm, return the tuple itself
return this;
}
// ### ALGEBRA ###########################################################
// Tuples are compared lexicographically.
compare (other) {
for (let [item1, item2] of this.iterPairs(other)) {
if (item1.isNothing()) return item2.isNothing() ? 0 : -1;
if (item2.isNothing()) return +1;
let cmp = item1.compare(item2);
if (cmp !== 0) return cmp;
}
return 0;
}
}
class Bool extends Item {
// ### TYPE CASTING ######################################################
// Returns true if this is TRUE
toBoolean () {
return this.unwrap();
}
// Serializes either to "FALSE" or to "TRUE"
toString () {
return this.unwrap() ? "TRUE" : "FALSE";
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Bool';
}
// ### ALGEBRA ###########################################################
// The sum of two Bool items corresponds to the logic OR
sum (other) {
return new this.constructor(this.unwrap() || other.unwrap());
}
// Additive inverse not defined
// negate () {}
// Returns true if this is FALSE
isNull () {
return !this.unwrap();
}
// The Additive neutral element is FALSE
static get null () {
return new this(false);
}
// The product of two Bool items corresponds to the logic AND
mul (other) {
return new this.constructor(this.unwrap() && other.unwrap());
}
// Multiplicative inverse not defined
// invert () {}
// Returns true if this is TRUE
isUnit () {
return this.unwrap();
}
// The Multiplicative neutral element is TRUE
static get unit () {
return new this(true);
}
// Exponentiation operation not defined
// pow (other) {}
// FALSE is less than TRUE
compare (other) {
return super.compare(other, (thisIsTrue, otherIsTrue) => {
const thisIsFalse = !thisIsTrue;
const otherIsFalse = !otherIsTrue;
if (thisIsFalse && otherIsTrue) return -1;
if (thisIsTrue && otherIsFalse) return +1;
return 0;
})
}
}
class Numb extends Item {
// ### TYPE CASTING ######################################################
// Returns false if 0, or else true
toBoolean () {
return this.unwrap() !== 0;
}
// Returns the number as a string
toString () {
return String( this.unwrap() );
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Numb';
}
// ### ALGEBRA ###########################################################
// Real numbers sum
sum (other) {
return new this.constructor(this.unwrap() + other.unwrap());
}
// Real numbers negation
negate () {
return new this.constructor(-this.unwrap());
}
// True if the number is 0
isNull () {
return this.unwrap() === 0;
}
// 0
static get null () {
return new this(0);
}
// Real numbers product
mul (other) {
return new this.constructor(this.unwrap() * other.unwrap());
}
// Real numbers inverse (1/x)
invert () {
return new this.constructor(1 / this.unwrap());
}
// True if the number is 1
isUnit () {
return this.unwrap() === 1;
}
// 1
static get unit () {
return new this(1);
}
// Real numbers power
pow (other) {
return new this.constructor(this.unwrap() ** other.unwrap());
}
// Real numbers comparison
compare (other) {
return super.compare(other, (thisValue, otherValue) => {
return thisValue === otherValue ? 0 : (thisValue < otherValue ? -1 : +1);
});
}
}
class Applicable extends Item {
// An applicable is any type with an `apply` method.
// Applicables accept the swan apply operation `X Y`.
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Applicable';
}
}
class Mapping extends Applicable {
// A mapping is an item that maps a set of items (domain of the mapping) to
// another set of items. For example, a string is a mapping between integer
// numbers and characters.
// ### MAPPING-SPECIFIC METHODS ##########################################
// This should return an array of all the possible x values that this
// mapping maps
get domain () {}
// This should return the value that this mapping maps to x
// It should always return a defined value if x is part of the domain
// It should always return `undefined` if x is not part of the domain
vget (x) {}
// This should return the item that this mapping maps to x
// It should Undefined Mapping if x is not part of the domain
iget (x) {
const value = this.vget(x);
return value === undefined ? new Undefined("Mapping", x) : wrap(value);
}
// The size of a mapping is the number of items in its domain
get size () {
return this.domain.length;
}
// The image of a mapping is the array of all the mapped values
get image () {
return this.domain.map(x => this.vget(x));
}
// The apply operation takes a tuple of items and returns the corresponding
// tuple of mapped items. If an item of the tuple is not part of the domain,
// the apply operation results in an Undefined item.
apply (...X) {
const Y = X.map(x => this.iget(x));
return Y.length === 1 ? Y[0] : new Tuple(...Y);
}
// ### TYPE CASTING ######################################################
// A mapping with no pairs (empty mapping) booleanizes to false
toBoolean () {
return this.size > 0;
}
// Returns "[[<typename> of n terms]]" where n is the mapping size
toString () {
const n = this.size;
return n === 1 ? `[[${this.typeName} of 1 item]]` : `[[${this.typeName} of ${n} items]]`;
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Mapping';
}
}
class Sequence extends Mapping {
// A sequence is a special mapping that maps integer numbers to generic
// items.
// ### MAPPING-SPECIFIC METHODS ##########################################
// Returns the array of integers between 0 and size - 1
get domain () {
return Object.keys(this.unwrap()).map(Number);
}
// Returns the i-th item of the sequence
vget (i) {
return typeof i === 'number' ? this.unwrap()[i] : undefined;
}
// More efficient implementation of the size method
get size () {
return this.unwrap().length;
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Sequence';
}
}
class Text extends Sequence {
// ### TYPE CASTING ######################################################
// Returns the Text value
toString () {
return this.unwrap();
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Text';
}
// ### ALGEBRA ###########################################################
// The sum of two Text items is their concatenation
sum (other) {
return new this.constructor(this.unwrap() + other.unwrap());
}
// Additive inverse not defined
// negate () {}
// It returns true if the string is empty
isNull () {
return this.size === 0;
}
// It returns the empty string
static get null () {
return new this("");
}
// Product operation not defined
// mul (other) {}
// Multiplicative inverse not defined
// invert () {}
// Multiplicative neutral element not defined
// isUnit () {}
// Multiplicative neutral element not defined
// static get unit () {}
// Exponentiation operation not defined
// pow (other) {}
// Compare two Text items in alphabetical order
compare (other) {
return super.compare(other, (thisString, otherString) => {
return thisString.localeCompare(otherString);
});
}
}
class List extends Sequence {
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'List';
}
// ### ALGEBRA ###########################################################
// Concatenates the two arrays
sum (other) {
return new this.constructor( this.unwrap().concat(other.unwrap()) );
}
// Additive inverse not defined
// negate () {}
// Returns true if the list is empty
isNull () {
return this.size === 0;
}
// Returns an empty list
static get null () {
return new this([]);
}
// Product operation not defined
// mul (other) {}
// Multiplicative inverse not defined
// invert () {}
// Multiplicative neutral element not defined
// isUnit () {}
// Multiplicative neutral element not defined
// static get unit () {}
// Exponentiation operation not defined
// pow (other) {}
// Lexicographical order
compare (other) {
return super.compare(other, (thisArray, otherArray) => {
const thisTuple = new Tuple(...thisArray);
const otherTuple = new Tuple(...otherArray);
return thisTuple.compare(otherTuple);
});
}
}
class Namespace extends Mapping {
// ### MAPPING-SPECIFIC METHODS ##########################################
// Array of object keys which are valid identifiers
get domain () {
return Object.keys(this.unwrap()).filter(matchIdentifier);
}
// Returns the value mapped to the give key
vget (key) {
const object = this.unwrap();
if (matchIdentifier(key) && object.hasOwnProperty(key)) {
return object[key];
}
}
// Apply operation
apply (...X) {
const __apply__ = this.iget('__apply__');
if (__apply__ instanceof Func) {
return __apply__.apply(...X);
} else {
return super.apply(...X);
}
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Namespace';
}
// ### ALGEBRA ###########################################################
// Merges two namespaces
sum (other) {
const sumObject = Object.assign({}, unwrap(this), unwrap(other));
return new this.constructor(sumObject);
}
// Additive inverse not defined
// negate () {}
// True if the namespace domain is empty
isNull () {
return this.size === 0;
}
// Returns an empty namespace
static get null () {
return new this({});
}
// Product operation not defined
// mul (other) {}
// Multiplicative inverse not defined
// invert () {}
// Multiplicative neutral element not defined
// isUnit () {}
// Multiplicative neutral element not defined
// static get unit () {}
// Exponentiation operation not defined
// pow (other) {}
// Two namespace are equal if they have same keys and equal values.
// No order is defined for namespaces: compare returns either '=' or '#'.
compare (other) {
return super.compare(other, (thisObject, otherObject) => {
const thisNames = this.domain;
const otherNames = other.domain;
if (thisNames.length !== otherNames.length) return '#';
for (let name of thisNames) {
const term1 = this.apply(name);
const term2 = other.apply(name);
const cmp = term1.compare(term2);
if (cmp !== 0) return cmp;
}
return 0;
});
}
}
class Func extends Applicable {
// ### FUNC-SPECIFIC METHODS #############################################
// Calls the wrapped function and returns its output wrapped
async apply (...items) {
const func = this.unwrap();
const args = items.map(unwrap);
return wrap( await func(...args) );
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Func';
}
// ### ALGEBRA ###########################################################
// Sum operation not defined
// sum (other) {}
// Additive inverse not defined
// negate () {}
// Additive neutral element not defined
// isNull () {}
// Additive neutral element not defined
// static get null () {}
// Product operation not defined
// mul (other) {}
// Multiplicative inverse not defined
// invert () {}
// Multiplicative neutral element not defined
// isUnit () {}
// Multiplicative neutral element not defined
// static get unit () {}
// Exponentiation operation not defined
// pow (other) {}
// Default comparison: equal if identical and no order defined
// compare (other) {}
}
class Undefined extends Item {
// Swan operations do not throw exceptions: they return Undefined instead.
// For debugging purposes, an Undefined item contains information about the
// undefined operation and the operands.
constructor (type, ...args) {
super({
type: type,
args: args.map(unwrap)
});
}
// ### UNDEFINED-SPECIFIC PROPERTIES #####################################
// A string containing the name of the undefined operation
get type () {
return super.unwrap().type;
}
// Array of operands
get args () {
return super.unwrap().args;
}
// ### TYPE CASTING ######################################################
toBoolean () {
return false;
}
toString () {
return `[[Undefined ${this.type}]]`;
}
// ### MISCELLANEOUS METHODS #############################################
// Class name
get typeName () {
return 'Undefined';
}
// Unwraps to itself (no equivalent JavaScript type available)
unwrap () {
return this;
}
// ### ALGEBRA ###########################################################
// Sum operation not defined
// sum (other) {}
// Additive inverse not defined
// negate () {}
// Additive neutral element not defined
// isNull () {}
// Additive neutral element not defined
// static get null () {}
// Product operation not defined
// mul (other) {}
// Multiplicative inverse not defined
// invert () {}
// Multiplicative neutral element not defined
// isUnit () {}
// Multiplicative neutral element not defined
// static get unit () {}
// Exponentiation operation not defined
// pow (other) {}
// Default comparison: equal if identical and no order defined
// compare (other) {}
}
// Takes a javascript value and turns it into the corresponding swan term.
// If the passed value is already wrapped, it returns it as it is.
function wrap (value) {
// if already wrpped
if (value instanceof Term) return value;
// if not wrapped
switch (typeof(value)) {
case "undefined":
return new Tuple();
case "boolean":
return new Bool(value);
case "number":
return Number.isNaN(value) ? new Undefined("Number") : new Numb(value);
case "string":
return new Text(value);
case "function":
return new Func(value);
// it must be an object
default:
// if null
if (value === null) return new Tuple();
// if an array
if (Array.isArray(value)) return new List(value);
// if a primitive object
switch (Object.prototype.toString.call(value)) {
case '[object Boolean]' : return new Bool(Boolean(value));
case '[object Number]' : return new Numb(Number(value));
case '[object String]' : return new Text(String(value));
case '[object Function]' : return new Func(value);
}
return new Namespace(value);
}
}
// Takes a swan term and returns the origina javascript value.
// If the parameter is already unwrapped, it returns it as it is.
function unwrap (term) {
return (term instanceof Term) ? term.unwrap() : term;
}
module.exports = {
Term,
Tuple,
Item,
Bool,
Numb,
Applicable,
Mapping,
Sequence,
Text,
List,
Namespace,
Func,
Undefined,
wrap, unwrap
};