diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 089da2d..726ebde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 5.2.5 dope2: specifier: git+https://deemz.org/git/joeri/dope2.git - version: git+https://deemz.org/git/joeri/dope2.git#d3515d39a51d0baf9fcc775b8d2fb74c74e24868 + version: git+https://deemz.org/git/joeri/dope2.git#1fe40858444cc4d0f9e4ba4223b280aef0ea4fb2 react: specifier: ^19.1.0 version: 19.1.0 @@ -597,8 +597,8 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dope2@git+https://deemz.org/git/joeri/dope2.git#d3515d39a51d0baf9fcc775b8d2fb74c74e24868: - resolution: {commit: d3515d39a51d0baf9fcc775b8d2fb74c74e24868, repo: https://deemz.org/git/joeri/dope2.git, type: git} + dope2@git+https://deemz.org/git/joeri/dope2.git#1fe40858444cc4d0f9e4ba4223b280aef0ea4fb2: + resolution: {commit: 1fe40858444cc4d0f9e4ba4223b280aef0ea4fb2, repo: https://deemz.org/git/joeri/dope2.git, type: git} version: 0.0.1 esbuild@0.25.4: @@ -1431,7 +1431,7 @@ snapshots: deep-is@0.1.4: {} - dope2@git+https://deemz.org/git/joeri/dope2.git#d3515d39a51d0baf9fcc775b8d2fb74c74e24868: + dope2@git+https://deemz.org/git/joeri/dope2.git#1fe40858444cc4d0f9e4ba4223b280aef0ea4fb2: dependencies: functional-red-black-tree: 1.0.1 diff --git a/src/EnvContext.ts b/src/EnvContext.ts index f2d100e..9717ae4 100644 --- a/src/EnvContext.ts +++ b/src/EnvContext.ts @@ -1,13 +1,20 @@ import { createContext } from "react"; import { getDefaultTypeParser, module2Env, ModuleStd } from "dope2"; +import type { Dynamic, Environment } from "./eval"; export const functionWith3Params = i => j => k => i+j+k; export const functionWith4Params = i => j => k => l => i+j+k+l; const mkType = getDefaultTypeParser(); -export const extendedEnv = module2Env(ModuleStd.concat([ - ["functionWith3Params", { i: functionWith3Params, t: mkType("Int->Int->Int->Int") }], - ["functionWith4Params", { i: functionWith4Params, t: mkType("Int->Int->Int->Int->Int")}] -])); + +export const extendedEnv: Environment = { + names: module2Env(ModuleStd.concat([ + ["functionWith3Params", { i: functionWith3Params, t: mkType("Int->Int->Int->Int") }], + ["functionWith4Params", { i: functionWith4Params, t: mkType("Int->Int->Int->Int->Int")}] + ]).map(([name, {i,t}]) => [name, { kind: "value", i, t, unification: new Map() }] as [string, Dynamic]) + ).name2dyn, + nextFreeTypeVar: 0, + typeVars: new Set(), +}; export const EnvContext = createContext(extendedEnv); \ No newline at end of file diff --git a/src/InputBlock.tsx b/src/InputBlock.tsx index 3ea0345..8151a9f 100644 --- a/src/InputBlock.tsx +++ b/src/InputBlock.tsx @@ -44,7 +44,7 @@ const computeSuggestions = (text, env, suggestionPriority: (s: ResolvedType) => ... literals.map((lit) => ["literal", text, lit]), // names - ... trie.suggest(env.name2dyn)(text)(Infinity) + ... trie.suggest(env.names)(text)(Infinity) .map(([name,type]) => [ "name", name, { @@ -53,8 +53,7 @@ const computeSuggestions = (text, env, suggestionPriority: (s: ResolvedType) => kind: type.kind || "value", }]), ] - // return ls; - return []; + return []; // <-- uncomment to disable suggestions (useful for debugging) return ls .map(suggestion => [suggestionPriority(suggestion[2]), ...suggestion] as PrioritizedSuggestionType) .sort(([priorityA], [priorityB]) => priorityB - priorityA) @@ -67,7 +66,7 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In const [i, setI] = useState(0); // selected suggestion idx const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not - const singleSuggestion = trie.growPrefix(env.name2dyn)(text); + const singleSuggestion = trie.growPrefix(env.names)(text); const suggestions = useMemo(() => computeSuggestions(text, env, suggestionPriority), [text, suggestionPriority, env]); useEffect(() => autoInputWidth(inputRef, text+singleSuggestion), [inputRef, text, singleSuggestion]); diff --git a/src/LambdaBlock.tsx b/src/LambdaBlock.tsx index 1db6261..eb9592a 100644 --- a/src/LambdaBlock.tsx +++ b/src/LambdaBlock.tsx @@ -1,13 +1,14 @@ import { useContext, useEffect, useRef } from "react"; -import { growEnv } from "dope2"; +import { eqType, getSymbol, reduceUnification, trie } from "dope2"; import { ExprBlock, type ExprBlockState, type State2Props } from "./ExprBlock"; import { EnvContext } from "./EnvContext"; -import { getUnusedTypeVar } from "./eval"; +import { evalEditorBlock, makeInnerEnv, makeTypeVar } from "./eval"; import { autoInputWidth } from "./util/dom_trickery"; import "./LambdaBlock.css"; +import { Type } from "./Type"; export interface LambdaBlockState { kind: "lambda"; @@ -47,11 +48,20 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr useEffect(() => autoInputWidth(nameRef, state.paramName, 60), [nameRef, state.paramName]); - const innerEnv = growEnv(env)(state.paramName)({ - kind: "unknown", - i: undefined, - t: getUnusedTypeVar(env), - }); + const [paramType, staticInnerEnv] = makeTypeVar(env, state.paramName); + + const exprResolved = evalEditorBlock(state.expr, staticInnerEnv); + + const inferredParamType = reduceUnification(exprResolved.unification).get(getSymbol(paramType)) || paramType; + + const betterInnerEnv = eqType(paramType)(inferredParamType) + ? staticInnerEnv + : makeInnerEnv(env, state.paramName, { + kind: "unknown", + t: inferredParamType, + unification: new Map(), // <- is this correct? + }) + return λ @@ -66,11 +76,14 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr onChange={e => setParamName(e.target.value)} /> +
+  ::  +
  :  
