change the way text suggestions are rendered + option to disable syntactic sugar

This commit is contained in:
Joeri Exelmans 2025-05-15 22:22:45 +02:00
parent ea8c015eff
commit 2d81e42447
12 changed files with 357 additions and 291 deletions

52
pnpm-lock.yaml generated
View file

@ -10,7 +10,7 @@ importers:
dependencies:
dope2:
specifier: git+https://deemz.org/git/joeri/dope2.git
version: git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994
version: git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381
react:
specifier: ^19.1.0
version: 19.1.0
@ -262,8 +262,8 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@modelcontextprotocol/sdk@1.11.2':
resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==}
'@modelcontextprotocol/sdk@1.11.3':
resolution: {integrity: sha512-rmOWVRUbUJD7iSvJugjUbFZshTAuJ48MXoZ80Osx1GM0K/H1w7rSEvmw8m6vdWxNASgtaHIhAgre4H/E9GJiYQ==}
engines: {node: '>=18'}
'@nodelib/fs.scandir@2.1.5':
@ -617,8 +617,8 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
@ -633,8 +633,8 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994:
resolution: {commit: 443a13998dc3eccab26c27bee4fa056cdbc8f994, repo: https://deemz.org/git/joeri/dope2.git, type: git}
dope2@git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381:
resolution: {commit: d75bf9f0f200769a5248ace8e4e2ace04fd60381, repo: https://deemz.org/git/joeri/dope2.git, type: git}
version: 0.0.1
dunder-proto@1.0.1:
@ -729,8 +729,8 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
eventsource-parser@3.0.1:
resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==}
eventsource-parser@3.0.2:
resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==}
engines: {node: '>=18.0.0'}
eventsource@3.0.7:
@ -1369,7 +1369,7 @@ snapshots:
'@eslint/config-array@0.20.0':
dependencies:
'@eslint/object-schema': 2.1.6
debug: 4.4.0
debug: 4.4.1
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -1383,7 +1383,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
debug: 4.4.0
debug: 4.4.1
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.2
@ -1416,7 +1416,7 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@modelcontextprotocol/sdk@1.11.2':
'@modelcontextprotocol/sdk@1.11.3':
dependencies:
content-type: 1.0.5
cors: 2.8.5
@ -1590,7 +1590,7 @@ snapshots:
'@typescript-eslint/types': 8.32.1
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.32.1
debug: 4.4.0
debug: 4.4.1
eslint: 9.26.0
typescript: 5.8.3
transitivePeerDependencies:
@ -1605,7 +1605,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
'@typescript-eslint/utils': 8.32.1(eslint@9.26.0)(typescript@5.8.3)
debug: 4.4.0
debug: 4.4.1
eslint: 9.26.0
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
@ -1618,7 +1618,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 8.32.1
'@typescript-eslint/visitor-keys': 8.32.1
debug: 4.4.0
debug: 4.4.1
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@ -1681,7 +1681,7 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.0
debug: 4.4.1
http-errors: 2.0.0
iconv-lite: 0.6.3
on-finished: 2.4.1
@ -1754,7 +1754,7 @@ snapshots:
csstype@3.1.3: {}
debug@4.4.0:
debug@4.4.1:
dependencies:
ms: 2.1.3
@ -1762,7 +1762,7 @@ snapshots:
depd@2.0.0: {}
dope2@git+https://deemz.org/git/joeri/dope2.git#443a13998dc3eccab26c27bee4fa056cdbc8f994:
dope2@git+https://deemz.org/git/joeri/dope2.git#d75bf9f0f200769a5248ace8e4e2ace04fd60381:
dependencies:
functional-red-black-tree: 1.0.1
@ -1846,13 +1846,13 @@ snapshots:
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@modelcontextprotocol/sdk': 1.11.2
'@modelcontextprotocol/sdk': 1.11.3
'@types/estree': 1.0.7
'@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.0
debug: 4.4.1
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@ -1895,11 +1895,11 @@ snapshots:
etag@1.8.1: {}
eventsource-parser@3.0.1: {}
eventsource-parser@3.0.2: {}
eventsource@3.0.7:
dependencies:
eventsource-parser: 3.0.1
eventsource-parser: 3.0.2
express-rate-limit@7.5.0(express@5.1.0):
dependencies:
@ -1913,7 +1913,7 @@ snapshots:
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.0
debug: 4.4.1
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@ -1969,7 +1969,7 @@ snapshots:
finalhandler@2.1.0:
dependencies:
debug: 4.4.0
debug: 4.4.1
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@ -2260,7 +2260,7 @@ snapshots:
router@2.2.0:
dependencies:
debug: 4.4.0
debug: 4.4.1
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@ -2282,7 +2282,7 @@ snapshots:
send@1.2.0:
dependencies:
debug: 4.4.0
debug: 4.4.1
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1

