From 2d81e424475f696ab8047b36aca8aa447a5ab669 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Thu, 15 May 2025 22:22:45 +0200 Subject: [PATCH] change the way text suggestions are rendered + option to disable syntactic sugar --- pnpm-lock.yaml | 52 +++++++------- src/App.tsx | 12 +++- src/CallBlock.tsx | 158 ++++++++++++++++++++++-------------------- src/CommandContext.ts | 1 + src/Editor.css | 1 + src/Editor.tsx | 47 +++++++------ src/InputBlock.css | 58 +++++++--------- src/InputBlock.tsx | 107 ++++++++++++++++------------ src/LetInBlock.tsx | 4 +- src/Value.tsx | 38 +++------- src/eval.ts | 150 +++++++++++++++++++++++++++++---------- src/util/parse.ts | 20 ------ 12 files changed, 357 insertions(+), 291 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7b5fdd..c0542d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/App.tsx b/src/App.tsx index ee63d66..becf6ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { +
- + {}} - suggestionPriority={() => 0} + suggestionPriority={() => 1} /> diff --git a/src/CallBlock.tsx b/src/CallBlock.tsx index aaf757a..8472644 100644 --- a/src/CallBlock.tsx +++ b/src/CallBlock.tsx @@ -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 + return
-
+ {/* Sequence of input parameters */} - {/* Output (or Error) */} - { resolved instanceof DeepError && resolved.e.toString() - || resolved && <> - } -
+ +
+
; +} + +export function Output({resolved, children}) { + return
+ {children} + { (resolved.kind === "error") && resolved.e.toString() + || (resolved.kind === "value") && + || "unknown" } +
; +} + +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 + +
+ +
+ computePriority( + evalEditorBlock(state.fn, env), // fn *may* be set + inputSuggestion[2], // suggestions will be for input + suggestionPriority, // priority function we get from parent block + )} + /> +
+
; } function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number) { const resolved = evalCallBlock(fn, input); - if ((resolved && !(resolved instanceof DeepError))) { + + const r = outPriority(['literal', '', + // @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', '', 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,23 +157,28 @@ function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) { } else { // end of recursion - draw function name - return -  𝑓𝑛  - computePriority( - fnSuggestion[2], // suggestions will be for function - evalEditorBlock(input, env), // input *may* be set - suggestionPriority, // priority function we get from parent block - )} - /> - ; + return ; } } +function FunctionName({fn, setFn, onFnCancel, suggestionPriority, input}) { + const env = useContext(EnvContext); + return +  𝑓𝑛  + computePriority( + fnSuggestion[2], // suggestions will be for function + evalEditorBlock(input, env), // input *may* be set + suggestionPriority, // priority function we get from parent block + )} + /> + ; +} + function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth, suggestionPriority }) { const env = useContext(EnvContext); let nestedParams; @@ -158,6 +210,7 @@ function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDe const isOffending = depth === errorDepth; return
{nestedParams} + {/* Our own input param */}
; - // {(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: - // - // } - // {/* Our own input */} - // computePriority( - // evalEditorBlock(fn, env), // fn *may* be set - // inputSuggestion[2], // suggestions will be for input - // suggestionPriority, // priority function we get from parent block - // )} - // /> - // ; } - -// function NestedParams({fn, setFn, depth, errorDepth, suggestionPriority}) { -// const env = useContext(EnvContext); -// const { -// setFn : setFnFn, -// setInput : setFnInput, -// } = headlessCallBlock(setFn); -// return {/*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 -// )} -// />; -// } \ No newline at end of file diff --git a/src/CommandContext.ts b/src/CommandContext.ts index 58bca8f..01e12a9 100644 --- a/src/CommandContext.ts +++ b/src/CommandContext.ts @@ -4,6 +4,7 @@ interface GlobalActions { undo: () => void; redo: () => void; doHighlight: {[key:string]: () => void}; + syntacticSugar: boolean; } export const CommandContext = createContext(null); diff --git a/src/Editor.css b/src/Editor.css index 2104fd7..8e616d2 100644 --- a/src/Editor.css +++ b/src/Editor.css @@ -4,4 +4,5 @@ .commandInput { width: 90px; + margin-left: 10px; } \ No newline at end of file diff --git a/src/Editor.tsx b/src/Editor.tsx index f3e4d65..9be2868 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -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": - return EditorState)=>void} - suggestionPriority={suggestionPriority} - />; + if (globalContext?.syntacticSugar) { + return EditorState)=>void} + suggestionPriority={suggestionPriority} + />; + } + else { + return EditorState)=>void} + suggestionPriority={suggestionPriority} + />; + } case "let": return {renderBlock()} - { - (resolved && !(resolved instanceof DeepError)) - ?
- :: -
- : <> - } +
+  ::  +
`} - onKeyDown={onCommand} - value={""} - onChange={() => {}} /> + ref={commandInputRef} + spellCheck={false} + className="editable commandInput" + placeholder={``} + onKeyDown={onCommand} + value={""} + onChange={() => {}} /> ; } diff --git a/src/InputBlock.css b/src/InputBlock.css index e9dd622..57a52aa 100644 --- a/src/InputBlock.css +++ b/src/InputBlock.css @@ -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; diff --git a/src/InputBlock.tsx b/src/InputBlock.tsx index 184aba6..dc8fe16 100644 --- a/src/InputBlock.tsx +++ b/src/InputBlock.tsx @@ -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 { } 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,51 +163,67 @@ export function InputBlock({ state, setState, suggestionPriority, onCancel }: In } }; - return - - {/* Dropdown suggestions */} - {haveFocus && - + return + {/* Dropdown suggestions */} + {haveFocus && + - - } - {/* Input box */} - setHaveFocus(true)} - onBlur={() => setHaveFocus(false)} - spellCheck={false}/> - {/* Single 'grey' suggestion */} - {singleSuggestion} - + + } + {/* Single 'grey' suggestion */} + {text}{singleSuggestion} + {/* Input box */} + setHaveFocus(true)} + onBlur={() => setHaveFocus(false)} + spellCheck={false}/> ; } function Suggestions({ suggestions, onSelect, i, setI }) { + return <>{(suggestions.length > 0) && +
+ {suggestions.map((suggestion, j) => + )} +
+ }; +} + +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) && -
- {suggestions.map(([priority, kind, name, dynamic], j) => -
- ({priority}) ({kind}) {name} :: -
)} -
- }; -} \ No newline at end of file + return
+ ({priority}) ({kind}) {name} :: +
+} + +const SuggestionMemo = memo(Suggestion); \ No newline at end of file diff --git a/src/LetInBlock.tsx b/src/LetInBlock.tsx index aac23f1..7064975 100644 --- a/src/LetInBlock.tsx +++ b/src/LetInBlock.tsx @@ -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 { } export function makeInnerEnv(env, name: string, value: ResolvedType) { - if (value && !(value instanceof DeepError)) { + if (value.kind === "value") { return growEnv(env)(name)(value) } return env; diff --git a/src/Value.tsx b/src/Value.tsx index 28a789f..a7e9b6e 100644 --- a/src/Value.tsx +++ b/src/Value.tsx @@ -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 ; } + if (eqType(type)(Unit)) { + return ; + } const symbol = getSymbol(type); switch (symbol) { @@ -47,9 +50,6 @@ function ValueFunction() { function ValueBool({val}) { return {val.toString()}; } -// function Sum({val, elemType}) { -// return -// } function ValueList({val, elemType}) { return [{val.map((v, i) => )}]; } @@ -57,9 +57,10 @@ function ValueSet({val, elemType}) { return {'{'}{set.fold(acc => elem => acc.concat([elem]))([])(val).map((v, i) => )}{'}'}; } function ValueDict({val, keyType, valueType}) { - return {'{'}{set.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => - - + return {'{'}{dict.fold(acc => key => value => acc.concat([[key,value]]))([])(val).map(([key, value], i) => + + ⇒ + )}{'}'}; } function ValueSum({val, leftType, rightType}) { @@ -70,23 +71,6 @@ function ValueSum({val, leftType, rightType}) { function ValueProduct({val, leftType, rightType}) { return (); } -// function ValueDict({val, keyType, valueType}) { -// let i=0; -// return {'{'}<>{ -// dict.fold -// (acc => key => value => { -// console.log({acc, key, value}); -// return acc.concat([<> -// -// ⇒ -// -// ]); -// }) -// ([]) -// (val) -// .map(result => { -// console.log(result); -// return result; -// }) -// }{'}'}; -// } +function ValueUnit() { + return <>{'()'}; +} diff --git a/src/eval.ts b/src/eval.ts index 9c90e6f..4d5a67f 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -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)) { - try { - const outputResolved = apply(input)(fn); // may throw - return outputResolved; // success - } - catch (e) { - if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) { - throw e; - } - return new DeepError(e, 0); // eval error +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, + }; } - else if (input instanceof DeepError) { - return input; // bubble up the error - } - else if (fn instanceof DeepError) { - return new DeepError(fn.e, fn.depth+1); - } -} + try { + // fn is a function... + const outType = assignFn(fn.t, input.t); // may throw -function parseLiteral(text: string, type: string) { - // dirty - if (type === "Int") { - return { i: parseInt(text), t: Int }; + 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 }; + } } - if (type === "Double") { - return { i: parseDouble(text), t: Double }; + catch (e) { + if ((e instanceof UnifyError)) { + // even though fn was incompatible with the given parameter, we can still suppose that our output-type will be that of fn...? + const outType = fn.t.params[1](fn.t); + return { + kind: "error", + e, + depth: 0, + t: outType, + }; + } + throw e; } } 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[]; +} diff --git a/src/util/parse.ts b/src/util/parse.ts index fbd24f1..8b13789 100644 --- a/src/util/parse.ts +++ b/src/util/parse.ts @@ -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) { }; -}