nicer looking

This commit is contained in:
Joeri Exelmans 2025-05-17 09:25:13 +02:00
parent 8abbac4bc9
commit e850952738
14 changed files with 547 additions and 104 deletions

8
pnpm-lock.yaml generated
View file

@ -10,7 +10,7 @@ importers:
dependencies: dependencies:
dope2: dope2:
specifier: git+https://deemz.org/git/joeri/dope2.git specifier: git+https://deemz.org/git/joeri/dope2.git
version: git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381 version: git+https://deemz.org/git/joeri/dope2.git#0096bb5559224b4c9bbe74317e07dc71cfc09c70
react: react:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0 version: 19.1.0
@ -633,8 +633,8 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dope2@git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381: dope2@git+https://deemz.org/git/joeri/dope2.git#0096bb5559224b4c9bbe74317e07dc71cfc09c70:
resolution: {commit: d75bf9f0f200769a5248ace8e4e2ace04fd60381, repo: https://deemz.org/git/joeri/dope2.git, type: git} resolution: {commit: 0096bb5559224b4c9bbe74317e07dc71cfc09c70, repo: https://deemz.org/git/joeri/dope2.git, type: git}
version: 0.0.1 version: 0.0.1
dunder-proto@1.0.1: dunder-proto@1.0.1:
@ -1762,7 +1762,7 @@ snapshots:
depd@2.0.0: {} depd@2.0.0: {}
dope2@git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381: dope2@git+https://deemz.org/git/joeri/dope2.git#0096bb5559224b4c9bbe74317e07dc71cfc09c70:
dependencies: dependencies:
functional-red-black-tree: 1.0.1 functional-red-black-tree: 1.0.1

View file

@ -3,20 +3,23 @@ import './App.css';
import { CommandContext } from './CommandContext'; import { CommandContext } from './CommandContext';
import { Editor, type EditorState } from './Editor'; import { Editor, type EditorState } from './Editor';
import { extendedEnv } from './EnvContext'; import { extendedEnv } from './EnvContext';
import { initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations"; import { biggerExample, initialEditorState, nonEmptyEditorState, tripleFunctionCallEditorState } from "./configurations";
import { evalEditorBlock } from "./eval"; import { evalEditorBlock } from "./eval";
const commands: [string, string[], string][] = [ const commands: [string, string[], string][] = [
["call" , ['c' ], "call" ], ["call" , ['c' ], "call" ],
["eval" , ['e','Tab','Enter'], "eval" ], ["eval" , ['e','Tab','Enter'], "eval" ],
["transform", ['t', '.' ], "transform" ], ["transform", ['t', '.' ], "transform" ],
["let" , ['l', '=', 'a' ], "let ... in ..."], ["let" , ['l', '=' ], "let ... in ..."],
["lambda" , ['a' ], "lambda" ],
]; ];
const examples: [string, EditorState][] = [ const examples: [string, EditorState][] = [
["empty editor", initialEditorState], ["empty editor", initialEditorState],
["push to list", nonEmptyEditorState], ["push to list", nonEmptyEditorState],
["function w/ 4 params", tripleFunctionCallEditorState]]; ["function w/ 4 params", tripleFunctionCallEditorState],
["bigger example", biggerExample],
];
type AppState = { type AppState = {
history: EditorState[], history: EditorState[],

View file

@ -1,11 +1,16 @@
.functionBlock { .functionBlock {
border: solid 1px darkgray; border: solid 1px darkgray;
display: inline-block; display: inline-block;
margin: 4px; margin: 2px;
color: black; color: black;
background-color: white; background-color: white;
vertical-align: top;
} }
/* * {
vertical-align: text-top;
} */
.functionName { .functionName {
/* text-align: center; */ /* text-align: center; */
background-color: white; background-color: white;
@ -14,11 +19,11 @@
.inputParam:after { .inputParam:after {
content: ""; content: "";
position: absolute; position: absolute;
border: solid transparent; right: 0;
border-width: 10px; clip-path: polygon(1% 0%, 100% 50%, 0% 100%);
right: -19px; height: 100%;
top: 0; aspect-ratio: 0.25/1;
bottom:0; transform: translateX(99%);
} }
.inputParam { .inputParam {
margin-right: 20px; margin-right: 20px;
@ -30,7 +35,7 @@
/* Count nested level AFTER .outputParam (resets the depth) */ /* Count nested level AFTER .outputParam (resets the depth) */
.outputParam > .inputParam:after { .outputParam > .inputParam:after {
border-left-color: rgb(242, 253, 146); background-color: rgb(242, 253, 146);
} }
.outputParam > .inputParam { .outputParam > .inputParam {
background-color: rgb(242, 253, 146); background-color: rgb(242, 253, 146);
@ -39,19 +44,19 @@
background-color: rgb(180, 248, 214); background-color: rgb(180, 248, 214);
} }
.outputParam > .inputParam > .inputParam:after { .outputParam > .inputParam > .inputParam:after {
border-left-color: rgb(180, 248, 214); background-color: rgb(180, 248, 214);
} }
.outputParam > .inputParam > .inputParam > .inputParam { .outputParam > .inputParam > .inputParam > .inputParam {
background-color: rgb(153, 212, 214); background-color: rgb(153, 212, 214);
} }
.outputParam > .inputParam > .inputParam > .inputParam:after { .outputParam > .inputParam > .inputParam > .inputParam:after {
border-left-color: rgb(153, 212, 214); background-color: rgb(153, 212, 214);
} }
.outputParam > .inputParam > .inputParam > .inputParam > .inputParam { .outputParam > .inputParam > .inputParam > .inputParam > .inputParam {
background-color: rgb(111, 186, 209); background-color: rgb(111, 186, 209);
} }
.outputParam > .inputParam > .inputParam > .inputParam > .inputParam:after { .outputParam > .inputParam > .inputParam > .inputParam > .inputParam:after {
border-left-color: rgb(111, 186, 209); background-color: rgb(111, 186, 209);
} }
.typeAnnot { .typeAnnot {

View file

@ -1,8 +1,30 @@
.editor {
}
.typeSignature { .typeSignature {
display: none;
position: absolute;
z-index: 1;
background-color: white;
/* border: 1px solid black; */
}
.editor:hover > .typeSignature {
display: inline-block; display: inline-block;
} }
.commandInput { .commandInput {
width: 90px; width: 30px;
margin-left: 10px; margin-left: 10px;
} }
.keyword {
color: blue;
font-weight: bold;
/* vertical-align: top; */
}
* {
/* vertical-align: top; */
}

View file

@ -9,7 +9,7 @@ import { evalEditorBlock } from "./eval";
import { CommandContext } from "./CommandContext"; import { CommandContext } from "./CommandContext";
import "./Editor.css"; import "./Editor.css";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
import type { LambdaBlockState } from "./LambdaBlock"; import { LambdaBlock, type LambdaBlockState } from "./LambdaBlock";
import { LetInBlock, type LetInBlockState } from "./LetInBlock"; import { LetInBlock, type LetInBlockState } from "./LetInBlock";
import { initialEditorState } from "./configurations"; import { initialEditorState } from "./configurations";
import { focusNextElement, focusPrevElement } from "./util/dom_trickery"; import { focusNextElement, focusPrevElement } from "./util/dom_trickery";
@ -71,9 +71,11 @@ 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 type = getType(state.resolved);
// const commands = getCommands(type); // const commands = getCommands(type);
const commands = ['e', 't', 'Enter', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Tab', 'l', '=', '.', 'c']; 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;
} }
@ -124,18 +126,35 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
} }
// l -> Let ... in ... // l -> Let ... in ...
// = -> assign to name // = -> assign to name
if (e.key === 'l' || e.key === '=') { if (e.key === 'l' || e.key === '=' && !e.shiftKey) {
// we become LetInBlock // we become LetInBlock
setState(state => ({ setState(state => ({
kind: "let", kind: "let",
inner: removeFocus(initialEditorState), inner: removeFocus(initialEditorState),
name: "", name: "",
value: removeFocus(state), value: removeFocus(state),
resolved: undefined,
})); }));
globalContext?.doHighlight.let(); globalContext?.doHighlight.let();
return; return;
} }
if (e.key === 'L' || e.key === '=' && e.shiftKey) {
setState(state => ({
kind: "let",
inner: removeFocus(state),
name: "",
value: removeFocus(initialEditorState),
}));
}
// a -> lAmbdA
if (e.key === "a") {
setState(state => ({
kind: "lambda",
paramName: "",
expr: removeFocus(state),
}));
globalContext?.doHighlight.lambda();
return;
}
}; };
const renderBlock = () => { const renderBlock = () => {
@ -169,22 +188,26 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
suggestionPriority={suggestionPriority} suggestionPriority={suggestionPriority}
/>; />;
case "lambda": case "lambda":
return <></>; return <LambdaBlock
state={state}
setState={setState as (callback:(p:LambdaBlockState)=>EditorState)=>void}
suggestionPriority={suggestionPriority}
/>;
} }
} }
const resolved = evalEditorBlock(state, env); const resolved = evalEditorBlock(state, env);
return <> return <span className="editor">
{renderBlock()} {renderBlock()}
<div className="typeSignature"> <div className="typeSignature">
&nbsp;::&nbsp;<Type type={getType(resolved)} /> <Type type={getType(resolved)} />
</div> </div>
<input <input
ref={commandInputRef} ref={commandInputRef}
spellCheck={false} spellCheck={false}
className="editable commandInput" className="editable commandInput"
placeholder={`<command>`} placeholder={`<c>`}
onKeyDown={onCommand} onKeyDown={onCommand}
value={""} value={""}
onChange={() => {}} /> onChange={() => {}} />
</>; </span>;
} }

View file

@ -40,13 +40,13 @@ 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);
const ls = [ const ls: SuggestionType[] = [
// literals // literals
... literals.map((lit) => ["literal", text, lit]), ... literals.map((lit) => ["literal", text, lit]),
// names // names
... trie.suggest(env.name2dyn)(text)(Infinity) ... trie.suggest(env.name2dyn)(text)(Infinity)
.map(([name,type]) => ["name", name, type]), .map(([name,type]) => ["name", name, {...type, substitutions: new Map()}]),
] ]
// return ls; // return ls;
return ls return ls