View file

@ -48,6 +48,8 @@ export function App() {
// load from localStorage
const [appState, setAppState] = useState(loadFromLocalStorage());
const [syntacticSugar, setSyntacticSugar] = useState(true);
useEffect(() => {
// persist accross reloads
localStorage["appState"] = JSON.stringify(appState);
@ -146,15 +148,21 @@ export function App() {
<button className="factoryReset" onClick={factoryReset}>
FACTORY RESET
</button>
<label>
<input type="checkbox"
checked={syntacticSugar}
onChange={e => setSyntacticSugar(e.target.checked)}/>
syntactic sugar
</label>
</header>
<main onKeyDown={onKeyDown}>
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight}}>
<CommandContext value={{undo: onUndo, redo: onRedo, doHighlight, syntacticSugar}}>
<Editor
state={appState.history.at(-1)!}
setState={pushHistory}
onCancel={() => {}}
suggestionPriority={() => 0}
suggestionPriority={() => 1}
/>
</CommandContext>

View file

@ -1,14 +1,14 @@
import { useContext } from "react";
import { useContext, useInsertionEffect } from "react";
import { Editor, type EditorState } from "./Editor";
import { Value } from "./Value";
import { type SetStateFn, type State2Props } from "./Editor";
import { DeepError, evalCallBlock, evalEditorBlock, haveValue } from "./eval";
import { evalCallBlock, evalEditorBlock } from "./eval";
import { type ResolvedType } from "./eval";
import "./CallBlock.css";
import { EnvContext } from "./EnvContext";
import type { SuggestionType } from "./InputBlock";
import { getType, NotAFunctionError, unify, UnifyError } from "dope2";
import { UnifyError } from "dope2";
export interface CallBlockState<
FnState=EditorState,
@ -47,7 +47,7 @@ export function CallBlock({ state, setState, suggestionPriority }: CallBlockProp
= headlessCallBlock(setState);
const env = useContext(EnvContext);
const resolved = evalEditorBlock(state, env);
return <span className={"functionBlock" + ((resolved instanceof DeepError) ? " unifyError" : "")}>
return <span className={"functionBlock" + ((resolved.kind === "error") ? " unifyError" : "")}>
<FunctionHeader
fn={state.fn}
setFn={setFn}
@ -56,32 +56,79 @@ export function CallBlock({ state, setState, suggestionPriority }: CallBlockProp
suggestionPriority={suggestionPriority}
/>
<div className="functionParams">
<div className="outputParam">
<Output resolved={resolved}>
{/* Sequence of input parameters */}
<InputParams
fn={state.fn} setFn={setFn}
input={state.input} setInput={setInput}
onInputCancel={onInputCancel}
depth={0}
errorDepth={resolved instanceof DeepError ? (resolved.depth) : -1}
errorDepth={(resolved.kind === "error") ? (resolved.depth) : -1}
suggestionPriority={suggestionPriority}
/>
{/* Output (or Error) */}
{ resolved instanceof DeepError && resolved.e.toString()
|| resolved && <><Value dynamic={resolved} />
</>}
</Output>
</div>
</span>;
}
export function Output({resolved, children}) {
return <div className="outputParam">
{children}
{ (resolved.kind === "error") && resolved.e.toString()
|| (resolved.kind === "value") && <Value dynamic={resolved} />
|| "unknown" }
</div>;
}
export function CallBlockNoSugar({ state, setState, suggestionPriority }: CallBlockProps) {
const {setFn, setInput, onFnCancel, onInputCancel}
= headlessCallBlock(setState);
const env = useContext(EnvContext);
const resolved = evalEditorBlock(state, env);
const isOffending = (resolved.kind === "error") ? (resolved.depth===0) : false;
return <span className={"functionBlock" + ((resolved.kind === "error") ? " unifyError" : "")}>
<FunctionName fn={state.fn} setFn={setFn} onFnCancel={onFnCancel} suggestionPriority={suggestionPriority} input={state.input} />
<div className="functionParams">
<Output resolved={resolved}>
<div className={"inputParam" + (isOffending ? " offending" : "")}>
<Editor
state={state.input}
setState={setInput}
onCancel={onInputCancel}
suggestionPriority={
(inputSuggestion: SuggestionType) => computePriority(
evalEditorBlock(state.fn, env), // fn *may* be set
inputSuggestion[2], // suggestions will be for input
suggestionPriority, // priority function we get from parent block
)}
/>
</div>
</Output>
</div>
</span>;
}
function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number) {
const resolved = evalCallBlock(fn, input);
if ((resolved && !(resolved instanceof DeepError))) {
const r = outPriority(['literal', '<computed>',
// @ts-ignore: // TODO fix this
{t: resolved.t}]);
if (resolved.kind === "value") {
// The computed output becomes an input for the surrounding block, which may also have a priority function defined, for instance our output is the input to another function
return 1 + outPriority(['literal', '<computed>', resolved]);
console.log('r:', r);
return 1 + r;
}
else if (resolved.kind === "unknown") {
return 0 + r;
}
if (resolved.e instanceof UnifyError) {
return -1 + r; // parameter doesn't match
}
else {
return -2 + r; // even worse: fn is not a function!
}
return 0; // no fit
}
function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
@ -110,6 +157,12 @@ function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
}
else {
// end of recursion - draw function name
return <FunctionName fn={fn} setFn={setFn} onFnCancel={onFnCancel} suggestionPriority={suggestionPriority} input={input}/>;
}
}
function FunctionName({fn, setFn, onFnCancel, suggestionPriority, input}) {
const env = useContext(EnvContext);
return <span className="functionName">
&nbsp;&#119891;&#119899;&nbsp;
<Editor
@ -124,7 +177,6 @@ function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
)}
/>
</span>;
}
}
function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth, suggestionPriority }) {
@ -158,6 +210,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
const isOffending = depth === errorDepth;
return <div className={"inputParam" + (isOffending ? " offending" : "")}>
{nestedParams}
{/* Our own input param */}
<Editor
state={input}
setState={setInput}
@ -170,53 +223,4 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe
)}
/>
</div>;
// {(fn.kind === "call") &&
// // if the function we're calling is itself the result of a function call,
// // then we render its input parameter nested in our own input parameter box, which is way more readable
// // recurse:
// <NestedParams
// fn={fn}
// setFn={setFn}
// depth={depth}
// errorDepth={errorDepth}
// suggestionPriority={suggestionPriority}
// />
// }
// {/* Our own input */}
// <Editor
// state={input}
// setState={setInput}
// onCancel={onInputCancel}
// suggestionPriority={
// (inputSuggestion: SuggestionType) => computePriority(
// evalEditorBlock(fn, env), // fn *may* be set
// inputSuggestion[2], // suggestions will be for input
// suggestionPriority, // priority function we get from parent block
// )}
// />
// </div>;
}
// function NestedParams({fn, setFn, depth, errorDepth, suggestionPriority}) {
// const env = useContext(EnvContext);
// const {
// setFn : setFnFn,
// setInput : setFnInput,
// } = headlessCallBlock(setFn);
// return <InputParams
// fn={fn.fn}
// setFn={setFnFn}
// input={fn.input}
// setInput={setFnInput}
// onInputCancel={() => {/*todo*/}}
// depth={depth+1}
// errorDepth={errorDepth}
// suggestionPriority={
// (inputSuggestion: SuggestionType) => computePriority(
// evalEditorBlock(fn.fn, env), // fn *may* be set
// inputSuggestion[2], // suggestions will be for input
// suggestionPriority, // priority function we get from parent block
// )}
// />;
// }

