From 8385f08923f4478b9125c9fc69c9e725279d4306 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 28 May 2025 12:20:31 +0200 Subject: [PATCH] add deep evaluation + remove environment React context (passed via typeInfo instead) + improve highlighting of statically unknown values --- src/component/app/App.tsx | 6 +- src/component/app/configurations.ts | 4 +- .../app/environment.ts} | 5 +- src/component/expr/CallBlock.tsx | 30 ++-- src/component/expr/ExprBlock.tsx | 15 +- src/component/expr/InputBlock.tsx | 3 +- src/component/expr/LambdaBlock.tsx | 27 ++-- src/component/expr/LetInBlock.tsx | 53 +++--- src/eval/deep_eval.ts | 151 ++++++++++++++++++ src/eval/infer_type.ts | 8 + 10 files changed, 223 insertions(+), 79 deletions(-) rename src/{context/EnvContext.ts => component/app/environment.ts} (86%) create mode 100644 src/eval/deep_eval.ts diff --git a/src/component/app/App.tsx b/src/component/app/App.tsx index e2f557f..9a099f4 100644 --- a/src/component/app/App.tsx +++ b/src/component/app/App.tsx @@ -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} /> = 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); \ No newline at end of file diff --git a/src/component/expr/CallBlock.tsx b/src/component/expr/CallBlock.tsx index 267a7c9..7ce443b 100644 --- a/src/component/expr/CallBlock.tsx +++ b/src/component/expr/CallBlock.tsx @@ -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 { 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
{props.state.fn.kind === "call" && globalContext?.syntacticSugar && } {/* Our own input param */} - - - +
; } diff --git a/src/component/expr/ExprBlock.tsx b/src/component/expr/ExprBlock.tsx index 747b18d..8b165a6 100644 --- a/src/component/expr/ExprBlock.tsx +++ b/src/component/expr/ExprBlock.tsx @@ -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 { // All types are inferred once after every App-state change, and passed deeply to all descendants. typeInfo: TypeInfo; + + evalResult: DeepEvalResult; } interface ExprBlockProps extends State2Props { @@ -38,7 +39,6 @@ interface ExprBlockProps extends State2Props { } 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 + const err = props.typeInfo.err || props.evalResult.err; + return {renderBlock[props.state.kind]()} - {/* {(err !== undefined) && - (
- {err.message.trim()} -
)} */} (null); const [i, setI] = useState(0); // selected suggestion idx diff --git a/src/component/expr/LambdaBlock.tsx b/src/component/expr/LambdaBlock.tsx index 2c01878..292e4eb 100644 --- a/src/component/expr/LambdaBlock.tsx +++ b/src/component/expr/LambdaBlock.tsx @@ -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 { 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 :  
- - setState(state => state.expr)} - score={suggestion => score({...state, expr: suggestion})} - typeInfo={typeInfo.inner} - /> - + setState(state => state.expr)} + score={suggestion => score({...state, expr: suggestion})} + typeInfo={typeInfo.inner} + evalResult={evalResult.inner} + />
} diff --git a/src/component/expr/LetInBlock.tsx b/src/component/expr/LetInBlock.tsx index c1e0c6e..35e9ee5 100644 --- a/src/component/expr/LetInBlock.tsx +++ b/src/component/expr/LetInBlock.tsx @@ -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) {
} -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} /> {state.inner.kind === "let" && globalContext?.syntacticSugar && - - score({ ...state, inner: suggestion })} - typeInfo={typeInfo.inner} - /> - + 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 - score({ ...state, inner: suggestion })} - typeInfo={typeInfo.inner} - /> - ; + return score({ ...state, inner: suggestion })} + typeInfo={typeInfo.inner} + evalResult={evalResult.inner} + />; } else { - return - score({ ...state, inner: suggestion })} - onCancel={onCancel} // keep value - typeInfo={typeInfo.inner} - /> - + return score({ ...state, inner: suggestion })} + onCancel={onCancel} // keep value + typeInfo={typeInfo.inner} + evalResult={evalResult.inner} + />; } } \ No newline at end of file diff --git a/src/eval/deep_eval.ts b/src/eval/deep_eval.ts new file mode 100644 index 0000000..8a27e0b --- /dev/null +++ b/src/eval/deep_eval.ts @@ -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, + }; +} \ No newline at end of file diff --git a/src/eval/infer_type.ts b/src/eval/infer_type.ts index 8b0bcf4..53ef7e0 100644 --- a/src/eval/infer_type.ts +++ b/src/eval/infer_type.ts @@ -22,6 +22,7 @@ export type Substitutions = Map; 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, }; });