@gobstones/gobstones-lang
Version:
776 lines • 22 kB
JavaScript
/* eslint-disable no-underscore-dangle */
import { i18n } from './i18n';
/* Each value has a type.
*
* A type is a tree, represented with instances of Type (or its subclasses).
* We write:
* r(c1, ..., cN)
* for a tree whose root is r and whose children are c1, ..., cN.
*
* The type of a value may be one of the following:
* new TypeAny() (unknown)
* new TypeInteger()
* new TypeString()
* new TypeTuple([t1, ..., tN])
* where ti is the type of the i-th component.
* new TypeList(t)
* where t is the type of the elements.
* new TypeStructure(typeName, cases)
* where typeName is the name of the type (e.g. 'Bool').
* Moreover, cases is an object of the following "type":
* Map String (Map String Type)
* more precisely,
* - cases is dictionary indexed by constructor names,
* - if c is a constructor name, cases[c] is a dictionary
* indexed by field name,
* - if f is a field name, cases[c][f] is the type of the
* field f for the constructor c.
*
* For example, consider the following type definition:
* type A is variant {
* case B {
* field x
* field y
* }
* case C {
* field z
* }
* }
*
* Then the following expression in Gobstones:
* [B(x <- 1, y <- "foo")]
* is a list whose type is represented as:
* new TypeList(
* new TypeStructure('A', {
* 'B': {'x': new TypeInteger(), 'y': new TypeString()}
* })
* )
*
* The following expression in Gobstones:
* [B(x <- 1, y <- "foo"), C(z <- "bar")]
* is a list whose type is represented as:
* new TypeList(
* new TypeStructure('A', {
* 'B': {'x': new TypeInteger(), 'y': new TypeString()},
* 'C': {'z': new TypeString()},
* })
* )
*/
const Ty_Any = Symbol.for('Ty_Any');
const Ty_Integer = Symbol.for('Ty_Integer');
const Ty_String = Symbol.for('Ty_String');
const Ty_Tuple = Symbol.for('Ty_Tuple');
const Ty_List = Symbol.for('Ty_List');
const Ty_Structure = Symbol.for('Ty_Structure');
const Ty_Unkown = Symbol.for('?');
export class Type {
constructor(tag) {
this._tag = tag;
}
get tag() {
return this._tag;
}
isAny() {
return false;
}
isInteger() {
return false;
}
isString() {
return false;
}
isTuple() {
return false;
}
isList() {
return false;
}
isStructure() {
return false;
}
isBoolean() {
return false;
}
isColor() {
return false;
}
isDirection() {
return false;
}
}
export class TypeAny extends Type {
constructor() {
super(Ty_Any);
}
toString() {
return '?';
}
isAny() {
return true;
}
}
export class TypeInteger extends Type {
constructor() {
super(Ty_Integer);
}
toString() {
return i18n('TYPE:Integer');
}
isInteger() {
return true;
}
}
export class TypeString extends Type {
constructor() {
super(Ty_String);
}
toString() {
return i18n('TYPE:String');
}
isString() {
return true;
}
}
export class TypeTuple extends Type {
constructor(componentTypes) {
super(Ty_Tuple);
this._componentTypes = componentTypes;
}
get componentTypes() {
return this._componentTypes;
}
toString() {
const strings = [];
for (const t of this._componentTypes) {
strings.push(t.toString());
}
return i18n('TYPE:Tuple') + '(' + strings.join(', ') + ')';
}
isTuple() {
return true;
}
}
export class TypeList extends Type {
constructor(contentType) {
super(Ty_List);
this._contentType = contentType;
}
get contentType() {
return this._contentType;
}
toString() {
let suffix = '';
if (!this._contentType.isAny()) {
suffix = '(' + this._contentType.toString() + ')';
}
return i18n('TYPE:List') + suffix;
}
isList() {
return true;
}
}
export class TypeStructure extends Type {
constructor(typeName, cases) {
super(Ty_Structure);
this._typeName = typeName;
this._cases = cases;
}
get typeName() {
return this._typeName;
}
get cases() {
return this._cases;
}
toString() {
const caseStrings = [];
for (const constructorName of sortedKeys(this._cases)) {
const fieldTypes = this._cases[constructorName];
const fieldStrings = [];
for (const fieldName of sortedKeys(fieldTypes)) {
fieldStrings.push(fieldName + ' <- ' + fieldTypes[fieldName].toString());
}
if (fieldStrings.length !== 0) {
caseStrings.push(constructorName + '(' + fieldStrings.join(', ') + ')');
}
}
if (caseStrings.length === 0) {
return this._typeName;
}
else {
return this._typeName + ' { ' + caseStrings.join(' | ') + ' }';
}
}
isStructure() {
return true;
}
isBoolean() {
return this._typeName === i18n('TYPE:Bool');
}
isColor() {
return this._typeName === i18n('TYPE:Color');
}
isDirection() {
return this._typeName === i18n('TYPE:Dir');
}
}
/* Attempts to calculate the "join" of two types.
*
* To join two types:
* - any occurrence of TypeAny() may be replaced by an arbitrary type,
* - structures of the same type built with different constructors
* are joinable,
* - structures of the same type built with the same constructors
* are joinable if their matching fields are joinable.
*
* If the types are joinable, return their join.
* If the types are not joinable, return undefined.
*/
export function joinTypes(type1, type2) {
if (type1 === undefined || type2 === undefined) {
return undefined;
}
else if (type1.tag === Ty_Any) {
return type2;
}
else if (type2.tag === Ty_Any) {
return type1;
}
else if (type1.tag === Ty_Integer && type2.tag === Ty_Integer) {
return type1;
}
else if (type1.tag === Ty_String && type2.tag === Ty_String) {
return type1;
}
else if (type1.tag === Ty_Tuple && type2.tag === Ty_Tuple) {
return joinTupleTypes(type1, type2);
}
else if (type1.tag === Ty_List && type2.tag === Ty_List) {
return joinListTypes(type1, type2);
}
else if (type1.tag === Ty_Structure && type2.tag === Ty_Structure) {
return joinStructureTypes(type1, type2);
}
else {
/* Otherwise the types are not joinable */
return undefined;
}
}
const joinTupleTypes = (type1, type2) => {
if (type1.componentTypes.length !== type2.componentTypes.length) {
/* Tuples are of different length */
return undefined;
}
const joinedComponents = [];
for (let i = 0; i < type1.componentTypes.length; i++) {
const t1 = type1.componentTypes[i];
const t2 = type2.componentTypes[i];
const tj = joinTypes(t1, t2);
if (tj === undefined) {
/* Cannot join the i-th component */
return undefined;
}
joinedComponents.push(tj);
}
return new TypeTuple(joinedComponents);
};
const joinListTypes = (type1, type2) => {
const joinedContent = joinTypes(type1.contentType, type2.contentType);
if (joinedContent === undefined) {
/* Cannot join the contents of the lists */
return undefined;
}
return new TypeList(joinedContent);
};
/*
* The join of two structures is quite like a least common multiple.
* We must:
* - Check that they are structures of the same type.
* - Include all the non-common constructors verbatim
* (with "non-common" we mean those that are in type1
* but not in type2 or vice-versa).
* - For all common constructors, we must recursively join
* the types of their respective fields.
*/
const joinStructureTypes = (type1, type2) => {
if (type1.typeName !== type2.typeName) {
return undefined;
}
const joinedCases = {};
/* Include all the non-common constructors */
function joinCommon(typeA, typeB) {
for (const constructorName in typeA.cases) {
if (!(constructorName in typeB.cases)) {
joinedCases[constructorName] = typeA.cases[constructorName];
}
}
}
joinCommon(type1, type2);
joinCommon(type2, type1);
/* Include all the common constructors */
for (const constructorName in type1.cases) {
if (constructorName in type2.cases) {
const joinedFields = joinFields(type1.cases[constructorName], type2.cases[constructorName]);
if (joinedFields === undefined) {
return undefined;
}
joinedCases[constructorName] = joinedFields;
}
}
return new TypeStructure(type1.typeName, joinedCases);
};
const joinFields = (fields1, fields2) => {
/* Ensure that they have the same set of fields */
function checkIncluded(fieldsA, fieldsB) {
for (const fieldName in fieldsA) {
if (!(fieldName in fieldsB)) {
throw Error('Join fields: structures built using the same constructor ' +
'should have the same set of fields.');
}
}
}
checkIncluded(fields1, fields2);
checkIncluded(fields2, fields1);
/* Recursively join the types of the common fields */
const joinedFields = {};
for (const fieldName in fields1) {
const type1 = fields1[fieldName];
const type2 = fields2[fieldName];
const joinedTypes = joinTypes(type1, type2);
if (joinedTypes === undefined) {
return undefined;
}
joinedFields[fieldName] = joinedTypes;
}
return joinedFields;
};
/* Helper function */
function sortedKeys(dictionary) {
const keys = [];
for (const key in dictionary) {
keys.push(key);
}
return keys.sort();
}
/* Value tags */
export const V_Integer = Symbol.for('V_Integer');
export const V_String = Symbol.for('V_String');
export const V_Tuple = Symbol.for('V_Tuple');
export const V_List = Symbol.for('V_List');
export const V_Structure = Symbol.for('V_Structure');
export class Value {
constructor(tag) {
this._tag = tag;
}
get tag() {
return this._tag;
}
type() {
return new Type(Ty_Unkown);
}
isInteger() {
return this.type().isInteger();
}
isString() {
return this.type().isString();
}
isTuple() {
return this.type().isTuple();
}
isList() {
return this.type().isList();
}
isStructure() {
return this.type().isStructure();
}
isBoolean() {
return this.type().isBoolean();
}
equal(other) {
return false;
}
}
export class ValueInteger extends Value {
constructor(number) {
super(V_Integer);
this._number = typeof number === 'number' ? number : parseInt(number, 10);
}
toString() {
return this._number.toString();
}
get number() {
return this._number;
}
type() {
return new TypeInteger();
}
equal(other) {
return other.tag === V_Integer && this.number === other.number;
}
add(other) {
const a = this._number;
const b = other._number;
return new ValueInteger(a + b);
}
sub(other) {
const a = this._number;
const b = other._number;
return new ValueInteger(a - b);
}
mul(other) {
const a = this._number;
const b = other._number;
return new ValueInteger(a * b === 0 ? 0 : a * b);
}
/* Gobstones calculates quotients using
* modulo (i.e.truncating towards minus infinity)
* rather than
* remainder (i.e.truncating towards 0).
*
* We need to adjust the result to match the standard Gobstones
* semantics, namely:
*
* if a and b have the same sign, then
* a div b = abs(a) / abs(b)
*
* if a and b have different signs, then
* a div b = -((abs(a) + abs(b) - 1) / abs(b))
*
* Here "div" denotes the official Gobstones division operator,
* while "/" denotes the JavaScript/bigint implementation.
*/
div(other) {
const z = new ValueInteger(0);
if (this.gt(z) === other.gt(z)) {
/* Same sign */
const a = this.abs()._number;
const b = other.abs()._number;
const c = Math.floor(a / b) === 0 ? 0 : Math.floor(a / b);
return new ValueInteger(c);
}
else {
/* Different sign */
const inc = other.abs().sub(new ValueInteger(1));
const a = this.abs().add(inc)._number;
const b = other.abs()._number;
const c = Math.floor(a / b) * -1 === 0 ? 0 : Math.floor(a / b) * -1;
return new ValueInteger(c);
}
}
/* Calculate the modulus from the equation a = qb + r,
* i.e. r = a - qb */
mod(other) {
const q = this.div(other);
return this.sub(q.mul(other));
}
/* Assumes that 'other' is non-negative */
pow(other) {
const a = this._number;
const b = other._number;
return new ValueInteger(Math.pow(a, b));
}
eq(other) {
return this.equal(other);
}
ne(other) {
return !this.equal(other);
}
le(other) {
const a = this._number;
const b = other._number;
return a <= b;
}
lt(other) {
const a = this._number;
const b = other._number;
return a < b;
}
ge(other) {
const a = this._number;
const b = other._number;
return a >= b;
}
gt(other) {
const a = this._number;
const b = other._number;
return a > b;
}
negate() {
const a = this._number;
let x = a * -1;
x = x === 0 ? 0 : x;
return new ValueInteger(x);
}
abs() {
if (this.gt(new ValueInteger(0))) {
return this;
}
else {
return this.negate();
}
}
asNumber() {
return this._number;
}
}
export class ValueString extends Value {
constructor(string) {
super(V_String);
this._string = string;
}
toString() {
const res = ['"'];
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < this._string.length; i++) {
const chr = this._string[i];
switch (chr) {
case '"':
res.push('\\');
res.push('"');
break;
case '\\':
res.push('\\');
res.push('\\');
break;
case '\u0007':
res.push('\\');
res.push('a');
break;
case '\b':
res.push('\\');
res.push('b');
break;
case '\f':
res.push('\\');
res.push('f');
break;
case '\n':
res.push('\\');
res.push('n');
break;
case '\r':
res.push('\\');
res.push('r');
break;
case '\t':
res.push('\\');
res.push('t');
break;
case '\v':
res.push('\\');
res.push('v');
break;
default:
res.push(chr);
break;
}
}
res.push('"');
return res.join('');
}
get string() {
return this._string;
}
equal(other) {
return other.tag === V_String && this.string === other.string;
}
type() {
return new TypeString();
}
}
export class ValueTuple extends Value {
constructor(components) {
super(V_Tuple);
this._components = components;
this._type = this._inferType();
}
toString() {
const res = [];
for (const component of this._components) {
res.push(component.toString());
}
return '(' + res.join(', ') + ')';
}
get components() {
return this._components;
}
size() {
return this._components.length;
}
equal(other) {
if (other.tag !== V_Tuple) {
return false;
}
if (this.components.length !== other.components.length) {
return false;
}
for (let i = 0; i < this.components.length; i++) {
if (!this.components[i].equal(other.components[i])) {
return false;
}
}
return true;
}
type() {
return this._type;
}
_inferType() {
const componentTypes = [];
for (const component of this._components) {
componentTypes.push(component.type());
}
return new TypeTuple(componentTypes);
}
}
export class ValueList extends Value {
constructor(elements) {
super(V_List);
this._elements = elements;
this._type = this._inferType();
}
toString() {
const res = [];
for (const element of this._elements) {
res.push(element.toString());
}
return '[' + res.join(', ') + ']';
}
get elements() {
return this._elements;
}
equal(other) {
if (other.tag !== V_List) {
return false;
}
if (this.elements.length !== other.elements.length) {
return false;
}
for (let i = 0; i < this.elements.length; i++) {
if (!this.elements[i].equal(other.elements[i])) {
return false;
}
}
return true;
}
type() {
return this._type;
}
length() {
return this._elements.length;
}
_inferType() {
let contentType = new TypeAny();
for (const element of this._elements) {
contentType = joinTypes(contentType, element.type());
}
return new TypeList(contentType);
}
append(other) {
const allElements = [];
for (const elem of this.elements) {
allElements.push(elem);
}
for (const elem of other.elements) {
allElements.push(elem);
}
return new ValueList(allElements);
}
head() {
return this.elements[0];
}
tail() {
const elements = [];
for (let i = 1; i < this.elements.length; i++) {
elements.push(this.elements[i]);
}
return new ValueList(elements);
}
init() {
const elements = [];
for (let i = 0; i < this.elements.length - 1; i++) {
elements.push(this.elements[i]);
}
return new ValueList(elements);
}
last() {
return this.elements[this.elements.length - 1];
}
}
/* An instance of ValueStructure represents a 'structure' i.e. a value
* inhabiting an 'inductive' datatype.
*
* This includes built-in enumerations (e.g. booleans), the "event" type
* received by an interactive program, and user-defined records and variants.
*
* The second parameter "fields" should be a dictionary mapping field names to
* values
*/
export class ValueStructure extends Value {
constructor(typeName, constructorName, fields) {
super(V_Structure);
this._typeName = typeName;
this._constructorName = constructorName;
this._fields = fields;
}
toString() {
const res = [];
const fieldNames = this.fieldNames();
if (fieldNames.length === 0) {
return this._constructorName;
}
for (const fieldName of fieldNames) {
res.push(fieldName + ' <- ' + this.fields[fieldName].toString());
}
return this._constructorName + '(' + res.join(', ') + ')';
}
get typeName() {
return this._typeName;
}
get constructorName() {
return this._constructorName;
}
get fields() {
return this._fields;
}
fieldNames() {
return sortedKeys(this._fields);
}
_clone() {
const newFields = {};
for (const fieldName in this._fields) {
newFields[fieldName] = this._fields[fieldName];
}
return new ValueStructure(this._typeName, this._constructorName, newFields);
}
updateFields(fields) {
const newStructure = this._clone();
for (const fieldName in fields) {
newStructure.fields[fieldName] = fields[fieldName];
}
return newStructure;
}
equal(other) {
if (other.tag !== V_Structure) {
return false;
}
if (this.constructorName !== other.constructorName) {
return false;
}
const fieldNames = this.fieldNames();
for (const fieldName of fieldNames) {
if (!this.fields[fieldName].equal(other.fields[fieldName])) {
return false;
}
}
return true;
}
type() {
const fieldTypes = {};
for (const fieldName in this._fields) {
fieldTypes[fieldName] = this._fields[fieldName].type();
}
const cases = {};
cases[this._constructorName] = fieldTypes;
return new TypeStructure(this._typeName, cases);
}
}
//# sourceMappingURL=value.js.map