UNPKG

@gobstones/gobstones-lang

Version:
910 lines (787 loc) 25 kB
/* 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); } }