use unused typevar when encountering unknown value or lambda parameter - type inferencing still not entirely correct

This commit is contained in:
Joeri Exelmans 2025-05-17 10:38:54 +02:00
parent d7a4e210a2
commit 496463bbac
8 changed files with 94 additions and 66 deletions

View file

@ -64,13 +64,9 @@
.outputParam { .outputParam {
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
/* margin-left: 28px; */
padding: 0px; padding: 0px;
/* padding-left: 14px; */
display: inline-block; display: inline-block;
/* border: solid 2px orange; */
background-color: rgb(233, 224, 205); background-color: rgb(233, 224, 205);
/* border-radius: 10px; */
width: 100%; width: 100%;
} }
@ -83,28 +79,28 @@
color: black; color: black;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam:after { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam:after {
border-left-color: pink; background-color: pink;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam {
background-color: pink; background-color: pink;
color: black; color: black;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after {
border-left-color: pink; background-color: pink;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam {
background-color: pink; background-color: pink;
color: black; color: black;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam:after { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam:after {
border-left-color: pink; background-color: pink;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam {
background-color: pink; background-color: pink;
color: black; color: black;
} }
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam:after { .functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam:after {
border-left-color: pink; background-color: pink;
} }
.inputParam.offending { .inputParam.offending {
@ -112,5 +108,5 @@
color: white !important; color: white !important;
} }
.inputParam.offending:after { .inputParam.offending:after {
border-left-color: darkred !important; background-color: darkred !important;
} }

View file

@ -3,7 +3,7 @@ import { useContext, useInsertionEffect } from "react";
import { Editor, type EditorState } from "./Editor"; import { Editor, type EditorState } from "./Editor";
import { Value } from "./Value"; import { Value } from "./Value";
import { type SetStateFn, type State2Props } from "./Editor"; import { type SetStateFn, type State2Props } from "./Editor";
import { evalCallBlock, evalEditorBlock, scoreResolved } from "./eval"; import { evalCallBlock, evalCallBlock2, evalEditorBlock, scoreResolved } from "./eval";
import { type ResolvedType } from "./eval"; import { type ResolvedType } from "./eval";
import "./CallBlock.css"; import "./CallBlock.css";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
@ -97,6 +97,7 @@ export function CallBlockNoSugar({ state, setState, suggestionPriority }: CallBl
evalEditorBlock(state.fn, env), // fn *may* be set evalEditorBlock(state.fn, env), // fn *may* be set
inputSuggestion[2], // suggestions will be for input inputSuggestion[2], // suggestions will be for input
suggestionPriority, // priority function we get from parent block suggestionPriority, // priority function we get from parent block
env,
)} )}
/> />
</div> </div>
@ -105,8 +106,8 @@ export function CallBlockNoSugar({ state, setState, suggestionPriority }: CallBl
</span>; </span>;
} }
function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number) { function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number, env) {
const resolved = evalCallBlock(fn, input); const resolved = evalCallBlock2(fn, input, env);
return scoreResolved(resolved, outPriority); return scoreResolved(resolved, outPriority);
} }
@ -131,6 +132,7 @@ function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
fnSuggestion[2], fnSuggestion[2],
evalEditorBlock(fn.input, env), evalEditorBlock(fn.input, env),
suggestionPriority, suggestionPriority,
env,
)} )}
/>; />;
} }
@ -153,6 +155,7 @@ function FunctionName({fn, setFn, onFnCancel, suggestionPriority, input}) {
fnSuggestion[2], // suggestions will be for function fnSuggestion[2], // suggestions will be for function
evalEditorBlock(input, env), // input *may* be set evalEditorBlock(input, env), // input *may* be set
suggestionPriority, // priority function we get from parent block suggestionPriority, // priority function we get from parent block
env,
)} )}
/> />
</span>; </span>;
@ -180,6 +183,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
evalEditorBlock(fn.fn, env), evalEditorBlock(fn.fn, env),
inputSuggestion[2], inputSuggestion[2],
suggestionPriority, suggestionPriority,
env,
)} )}
/>; />;
} }
@ -199,6 +203,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
evalEditorBlock(fn, env), // fn *may* be set evalEditorBlock(fn, env), // fn *may* be set
inputSuggestion[2], // suggestions will be for input inputSuggestion[2], // suggestions will be for input
suggestionPriority, // priority function we get from parent block suggestionPriority, // priority function we get from parent block
env,
)} )}
/> />
</div>; </div>;

