@gobstones/gobstones-lang
Version:
910 lines (787 loc) • 25 kB
text/typescript
/* 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 {
private _tag: symbol;
public constructor(tag: symbol) {
this._tag = tag;
}
public get tag(): symbol {
return this._tag;
}
public isAny(): boolean {
return false;
}
public isInteger(): boolean {
return false;
}
public isString(): boolean {
return false;
}
public isTuple(): boolean {
return false;
}
public isList(): boolean {
return false;
}
public isStructure(): boolean {
return false;
}
public isBoolean(): boolean {
return false;
}
public isColor(): boolean {
return false;
}
public isDirection(): boolean {
return false;
}
}
export class TypeAny extends Type {
public constructor() {
super(Ty_Any);
}
public toString(): string {
return '?';
}
public isAny(): boolean {
return true;
}
}
export class TypeInteger extends Type {
public constructor() {
super(Ty_Integer);
}
public toString(): string {
return i18n('TYPE:Integer');
}
public isInteger(): boolean {
return true;
}
}
export class TypeString extends Type {
public constructor() {
super(Ty_String);
}
public toString(): string {
return i18n('TYPE:String');
}
public isString(): boolean {
return true;
}
}
export class TypeTuple extends Type {
private _componentTypes: any;
public constructor(componentTypes: Type[]) {
super(Ty_Tuple);
this._componentTypes = componentTypes;
}
public get componentTypes(): Type[] {
return this._componentTypes;
}
public toString(): string {
const strings = [];
for (const t of this._componentTypes) {
strings.push(t.toString());
}
return i18n('TYPE:Tuple') + '(' + strings.join(', ') + ')';
}
public isTuple(): boolean {
return true;
}
}
export class TypeList extends Type {
private _contentType: any;
public constructor(contentType: Type) {
super(Ty_List);
this._contentType = contentType;
}
public get contentType(): Type {
return this._contentType;
}
public toString(): string {
let suffix = '';
if (!this._contentType.isAny()) {
suffix = '(' + this._contentType.toString() + ')';
}
return i18n('TYPE:List') + suffix;
}
public isList(): boolean {
return true;
}
}
export class TypeStructure extends Type {
private _typeName: any;
private _cases: any;
public constructor(typeName: string, cases: Record<string, Record<string, Type>>) {
super(Ty_Structure);
this._typeName = typeName;
this._cases = cases;
}
public get typeName(): string {
return this._typeName;
}
public get cases(): Record<string, Record<string, Type>> {
return this._cases;
}
public toString(): string {
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(' | ') + ' }';
}
}
public isStructure(): boolean {
return true;
}
public isBoolean(): boolean {
return this._typeName === i18n('TYPE:Bool');
}
public isColor(): boolean {
return this._typeName === i18n('TYPE:Color');
}
public isDirection(): boolean {
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: Type, type2: Type): Type {
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 as TypeTuple, type2 as TypeTuple);
} else if (type1.tag === Ty_List && type2.tag === Ty_List) {
return joinListTypes(type1 as TypeList, type2 as TypeList);
} else if (type1.tag === Ty_Structure && type2.tag === Ty_Structure) {
return joinStructureTypes(type1 as TypeStructure, type2 as TypeStructure);
} else {
/* Otherwise the types are not joinable */
return undefined;
}
}
const joinTupleTypes = (type1: TypeTuple, type2: TypeTuple): TypeTuple => {
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: TypeList, type2: TypeList): TypeList => {
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: TypeStructure, type2: TypeStructure): TypeStructure => {
if (type1.typeName !== type2.typeName) {
return undefined;
}
const joinedCases = {};
/* Include all the non-common constructors */
function joinCommon(typeA, typeB): void {
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: Record<string, Type>,
fields2: Record<string, Type>
): Record<string, Type> => {
/* Ensure that they have the same set of fields */
function checkIncluded(fieldsA: Record<string, Type>, fieldsB: Record<string, Type>): void {
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: Record<string, unknown>): string[] {
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 {
private _tag: symbol;
public constructor(tag: symbol) {
this._tag = tag;
}
public get tag(): symbol {
return this._tag;
}
public type(): Type {
return new Type(Ty_Unkown);
}
public isInteger(): boolean {
return this.type().isInteger();
}
public isString(): boolean {
return this.type().isString();
}
public isTuple(): boolean {
return this.type().isTuple();
}
public isList(): boolean {
return this.type().isList();
}
public isStructure(): boolean {
return this.type().isStructure();
}
public isBoolean(): boolean {
return this.type().isBoolean();
}
public equal(other: Value): boolean {
return false;
}
}
export class ValueInteger extends Value {
private _number: number;
public constructor(number: number | string) {
super(V_Integer);
this._number = typeof number === 'number' ? number : parseInt(number, 10);
}
public toString(): string {
return this._number.toString();
}
public get number(): number {
return this._number;
}
public type(): Type {
return new TypeInteger();
}
public equal(other: Value): boolean {
return other.tag === V_Integer && this.number === (other as ValueInteger).number;
}
public add(other: ValueInteger): ValueInteger {
const a = this._number;
const b = other._number;
return new ValueInteger(a + b);
}
public sub(other: ValueInteger): ValueInteger {
const a = this._number;
const b = other._number;
return new ValueInteger(a - b);
}
public mul(other: ValueInteger): ValueInteger {
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.
*/
public div(other: ValueInteger): ValueInteger {
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 */
public mod(other: ValueInteger): ValueInteger {
const q = this.div(other);
return this.sub(q.mul(other));
}
/* Assumes that 'other' is non-negative */
public pow(other: ValueInteger): ValueInteger {
const a = this._number;
const b = other._number;
return new ValueInteger(a ** b);
}
public eq(other: ValueInteger): boolean {
return this.equal(other);
}
public ne(other: ValueInteger): boolean {
return !this.equal(other);
}
public le(other: ValueInteger): boolean {
const a = this._number;
const b = other._number;
return a <= b;
}
public lt(other: ValueInteger): boolean {
const a = this._number;
const b = other._number;
return a < b;
}
public ge(other: ValueInteger): boolean {
const a = this._number;
const b = other._number;
return a >= b;
}
public gt(other: ValueInteger): boolean {
const a = this._number;
const b = other._number;
return a > b;
}
public negate(): ValueInteger {
const a = this._number;
let x = a * -1;
x = x === 0 ? 0 : x;
return new ValueInteger(x);
}
public abs(): ValueInteger {
if (this.gt(new ValueInteger(0))) {
return this;
} else {
return this.negate();
}
}
public asNumber(): number {
return this._number;
}
}
export class ValueString extends Value {
private _string: string;
public constructor(string: string) {
super(V_String);
this._string = string;
}
public toString(): string {
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('');
}
public get string(): string {
return this._string;
}
public equal(other: Value): boolean {
return other.tag === V_String && this.string === (other as ValueString).string;
}
public type(): Type {
return new TypeString();
}
}
export class ValueTuple extends Value {
private _components: Value[];
private _type: TypeTuple;
public constructor(components: Value[]) {
super(V_Tuple);
this._components = components;
this._type = this._inferType();
}
public toString(): string {
const res = [];
for (const component of this._components) {
res.push(component.toString());
}
return '(' + res.join(', ') + ')';
}
public get components(): Value[] {
return this._components;
}
public size(): number {
return this._components.length;
}
public equal(other: Value): boolean {
if (other.tag !== V_Tuple) {
return false;
}
if (this.components.length !== (other as ValueTuple).components.length) {
return false;
}
for (let i = 0; i < this.components.length; i++) {
if (!this.components[i].equal((other as ValueTuple).components[i])) {
return false;
}
}
return true;
}
public type(): Type {
return this._type;
}
public _inferType(): TypeTuple {
const componentTypes = [];
for (const component of this._components) {
componentTypes.push(component.type());
}
return new TypeTuple(componentTypes);
}
}
export class ValueList extends Value {
private _elements: Value[];
private _type: Type;
public constructor(elements: Value[]) {
super(V_List);
this._elements = elements;
this._type = this._inferType();
}
public toString(): string {
const res = [];
for (const element of this._elements) {
res.push(element.toString());
}
return '[' + res.join(', ') + ']';
}
public get elements(): Value[] {
return this._elements;
}
public equal(other: Value): boolean {
if (other.tag !== V_List) {
return false;
}
if (this.elements.length !== (other as ValueList).elements.length) {
return false;
}
for (let i = 0; i < this.elements.length; i++) {
if (!this.elements[i].equal((other as ValueList).elements[i])) {
return false;
}
}
return true;
}
public type(): Type {
return this._type;
}
public length(): number {
return this._elements.length;
}
public _inferType(): Type {
let contentType = new TypeAny();
for (const element of this._elements) {
contentType = joinTypes(contentType, element.type());
}
return new TypeList(contentType);
}
public append(other: ValueList): ValueList {
const allElements = [];
for (const elem of this.elements) {
allElements.push(elem);
}
for (const elem of other.elements) {
allElements.push(elem);
}
return new ValueList(allElements);
}
public head(): Value {
return this.elements[0];
}
public tail(): ValueList {
const elements = [];
for (let i = 1; i < this.elements.length; i++) {
elements.push(this.elements[i]);
}
return new ValueList(elements);
}
public init(): ValueList {
const elements = [];
for (let i = 0; i < this.elements.length - 1; i++) {
elements.push(this.elements[i]);
}
return new ValueList(elements);
}
public last(): Value {
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 {
private _typeName: string;
private _constructorName: string;
private _fields: Record<string, Value>;
public constructor(typeName: string, constructorName: string, fields: Record<string, Value>) {
super(V_Structure);
this._typeName = typeName;
this._constructorName = constructorName;
this._fields = fields;
}
public toString(): string {
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(', ') + ')';
}
public get typeName(): string {
return this._typeName;
}
public get constructorName(): string {
return this._constructorName;
}
public get fields(): Record<string, Value> {
return this._fields;
}
public fieldNames(): string[] {
return sortedKeys(this._fields);
}
public _clone(): ValueStructure {
const newFields = {};
for (const fieldName in this._fields) {
newFields[fieldName] = this._fields[fieldName];
}
return new ValueStructure(this._typeName, this._constructorName, newFields);
}
public updateFields(fields: Record<string, Value>): ValueStructure {
const newStructure = this._clone();
for (const fieldName in fields) {
newStructure.fields[fieldName] = fields[fieldName];
}
return newStructure;
}
public equal(other: Value): boolean {
if (other.tag !== V_Structure) {
return false;
}
if (this.constructorName !== (other as ValueStructure).constructorName) {
return false;
}
const fieldNames = this.fieldNames();
for (const fieldName of fieldNames) {
if (!this.fields[fieldName].equal((other as ValueStructure).fields[fieldName])) {
return false;
}
}
return true;
}
public type(): 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);
}
}