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 {
text-align: left;
vertical-align: top;
/* margin-left: 28px; */
padding: 0px;
/* padding-left: 14px; */
display: inline-block;
/* border: solid 2px orange; */
background-color: rgb(233, 224, 205);
/* border-radius: 10px; */
width: 100%;
}
@ -83,28 +79,28 @@
color: black;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam:after {
border-left-color: pink;
background-color: pink;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam {
background-color: pink;
color: black;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam:after {
border-left-color: pink;
background-color: pink;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam {
background-color: pink;
color: black;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam:after {
border-left-color: pink;
background-color: pink;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam {
background-color: pink;
color: black;
}
.functionBlock.unifyError > .functionParams > .outputParam > .inputParam > .inputParam > .inputParam > .inputParam:after {
border-left-color: pink;
background-color: pink;
}
.inputParam.offending {
@ -112,5 +108,5 @@
color: white !important;
}
.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 { Value } from "./Value";
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 "./CallBlock.css";
import { EnvContext } from "./EnvContext";
@ -97,6 +97,7 @@ export function CallBlockNoSugar({ state, setState, suggestionPriority }: CallBl
evalEditorBlock(state.fn, env), // fn *may* be set
inputSuggestion[2], // suggestions will be for input
suggestionPriority, // priority function we get from parent block
env,
)}
/>
</div>
@ -105,8 +106,8 @@ export function CallBlockNoSugar({ state, setState, suggestionPriority }: CallBl
</span>;
}
function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number) {
const resolved = evalCallBlock(fn, input);
function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number, env) {
const resolved = evalCallBlock2(fn, input, env);
return scoreResolved(resolved, outPriority);
}
@ -131,6 +132,7 @@ function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
fnSuggestion[2],
evalEditorBlock(fn.input, env),
suggestionPriority,
env,
)}
/>;
}
@ -153,6 +155,7 @@ function FunctionName({fn, setFn, onFnCancel, suggestionPriority, input}) {
fnSuggestion[2], // suggestions will be for function
evalEditorBlock(input, env), // input *may* be set
suggestionPriority, // priority function we get from parent block
env,
)}
/>
</span>;
@ -180,6 +183,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
evalEditorBlock(fn.fn, env),
inputSuggestion[2],
suggestionPriority,
env,
)}
/>;
}
@ -199,6 +203,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
evalEditorBlock(fn, env), // fn *may* be set
inputSuggestion[2], // suggestions will be for input
suggestionPriority, // priority function we get from parent block
env,
)}
/>
</div>;

View file

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

View file