View file

@ -4,6 +4,7 @@ interface GlobalActions {
undo: () => void;
redo: () => void;
doHighlight: {[key:string]: () => void};
syntacticSugar: boolean;
}
export const CommandContext = createContext<GlobalActions|null>(null);

View file

@ -4,4 +4,5 @@
.commandInput {
width: 90px;
margin-left: 10px;
}

View file

@ -2,10 +2,10 @@ import { useContext, useEffect, useRef, useState } from "react";
import { getSymbol, getType, symbolFunction } from "dope2";
import { CallBlock, type CallBlockState } from "./CallBlock";
import { CallBlock, CallBlockNoSugar, type CallBlockState } from "./CallBlock";
import { InputBlock, type InputBlockState, type SuggestionType } from "./InputBlock";
import { Type } from "./Type";
import { DeepError, evalEditorBlock } from "./eval";
import { evalEditorBlock } from "./eval";
import { CommandContext } from "./CommandContext";
import "./Editor.css";
import { EnvContext } from "./EnvContext";
@ -148,11 +148,20 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
onCancel={onCancel}
/>;
case "call":
if (globalContext?.syntacticSugar) {
return <CallBlock
state={state}
setState={setState as (callback:(p:CallBlockState)=>EditorState)=>void}
suggestionPriority={suggestionPriority}
/>;
}
else {
return <CallBlockNoSugar
state={state}
setState={setState as (callback:(p:CallBlockState)=>EditorState)=>void}
suggestionPriority={suggestionPriority}
/>;
}
case "let":
return <LetInBlock
state={state}
@ -165,13 +174,9 @@ export function Editor({state, setState, onCancel, suggestionPriority}: EditorPr
const resolved = evalEditorBlock(state, env);
return <>
{renderBlock()}
{
(resolved && !(resolved instanceof DeepError))
? <div className="typeSignature">
:: <Type type={getType(resolved)} />
<div className="typeSignature">
&nbsp;::&nbsp;<Type type={getType(resolved)} />
</div>
: <></>
}
<input
ref={commandInputRef}
spellCheck={false}

View file

@ -1,62 +1,52 @@
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@500&display=swap');
.suggest {
margin-left: -3.5px;
margin-right: 5px;
color: #aaa;
min-width: 30px;
.inputBlock {
position: relative;
}
.editable {
position: relative;
outline: 0px solid transparent;
display: inline-block;
border: 0;
font-size: 13pt;
font-family: "Inconsolata", monospace;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings: "wdth" 100;
background-color: transparent;
color: inherit;
padding: 0;
}
.suggestions {
display: none;
color: black;
.suggest {
left: 0;
top: 0;
position: absolute;
color: #aaa;
}
.suggestionsPlaceholder {
display: inline-block;
position: relative;
vertical-align: bottom;
}
.suggestions {
display: block;
color: black;
text-align: left;
position: absolute;
margin-top: 7px;
margin-left: 4px;
border: solid 1px dodgerblue;
cursor: pointer;
max-height: calc(100vh - 64px);
overflow: auto;
z-index: 10;
background-color: white;
width: max-content;
max-width: 500px;
}
.selected {
background-color: dodgerblue;
color: white;
}
.editable {
outline: 0px solid transparent;
display: inline-block;
border: 0;
/* box-sizing: border-box; */
/* width: ; */
/* border: 1px black solid; */
/* border-style: dashed none dashed; */
margin-left: 4px;
margin-right: 0;
font-size: 13pt;
font-family: "Inconsolata", monospace;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
font-variation-settings: "wdth" 100;
background-color: transparent;
color: inherit;
}
.border-around-input {
border: 1px solid black;
padding: 1px;

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { memo, useContext, useEffect, useMemo, useRef, useState } from "react";
import { Double, getType, Int, newDynamic, prettyT, trie } from "dope2";
import { getType, prettyT, trie } from "dope2";
import { EnvContext } from "./EnvContext";
import type { Dynamic } from "./eval";
@ -8,7 +8,7 @@ import "./InputBlock.css";
import { Type } from "./Type";
import type { State2Props } from "./Editor";
import { autoInputWidth, focusNextElement, focusPrevElement, setRightMostCaretPosition } from "./util/dom_trickery";
import { parseDouble, parseInt } from "./util/parse";
import { attemptParseLiteral } from "./eval";
interface Literal {
kind: "literal";
@ -38,13 +38,15 @@ interface InputBlockProps extends State2Props<InputBlockState> {
}
const computeSuggestions = (text, env, suggestionPriority: (s: SuggestionType) => number): PrioritizedSuggestionType[] => {
const asDouble = parseDouble(text);
const asInt = parseInt(text);
const literals = attemptParseLiteral(text);
const ls = [
... (asDouble ? [["literal", asDouble.toString(), newDynamic(asDouble)(Double)]] : []),
... (asInt ? [["literal", asInt.toString(), newDynamic(BigInt(asInt))(Int)]] : []),
... trie.suggest(env.name2dyn)(text)(Infinity).map(([name,type]) => ["name", name, type]),
// literals
... literals.map((lit) => ["literal", text, lit]),
// names
... trie.suggest(env.name2dyn)(text)(Infinity)
.map(([name,type]) => ["name", name, type]),
]
// return ls;
return ls
@ -60,9 +62,9 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
const [haveFocus, setHaveFocus] = useState(false); // whether to render suggestions or not
const singleSuggestion = trie.growPrefix(env.name2dyn)(text);
const suggestions = useMemo(() => computeSuggestions(text, env, suggestionPriority), [text]);
const suggestions = useMemo(() => computeSuggestions(text, env, suggestionPriority), [text, suggestionPriority, env]);
useEffect(() => autoInputWidth(inputRef, text), [inputRef, text]);
useEffect(() => autoInputWidth(inputRef, text+singleSuggestion), [inputRef, text, singleSuggestion]);
useEffect(() => {
if (focus) {
@ -81,7 +83,6 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
}
const onTextChange = newText => {
const found = trie.get(env.name2dyn)(newText);
setState(state => ({...state, text: newText}));
}
@ -162,17 +163,18 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
}
};
return <span>
<span className="">
return <span className="inputBlock">
{/* Dropdown suggestions */}
{haveFocus &&
<span style={{display:'inline-block'}}>
<span className="suggestionsPlaceholder">
<Suggestions
suggestions={suggestions}
onSelect={onSelectSuggestion}
i={i} setI={setI} />
</span>
}
{/* Single 'grey' suggestion */}
<span className="editable suggest">{text}{singleSuggestion}</span>
{/* Input box */}
<input ref={inputRef}
placeholder="<name or literal>"
@ -183,30 +185,45 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In
onFocus={() => setHaveFocus(true)}
onBlur={() => setHaveFocus(false)}
spellCheck={false}/>
{/* Single 'grey' suggestion */}
<span className="text-block suggest">{singleSuggestion}</span>
</span>
</span>;
}
function Suggestions({ suggestions, onSelect, i, setI }) {
return <>{(suggestions.length > 0) &&
<div className={"suggestions"}>
{suggestions.map((suggestion, j) =>
<SuggestionMemo key={j}
{...{setI, j,
onSelect,
highlighted: i===j,
suggestion}}/>)}
</div>
}</>;
}
interface SuggestionProps {
setI: any;
j: number;
onSelect: any;
highlighted: boolean;
suggestion: PrioritizedSuggestionType;
}
function Suggestion({ setI, j, onSelect, highlighted, suggestion: [priority, kind, name, dynamic] }: SuggestionProps) {
const onMouseEnter = j => () => {
setI(j);
};
const onMouseDown = j => () => {
setI(j);
onSelect(suggestions[i]);
onSelect([priority, kind, name, dynamic]);
};
return <>{(suggestions.length > 0) &&
<div className={"suggestions"}>
{suggestions.map(([priority, kind, name, dynamic], j) =>
<div
return <div
key={`${j}_${name}`}
className={(i === j ? " selected" : "")}
className={(highlighted ? " selected" : "")}
onMouseEnter={onMouseEnter(j)}
onMouseDown={onMouseDown(j)}>
({priority}) ({kind}) {name} :: <Type type={getType(dynamic)} />
</div>)}
</div>
}</>;
}
const SuggestionMemo = memo<SuggestionProps>(Suggestion);

View file

@ -4,7 +4,7 @@ import { growEnv } from "dope2";
import { Editor, type EditorState } from "./Editor";
import { EnvContext } from "./EnvContext";
import { DeepError, evalEditorBlock, type ResolvedType } from "./eval";
import { evalEditorBlock, type ResolvedType } from "./eval";
import { type State2Props } from "./Editor";
import { autoInputWidth } from "./util/dom_trickery";
@ -21,7 +21,7 @@ interface LetInBlockProps extends State2Props<LetInBlockState> {
}
export function makeInnerEnv(env, name: string, value: ResolvedType) {
if (value && !(value instanceof DeepError)) {
if (value.kind === "value") {
return growEnv(env)(name)(value)
}
return env;

View file

@ -1,4 +1,4 @@
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set} from "dope2";
import {getType, getInst, getSymbol, Double, Int, symbolFunction, symbolProduct, symbolSum, symbolDict, symbolSet, symbolList, eqType, match, getLeft, getRight, dict, Bool, set, Unit} from "dope2";
import "./Value.css";
@ -14,6 +14,9 @@ export function Value({dynamic}) {
if (eqType(type)(Bool)) {
return <ValueBool val={inst}/>;
}
if (eqType(type)(Unit)) {
return <ValueUnit/>;
}
const symbol = getSymbol(type);
switch (symbol) {
@ -47,9 +50,6 @@ function ValueFunction() {
function ValueBool({val}) {
return <span className="valuePrimitive">{val.toString()}</span>;
}
// function Sum({val, elemType}) {
// return
// }
function ValueList({val, elemType}) {
return <span className="listType">[{val.map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}]</span>;
}
@ -57,9 +57,10 @@ function ValueSet({val, elemType}) {
return <span className="setType">{'{'}{set.fold(acc => elem => acc.concat([elem]))([])(val).map((v, i) => <Value key={i} dynamic={{i:v, t:elemType}}/>)}{'}'}</span>;
}
function ValueDict({val, keyType, valueType}) {
return <span className="dictType">{'{'}{set.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => <span key={i}>
<Value key={i} dynamic={{i:key, t:keyType}}/>
<Value key={i} dynamic={{i:value, t:valueType}}/>
return <span className="dictType">{'{'}{dict.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => <span key={i}>
<Value dynamic={{i:key, t:keyType}}/>
&rArr;
<Value dynamic={{i:value, t:valueType}}/>
</span>)}{'}'}</span>;
}
function ValueSum({val, leftType, rightType}) {
@ -70,23 +71,6 @@ function ValueSum({val, leftType, rightType}) {
function ValueProduct({val, leftType, rightType}) {
return <span className="productType">(<Value dynamic={{i:getLeft(val), t:leftType}}/>,&nbsp;<Value dynamic={{i:getRight(val), t:rightType}} />)</span>;
}
// function ValueDict({val, keyType, valueType}) {
// let i=0;
// return <span className="dictType">{'{'}<>{
// dict.fold
// (acc => key => value => {
// console.log({acc, key, value});
// return acc.concat([<>
// <Value key={i++} dynamic={{i: key, t: keyType}}/>
// &rArr;
// <Value key={i++} dynamic={{i: value, t: valueType}}/>
// </>]);
// })
// ([])
// (val)
// .map(result => {
// console.log(result);
// return result;
// })
// }</>{'}'}</span>;
// }
function ValueUnit() {
return <>{'()'}</>;
}

View file

@ -1,27 +1,32 @@
import { apply, Double, Int, NotAFunctionError, trie, UnifyError } from "dope2";
import { apply, assignFn, Double, getSymbol, Int, makeGeneric, NotAFunctionError, prettyT, symbolFunction, trie, UnifyError } from "dope2";
import type { EditorState } from "./Editor";
import type { InputValueType } from "./InputBlock";
import { makeInnerEnv } from "./LetInBlock";
import { parseDouble, parseInt } from "./util/parse";
export class DeepError {
export interface DeepError {
kind: "error";
e: Error;
depth: number;
constructor(e, depth) {
this.e = e;
this.depth = depth;
}
};
t: any;
}
// a dynamically typed value = tuple (instance, type)
export interface Dynamic {
kind: "value",
i: any;
t: any;
};
export interface Unknown {
kind: "unknown";
t: any;
}
export const entirelyUnknown: Unknown = { kind: "unknown", t: makeGeneric(a => a) };
// the value of every block is either known (Dynamic), an error, or unknown
export type ResolvedType = Dynamic | DeepError | undefined;
export type ResolvedType = Dynamic | DeepError | Unknown;
export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
if (s.kind === "input") {
@ -39,8 +44,9 @@ export const evalEditorBlock = (s: EditorState, env): ResolvedType => {
}
if (s.kind === "lambda") {
const expr = evalEditorBlock(s.expr, env);
return undefined; // todo
// todo
}
return entirelyUnknown; // todo
};
export function evalInputBlock(text: string, value: InputValueType, env): ResolvedType {
@ -48,45 +54,115 @@ export function evalInputBlock(text: string, value: InputValueType, env): Resolv
return parseLiteral(text, value.type);
}
else if (value.kind === "name") {
return trie.get(env.name2dyn)(text);
const found = trie.get(env.name2dyn)(text);
if (found) {
return { kind: "value", ...found };
} else {
return entirelyUnknown;
}
}
else { // kind === "text" -> unresolved
return;
return entirelyUnknown;
}
}
export function evalCallBlock(fn: ResolvedType, input: ResolvedType) {
if (haveValue(input) && haveValue(fn)) {
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
}
// 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,
depth: 0,
};
}
try {
const outputResolved = apply(input)(fn); // may throw
return outputResolved; // success
// fn is a function...
const outType = assignFn(fn.t, input.t); // may throw
if (input.kind === "error") {
return {
kind: "error",
e: input.e, // bubble up the error
depth: 0,
t: outType,
};
}
if (fn.kind === "error") {
// also bubble up
return {
kind: "error",
e: fn.e,
depth: fn.depth+1,
t: outType,
};
}
// if the above statement did not throw => types are compatible...
if (input.kind === "value" && fn.kind === "value") {
const outValue = fn.i(input.i);
return { kind: "value", i: outValue, t: outType };
}
else {
// we don't know the value, but we do know the type:
return { kind: "unknown", t: outType };
}
}
catch (e) {
if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
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);
return {
kind: "error",
e,
depth: 0,
t: outType,
};
}
throw e;
}
return new DeepError(e, 0); // eval error
}
}
else if (input instanceof DeepError) {
return input; // bubble up the error
}
else if (fn instanceof DeepError) {
return new DeepError(fn.e, fn.depth+1);
}
}
function parseLiteral(text: string, type: string) {
// dirty
if (type === "Int") {
return { i: parseInt(text), t: Int };
}
if (type === "Double") {
return { i: parseDouble(text), t: Double };
}
}
export function haveValue(resolved: ResolvedType) {
return resolved && !(resolved instanceof DeepError);
// return resolved && !(resolved instanceof DeepError);
return resolved.kind === "value";
}
function parseLiteral(text: string, type: string): ResolvedType {
// dirty
if (type === "Int") {
return parseAsInt(text);
}
if (type === "Double") {
return parseAsDouble(text);
}
return entirelyUnknown;
}
function parseAsDouble(text: string): ResolvedType {
if (text !== '') {
const num = Number(text);
if (!Number.isNaN(num)) {
return { kind: "value", i: num, t: Double };
}
}
return entirelyUnknown;
}
function parseAsInt(text: string): ResolvedType {
if (text !== '') {
try {
return { kind: "value", i: BigInt(text), t: Int }; // may throw
}
catch {}
}
return entirelyUnknown;
}
const literalParsers = [parseAsDouble, parseAsInt];
export function attemptParseLiteral(text: string): Dynamic[] {
return literalParsers.map(parseFn => parseFn(text))
.filter(resolved => (resolved.kind !== "unknown" && resolved.kind !== "error")) as unknown as Dynamic[];
}

View file

@ -1,21 +1 @@
// Helpers...
export function parseDouble(text: string): number | undefined {
if (text === '') {
return;
}
const num = Number(text);
if (Number.isNaN(num)) {
return;
}
return num;
}
export function parseInt(text: string): bigint | undefined {
if (text === '') {
return;
}
try {
return BigInt(text);
}
catch (e) { };
}