add deep evaluation + remove environment React context (passed via typeInfo instead) + improve highlighting of statically unknown values

This commit is contained in:
Joeri Exelmans 2025-05-28 12:20:31 +02:00
parent 8576f7cb8d
commit 8385f08923
10 changed files with 223 additions and 79 deletions

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { extendedEnv } from '../../context/EnvContext';
import { extendedEnv } from './environment';
import { GlobalContext } from '../../context/GlobalContext';
import { inferType, scoreTypeInfo } from '../../eval/infer_type';
import { ExprBlock, type ExprBlockState } from '../expr/ExprBlock';
@ -10,6 +10,7 @@ import './App.css';
import { evalExpr } from '../../eval/eval';
import { Value } from '../other/Value';
import { Type, TypeInfoBlock } from '../other/Type';
import { deepEvalExpr } from '../../eval/deep_eval';
const examples: [string, ExprBlockState][] = [
@ -141,6 +142,8 @@ export function App() {
const typeInfo = useMemo(() => inferType(currentState, extendedEnv), [currentState]);
// dynamic evalutions
const evalResult = evalExpr(currentState, extendedEnv);
const deepEvalResult = deepEvalExpr(currentState, extendedEnv);
console.log({deepEvalResult});
const onCopy = () => {
const serialized = JSON.stringify(currentState);
@ -209,6 +212,7 @@ export function App() {
return scoreTypeInfo(typeInfo);
}}
typeInfo={typeInfo}
evalResult={deepEvalResult}
/>
=
<Value dynamic={{

View file

@ -1,5 +1,7 @@
import type { ExprBlockState } from "../expr/ExprBlock";
// Note: new configurations can be added here by using the 'Copy' button in the app.
export const initialEditorState: ExprBlockState = {
kind: "input",
text: "",
@ -75,7 +77,7 @@ export const tripleFunctionCallEditorState: ExprBlockState = {
},
};
export const biggerExample: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"input","text":"myList","value":{"kind":"name"}},"name":"myListInc","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"myList","value":{"kind":"name"}}},"input":{"kind":"input","text":"inc","value":{"kind":"name"},"focus":false}}},"input":{"kind":"input","text":"id","value":{"kind":"name"},"focus":true}}},"name":"myList","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"list.emptyList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"2","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"3","value":{"kind":"literal","type":"Int"},"focus":false}}},"name":"id","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}}},"name":"inc","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":true}}}};
export const biggerExample: ExprBlockState = {"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"let","focus":false,"inner":{"kind":"input","text":"myListInc","value":{"kind":"name"}},"name":"myListInc","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.map","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"myList","value":{"kind":"name"}}},"input":{"kind":"input","text":"inc","value":{"kind":"name"},"focus":false}}},"input":{"kind":"input","text":"id","value":{"kind":"name"},"focus":true}}},"name":"myList","value":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"list.push","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"list.emptyList","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"2","value":{"kind":"literal","type":"Int"},"focus":false}}},"input":{"kind":"input","text":"3","value":{"kind":"literal","type":"Int"},"focus":false}}},"name":"id","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"input","text":"x","value":{"kind":"name"}}}},"name":"inc","value":{"kind":"lambda","focus":false,"paramName":"x","expr":{"kind":"call","fn":{"kind":"call","fn":{"kind":"input","text":"addInt","value":{"kind":"name"},"focus":false},"input":{"kind":"input","text":"x","value":{"kind":"name"},"focus":false}},"input":{"kind":"input","text":"1","value":{"kind":"literal","type":"Int"},"focus":true}}}};
export const lambda2Params: ExprBlockState = {
"kind": "let",

View file

@ -1,7 +1,6 @@
import { createContext } from "react";
import { getDefaultTypeParser, module2Env, ModuleStd } from "dope2";
// import type { Dynamic, Environment } from "./eval";
import type { StaticEnvironment } from "../eval/infer_type";
import type { StaticEnvironment } from "../../eval/infer_type";
export const functionWith3Params = i => j => k => i+j+k;
export const functionWith4Params = i => j => k => l => i+j+k+l;
@ -20,5 +19,3 @@ export const extendedEnv: StaticEnvironment = {
// nextFreeTypeVar: 0,
typevars: new Set(),
};
export const EnvContext = createContext(extendedEnv);

View file

@ -1,13 +1,13 @@
import { useContext } from "react";
import { CallContext } from "../../context/CallContext";
import { EnvContext } from "../../context/EnvContext";
import { GlobalContext } from "../../context/GlobalContext";
import { type StaticEnvironment, type TypeInfoCall } from "../../eval/infer_type";
import { type TypeInfoCall } from "../../eval/infer_type";
import { getActions } from "../app/actions";
import { Type, TypeInfoBlock } from "../other/Type";
import { TypeInfoBlock } from "../other/Type";
import "./CallBlock.css";
import { ExprBlock, type ExprBlockState, type SetStateFn, type State2Props } from "./ExprBlock";
import type { DeepEvalResultCall } from "../../eval/deep_eval";
export interface CallBlockState {
kind: "call";
@ -20,9 +20,10 @@ export interface CallBlockProps<
InputState=ExprBlockState,
> extends State2Props<CallBlockState,ExprBlockState> {
typeInfo: TypeInfoCall;
evalResult: DeepEvalResultCall;
}
function nestedFnProperties({state, setState, score, typeInfo}: CallBlockProps, env: StaticEnvironment) {
function nestedFnProperties({state, setState, score, typeInfo, evalResult}: CallBlockProps) {
const setFn = (callback: SetStateFn) => {
setState(state => ({...state, fn: callback(state.fn)}));
};
@ -32,10 +33,10 @@ function nestedFnProperties({state, setState, score, typeInfo}: CallBlockProps,
const scoreFn = (fnSuggestion: ExprBlockState) => {
return score({ ...state, fn: fnSuggestion });
};
return {state: state.fn, setState: setFn, onCancel: onFnCancel, score: scoreFn, typeInfo: typeInfo.fn};
return {state: state.fn, setState: setFn, onCancel: onFnCancel, score: scoreFn, typeInfo: typeInfo.fn, evalResult: evalResult.fn};
}
function nestedInputProperties({state, setState, score, typeInfo}: CallBlockProps, env: StaticEnvironment) {
function nestedInputProperties({state, setState, score, typeInfo, evalResult}: CallBlockProps) {
const setInput = (callback: SetStateFn) => {
setState(state => ({...state, input: callback(state.input)}));
};
@ -45,7 +46,7 @@ function nestedInputProperties({state, setState, score, typeInfo}: CallBlockProp
const scoreInput = (inputSuggestion: ExprBlockState) => {
return score({ ...state, input: inputSuggestion });
};
return {state: state.input, setState: setInput, onCancel: onInputCancel, score: scoreInput, typeInfo: typeInfo.input};
return {state: state.input, setState: setInput, onCancel: onInputCancel, score: scoreInput, typeInfo: typeInfo.input, evalResult: evalResult.input};
}
export function CallBlock(props: CallBlockProps) {
@ -69,9 +70,8 @@ export function CallBlock(props: CallBlockProps) {
}
function FunctionHeader(props) {
const env = useContext(EnvContext);
const globalContext = useContext(GlobalContext);
const nestedProperties = nestedFnProperties(props, env);
const nestedProperties = nestedFnProperties(props);
if (props.state.fn.kind === "call" && globalContext?.syntacticSugar) {
// if the function we're calling is itself the result of a function call,
// then we are anonymous, and so we don't draw a function name
@ -87,21 +87,17 @@ function FunctionHeader(props) {
}
function InputParams(props) {
const env = useContext(EnvContext);
const globalContext = useContext(GlobalContext);
const inputEnv = props.typeInfo.fn.newEnv;
const isOffending = props.typeInfo.err;
return <div className={"inputParam" + (isOffending ? " offending" : "")}>
{props.state.fn.kind === "call"
&& globalContext?.syntacticSugar
&& <InputParams
{...nestedFnProperties(props as CallBlockProps, env)}
{...nestedFnProperties(props as CallBlockProps)}
/>}
{/* Our own input param */}
<EnvContext value={inputEnv}>
<ExprBlock
{...nestedInputProperties(props as CallBlockProps, env)}
/>
</EnvContext>
<ExprBlock
{...nestedInputProperties(props as CallBlockProps)}
/>
</div>;
}

View file

@ -1,6 +1,5 @@
import { useContext } from "react";
import { EnvContext } from "../../context/EnvContext";
import { GlobalContext } from "../../context/GlobalContext";
import { type TypeInfo } from "../../eval/infer_type";
import { getActions } from "../app/actions";
@ -11,7 +10,7 @@ import { LambdaBlock, type LambdaBlockProps, type LambdaBlockState } from "./Lam
import { LetInBlock, type LetInBlockProps, type LetInBlockState } from "./LetInBlock";
import "./ExprBlock.css";
import { evalExpr } from "../../eval/eval";
import type { DeepEvalResult } from "../../eval/deep_eval";
export type ExprBlockState =
InputBlockState
@ -31,6 +30,8 @@ export interface State2Props<InType, OutType = InType> {
// All types are inferred once after every App-state change, and passed deeply to all descendants.
typeInfo: TypeInfo;
evalResult: DeepEvalResult;
}
interface ExprBlockProps extends State2Props<ExprBlockState> {
@ -38,7 +39,6 @@ interface ExprBlockProps extends State2Props<ExprBlockState> {
}
export function ExprBlock(props: ExprBlockProps) {
const env = useContext(EnvContext);
const globalContext = useContext(GlobalContext);
const renderBlock = {
@ -51,14 +51,9 @@ export function ExprBlock(props: ExprBlockProps) {
const actions = getActions(globalContext, props.setState);
const extraHandlers = Object.fromEntries(Object.entries(actions).map(([shortcut, action]) =>
[shortcut, (e) => { e.preventDefault(); action(); }]));
const evalResult = evalExpr(props.state, env);
const err = props.typeInfo.err || evalResult.err;
return <span className={"editor" + (err ? " error" : "") + ((evalResult.val === undefined) ? " unknown" : "")}>
const err = props.typeInfo.err || props.evalResult.err;
return <span className={"editor" + (err ? " error" : "") + ((props.evalResult.val === undefined) ? " unknown" : "")}>
{renderBlock[props.state.kind]()}
{/* {(err !== undefined) &&
(<div className="errorMessage">
{err.message.trim()}
</div>)} */}
<Input
placeholder="<c>"
text=""

View file

@ -3,7 +3,6 @@ import { memo, useContext, useEffect, useMemo, useRef, useState } from "react";
import { trie } from "dope2";
import { CallContext } from "../../context/CallContext";
import { EnvContext } from "../../context/EnvContext";
import { GlobalContext } from "../../context/GlobalContext";
import { inferTypeInput, type StaticEnvironment, type Type, type TypeInfoInput } from "../../eval/infer_type";
import { getActions } from "../app/actions";
@ -88,7 +87,7 @@ const computeSuggestions = (
export function InputBlock({ state, setState, score, onCancel, typeInfo }: InputBlockProps) {
const {text, focus} = state;
const globalContext = useContext(GlobalContext);
const env = useContext(EnvContext);
const env = typeInfo.env;
const callContext = useContext(CallContext);
const inputRef = useRef<HTMLInputElement>(null);
const [i, setI] = useState(0); // selected suggestion idx

View file

@ -1,12 +1,10 @@
import { useContext } from "react";
import { EnvContext } from "../../context/EnvContext";
import { ExprBlock, type ExprBlockState, type State2Props } from "./ExprBlock";
import { type TypeInfoLambda } from "../../eval/infer_type";
import { Input } from "../other/Input";
import { Type } from "../other/Type";
import "./LambdaBlock.css";
import type { DeepEvalResultLambda } from "../../eval/deep_eval";
export interface LambdaBlockState {
kind: "lambda";
@ -20,12 +18,10 @@ export interface LambdaBlockProps<
InputState=ExprBlockState,
> extends State2Props<LambdaBlockState,ExprBlockState> {
typeInfo: TypeInfoLambda;
evalResult: DeepEvalResultLambda;
}
export function LambdaBlock({state, setState, score, typeInfo}: LambdaBlockProps) {
const env = useContext(EnvContext);
export function LambdaBlock({state, setState, score, typeInfo, evalResult}: LambdaBlockProps) {
const setParamName = paramName => setState(state => ({
...state,
paramName,
@ -56,15 +52,14 @@ export function LambdaBlock({state, setState, score, typeInfo}: LambdaBlockProps
<span className="keyword">:</span>
&nbsp;
<div className="lambdaInner">
<EnvContext value={typeInfo.innerEnv}>
<ExprBlock
state={state.expr}
setState={setExpr}
onCancel={() => setState(state => state.expr)}
score={suggestion => score({...state, expr: suggestion})}
typeInfo={typeInfo.inner}
/>
</EnvContext>
<ExprBlock
state={state.expr}
setState={setExpr}
onCancel={() => setState(state => state.expr)}
score={suggestion => score({...state, expr: suggestion})}
typeInfo={typeInfo.inner}
evalResult={evalResult.inner}
/>
</div>
</span>
}

View file

@ -1,6 +1,5 @@
import { useContext } from "react";
import { EnvContext } from "../../context/EnvContext";
import { GlobalContext } from "../../context/GlobalContext";
import { type TypeInfoLet } from "../../eval/infer_type";
import { Input } from "../other/Input";
@ -32,7 +31,7 @@ export function LetInBlock(props: LetInBlockProps) {
</span>
}
function DeclColumns({state, setState, score, typeInfo}) {
function DeclColumns({state, setState, score, typeInfo, evalResult}) {
const globalContext = useContext(GlobalContext);
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
@ -61,45 +60,43 @@ function DeclColumns({state, setState, score, typeInfo}) {
score={suggestion => score({ ...state, value: suggestion })}
onCancel={() => setState(state => state.inner)} // keep inner
typeInfo={typeInfo.value}
evalResult={evalResult.value}
/>
</span>
{state.inner.kind === "let" &&
globalContext?.syntacticSugar &&
<EnvContext value={typeInfo.innerEnv}>
<DeclColumns
state={state.inner}
setState={setInner}
score={suggestion => score({ ...state, inner: suggestion })}
typeInfo={typeInfo.inner}
/>
</EnvContext>
<DeclColumns
state={state.inner}
setState={setInner}
score={suggestion => score({ ...state, inner: suggestion })}
typeInfo={typeInfo.inner}
evalResult={evalResult.inner}
/>
}
</>;
}
function InnerMost({state, setState, score, typeInfo}) {
function InnerMost({state, setState, score, typeInfo, evalResult}) {
const globalContext = useContext(GlobalContext);
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
const onCancel = () => setState(state => state.value);
if (state.inner.kind === "let" && globalContext?.syntacticSugar) {
return <EnvContext value={typeInfo.innerEnv}>
<InnerMost
state={state.inner}
setState={setInner}
score={suggestion => score({ ...state, inner: suggestion })}
typeInfo={typeInfo.inner}
/>
</EnvContext>;
return <InnerMost
state={state.inner}
setState={setInner}
score={suggestion => score({ ...state, inner: suggestion })}
typeInfo={typeInfo.inner}
evalResult={evalResult.inner}
/>;
}
else {
return <EnvContext value={typeInfo.innerEnv}>
<ExprBlock
state={state.inner}
setState={setInner}
score={suggestion => score({ ...state, inner: suggestion })}
onCancel={onCancel} // keep value
typeInfo={typeInfo.inner}
/>
</EnvContext>
return <ExprBlock
state={state.inner}
setState={setInner}
score={suggestion => score({ ...state, inner: suggestion })}
onCancel={onCancel} // keep value
typeInfo={typeInfo.inner}
evalResult={evalResult.inner}
/>;
}
}

151
src/eval/deep_eval.ts Normal file
View file

@ -0,0 +1,151 @@
import { trie } from "dope2";
import type { CallBlockState } from "../component/expr/CallBlock";
import type { ExprBlockState } from "../component/expr/ExprBlock";
import type { InputBlockState } from "../component/expr/InputBlock";
import type { LambdaBlockState } from "../component/expr/LambdaBlock";
import type { LetInBlockState } from "../component/expr/LetInBlock";
import { evalExpr, type DynamicEnvironment, type EvalResult } from "./eval";
export interface DeepEvalResultInput extends EvalResult {
kind: "input";
}
export interface DeepEvalResultCall extends EvalResult {
kind: "call";
fn: DeepEvalResult;
input: DeepEvalResult;
}
export interface DeepEvalResultLet extends EvalResult {
kind: "let";
value: DeepEvalResult;
inner: DeepEvalResult;
}
export interface DeepEvalResultLambda extends EvalResult {
kind: "lambda";
inner: DeepEvalResult;
}
export type DeepEvalResult = DeepEvalResultInput | DeepEvalResultCall | DeepEvalResultLet | DeepEvalResultLambda;
export function deepEvalExpr(s: ExprBlockState, env: DynamicEnvironment): DeepEvalResult {
if (s.kind === "input") {
return deepEvalInput(s, env);
}
else if (s.kind === "call") {
return deepEvalCall(s, env);
}
else if (s.kind === "let") {
return deepEvalLet(s, env);
}
else { // (s.kind === "lambda")
return deepEvalLambda(s, env);
}
}
export function deepEvalInput(s: InputBlockState, env: DynamicEnvironment): DeepEvalResultInput {
if (s.value.kind === "literal") {
if (s.text === '') {
return {
kind: "input",
err: new Error('cannot parse empty string as '+s.value.type),
};
}
const ctor = {
Int: BigInt,
Double: Number,
}[s.value.type] as (s: string) => any;
return {
kind: "input",
val: ctor(s.text)
};
}
else if (s.value.kind === "name") {
const found = trie.get(env.names)(s.text);
if (found) {
if (found.recursive) {
// dirty
return {
kind: "input",
...found.i(),
};
}
return {
kind: "input",
val: found.i,
};
}
}
return {
kind: "input",
err: new Error(`'${s.text}' not found`),
}
}
export function deepEvalCall(s: CallBlockState, env: DynamicEnvironment): DeepEvalResultCall {
const fn = deepEvalExpr(s.fn, env);
const input = deepEvalExpr(s.input, env);
if (fn.val !== undefined && input.val !== undefined) {
try {
const result = fn.val(input.val)
return {
kind: "call",
val: result,
fn,
input,
};
}
catch (e: any) {
return {
kind: "call",
err: e,
fn,
input,
};
}
}
return {
kind: "call",
fn,
input,
}
}
export function deepEvalLet(s: LetInBlockState, env: DynamicEnvironment): DeepEvalResultLet {
const valueEnv = {
names: trie.insert(env.names)(s.name)({
recursive: true,
i: () => {
try {
return { val: valueResult.val };
} catch (e) {
return { err: e };
}
},
}),
};
const valueResult = deepEvalExpr(s.value, valueEnv);
const innerEnv = {
names: trie.insert(env.names)(s.name)({i: valueResult.val}),
}
const innerResult = deepEvalExpr(s.inner, innerEnv)
return {
kind: "let",
val: innerResult.val,
err: innerResult.err,
inner: innerResult,
value: valueResult,
};
}
export function deepEvalLambda(s: LambdaBlockState, env: DynamicEnvironment): DeepEvalResultLambda {
const fn = x => {
const innerEnv = {
names: trie.insert(env.names)(s.paramName)({i: x})
};
const result = evalExpr(s.expr, innerEnv); // shallow eval
return result.val;
};
const staticResult = deepEvalExpr(s.expr, {
names: trie.insert(env.names)(s.paramName)({i: undefined}),
});
return {
kind: "lambda",
val: fn,
inner: staticResult,
};
}

View file

@ -22,6 +22,7 @@ export type Substitutions = Map<string, Type>;
interface TypeInfoCommon {
type: Type;
subs: Substitutions;
env: StaticEnvironment;
newEnv: StaticEnvironment;
err?: IncompatibleTypesError;
}
@ -74,6 +75,7 @@ export const inferTypeInput = memoize(function inferTypeInput(s: InputBlockState
kind: "input",
type,
subs: new Map(),
env,
newEnv: env,
}
}
@ -87,6 +89,7 @@ export const inferTypeInput = memoize(function inferTypeInput(s: InputBlockState
kind: "input",
type,
subs: new Map(),
env,
newEnv,
};
}
@ -97,6 +100,7 @@ export const inferTypeInput = memoize(function inferTypeInput(s: InputBlockState
kind: "input",
type,
subs: new Map(),
env,
newEnv,
err: new Error(`'${s.text}' not found`),
}
@ -140,6 +144,7 @@ export const inferTypeCall = memoize(function inferTypeCall(s: CallBlockState, e
kind: "call",
type,
subs: mergedSubs,
env,
newEnv,
fn: fnTypeInfo,
input: inputTypeInfo,
@ -152,6 +157,7 @@ export const inferTypeCall = memoize(function inferTypeCall(s: CallBlockState, e
kind: "call",
type,
subs: new Map(),
env,
newEnv,
err: e,
fn: fnTypeInfo,
@ -177,6 +183,7 @@ export const inferTypeLet = memoize(function inferTypeLet(s: LetInBlockState, en
type: innerTypeInfo.type,
value: recursiveTypeInfo.inner,
subs: innerTypeInfo.subs,
env,
newEnv: innerTypeInfo.newEnv,
inner: innerTypeInfo,
innerEnv,
@ -189,6 +196,7 @@ export const inferTypeLambda = memoize(function inferTypeLambda(s: LambdaBlockSt
kind: "lambda",
type: fnType(_ => recursiveTypeInfo.paramType)(_ => recursiveTypeInfo.inner.type),
...recursiveTypeInfo,
env,
};
});