parser for types + start moving all types to separate modules

This commit is contained in:
Joeri Exelmans 2025-05-06 23:41:12 +02:00
parent 8eec5b9239
commit 1d826ea8d4
11 changed files with 277 additions and 88 deletions

15
examples/parser.js Normal file
View file

@ -0,0 +1,15 @@
import { parse } from "../parser/parser.js";
import { prettyGenT, prettyT } from "../util/pretty.js";
console.log(prettyT(parse("Int"))); // Int
console.log(prettyT(parse("Int * Bool"))); // (Int Bool)
console.log(prettyT(parse("(((((((Int)))) => ((Bool)))))"))); // (Int => Bool)
console.log(prettyT(parse("#0((Int * #0) + Unit)"))) // #0((Int #0) + Unit)
console.log(prettyGenT(parse("∀a: #0((a * #0) + Unit"))); // ∀a: #0((a #0) + Unit)
console.log(prettyGenT(parse("∀a,b,c,d: (a*b) + (c*d)"))); // ∀a,b,c,d: ((a b) + (c d))

View file

@ -85,3 +85,12 @@ const unified = unify(
makeGeneric(() => linkedListOfInt)); makeGeneric(() => linkedListOfInt));
console.log(prettyGenT(unified)); // ∀: #0((Int #0) + Unit) console.log(prettyGenT(unified)); // ∀: #0((Int #0) + Unit)
// unification (strange case)
const unified2 = unify(
makeGeneric(() => listOfSetOfSelf),
genericList,
);
console.log(prettyGenT(unified2)); // ∀: #0[{#0}]

View file