View file

@ -3,10 +3,11 @@
} }
.typeSignature { .typeSignature {
display: none; display: inline-block;
position: absolute; /* display: none; */
/* position: absolute; */
z-index: 1; z-index: 1;
background-color: white; /* background-color: white; */
/* border: 1px solid black; */ /* border: 1px solid black; */
} }

View file

@ -71,10 +71,6 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
const globalContext = useContext(CommandContext); const globalContext = useContext(CommandContext);
const onCommand = (e: React.KeyboardEvent) => { const onCommand = (e: React.KeyboardEvent) => {
console.log(e);
// const type = getType(state.resolved);
// const commands = getCommands(type);
const commands = ['e', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', 'L', '=', '.', 'c', 'a']; const commands = ['e', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', 'L', '=', '.', 'c', 'a'];
if (!commands.includes(e.key)) { if (!commands.includes(e.key)) {
return; return;
@ -199,7 +195,7 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
return <span className="editor"> return <span className="editor">
{renderBlock()} {renderBlock()}
<div className="typeSignature"> <div className="typeSignature">
<Type type={getType(resolved)} /> &nbsp;::&nbsp;<Type type={getType(resolved)} />
</div> </div>
<input <input
ref={commandInputRef} ref={commandInputRef}

View file

@ -38,7 +38,7 @@ interface InputBlockProps extends State2Props<InputBlockState> {
} }
const computeSuggestions = (text, env, suggestionPriority: (s: SuggestionType) => number): PrioritizedSuggestionType[] => { const computeSuggestions = (text, env, suggestionPriority: (s: SuggestionType) => number): PrioritizedSuggestionType[] => {
const literals = attemptParseLiteral(text); const literals = attemptParseLiteral(text, env);
const ls: SuggestionType[] = [ const ls: SuggestionType[] = [
// literals // literals

View file

@ -0,0 +1,5 @@
.lambdaExpr {
display: inline-block;
border: solid 1px darkgrey;
margin: 2px;
}

View file

@ -7,6 +7,7 @@ import { growEnv, TYPE_VARS } from "dope2";
import { autoInputWidth } from "./util/dom_trickery"; import { autoInputWidth } from "./util/dom_trickery";
import "./LambdaBlock.css"; import "./LambdaBlock.css";
import { getUnusedTypeVar } from "./eval";
export interface LambdaBlockState { export interface LambdaBlockState {
@ -31,12 +32,18 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
...state, ...state,
paramName, paramName,
})); }));
const setExpr = callback => setState(state => ({ const setExpr = callback => setState(state => ({
...state, ...state,
expr: callback(state.expr), expr: callback(state.expr),
})); }));
const onChangeName = (e) => {
if (state.paramName === "" && e.key === 'Backspace') {
setState(state => state.expr);
e.preventDefault();
}
};
useEffect(() => { useEffect(() => {
nameRef.current?.focus(); nameRef.current?.focus();
}, []); }, []);
@ -46,7 +53,7 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
const innerEnv = growEnv(env)(state.paramName)({ const innerEnv = growEnv(env)(state.paramName)({
kind: "unknown", kind: "unknown",
i: undefined, i: undefined,
t: TYPE_VARS[0], t: getUnusedTypeVar(env),
}); });
return <span> return <span>
@ -58,13 +65,14 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
className='editable' className='editable'
value={state.paramName} value={state.paramName}
placeholder="<name>" placeholder="<name>"
onKeyDown={onChangeName}
onChange={e => setParamName(e.target.value)} onChange={e => setParamName(e.target.value)}
/> />
</span> </span>
&nbsp; &nbsp;
<span className="keyword">:</span> <span className="keyword">:</span>
&nbsp; &nbsp;
<span className="lambdaExpr"> <div className="lambdaExpr">
<EnvContext value={innerEnv}> <EnvContext value={innerEnv}>
<Editor <Editor
state={state.expr} state={state.expr}
@ -75,6 +83,6 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
}} }}
/> />
</EnvContext> </EnvContext>
</span> </div>
</span> </span>
} }