@ -71,10 +71,6 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
const globalContext = useContext(CommandContext);
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'];
if (!commands.includes(e.key)) {
return;
@ -199,7 +195,7 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
return <span className="editor">
{renderBlock()}
<div className="typeSignature">
<Type type={getType(resolved)} />
&nbsp;::&nbsp;<Type type={getType(resolved)} />
</div>
<input
ref={commandInputRef}

View file

@ -38,7 +38,7 @@ interface InputBlockProps extends State2Props<InputBlockState> {
}
const computeSuggestions = (text, env, suggestionPriority: (s: SuggestionType) => number): PrioritizedSuggestionType[] => {
const literals = attemptParseLiteral(text);
const literals = attemptParseLiteral(text, env);
const ls: SuggestionType[] = [
// 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 "./LambdaBlock.css";
import { getUnusedTypeVar } from "./eval";
export interface LambdaBlockState {
@ -31,12 +32,18 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
...state,
paramName,
}));
const setExpr = callback => setState(state => ({
...state,
expr: callback(state.expr),
}));
const onChangeName = (e) => {
if (state.paramName === "" && e.key === 'Backspace') {
setState(state => state.expr);
e.preventDefault();
}
};
useEffect(() => {
nameRef.current?.focus();
}, []);
@ -46,7 +53,7 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
const innerEnv = growEnv(env)(state.paramName)({
kind: "unknown",
i: undefined,
t: TYPE_VARS[0],
t: getUnusedTypeVar(env),
});
return <span>
@ -58,13 +65,14 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
className='editable'
value={state.paramName}
placeholder="<name>"
onKeyDown={onChangeName}
onChange={e => setParamName(e.target.value)}
/>
</span>
&nbsp;
<span className="keyword">:</span>
&nbsp;
<span className="lambdaExpr">
<div className="lambdaExpr">
<EnvContext value={innerEnv}>
<Editor
state={state.expr}
@ -75,6 +83,6 @@ export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockPr
}}
/>
</EnvContext>
</span>
</div>
</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 { InputValueType, SuggestionType } from "./InputBlock";
@ -30,7 +30,11 @@ export interface Unknown {
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
export type ResolvedType = Dynamic | DeepError | Unknown;
@ -40,9 +44,7 @@ export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
return evalInputBlock(s.text, s.value, env);
}
if (s.kind === "call") {
const fn = evalEditorBlock(s.fn, env);
const input = evalEditorBlock(s.input, env);
return evalCallBlock(fn, input);
return evalCallBlock(s.fn, s.input, env);
}
if (s.kind === "let") {
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 entirelyUnknown; // todo
return entirelyUnknown(env); // todo
};
export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType {
if (value.kind === "literal") {
return parseLiteral(text, value.type);
return parseLiteral(text, value.type, env);
}
else if (value.kind === "name") {
const found = trie.get(env.name2dyn)(text);
@ -67,11 +69,11 @@ export function evalInputBlock(text: string, value: InputValueType, env): Resolv
substitutions: new Map(),
};
} else {
return entirelyUnknown;
return entirelyUnknown(env);
}
}
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]));
}
export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedType {
if (getSymbol(fn.t) !== symbolFunction) {
if (fn.kind === "unknown") {
return entirelyUnknown; // don't flash everything red, giving the user a heart attack
export function evalCallBlock2(fnResolved: ResolvedType, inputResolved: ResolvedType, env): ResolvedType {
if (getSymbol(fnResolved.t) !== symbolFunction) {
if (fnResolved.kind === "unknown") {
return entirelyUnknown(env); // don't flash everything red, giving the user a heart attack
}
// worst outcome: we know nothing about the result!
return {
kind: "error",
e: new NotAFunctionError(`${prettyT(fn.t)} is not a function type!`),
t: entirelyUnknown.t,
substitutions: mergeMaps(fn.substitutions, input.substitutions),
e: new NotAFunctionError(`${prettyT(fnResolved.t)} is not a function type!`),
t: getUnusedTypeVar(env),
substitutions: mergeMaps(fnResolved.substitutions, inputResolved.substitutions),
depth: 0,
};
}
try {
// 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 {
kind: "error",
e: input.e, // bubble up the error
e: inputResolved.e, // bubble up the error
depth: 0,
t: outType,
substitutions: mergedSubstitutions,
};
}
if (fn.kind === "error") {
if (fnResolved.kind === "error") {
// also bubble up
return {
kind: "error",
e: fn.e,
depth: fn.depth+1,
e: fnResolved.e,
depth: fnResolved.depth+1,
t: outType,
substitutions: mergedSubstitutions,
};
}
// if the above statement did not throw => types are compatible...
if (input.kind === "value" && fn.kind === "value") {
const outValue = fn.i(input.i);
if (inputResolved.kind === "value" && fnResolved.kind === "value") {
const outValue = fnResolved.i(inputResolved.i);
return {
kind: "value",
i: outValue,
@ -140,31 +142,46 @@ export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedTy
catch (e) {
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...?
const outType = fn.t.params[1](fn.t);
const outType = fnResolved.t.params[1](fnResolved.t);
return {
kind: "error",
e,
depth: 0,
t: outType,
substitutions: mergeMaps(fn.substitutions, input.substitutions),
substitutions: mergeMaps(fnResolved.substitutions, inputResolved.substitutions),
};
}
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 {
const valueResolved = evalEditorBlock(value, env);
const innerEnv = makeInnerEnv(env, name, valueResolved)
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 {
const paramType = getUnusedTypeVar(env);
const fn = (x: any) => {
const innerEnv = makeInnerEnv(env, paramName, {
kind: "value",
i: x,
t: TYPE_VARS[0],
t: paramType,
substitutions: new Map(),
});
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
const staticInnerEnv = makeInnerEnv(env, paramName, {
kind: "unknown", // parameter value is not statically known
t: TYPE_VARS[0],
t: paramType,
substitutions: new Map(),
});
const abstractOutput = evalEditorBlock(expr, staticInnerEnv);
const t = fnType(_ => entirelyUnknown.t)(_ => abstractOutput.t);
const t = fnType(_ => paramType)(_ => abstractOutput.t);
const T = substitute(t, abstractOutput.substitutions, [])
return {
kind: "value",
@ -194,18 +211,18 @@ export function haveValue(resolved: ResolvedType) {
return resolved.kind === "value";
}
function parseLiteral(text: string, type: string): ResolvedType {
function parseLiteral(text: string, type: string, env): ResolvedType {
// dirty
if (type === "Int") {
return parseAsInt(text);
return parseAsInt(text, env);
}
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 !== '') {
const num = Number(text);
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 !== '') {
try {
return {
@ -231,13 +248,13 @@ function parseAsInt(text: string): ResolvedType {
}
catch {}
}
return entirelyUnknown;
return entirelyUnknown(env);
}
const literalParsers = [parseAsDouble, parseAsInt];
export function attemptParseLiteral(text: string): Dynamic[] {
return literalParsers.map(parseFn => parseFn(text))
export function attemptParseLiteral(text: string, env): Dynamic[] {
return literalParsers.map(parseFn => parseFn(text, env))
.filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[];
}