@ -96,7 +96,7 @@ export const mergeTwoWay = (m1, m2) => {
// fType, aType: generic types to unify // fType, aType: generic types to unify
// fStack, aStack: internal use. // fStack, aStack: internal use.
const __unify = (typeVars, fType, aType, fStack=[], aStack=[]) => { const __unify = (typeVars, fType, aType, fStack=[], aStack=[]) => {
console.log("__unify", {typeVars, fType, aType, fStack, aStack}); // console.log("__unify", {typeVars, fType, aType, fStack, aStack});
if (typeVars.has(fType)) { if (typeVars.has(fType)) {
// simplest case: formalType is a type paramater // simplest case: formalType is a type paramater
// => substitute with actualType // => substitute with actualType
@ -182,12 +182,13 @@ const __unify = (typeVars, fType, aType, fStack=[], aStack=[]) => {
export const unify = (fGenericType, aGenericType) => { export const unify = (fGenericType, aGenericType) => {
let allTypeVars; let allTypeVars;
[allTypeVars, fGenericType, aGenericType] = safeUnionTypeVars(fGenericType, aGenericType); [allTypeVars, fGenericType, aGenericType] = safeUnionTypeVars(fGenericType, aGenericType);
const {genericType} = __unify(allTypeVars, fGenericType.type, aGenericType.type); const {genericType, substitutions} = __unify(allTypeVars, fGenericType.type, aGenericType.type);
// console.log('unification complete! substitutions:', substitutions);
return recomputeTypeVars(genericType); return recomputeTypeVars(genericType);
}; };
export const substitute = (type, substitutions, stack=[]) => { export const substitute = (type, substitutions, stack=[]) => {
console.log('substitute...', {type, substitutions, stack}); // console.log('substitute...', {type, substitutions, stack});
return substitutions.get(type) return substitutions.get(type)
|| { || {
symbol: type.symbol, symbol: type.symbol,

174
parser/parser.js Normal file
View file

@ -0,0 +1,174 @@
import { Bool, Char, Double, Int, Unit } from "../primitives/types.js";
import { dictType, fnType, lsType, prodType, setType, sumType } from "../structures/types.js";
const bracketOperators = new Map([
['(', [')', null]],
['[', [']', lsType]],
['{', ['}', setType]],
// can only occur at beginning
// we use these to extract the type variables
['∀', [':', null]],
]);
const infixOperators = new Map([
['+', sumType],
['|', sumType],
['', prodType],
['*', prodType],
['→', fnType],
['->', fnType],
['⇒', dictType],
['=>', dictType],
// only used for type variables (e.g., ∀a,b,c:)
[',', fnX => fnY => {
const x = fnX();
const y = fnY();
return Array.isArray(x) ? x.concat(y) : [x].concat(y)
}],
]);
const a = Symbol('a');
const b = Symbol('b');
const c = Symbol('c');
const d = Symbol('d');
const e = Symbol('e');
const primitives = new Map([
['Int', Int],
['Double', Double],
['Bool', Bool],
['Char', Char],
['Unit', Unit],
['a', a],
['b', b],
['c', c],
['d', d],
['e', e],
]);
const TOKENS = [
...bracketOperators.keys(),
...[...bracketOperators.values()].map(v => v[0]),
...infixOperators.keys(),
...primitives.keys(),
];
// console.log('TOKENS =', TOKENS);
const tokenize = expr => {
const tokens = [];
let i=0;
outerloop: while (i<expr.length) {
if (/\s/.test(expr[i])) {
i++;
continue outerloop; // skip whitespace
}
if (expr[i] === '#') {
const label = '#' + parseInt(expr.slice(i+1));
tokens.push(label);
i += label.length;
continue outerloop;
}
for (const token of TOKENS) {
if (expr.startsWith(token, i)) {
tokens.push(token);
i += token.length;
continue outerloop;
}
}
throw new Error(`Couldn't match any token at position ${i} in\n ${expr}\n ${' '.repeat(i)}^`);
}
// console.log({tokens});
return tokens;
}
const consumeGroup = (tokens) => {
const bracket = bracketOperators.get(tokens[0]);
if (bracket === undefined) {
// no group, just a single token:
const [firstToken, ...rest] = tokens;
return [[firstToken], null, rest];
}
else {
// find where group ends:
const [closing, fn] = bracket;
const opening = tokens[0]
let depth = 1;
let i = 1;
for (; i<tokens.length; i++) {
if (tokens[i] === opening) {
depth++;
}
else if (tokens[i] === closing) {
depth--;
}
if (depth === 0) {
break;
}
}
const tokensInGroup = tokens.slice(1, i); // don't include brackets
const rest = tokens.slice(i+1);
return [tokensInGroup, fn, rest];
}
}
const parseGroup = (tokensInGroup, fn, labels, label) => {
// console.log('parseGroup ', tokensInGroup, fn);
return (fn === null)
? __parse(tokensInGroup, labels, label)
: fn(self => {
return __parse(tokensInGroup, extendLabels(labels, label, self));
});
}
const extendLabels = (labels, label, self) => {
return (label === null) ? labels : new Map([...labels, [label, self]])
};
const __parse = (tokens, labels = new Map(), label = null) => {
// console.log('parse ', tokens);
if (tokens[0].startsWith('#')) {
if (labels.has(tokens[0])) {
return labels.get(tokens[0]);
}
else {
// pass label and parse 'rest'
return __parse(tokens.slice(1), labels, tokens[0]);
}
}
if (tokens.length === 1) {
return primitives.get(tokens[0]);
}
else {
const [lhsTokens, fnGrp, rest] = consumeGroup(tokens);
if (rest.length === 0) {
return parseGroup(lhsTokens, fnGrp, labels, label);
}
const [operator, ...rhsTokens] = rest;
for (const [operatorChar, fn] of infixOperators) {
if (operator === operatorChar) {
return fn
(self => {
return parseGroup(lhsTokens, fnGrp, extendLabels(labels, label, self));
})(self => {
return __parse(rhsTokens, extendLabels(labels, label, self));
});
}
}
throw new Error("unknown operator: "+operator)
}
};
export const parse = expr => {
const tokens = tokenize(expr);
if (tokens[0] === '∀') {
// generic type
const [typeVarTokens, _, rest] = consumeGroup(tokens);
const typeVars = [].concat(__parse(typeVarTokens))
const type = __parse(rest);
return { typeVars, type };
}
return __parse(tokens);
}

View file

@ -3,52 +3,8 @@
// A Product-type always has only two fields, called "left" and "right". // A Product-type always has only two fields, called "left" and "right".
// Product-types of more fields (called Structs) can be constructed by nesting Product-types. // Product-types of more fields (called Structs) can be constructed by nesting Product-types.
import { makeGeneric } from "../generics/generics.js";
import { GenericType, Type } from "../primitives/types.js";
import { typedFnType } from "./types.js";
import { prodType } from "./types.js";
// In JS, all products are encoded in the same way: // In JS, all products are encoded in the same way:
export const newProduct = l => r => ({l, r}); export const newProduct = l => r => ({l, r});
export const getLeft = product => product.l; export const getLeft = product => product.l;
export const getRight = product => product.r; export const getRight = product => product.r;
export const ModuleProduct = {l: [
// binary type constructor
// Type -> Type -> Type
...typedFnType(prodType, fnType =>
fnType
(Type)
(fnType
(Type)
(Type)
)
),
// a -> b -> (a, b)
...typedFnType(newProduct, fnType =>
makeGeneric((a, b) =>
fnType
(a)
(fnType
(b)
(prodType(() => a)(() => b))
)
), GenericType),
// (a, b) -> a
...typedFnType(getLeft, fnType =>
makeGeneric((a, b) =>
fnType
(prodType(() => a)(() => b))
(a)
), GenericType),
// (a, b) -> b
...typedFnType(getRight, fnType =>
makeGeneric((a, b) =>
fnType
(prodType(() => a)(() => b))
(b)
), GenericType),
]};

View file

@ -0,0 +1,27 @@
import { makeGeneric } from "../generics/generics.js";
import { Type, GenericType } from "../primitives/types.js";
import { newProduct, getLeft, getRight } from "./product.js";
import { typedFnType, prodType } from "./types.js";
export const ModuleProduct = {
l: [
// binary type constructor
// Type -> Type -> Type
...typedFnType(prodType, fnType => fnType(Type)(fnType(Type)(Type)
)
),
// a -> b -> (a, b)
...typedFnType(newProduct, fnType => makeGeneric((a, b) => fnType(a)(fnType(b)(prodType(() => a)(() => b))
)
), GenericType),
// (a, b) -> a
...typedFnType(getLeft, fnType => makeGeneric((a, b) => fnType(prodType(() => a)(() => b))(a)
), GenericType),
// (a, b) -> b
...typedFnType(getRight, fnType => makeGeneric((a, b) => fnType(prodType(() => a)(() => b))(b)
), GenericType),
]
};

View file

@ -1,7 +1,3 @@
import { fnType, setType } from "./types.js";
import { Int } from "../primitives/types.js";
import { makeGeneric } from "../generics/generics.js";
import createRBTree from "functional-red-black-tree"; import createRBTree from "functional-red-black-tree";
import { inspect } from "node:util"; import { inspect } from "node:util";
@ -9,6 +5,7 @@ export class RBTreeWrapper {
constructor(tree) { constructor(tree) {
this.tree = tree; this.tree = tree;
} }
// pretty print to console
[inspect.custom](depth, options, inspect) { [inspect.custom](depth, options, inspect) {
const entries = []; const entries = [];
this.tree.forEach((key,val) => {entries.push(`${inspect(key)} => ${inspect(val)}`);}); this.tree.forEach((key,val) => {entries.push(`${inspect(key)} => ${inspect(val)}`);});
@ -19,7 +16,6 @@ export class RBTreeWrapper {
// (a -> a -> Int) -> Set(a) // (a -> a -> Int) -> Set(a)
export const emptySet = compareFn => new RBTreeWrapper(createRBTree((x, y) => compareFn(x)(y))); export const emptySet = compareFn => new RBTreeWrapper(createRBTree((x, y) => compareFn(x)(y)));
// const emptySetType = makeGeneric(a => fnType(() => fnType(a)(fnType(a)(Int)))(() => set(() => a)));
export const has = set => key => set.tree.get(key) === true; export const has = set => key => set.tree.get(key) === true;
export const add = set => key => set.tree.get(key) === true ? set : new RBTreeWrapper(set.tree.insert(key, true)); export const add = set => key => set.tree.get(key) === true ? set : new RBTreeWrapper(set.tree.insert(key, true));
@ -42,33 +38,3 @@ export const read = iter => ifNotDone => ifDone => {
export const forEach = set => fn => { export const forEach = set => fn => {
set.tree.forEach(key => { fn(key); }); set.tree.forEach(key => { fn(key); });
}; };
export const ModuleSet = {l:[
// // Type -> Type
// ...typedFnType(setType, fnType =>
// fnType
// /* in */ (Type)
// /* out */ (Type)
// ),
// {i: emptySet , t: emptySetType},
// {i: emptySetType, t: GenericType },
// ...typedFnType(has, fnType =>
// makeGeneric(a =>
// fnType
// /* in */ (set(() => a))
// /* out */ (fnType
// /* in */ (a)
// /* out */ (Bool)
// )), GenericType),
// ...typedFnType(add, fnType =>
// makeGeneric(a =>
// fnType
// /* in */ (set(() => a))
// /* out */ (fnType
// /* in */ (a)
// /* out */ (set(() => a))
// )), GenericType),
]};

31
structures/set.types.js Normal file
View file

@ -0,0 +1,31 @@
import { makeGeneric } from "../generics/generics.js";
import { Int } from "../primitives/types.js";
import { emptySet, has, add } from "./set.js";
import { fnType, setType } from "./types.js";
const emptySetType = makeGeneric(a =>
fnType
// comparison function:
(_ => fnType
(_ => a)
(_ => fnType(_ => a)(_ => Int)))
// the set:
(_ => setType(_ => a))
);
export const ModuleSet = {
l: [
// Type -> Type
...typedFnType(setType, fnType => fnType(_ => Type)(_ => Type)
),
{ i: emptySet, t: emptySetType },
{ i: emptySetType, t: GenericType },
...typedFnType(has, fnType => makeGeneric(a => fnType(_ => setType(_ => a))(_ => fnType(_ => a)(_ => Bool)
)), GenericType),
...typedFnType(add, fnType => makeGeneric(a => fnType(setType(_ => a))(fnType(a)(setType(_ => a))
)), GenericType),
]
};

View file

@ -52,3 +52,8 @@ export const lsType = makeTypeConstructor(symbolList)(1);
export const symbolSet = Symbol('Set'); export const symbolSet = Symbol('Set');
export const setType = makeTypeConstructor(symbolSet)(1); export const setType = makeTypeConstructor(symbolSet)(1);
// Dict type
export const symbolDict = Symbol('Dict');
export const dictType = makeTypeConstructor(symbolDict)(2);

View file

@ -39,12 +39,16 @@ const __makeTypeConstructor = (symbol, nAry, params) => {
if (nAry === 0) { if (nAry === 0) {
return { symbol, params }; return { symbol, params };
} }
return typeParam => { // only for debugging, do we give the function a name
const fName = `${symbol.description.toLowerCase()}Type${params.length>0?params.length:''}`;
return {
[fName]: typeParam => {
if (typeof typeParam !== 'function') { if (typeof typeParam !== 'function') {
throw new Error("all type params must be functions"); throw new Error("all type params must be functions");
} }
return __makeTypeConstructor(symbol, nAry-1, params.concat([typeParam])); return __makeTypeConstructor(symbol, nAry-1, params.concat([typeParam]));
} }
}[fName];
} }
// Creates a new nominal type // Creates a new nominal type

View file

@ -1,5 +1,5 @@
import { inspect } from 'node:util'; import { inspect } from 'node:util';
import { symbolFunction, symbolList, symbolProduct, symbolSet, symbolSum } from '../structures/types.js'; import { symbolDict, symbolFunction, symbolList, symbolProduct, symbolSet, symbolSum } from '../structures/types.js';
import { mapRecursiveStructure } from './util.js'; import { mapRecursiveStructure } from './util.js';
export function pretty(obj) { export function pretty(obj) {
@ -35,6 +35,7 @@ const renderType = (symbol, annot, params) => {
[symbolFunction]: `${annot}(${params[0]} -> ${params[1]})`, [symbolFunction]: `${annot}(${params[0]} -> ${params[1]})`,
[symbolSum] : `${annot}(${params[0]} + ${params[1]})`, [symbolSum] : `${annot}(${params[0]} + ${params[1]})`,
[symbolProduct] : `${annot}(${params[0]} ${params[1]})`, [symbolProduct] : `${annot}(${params[0]} ${params[1]})`,
[symbolDict] : `${annot}(${params[0]} => ${params[1]})`,
}[symbol] || symbol.description; }[symbol] || symbol.description;
}; };