interactive prompt can handle polymorphic types

This commit is contained in:
Joeri Exelmans 2025-04-02 15:49:43 +02:00
parent a0e3aa0cb3
commit 4a4983f693
20 changed files with 485 additions and 276 deletions

View file

@ -1,160 +1,180 @@
import { eqType } from "../type.js";
import { eqType } from "../primitives/type.js";
import { zip } from "../util/util.js";
import { pretty } from '../util/pretty.js';
import { prettyT } from "../structures/types.js";
// constructor for generic types
// for instance, the type:
// a -> a -> Bool
// ∀a: a -> a -> Bool
// is created by
// makeGeneric(a => fnType({in: a, out: fnType({in: a, out: Bool})}))
// makeGeneric(a => fnType(a)(fnType(a)(Bool)))
export const makeGeneric = callback => {
// type variables to make available:
const typeVars = ['a', 'b', 'c', 'd', 'e'].map(
letter => ({
symbol: Symbol(letter),
params: [],
}));
const typeVars = ['a', 'b', 'c', 'd', 'e'].map(Symbol);
const type = callback(...typeVars);
return {
typeVars: occurring(type, new Set(typeVars)),
type,
};
return onlyOccurring(type, new Set(typeVars));
};
export const onlyOccurring = (type, typeVars) => ({
typeVars: occurring(type, typeVars),
type,
});
// 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);
if (typeVars.has(type)) {
// type IS a type variable:
return new Set([type]);
}
return new Set(type.params.flatMap(p => [...occurring(p, typeVars)]));
}
};
// Merge 2 substitution-mappings, uni-directional.
const mergeOneWay = (m1, m2) => {
const m1copy = new Map(m1);
const m2copy = new Map(m2);
for (const [key1, val1] of m1copy.entries()) {
if (m2copy.has(val1)) {
m1copy.set(key1, m2.get(val1));
m2copy.delete(val1);
return [false, m1copy, m2copy, new Set([val1])];
for (const [var1, typ1] of m1copy.entries()) {
if (m2copy.has(typ1)) {
// typ1 is a typeVar for which we also have a substitution
// -> fold substitutions
m1copy.set(var1, m2.get(typ1));
m2copy.delete(typ1);
return [false, m1copy, m2copy, new Set([typ1])];
}
}
return [true, m1copy, m2copy, new Set()]; // stable
}
};
const checkConflict = (m1, m2) => {
for (const [var1, typ1] of m1) {
if (m2.has(var1)) {
const other = m2.get(var1);
if (!eqType(typ1, other)) {
throw new Error(`conflicting substitution: ${pretty(typ1)}vs. ${pretty(other)}`);
}
}
}
};
// Merge 2 substitution-mappings, bi-directional.
export const mergeTwoWay = (m1, m2) => {
// check for conflicts:
for (const [typeVar, actual] of m1) {
if (m2.has(typeVar)) {
const other = m2.get(typeVar);
if (!eqType(actual, other)) {
throw new Error(`conflicting substitution: ${pretty(actual)}vs. ${pretty(other)}`);
}
}
}
// console.log("mergeTwoWay", {m1, m2});
checkConflict(m1, m2);
// checkConflict(m2, m1); // <- don't think this is necessary...
// actually merge
let stable = false;
let deletedTypeVars = new Set();
let deleted = new Set();
while (!stable) {
let d;
// notice we swap m2 and m1, so the rewriting can happen both ways:
[stable, m2, m1, d] = mergeOneWay(m1, m2);
deletedTypeVars = deletedTypeVars.union(d);
deleted = deleted.union(d);
}
return [new Map([...m1, ...m2]), deletedTypeVars];
}
const result = {
substitutions: new Map([...m1, ...m2]),
deleted, // deleted type variables
};
// console.log("mergeTwoWay result =", result);
return result;
};
// Thanks to Hans for pointing out that this algorithm exactly like "Unification" in Prolog (hence the function name):
// https://www.dai.ed.ac.uk/groups/ssp/bookpages/quickprolog/node12.html
export const unify = (
{typeVars: formalTypeVars, type: formalType},
{typeVars: actualTypeVars, type: actualType},
) => {
// console.log("unify", pretty({formalTypeVars, formalType, actualTypeVars, actualType}));
const unifyInternal = (typeVars, fType, aType) => {
// console.log("unify", pretty({typeVars, fType, aType}));
if (formalTypeVars.has(formalType)) {
if (typeVars.has(fType)) {
// simplest case: formalType is a type paramater
// => substitute with actualType
// console.log("assign actual to formal");
return {
substitutions: new Map([[formalType, actualType]]),
typeVars: new Set([
...actualTypeVars,
...[...formalTypeVars].filter(a => a !== formalType),
]),
type: actualType,
substitutions: new Map([[fType, aType]]),
genericType: {
typeVars: typeVars.difference(new Set([fType])),
type: aType,
},
};
}
if (actualTypeVars.has(actualType)) {
if (typeVars.has(aType)) {
// same as above, but in the other direction
// console.log("assign formal to actual");
return {
substitutions: new Map([[actualType, formalType]]),
typeVars: new Set([
...[...actualTypeVars].filter(a => a !== actualType),
...formalTypeVars,
]),
type: formalType,
substitutions: new Map([[aType, fType]]),
genericType: {
typeVars: typeVars.difference(new Set([aType])),
type: fType,
},
};
}
// recursively unify
if (formalType.symbol !== actualType.symbol) {
throw new Error(`cannot unify ${pretty(formalType.symbol)} and ${pretty(actualType.symbol)}`);
if (fType.symbol !== aType.symbol) {
throw new Error(`cannot unify ${prettyT(fType)} and ${prettyT(aType)}`);
}
else {
// console.log("symbols match - unify recursively", formalType.symbol);
const unifiedParams = zip(formalType.params, actualType.params).map(([formalParam, actualParam]) => unify({typeVars: formalTypeVars, type: formalParam}, {typeVars: actualTypeVars, type: actualParam}));
const [unifiedSubstitusions, deleted] = unifiedParams.reduce(([substitutionsSoFar, deletedSoFar], cur) => {
// console.log('merging', substitutionsSoFar, cur.substitutions);
const [newSubstitutions, deleted] = mergeTwoWay(substitutionsSoFar, cur.substitutions);
return [newSubstitutions, deletedSoFar.union(deleted)];
}, [new Map(), new Set()]);
const unifiedTypeVars = new Set([
...actualTypeVars,
...formalTypeVars,
].filter(a => !unifiedSubstitusions.has(a) && !deleted.has(a)));
// console.log("symbols match - unify recursively", formal.symbol);
const unifiedParams =
zip(fType.params, aType.params)
.map(([fParam, aParam]) => unifyInternal(typeVars, fParam, aParam));
const {substitutions, deleted} =
unifiedParams.reduce(({substitutions: s, deleted: d}, cur) => {
// console.log('merging', s, cur.substitutions);
const {substitutions, deleted} = mergeTwoWay(s, cur.substitutions);
return {
substitutions: unifiedSubstitusions,
typeVars: unifiedTypeVars,
substitutions,
deleted: deleted.union(d),
};
}, { substitutions: new Map(), deleted: new Set() });
// console.log(pretty({unifiedParams}));
return {
substitutions,
genericType: {
typeVars: typeVars.difference(substitutions).difference(deleted),
type: {
symbol: formalType.symbol,
params: unifiedParams.map(p => p.type),
symbol: fType.symbol,
params: unifiedParams.map(p => p.genericType.type),
},
},
};
}
};
export const unify = (fGenericType, aGenericType) => {
const {genericType} = unifyInternal(
fGenericType.typeVars.union(aGenericType.typeVars),
fGenericType.type,
aGenericType.type,
)
return genericType;
}
export const substitute = (type, substitutions) => {
// console.log("substitute", {type, substitutions})
if (substitutions.has(type)) {
// type IS a type var to be substituted:
return substitutions.get(type);
}
if (type.params.length === 0) {
// Attention: there's a reason why we have this special case.
// Types are compared by object ID, so we don't want to create a new object for a type that takes no type parameters (then the newly create type would differ).
// Should fix this some day.
return type;
if (typeof type === "symbol") {
return type; // nothing to substitute here
}
return {
symbol: type.symbol,
params: type.params.map(p => substitute(p, substitutions)),
};
}
};
export const assign = (genFnType, paramType) => {
const [inType, outType] = genFnType.type.params;
const matchedInType = unify({
typeVars: genFnType.typeVars,
type: inType,
}, paramType);
const substitutedOutType = substitute(outType, matchedInType.substitutions);
return {
typeVars: matchedInType.typeVars,
type: substitutedOutType,
};
const allTypeVars = genFnType.typeVars.union(paramType.typeVars)
const {substitutions} = unifyInternal(allTypeVars, inType, paramType.type);
const substitutedOutType = substitute(outType, substitutions);
return onlyOccurring(substitutedOutType, allTypeVars);
};
export const assignFn = (genFnType, paramType) => {
const [inType] = genFnType.type.params;
const allTypeVars = genFnType.typeVars.union(paramType.typeVars)
const {substitutions} = unifyInternal(allTypeVars, inType, paramType.type);
// console.log({genFnType: prettyT(genFnType), paramType: prettyT(paramType), substitutions})
const substitutedFnType = substitute(genFnType.type, substitutions);
return onlyOccurring(substitutedFnType, allTypeVars);
}

View file

@ -1,21 +1,20 @@
import { Bool, Int } from "../primitives/types.js";
import { fnType, lsType } from "../structures/types.js";
import { fnType, lsType, prettyT } from "../structures/types.js";
import { assign, makeGeneric, unify } from "./generics.js";
import { pretty } from "../util/pretty.js";
// a -> Int
const a_to_Int = makeGeneric(a => fnType(a)(Int));
// Bool -> Int
const Bool_to_Int = makeGeneric(() => fnType(lsType(Bool))(Int));
console.log("should be: [Bool] -> Int")
console.log(pretty(unify(a_to_Int, Bool_to_Int)));
console.log(prettyT(unify(a_to_Int, Bool_to_Int)));
// (a -> a) -> b
const fnType2 = makeGeneric((a,b) => fnType(fnType(a)(a))(b));
// (Bool -> Bool) -> a
const fnType3 = makeGeneric(a => fnType(fnType(Bool)(Bool))(a));
console.log("should be: (Bool -> Bool) -> a");
console.log(pretty(unify(fnType2, fnType3)));
console.log(prettyT(unify(fnType2, fnType3)));
// (a -> b) -> [a] -> [b]
const mapFnType = makeGeneric((a,b) =>
@ -23,10 +22,10 @@ const mapFnType = makeGeneric((a,b) =>
(fnType(a)(b))
(fnType(lsType(a))(lsType(b))))
// a -> a
const idFnType = makeGeneric(a =>
fnType(a)(a));
console.log("should be: [a] -> [a]");
console.log(pretty(assign(mapFnType, idFnType)));
const idFnType = makeGeneric((_,__,c) =>
fnType(c)(c));
console.log("should be: [c] -> [c]");
console.log(prettyT(assign(mapFnType, idFnType)));
// (a -> Int) -> [a] -> a
const weirdFnType = makeGeneric(a =>
@ -40,4 +39,4 @@ const weirdFnType = makeGeneric(a =>
// a := b
// b := Int
console.log("should be: [Int] -> Int");
console.log(pretty(assign(weirdFnType, idFnType)));
console.log(prettyT(assign(weirdFnType, idFnType)));

View file

@ -3,20 +3,17 @@ import { Bool, SymbolT, Type } from "../primitives/types.js";
import { String } from "../structures/list.js";
import { typedFnType } from "../structures/types.js";
// The way instances of SymbolT are currently encoded, their constructor is not a valid DOPE function, because it is impure.
// The only way to construct symbols is to do it in JS code.
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))),
...typedFnType(eqSymbol, fnType => fnType(SymbolT)(fnType(SymbolT)(Bool))),
]};

View file

@ -0,0 +1,21 @@
import { constructorLeft, constructorRight } from "../structures/sum.js";
import { fnType, setType, sumType, typedFnType } from "../structures/types.js";
import { GenericType, SymbolT, Type, Unit } from "./types.js";
import { unit } from "./unit.js";
export const getType = genericType => genericType.type;
export const getTypeVars = genericType => genericType.typeVars;
export const toNonGeneric = genericType => (genericType.typeVars.size === 0)
? constructorRight(genericType.type)
: constructorLeft(unit);
export const ModuleGenericType = {l:[
{i: GenericType, t: Type},
...typedFnType(getType, fnType => fnType(GenericType)(Type)),
...typedFnType(getTypeVars, fnType => fnType(GenericType)(setType(SymbolT))),
...typedFnType(toNonGeneric, fnType => fnType(GenericType)(sumType(Unit)(Type))),
]};

View file

@ -1,7 +1,7 @@
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";
import { Bool, SymbolT, Type } from "./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 = t1 => t2 => deepEqual(t1, t2);

View file

@ -10,6 +10,7 @@ const SymbolChar = Symbol('Char');
const SymbolUnit = Symbol('Unit');
const SymbolSymbol = Symbol('Symbol');
const SymbolType = Symbol('Type');
const SymbolGenericType = Symbol('GenericType');
export const Int = makeTypeConstructor(SymbolInt)(0);
export const Bool = makeTypeConstructor(SymbolBool)(0);
@ -24,6 +25,8 @@ export const SymbolT = makeTypeConstructor(SymbolSymbol)(0);
export const Type = makeTypeConstructor(SymbolType)(0);
export const GenericType = makeTypeConstructor(SymbolGenericType)(0);
export const ModuleSymbols = {l:[
{i: SymbolInt , t: SymbolT},
@ -34,4 +37,5 @@ export const ModuleSymbols = {l:[
{i: SymbolUnit , t: SymbolT},
{i: SymbolSymbol, t: SymbolT},
{i: SymbolType , t: SymbolT},
{i: SymbolGenericType, t: SymbolT},
]};

View file

@ -1,10 +1,12 @@
import { typedFnType } from "../structures/types.js";
import { Bool, Type, Unit } from "./types.js";
const eqUnit = x => y => x === y;
export const eqUnit = x => y => x === y;
export const unit = {};
export const ModuleUnit = {l:[
{i: {}, t: Unit},
{i: unit, t: Unit},
{i: Unit, t: Type},

View file

@ -4,8 +4,37 @@ 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 { Double, Int, SymbolT, Type } from "../primitives/types.js";
import { eqType } from '../type.js';
import { Double, GenericType, Int, SymbolT, Type } from "../primitives/types.js";
import { eqType } from '../primitives/type.js';
import { Any } from '../typed.js';
import { assign, assignFn, makeGeneric, onlyOccurring } from '../generics/generics.js';
// import {emitKeypressEvents} from 'node:readline';
// // Configure readline to read from stdin
// emitKeypressEvents(process.stdin);
// process.stdin.setRawMode(true);
// console.log('Press any key (ESC to exit)...');
// process.stdin.on('keypress', (str, key) => {
// if (key.name === 'escape') {
// console.log('Escape key pressed!');
// process.exit();
// }
// });
const prettyIT = ({i, t}) => ({
strI: isType(i) ? prettyT(i) : pretty(i),
strT: prettyT(t),
// strI: pretty(i),
// strT: pretty(t),
});
const isType = i => i.typeVars || i.symbol;
// ctx.types.getdefault(i).has(Type)
// || ctx.types.getdefault(i).has(GenericType);
class Context {
constructor(mod) {
@ -14,64 +43,44 @@ class Context {
this.instances = new DefaultMap(() => new Set()); // type to instance
for (const {i, t} of mod.l) {
const {strI, strT} = prettyIT({i,t})
// console.log(strI, '::', strT);
this.types.getdefault(i, true).add(t);
this.types.getdefault(i, true).add(Any);
this.instances.getdefault(t, true).add(i);
this.instances.getdefault(Any, true).add(i);
}
this.functionsFrom = new DefaultMap(() => new Set()); // type to outgoing function
this.functionsTo = new DefaultMap(() => new Set()); // type to incoming function
for (const t of this.instances.m.keys()) {
const addIfFunctionType = (t, originalT, add) => {
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);
this.functionsTo .getdefault(t.params[1], true).add(fn);
for (const fn of this.instances.getdefault(originalT)) {
add(fn);
}
}
}
// this.typeVarAssigns = new Map();
// for (const t of this.instances.m.keys()) {
// if (t.typeVars) {
// for (const t2 of this.instances.m.keys()) {
// const genericT2 = (t2.typeVars === undefined)
// ? makeGeneric(() => t2)
// : t2;
// try {
// const unification = unify(t, t2);
// console.log(unification);
// } catch (e) {
// // skip
// }
// }
// }
// }
this.functions = [];
for (const type of this.instances.getdefault(Type)) {
addIfFunctionType(type, type, fn => this.functions.push({fn, type}));
}
this.genericFunctions = [];
for (const genericType of this.instances.getdefault(GenericType)) {
addIfFunctionType(genericType.type, genericType, fn => this.genericFunctions.push({fn, genericType}));
}
}
addToCtx({i, t}) {
return new Context({l:[
...this.mod.l,
{i, t},
]})
]});
}
}
let ctx = new Context({l:[
...ModuleStd.l,
...ModulePoint.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 => {
const {strI, strT} = prettyIT({i, t});
@ -82,50 +91,33 @@ const toChoices = ([i, types]) => {
short: `${strI} :: ${strT}`,
};
});
}
const isType = i => ctx.types.getdefault(i).has(Type);
};
async function topPrompt() {
const action = await select({
message: "What do you want to do?",
choices: [
"list all types",
"list all generic types",
"list all functions",
"list all",
],
});
if (action === "list all types") {
await listInstances(Type);
// await listAllTypes();
}
if (action === "list all generic types") {
await listInstances(GenericType);
}
if (action === "list all functions") {
await listAllFunctions();
}
if (action === "list all") {
await listAllInstances();
await listAll();
}
return topPrompt();
}
// async function listAllTypes() {
// const choice = await select({
// message: "select type:",
// choices: [
// "(go back)",
// ...[...ctx.instances.m.keys()].map(t => ({
// value: t,
// name: prettyT(t),
// })),
// ]
// });
// if (choice === "(go back)") {
// return;
// }
// await typeOptions(choice);
// return listAllTypes();
// }
async function listAllFunctions() {
const choice = await select({
message: "select function:",
@ -150,11 +142,12 @@ async function listAllFunctions() {
return listAllFunctions();
}
async function typeOptions(t) {
async function typeOptions(t, tt) {
const choice = await select({
message: `actions for type ${prettyT(t)} :: Type`,
message: `actions for type ${prettyT(t)} :: ${prettyT(tt)}`,
choices: [
"(go back)",
"create instance",
"list instances",
// "list outgoing functions",
// "list incoming functions",
@ -165,6 +158,13 @@ async function typeOptions(t) {
if (choice === "(go back)") {
return;
}
else if (choice === "create instance") {
const i = await createInstance(t);
if (i !== undefined) {
ctx = ctx.addToCtx({i, t});
return instanceOrTypeOrFnOptions({i, t});
}
}
else if (choice === "list instances") {
await listInstances(t);
}
@ -172,15 +172,15 @@ async function typeOptions(t) {
console.log(pretty(t));
}
else if (choice === "treat as instance") {
await instanceOptions(t, Type)
await instanceOptions(t, tt)
}
else {
console.log("unimplemented:", choice);
}
return typeOptions(t);
return typeOptions(t, tt);
}
async function listAllInstances() {
async function listAll() {
const choice = await select({
message: `all instances:`,
choices: [
@ -195,11 +195,16 @@ async function listAllInstances() {
}
async function instanceOrTypeOrFnOptions({i, t}) {
if (t.typeVars) {
if (isFunction(t.type)) {
return functionOptions(i, t);
}
}
if (isFunction(t)) {
return functionOptions(i, t);
}
if (isType(i)) {
return typeOptions(i);
if (t === Type || t === GenericType) {
return typeOptions(i, t);
}
return instanceOptions(i,t);
}
@ -230,8 +235,8 @@ async function listTypes(i) {
if (choice === "(go back)") {
return;
}
const {i: chosenType} = choice;
await typeOptions(chosenType);
const {i: chosenType, t: typeOfChosenType} = choice;
await typeOptions(chosenType, typeOfChosenType);
return listTypes(i);
}
@ -257,23 +262,11 @@ async function functionOptions(fn, fnT) {
return 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({
message: `select parameter for function ${strI} :: ${strT}`,
choices: [
"(go back)",
"(new)",
... [...ctx.instances.getdefault(inType)].flatMap(i => toChoices([i, ctx.types.getdefault(i)])),
],
});
let i;
if (choice === "(go back)") {
return;
async function createInstance(t) {
if (t.typeVars && t.typeVars.size === 0) {
t = t.type; // can treat as non-generic
}
if (choice === "(new)") {
if (eqType(inType)(Int)) {
if (eqType(t)(Int)) {
const n = await number({
message: `enter an integer (leave empty to go back):`,
step: 1, // only integers
@ -281,29 +274,86 @@ async function callFunction(fn, fnT) {
if (n === undefined) {
return;
}
i = BigInt(n);
return BigInt(n);
}
else if (eqType(inType)(Double)) {
else if (eqType(t)(Double)) {
const n = await number({
message: `enter a number (leave empty to go back):`,
step: 'any',
});
if (n === undefined) {
return;
return n;
}
i = n;
}
else if (eqType(inType)(SymbolT)) {
else if (eqType(t)(SymbolT)) {
console.log("Note: you are creating a new Symbol. Even if the description matches that of another symbol (e.g., \"Int\"), a new Symbol will be created that is unique and only equal to itself.");
const symbolDescr = await input({message: "enter symbol description:"});
i = Symbol(symbolDescr);
return Symbol(symbolDescr);
}
else {
console.log("no prompt handler for creating new", prettyT(inType));
return callFunction(fn, fnT);
console.log("no prompt handler for creating new", prettyT(t));
}
}
async function callFunction(fn, fnT) {
const {strI, strT} = prettyIT({i: fn, t: fnT});
let choices;
let inType;
if (fnT.typeVars) {
// generic
choices = [...ctx.types.m.entries()].flatMap(([i, types]) => {
return [...types].flatMap(t => {
const genT = t.typeVars ? t : makeGeneric(() => t);
let assignedFnType;
try {
assignedFnType = assignFn(fnT, genT);
} catch (e) {
if (e.message.startsWith("cannot unify")) {
// console.warn(e);
return [];
}
throw e;
}
const assignedInType = onlyOccurring(assignedFnType.type.params[0], assignedFnType.typeVars);
if (assignedInType.typeVars.size > 0) {
return toChoices([i, [assignedInType]]);
}
else {
return toChoices([i, [assignedInType.type]]);
}
});
});
inType = onlyOccurring(fnT.type.params[0], fnT.typeVars)
}
else {
inType = fnT.params[0];
choices = [...ctx.instances.getdefault(inType)].flatMap(i => toChoices([i, ctx.types.getdefault(i)]));
}
const choice = await select({
message: `select parameter of type ${prettyT(inType)} for function ${strI} :: ${strT}`,
choices: [
"(go back)",
"(new)",
...choices,
],
});
let i, t;
if (choice === "(go back)") {
return;
}
else if (choice === "(new)") {
i = await createInstance(inType);
t = inType;
if (i === undefined) {
return;
}
}
await apply(i, fn, fnT);
else {
i = choice.i;
t = choice.t;
}
const genT = t.typeVars ? t : makeGeneric(() => t);
const assignedFnType = assignFn(fnT, genT);
await apply(i, fn, assignedFnType);
return callFunction(fn, fnT);
}
@ -333,11 +383,34 @@ async function transform(i, t) {
const {strI, strT} = prettyIT({i, t});
// console.log(ctx.functionsFrom.getdefault(t));
const genT = t.typeVars ? t : makeGeneric(() => t);
const choice = await select({
message: `choose transformation to perform on ${strI} :: ${strT}`,
choices: [
"(go back)",
... [...ctx.functionsFrom.getdefault(t)].flatMap(fn => toChoices([fn, ctx.types.getdefault(fn)])),
...ctx.functions
.filter(({type}) => {
// console.log(type.params[0], t);
return eqType(type.params[0])(t)
})
.flatMap(({fn, type}) => toChoices([fn, [type]])),
...ctx.genericFunctions
.flatMap(({fn, genericType}) => {
let fnType;
try {
fnType = assignFn(genericType, genT);
} catch (e) {
if (e.message.startsWith("cannot unify")) {
// console.warn(e);
return [];
}
throw e;
}
return toChoices([fn, [fnType]]);
}),
],
});
if (choice === "(go back)") {
@ -352,9 +425,18 @@ async function transform(i, t) {
async function apply(i, fn, fnT) {
const result = fn(i);
// console.log(fn, '(', i, ')', '=', result);
const resultType = fnT.params[1];
let resultType;
// console.log(fnT);
if (fnT.typeVars) {
resultType = onlyOccurring(fnT.type.params[1], fnT.typeVars);
}
else {
resultType = fnT.params[1];
}
// update context with newly produced value
ctx = ctx.addToCtx({i: result, t: resultType});
ctx = ctx
.addToCtx({i: result, t: resultType})
.addToCtx({i: resultType, t: resultType.typeVars ? GenericType : Type});
const {strI: strResult, strT: strResultType} = prettyIT({i: result, t: resultType});
console.log(`result = ${strResult} :: ${strResultType}`);
return instanceOrTypeOrFnOptions({i: result, t: resultType});

View file

@ -11,11 +11,14 @@ import { ModuleFunction } from "./structures/function.js";
import { ModuleList } from "./structures/list.js";
import { ModuleProduct } from "./structures/product.js";
import { ModuleSum } from "./structures/sum.js";
import { ModuleType } from "./type.js";
import { ModuleType } from "./primitives/type.js";
import { ModuleTyped } from "./typed.js";
import { ModuleSet } from "./structures/set.js";
import { ModuleGenericType } from "./primitives/generic_type.js";
export const ModuleStd = {l:[
...ModuleType.l,
...ModuleGenericType.l,
...ModuleTyped.l,
...ModuleTypeConstructor.l,
@ -35,4 +38,5 @@ export const ModuleStd = {l:[
...ModuleList.l,
...ModuleProduct.l,
...ModuleSum.l,
...ModuleSet.l,
]};

View file

@ -1,16 +1,19 @@
import { typedFnType } from "./types.js";
import { Char, Type } from "../primitives/types.js";
import { Char, GenericType, Type } from "../primitives/types.js";
import { Int } from "../primitives/types.js";
import { makeGeneric } from "../generics/generics.js";
import { lsType } from "./types.js";
import { Typed } from "../typed.js"
// 'normal' implementation
const emptyList = {l:[]};
const emptyListType = makeGeneric(a => lsType(a));
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 Module = lsType(Typed);
export const ModuleList = {l:[
// Type -> Type
@ -21,7 +24,8 @@ export const ModuleList = {l:[
),
// [a]
{i: emptyList, t: makeGeneric(a => lsType(a))},
{i: emptyList, t: emptyListType},
{i: emptyListType, t: GenericType},
// [a] -> Int -> a
...typedFnType(get, fnType =>
@ -31,7 +35,7 @@ export const ModuleList = {l:[
/* out */ (fnType
/* in */ (Int)
/* out */ (a)
))),
)), GenericType),
// [a] -> Int -> a -> [a]
...typedFnType(put, fnType =>
@ -44,7 +48,7 @@ export const ModuleList = {l:[
/* in */ (a)
/* out */ (lsType(a))
)
))),
)), GenericType),
// [a] -> a -> [a]
...typedFnType(push, fnType =>
@ -55,8 +59,5 @@ export const ModuleList = {l:[
(a)
(lsType(a))
)
)
),
// {i: String, t: Type}, // alias
), GenericType),
]};

View file

@ -1,13 +1,76 @@
import { Any } from "../typed.js";
import { String } from "./list.js";
import { sumType, prodType, fnType } from "./types.js";
import { SymbolT, Type } from "../primitives/types.js";
import { makeTypeConstructor } from "../type_constructor.js";
import { Module, String } from "./list.js";
import { prodType, fnType, lsType } from "./types.js";
function capitalizeFirstLetter(val) {
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
}
export const createNominalADT = symbol => variants => {
makeTypeConstructor(symbol, 0, )
export const createStruct = (typeVars, symbol, fields) => {
const makeConstructor = (remainingFields, obj={}) => {
if (remainingFields.length===0) {
return obj;
}
const {left: fieldName} = remainingFields[remainingFields.length-1];
return v => makeConstructor(
remainingFields.slice(0,-1),
Object.assign({[fieldName]: v}, obj));
};
const constructor = makeConstructor(fields);
const type = makeTypeConstructor(symbol)(typeVars.size);
const types = [ type ];
const recordFnType = inType => outType => {
const fnT = fnType(inType)(outType);
types.push(fnT);
return fnT;
}
const makeConstructorType = (remainingFields, type) => {
if (remainingFields.length===0) {
return type;
}
const {right: fieldType} = remainingFields[remainingFields.length-1];
return recordFnType(makeConstructorType(remainingFields.slice(0,-1)))(fieldType);
};
const constructorType = makeConstructorType(fields);
const functions = [
["constructor", constructor, constructorType],
...fields.map(({left: fieldName, right: fieldType}) => {
const getterName = 'get'+capitalizeFirstLetter(fieldName);
const getter = {
// stupid trick to give the JS-function a computed name.
// only important for debugging, so it says [Function: getAge] instead of [Function (anonymous)]:
[getterName]: obj => obj[fieldName],
}[getterName];
if (typeVars.has(fieldType)) {
// getterFnType = recordFnType(type)(fieldType)
}
const getterFnType = recordFnType(type)(fieldType);
return [fieldName, getter, getterFnType];
}),
];
const module = {l:[
{i: type, t: Type},
...functions.flatMap(([_, getter, getterFnType]) => [
{i: getter , t: getterFnType},
]),
...types.map(type => ({i: type, t: Type})),
]};
return {
module,
constructor,
functions: Object.fromEntries(functions),
};
};
export const createNominalADTFnType =
fnType
(Any)
();
export const createNominalADTModuleFnType =
fnType(SymbolT)
(fnType(lsType(prodType(String)(Type)))
(Module));

View file

@ -1,5 +1,5 @@
import { makeGeneric } from "../generics/generics.js";
import { Type } from "../primitives/types.js";
import { GenericType, Type } from "../primitives/types.js";
import { typedFnType } from "./types.js";
import { prodType } from "./types.js";
@ -29,7 +29,7 @@ export const ModuleProduct = {l: [
(b)
(prodType(a)(b))
)
)),
), GenericType),
// (a, b) -> a
...typedFnType(getLeft, fnType =>
@ -37,7 +37,7 @@ export const ModuleProduct = {l: [
fnType
(prodType(a)(b))
(a)
)),
), GenericType),
// (a, b) -> b
...typedFnType(getRight, fnType =>
@ -45,5 +45,5 @@ export const ModuleProduct = {l: [
fnType
(prodType(a)(b))
(b)
)),
), GenericType),
]};

View file

@ -1,13 +1,14 @@
import { setType, typedFnType } from "./types.js";
import { Bool, Type } from "../primitives/types.js";
import { Bool, GenericType, Type } from "../primitives/types.js";
import { makeGeneric } from "../generics/generics.js";
// 'normal' implementation
const emptySet = new Set();
const emptySetType = makeGeneric(a => setType(a));
const has = set => elem => set.has(elem);
const add = set => elem => new Set([...set, elem]);
export const ModuleList = {l:[
export const ModuleSet = {l:[
// Type -> Type
...typedFnType(setType, fnType =>
fnType
@ -15,7 +16,8 @@ export const ModuleList = {l:[
/* out */ (Type)
),
{i: emptySet, t: makeGeneric(a => setType(a))},
{i: emptySet , t: emptySetType},
{i: emptySetType, t: GenericType },
...typedFnType(has, fnType =>
makeGeneric(a =>
@ -24,7 +26,7 @@ export const ModuleList = {l:[
/* out */ (fnType
/* in */ (a)
/* out */ (Bool)
))),
)), GenericType),
...typedFnType(add, fnType =>
makeGeneric(a =>
@ -33,6 +35,5 @@ export const ModuleList = {l:[
/* out */ (fnType
/* in */ (a)
/* out */ (setType(a))
))),
)), GenericType),
]};

View file

@ -1,5 +1,5 @@
import { prodType } from "./types.js";
import { Type } from "../primitives/types.js";
import { GenericType, Type } from "../primitives/types.js";
import { typedFnType } from "./types.js";
import { makeGeneric } from "../generics/generics.js";
import { sumType } from "./types.js";
@ -31,7 +31,7 @@ export const ModuleSum = {l:[
fnType
(a)
(sumType(a)(b))
)),
), GenericType),
// b -> a | b
...typedFnType(constructorRight, fnType =>
@ -39,7 +39,7 @@ export const ModuleSum = {l:[
fnType
(b)
(sumType(a)(b))
)),
), GenericType),
// a | b -> (a -> c, b-> c) -> c
...typedFnType(match, fnType =>
@ -53,5 +53,5 @@ export const ModuleSum = {l:[
)
(c)
)
)),
), GenericType),
]};

View file

@ -13,17 +13,8 @@ export const fnType = makeTypeConstructor(symbolFunction)(2);
export const isFunction = type => getSymbol(type) === symbolFunction;
// Convenience function. Wrapper around function below.
export const typedFnType = (instance, callback) => {
const [t, typesOfFns] = typedFnType2(callback);
const res = [
{ i: instance, t },
...typesOfFns,
];
return res;
};
// Convenience function. Creates a function type, and also create Type-links for the function type (being typed by Function) and for all the nested function types. Saves a lot of code writing.
export const typedFnType2 = callback => {
export const typedFnType = (instance, callback, typeOfType = Type) => {
const fnTs = [];
const wrappedFnType = inType => outType => {
const fnT = fnType(inType)(outType);
@ -31,10 +22,15 @@ export const typedFnType2 = callback => {
return fnT;
};
const t = callback(wrappedFnType); // force evaluation
return [
t,
fnTs.map(fnT => ({ i: fnT, t: Type })),
if (t.typeVars && typeOfType === Type) {
throw new Error("you probably meant to create a GenericType");
}
const res = [
{ i: instance, t },
{ i: t , t: typeOfType },
// ...fnTs.map(fnT => ({ i: fnT, t: Type })),
];
return res;
};
// Sum type
@ -57,15 +53,21 @@ export const lsType = makeTypeConstructor(symbolList)(1);
const symbolSet = Symbol('Set');
export const setType = makeTypeConstructor(symbolSet)(1);
// Pretty print type
export function prettyT(type) {
// console.log("pretty:", type);
if (typeof type === "symbol") {
return type.description;
}
if (type.typeVars) {
if (type.typeVars.size > 0) {
return `${[...type.typeVars].map(prettyT).join(", ")}: ${prettyT(type.type)}`;
return `(${[...type.typeVars].map(prettyT).join(", ")}): ${prettyT(type.type)}`;
}
else {
return prettyT(type.type);
}
}
if (type.symbol === symbolFunction) {
return `${prettyT(type.params[0])} -> ${prettyT(type.params[1])}`;
}

View file

@ -9,17 +9,22 @@ const symbolVersioned = Symbol("Versioned");
export const versionedType = makeTypeConstructor(symbolVersioned)(1);
export const constructor = parents => value => {
export const constructor = parents => alternatives => {
return { parents, alternatives };
}
const constructorType = makeGeneric(a =>
fnType
(a)
(setType(versionedType(a)))
(fnType
(setType(a))
(versionedType(a))
)
);
// const getValue = v =>
const initial = x => ({ parents: new Set(), alternatives: new Set(x) });
const initialFnType = makeGeneric(a => fnType(a)(versionedType(a)));
const eq = eqDict => vA => vB => {
return getEq(eqDict)(vA.value,vB.value) // compare values

View file

@ -1,6 +1,11 @@
import { DefaultMap } from "./util/defaultmap.js";
const nullaryTypeConstructors = new DefaultMap(symbol => ({symbol, params: []})); // symbol -> 0-ary type constructor (= a type, basically)
const nullaryTypeConstructors = new DefaultMap(
// symbol -> 0-ary type constructor (= a type, basically)
symbol => ({
symbol,
params: [],
}));
const makeTypeConstructorInternal = (symbol, n_ary, params = []) => {
// console.log("n_ary:", n_ary);

View file

@ -1,5 +1,5 @@
import { makeGeneric } from "../generics/generics";
import { SymbolT, Type, Unit } from "../primitives/types";
import { GenericType, SymbolT, Type, Unit } from "../primitives/types";
import { typedFnType } from "../structures/types";
import { Bool, Byte, Char, Double, Int } from "../primitives/types";
import { deepEqual } from "../util/util";
@ -22,7 +22,7 @@ export const ModuleEq = {l:[
(a)
(Bool)
)
))),
)), GenericType),
]};
// all our data (and types) are encoded such that we can test equality the same way:

View file

@ -1,18 +1,22 @@
import { typedFnType } from "./structures/types.js";
import { Type } from "./primitives/types.js";
import { makeTypeConstructor } from "./type_constructor.js";
// Everything is (implicitly) typed by the Any type.
export const Any = { symbol: Symbol('Any'), params: [] };
const symbolAny = Symbol('Any');
export const Any = makeTypeConstructor(symbolAny)(0);
// A type-link, connecting a value to its Type.
export const Typed = { symbol: Symbol('Typed'), params: [] };
const symbolTyped = Symbol('Typed');
export const Typed = makeTypeConstructor(symbolTyped)(0);
const getInst = lnk => lnk.i;
const getType = lnk => lnk.t;
export const ModuleTyped = {l:[
{i: Typed, t: Type},
{i: Any , t: Type},
...typedFnType(getInst, fnType => fnType(Typed)(Type)),
...typedFnType(getType, fnType => fnType(Typed)(Type)),
...typedFnType(getInst, fnType => fnType(Typed)(Any)),
...typedFnType(getType, fnType => fnType(Typed)(Any)),
]};

View file

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