- + ; +} interface Type { symbol: string; params: any[]; }; +type Unification = Map; + export interface DeepError { kind: "error"; e: Error; depth: number; t: Type; - substitutions: Map; + unification: Unification; } // a dynamically typed value = tuple (instance, type) @@ -21,21 +29,15 @@ export interface Dynamic { kind: "value", i: any; t: Type; - substitutions: Map; + unification: Unification; }; export interface Unknown { kind: "unknown"; t: Type; - substitutions: Map; + unification: Unification; } -export const entirelyUnknown = env => ({ - kind: "unknown", - t: getUnusedTypeVar(env), - substitutions: new Map(), -} as Unknown); - // the value of every block is either known (Dynamic), an error, or unknown export type ResolvedType = Dynamic | DeepError | Unknown; @@ -43,17 +45,15 @@ export const evalEditorBlock = (s: ExprBlockState, env): ResolvedType => { if (s.kind === "input") { return evalInputBlock(s.text, s.value, env); } - if (s.kind === "call") { + else if (s.kind === "call") { return evalCallBlock(s.fn, s.input, env); } - if (s.kind === "let") { + else if (s.kind === "let") { return evalLetInBlock(s.value, s.name, s.inner, env); } - if (s.kind === "lambda") { + else { // (s.kind === "lambda") return evalLambdaBlock(s.paramName, s.expr, env); - } - return entirelyUnknown(env); // todo }; export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType { @@ -61,56 +61,57 @@ export function evalInputBlock(text: string, value: InputValueType, env): Resolv return parseLiteral(text, value.type, env); } else if (value.kind === "name") { - const found = trie.get(env.name2dyn)(text); + const found = trie.get(env.names)(text); if (found) { - return { - kind: found.kind || "value", - ...found, - substitutions: found.substitutions || new Map(), - }; + return found; } } // kind === "text" -> unresolved return { kind: "error", - t: getUnusedTypeVar(env), + t: makeTypeVar(env, 'err')[0], e: new Error(`'${text}' not found`), depth: 0, - substitutions: new Map(), + unification: new Map(), }; } -const mergeMaps = (...maps: Map[]) => { - return new Map(maps.flatMap(m => [...m])); -} export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: ResolvedType, env): ResolvedType { if (getSymbol(fnResolved.t) !== symbolFunction) { // worst outcome: we know nothing about the result! - return { - kind: "error", - e: new NotAFunctionError(`${prettyT(fnResolved.t)} is not a function type!`), - t: getUnusedTypeVar(env), - substitutions: mergeMaps(fnResolved.substitutions, inputResolved.substitutions), - depth: 0, - }; + return makeError(env, + new NotAFunctionError(`${prettyT(fnResolved.t)} is not a function type!`), + mergeUnifications(fnResolved.unification, inputResolved.unification), + ) } try { // fn is a function... - const [_inType, inSubst, outType, _outSubst] = assignFnSubstitutions(fnResolved.t, inputResolved.t, getUnusedTypeVarIdx(env)); // may throw + const [rewrittenFnType] = recomputeTypeVars([fnResolved.t], env.nextFreeTypeVar); + const unification = unifyLL(rewrittenFnType.params[0](rewrittenFnType), inputResolved.t); - console.log('==================================') - console.log('fnResolvedT:', prettyT(fnResolved.t)); - console.log('inputResolvedT:', prettyT(inputResolved.t)); - console.log('_inType:',prettyT(_inType)); - console.log('inSubst:', [...inSubst].map(([key,val]) => [key,prettyT(val)])); - console.log('outType:',prettyT(outType)); - console.log('_outSubst:', [..._outSubst].map(([key,val]) => [key,prettyT(val)])); + const inputTypeVars = occurring(inputResolved.t); + const subsetOfUnification = new Map([...unification].filter(([typeVar]) => inputTypeVars.has(typeVar))); + + const outType = substitute( + rewrittenFnType.params[1](rewrittenFnType), + reduceUnification(subsetOfUnification), + []); // <- not important + + console.log('========= evalCallBlock2 =========') + console.log('fnType :', prettyT(fnResolved.t)); + console.log('rewrittenFnType :', prettyT(rewrittenFnType)); + console.log('inputType :', prettyT(inputResolved.t)); + console.log('outType :', prettyT(outType)); + console.log('unification :', prettyU(unification)); + console.log('inputTypeVars :', [...inputTypeVars].map(getHumanReadableName)); + console.log('fn.unification :', prettyU(fnResolved.unification)); + console.log('input.unification :', prettyU(inputResolved.unification)); + console.log('subsetOfUnification:', prettyU(subsetOfUnification)); console.log('==================================') - // console.log('assignFn...', 'fn.t:', prettyT(fnResolved.t), 'input:', inputResolved, 'input.t:', prettyT(inputResolved.t), '\nout =', prettyT(outType), 'subst:', substitutions, substitutions.size); - - const mergedSubstitutions = mergeMaps(inSubst, fnResolved.substitutions, inputResolved.substitutions); + const grandUnification = [fnResolved.unification, inputResolved.unification] + .reduce(mergeUnifications, subsetOfUnification); if (inputResolved.kind === "error") { return { @@ -118,7 +119,7 @@ export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: Resolved e: inputResolved.e, // bubble up the error depth: 0, t: outType, - substitutions: mergedSubstitutions, + unification: grandUnification, }; } if (fnResolved.kind === "error") { @@ -128,7 +129,7 @@ export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: Resolved e: fnResolved.e, depth: fnResolved.depth+1, t: outType, - substitutions: mergedSubstitutions, + unification: grandUnification, }; } // if the above statement did not throw => types are compatible... @@ -138,7 +139,7 @@ export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: Resolved kind: "value", i: outValue, t: outType, - substitutions: mergedSubstitutions, + unification: grandUnification, }; } else { @@ -146,7 +147,7 @@ export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: Resolved return { kind: "unknown", t: outType, - substitutions: mergedSubstitutions, + unification: grandUnification, }; } } @@ -159,7 +160,7 @@ export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: Resolved e, depth: 0, t: outType, - substitutions: mergeMaps(fnResolved.substitutions, inputResolved.substitutions), + unification: mergeUnifications(fnResolved.unification, inputResolved.unification), }; } throw e; @@ -174,35 +175,28 @@ export function evalCallBlock(fn: ExprBlockState, input: ExprBlockState, env): R export function evalLetInBlock(value: ExprBlockState, name: string, inner: ExprBlockState, env): ResolvedType { const valueResolved = evalEditorBlock(value, env); - // console.log('eval', name, '...', valueResolved.kind, valueResolved.e); - // const innerEnv = growEnv(env)(name)(valueResolved); const innerEnv = makeInnerEnv(env, name, valueResolved); return evalEditorBlock(inner, innerEnv); } -function getUnusedTypeVarIdx(env) { - for (let i=0; ; i++) { - if (!dict.has(env.typeDict)(TYPE_VARS[i])) { - return i; - } - } -} - -export function getUnusedTypeVar(env) { - return TYPE_VARS[getUnusedTypeVarIdx(env)]; -} export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env): ResolvedType { - const paramType = getUnusedTypeVar(env); - // static env: we only know the name and the type - const staticInnerEnv = growEnv(env)(paramName)({ - kind: "unknown", // parameter value is not statically known - t: paramType, - substitutions: new Map(), - }) + const [paramType, staticInnerEnv] = makeTypeVar(env, paramName); const exprResolved = evalEditorBlock(expr, staticInnerEnv); const lambdaT = fnType(_ => paramType)(_ => exprResolved.t); - const lambdaTSubstituted = substitute(lambdaT, exprResolved.substitutions, []); + // 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 + + 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('===================================') + // console.log('inner kind', exprResolved.kind, paramName); if (exprResolved.kind === "error") { return { @@ -210,8 +204,7 @@ export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env): R t: lambdaTSubstituted, depth: 0, e: exprResolved.e, - // substitutions: new Map(), - substitutions: exprResolved.substitutions, + unification: exprResolved.unification, } } const paramTypeSubstituted = lambdaTSubstituted.params[0](lambdaTSubstituted); @@ -220,7 +213,7 @@ export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env): R kind: "value", i: x, t: paramTypeSubstituted, - substitutions: new Map(), + unification: new Map(), }); const result = evalEditorBlock(expr, innerEnv); if (result.kind === "value") { @@ -231,7 +224,7 @@ export function evalLambdaBlock(paramName: string, expr: ExprBlockState, env): R kind: "value", t: lambdaTSubstituted, i: fn, - substitutions: exprResolved.substitutions, + unification: exprResolved.unification, }; } @@ -248,7 +241,7 @@ function parseLiteral(text: string, type: string, env): ResolvedType { if (type === "Double") { return parseAsDouble(text, env); } - return entirelyUnknown(env); + return makeError(env, new Error("Failed to parse")); } function parseAsDouble(text: string, env): ResolvedType { @@ -259,11 +252,11 @@ function parseAsDouble(text: string, env): ResolvedType { kind: "value", i: num, t: Double, - substitutions: new Map(), + unification: new Map(), }; } } - return entirelyUnknown(env); + return makeError(env, new Error("Failed to parse as Double")); } function parseAsInt(text: string, env): ResolvedType { if (text !== '') { @@ -272,12 +265,12 @@ function parseAsInt(text: string, env): ResolvedType { kind: "value", i: BigInt(text), t: Int, - substitutions: new Map(), + unification: new Map(), }; // may throw } catch {} } - return entirelyUnknown(env); + return makeError(env, new Error("Failed to parse as Int")); } const literalParsers = [parseAsDouble, parseAsInt]; @@ -304,10 +297,45 @@ export function scoreResolved(resolved: ResolvedType, outPriority: (s:ResolvedTy } } -export function makeInnerEnv(env, name: string, value: ResolvedType) { - if (name !== "" && value.kind === "value") { - return growEnv(env)(name)(value); +export function makeInnerEnv(env: Environment, name: string, value: ResolvedType): Environment { + if (name !== "") { + return { + names: trie.insert(env.names)(name)(value), + nextFreeTypeVar: env.nextFreeTypeVar, + typeVars: env.typeVars, + } } return env; } +export function makeTypeVar(env: Environment, name: string): [Type, Environment] { + const idx = env.nextFreeTypeVar; + const typeVar = TYPE_VARS[idx]; + const unknown: Unknown = { + kind: "unknown", + t: typeVar, + unification: new Map(), + }; + return [ typeVar, { + names: trie.insert(env.names)(name)(unknown), + nextFreeTypeVar: idx + 1, + typeVars: new Set([...env.typeVars, UNBOUND_SYMBOLS[idx]]), + }]; +} + +function makeError(env: Environment, e: Error, unification: Unification=new Map()): DeepError { + const idx = env.nextFreeTypeVar; + const typeVar = TYPE_VARS[idx]; + const deepError: DeepError = { + kind: "error", + t: typeVar, + unification: new Map(), + e, + depth: 0, + }; + return deepError; + // , { + // names: trie.insert(env.names)('err')(deepError), + // nextFreeTypeVar: idx + 1, + // }]; +} \ No newline at end of file