progress and some refactoring

This commit is contained in:
Joeri Exelmans 2025-03-31 15:35:02 +02:00
parent d236eca5e5
commit d8ca2f3999
25 changed files with 376 additions and 163 deletions

View file

@ -1,5 +1,6 @@
import { eqType } from "../type.js";
import { pretty, zip } from "../util.js";
import { zip } from "../util/util.js";
import { pretty } from '../util/pretty.js';
// constructor for generic types
// for instance, the type:
@ -22,6 +23,8 @@ export const makeGeneric = callback => {
// From the given set of type variables, return only those that occur in the given type.
export const occurring = (type, typeVars) => {
console.log("occurring", type, typeVars);
if (typeVars.has(type)) {
// type IS a type variable:
return new Set([type]);

View file

@ -1,7 +1,7 @@
import { Bool, Int } from "../primitives/types.js";
import { fnType, lsType } from "../structures/types.js";
import { assign, makeGeneric, unify } from "./generics.js";
import { pretty } from "../util.js";
import { pretty } from "../util/pretty.js";
// a -> Int
const a_to_Int = makeGeneric(a => fnType(a)(Int));

22
lib/symbol.js Normal file
View file

@ -0,0 +1,22 @@
import { constructSymbol, eqSymbol, getName } from "../primitives/symbol.js";
import { Bool, SymbolT, Type } from "../primitives/types.js";
import { String } from "../structures/list.js";
import { typedFnType } from "../structures/types.js";
export const ModuleSymbol = {l:[
{i: SymbolT, t: Type},
...typedFnType(constructSymbol, fnType =>
fnType
(String)
(SymbolT)
),
...typedFnType(getName, fnType =>
fnType
(SymbolT)
(String)
),
...typedFnType(eqSymbol, fnType => fnType(SymbolT, fnType(SymbolT, Bool))),
]};

14
lib/type_constructor.js Normal file
View file

@ -0,0 +1,14 @@
import { Int, SymbolT, Type } from "../primitives/types.js";
import { typedFnType } from "../structures/types.js";
import { makeTypeConstructor } from "../type_constructor.js";
// This function and its type signature cannot be in the same file as 'makeTypeConstructor' because then we get an import cycle among JS modules.
export const ModuleTypeConstructor = {l:[
...typedFnType(makeTypeConstructor, fnType =>
fnType
(SymbolT)
(fnType
(Int)
(Type)
)),
]};

8
primitives/symbol.js Normal file
View file

@ -0,0 +1,8 @@
// The functions are only defined here. For their types, see lib/symbol.js
// The point of having an explicit constructor for SymbolT, is that we can later swap it out for something that is (de-)serializable.
export const constructSymbol = name => Symbol(name);
export const getName = symbol => symbol.description;
export const eqSymbol = a => b => a === b;

View file

@ -1,9 +1,17 @@
// to break up dependency cycles, primitive types are defined in their own JS module
export const Int = { symbol: Symbol('Int') , params: [] };
export const Bool = { symbol: Symbol('Bool') , params: [] };
export const Double = { symbol: Symbol('Double'), params: [] };
export const Byte = { symbol: Symbol('Byte') , params: [] };
export const Char = { symbol: Symbol('Char') , params: [] };
import { makeTypeConstructor } from "../type_constructor.js";
import { constructSymbol } from "./symbol.js";
export const Type = { symbol: Symbol('Type'), params: [] };
export const Int = makeTypeConstructor(constructSymbol('Int'), 0);
export const Bool = makeTypeConstructor(constructSymbol('Bool'), 0);
export const Double = makeTypeConstructor(constructSymbol('Double'), 0);
export const Byte = makeTypeConstructor(constructSymbol('Byte'), 0);
export const Char = makeTypeConstructor(constructSymbol('Char'), 0);
// Unit type has only 1 instance, the empty tuple.
export const Unit = makeTypeConstructor(constructSymbol('Unit'), 0);
export const SymbolT = makeTypeConstructor(constructSymbol('Symbol'), 0);
export const Type = makeTypeConstructor(constructSymbol('Type'), 0);

13
primitives/unit.js Normal file
View file

@ -0,0 +1,13 @@
import { typedFnType } from "../structures/types.js";
import { Bool, Type, Unit } from "./types.js";
const eqUnit = x => y => x === y;
export const ModuleUnit = {l:[
{i: {}, t: Unit},
{i: Unit, t: Type},
// Unit -> Unit -> Bool
...typedFnType(eqUnit, fnType => fnType(Unit)(fnType(Unit)(Bool))),
]};

View file

@ -2,7 +2,7 @@ import { assign, makeGeneric, unify } from "../generics/generics.js";
import { Bool, Int } from "../primitives/types.js";
import { constructorLeft, constructorRight, match } from "../structures/sum.js";
import { fnType, sumType } from "../structures/types.js";
import { pretty } from "../util.js";
import { pretty } from '../util/pretty.js';
const IntOrBool = sumType(Int)(Bool);

View file

@ -1,10 +1,11 @@
import { select } from '@inquirer/prompts';
import { select, number } from '@inquirer/prompts';
import { ModulePoint } from "../lib/point.js";
import { DefaultMap, pretty, prettyT } from '../util.js';
import { symbolFunction } from '../structures/types.js';
import { DefaultMap } from "../util/defaultmap.js";
import { pretty } from '../util/pretty.js';
import { isFunction, prettyT } from '../structures/types.js';
import { ModuleStd } from '../stdlib.js';
import { Type } from "../primitives/types.js";
import { assign, makeGeneric, unify } from '../generics/generics.js';
import { Double, Int, Type } from "../primitives/types.js";
import { eqType } from '../type.js';
class Context {
constructor(mod) {
@ -21,7 +22,7 @@ class Context {
this.functionsTo = new DefaultMap(() => new Set()); // type to incoming function
for (const t of this.instances.m.keys()) {
if (t.symbol === symbolFunction) {
if (isFunction(t)) {
// 't' is a function signature
for (const fn of this.instances.getdefault(t)) {
this.functionsFrom.getdefault(t.params[0], true).add(fn);
@ -67,7 +68,9 @@ let ctx = new Context({l:[
const prettyIT = ({i, t}) => ({
strI: isType(i) ? prettyT(i) : pretty(i),
strT: prettyT(t),
})
// strI: pretty(i),
// strT: pretty(t),
});
const toChoices = ([i, types]) => {
return [...types].map(t => {
@ -131,7 +134,7 @@ async function listAllFunctions() {
...[...ctx.types.m.entries()]
.filter(([i, types]) => {
for (const type of types) {
if (type.symbol === symbolFunction)
if (isFunction(type))
return true;
}
return false;
@ -192,7 +195,7 @@ async function listAllInstances() {
}
async function instanceOrTypeOrFnOptions({i, t}) {
if (t.symbol === symbolFunction) {
if (isFunction(t)) {
return functionOptions(i, t);
}
if (isType(i)) {
@ -257,13 +260,29 @@ async function functionOptions(fn, fnT) {
async function callFunction(fn, fnT) {
const {strI, strT} = prettyIT({i: fn, t: fnT});
const inType = fnT.params[0];
const choice = await select({
const choice = await (async () => {
if (eqType(inType)(Int)) {
const n = await number({
message: `enter an integer (leave empty to go back):`,
step: 1, // only integers
});
return (n === undefined) ? "(go back)" : {i: BigInt(n), t: Int};
}
if (eqType(inType)(Double)) {
const n = await number({
message: `enter a number (leave empty to go back):`,
step: 'any',
});
return (n === undefined) ? "(go back)" : {i: n, t: Int};
}
return select({
message: `select parameter for function ${strI} :: ${strT}`,
choices: [
"(go back)",
... [...ctx.instances.getdefault(inType)].flatMap(i => toChoices([i, ctx.types.getdefault(i)])),
],
});
})();
if (choice === "(go back)") {
return;
}
@ -296,7 +315,7 @@ async function instanceOptions(i,t) {
async function transform(i, t) {
const {strI, strT} = prettyIT({i, t});
console.log(ctx.functionsFrom.getdefault(t));
// console.log(ctx.functionsFrom.getdefault(t));
const choice = await select({
message: `choose transformation to perform on ${strI} :: ${strT}`,

View file

@ -1,8 +1,11 @@
import { ModuleSymbol } from "./lib/symbol.js";
import { ModuleTypeConstructor } from "./lib/type_constructor.js";
import { ModuleBool } from "./primitives/bool.js";
import { ModuleByte } from "./primitives/byte.js";
import { ModuleChar } from "./primitives/char.js";
import { ModuleDouble } from "./primitives/double.js";
import { ModuleInt } from "./primitives/int.js";
import { ModuleUnit } from "./primitives/unit.js";
import { ModuleFunction } from "./structures/function.js";
import { ModuleList } from "./structures/list.js";
import { ModuleProduct } from "./structures/product.js";
@ -14,12 +17,16 @@ export const ModuleStd = {l:[
...ModuleType.l,
...ModuleTyped.l,
...ModuleTypeConstructor.l,
// Primitive types
...ModuleBool.l,
...ModuleByte.l,
...ModuleChar.l,
...ModuleDouble.l,
...ModuleInt.l,
...ModuleSymbol.l,
...ModuleUnit.l,
// Types that consist of other types
...ModuleFunction.l,

View file

@ -1,5 +1,5 @@
import { fnType, typedFnType } from "./types.js";
import { Type } from "../primitives/types.js";
import { typedFnType } from "./types.js";
import { Char, Type } from "../primitives/types.js";
import { Int } from "../primitives/types.js";
import { makeGeneric } from "../generics/generics.js";
import { lsType } from "./types.js";
@ -10,6 +10,8 @@ const get = ls => i => ls.l[i];
const put = ls => i => elem => ({l: ls.l.with(Number(i), elem)});
const push = ls => elem => ({l:ls.l.concat([elem])});
export const String = lsType(Char); // alias
export const ModuleList = {l:[
// Type -> Type
...typedFnType(lsType, fnType =>
@ -55,4 +57,6 @@ export const ModuleList = {l:[
)
)
),
// {i: String, t: Type}, // alias
]};

13
structures/nominal.js Normal file
View file

@ -0,0 +1,13 @@
import { Any } from "../typed.js";
import { String } from "./list.js";
import { sumType, prodType, fnType } from "./types.js";
export const createNominalADT = symbol => variants => {
};
export const createNominalADTFnType =
fnType
(Any)
();

38
structures/set.js Normal file
View file

@ -0,0 +1,38 @@
import { setType, typedFnType } from "./types.js";
import { Bool, Type } from "../primitives/types.js";
import { makeGeneric } from "../generics/generics.js";
// 'normal' implementation
const emptySet = new Set();
const has = set => elem => set.has(elem);
const add = set => elem => new Set([...set, elem]);
export const ModuleList = {l:[
// Type -> Type
...typedFnType(setType, fnType =>
fnType
/* in */ (Type)
/* out */ (Type)
),
{i: emptySet, t: makeGeneric(a => setType(a))},
...typedFnType(has, fnType =>
makeGeneric(a =>
fnType
/* in */ (setType(a))
/* out */ (fnType
/* in */ (a)
/* out */ (Bool)
))),
...typedFnType(add, fnType =>
makeGeneric(a =>
fnType
/* in */ (setType(a))
/* out */ (fnType
/* in */ (a)
/* out */ (setType(a))
))),
]};

View file

@ -1,21 +1,17 @@
// to break up dependency cycles, type constructors are defined in their own JS module
import { Type } from "../primitives/types.js";
import { DefaultMap } from "../util.js";
import { getSymbol, makeTypeConstructor } from "../type_constructor.js";
// Function type
// The registry ensures that we never accidentally create more than one JS object for the same function type.
// It is a cheap workaround for JS lacking customizable hash-functions and equality-testing-functions.
// This same pattern is repeated throughout the code for all non-nullary type constructors (list, sum, product, ...)
export const symbolFunction = Symbol('Function');
const fnTypeRegistry = new DefaultMap(inType => new DefaultMap(outType => ({
symbol: symbolFunction,
params: [inType, outType],
})));
// type constructor
export const fnType = inType => outType => fnTypeRegistry.getdefault(inType, true).getdefault(outType, true);
const symbolFunction = Symbol('Function');
export const fnType = makeTypeConstructor(symbolFunction, 2);
export const isFunction = type => getSymbol(type) === symbolFunction;
// Convenience function. Wrapper around function below.
export const typedFnType = (instance, callback) => {
@ -41,35 +37,48 @@ export const typedFnType2 = callback => {
];
};
// Sum type
export const symbolSum = Symbol("Sum");
const sumTypeRegistry = new DefaultMap(leftType => new DefaultMap(rightType => ({
symbol: symbolSum,
params: [leftType, rightType],
})));
// type constructor
export const sumType = leftType => rightType => sumTypeRegistry.getdefault(leftType, true).getdefault(rightType, true);
const symbolSum = Symbol("Sum");
export const sumType = makeTypeConstructor(symbolSum, 2);
// Product type
export const symbolProduct = Symbol("Product");
const productTypeRegistry = new DefaultMap(leftType => new DefaultMap(rightType => ({
symbol: symbolProduct,
params: [leftType, rightType],
})));
// type constructor
export const prodType = leftType => rightType => productTypeRegistry.getdefault(leftType, true).getdefault(rightType, true);
const symbolProduct = Symbol("Product");
export const prodType = makeTypeConstructor(symbolProduct, 2);
// List type
export const symbolList = Symbol('List');
const listTypeRegistry = new DefaultMap(elementType => ({
symbol: symbolList,
params: [elementType],
}));
// type constructor
export const lsType = elementType => listTypeRegistry.getdefault(elementType, true);
const symbolList = Symbol('List');
export const lsType = makeTypeConstructor(symbolList, 1);
// Set type
const symbolSet = Symbol('Set');
export const setType = makeTypeConstructor(symbolSet, 1);
// Pretty print type
export function prettyT(type) {
if (type.typeVars) {
if (type.typeVars.size > 0) {
return `${[...type.typeVars].map(prettyT).join(", ")}: ${prettyT(type.type)}`;
}
return prettyT(type.type);
}
if (type.symbol === symbolFunction) {
return `${prettyT(type.params[0])} -> ${prettyT(type.params[1])}`;
}
if (type.symbol === symbolList) {
return `[${prettyT(type.params[0])}]`;
}
if (type.symbol === symbolProduct) {
return `(${prettyT(type.params[0])}, ${prettyT(type.params[1])})`;
}
if (type.symbol === symbolSum) {
return `(${prettyT(type.params[0])} | ${prettyT(type.params[1])})`;
}
if (type.params.length === 0) {
return type.symbol.description;
}
return `${type.symbol.description}(${type.params.map(prettyT).join(", ")})`;
}

63
structures/versioned.js Normal file
View file

@ -0,0 +1,63 @@
import { makeGeneric } from "../generics/generics.js";
import { Bool } from "../primitives/types.js";
import { makeTypeConstructor } from "../type_constructor.js";
import { getEq } from "../typeclasses/eq.js";
import { eqDictType } from "../typeclasses/eq_type.js";
import { fnType, setType } from "./types.js";
const symbolVersioned = Symbol("Versioned");
export const versionedType = makeTypeConstructor(symbolVersioned, 1);
export const constructor = parents => value => {
return { parents, alternatives };
}
const constructorType = makeGeneric(a =>
fnType
(a)
(setType(versionedType(a)))
);
// const getValue = v =>
const eq = eqDict => vA => vB => {
return getEq(eqDict)(vA.value,vB.value) // compare values
&& (vA.parents.symmetricDifference(vB.parents).size === 0); // compare parents
}
// EqDict a -> Versioned a -> Versioned a -> Bool
const eqVersioned = makeGeneric(a =>
fnType
(eqDictType(a))
(fnType
(versionedType(a))
(fnType
(versionedType(a))
(Bool)
)
)
);
// EqDict a -> Versioned a -> Versioned a -> Versioned a -> Versioned a
export const mergeThreeWay = eqDict => vLCA => vA => vB => {
if (eq(eqDict)(vLCA)(vA)) {
return vB; // vB successor of vA
}
if (eq(eqDict)(vLCA)(vB)) {
return vA; // vA successor of vB
}
return mergeConcurrent(vLCA)(vA)(vB);
};
export const mergePrimitives = vA => vB => vA.union(vB);
// Versioned a -> Versioned a -> Versioned a
export const mergePrimitivesType = makeGeneric(a =>
fnType
(versionedType(a))
(fnType
(versionedType(a))
(versionedType(a))
)
);

22
type.js
View file

@ -1,12 +1,10 @@
import { Bool, Type } from "./primitives/types.js";
import { typedFnType } from "./structures/types.js";
import { deepEqual } from "./util.js";
export const getSymbol = type => type.symbol;
export const getParams = type => type.params;
import { Bool, SymbolT, Type } from "./primitives/types.js";
import { isFunction, lsType, typedFnType } from "./structures/types.js";
import { getSymbol, getParams } from "./type_constructor.js";
import { deepEqual } from "./util/util.js";
// we can test whether types are equal:
export const eqType = deepEqual;
export const eqType = t1 => t2 => deepEqual(t1, t2);
// a module is just a set of typed objects
// each 'typed object' is implicitly an instance of TypeLink (defined below)
@ -17,13 +15,8 @@ export const ModuleType = {l:[
// ...
// see: https://lean-lang.org/functional_programming_in_lean/functor-applicative-monad/universes.html
// Type :: Type
{i: Type, t: Type},
// ...typedFnType(getSymbol, fnType => fnType({in: Type, out: Int})),
// ...typedFnType(getParams, fnType => fnType({in: Type, out: lsType(Type)})),
// Type -> Type -> Bool
...typedFnType(eqType, fnType =>
fnType
@ -32,4 +25,9 @@ export const ModuleType = {l:[
(Type)
(Bool)
)),
...typedFnType(getSymbol, fnType => fnType(Type)(SymbolT)),
...typedFnType(getParams, fnType => fnType(Type)(lsType(Type))),
...typedFnType(isFunction, fnType => fnType(Type)(Bool)),
]};

15
type_constructor.js Normal file
View file

@ -0,0 +1,15 @@
import { DefaultMap } from "./util/defaultmap.js";
// Creates a new nominal type
export const makeTypeConstructor = (name, n_ary, params = []) => {
if (n_ary === 0) {
return { symbol: Symbol(name), params };
}
else {
const m = new DefaultMap(typeParam => makeTypeConstructor(name, n_ary - 1, params.concat([typeParam])));
return typeParam => m.getdefault(typeParam, true);
}
};
export const getSymbol = type => type.symbol;
export const getParams = type => ({ l: type.params });

View file

@ -1,8 +1,8 @@
import { makeGeneric } from "../generics/generics";
import { Type } from "../primitives/types";
import { SymbolT, Type, Unit } from "../primitives/types";
import { typedFnType } from "../structures/types";
import { Bool, Byte, Char, Double, Int } from "../primitives/types";
import { deepEqual } from "../util";
import { deepEqual } from "../util/util";
import { eqDictType } from "./eq_type";
export const getEq = numDict => numDict.eq;
@ -34,8 +34,10 @@ const EqDict = {eq};
export const EqInstances = new Map([
[Int , EqDict],
[Bool , EqDict],
[Double, EqDict],
[Double , EqDict],
[Byte , EqDict],
[Char , EqDict],
[Unit , EqDict],
[Type , EqDict],
[SymbolT, EqDict],
]);

View file

@ -1,4 +1,4 @@
import { DefaultMap } from "../util.js";
import { DefaultMap } from "../util/defaultmap.js";
const eqDictSymbol = Symbol('EqDict');
const eqDictTypeRegistry = new DefaultMap(a => ({

View file

@ -2,7 +2,7 @@ import { assign } from "../generics/generics.js";
import { makeGeneric } from "../generics/generics.js";
import { Double, Int } from "../primitives/types.js";
import { fnType } from "../structures/types.js";
import { pretty } from "../util.js";
import { pretty } from '../util/pretty.js';
import { getMul, NumInstances } from "./num.js";
import { numDictType } from "./num_type.js";

View file

@ -1,4 +1,4 @@
import { DefaultMap } from "../util.js";
import { DefaultMap } from "../util/defaultmap.js";
const numDictSymbol = Symbol("NumDict");
const numDictTypeRegistry = new DefaultMap(a => ({

79
util.js
View file

@ -1,79 +0,0 @@
import { inspect } from 'node:util';
import { symbolFunction, symbolList, symbolProduct, symbolSum } from './structures/types.js';
// re-inventing the wheel:
export function deepEqual(a, b) {
if (a === b) return true; // <- shallow equality and primitives
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
return false;
}
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) return false;
}
return true;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
// zip two arrays
export function zip(a, b) {
return a.map((k, i) => [k, b[i]]);
}
export class DefaultMap {
constructor(defaultValue, ...rest) {
this.defaultValue = defaultValue;
this.m = new Map(rest);
}
getdefault(key, addToMapping=false) {
return this.m.get(key) || (() => {
const val = this.defaultValue(key);
if (addToMapping)
this.m.set(key, val);
return val;
})();
}
entries() {
return this.m.entries();
}
}
export function pretty(obj) {
return inspect(obj, {colors: true, depth: null});
}
// Pretty print type
export function prettyT(type) {
if (type.typeVars) {
if (type.typeVars.size > 0) {
return `${[...type.typeVars].map(prettyT).join(", ")}: ${prettyT(type.type)}`;
}
return prettyT(type.type);
}
if (type.symbol === symbolFunction) {
return `${prettyT(type.params[0])} -> ${prettyT(type.params[1])}`;
}
if (type.symbol === symbolList) {
return `[${prettyT(type.params[0])}]`;
}
if (type.symbol === symbolProduct) {
return `(${prettyT(type.params[0])}, ${prettyT(type.params[1])})`;
}
if (type.symbol === symbolSum) {
return `(${prettyT(type.params[0])} | ${prettyT(type.params[1])})`;
}
if (type.params.length === 0) {
return type.symbol.description;
}
return `${type.symbol.description}(${type.params.map(prettyT).join(", ")})`;
}

18
util/defaultmap.js Normal file
View file

@ -0,0 +1,18 @@
export class DefaultMap {
constructor(defaultValue, ...rest) {
this.defaultValue = defaultValue;
this.m = new Map(rest);
}
getdefault(key, addToMapping = false) {
return this.m.get(key) || (() => {
const val = this.defaultValue(key);
if (addToMapping)
this.m.set(key, val);
return val;
})();
}
entries() {
return this.m.entries();
}
}

7
util/pretty.js Normal file
View file

@ -0,0 +1,7 @@
import { inspect } from 'node:util';
export function pretty(obj) {
return inspect(obj, { colors: true, depth: null });
}

31
util/util.js Normal file
View file

@ -0,0 +1,31 @@
// re-inventing the wheel:
export function deepEqual(a, b) {
if (a === b) return true; // <- shallow equality and primitives
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
return false;
}
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) return false;
}
return true;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
// zip two arrays
export function zip(a, b) {
return a.map((k, i) => [k, b[i]]);
}