0
src/LambdaBlock.css Normal file
View file

View file

@ -1,6 +1,12 @@
import type { EditorState } from "./Editor"; import { useContext, useEffect, useRef } from "react";
import type { Dynamic } from "./eval";
import type { ResolvedType } from "./eval"; import { Editor, type EditorState, type State2Props } from "./Editor";
import type { SuggestionType } from "./InputBlock";
import { EnvContext } from "./EnvContext";
import { growEnv, TYPE_VARS } from "dope2";
import { autoInputWidth } from "./util/dom_trickery";
import "./LambdaBlock.css";
export interface LambdaBlockState { export interface LambdaBlockState {
@ -9,6 +15,66 @@ export interface LambdaBlockState {
expr: EditorState; expr: EditorState;
} }
export function LambdaBlock { interface LambdaBlockProps<
FnState=EditorState,
InputState=EditorState,
> extends State2Props<LambdaBlockState,EditorState> {
suggestionPriority: (suggestion: SuggestionType) => number;
}
export function LambdaBlock({state, setState, suggestionPriority}: LambdaBlockProps) {
const env = useContext(EnvContext);
const nameRef = useRef<HTMLInputElement>(null);
const setParamName = paramName => setState(state => ({
...state,
paramName,
}));
const setExpr = callback => setState(state => ({
...state,
expr: callback(state.expr),
}));
useEffect(() => {
nameRef.current?.focus();
}, []);
useEffect(() => autoInputWidth(nameRef, state.paramName, 60), [nameRef, state.paramName]);
const innerEnv = growEnv(env)(state.paramName)({
kind: "unknown",
i: undefined,
t: TYPE_VARS[0],
});
return <span>
<span className="keyword">&#955;</span>
&nbsp;
<span className="lambdaInputParam">
<input
ref={nameRef}
className='editable'
value={state.paramName}
placeholder="<name>"
onChange={e => setParamName(e.target.value)}
/>
</span>
&nbsp;
<span className="keyword">:</span>
&nbsp;
<span className="lambdaExpr">
<EnvContext value={innerEnv}>
<Editor
state={state.expr}
setState={setExpr}
onCancel={() => setState(state => state.expr)}
suggestionPriority={(suggestion: SuggestionType) => {
return suggestionPriority(suggestion);
}}
/>
</EnvContext>
</span>
</span>
} }

View file

@ -1,6 +1,17 @@
.letIn {
.keyword { display: inline-block;
color: blue; border: 1px solid darkgrey;
/* margin: 0 2px 0 2px; */
} }
.decl {
display: grid;
grid-template-columns: auto auto auto auto;
}
.column.rightAlign {
/* text-align: right; */
}
.column {
vertical-align: top;
}

View file

@ -1,10 +1,9 @@
import { useContext, useEffect, useRef } from "react"; import { useContext, useEffect, useRef } from "react";
import { growEnv } from "dope2";
import { Editor, type EditorState } from "./Editor"; import { Editor, type EditorState } from "./Editor";
import { EnvContext } from "./EnvContext"; import { EnvContext } from "./EnvContext";
import { evalEditorBlock, evalLetInBlock, scoreResolved, type ResolvedType } from "./eval"; import { evalEditorBlock, evalLetInBlock, makeInnerEnv, scoreResolved } from "./eval";
import { type State2Props } from "./Editor"; import { type State2Props } from "./Editor";
import { autoInputWidth } from "./util/dom_trickery"; import { autoInputWidth } from "./util/dom_trickery";
@ -22,40 +21,54 @@ interface LetInBlockProps extends State2Props<LetInBlockState,EditorState> {
suggestionPriority: (suggestion: SuggestionType) => number; suggestionPriority: (suggestion: SuggestionType) => number;
} }
export function makeInnerEnv(env, name: string, value: ResolvedType) {
if (value.kind === "value") {
return growEnv(env)(name)(value)
}
return env;
}
export function LetInBlock({state, setState, suggestionPriority}: LetInBlockProps) { export function LetInBlock({state, setState, suggestionPriority}: LetInBlockProps) {
const {name, value, inner} = state; return <span className="letIn">
<div className="decl">
<DeclColumns
state={state}
setState={setState}
suggestionPriority={suggestionPriority}
/>
</div>
<div className="inner">
<InnerMost
state={state}
setState={setState}
suggestionPriority={suggestionPriority}
/>
</div>
</span>
}
function DeclColumns({state, setState, suggestionPriority}) {
const env = useContext(EnvContext); const env = useContext(EnvContext);
const valueResolved = evalEditorBlock(value, env); const {name, value, inner} = state;
const innerEnv = makeInnerEnv(env, name, valueResolved);
const nameRef = useRef<HTMLInputElement>(null);
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)})); const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
const setValue = callback => setState(state => ({...state, value: callback(state.value)})); const setValue = callback => setState(state => ({...state, value: callback(state.value)}));
const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => { const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
setState(state => ({...state, name: e.target.value})); setState(state => ({...state, name: e.target.value}));
} }
const valueSuggestionPriority = (suggestion: SuggestionType) => {
const innerEnv = makeInnerEnv(env, name, suggestion[2]);
const resolved = evalEditorBlock(inner, innerEnv);
return scoreResolved(resolved, suggestionPriority);
};
const innerSuggestionPriority = suggestionPriority;
const nameRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
nameRef.current?.focus(); nameRef.current?.focus();
}, []); }, []);
useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]); useEffect(() => autoInputWidth(nameRef, name, 60), [nameRef, name]);
console.log({innerEnv}); const valueResolved = evalEditorBlock(state.value, env);
const innerEnv = makeInnerEnv(env, state.name, valueResolved);
return <span className="letIn"> return <>
<div className="decl"> <span className="keyword column">let&nbsp;</span>
<span className="keyword">let</span> <span className="column rightAlign">
&nbsp;
<input <input
ref={nameRef} ref={nameRef}
className='editable' className='editable'
@ -63,30 +76,52 @@ export function LetInBlock({state, setState, suggestionPriority}: LetInBlockProp
placeholder="<name>" placeholder="<name>"
onChange={onChangeName} onChange={onChangeName}
/> />
<span className="keyword">&nbsp;=&nbsp;</span> </span>
<span className="keyword column">&nbsp;=&nbsp;</span>
<span className="column">
<Editor <Editor
state={value} state={value}
setState={setValue} setState={setValue}
suggestionPriority={(suggestion: SuggestionType) => { suggestionPriority={valueSuggestionPriority}
const innerEnv = makeInnerEnv(env, name, suggestion[2]);
const resolved = evalEditorBlock(inner, innerEnv);
return scoreResolved(resolved, suggestionPriority);
}}
onCancel={() => setState(state => state.inner)} // keep inner onCancel={() => setState(state => state.inner)} // keep inner
/> />
<span className="keyword">in</span> </span>
</div> {/* <span className="keyword column">in</span> */}
<div className="inner"> {inner.kind === "let" &&
<EnvContext value={innerEnv}> <EnvContext value={innerEnv}>
<Editor <DeclColumns
state={inner} state={inner}
setState={setInner} setState={setInner}
suggestionPriority={(suggestion: SuggestionType) => { suggestionPriority={innerSuggestionPriority}
return suggestionPriority(suggestion)
}}
onCancel={() => setState(state => state.value)} // keep value
/> />
</EnvContext> </EnvContext>
</div> }
</span> </>;
}
function InnerMost({state, setState, suggestionPriority}) {
const env = useContext(EnvContext);
const setInner = callback => setState(state => ({...state, inner: callback(state.inner)}));
const valueResolved = evalEditorBlock(state.value, env);
const innerEnv = makeInnerEnv(env, state.name, valueResolved);
const onCancel = () => setState(state => state.value);
if (state.inner.kind === "let") {
return <EnvContext value={innerEnv}>
<InnerMost
state={state.inner}
setState={setInner}
suggestionPriority={suggestionPriority}
/>
</EnvContext>;
}
else {
return <EnvContext value={innerEnv}>
<Editor
state={state.inner}
setState={setInner}
suggestionPriority={suggestionPriority}
onCancel={onCancel} // keep value
/>
</EnvContext>
}
} }

