everything seems to work ... but code is dirty

This commit is contained in:
Joeri Exelmans 2025-05-19 23:59:21 +02:00
parent a19dbe1b34
commit 5b6bcf5ffa
10 changed files with 253 additions and 131 deletions

View file

@ -1,4 +1,4 @@
import { Double, fnType, getHumanReadableName, getSymbol, Int, mergeUnifications, NotAFunctionError, occurring, prettyT, prettyU, recomputeTypeVars, reduceUnification, substitute, symbolFunction, trie, TYPE_VARS, UNBOUND_SYMBOLS, UnifyError, unifyLL, transitivelyGrow } from "dope2";
import { Double, fnType, getHumanReadableName, getSymbol, Int, mergeUnifications, NotAFunctionError, occurring, prettyT, prettyU, recomputeTypeVars, reduceUnification, substitute, symbolFunction, trie, TYPE_VARS, UNBOUND_SYMBOLS, UnifyError, unifyLL, transitivelyGrow, isTypeVar, set, compareTypes, recomputeTypeVarsWithInverse } from "dope2";
import type { ExprBlockState } from "./ExprBlock";
import type { InputValueType } from "./InputBlock";
@ -43,7 +43,15 @@ export interface Unknown {
// the value of every block is either known (Dynamic), an error, or unknown
export type ResolvedType = Dynamic | DeepError | Unknown;
export const evalEditorBlock = (s: ExprBlockState, env): ResolvedType => {
// export const evalEditorBlock = (s: ExprBlockState, env: Environment): [ResolvedType,Environment] => {
// const [resolved] = proxyEditorBlock(s, env);
// const [t, newEnv] = recomputeTypeVarsForEnv(resolved.t, env);
// return [{...resolved, t }, newEnv];
// };
class NotFoundError extends Error {}
export const evalEditorBlock = (s: ExprBlockState, env: Environment): [ResolvedType,Environment] => {
if (s.kind === "input") {
return evalInputBlock(s.text, s.value, env);
}
@ -58,121 +66,211 @@ export const evalEditorBlock = (s: ExprBlockState, env): ResolvedType => {
}
};
export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType {
export function evalInputBlock(text: string, value: InputValueType, env: Environment): [ResolvedType,Environment] {
if (value.kind === "literal") {
return parseLiteral(text, value.type, env);
}
else if (value.kind === "name") {
const found = trie.get(env.names)(text);
if (found) {
return found;
if (found.kind === "unknown") {
console.log('returning', text, 'as-is');
return [found, env]; // don't rewrite lambda parameters
}
console.log('rewriting', text);
return recomputeTypeVarsForEnv(text, found, env);
}
}
// kind === "text" -> unresolved
return {
const [t, env2] = makeTypeVar(env, 'err')
return [{
kind: "error",
t: makeTypeVar(env, 'err')[0],
e: new Error(`'${text}' not found`),
t,
e: new NotFoundError(`'${text}' not found`),
depth: 0,
unification: new Map(),
};
}, env2];
}
export function evalCallBlock(fn: ExprBlockState, input: ExprBlockState, env: Environment): [ResolvedType,Environment] {
const [fnResolved, env2] = evalEditorBlock(fn, env);
const [inputResolved, env3] = evalEditorBlock(input, env2);
console.log('==== evalCallBlock ====');
console.log('env :', env);
console.log('fnResolved :', fnResolved);
console.log('env2 :', env2);
console.log('inputResolved:', inputResolved);
console.log('env3 :', env3);
console.log('=======================');
return evalCallBlock2(fnResolved, inputResolved, env3);
}
export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: ResolvedType, env: Environment): [ResolvedType,Environment] {
if (getSymbol(fnResolved.t) !== symbolFunction) {
// not a function...
if (isTypeVar(fnResolved.t)) {
// ... but typeVars are OK (good guys!)
}
else {
// worst outcome
return makeError(env,
new NotAFunctionError(`${prettyT(fnResolved.t)} is not a function type!`),
mergeUnifications(fnResolved.unification, inputResolved.unification),
)
}
}
// it's is a function, continue...
return evalCallBlock3(fnResolved, inputResolved, env);
};
const highestTypeVar = type => {
let highest = -1;
for (const typeVar of occurring(type)) {
highest = Math.max(highest, UNBOUND_SYMBOLS.indexOf(typeVar));
}
return highest;
}
const highestTypeVar2 = typeVars => {
let highest = -1;
for (const typeVar of typeVars) {
highest = Math.max(highest, UNBOUND_SYMBOLS.indexOf(typeVar));
}
return highest;
}
export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: ResolvedType, env): ResolvedType {
if (getSymbol(fnResolved.t) !== symbolFunction) {
// worst outcome: we know nothing about the result!
return makeError(env,
new NotAFunctionError(`${prettyT(fnResolved.t)} is not a function type!`),
mergeUnifications(fnResolved.unification, inputResolved.unification),
)
}
const inverseUnification = (uni, inverse) => {
return new Map([...uni]
.filter(([symbol]) => !inverse.has(inverse.get(symbol)))
.map(([symbol, types]) => [inverse.get(symbol) || symbol, types])
);
}
function recomputeTypeVarsForEnv(name: string, resolved: ResolvedType, env: Environment): [ResolvedType,Environment] {
const [[newType], inverse] = recomputeTypeVarsWithInverse([resolved.t], env.nextFreeTypeVar);
const newResolved: ResolvedType = {
...resolved,
t: newType,
unification: inverseUnification(resolved.unification, inverse),
};
// hacky
const typeVars = env.typeVars.union(occurring(newType)) as Set<string>;
const newEnv: Environment = {
names: trie.insert(env.names)(name)(newResolved),
typeVars,
nextFreeTypeVar: highestTypeVar2(typeVars) + 1,
};
return [newResolved, newEnv];
}
function evalCallBlock3(fnResolved: ResolvedType, inputResolved: ResolvedType, env: Environment): [ResolvedType,Environment] {
try {
// fn is a function...
const [rewrittenFnType] = recomputeTypeVars([fnResolved.t], env.nextFreeTypeVar);
const unification = (unifyLL(rewrittenFnType.params[0](rewrittenFnType), inputResolved.t));
const inputTypeVars = occurring(inputResolved.t);
const fnTypeVars = occurring(fnResolved.t);
const subsetOfUnification = new Map([...unification].filter(([typeVar]) => inputTypeVars.has(typeVar)));
const otherSubSetOfUnification = new Map([...unification].filter(([typeVar]) => fnTypeVars.has(typeVar)));
const outType = substitute(
rewrittenFnType.params[1](rewrittenFnType),
reduceUnification(unification),
[]); // <- not important
const grandUnification = [fnResolved.unification, inputResolved.unification]
.reduce(mergeUnifications, unification);
// turn input in to a function
const [abstractOutputType, env2] = makeTypeVar(env, "<out>");
const matchFnType = fnType(_ => inputResolved.t)(_ => abstractOutputType);
if (IS_DEV) {
console.log('========= evalCallBlock2 =========')
console.log('========= evalCallBlock3 =========')
console.log('env :', env);
console.log('fnKind :', fnResolved.kind);
console.log('inputKind :', inputResolved.kind);
console.log('fnType :', prettyT(fnResolved.t));
console.log('rewrittenFnType :', prettyT(rewrittenFnType));
console.log('inputType :', prettyT(inputResolved.t));
console.log('matchFnType :', prettyT(matchFnType));
}
// unify both functions
const unification = /*transitivelyGrow*/(unifyLL(fnResolved.t, matchFnType));
const unificationR = reduceUnification(unification);
const unifiedFnType = substitute(
// matchFnType,
fnResolved.t,
unificationR, []);
const outType = unifiedFnType.params[1](unifiedFnType);
const newEnv = (outType === abstractOutputType) ? env2 : env;
// we don't want to 'bubble up' our outType substitution, because it's just a temporary variable
const unificationWithoutOutType = new Map([...unification].filter(([symbol]) => symbol !== abstractOutputType.symbol));
if (IS_DEV) {
console.log('unification :', prettyU(unification));
console.log('subsetOfUnification :', prettyU(subsetOfUnification));
console.log('otherSubSetOfUnification:', prettyU(otherSubSetOfUnification));
console.log('unificationInvR :', prettyRU(unificationR));
console.log('unifiedFnType :', prettyT(unifiedFnType));
console.log('outType :', prettyT(outType));
// console.log('inputTypeVars :', `{${[...inputTypeVars].map(getHumanReadableName).join(', ')}}`);
// console.log('fnTypeVars :', `{${[...fnTypeVars].map(getHumanReadableName).join(', ')}}`);
console.log('fn.unification :', prettyU(fnResolved.unification));
console.log('input.unification :', prettyU(inputResolved.unification));
console.log('unificationWithoutOutType:', prettyU(unificationWithoutOutType));
}
const grandUnification = [fnResolved.unification, inputResolved.unification]
.reduce(mergeUnifications, unificationWithoutOutType);
// const grandUnification = unificationWithoutOutType;
if (IS_DEV) {
console.log('grandUnification :', prettyU(grandUnification));
console.log('==================================')
}
if (inputResolved.kind === "error") {
return {
// throw inputResolved.e;
return [{
kind: "error",
e: inputResolved.e, // bubble up the error
depth: 0,
t: outType,
unification: grandUnification,
};
}, newEnv];
}
if (fnResolved.kind === "error") {
// throw fnResolved.e;
// also bubble up
return {
return [{
kind: "error",
e: fnResolved.e,
depth: fnResolved.depth+1,
t: outType,
unification: grandUnification,
};
}, newEnv];
}
// if the above statement did not throw => types are compatible...
if (inputResolved.kind === "value" && fnResolved.kind === "value") {
const outValue = fnResolved.i(inputResolved.i);
return {
console.log('outValue:', outValue);
return [{
kind: "value",
i: outValue,
t: outType,
unification: grandUnification,
};
}, newEnv];
}
else {
// we don't know the value, but we do know the type:
return {
return [{
kind: "unknown",
t: outType,
unification: grandUnification,
};
}, newEnv];
}
}
catch (e) {
if ((e instanceof UnifyError)) {
// if ((e instanceof UnifyError)) {
console.log('UnifyError!', (e as Error).message);
// even though fn was incompatible with the given parameter, we can still suppose that our output-type will be that of fn...?
const outType = fnResolved.t.params[1](fnResolved.t);
try {
return {
return [{
kind: "error",
e,
e: e as Error,
depth: 0,
t: outType,
unification: mergeUnifications(fnResolved.unification, inputResolved.unification), // may throw!
};
}, env];
}
catch (e) {
if ((e instanceof UnifyError)) {
@ -180,46 +278,43 @@ export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: Resolved
}
throw e;
}
}
throw e;
// }
// throw e;
}
};
export function evalCallBlock(fn: ExprBlockState, input: ExprBlockState, env): ResolvedType {
const fnResolved = evalEditorBlock(fn, env);
const inputResolved = evalEditorBlock(input, env);
return evalCallBlock2(fnResolved, inputResolved, env);
}
export function evalLetInBlock(value: ExprBlockState, name: string, inner: ExprBlockState, env): ResolvedType {
const valueResolved = evalEditorBlock(value, env);
export function evalLetInBlock(value: ExprBlockState, name: string, inner: ExprBlockState, env: Environment): [ResolvedType,Environment] {
const [valueResolved] = evalEditorBlock(value, env);
const innerEnv = makeInnerEnv(env, name, valueResolved);
return evalEditorBlock(inner, innerEnv);
}
const prettyRU = (rUni: Map<string, Type>) => {
return '{'+[...rUni].map(([symbol,type]) => `${getHumanReadableName(symbol)} => ${prettyT(type)}`).join(', ')+'}';
}
export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env): ResolvedType {
export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env: Environment): [ResolvedType,Environment] {
const [paramType, staticInnerEnv] = makeTypeVar(env, paramName);
const exprResolved = evalEditorBlock(expr, staticInnerEnv);
const lambdaT = fnType(_ => paramType)(_ => exprResolved.t);
// This is the only place in the code where we actually do something with the 'substitutions'. We compute the type of our lambda function:
const lambdaTSubstituted = substitute(
lambdaT,
reduceUnification(exprResolved.unification),
[]); // <- not important
if (IS_DEV) {
console.log('========= evalLambdaBlock =========')
console.log('paramType :', prettyT(paramType));
console.log('exprType :', prettyT(exprResolved.t));
console.log('lambdaType :', prettyT(lambdaT));
console.log('lambdaTypeSubsituted:', prettyT(lambdaTSubstituted));
console.log('====== begin evalLambdaBlock ======')
console.log('paramName :', paramName);
console.log('paramType :', prettyT(paramType));
console.log('staticInnerEnv:', staticInnerEnv);
console.log('===================================')
}
// console.log('inner kind', exprResolved.kind, paramName);
if (exprResolved.kind === "error") {
return {
const [exprResolved] = evalEditorBlock(expr, staticInnerEnv);
const lambdaT = fnType(_ => paramType)(_ => exprResolved.t);
// This is the only place in the code where we actually do something with the 'substitutions'. We compute the type of our lambda function:
const reduced = reduceUnification(exprResolved.unification);
const lambdaTSubstituted = substitute(
lambdaT,
reduced,
[]); // <- not important
let lambdaResolved: ResolvedType;
if (exprResolved.kind === "error") {
lambdaResolved = {
kind: "error",
t: lambdaTSubstituted,
depth: 0,
@ -227,25 +322,41 @@ export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env): R
unification: exprResolved.unification,
}
}
const paramTypeSubstituted = lambdaTSubstituted.params[0](lambdaTSubstituted);
const fn = (x: any) => {
const innerEnv = makeInnerEnv(env, paramName, {
else {
const paramTypeSubstituted = lambdaTSubstituted.params[0](lambdaTSubstituted);
lambdaResolved = {
kind: "value",
i: x,
t: paramTypeSubstituted,
unification: new Map(),
});
const result = evalEditorBlock(expr, innerEnv);
if (result.kind === "value") {
return result.i;
t: lambdaTSubstituted,
i: (x: any) => {
const innerEnv = makeInnerEnv(env, paramName, {
kind: "value",
i: x,
t: paramTypeSubstituted,
unification: new Map(),
});
const [result] = evalEditorBlock(expr, innerEnv);
if (result.kind === "value") {
return result.i;
}
},
unification: exprResolved.unification,
}
}
return {
kind: "value",
t: lambdaTSubstituted,
i: fn,
unification: exprResolved.unification,
};
// const [lambdaResolvedNormalized, resultEnv] = recomputeTypeVarsForEnv(paramName, lambdaResolved, env);
if (IS_DEV) {
console.log('======= end evalLambdaBlock =======')
console.log('paramType :', prettyT(paramType));
console.log('exprType :', prettyT(exprResolved.t));
console.log('exprUnification :', prettyU(exprResolved.unification));
console.log('exprUnificationR :', prettyRU(reduced));
console.log('lambdaType :', prettyT(lambdaT));
console.log('lambdaTypeSubsituted:', prettyT(lambdaTSubstituted));
// console.log('normalizedT :', prettyT(lambdaResolvedNormalized.t));
console.log('===================================')
}
return [lambdaResolved, env];
}
export function haveValue(resolved: ResolvedType) {
@ -253,7 +364,7 @@ export function haveValue(resolved: ResolvedType) {
return resolved.kind === "value";
}
function parseLiteral(text: string, type: string, env): ResolvedType {
function parseLiteral(text: string, type: string, env: Environment): [ResolvedType,Environment] {
// dirty
if (type === "Int") {
return parseAsInt(text, env);
@ -264,29 +375,29 @@ function parseLiteral(text: string, type: string, env): ResolvedType {
return makeError(env, new Error("Failed to parse"));
}
function parseAsDouble(text: string, env): ResolvedType {
function parseAsDouble(text: string, env: Environment): [ResolvedType,Environment] {
if (text !== '') {
const num = Number(text);
if (!Number.isNaN(num)) {
return {
return [{
kind: "value",
i: num,
t: Double,
unification: new Map(),
};
}, env];
}
}
return makeError(env, new Error("Failed to parse as Double"));
}
function parseAsInt(text: string, env): ResolvedType {
function parseAsInt(text: string, env: Environment): [ResolvedType,Environment] {
if (text !== '') {
try {
return {
return [{
kind: "value",
i: BigInt(text),
t: Int,
unification: new Map(),
}; // may throw
}, env]; // may throw
}
catch {}
}
@ -295,9 +406,10 @@ function parseAsInt(text: string, env): ResolvedType {
const literalParsers = [parseAsDouble, parseAsInt];
export function attemptParseLiteral(text: string, env): Dynamic[] {
export function attemptParseLiteral(text: string, env: Environment): Dynamic[] {
return literalParsers.map(parseFn => parseFn(text, env))
.filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[];
.map(([resolved]) => resolved)
.filter((resolved) => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[];
}
export function scoreResolved(resolved: ResolvedType, outPriority: (s:ResolvedType) => number) {
@ -343,7 +455,7 @@ export function makeTypeVar(env: Environment, name: string): [Type, Environment]
}];
}
function makeError(env: Environment, e: Error, unification: Unification=new Map()): DeepError {
function makeError(env: Environment, e: Error, unification: Unification=new Map()): [DeepError, Environment] {
const idx = env.nextFreeTypeVar;
const typeVar = TYPE_VARS[idx];
const deepError: DeepError = {
@ -353,9 +465,9 @@ function makeError(env: Environment, e: Error, unification: Unification=new Map(
e,
depth: 0,
};
return deepError;
// , {
// names: trie.insert(env.names)('err')(deepError),
// nextFreeTypeVar: idx + 1,
// }];
return [deepError, {
names: env.names,
nextFreeTypeVar: idx + 1,
typeVars: new Set([...env.typeVars, UNBOUND_SYMBOLS[idx]]),
}];
}