View file

@ -1,4 +1,4 @@
import { assignFnSubstitutions, Double, fnType, getSymbol, growEnv, Int, makeGeneric, NotAFunctionError, prettyT, substitute, symbolFunction, trie, TYPE_VARS, UnifyError } from "dope2"; import { assignFnSubstitutions, dict, Double, fnType, getSymbol, growEnv, Int, makeGeneric, NotAFunctionError, prettyT, set, substitute, symbolFunction, trie, TYPE_VARS, UnifyError } from "dope2";
import type { EditorState } from "./Editor"; import type { EditorState } from "./Editor";
import type { InputValueType, SuggestionType } from "./InputBlock"; import type { InputValueType, SuggestionType } from "./InputBlock";
@ -30,7 +30,11 @@ export interface Unknown {
substitutions: Map<Type,Type>; substitutions: Map<Type,Type>;
} }
export const entirelyUnknown: Unknown = { kind: "unknown", t: makeGeneric(a => a), substitutions: new Map() }; 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 // the value of every block is either known (Dynamic), an error, or unknown
export type ResolvedType = Dynamic | DeepError | Unknown; export type ResolvedType = Dynamic | DeepError | Unknown;
@ -40,9 +44,7 @@ export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
return evalInputBlock(s.text, s.value, env); return evalInputBlock(s.text, s.value, env);
} }
if (s.kind === "call") { if (s.kind === "call") {
const fn = evalEditorBlock(s.fn, env); return evalCallBlock(s.fn, s.input, env);
const input = evalEditorBlock(s.input, env);
return evalCallBlock(fn, input);
} }
if (s.kind === "let") { if (s.kind === "let") {
return evalLetInBlock(s.value, s.name, s.inner, env); return evalLetInBlock(s.value, s.name, s.inner, env);
@ -51,12 +53,12 @@ export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
return evalLambdaBlock(s.paramName, s.expr, env); return evalLambdaBlock(s.paramName, s.expr, env);
} }
return entirelyUnknown; // todo return entirelyUnknown(env); // todo
}; };
export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType { export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType {
if (value.kind === "literal") { if (value.kind === "literal") {
return parseLiteral(text, value.type); return parseLiteral(text, value.type, env);
} }
else if (value.kind === "name") { else if (value.kind === "name") {
const found = trie.get(env.name2dyn)(text); const found = trie.get(env.name2dyn)(text);
@ -67,11 +69,11 @@ export function evalInputBlock(text: string, value: InputValueType, env): Resolv
substitutions: new Map(), substitutions: new Map(),
}; };
} else { } else {
return entirelyUnknown; return entirelyUnknown(env);
} }
} }
else { // kind === "text" -> unresolved else { // kind === "text" -> unresolved
return entirelyUnknown; return entirelyUnknown(env);
} }
} }
@ -79,48 +81,48 @@ const mergeMaps = (...maps: Map<Type,Type>[]) => {
return new Map(maps.flatMap(m => [...m])); return new Map(maps.flatMap(m => [...m]));
} }
export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedType { export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: ResolvedType, env): ResolvedType {
if (getSymbol(fn.t) !== symbolFunction) { if (getSymbol(fnResolved.t) !== symbolFunction) {
if (fn.kind === "unknown") { if (fnResolved.kind === "unknown") {
return entirelyUnknown; // don't flash everything red, giving the user a heart attack return entirelyUnknown(env); // don't flash everything red, giving the user a heart attack
} }
// worst outcome: we know nothing about the result! // worst outcome: we know nothing about the result!
return { return {
kind: "error", kind: "error",
e: new NotAFunctionError(`${prettyT(fn.t)} is not a function type!`), e: new NotAFunctionError(`${prettyT(fnResolved.t)} is not a function type!`),
t: entirelyUnknown.t, t: getUnusedTypeVar(env),
substitutions: mergeMaps(fn.substitutions, input.substitutions), substitutions: mergeMaps(fnResolved.substitutions, inputResolved.substitutions),
depth: 0, depth: 0,
}; };
} }
try { try {
// fn is a function... // fn is a function...
const [outType, substitutions] = assignFnSubstitutions(fn.t, input.t); // may throw const [outType, substitutions] = assignFnSubstitutions(fnResolved.t, inputResolved.t); // may throw
const mergedSubstitutions = mergeMaps(substitutions, fn.substitutions, input.substitutions); const mergedSubstitutions = mergeMaps(substitutions, fnResolved.substitutions, inputResolved.substitutions);
if (input.kind === "error") { if (inputResolved.kind === "error") {
return { return {
kind: "error", kind: "error",
e: input.e, // bubble up the error e: inputResolved.e, // bubble up the error
depth: 0, depth: 0,
t: outType, t: outType,
substitutions: mergedSubstitutions, substitutions: mergedSubstitutions,
}; };
} }
if (fn.kind === "error") { if (fnResolved.kind === "error") {
// also bubble up // also bubble up
return { return {
kind: "error", kind: "error",
e: fn.e, e: fnResolved.e,
depth: fn.depth+1, depth: fnResolved.depth+1,
t: outType, t: outType,
substitutions: mergedSubstitutions, substitutions: mergedSubstitutions,
}; };
} }
// if the above statement did not throw => types are compatible... // if the above statement did not throw => types are compatible...
if (input.kind === "value" && fn.kind === "value") { if (inputResolved.kind === "value" && fnResolved.kind === "value") {
const outValue = fn.i(input.i); const outValue = fnResolved.i(inputResolved.i);
return { return {
kind: "value", kind: "value",
i: outValue, i: outValue,
@ -140,31 +142,46 @@ export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedTy
catch (e) { catch (e) {
if ((e instanceof UnifyError)) { if ((e instanceof UnifyError)) {
// even though fn was incompatible with the given parameter, we can still suppose that our output-type will be that of fn...? // even though fn was incompatible with the given parameter, we can still suppose that our output-type will be that of fn...?
const outType = fn.t.params[1](fn.t); const outType = fnResolved.t.params[1](fnResolved.t);
return { return {
kind: "error", kind: "error",
e, e,
depth: 0, depth: 0,
t: outType, t: outType,
substitutions: mergeMaps(fn.substitutions, input.substitutions), substitutions: mergeMaps(fnResolved.substitutions, inputResolved.substitutions),
}; };
} }
throw e; throw e;
} }
} }
export function evalCallBlock(fn: EditorState, input: EditorState, env): ResolvedType {
const fnResolved = evalEditorBlock(fn, env);
const inputResolved = evalEditorBlock(input, env);
return evalCallBlock2(fnResolved, inputResolved, env);
}
export function evalLetInBlock(value: EditorState, name: string, inner: EditorState, env): ResolvedType { export function evalLetInBlock(value: EditorState, name: string, inner: EditorState, env): ResolvedType {
const valueResolved = evalEditorBlock(value, env); const valueResolved = evalEditorBlock(value, env);
const innerEnv = makeInnerEnv(env, name, valueResolved) const innerEnv = makeInnerEnv(env, name, valueResolved)
return evalEditorBlock(inner, innerEnv); return evalEditorBlock(inner, innerEnv);
} }
export function getUnusedTypeVar(env) {
for (let i=0; ; i++) {
if (!dict.has(env.typeDict)(TYPE_VARS[i])) {
return TYPE_VARS[i];
}
}
}
export function evalLambdaBlock(paramName: string, expr: EditorState, env): ResolvedType { export function evalLambdaBlock(paramName: string, expr: EditorState, env): ResolvedType {
const paramType = getUnusedTypeVar(env);
const fn = (x: any) => { const fn = (x: any) => {
const innerEnv = makeInnerEnv(env, paramName, { const innerEnv = makeInnerEnv(env, paramName, {
kind: "value", kind: "value",
i: x, i: x,
t: TYPE_VARS[0], t: paramType,
substitutions: new Map(), substitutions: new Map(),
}); });
const result = evalEditorBlock(expr, innerEnv); const result = evalEditorBlock(expr, innerEnv);
@ -175,11 +192,11 @@ export function evalLambdaBlock(paramName: string, expr: EditorState, env): Reso
// static env: we only know the name and the type // static env: we only know the name and the type
const staticInnerEnv = makeInnerEnv(env, paramName, { const staticInnerEnv = makeInnerEnv(env, paramName, {
kind: "unknown", // parameter value is not statically known kind: "unknown", // parameter value is not statically known
t: TYPE_VARS[0], t: paramType,
substitutions: new Map(), substitutions: new Map(),
}); });
const abstractOutput = evalEditorBlock(expr, staticInnerEnv); const abstractOutput = evalEditorBlock(expr, staticInnerEnv);
const t = fnType(_ => entirelyUnknown.t)(_ => abstractOutput.t); const t = fnType(_ => paramType)(_ => abstractOutput.t);
const T = substitute(t, abstractOutput.substitutions, []) const T = substitute(t, abstractOutput.substitutions, [])
return { return {
kind: "value", kind: "value",
@ -194,18 +211,18 @@ export function haveValue(resolved: ResolvedType) {
return resolved.kind === "value"; return resolved.kind === "value";
} }
function parseLiteral(text: string, type: string): ResolvedType { function parseLiteral(text: string, type: string, env): ResolvedType {
// dirty // dirty
if (type === "Int") { if (type === "Int") {
return parseAsInt(text); return parseAsInt(text, env);
} }
if (type === "Double") { if (type === "Double") {
return parseAsDouble(text); return parseAsDouble(text, env);
} }
return entirelyUnknown; return entirelyUnknown(env);
} }
function parseAsDouble(text: string): ResolvedType { function parseAsDouble(text: string, env): ResolvedType {
if (text !== '') { if (text !== '') {
const num = Number(text); const num = Number(text);
if (!Number.isNaN(num)) { if (!Number.isNaN(num)) {
@ -217,9 +234,9 @@ function parseAsDouble(text: string): ResolvedType {
}; };
} }
} }
return entirelyUnknown; return entirelyUnknown(env);
} }
function parseAsInt(text: string): ResolvedType { function parseAsInt(text: string, env): ResolvedType {
if (text !== '') { if (text !== '') {
try { try {
return { return {
@ -231,13 +248,13 @@ function parseAsInt(text: string): ResolvedType {
} }
catch {} catch {}
} }
return entirelyUnknown; return entirelyUnknown(env);
} }
const literalParsers = [parseAsDouble, parseAsInt]; const literalParsers = [parseAsDouble, parseAsInt];
export function attemptParseLiteral(text: string): Dynamic[] { export function attemptParseLiteral(text: string, env): Dynamic[] {
return literalParsers.map(parseFn => parseFn(text)) return literalParsers.map(parseFn => parseFn(text, env))
.filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[]; .filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[];
} }