View file

@ -7,3 +7,13 @@
padding-right: 2px; padding-right: 2px;
background-color: white; background-color: white;
} }
.valueUUID {
border-radius: 10px;
background-color: lightyellow;
border: 1px solid black;
margin-left: 2px;
margin-right: 2px;
padding-left: 2px;
padding-right: 2px;
}

View file

@ -1,6 +1,7 @@
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set, Unit} from "dope2"; import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set, Unit, symbolType, symbolUUID, getHumanReadableName} from "dope2";
import "./Value.css"; import "./Value.css";
import { Type } from "./Type";
export function Value({dynamic}) { export function Value({dynamic}) {
const type = getType(dynamic); const type = getType(dynamic);
@ -32,7 +33,10 @@ export function Value({dynamic}) {
return <ValueSet val={inst} elemType={type.params[0](type)} />; return <ValueSet val={inst} elemType={type.params[0](type)} />;
case symbolList: case symbolList:
return <ValueList val={inst} elemType={type.params[0](type)} />; return <ValueList val={inst} elemType={type.params[0](type)} />;
case symbolType:
return <Type type={inst}/>;
case symbolUUID:
return <ValueUUID val={inst}/>
default: default:
return <>don't know how to show value</>; return <>don't know how to show value</>;
} }
@ -74,3 +78,6 @@ function ValueProduct({val, leftType, rightType}) {
function ValueUnit() { function ValueUnit() {
return <>{'()'}</>; return <>{'()'}</>;
} }
function ValueUUID({val}) {
return <span className="valueUUID">{getHumanReadableName(val)}</span>;
}

View file

@ -74,3 +74,182 @@ export const tripleFunctionCallEditorState: EditorState = {
focus: false, focus: false,
}, },
}; };
export const biggerExample: EditorState = {
"kind": "let",
"inner": {
"kind": "let",
"inner": {
"kind": "let",
"inner": {
"kind": "let",
"inner": {
"kind": "input",
"text": "",
"value": {
"kind": "text"
},
"focus": false
},
"name": "myListInc",
"value": {
"kind": "call",
"fn": {
"kind": "call",
"fn": {
"kind": "input",
"text": "list.map",
"value": {
"kind": "name"
},
"focus": false
},
"input": {
"kind": "input",
"text": "myList",
"value": {
"kind": "name"
},
"focus": false
}
},
"input": {
"kind": "input",
"text": "inc",
"value": {
"kind": "name"
},
"focus": false
}
}
},
"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",
"paramName": "x",
"expr": {
"kind": "input",
"text": "x",
"value": {
"kind": "name"
},
"focus": false
}
}
},
"name": "inc",
"value": {
"kind": "lambda",
"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
}
}
}
};

View file

@ -1,29 +1,36 @@
import { apply, assignFn, Double, getSymbol, Int, makeGeneric, NotAFunctionError, prettyT, symbolFunction, trie, UnifyError } from "dope2"; import { assignFnSubstitutions, Double, fnType, getSymbol, growEnv, Int, makeGeneric, NotAFunctionError, prettyT, substitute, symbolFunction, trie, TYPE_VARS, UnifyError } from "dope2";
import type { EditorState } from "./Editor"; import type { EditorState } from "./Editor";
import type { InputValueType } from "./InputBlock"; import type { InputValueType, SuggestionType } from "./InputBlock";
import { makeInnerEnv } from "./LetInBlock";
interface Type {
symbol: string;
params: any[];
};
export interface DeepError { export interface DeepError {
kind: "error"; kind: "error";
e: Error; e: Error;
depth: number; depth: number;
t: any; t: Type;
substitutions: Map<Type,Type>;
} }
// a dynamically typed value = tuple (instance, type) // a dynamically typed value = tuple (instance, type)
export interface Dynamic { export interface Dynamic {
kind: "value", kind: "value",
i: any; i: any;
t: any; t: Type;
substitutions: Map<Type,Type>;
}; };
export interface Unknown { export interface Unknown {
kind: "unknown"; kind: "unknown";
t: any; t: Type;
substitutions: Map<Type,Type>;
} }
export const entirelyUnknown: Unknown = { kind: "unknown", t: makeGeneric(a => a) }; export const entirelyUnknown: Unknown = { kind: "unknown", t: makeGeneric(a => a), substitutions: new Map() };
// 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;
@ -41,8 +48,8 @@ export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
return evalLetInBlock(s.value, s.name, s.inner, env); return evalLetInBlock(s.value, s.name, s.inner, env);
} }
if (s.kind === "lambda") { if (s.kind === "lambda") {
const expr = evalEditorBlock(s.expr, env); return evalLambdaBlock(s.paramName, s.expr, env);
// todo
} }
return entirelyUnknown; // todo return entirelyUnknown; // todo
}; };
@ -54,7 +61,11 @@ export function evalInputBlock(text: string, value: InputValueType, env): Resolv
else if (value.kind === "name") { else if (value.kind === "name") {
const found = trie.get(env.name2dyn)(text); const found = trie.get(env.name2dyn)(text);
if (found) { if (found) {
return { kind: "value", ...found }; return {
kind: found.kind || "value",
...found,
substitutions: new Map(),
};
} else { } else {
return entirelyUnknown; return entirelyUnknown;
} }
@ -64,6 +75,10 @@ export function evalInputBlock(text: string, value: InputValueType, env): Resolv
} }
} }
const mergeMaps = (...maps: Map<Type,Type>[]) => {
return new Map(maps.flatMap(m => [...m]));
}
export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedType { export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedType {
if (getSymbol(fn.t) !== symbolFunction) { if (getSymbol(fn.t) !== symbolFunction) {
if (fn.kind === "unknown") { if (fn.kind === "unknown") {
@ -74,12 +89,15 @@ export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedTy
kind: "error", kind: "error",
e: new NotAFunctionError(`${prettyT(fn.t)} is not a function type!`), e: new NotAFunctionError(`${prettyT(fn.t)} is not a function type!`),
t: entirelyUnknown.t, t: entirelyUnknown.t,
substitutions: mergeMaps(fn.substitutions, input.substitutions),
depth: 0, depth: 0,
}; };
} }
try { try {
// fn is a function... // fn is a function...
const outType = assignFn(fn.t, input.t); // may throw const [outType, substitutions] = assignFnSubstitutions(fn.t, input.t); // may throw
const mergedSubstitutions = mergeMaps(substitutions, fn.substitutions, input.substitutions);
if (input.kind === "error") { if (input.kind === "error") {
return { return {
@ -87,6 +105,7 @@ export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedTy
e: input.e, // bubble up the error e: input.e, // bubble up the error
depth: 0, depth: 0,
t: outType, t: outType,
substitutions: mergedSubstitutions,
}; };
} }
if (fn.kind === "error") { if (fn.kind === "error") {
@ -96,16 +115,26 @@ export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedTy
e: fn.e, e: fn.e,
depth: fn.depth+1, depth: fn.depth+1,
t: outType, t: outType,
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 (input.kind === "value" && fn.kind === "value") {
const outValue = fn.i(input.i); const outValue = fn.i(input.i);
return { kind: "value", i: outValue, t: outType }; return {
kind: "value",
i: outValue,
t: outType,
substitutions: mergedSubstitutions,
};
} }
else { else {
// we don't know the value, but we do know the type: // we don't know the value, but we do know the type:
return { kind: "unknown", t: outType }; return {
kind: "unknown",
t: outType,
substitutions: mergedSubstitutions,
};
} }
} }
catch (e) { catch (e) {
@ -117,18 +146,49 @@ export function evalCallBlock(fn: ResolvedType, input: ResolvedType): ResolvedTy
e, e,
depth: 0, depth: 0,
t: outType, t: outType,
substitutions: mergeMaps(fn.substitutions, input.substitutions),
}; };
} }
throw e; throw e;
} }
} }
export function evalLetInBlock(value: EditorState, name: string, inner: EditorState, env) { 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 evalLambdaBlock(paramName: string, expr: EditorState, env): ResolvedType {
const fn = (x: any) => {
const innerEnv = makeInnerEnv(env, paramName, {
kind: "value",
i: x,
t: TYPE_VARS[0],
substitutions: new Map(),
});
const result = evalEditorBlock(expr, innerEnv);
if (result.kind === "value") {
return result.i;
}
}
// 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],
substitutions: new Map(),
});
const abstractOutput = evalEditorBlock(expr, staticInnerEnv);
const t = fnType(_ => entirelyUnknown.t)(_ => abstractOutput.t);
const T = substitute(t, abstractOutput.substitutions, [])
return {
kind: "value",
t: T,
i: fn,
substitutions: new Map(),
};
}
export function haveValue(resolved: ResolvedType) { export function haveValue(resolved: ResolvedType) {
// return resolved && !(resolved instanceof DeepError); // return resolved && !(resolved instanceof DeepError);
return resolved.kind === "value"; return resolved.kind === "value";
@ -149,7 +209,12 @@ function parseAsDouble(text: string): ResolvedType {
if (text !== '') { if (text !== '') {
const num = Number(text); const num = Number(text);
if (!Number.isNaN(num)) { if (!Number.isNaN(num)) {
return { kind: "value", i: num, t: Double }; return {
kind: "value",
i: num,
t: Double,
substitutions: new Map(),
};
} }
} }
return entirelyUnknown; return entirelyUnknown;
@ -157,7 +222,12 @@ function parseAsDouble(text: string): ResolvedType {
function parseAsInt(text: string): ResolvedType { function parseAsInt(text: string): ResolvedType {
if (text !== '') { if (text !== '') {
try { try {
return { kind: "value", i: BigInt(text), t: Int }; // may throw return {
kind: "value",
i: BigInt(text),
t: Int,
substitutions: new Map(),
}; // may throw
} }
catch {} catch {}
} }
@ -171,10 +241,14 @@ export function attemptParseLiteral(text: string): Dynamic[] {
.filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[]; .filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[];
} }
export function scoreResolved(resolved: ResolvedType, outPriority) { export function scoreResolved(resolved: ResolvedType, outPriority: (s:SuggestionType) => number) {
const bias = outPriority(['literal', '<computed>', const bias = outPriority(['literal', '<computed>',
// @ts-ignore: // TODO fix this {
{t: resolved.t}]); // @ts-ignore
kind: "unknown",
t: resolved.t,
substitutions: new Map(),
}]);
if (resolved.kind === "value") { if (resolved.kind === "value") {
return 1 + bias; return 1 + bias;
@ -189,3 +263,11 @@ export function scoreResolved(resolved: ResolvedType, outPriority) {
return -2 + bias; // even worse: fn is not a function! return -2 + bias; // even worse: fn is not a function!
} }
} }
export function makeInnerEnv(env, name: string, value: ResolvedType) {
if (name !== "" && value.kind === "value") {
return growEnv(env)(name)(value);
}
return env;
}