From 7a2ecb7b60ba2aef95cde7855f9a51c0e4893720 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 19 Dec 2023 19:45:29 -0600 Subject: [PATCH 01/40] semantic tooltips - show only on identifiers --- .../languageSupport/autocomplete.tsx | 8 ++-- .../CodeEditor/useTooltipsExtension.tsx | 38 ++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx index 3ce44432f5..d8616be053 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx +++ b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx @@ -19,19 +19,21 @@ export function getNameNodes(tree: Tree, from: number) { let direction: "start" | "sibling" | "parent" | undefined = "start"; while (1) { - // Only for sibling nodes; `foo = { }` shouldn't autocomplete `foo`. if (cursor.type.is("Binding") && direction === "sibling") { + // Only for sibling nodes; `foo = { }` shouldn't autocomplete `foo`. const nameNode = cursor.node.getChild("VariableName"); if (nameNode) { nameNodes.push(nameNode); } - // Only for sibling nodes; Squiggle doesn't support recursive calls. } else if (cursor.type.is("FunDeclaration") && direction === "sibling") { + // Only for sibling nodes; Squiggle doesn't support recursive calls. const nameNode = cursor.node.getChild("FunctionName"); if (nameNode) { nameNodes.push(nameNode); } } else if (cursor.type.is("FunDeclaration") && direction !== "sibling") { + // Function declaration that's a parent, let's autocomplete its parameter names. + // Note that we also allow `direction === "start"`, to handle `f(foo) = foo` correctly. const parameterNodes = cursor.node.getChild("LambdaArgs")?.getChildren("LambdaParameter") ?? []; @@ -42,8 +44,6 @@ export function getNameNodes(tree: Tree, from: number) { nameNodes.push(nameNode); } } - } else if (cursor.type.is("Decorator") && direction !== "sibling") { - // TODO } // Move to the next node and store the direction that we used. diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index 053a821e12..d34e02b040 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -1,3 +1,4 @@ +import { syntaxTree } from "@codemirror/language"; import { EditorView, hoverTooltip, repositionTooltips } from "@codemirror/view"; import { FC, useEffect } from "react"; import { createRoot } from "react-dom/client"; @@ -28,22 +29,38 @@ const HoverTooltip: FC<{ hover: Hover; view: EditorView }> = ({ // See also: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover const wordHover = hoverTooltip((view, pos, side) => { const { doc } = view.state; - const { from, to, text } = doc.lineAt(pos); - let start = pos, - end = pos; - while (start > from && /[\w.]/.test(text[start - from - 1])) start--; - while (end < to && /[\w.]/.test(text[end - from])) end++; - if ((start === pos && side < 0) || (end === pos && side > 0)) return null; - const token = text.slice(start - from, end - from); - const hover = getFunctionDocumentation(token); + const tree = syntaxTree(view.state); + const cursor = tree.cursorAt(pos, side); + + const getText = () => doc.sliceString(cursor.node.from, cursor.node.to); + + switch (cursor.name) { + case "IdentifierExpr": + if (getText().match(/^[A-Z]/)) { + // TODO - expand the namespace to the identifier, or just show the namespace documentation + return null; + } + // TODO - check that the identifier is not overwritten by a local variable + break; // this is ok, might be a builtin + case "Field": { + if (!cursor.parent()) { + return null; + } + break; + } + default: + return null; + } + + const hover = getFunctionDocumentation(getText()); if (!hover) { return null; } return { - pos: start, - end, + pos: cursor.node.from, + end: cursor.node.to, above: true, create() { const dom = document.createElement("div"); @@ -52,6 +69,7 @@ const wordHover = hoverTooltip((view, pos, side) => { return { dom }; }, }; + return null; }); const tooltipTheme = EditorView.baseTheme({ From 55f13be3481790e743dce6afaa7f97044c8eacec Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 19 Dec 2023 19:49:23 -0600 Subject: [PATCH 02/40] remove support for void values --- packages/prettier-plugin/src/printer.ts | 2 -- packages/squiggle-lang/src/ast/parse.ts | 2 -- packages/squiggle-lang/src/ast/peggyHelpers.ts | 9 +-------- packages/squiggle-lang/src/ast/peggyParser.peggy | 4 ---- packages/squiggle-lang/src/expression/compile.ts | 2 -- packages/squiggle-lang/src/expression/index.ts | 7 +------ 6 files changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/prettier-plugin/src/printer.ts b/packages/prettier-plugin/src/printer.ts index bc3e1c8e7f..d62d2268c6 100644 --- a/packages/prettier-plugin/src/printer.ts +++ b/packages/prettier-plugin/src/printer.ts @@ -343,8 +343,6 @@ export function createSquigglePrinter( node.kind === "C" ? " : " : " else ", path.call(print, "falseExpression"), ]; - case "Void": - return "()"; case "UnitValue": return [typedPath(node).call(print, "value"), node.unit]; case "lineComment": diff --git a/packages/squiggle-lang/src/ast/parse.ts b/packages/squiggle-lang/src/ast/parse.ts index 27f37c3230..e7374655b9 100644 --- a/packages/squiggle-lang/src/ast/parse.ts +++ b/packages/squiggle-lang/src/ast/parse.ts @@ -109,8 +109,6 @@ function nodeToString(node: ASTNode): string { return `'${node.value}'`; // TODO - quote? case "Ternary": return sExpr([node.condition, node.trueExpression, node.falseExpression]); - case "Void": - return "()"; case "UnitValue": // S-expression; we should migrate to S-expressions for other branches too, for easier testing. return sExpr([node.value, node.unit]); diff --git a/packages/squiggle-lang/src/ast/peggyHelpers.ts b/packages/squiggle-lang/src/ast/peggyHelpers.ts index 75c3147696..29c1f379b1 100644 --- a/packages/squiggle-lang/src/ast/peggyHelpers.ts +++ b/packages/squiggle-lang/src/ast/peggyHelpers.ts @@ -163,8 +163,6 @@ type NodeString = N<"String", { value: string }>; type NodeBoolean = N<"Boolean", { value: boolean }>; -type NodeVoid = N<"Void", object>; - export type ASTNode = | NodeArray | NodeDict @@ -188,8 +186,7 @@ export type ASTNode = | NodeTernary | NodeKeyValue | NodeString - | NodeBoolean - | NodeVoid; + | NodeBoolean; export function nodeCall( fn: ASTNode, @@ -440,10 +437,6 @@ export function nodeTernary( }; } -export function nodeVoid(location: LocationRange): NodeVoid { - return { type: "Void", location }; -} - export type ASTCommentNode = { type: "lineComment" | "blockComment"; value: string; diff --git a/packages/squiggle-lang/src/ast/peggyParser.peggy b/packages/squiggle-lang/src/ast/peggyParser.peggy index 8eb36af8b3..dcd6a24811 100644 --- a/packages/squiggle-lang/src/ast/peggyParser.peggy +++ b/packages/squiggle-lang/src/ast/peggyParser.peggy @@ -251,10 +251,6 @@ basicLiteral / number / boolean / variable - / voidLiteral - -voidLiteral 'void' - = "()" {return h.nodeVoid(location());} variable = dollarIdentifierWithModule / dollarIdentifier diff --git a/packages/squiggle-lang/src/expression/compile.ts b/packages/squiggle-lang/src/expression/compile.ts index a29955e69b..2d8e866b78 100644 --- a/packages/squiggle-lang/src/expression/compile.ts +++ b/packages/squiggle-lang/src/expression/compile.ts @@ -323,8 +323,6 @@ function compileToContent( } case "String": return [expression.eValue(vString(ast.value)), context]; - case "Void": - return [expression.eVoid(), context]; case "Identifier": { const offset = context.nameToPos.get(ast.value); if (offset === undefined) { diff --git a/packages/squiggle-lang/src/expression/index.ts b/packages/squiggle-lang/src/expression/index.ts index 5538c774c3..1324c13b81 100644 --- a/packages/squiggle-lang/src/expression/index.ts +++ b/packages/squiggle-lang/src/expression/index.ts @@ -3,7 +3,7 @@ * Expressions are evaluated by reducer's `evaluate` function. */ import { ASTNode } from "../ast/parse.js"; -import { Value, vVoid } from "../value/index.js"; +import { Value } from "../value/index.js"; export type LambdaExpressionParameter = { name: string; @@ -172,11 +172,6 @@ export const eTernary = ( }, }); -export const eVoid = (): ExpressionContent => ({ - type: "Value", - value: vVoid(), -}); - // Converts the expression to String. Useful for tests. export function expressionToString(expression: Expression): string { switch (expression.type) { From 6503fc8a0b70aaa6cd883523d73060901fd02f5c Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 19 Dec 2023 20:35:37 -0600 Subject: [PATCH 03/40] unify some AST node names in Lezer and Peggy --- .../languageSupport/autocomplete.tsx | 6 +-- .../languageSupport/highlightingStyle.ts | 2 +- .../languageSupport/squiggle.grammar | 42 +++++++++---------- .../CodeEditor/languageSupport/squiggle.ts | 30 +++++-------- packages/components/test/grammar.test.ts | 18 +++++--- 5 files changed, 46 insertions(+), 52 deletions(-) diff --git a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx index d8616be053..d217c6448d 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx +++ b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx @@ -19,19 +19,19 @@ export function getNameNodes(tree: Tree, from: number) { let direction: "start" | "sibling" | "parent" | undefined = "start"; while (1) { - if (cursor.type.is("Binding") && direction === "sibling") { + if (cursor.type.is("LetStatement") && direction === "sibling") { // Only for sibling nodes; `foo = { }` shouldn't autocomplete `foo`. const nameNode = cursor.node.getChild("VariableName"); if (nameNode) { nameNodes.push(nameNode); } - } else if (cursor.type.is("FunDeclaration") && direction === "sibling") { + } else if (cursor.type.is("DefunStatement") && direction === "sibling") { // Only for sibling nodes; Squiggle doesn't support recursive calls. const nameNode = cursor.node.getChild("FunctionName"); if (nameNode) { nameNodes.push(nameNode); } - } else if (cursor.type.is("FunDeclaration") && direction !== "sibling") { + } else if (cursor.type.is("DefunStatement") && direction !== "sibling") { // Function declaration that's a parent, let's autocomplete its parameter names. // Note that we also allow `direction === "start"`, to handle `f(foo) = foo` correctly. const parameterNodes = diff --git a/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts b/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts index e3ba235aa7..3e73cce6e4 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts +++ b/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts @@ -67,7 +67,7 @@ export const lightThemeHighlightingStyle = HighlightStyle.define([ fontWeight: "bold", color: operators, }, - { tag: [tags.meta, tags.comment], color: comments }, + { tag: tags.comment, color: comments }, { tag: tags.strong, fontWeight: "bold" }, { tag: tags.emphasis, fontStyle: "italic" }, { tag: tags.strikethrough, textDecoration: "line-through" }, diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar index b5b8f13797..35095a7034 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar @@ -40,7 +40,7 @@ commaSep { // when trailing comma is allowed commaSep1 { "" | content ("," content?)* } -Binding { export? VariableName { identifier } "=" expression } +LetStatement { export? VariableName { identifier } "=" expression } LambdaParameter { LambdaParameterName { identifier } (":" expression)? @@ -50,7 +50,7 @@ LambdaArgs { () | LambdaParameter ("," LambdaParameter)* } -FunDeclaration { export? FunctionName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression } +DefunStatement { export? FunctionName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression } Decorator { "@" DecoratorName { identifier } @@ -63,8 +63,8 @@ Decorator { statement { Decorator* ( - Binding - | FunDeclaration + LetStatement + | DefunStatement ) } @@ -72,26 +72,26 @@ expression[@isGroup="Expression"] { String | Boolean | Number - | BlockExpr { "{" blockContent "}" } - | DictExpr { + | Block { "{" blockContent "}" } + | Dict { "{" commaSep1< - Entry { Field[@dynamicPrecedence=1] { expression } ~inheritAmbig ":" expression } + KeyValue { Field[@dynamicPrecedence=1] { expression } ~inheritAmbig ":" expression } | InheritEntry { Field[@dynamicPrecedence=0] { identifier } ~inheritAmbig } > "}" } - | LambdaExpr { "{" ArgsOpen { "|" } LambdaArgs "|" blockContent "}" } - | IfExpr { if expression then expression !else else expression } + | Lambda { "{" ArgsOpen { "|" } LambdaArgs "|" blockContent "}" } | ParenExpr { "(" expression ")" } | IdentifierExpr { identifier } | AccessExpr { expression !deref "." Field { identifier } } - | CallExpr { expression ~callOrDeclaration !call "(" commaSep ")" } - | TernaryExpr { expression !logop LogicOp<"?"> expression LogicOp<":"> expression } - | KVAccessExpr { expression !call ("[" Key { expression } "]") } - | ArrayExpr { "[" commaSep1 "]" } - | UnaryExpr { !unary (ArithOp<"-"> | ArithOp<"!"> | DotArithOp<".-">) expression } - | LogicExpr { + | Call { expression ~callOrDeclaration !call "(" commaSep ")" } + | TernaryC { expression !logop LogicOp<"?"> expression LogicOp<":"> expression } + | TernaryIfThenElse { if expression then expression !else else expression } + | BracketLookup { expression !call ("[" Key { expression } "]") } + | Array { "[" commaSep1 "]" } + | UnaryCall { !unary (ArithOp<"-"> | ArithOp<"!"> | DotArithOp<".-">) expression } + | InfixCall { expression !or LogicOp<"||"> expression | expression !and LogicOp<"&&"> expression | expression !rel LogicOp<">"> expression @@ -99,22 +99,18 @@ expression[@isGroup="Expression"] { | expression !rel LogicOp<"<="> expression | expression !rel LogicOp<">="> expression | expression !rel LogicOp<"=="> expression - } - | ControlExpr { - expression !control ControlOp<"->"> expression - } - | ArithExpr { - expression !times ( ArithOp<"*"> | DotArithOp<".*"> ) expression + | expression !times ( ArithOp<"*"> | DotArithOp<".*"> ) expression | expression !times ( ArithOp<"/"> | DotArithOp<"./"> ) expression | expression !exp ( ArithOp<"^"> | DotArithOp<".^"> ) expression | expression !plus ( ArithOp<"+"> | DotArithOp<".+"> ) expression | expression !plus ( ArithOp<"-"> | DotArithOp<".-"> ) expression | expression !plus @extend[@name="ArithOp"] expression } + | Pipe { + expression !control ControlOp<"->"> expression + } } - - Boolean { @specialize[@name="Boolean"] } kw { @specialize[@name={term}] } diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts index c61c3f0a9c..060edca117 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts @@ -15,15 +15,9 @@ import { parser } from "./generated/squiggle.js"; const parserWithMetadata = parser.configure({ props: [ styleTags({ - if: t.keyword, - then: t.keyword, - else: t.keyword, - import: t.keyword, - export: t.keyword, - as: t.keyword, + "if then else import export as": t.keyword, Equals: t.definitionOperator, - ArithOp: t.arithmeticOperator, LogicOp: t.logicOperator, ControlOp: t.controlOperator, @@ -38,8 +32,8 @@ const parserWithMetadata = parser.configure({ Boolean: t.bool, Number: t.integer, String: t.string, - Comment: t.comment, - Void: t.escape, + LineComment: t.lineComment, + BlockComment: t.blockComment, Escape: t.escape, FunctionName: t.function(t.variableName), @@ -47,30 +41,28 @@ const parserWithMetadata = parser.configure({ DecoratorName: t.variableName, At: t.keyword, - LambdaSyntax: t.blockComment, - VariableName: t.constant(t.variableName), IdentifierExpr: t.variableName, Field: t.variableName, LambdaParameterName: t.variableName, }), foldNodeProp.add({ - LambdaExpr: (context) => ({ + Lambda: (context) => ({ from: context.getChild("NonEmptyProgram")?.from || 0, to: context.getChild("NonEmptyProgram")?.to || 0, }), - BlockExpr: foldInside, - DictExpr: foldInside, - ArrayExpr: foldInside, + Block: foldInside, + Dict: foldInside, + Array: foldInside, }), indentNodeProp.add({ - DictExpr: (context) => + Dict: (context) => context.baseIndent + (context.textAfter === "}" ? 0 : context.unit), - BlockExpr: (context) => + Block: (context) => context.baseIndent + (context.textAfter === "}" ? 0 : context.unit), - LambdaExpr: (context) => + Lambda: (context) => context.baseIndent + (context.textAfter === "}" ? 0 : context.unit), - ArrayExpr: (context) => + Array: (context) => context.baseIndent + (context.textAfter === "]" ? 0 : context.unit), }), ], diff --git a/packages/components/test/grammar.test.ts b/packages/components/test/grammar.test.ts index 2e9e9c023b..b7b3b3e598 100644 --- a/packages/components/test/grammar.test.ts +++ b/packages/components/test/grammar.test.ts @@ -3,7 +3,7 @@ import { parser } from "../src/components/CodeEditor/languageSupport/generated/s describe("Lezer grammar", () => { test("Basic", () => { expect(parser.parse("2+2").toString()).toBe( - "Program(ArithExpr(Number,ArithOp,Number))" + "Program(InfixCall(Number,ArithOp,Number))" ); }); @@ -16,7 +16,7 @@ bar = 6` ) .toString() ).toBe( - "Program(Binding(VariableName,Equals,Number),Binding(VariableName,Equals,Number))" + "Program(LetStatement(VariableName,Equals,Number),LetStatement(VariableName,Equals,Number))" ); }); @@ -30,7 +30,7 @@ foo + bar` ) .toString() ).toBe( - "Program(Binding(VariableName,Equals,Number),Binding(VariableName,Equals,Number),ArithExpr(IdentifierExpr,ArithOp,IdentifierExpr))" + "Program(LetStatement(VariableName,Equals,Number),LetStatement(VariableName,Equals,Number),InfixCall(IdentifierExpr,ArithOp,IdentifierExpr))" ); }); @@ -43,7 +43,7 @@ foo(5)` ) .toString() ).toBe( - 'Program(FunDeclaration(FunctionName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,IdentifierExpr),CallExpr(IdentifierExpr,"(",Argument(Number),")"))' + 'Program(DefunStatement(FunctionName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,IdentifierExpr),Call(IdentifierExpr,"(",Argument(Number),")"))' ); }); @@ -57,7 +57,7 @@ bar" ` ) .toString() - ).toBe("Program(Binding(VariableName,Equals,String))"); + ).toBe("Program(LetStatement(VariableName,Equals,String))"); }); test("Decorators", () => { @@ -70,7 +70,13 @@ x = 5 ) .toString() ).toBe( - 'Program(Decorator(At,DecoratorName,"(",Argument(String),")"),Binding(VariableName,Equals,Number))' + 'Program(Decorator(At,DecoratorName,"(",Argument(String),")"),LetStatement(VariableName,Equals,Number))' + ); + }); + + test("Pipe", () => { + expect(parser.parse("5 -> max(6)").toString()).toBe( + 'Program(Pipe(Number,ControlOp,Call(IdentifierExpr,"(",Argument(Number),")")))' ); }); }); From 1ec568c398085cf37d314b73eb43d1986d7c8cca Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 19 Dec 2023 20:51:38 -0600 Subject: [PATCH 04/40] parse&highlight keywords used as var names correctly --- .../CodeEditor/languageSupport/highlightingStyle.ts | 5 +---- .../components/CodeEditor/languageSupport/squiggle.grammar | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts b/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts index 3e73cce6e4..c6bbe4c9c0 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts +++ b/packages/components/src/components/CodeEditor/languageSupport/highlightingStyle.ts @@ -51,10 +51,7 @@ export const lightThemeHighlightingStyle = HighlightStyle.define([ ], color: numbers, }, - { - tag: [tags.escape], - color: escapes, - }, + { tag: tags.escape, color: escapes }, { tag: [ tags.operator, diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar index 35095a7034..245beceb91 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar @@ -70,7 +70,7 @@ statement { expression[@isGroup="Expression"] { String - | Boolean + | Boolean { @specialize[@name="Boolean"] } | Number | Block { "{" blockContent "}" } | Dict { @@ -111,9 +111,9 @@ expression[@isGroup="Expression"] { } } -Boolean { @specialize[@name="Boolean"] } +// use `@extend` instead of `@specialize`, because keywords are valid variable names in Squiggle, for now. +kw { @extend[@name={term}] } -kw { @specialize[@name={term}] } if { kw<"if"> } then { kw<"then"> } else { kw<"else"> } From 492dae05c75fc14ba3f8536a495017d0293e8629 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 19 Dec 2023 21:12:38 -0600 Subject: [PATCH 05/40] DecoratedStatement in Lezer grammar --- .../languageSupport/autocomplete.tsx | 48 ++++++++++++------- .../languageSupport/squiggle.grammar | 12 ++--- .../CodeEditor/languageSupport/squiggle.ts | 2 - .../components/test/autocompletion.test.ts | 20 ++++---- packages/components/test/grammar.test.ts | 4 +- 5 files changed, 49 insertions(+), 37 deletions(-) diff --git a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx index d217c6448d..6dc79c588c 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx +++ b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx @@ -11,25 +11,34 @@ import { getFunctionDocumentation, SqProject } from "@quri/squiggle-lang"; import { FnDocumentation } from "../../ui/FnDocumentation.js"; -export function getNameNodes(tree: Tree, from: number) { +type NameNode = { + node: SyntaxNode; + type: "function" | "variable"; +}; + +export function getNameNodes(tree: Tree, from: number): NameNode[] { const cursor = tree.cursorAt(from, -1); - const nameNodes: SyntaxNode[] = []; + const nameNodes: NameNode[] = []; // We walk up and backwards through the tree, looking for nodes that have names. let direction: "start" | "sibling" | "parent" | undefined = "start"; while (1) { - if (cursor.type.is("LetStatement") && direction === "sibling") { + if (cursor.type.is("Statement") && direction === "sibling") { // Only for sibling nodes; `foo = { }` shouldn't autocomplete `foo`. - const nameNode = cursor.node.getChild("VariableName"); - if (nameNode) { - nameNodes.push(nameNode); + + // Unwrap decorated statements. + let node: SyntaxNode | null = cursor.node; + while (node && node.type.is("DecoratedStatement")) { + node = node.getChild("Statement"); } - } else if (cursor.type.is("DefunStatement") && direction === "sibling") { - // Only for sibling nodes; Squiggle doesn't support recursive calls. - const nameNode = cursor.node.getChild("FunctionName"); - if (nameNode) { - nameNodes.push(nameNode); + + const nameNode = node?.getChild("VariableName"); + if (node && nameNode) { + nameNodes.push({ + node: nameNode, + type: node?.type.is("DefunStatement") ? "function" : "variable", + }); } } else if (cursor.type.is("DefunStatement") && direction !== "sibling") { // Function declaration that's a parent, let's autocomplete its parameter names. @@ -41,7 +50,12 @@ export function getNameNodes(tree: Tree, from: number) { for (const parameter of parameterNodes) { const nameNode = parameter.getChild("LambdaParameterName"); if (nameNode) { - nameNodes.push(nameNode); + nameNodes.push({ + node: nameNode, + // Is there a more specific type? There's no "parameter" type in CodeMirror. + // https://codemirror.net/docs/ref/#autocomplete.Completion.type + type: "variable", + }); } } } @@ -122,12 +136,14 @@ export function makeCompletionSource(project: SqProject) { if (identifier) { const { from } = identifier; const nameNodes = getNameNodes(tree, from); - const localCompletions = nameNodes.map((node): Completion => { - const name = cmpl.state.doc.sliceString(node.from, node.to); - const type = node.type.is("FunctionName") ? "function" : "variable"; + const localCompletions = nameNodes.map((nameNode): Completion => { + const name = cmpl.state.doc.sliceString( + nameNode.node.from, + nameNode.node.to + ); return { label: name, - type, + type: nameNode.type, }; }); diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar index 245beceb91..41e8a4688b 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar @@ -50,7 +50,7 @@ LambdaArgs { () | LambdaParameter ("," LambdaParameter)* } -DefunStatement { export? FunctionName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression } +DefunStatement { export? VariableName { identifier } ~callOrDeclaration "(" LambdaArgs ")" "=" expression } Decorator { "@" DecoratorName { identifier } @@ -60,12 +60,10 @@ Decorator { ) } -statement { - Decorator* - ( - LetStatement - | DefunStatement - ) +statement[@isGroup="Statement"] { + LetStatement + | DefunStatement + | DecoratedStatement { Decorator statement } } expression[@isGroup="Expression"] { diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts index 060edca117..ea7f8c4c9a 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts @@ -36,8 +36,6 @@ const parserWithMetadata = parser.configure({ BlockComment: t.blockComment, Escape: t.escape, - FunctionName: t.function(t.variableName), - DecoratorName: t.variableName, At: t.keyword, diff --git a/packages/components/test/autocompletion.test.ts b/packages/components/test/autocompletion.test.ts index 4e4a147c7d..5a840e7442 100644 --- a/packages/components/test/autocompletion.test.ts +++ b/packages/components/test/autocompletion.test.ts @@ -17,8 +17,8 @@ bar = 6 const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("bar"); // code is traversed backwards - expect(getText(code, nodes[1])).toBe("foo"); + expect(getText(code, nodes[0].node)).toBe("bar"); // code is traversed backwards + expect(getText(code, nodes[1].node)).toBe("foo"); }); test("Parameter names", () => { @@ -28,8 +28,8 @@ myFun(arg1, arg2) = 1+`; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("arg1"); - expect(getText(code, nodes[1])).toBe("arg2"); + expect(getText(code, nodes[0].node)).toBe("arg1"); + expect(getText(code, nodes[1].node)).toBe("arg2"); }); test("Parameter names at the start of function body", () => { @@ -39,8 +39,8 @@ myFun(arg1, arg2) = `; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("arg1"); - expect(getText(code, nodes[1])).toBe("arg2"); + expect(getText(code, nodes[0].node)).toBe("arg1"); + expect(getText(code, nodes[1].node)).toBe("arg2"); }); test("Don't suggest current binding", () => { @@ -51,7 +51,7 @@ bar = {`; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(1); - expect(getText(code, nodes[0])).toBe("foo"); + expect(getText(code, nodes[0].node)).toBe("foo"); }); test("Don't suggest parameters of sibling functions", () => { @@ -62,8 +62,8 @@ fun2(arg2) =`; const nodes = getNameNodes(tree, code.length); expect(nodes.length).toBe(2); - expect(getText(code, nodes[0])).toBe("arg2"); - expect(getText(code, nodes[1])).toBe("fun1"); + expect(getText(code, nodes[0].node)).toBe("arg2"); + expect(getText(code, nodes[1].node)).toBe("fun1"); }); test("Don't suggest bindings declared later", () => { @@ -78,6 +78,6 @@ bar = 2 const nodes = getNameNodes(tree, code1.length); expect(nodes.length).toBe(1); - expect(getText(code, nodes[0])).toBe("foo"); + expect(getText(code, nodes[0].node)).toBe("foo"); }); }); diff --git a/packages/components/test/grammar.test.ts b/packages/components/test/grammar.test.ts index b7b3b3e598..647f19b96f 100644 --- a/packages/components/test/grammar.test.ts +++ b/packages/components/test/grammar.test.ts @@ -43,7 +43,7 @@ foo(5)` ) .toString() ).toBe( - 'Program(DefunStatement(FunctionName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,IdentifierExpr),Call(IdentifierExpr,"(",Argument(Number),")"))' + 'Program(DefunStatement(VariableName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,IdentifierExpr),Call(IdentifierExpr,"(",Argument(Number),")"))' ); }); @@ -70,7 +70,7 @@ x = 5 ) .toString() ).toBe( - 'Program(Decorator(At,DecoratorName,"(",Argument(String),")"),LetStatement(VariableName,Equals,Number))' + 'Program(DecoratedStatement(Decorator(At,DecoratorName,"(",Argument(String),")"),LetStatement(VariableName,Equals,Number)))' ); }); From 3f351f218d6e40cd8a1d4727602a5137960b1096 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 19 Dec 2023 21:27:14 -0600 Subject: [PATCH 06/40] more Lezer/Peggy unification --- .../CodeEditor/languageSupport/autocomplete.tsx | 4 ++-- .../CodeEditor/languageSupport/squiggle.grammar | 10 +++++++--- .../CodeEditor/languageSupport/squiggle.ts | 2 +- .../components/CodeEditor/useTooltipsExtension.tsx | 2 +- packages/components/test/grammar.test.ts | 12 +++++++++--- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx index 6dc79c588c..3d8d089619 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx +++ b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx @@ -124,7 +124,7 @@ export function makeCompletionSource(project: SqProject) { snippetCompletion("|${args}| ${body}", { label: "|", detail: "lambda function", - type: "syntax", + type: "text", }), ], }; @@ -132,7 +132,7 @@ export function makeCompletionSource(project: SqProject) { } { - const identifier = cmpl.tokenBefore(["AccessExpr", "IdentifierExpr"]); + const identifier = cmpl.tokenBefore(["AccessExpr", "Identifier"]); if (identifier) { const { from } = identifier; const nameNodes = getNameNodes(tree, from); diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar index 41e8a4688b..00b4cfd169 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.grammar @@ -66,7 +66,12 @@ statement[@isGroup="Statement"] { | DecoratedStatement { Decorator statement } } -expression[@isGroup="Expression"] { +expression { + expressionWithoutParens + | ( "(" expression ")" ) +} + +expressionWithoutParens[@isGroup="Expression"] { String | Boolean { @specialize[@name="Boolean"] } | Number @@ -80,8 +85,7 @@ expression[@isGroup="Expression"] { "}" } | Lambda { "{" ArgsOpen { "|" } LambdaArgs "|" blockContent "}" } - | ParenExpr { "(" expression ")" } - | IdentifierExpr { identifier } + | Identifier { identifier } | AccessExpr { expression !deref "." Field { identifier } } | Call { expression ~callOrDeclaration !call "(" commaSep ")" } | TernaryC { expression !logop LogicOp<"?"> expression LogicOp<":"> expression } diff --git a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts index ea7f8c4c9a..5df0cca3a9 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts +++ b/packages/components/src/components/CodeEditor/languageSupport/squiggle.ts @@ -40,7 +40,7 @@ const parserWithMetadata = parser.configure({ At: t.keyword, VariableName: t.constant(t.variableName), - IdentifierExpr: t.variableName, + Identifier: t.variableName, Field: t.variableName, LambdaParameterName: t.variableName, }), diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index d34e02b040..7d97e9b459 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -36,7 +36,7 @@ const wordHover = hoverTooltip((view, pos, side) => { const getText = () => doc.sliceString(cursor.node.from, cursor.node.to); switch (cursor.name) { - case "IdentifierExpr": + case "Identifier": if (getText().match(/^[A-Z]/)) { // TODO - expand the namespace to the identifier, or just show the namespace documentation return null; diff --git a/packages/components/test/grammar.test.ts b/packages/components/test/grammar.test.ts index 647f19b96f..4e0d5fca6e 100644 --- a/packages/components/test/grammar.test.ts +++ b/packages/components/test/grammar.test.ts @@ -7,6 +7,12 @@ describe("Lezer grammar", () => { ); }); + test("Parens", () => { + expect(parser.parse("((2))").toString()).toBe( + 'Program("(","(",Number,")",")")' + ); + }); + test("Statements only", () => { expect( parser @@ -30,7 +36,7 @@ foo + bar` ) .toString() ).toBe( - "Program(LetStatement(VariableName,Equals,Number),LetStatement(VariableName,Equals,Number),InfixCall(IdentifierExpr,ArithOp,IdentifierExpr))" + "Program(LetStatement(VariableName,Equals,Number),LetStatement(VariableName,Equals,Number),InfixCall(Identifier,ArithOp,Identifier))" ); }); @@ -43,7 +49,7 @@ foo(5)` ) .toString() ).toBe( - 'Program(DefunStatement(VariableName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,IdentifierExpr),Call(IdentifierExpr,"(",Argument(Number),")"))' + 'Program(DefunStatement(VariableName,"(",LambdaArgs(LambdaParameter(LambdaParameterName)),")",Equals,Identifier),Call(Identifier,"(",Argument(Number),")"))' ); }); @@ -76,7 +82,7 @@ x = 5 test("Pipe", () => { expect(parser.parse("5 -> max(6)").toString()).toBe( - 'Program(Pipe(Number,ControlOp,Call(IdentifierExpr,"(",Argument(Number),")")))' + 'Program(Pipe(Number,ControlOp,Call(Identifier,"(",Argument(Number),")")))' ); }); }); From 5d1a0f8505a0228a7ca16a4c2bd938c2ac2751bc Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 20 Dec 2023 13:16:53 -0600 Subject: [PATCH 07/40] tooltips for top-level values --- .../src/components/CodeEditor/index.tsx | 2 +- .../CodeEditor/useSquiggleEditorView.ts | 5 +- .../CodeEditor/useTooltipsExtension.tsx | 181 +++++++++++++----- .../CodeEditor/useViewNodeExtension.ts | 4 +- .../src/components/SquiggleEditor.tsx | 3 +- 5 files changed, 147 insertions(+), 48 deletions(-) diff --git a/packages/components/src/components/CodeEditor/index.tsx b/packages/components/src/components/CodeEditor/index.tsx index 38d942fe00..a50717ca16 100644 --- a/packages/components/src/components/CodeEditor/index.tsx +++ b/packages/components/src/components/CodeEditor/index.tsx @@ -15,7 +15,7 @@ export type CodeEditorProps = { showGutter?: boolean; lineWrapping?: boolean; errors?: SqError[]; - sourceId?: string; + sourceId: string; project: SqProject; }; diff --git a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts index f8578fbdcd..7404475634 100644 --- a/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts +++ b/packages/components/src/components/CodeEditor/useSquiggleEditorView.ts @@ -110,7 +110,10 @@ export function useSquiggleEditorExtensions( const formatExtension = useFormatSquiggleExtension(); const errorsExtension = useErrorsExtension(view, params.errors); - const tooltipsExtension = useTooltipsExtension(); + const tooltipsExtension = useTooltipsExtension(view, { + project: params.project, + sourceId: params.sourceId, + }); const squiggleExtensions = [ squiggleLanguageExtension, diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index 7d97e9b459..e033c6c0b0 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -1,76 +1,158 @@ import { syntaxTree } from "@codemirror/language"; import { EditorView, hoverTooltip, repositionTooltips } from "@codemirror/view"; -import { FC, useEffect } from "react"; +import { SyntaxNode } from "@lezer/common"; +import { FC, PropsWithChildren, useEffect } from "react"; import { createRoot } from "react-dom/client"; -import { getFunctionDocumentation } from "@quri/squiggle-lang"; +import { + getFunctionDocumentation, + SqProject, + SqValue, +} from "@quri/squiggle-lang"; +import { valueHasContext } from "../../lib/utility.js"; +import { defaultPlaygroundSettings } from "../PlaygroundSettings.js"; +import { SquiggleValueChart } from "../SquiggleViewer/SquiggleValueChart.js"; import { FnDocumentation } from "../ui/FnDocumentation.js"; +import { useReactiveExtension } from "./codemirrorHooks.js"; type Hover = NonNullable>; -const HoverTooltip: FC<{ hover: Hover; view: EditorView }> = ({ - hover, +const TooltipBox: FC> = ({ view, + children, }) => { useEffect(() => { - // https://codemirror.net/docs/ref/#view.repositionTooltips need to be called on each render. + // https://codemirror.net/docs/ref/#view.repositionTooltips needs to be called on each render. repositionTooltips(view); }); return (
- + {children}
); }; +const ValueTooltip: FC<{ value: SqValue; view: EditorView }> = ({ + value, + view, +}) => { + if (valueHasContext(value)) { + return ( + + + /> + + ); + } else { + return null; // shouldn't happen + } +}; + +const HoverTooltip: FC<{ hover: Hover; view: EditorView }> = ({ + hover, + view, +}) => ( + + + +); + // Based on https://codemirror.net/examples/tooltip/#hover-tooltips // See also: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover -const wordHover = hoverTooltip((view, pos, side) => { - const { doc } = view.state; +function buildWordHoverExtension({ + project, + sourceId, +}: { + project: SqProject; + sourceId: string; +}) { + return hoverTooltip((view, pos, side) => { + const { doc } = view.state; - const tree = syntaxTree(view.state); - const cursor = tree.cursorAt(pos, side); + const tree = syntaxTree(view.state); + const cursor = tree.cursorAt(pos, side); - const getText = () => doc.sliceString(cursor.node.from, cursor.node.to); + const getText = (node: SyntaxNode) => doc.sliceString(node.from, node.to); - switch (cursor.name) { - case "Identifier": - if (getText().match(/^[A-Z]/)) { - // TODO - expand the namespace to the identifier, or just show the namespace documentation + const createBuiltinTooltip = (node: SyntaxNode) => { + const hover = getFunctionDocumentation(getText(node)); + if (!hover) { return null; } - // TODO - check that the identifier is not overwritten by a local variable - break; // this is ok, might be a builtin - case "Field": { - if (!cursor.parent()) { - return null; + + return { + pos: node.from, + end: node.to, + above: true, + create() { + const dom = document.createElement("div"); + const root = createRoot(dom); + root.render(); + return { dom }; + }, + }; + }; + + const createTopLevelVariableNameTooltip = (node: SyntaxNode) => { + const name = getText(node); + + const bindings = project.getBindings(sourceId); + if (!bindings.ok) return null; + + const value = bindings.value.get(name); + if (!value) return null; + + return { + pos: node.from, + end: node.to, + above: true, + create() { + const dom = document.createElement("div"); + const root = createRoot(dom); + root.render(); + return { dom }; + }, + }; + }; + + switch (cursor.name) { + case "Identifier": + if (getText(cursor.node).match(/^[A-Z]/)) { + // TODO - expand the namespace to the identifier, or just show the namespace documentation + return null; + } + // TODO - check that the identifier is not overwritten by a local variable + return createBuiltinTooltip(cursor.node); + case "Field": + // `Namespace.function`; go up to fully identified name. + if (!cursor.parent()) { + return null; + } + return createBuiltinTooltip(cursor.node); + case "VariableName": { + const node = cursor.node; + + // Let's find the statement that declares this variable. + if (!cursor.parent()) { + return null; + } + // Ascend through decorated statements. + while (cursor.type.is("Statement") && cursor.parent()); + + // Is this a top-level variable? + if (cursor.type.is("Program")) { + return createTopLevelVariableNameTooltip(node); + } } - break; } - default: - return null; - } - const hover = getFunctionDocumentation(getText()); - if (!hover) { return null; - } - - return { - pos: cursor.node.from, - end: cursor.node.to, - above: true, - create() { - const dom = document.createElement("div"); - const root = createRoot(dom); - root.render(); - return { dom }; - }, - }; - return null; -}); + }); +} const tooltipTheme = EditorView.baseTheme({ ".cm-tooltip-hover": { @@ -79,6 +161,19 @@ const tooltipTheme = EditorView.baseTheme({ }, }); -export function useTooltipsExtension() { - return [wordHover, tooltipTheme]; +export function useTooltipsExtension( + view: EditorView | undefined, + { + project, + sourceId, + }: { + project: SqProject; + sourceId: string; + } +) { + return useReactiveExtension( + view, + () => [buildWordHoverExtension({ project, sourceId }), tooltipTheme], + [project, sourceId] + ); } diff --git a/packages/components/src/components/CodeEditor/useViewNodeExtension.ts b/packages/components/src/components/CodeEditor/useViewNodeExtension.ts index b9981d2aa3..f65a69574f 100644 --- a/packages/components/src/components/CodeEditor/useViewNodeExtension.ts +++ b/packages/components/src/components/CodeEditor/useViewNodeExtension.ts @@ -9,12 +9,12 @@ export function useViewNodeExtension( view: EditorView | undefined, { project, - onViewValuePath, sourceId, + onViewValuePath, }: { project: SqProject; + sourceId: string; onViewValuePath?: (path: SqValuePath) => void; - sourceId?: string; } ) { const viewCurrentPosition = useCallback(() => { diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index 39f13179f6..825fdd7d71 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -30,7 +30,7 @@ export const SquiggleEditor: FC = ({ const runnerState = useRunnerState(code); - const [squiggleOutput, { project, isRunning }] = useSquiggle({ + const [squiggleOutput, { project, isRunning, sourceId }] = useSquiggle({ code: runnerState.renderedCode, executionId: runnerState.executionId, ...(propsProject ? { project: propsProject, continues } : { environment }), @@ -57,6 +57,7 @@ export const SquiggleEditor: FC = ({ showGutter={false} errors={errors} project={project} + sourceId={sourceId} ref={editorRef} onSubmit={() => runnerState.run()} /> From 4dcf9b345582bec652afdcb01ec0f000f4d4dcc6 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 11:57:57 -0600 Subject: [PATCH 08/40] minor cleanups + margin changes (WIP) --- .../SquiggleViewer/ValueWithContextViewer.tsx | 4 +- .../src/components/SquiggleViewer/index.tsx | 43 ++++++------------- .../components/src/widgets/BoolWidget.tsx | 10 +---- .../components/src/widgets/DurationWidget.tsx | 7 +-- .../components/src/widgets/NumberWidget.tsx | 7 +-- 5 files changed, 18 insertions(+), 53 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx b/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx index 26dd73561a..2602271fca 100644 --- a/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx +++ b/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx @@ -9,7 +9,6 @@ import { CommentIcon, TextTooltip } from "@quri/ui"; import { MarkdownViewer } from "../../lib/MarkdownViewer.js"; import { SqValueWithContext } from "../../lib/utility.js"; -import { leftWidgetMargin } from "../../widgets/utils.js"; import { ErrorBoundary } from "../ErrorBoundary.js"; import { CollapsedIcon, ExpandedIcon } from "./icons.js"; import { SquiggleValueChart } from "./SquiggleValueChart.js"; @@ -73,7 +72,6 @@ const WithComment: FC> = ({ value, children }) => {
@@ -185,7 +183,7 @@ export const ValueWithContextViewer: FC = ({ const headerClasses = () => { if (header === "large") { - return clsx("text-md font-bold ml-1", headerColor); + return clsx("text-md font-bold", headerColor); } else if (isRoot) { return "text-sm text-stone-600 font-semibold"; } else { diff --git a/packages/components/src/components/SquiggleViewer/index.tsx b/packages/components/src/components/SquiggleViewer/index.tsx index 64e990b521..d87340aed7 100644 --- a/packages/components/src/components/SquiggleViewer/index.tsx +++ b/packages/components/src/components/SquiggleViewer/index.tsx @@ -49,7 +49,7 @@ const FocusedNavigation: FC<{ : 0; return ( -
+
{!rootPath?.items.length && ( )} @@ -87,37 +87,22 @@ const SquiggleViewerWithoutProvider: FC = ({ value }) => { focusedItem = getSubvalueByPath(value, focused); } - const body = () => { - if (focused) { - if (focusedItem) { - return ( -
- -
- ); - } else { - return ; - } - } else { - return ; - } - }; - - return ( -
- {focused && ( - + + {focusedItem ? ( + + ) : ( + )} - {body()}
+ ) : ( + ); }; diff --git a/packages/components/src/widgets/BoolWidget.tsx b/packages/components/src/widgets/BoolWidget.tsx index 49584c50b2..e84b3b0557 100644 --- a/packages/components/src/widgets/BoolWidget.tsx +++ b/packages/components/src/widgets/BoolWidget.tsx @@ -1,17 +1,9 @@ -import { clsx } from "clsx"; - import { widgetRegistry } from "./registry.js"; -import { leftWidgetMargin } from "./utils.js"; widgetRegistry.register("Bool", { Preview: (value) => value.value.toString(), Chart: (value) => ( -
+
{value.value.toString()}
), diff --git a/packages/components/src/widgets/DurationWidget.tsx b/packages/components/src/widgets/DurationWidget.tsx index 69745341f4..64b3891683 100644 --- a/packages/components/src/widgets/DurationWidget.tsx +++ b/packages/components/src/widgets/DurationWidget.tsx @@ -1,11 +1,8 @@ -import { clsx } from "clsx"; - import { SqDurationValue } from "@quri/squiggle-lang"; import { NumberShower } from "../components/NumberShower.js"; import { formatNumber } from "../lib/d3/index.js"; import { widgetRegistry } from "./registry.js"; -import { leftWidgetMargin } from "./utils.js"; const showDuration = (duration: SqDurationValue) => { const numberFormat = duration.tags.numberFormat(); @@ -21,9 +18,7 @@ widgetRegistry.register("Duration", { Preview: (value) => showDuration(value), Chart: (value) => { return ( -
- {showDuration(value)} -
+
{showDuration(value)}
); }, }); diff --git a/packages/components/src/widgets/NumberWidget.tsx b/packages/components/src/widgets/NumberWidget.tsx index e772a04d90..dfabf3106c 100644 --- a/packages/components/src/widgets/NumberWidget.tsx +++ b/packages/components/src/widgets/NumberWidget.tsx @@ -1,11 +1,8 @@ -import { clsx } from "clsx"; - import { SqNumberValue } from "@quri/squiggle-lang"; import { NumberShower } from "../components/NumberShower.js"; import { formatNumber } from "../lib/d3/index.js"; import { widgetRegistry } from "./registry.js"; -import { leftWidgetMargin } from "./utils.js"; const showNumber = (value: SqNumberValue) => { const numberFormat = value.tags.numberFormat(); @@ -19,8 +16,6 @@ const showNumber = (value: SqNumberValue) => { widgetRegistry.register("Number", { Preview: (value) => showNumber(value), Chart: (value) => ( -
- {showNumber(value)} -
+
{showNumber(value)}
), }); From 69f1367c2e0bd5ce0cd9e383b38bcc3caf3d4a75 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 12:45:36 -0600 Subject: [PATCH 09/40] fix focused left margin --- packages/components/src/components/SquiggleViewer/index.tsx | 2 +- packages/components/src/widgets/ArrayWidget.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/index.tsx b/packages/components/src/components/SquiggleViewer/index.tsx index d87340aed7..8393b7bbb8 100644 --- a/packages/components/src/components/SquiggleViewer/index.tsx +++ b/packages/components/src/components/SquiggleViewer/index.tsx @@ -88,7 +88,7 @@ const SquiggleViewerWithoutProvider: FC = ({ value }) => { } return focused ? ( -
+
{focusedItem ? ( Date: Thu, 18 Jan 2024 13:26:37 -0600 Subject: [PATCH 10/40] viewerType in SquiggleViewer; improve tooltips --- .../CodeEditor/useTooltipsExtension.tsx | 30 ++++++++++++------- .../SquiggleViewer/ValueWithContextViewer.tsx | 25 ++++++++++++---- .../SquiggleViewer/ViewerProvider.tsx | 18 ++++++++++- .../components/src/widgets/NumberWidget.tsx | 19 ++++++++++-- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index e93390a6db..2b4148c9d5 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -11,8 +11,11 @@ import { } from "@quri/squiggle-lang"; import { valueHasContext } from "../../lib/utility.js"; -import { defaultPlaygroundSettings } from "../PlaygroundSettings.js"; import { SquiggleValueChart } from "../SquiggleViewer/SquiggleValueChart.js"; +import { + InnerViewerProvider, + useViewerContext, +} from "../SquiggleViewer/ViewerProvider.js"; import { FnDocumentation } from "../ui/FnDocumentation.js"; import { useReactiveExtension } from "./codemirrorHooks.js"; @@ -27,24 +30,27 @@ const TooltipBox: FC> = ({ repositionTooltips(view); }); - return ( -
- {children} -
- ); + return
{children}
; }; const ValueTooltip: FC<{ value: SqValue; view: EditorView }> = ({ value, view, }) => { + const { globalSettings } = useViewerContext(); + if (valueHasContext(value)) { return ( - - /> +
+ {/* Force a standalone ephermeral ViewerProvider, so that we won't sync up collapsed state with the top-level viewer */} + + + +
); } else { @@ -57,7 +63,9 @@ const HoverTooltip: FC<{ hover: Hover; view: EditorView }> = ({ view, }) => ( - +
+ +
); diff --git a/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx b/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx index 2602271fca..76f18bbabd 100644 --- a/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx +++ b/packages/components/src/components/SquiggleViewer/ValueWithContextViewer.tsx @@ -25,6 +25,7 @@ import { useRegisterAsItemViewer, useToggleCollapsed, useViewerContext, + useViewerType, } from "./ViewerProvider.js"; const CommentIconForValue: FC<{ value: SqValueWithContext }> = ({ value }) => { @@ -117,6 +118,7 @@ export const ValueWithContextViewer: FC = ({ const toggleCollapsed_ = useToggleCollapsed(); const focus = useFocus(); + const viewerType = useViewerType(); const { itemStore } = useViewerContext(); const itemState = itemStore.getStateOrInitialize(value); @@ -128,6 +130,8 @@ export const ValueWithContextViewer: FC = ({ const header = props.header ?? (isRoot ? "hide" : "show"); const collapsible = header === "hide" ? false : props.collapsible ?? true; const size = props.size ?? "normal"; + const enableDropdownMenu = viewerType !== "tooltip"; + const enableFocus = viewerType !== "tooltip"; const toggleCollapsed = () => { toggleCollapsed_(path); @@ -139,7 +143,12 @@ export const ValueWithContextViewer: FC = ({ // In that case, the output would look broken (empty). const isOpen = !collapsible || !itemState.collapsed; - const _focus = () => focus(path); + const _focus = () => { + if (!enableFocus) { + return; + } + focus(path); + }; const triangleToggle = () => { const Icon = itemState.collapsed ? CollapsedIcon : ExpandedIcon; @@ -187,7 +196,11 @@ export const ValueWithContextViewer: FC = ({ } else if (isRoot) { return "text-sm text-stone-600 font-semibold"; } else { - return clsx("text-sm cursor-pointer hover:underline", headerColor); + return clsx( + "text-sm", + enableFocus && "cursor-pointer hover:underline", + headerColor + ); } }; @@ -241,9 +254,11 @@ export const ValueWithContextViewer: FC = ({ )} {!isOpen && }
-
- -
+ {enableDropdownMenu && ( +
+ +
+ )} )} {isOpen && ( diff --git a/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx b/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx index fe3b94bc9c..358ae24488 100644 --- a/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx +++ b/packages/components/src/components/SquiggleViewer/ViewerProvider.tsx @@ -30,6 +30,8 @@ import { shouldBeginCollapsed, } from "./utils.js"; +type ViewerType = "normal" | "tooltip"; + type ItemHandle = { element: HTMLDivElement; forceUpdate: () => void; @@ -162,6 +164,7 @@ type ViewerContextShape = { setFocused: (value: SqValuePath | undefined) => void; editor?: CodeEditorHandle; itemStore: ItemStore; + viewerType: ViewerType; initialized: boolean; }; @@ -171,6 +174,7 @@ export const ViewerContext = createContext({ setFocused: () => undefined, editor: undefined, itemStore: new ItemStore(), + viewerType: "normal", initialized: false, }); @@ -296,14 +300,25 @@ export function useMergedSettings(path: SqValuePath) { return result; } +export function useViewerType() { + const { viewerType } = useViewerContext(); + return viewerType; +} + type Props = PropsWithChildren<{ partialPlaygroundSettings: PartialPlaygroundSettings; editor?: CodeEditorHandle; + viewerType?: ViewerType; }>; export const InnerViewerProvider = forwardRef( ( - { partialPlaygroundSettings: unstablePlaygroundSettings, editor, children }, + { + partialPlaygroundSettings: unstablePlaygroundSettings, + editor, + viewerType = "normal", + children, + }, ref ) => { const [itemStore] = useState(() => new ItemStore()); @@ -337,6 +352,7 @@ export const InnerViewerProvider = forwardRef( focused, setFocused, itemStore, + viewerType, initialized: true, }} > diff --git a/packages/components/src/widgets/NumberWidget.tsx b/packages/components/src/widgets/NumberWidget.tsx index dfabf3106c..bf375bce6e 100644 --- a/packages/components/src/widgets/NumberWidget.tsx +++ b/packages/components/src/widgets/NumberWidget.tsx @@ -1,6 +1,9 @@ +import { clsx } from "clsx"; + import { SqNumberValue } from "@quri/squiggle-lang"; import { NumberShower } from "../components/NumberShower.js"; +import { useViewerType } from "../components/SquiggleViewer/ViewerProvider.js"; import { formatNumber } from "../lib/d3/index.js"; import { widgetRegistry } from "./registry.js"; @@ -15,7 +18,17 @@ const showNumber = (value: SqNumberValue) => { widgetRegistry.register("Number", { Preview: (value) => showNumber(value), - Chart: (value) => ( -
{showNumber(value)}
- ), + Chart: (value) => { + const viewerType = useViewerType(); + return ( +
+ {showNumber(value)} +
+ ); + }, }); From bb3318417594b932667d5c178736930ac775e96f Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 13:38:42 -0600 Subject: [PATCH 11/40] fix scrolling --- .../src/components/CodeEditor/useTooltipsExtension.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index 2b4148c9d5..6d069e8063 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -30,7 +30,11 @@ const TooltipBox: FC> = ({ repositionTooltips(view); }); - return
{children}
; + return ( +
+ {children} +
+ ); }; const ValueTooltip: FC<{ value: SqValue; view: EditorView }> = ({ @@ -167,6 +171,9 @@ const tooltipTheme = EditorView.baseTheme({ backgroundColor: "white !important", border: "0 !important", }, + ".cm-tooltip-section": { + height: "100%", // necessary for scrolling, see also: "h-full" in `TooltipBox` + }, }); export function useTooltipsExtension( From 2c5b2e1951fe237b357b426b610c7bf5d01dd51b Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 14:28:10 -0600 Subject: [PATCH 12/40] don't show value tooltips on shadowed variables --- .../CodeEditor/useTooltipsExtension.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index 6d069e8063..a5a28b8875 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -109,15 +109,10 @@ function buildWordHoverExtension({ }; }; - const createTopLevelVariableNameTooltip = (node: SyntaxNode) => { - const name = getText(node); - - const bindings = project.getBindings(sourceId); - if (!bindings.ok) return null; - - const value = bindings.value.get(name); - if (!value) return null; - + const createTopLevelVariableNameTooltip = ( + node: SyntaxNode, + value: SqValue + ) => { return { pos: node.from, end: node.to, @@ -156,8 +151,32 @@ function buildWordHoverExtension({ while (cursor.type.is("Statement") && cursor.parent()); // Is this a top-level variable? - if (cursor.type.is("Program")) { - return createTopLevelVariableNameTooltip(node); + if (!cursor.type.is("Program")) { + return null; + } + + const name = getText(node); + + const bindings = project.getBindings(sourceId); + if (!bindings.ok) return null; + + const value = bindings.value.get(name); + if (!value) return null; + + // Should be LetStatement or DefunStatement + const valueAst = value.context?.valueAst; + + if ( + valueAst && + (valueAst.type === "LetStatement" || + valueAst.type === "DefunStatement") && + // If these don't match then variable was probably shadowed by a later statement and we can't show its value. + // Or it could be caused by code rot, if we change the logic of how `valueAst` is computed, or add another statement type in AST. + // TODO - if we can prove that the variable was shadowed, show the tooltip pointing to the latest assignment. + valueAst.variable.location.start.offset === node.from && + valueAst.variable.location.end.offset === node.to + ) { + return createTopLevelVariableNameTooltip(node, value); } } } From d7288528cf25b26ff3ef90ca221f84786c78152e Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 17:03:44 -0600 Subject: [PATCH 13/40] always start dist y scale from 0 --- .../components/src/widgets/DistWidget/DistributionsChart.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/components/src/widgets/DistWidget/DistributionsChart.tsx b/packages/components/src/widgets/DistWidget/DistributionsChart.tsx index 9578b77b98..a9014ae1ab 100644 --- a/packages/components/src/widgets/DistWidget/DistributionsChart.tsx +++ b/packages/components/src/widgets/DistWidget/DistributionsChart.tsx @@ -159,10 +159,7 @@ const InnerDistributionsChart: FC<{ ]); const yScale = sqScaleToD3(plot.yScale); - yScale.domain([ - Math.max(Math.min(...domain.map((p) => p.y)), 0), // min value, but at least 0 - Math.max(...domain.map((p) => p.y)), - ]); + yScale.domain([0, Math.max(...domain.map((p) => p.y))]); return { xScale, yScale }; }, [domain, plot.xScale, plot.yScale]); From 238d13cc32058dc96f59bbe1a5b8527f422b1c5a Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 18:28:28 -0600 Subject: [PATCH 14/40] extract ViewerBody and ViewerMenu components --- .../SquiggleOutputViewer/ViewerBody.tsx | 61 ++++++ .../SquiggleOutputViewer/ViewerMenu.tsx | 132 +++++++++++++ .../components/SquiggleOutputViewer/index.tsx | 184 +++--------------- 3 files changed, 216 insertions(+), 161 deletions(-) create mode 100644 packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx create mode 100644 packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx diff --git a/packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx b/packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx new file mode 100644 index 0000000000..7d6a5c2642 --- /dev/null +++ b/packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx @@ -0,0 +1,61 @@ +import { FC } from "react"; + +import { SqValue } from "@quri/squiggle-lang"; + +import { SquiggleOutput } from "../../lib/hooks/useSquiggle.js"; +import { ErrorBoundary } from "../ErrorBoundary.js"; +import { SquiggleErrorAlert } from "../SquiggleErrorAlert.js"; +import { SquiggleViewer } from "../SquiggleViewer/index.js"; +import { ViewerMode } from "./index.js"; + +type Props = { + mode: ViewerMode; + squiggleOutput: SquiggleOutput; + isRunning: boolean; +}; + +export const ViewerBody: FC = ({ squiggleOutput, mode, isRunning }) => { + const { output, code } = squiggleOutput; + + if (!code) { + return null; + } + + if (!output.ok) { + return ; + } + + const sqOutput = output.value; + let usedValue: SqValue | undefined; + switch (mode) { + case "Result": + usedValue = + sqOutput.result.tag === "Void" ? undefined : output.value.result; + break; + case "Variables": + usedValue = sqOutput.bindings.asValue(); + break; + case "Imports": + usedValue = sqOutput.imports.asValue(); + break; + case "Exports": + usedValue = sqOutput.exports.asValue(); + } + + if (!usedValue) { + return null; + } + + return ( +
+ {isRunning && ( + // `opacity-0 squiggle-semi-appear` would be better, but won't work reliably until we move Squiggle evaluation to Web Workers +
+ )} + + {/* we don't pass settings or editor here because they're already configured in ``; hopefully `` itself won't need to rely on settings, otherwise things might break */} + + +
+ ); +}; diff --git a/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx b/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx new file mode 100644 index 0000000000..20398f343d --- /dev/null +++ b/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx @@ -0,0 +1,132 @@ +import clsx from "clsx"; +import { FC } from "react"; + +import { + Button, + CodeBracketIcon, + Dropdown, + DropdownMenu, + DropdownMenuActionItem, + TriangleIcon, +} from "@quri/ui"; + +import { SquiggleOutput } from "../../lib/hooks/useSquiggle.js"; +import { + getResultExports, + getResultImports, + getResultValue, + getResultVariables, +} from "../../lib/utility.js"; +import { ViewerMode } from "./index.js"; + +const MenuItemTitle: FC<{ title: string; type: string | null }> = ({ + title, + type, +}) => { + const isEmpty = type === null; + return ( +
+ {title} + {isEmpty ? ( + Empty + ) : ( + {type} + )} +
+ ); +}; + +type Props = { + mode: ViewerMode; + setMode: (mode: ViewerMode) => void; + output: SquiggleOutput; +}; + +export const ViewerMenu: FC = ({ mode, setMode, output }) => { + const resultItem = getResultValue(output); + const resultExports = getResultExports(output); + const resultVariables = getResultVariables(output); + const resultImports = getResultImports(output); + + const hasResult = Boolean(resultItem?.ok); + const variablesCount = resultVariables?.ok + ? resultVariables.value.value.entries().length + : 0; + const importsCount = resultImports?.ok + ? resultImports.value.value.entries().length + : 0; + const exportsCount = resultExports?.ok + ? resultExports.value.value.entries().length + : 0; + + return ( + ( + + {Boolean(importsCount) && ( + + } + onClick={() => { + setMode("Imports"); + close(); + }} + /> + )} + {Boolean(variablesCount) && ( + + } + onClick={() => { + setMode("Variables"); + close(); + }} + /> + )} + {Boolean(exportsCount) && ( + + } + onClick={() => { + setMode("Exports"); + close(); + }} + /> + )} + + } + onClick={() => { + setMode("Result"); + close(); + }} + /> + + )} + > + + + ); +}; diff --git a/packages/components/src/components/SquiggleOutputViewer/index.tsx b/packages/components/src/components/SquiggleOutputViewer/index.tsx index f000566f78..d26034ee21 100644 --- a/packages/components/src/components/SquiggleOutputViewer/index.tsx +++ b/packages/components/src/components/SquiggleOutputViewer/index.tsx @@ -1,49 +1,15 @@ -import clsx from "clsx"; -import { FC, forwardRef, useState } from "react"; +import { forwardRef, useState } from "react"; -import { result, SqError, SqValue } from "@quri/squiggle-lang"; -import { - Button, - CodeBracketIcon, - Dropdown, - DropdownMenu, - DropdownMenuActionItem, - TriangleIcon, -} from "@quri/ui"; - -import { SquiggleViewer } from "../../index.js"; import { SquiggleOutput } from "../../lib/hooks/useSquiggle.js"; -import { - getResultExports, - getResultImports, - getResultValue, - getResultVariables, -} from "../../lib/utility.js"; +import { getResultExports, getResultValue } from "../../lib/utility.js"; import { CodeEditorHandle } from "../CodeEditor/index.js"; -import { ErrorBoundary } from "../ErrorBoundary.js"; import { PartialPlaygroundSettings } from "../PlaygroundSettings.js"; -import { SquiggleErrorAlert } from "../SquiggleErrorAlert.js"; import { SquiggleViewerHandle } from "../SquiggleViewer/index.js"; import { ViewerProvider } from "../SquiggleViewer/ViewerProvider.js"; import { Layout } from "./Layout.js"; import { RenderingIndicator } from "./RenderingIndicator.js"; - -const MenuItemTitle: FC<{ title: string; type: string | null }> = ({ - title, - type, -}) => { - const isEmpty = type === null; - return ( -
- {title} - {isEmpty ? ( - Empty - ) : ( - {type} - )} -
- ); -}; +import { ViewerBody } from "./ViewerBody.js"; +import { ViewerMenu } from "./ViewerMenu.js"; type Props = { squiggleOutput: SquiggleOutput; @@ -51,150 +17,46 @@ type Props = { editor?: CodeEditorHandle; } & PartialPlaygroundSettings; +export type ViewerMode = "Imports" | "Exports" | "Variables" | "Result"; + /* Wrapper for SquiggleViewer that shows the rendering stats and isRunning state. */ export const SquiggleOutputViewer = forwardRef( ({ squiggleOutput, isRunning, editor, ...settings }, viewerRef) => { const resultItem = getResultValue(squiggleOutput); - const resultVariables = getResultVariables(squiggleOutput); - const resultImports = getResultImports(squiggleOutput); const resultExports = getResultExports(squiggleOutput); const hasResult = Boolean(resultItem?.ok); - const variablesCount = resultVariables?.ok - ? resultVariables.value.value.entries().length - : 0; - const importsCount = resultImports?.ok - ? resultImports.value.value.entries().length - : 0; const exportsCount = resultExports?.ok ? resultExports.value.value.entries().length : 0; - const [mode, setMode] = useState< - "Imports" | "Exports" | "Variables" | "Result" - >(hasResult ? "Result" : exportsCount > 0 ? "Exports" : "Variables"); - - let squiggleViewer: JSX.Element | null = null; - if (squiggleOutput.code) { - let usedResult: result | undefined; - switch (mode) { - case "Result": - usedResult = resultItem; - break; - case "Variables": - usedResult = resultVariables; - break; - case "Imports": - usedResult = resultImports; - break; - case "Exports": - usedResult = resultExports; - } - - if (usedResult) { - squiggleViewer = usedResult.ok ? ( -
- {isRunning && ( - // `opacity-0 squiggle-semi-appear` would be better, but won't work reliably until we move Squiggle evaluation to Web Workers -
- )} - - {/* we don't pass settings or editor here because they're already configured in ``; hopefully `` itself won't need to rely on settings, otherwise things might break */} - - -
- ) : ( - - ); - } - } + const [mode, setMode] = useState( + hasResult ? "Result" : exportsCount > 0 ? "Exports" : "Variables" + ); return ( - + ( - - {Boolean(importsCount) && ( - - } - onClick={() => { - setMode("Imports"); - close(); - }} - /> - )} - {Boolean(variablesCount) && ( - - } - onClick={() => { - setMode("Variables"); - close(); - }} - /> - )} - {Boolean(exportsCount) && ( - - } - onClick={() => { - setMode("Exports"); - close(); - }} - /> - )} - - } - onClick={() => { - setMode("Result"); - close(); - }} - /> - - )} - > - - + } indicator={ } - viewer={squiggleViewer} + viewer={ + + } /> ); } ); -SquiggleOutputViewer.displayName = "DynamicSquiggleViewer"; +SquiggleOutputViewer.displayName = "SquiggleOutputViewer"; From d9200516b1a3f4efb052aadf123fe5ad763c6f54 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 18:58:32 -0600 Subject: [PATCH 15/40] cleanups; remove some utility functions --- .../src/components/SquiggleChart.tsx | 23 +++++------- .../SquiggleOutputViewer/ViewerBody.tsx | 3 +- .../SquiggleOutputViewer/ViewerMenu.tsx | 31 +++++----------- .../components/SquiggleOutputViewer/index.tsx | 33 +++++++++++------ packages/components/src/lib/utility.ts | 37 +------------------ .../src/public/SqValue/SqDict.ts | 8 ++++ packages/squiggle-lang/src/value/VDict.ts | 8 ++++ 7 files changed, 58 insertions(+), 85 deletions(-) diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 52bfdb99f6..bc946f34e9 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -10,7 +10,6 @@ import { StandaloneExecutionProps, useSquiggle, } from "../lib/hooks/useSquiggle.js"; -import { getResultValue, getResultVariables } from "../lib/utility.js"; import { MessageAlert } from "./Alert.js"; import { PartialPlaygroundSettings } from "./PlaygroundSettings.js"; import { SquiggleErrorAlert } from "./SquiggleErrorAlert.js"; @@ -52,23 +51,21 @@ export const SquiggleChart: FC = memo( } if (rootPathOverride) { + const { output } = squiggleOutput; + if (!output.ok) { + return ; + } const rootValue = rootPathOverride.root === "result" - ? getResultValue(squiggleOutput) - : getResultVariables(squiggleOutput); - if (!rootValue) { - return ; - } - if (!rootValue.ok) { - return ; - } + ? output.value.result + : output.value.bindings.asValue(); - const value = getSubvalueByPath(rootValue.value, rootPathOverride); - if (!value) { + const value = getSubvalueByPath(rootValue, rootPathOverride); + if (value) { + return ; + } else { return ; } - - return ; } else { return ( = ({ squiggleOutput, mode, isRunning }) => { let usedValue: SqValue | undefined; switch (mode) { case "Result": - usedValue = - sqOutput.result.tag === "Void" ? undefined : output.value.result; + usedValue = output.value.result; break; case "Variables": usedValue = sqOutput.bindings.asValue(); diff --git a/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx b/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx index 20398f343d..f97499df45 100644 --- a/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx +++ b/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx @@ -11,12 +11,6 @@ import { } from "@quri/ui"; import { SquiggleOutput } from "../../lib/hooks/useSquiggle.js"; -import { - getResultExports, - getResultImports, - getResultValue, - getResultVariables, -} from "../../lib/utility.js"; import { ViewerMode } from "./index.js"; const MenuItemTitle: FC<{ title: string; type: string | null }> = ({ @@ -42,22 +36,15 @@ type Props = { output: SquiggleOutput; }; -export const ViewerMenu: FC = ({ mode, setMode, output }) => { - const resultItem = getResultValue(output); - const resultExports = getResultExports(output); - const resultVariables = getResultVariables(output); - const resultImports = getResultImports(output); - - const hasResult = Boolean(resultItem?.ok); - const variablesCount = resultVariables?.ok - ? resultVariables.value.value.entries().length - : 0; - const importsCount = resultImports?.ok - ? resultImports.value.value.entries().length - : 0; - const exportsCount = resultExports?.ok - ? resultExports.value.value.entries().length - : 0; +export const ViewerMenu: FC = ({ + mode, + setMode, + output: { output }, +}) => { + const hasResult = output.ok && output.value.result.tag !== "Void"; + const variablesCount = output.ok ? output.value.bindings.size() : 0; + const importsCount = output.ok ? output.value.imports.size() : 0; + const exportsCount = output.ok ? output.value.exports.size() : 0; return ( (() => { + // Pick the initial mode value + + if (!outputResult.ok) { + return "Variables"; + } + + const output = outputResult.value; + if (output.result.tag !== "Void") { + return "Result"; + } + if (!output.exports.isEmpty()) { + return "Exports"; + } + return "Variables"; + }); +} + /* Wrapper for SquiggleViewer that shows the rendering stats and isRunning state. */ export const SquiggleOutputViewer = forwardRef( ({ squiggleOutput, isRunning, editor, ...settings }, viewerRef) => { - const resultItem = getResultValue(squiggleOutput); - const resultExports = getResultExports(squiggleOutput); - - const hasResult = Boolean(resultItem?.ok); - const exportsCount = resultExports?.ok - ? resultExports.value.value.entries().length - : 0; - - const [mode, setMode] = useState( - hasResult ? "Result" : exportsCount > 0 ? "Exports" : "Variables" - ); + const [mode, setMode] = useMode(squiggleOutput.output); return ( x || y, false); } -export function getResultVariables({ - output, -}: SquiggleOutput): result { - return resultMap(output, (value) => value.bindings.asValue()); -} - -export function getResultImports({ - output, -}: SquiggleOutput): result { - return resultMap(output, (value) => value.imports.asValue()); -} - -export function getResultExports({ - output, -}: SquiggleOutput): result { - return resultMap(output, (value) => value.exports.asValue()); -} - -export function getResultValue({ - output, -}: SquiggleOutput): result | undefined { - if (output.ok) { - const isResult = output.value.result.tag !== "Void"; - return isResult ? { ok: true, value: output.value.result } : undefined; - } else { - return output; - } -} - export function getErrors(result: SquiggleOutput["output"]) { if (!result.ok) { return [result.value]; diff --git a/packages/squiggle-lang/src/public/SqValue/SqDict.ts b/packages/squiggle-lang/src/public/SqValue/SqDict.ts index 9325f1c61c..f5e8d0313b 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqDict.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqDict.ts @@ -37,4 +37,12 @@ export class SqDict { asValue() { return new SqDictValue(this._value, this.context); } + + isEmpty() { + return this._value.isEmpty(); + } + + size() { + return this._value.size(); + } } diff --git a/packages/squiggle-lang/src/value/VDict.ts b/packages/squiggle-lang/src/value/VDict.ts index a4c7c8b067..0d9e5a2970 100644 --- a/packages/squiggle-lang/src/value/VDict.ts +++ b/packages/squiggle-lang/src/value/VDict.ts @@ -68,6 +68,14 @@ export class VDict extends BaseValue implements Indexable { return true; } + + isEmpty(): boolean { + return this.value.isEmpty(); + } + + size(): number { + return this.value.size; + } } export const vDict = (v: ValueMap) => new VDict(v); From 55d4a46330e3c143cf26a444873b738111939e28 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 19:11:56 -0600 Subject: [PATCH 16/40] minor cleanup --- packages/squiggle-lang/src/public/SqProject/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/squiggle-lang/src/public/SqProject/index.ts b/packages/squiggle-lang/src/public/SqProject/index.ts index a06508134a..3c954ef8a3 100644 --- a/packages/squiggle-lang/src/public/SqProject/index.ts +++ b/packages/squiggle-lang/src/public/SqProject/index.ts @@ -237,12 +237,10 @@ export class SqProject { const hasEndExpression = !!lastStatement && !isBindingStatement(lastStatement); - const _this = this; - - function newContext(root: Root) { + const newContext = (root: Root) => { const isResult = root === "result"; return new SqValueContext({ - project: _this, + project: this, sourceId, source: source!, ast, @@ -253,11 +251,11 @@ export class SqProject { items: [], }), }); - } + }; - function wrapSqDict(innerDict: VDict, root: Root): SqDict { + const wrapSqDict = (innerDict: VDict, root: Root): SqDict => { return new SqDict(innerDict, newContext(root)); - } + }; const { result, bindings, exports, externals } = internalOutputR.value; From 9738be54651687403f26442a0e2c90c43d92a23e Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 19:33:00 -0600 Subject: [PATCH 17/40] empty programs with possible comments are valid --- packages/components/src/components/SquiggleEditor.tsx | 2 +- .../src/components/SquiggleOutputViewer/ViewerBody.tsx | 6 +----- packages/components/src/lib/hooks/useSquiggle.ts | 2 +- packages/squiggle-lang/__tests__/ast/parse_test.ts | 5 +++++ packages/squiggle-lang/src/ast/peggyParser.peggy | 2 ++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index cb975405c7..825d38cf43 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -64,7 +64,7 @@ export const SquiggleEditor: FC = ({ onSubmit={() => runnerState.run()} />
- {hideViewer || !squiggleOutput?.code ? null : ( + {hideViewer || !squiggleOutput ? null : ( = ({ squiggleOutput, mode, isRunning }) => { - const { output, code } = squiggleOutput; - - if (!code) { - return null; - } + const { output } = squiggleOutput; if (!output.ok) { return ; diff --git a/packages/components/src/lib/hooks/useSquiggle.ts b/packages/components/src/lib/hooks/useSquiggle.ts index c3558e3922..14b14d95cf 100644 --- a/packages/components/src/lib/hooks/useSquiggle.ts +++ b/packages/components/src/lib/hooks/useSquiggle.ts @@ -53,7 +53,7 @@ export type UseSquiggleOutput = [ { project: SqProject; isRunning: boolean; - sourceId: string; + sourceId: string; // if you don't provide `sourceId` in `SquiggleArgs` then it will be picked randomly }, ]; diff --git a/packages/squiggle-lang/__tests__/ast/parse_test.ts b/packages/squiggle-lang/__tests__/ast/parse_test.ts index 453ea28201..0198dadc0c 100644 --- a/packages/squiggle-lang/__tests__/ast/parse_test.ts +++ b/packages/squiggle-lang/__tests__/ast/parse_test.ts @@ -6,6 +6,11 @@ import { } from "../helpers/reducerHelpers.js"; describe("Peggy parse", () => { + describe("empty", () => { + testParse("", "(Program)"); + testParse("// comment", "(Program)"); + }); + describe("float", () => { test.each([ ["1.", { integer: 1, fractional: null, exponent: null }], diff --git a/packages/squiggle-lang/src/ast/peggyParser.peggy b/packages/squiggle-lang/src/ast/peggyParser.peggy index 8eb36af8b3..a1c616f08e 100644 --- a/packages/squiggle-lang/src/ast/peggyParser.peggy +++ b/packages/squiggle-lang/src/ast/peggyParser.peggy @@ -23,6 +23,8 @@ outerBlock / imports:importStatementsList finalExpression:expression { return h.nodeProgram(imports, [finalExpression], location()); } + / imports:importStatementsList + { return h.nodeProgram(imports, [], location()); } importStatementsList = (@importStatement __nl)* From 86b4c129764a322b7544ef18493bf59d095ff5cb Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 19:38:10 -0600 Subject: [PATCH 18/40] cleanup some types --- .../SquiggleOutputViewer/ViewerBody.tsx | 8 +++---- .../SquiggleOutputViewer/ViewerMenu.tsx | 10 +++------ .../components/SquiggleOutputViewer/index.tsx | 13 ++++-------- .../components/src/lib/hooks/useSquiggle.ts | 21 ++++--------------- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx b/packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx index 728349d5db..2e57555fa0 100644 --- a/packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx +++ b/packages/components/src/components/SquiggleOutputViewer/ViewerBody.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { SqValue } from "@quri/squiggle-lang"; -import { SquiggleOutput } from "../../lib/hooks/useSquiggle.js"; +import { SqOutputResult } from "../../../../squiggle-lang/src/public/types.js"; import { ErrorBoundary } from "../ErrorBoundary.js"; import { SquiggleErrorAlert } from "../SquiggleErrorAlert.js"; import { SquiggleViewer } from "../SquiggleViewer/index.js"; @@ -10,13 +10,11 @@ import { ViewerMode } from "./index.js"; type Props = { mode: ViewerMode; - squiggleOutput: SquiggleOutput; + output: SqOutputResult; isRunning: boolean; }; -export const ViewerBody: FC = ({ squiggleOutput, mode, isRunning }) => { - const { output } = squiggleOutput; - +export const ViewerBody: FC = ({ output, mode, isRunning }) => { if (!output.ok) { return ; } diff --git a/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx b/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx index f97499df45..7ab57fc4c6 100644 --- a/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx +++ b/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx @@ -10,7 +10,7 @@ import { TriangleIcon, } from "@quri/ui"; -import { SquiggleOutput } from "../../lib/hooks/useSquiggle.js"; +import { SqOutputResult } from "../../../../squiggle-lang/src/public/types.js"; import { ViewerMode } from "./index.js"; const MenuItemTitle: FC<{ title: string; type: string | null }> = ({ @@ -33,14 +33,10 @@ const MenuItemTitle: FC<{ title: string; type: string | null }> = ({ type Props = { mode: ViewerMode; setMode: (mode: ViewerMode) => void; - output: SquiggleOutput; + output: SqOutputResult; }; -export const ViewerMenu: FC = ({ - mode, - setMode, - output: { output }, -}) => { +export const ViewerMenu: FC = ({ mode, setMode, output }) => { const hasResult = output.ok && output.value.result.tag !== "Void"; const variablesCount = output.ok ? output.value.bindings.size() : 0; const importsCount = output.ok ? output.value.imports.size() : 0; diff --git a/packages/components/src/components/SquiggleOutputViewer/index.tsx b/packages/components/src/components/SquiggleOutputViewer/index.tsx index 1ba6be7a08..79b2108fe6 100644 --- a/packages/components/src/components/SquiggleOutputViewer/index.tsx +++ b/packages/components/src/components/SquiggleOutputViewer/index.tsx @@ -41,7 +41,8 @@ function useMode(outputResult: SqOutputResult) { /* Wrapper for SquiggleViewer that shows the rendering stats and isRunning state. */ export const SquiggleOutputViewer = forwardRef( ({ squiggleOutput, isRunning, editor, ...settings }, viewerRef) => { - const [mode, setMode] = useMode(squiggleOutput.output); + const { output } = squiggleOutput; + const [mode, setMode] = useMode(output); return ( ( ref={viewerRef} > - } + menu={} indicator={ } viewer={ - + } /> diff --git a/packages/components/src/lib/hooks/useSquiggle.ts b/packages/components/src/lib/hooks/useSquiggle.ts index 14b14d95cf..2b97226d34 100644 --- a/packages/components/src/lib/hooks/useSquiggle.ts +++ b/packages/components/src/lib/hooks/useSquiggle.ts @@ -1,14 +1,8 @@ import { useEffect, useMemo, useState } from "react"; -import { - Env, - result, - SqDict, - SqError, - SqProject, - SqValue, -} from "@quri/squiggle-lang"; +import { Env, SqProject } from "@quri/squiggle-lang"; +import { SqOutputResult } from "../../../../squiggle-lang/src/public/types.js"; import { WINDOW_VARIABLE_NAME } from "../constants.js"; // Props needed for a standalone execution. @@ -33,16 +27,9 @@ export type SquiggleArgs = { executionId?: number; } & (StandaloneExecutionProps | ProjectExecutionProps); +// TODO - think of a better name, `SquiggleOutput` is too similar to `SqOutput` export type SquiggleOutput = { - output: result< - { - exports: SqDict; - result: SqValue; - imports: SqDict; - bindings: SqDict; - }, - SqError - >; + output: SqOutputResult; code: string; executionId: number; executionTime: number; From d801ff1411c63fb2568a79bc3a31d37d8da61ed9 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 18 Jan 2024 19:46:38 -0600 Subject: [PATCH 19/40] VoidWidget for empty result values --- packages/components/src/widgets/VoidWidget.tsx | 6 ++++++ packages/components/src/widgets/index.ts | 1 + 2 files changed, 7 insertions(+) create mode 100644 packages/components/src/widgets/VoidWidget.tsx diff --git a/packages/components/src/widgets/VoidWidget.tsx b/packages/components/src/widgets/VoidWidget.tsx new file mode 100644 index 0000000000..cbdcc090c8 --- /dev/null +++ b/packages/components/src/widgets/VoidWidget.tsx @@ -0,0 +1,6 @@ +import { widgetRegistry } from "./registry.js"; + +widgetRegistry.register("Void", { + Preview: () => "Empty value", + Chart: () =>
Empty value
, +}); diff --git a/packages/components/src/widgets/index.ts b/packages/components/src/widgets/index.ts index b28e4c4a01..1b3a809202 100644 --- a/packages/components/src/widgets/index.ts +++ b/packages/components/src/widgets/index.ts @@ -10,3 +10,4 @@ import "./PlotWidget/index.js"; import "./StringWidget.js"; import "./TableChartWidget.js"; import "./DurationWidget.js"; +import "./VoidWidget.js"; From 4c3eaeeaeb6a121af67f4bc9a679d5b4748f54a9 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 11:03:38 -0800 Subject: [PATCH 20/40] Refactoring sqPath --- .../src/components/SquiggleViewer/index.tsx | 6 +- .../src/components/SquiggleViewer/utils.ts | 81 ++++++------- packages/squiggle-lang/src/index.ts | 6 +- .../src/public/SqValue/SqArray.ts | 3 +- .../src/public/SqValue/SqCalculator.ts | 4 +- .../src/public/SqValue/SqDict.ts | 12 +- .../src/public/SqValue/SqPlot.ts | 9 +- .../src/public/SqValue/SqTableChart.ts | 6 +- .../src/public/SqValueContext.ts | 19 ++-- .../squiggle-lang/src/public/SqValuePath.ts | 107 ++++++++++++++++-- 10 files changed, 167 insertions(+), 86 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/index.tsx b/packages/components/src/components/SquiggleViewer/index.tsx index 706121d1a7..57b910a6b2 100644 --- a/packages/components/src/components/SquiggleViewer/index.tsx +++ b/packages/components/src/components/SquiggleViewer/index.tsx @@ -6,7 +6,7 @@ import { ChevronRightIcon } from "@quri/ui"; import { MessageAlert } from "../Alert.js"; import { CodeEditorHandle } from "../CodeEditor/index.js"; import { PartialPlaygroundSettings } from "../PlaygroundSettings.js"; -import { pathIsEqual, pathItemFormat, useGetSubvalueByPath } from "./utils.js"; +import { useGetSubvalueByPath } from "./utils.js"; import { ValueViewer } from "./ValueViewer.js"; import { SquiggleViewerHandle, @@ -38,7 +38,7 @@ const FocusedNavigation: FC<{ const unfocus = useUnfocus(); const focus = useFocus(); - const isFocusedOnRootPath = rootPath && pathIsEqual(focusedPath, rootPath); + const isFocusedOnRootPath = rootPath && rootPath.isEqual(focusedPath); if (isFocusedOnRootPath) { return null; @@ -62,7 +62,7 @@ const FocusedNavigation: FC<{ focus(path)} - text={pathItemFormat(path.items[i + rootPathFocusedAdjustment])} + text={path.items[i + rootPathFocusedAdjustment].toString()} /> ))}
diff --git a/packages/components/src/components/SquiggleViewer/utils.ts b/packages/components/src/components/SquiggleViewer/utils.ts index 4a4831c6f1..3bbdf7962b 100644 --- a/packages/components/src/components/SquiggleViewer/utils.ts +++ b/packages/components/src/components/SquiggleViewer/utils.ts @@ -1,25 +1,11 @@ import isEqual from "lodash/isEqual.js"; -import { PathItem, SqDict, SqValue, SqValuePath } from "@quri/squiggle-lang"; +import { SqDict, SqValue, SqValuePath } from "@quri/squiggle-lang"; import { SHORT_STRING_LENGTH } from "../../lib/constants.js"; import { SqValueWithContext } from "../../lib/utility.js"; import { useViewerContext } from "./ViewerProvider.js"; -export const pathItemFormat = (item: PathItem): string => { - if (item.type === "cellAddress") { - return `Cell (${item.value.row},${item.value.column})`; - } else if (item.type === "calculator") { - return `calculator`; - } else { - return String(item.value); - } -}; - -function isTopLevel(path: SqValuePath): boolean { - return path.items.length === 0; -} - function topLevelName(path: SqValuePath): string { return { result: "Result", @@ -30,23 +16,15 @@ function topLevelName(path: SqValuePath): string { } export function pathAsString(path: SqValuePath) { - if (isTopLevel(path)) { - return topLevelName(path); - } else { - return [topLevelName(path), ...path.items.map(pathItemFormat)].join("."); - } -} - -export function pathIsEqual(path1: SqValuePath, path2: SqValuePath) { - return pathAsString(path1) === pathAsString(path2); + return [topLevelName(path), ...path.items.map((p) => p.toString())].join("."); } export function pathToShortName(path: SqValuePath): string { - if (isTopLevel(path)) { + if (path.isRoot()) { return topLevelName(path); } else { const lastPathItem = path.items[path.items.length - 1]; - return pathItemFormat(lastPathItem); + return lastPathItem.toString(); } } @@ -66,43 +44,50 @@ export function getChildrenValues(value: SqValue): SqValue[] { export function useGetSubvalueByPath() { const { itemStore } = useViewerContext(); - return (value: SqValue, path: SqValuePath): SqValue | undefined => { - const { context } = value; + return ( + topValue: SqValue, + pathToSubvalue: SqValuePath + ): SqValue | undefined => { + const { context } = topValue; if (!context) { return; } - if (context.path.root !== path.root) { - return; - } - if (context.path.items.length > path.items.length) { + + if (!pathToSubvalue.contains(context.path)) { return; } - for (let i = 0; i < path.items.length; i++) { + let currentValue = topValue; + for (let i = 0; i < pathToSubvalue.items.length; i++) { if (i < context.path.items.length) { // check that `path` is a subpath of `context.path` - if (!isEqual(context.path.items[i], path.items[i])) { + if (!isEqual(context.path.items[i], pathToSubvalue.items[i])) { return; } continue; } - const pathItem = path.items[i]; + const pathItem = pathToSubvalue.items[i]; + { let nextValue: SqValue | undefined; - if (pathItem.type === "number" && value.tag === "Array") { - nextValue = value.value.getValues()[pathItem.value]; - } else if (pathItem.type === "string" && value.tag === "Dict") { - nextValue = value.value.get(pathItem.value); + + if (currentValue.tag === "Array" && pathItem.value.type === "number") { + nextValue = currentValue.value.getValues()[pathItem.value.value]; + } else if ( + currentValue.tag === "Dict" && + pathItem.value.type === "string" + ) { + nextValue = currentValue.value.get(pathItem.value.value); } else if ( - pathItem.type === "cellAddress" && - value.tag === "TableChart" + currentValue.tag === "TableChart" && + pathItem.value.type === "cellAddress" ) { // Maybe it would be better to get the environment in a different way. const environment = context.project.getEnvironment(); - const item = value.value.item( - pathItem.value.row, - pathItem.value.column, + const item = currentValue.value.item( + pathItem.value.value.row, + pathItem.value.value.column, environment ); if (item.ok) { @@ -114,8 +99,8 @@ export function useGetSubvalueByPath() { // The previous path item is the one that is the parent of the calculator result. // This is the one that we use in the ViewerContext to store information about the calculator. const calculatorPath = new SqValuePath({ - root: path.root, - items: path.items.slice(0, i), + root: pathToSubvalue.root, + items: pathToSubvalue.items.slice(0, i), }); const calculatorState = itemStore.getCalculator(calculatorPath); const result = calculatorState?.calculatorResult; @@ -128,10 +113,10 @@ export function useGetSubvalueByPath() { if (!nextValue) { return; } - value = nextValue; + currentValue = nextValue; } } - return value; + return currentValue; }; } diff --git a/packages/squiggle-lang/src/index.ts b/packages/squiggle-lang/src/index.ts index 64e4bdb7df..b7d69300dc 100644 --- a/packages/squiggle-lang/src/index.ts +++ b/packages/squiggle-lang/src/index.ts @@ -60,7 +60,11 @@ export { SqTableChart } from "./public/SqValue/SqTableChart.js"; export { SqCalculator } from "./public/SqValue/SqCalculator.js"; export { SqDict } from "./public/SqValue/SqDict.js"; export { SqScale } from "./public/SqValue/SqScale.js"; -export { type PathItem, SqValuePath } from "./public/SqValuePath.js"; +export { + type PathItem, + SqPathItem, + SqValuePath, +} from "./public/SqValuePath.js"; export { parse } from "./public/parse.js"; export { fmap as resultMap, type result } from "./utility/result.js"; diff --git a/packages/squiggle-lang/src/public/SqValue/SqArray.ts b/packages/squiggle-lang/src/public/SqValue/SqArray.ts index 6aceebb74d..f8252b1f5c 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqArray.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqArray.ts @@ -1,5 +1,6 @@ import { Value } from "../../value/index.js"; import { SqValueContext } from "../SqValueContext.js"; +import { SqPathItem } from "../SqValuePath.js"; import { wrapValue } from "./index.js"; export class SqArray { @@ -10,7 +11,7 @@ export class SqArray { getValues() { return this._value.map((v, i) => - wrapValue(v, this.context?.extend({ type: "number", value: i })) + wrapValue(v, this.context?.extend(SqPathItem.fromNumber(i))) ); } } diff --git a/packages/squiggle-lang/src/public/SqValue/SqCalculator.ts b/packages/squiggle-lang/src/public/SqValue/SqCalculator.ts index dea19bd09e..e6504341de 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqCalculator.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqCalculator.ts @@ -3,6 +3,7 @@ import * as Result from "../../utility/result.js"; import { Calculator } from "../../value/VCalculator.js"; import { SqError, SqOtherError } from "../SqError.js"; import { SqValueContext } from "../SqValueContext.js"; +import { SqPathItem } from "../SqValuePath.js"; import { SqValue, wrapValue } from "./index.js"; import { SqInput, wrapInput } from "./SqInput.js"; import { SqLambda } from "./SqLambda.js"; @@ -17,8 +18,7 @@ export class SqCalculator { const sqLambda = new SqLambda(this._value.fn, undefined); const response = sqLambda.call(_arguments, env); - const newContext = - this.context && this.context.extend({ type: "calculator" }); + const newContext = this.context?.extend(SqPathItem.fromCalculator()); if (!newContext) { return Result.Err( diff --git a/packages/squiggle-lang/src/public/SqValue/SqDict.ts b/packages/squiggle-lang/src/public/SqValue/SqDict.ts index 9325f1c61c..3d5749b8b2 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqDict.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqDict.ts @@ -1,6 +1,7 @@ import { VDict } from "../../value/VDict.js"; import { vString } from "../../value/VString.js"; import { SqValueContext } from "../SqValueContext.js"; +import { SqPathItem } from "../SqValuePath.js"; import { SqDictValue, SqValue, wrapValue } from "./index.js"; export class SqDict { @@ -11,10 +12,10 @@ export class SqDict { entries(): [string, SqValue][] { return [...this._value.value.entries()].map( - ([k, v]) => + ([key, v]) => [ - k, - wrapValue(v, this.context?.extend({ type: "string", value: k })), + key, + wrapValue(v, this.context?.extend(SqPathItem.fromString(key))), ] as const ); } @@ -24,10 +25,7 @@ export class SqDict { if (value === undefined) { return undefined; } - return wrapValue( - value, - this.context?.extend({ type: "string", value: key }) - ); + return wrapValue(value, this.context?.extend(SqPathItem.fromString(key))); } toString() { diff --git a/packages/squiggle-lang/src/public/SqValue/SqPlot.ts b/packages/squiggle-lang/src/public/SqValue/SqPlot.ts index 07db0bdc43..ee76c59421 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqPlot.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqPlot.ts @@ -6,6 +6,7 @@ import * as Result from "../../utility/result.js"; import { Plot, vPlot } from "../../value/VPlot.js"; import { SqError, SqOtherError } from "../SqError.js"; import { SqValueContext } from "../SqValueContext.js"; +import { SqPathItem } from "../SqValuePath.js"; import { SqPlotValue } from "./index.js"; import { SqDistribution, @@ -159,7 +160,7 @@ export class SqNumericFnPlot extends SqAbstractPlot<"numericFn"> { this.context ? this.createdProgrammatically ? this.context - : this.context.extend({ type: "string", value: "fn" }) + : this.context.extend(SqPathItem.fromString("fn")) : undefined ); } @@ -227,7 +228,7 @@ export class SqDistFnPlot extends SqAbstractPlot<"distFn"> { this.context ? this.createdProgrammatically ? this.context - : this.context.extend({ type: "string", value: "fn" }) + : this.context.extend(SqPathItem.fromString("fn")) : undefined ); } @@ -314,9 +315,7 @@ export class SqRelativeValuesPlot extends SqAbstractPlot<"relativeValues"> { get fn(): SqLambda { return new SqLambda( this._value.fn, - this.context - ? this.context.extend({ type: "string", value: "fn" }) - : undefined + this.context?.extend(SqPathItem.fromString("fn")) ); } } diff --git a/packages/squiggle-lang/src/public/SqValue/SqTableChart.ts b/packages/squiggle-lang/src/public/SqValue/SqTableChart.ts index 25d441800c..d5f9787867 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqTableChart.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqTableChart.ts @@ -4,6 +4,7 @@ import * as Result from "../../utility/result.js"; import { TableChart } from "../../value/VTableChart.js"; import { SqError, SqOtherError } from "../SqError.js"; import { SqValueContext } from "../SqValueContext.js"; +import { SqPathItem } from "../SqValuePath.js"; import { SqValue, wrapValue } from "./index.js"; import { SqLambda } from "./SqLambda.js"; @@ -20,8 +21,9 @@ const getItem = ( context?: SqValueContext ): Result.result => { const response = fn.call([element], env); - const newContext: SqValueContext | undefined = - context && context.extend({ type: "cellAddress", value: { row, column } }); + const newContext: SqValueContext | undefined = context?.extend( + SqPathItem.fromCellAddress(row, column) + ); if (response.ok && context) { return Result.Ok(wrapValue(response.value._value, newContext)); diff --git a/packages/squiggle-lang/src/public/SqValueContext.ts b/packages/squiggle-lang/src/public/SqValueContext.ts index aac2a3b206..2d33f1e9a3 100644 --- a/packages/squiggle-lang/src/public/SqValueContext.ts +++ b/packages/squiggle-lang/src/public/SqValueContext.ts @@ -1,7 +1,7 @@ import { AST, ASTNode } from "../ast/parse.js"; import { isBindingStatement } from "../ast/utils.js"; import { SqProject } from "./SqProject/index.js"; -import { PathItem, SqValuePath } from "./SqValuePath.js"; +import { SqPathItem, SqValuePath } from "./SqValuePath.js"; export class SqValueContext { public project: SqProject; @@ -37,12 +37,13 @@ export class SqValueContext { this.path = props.path; } - extend(item: PathItem): SqValueContext { + extend(item: SqPathItem): SqValueContext { let ast = this.valueAst; + const pathItem = item.value; let newAst: ASTNode | undefined; const itemisNotTableIndexOrCalculator = - item.type !== "cellAddress" && item.type !== "calculator"; + pathItem.type !== "cellAddress" && pathItem.type !== "calculator"; if (this.valueAstIsPrecise && itemisNotTableIndexOrCalculator) { // now we can try to look for the next nested valueAst @@ -63,18 +64,20 @@ export class SqValueContext { switch (ast.type) { case "Program": { - if (this.path.root === "bindings") { - newAst = ast.symbols[item.value]; + if (this.path.root === "bindings" && pathItem.type === "string") { + newAst = ast.symbols[pathItem.value]; break; } break; } case "Dict": - newAst = ast.symbols[item.value]; + if (pathItem.type === "string") { + newAst = ast.symbols[pathItem.value]; + } break; case "Array": - if (typeof item === "number") { - const element = ast.elements[item]; + if (pathItem.type === "number") { + const element = ast.elements[pathItem.value]; // Seems like this was broken before. if (element) { newAst = element; } diff --git a/packages/squiggle-lang/src/public/SqValuePath.ts b/packages/squiggle-lang/src/public/SqValuePath.ts index 12ba4738fb..92f99340bd 100644 --- a/packages/squiggle-lang/src/public/SqValuePath.ts +++ b/packages/squiggle-lang/src/public/SqValuePath.ts @@ -9,24 +9,113 @@ export type PathItem = type: "calculator"; }; +function pathItemIsEqual(a: PathItem, b: PathItem): boolean { + if (a.type !== b.type) { + return false; + } + switch (a.type) { + case "string": + return a.value === (b as { type: "string"; value: string }).value; + case "number": + return a.value === (b as { type: "number"; value: number }).value; + case "cellAddress": + return ( + a.value.row === + (b as { type: "cellAddress"; value: { row: number; column: number } }) + .value.row && + a.value.column === + (b as { type: "cellAddress"; value: { row: number; column: number } }) + .value.column + ); + case "calculator": + return true; + } +} + +function pathItemToString(item: PathItem): string { + switch (item.type) { + case "string": + return item.value; + case "number": + return String(item.value); + case "cellAddress": + return `Cell (${item.value.row},${item.value.column})`; + case "calculator": + return "calculator"; + } +} + +export class SqPathItem { + private constructor(public value: PathItem) {} + static fromString(str: string): SqPathItem { + return new SqPathItem({ type: "string", value: str }); + } + static fromNumber(num: number): SqPathItem { + return new SqPathItem({ type: "number", value: num }); + } + static fromCalculator(): SqPathItem { + return new SqPathItem({ type: "calculator" }); + } + static fromCellAddress(row: number, column: number): SqPathItem { + return new SqPathItem({ type: "cellAddress", value: { row, column } }); + } + toString() { + return pathItemToString(this.value); + } + isEqual(other: SqPathItem) { + return pathItemIsEqual(this.value, other.value); + } + get type() { + return this.value.type; + } +} + export type Root = "result" | "bindings" | "imports" | "exports"; export class SqValuePath { public root: Root; - public items: PathItem[]; + public items: SqPathItem[]; - constructor(props: { root: Root; items: PathItem[] }) { + constructor(props: { root: Root; items: SqPathItem[] }) { this.root = props.root; this.items = props.items; } - extend(item: PathItem) { + extend(item: SqPathItem) { return new SqValuePath({ root: this.root, items: [...this.items, item], }); } + contains(other: SqValuePath) { + if (this.items.length > other.items.length) { + return false; + } + for (let i = 0; i < this.items.length; i++) { + if (!this.items[i].isEqual(other.items[i])) { + return false; + } + } + return true; + } + + isEqual(other: SqValuePath) { + if (this.items.length !== other.items.length) { + return false; + } + for (let i = 0; i < this.items.length; i++) { + if (!this.items[i].isEqual(other.items[i])) { + return false; + } + } + return true; + } + + toString() { + return [this.root, ...this.items.map((f) => f.toString())].join("."); + } + static findByOffset({ ast, offset, @@ -34,7 +123,7 @@ export class SqValuePath { ast: ASTNode; offset: number; }): SqValuePath | undefined { - const findLoop = (ast: ASTNode): PathItem[] => { + const findLoop = (ast: ASTNode): SqPathItem[] => { switch (ast.type) { case "Program": { for (const statement of ast.statements) { @@ -64,11 +153,11 @@ export class SqValuePath { pair.key.type === "String" // only string keys are supported ) { return [ - { type: "string", value: pair.key.value }, + SqPathItem.fromString(pair.key.value), ...findLoop(pair.value), ]; } else if (pair.type === "Identifier") { - return [{ type: "string", value: pair.value }]; // this is a final node, no need to findLoop recursively + return [SqPathItem.fromString(pair.value)]; // this is a final node, no need to findLoop recursively } } return []; @@ -77,20 +166,20 @@ export class SqValuePath { for (let i = 0; i < ast.elements.length; i++) { const element = ast.elements[i]; if (locationContains(element.location, offset)) { - return [{ type: "number", value: i }, ...findLoop(element)]; + return [SqPathItem.fromNumber(i), ...findLoop(element)]; } } return []; } case "LetStatement": { return [ - { type: "string", value: ast.variable.value }, + SqPathItem.fromString(ast.variable.value), ...findLoop(ast.value), ]; } case "DefunStatement": { return [ - { type: "string", value: ast.variable.value }, + SqPathItem.fromString(ast.variable.value), ...findLoop(ast.value), ]; } From 57201a010cd669adecef1d8b3a41bf56f37ddb9b Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 13:26:12 -0600 Subject: [PATCH 21/40] typo fix --- .../src/components/CodeEditor/useTooltipsExtension.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index a5a28b8875..0572584d8e 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -47,7 +47,7 @@ const ValueTooltip: FC<{ value: SqValue; view: EditorView }> = ({ return (
- {/* Force a standalone ephermeral ViewerProvider, so that we won't sync up collapsed state with the top-level viewer */} + {/* Force a standalone ephemeral ViewerProvider, so that we won't sync up collapsed state with the top-level viewer */} Date: Fri, 19 Jan 2024 11:48:12 -0800 Subject: [PATCH 22/40] Small fixes for tests --- packages/components/src/stories/SquiggleChart.stories.tsx | 4 ++-- .../components/src/stories/SquiggleChart/Basic.stories.tsx | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/components/src/stories/SquiggleChart.stories.tsx b/packages/components/src/stories/SquiggleChart.stories.tsx index 4628c3493d..a27c046b5a 100644 --- a/packages/components/src/stories/SquiggleChart.stories.tsx +++ b/packages/components/src/stories/SquiggleChart.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { SqValuePath } from "@quri/squiggle-lang"; +import { SqPathItem, SqValuePath } from "@quri/squiggle-lang"; import { SquiggleChart } from "../components/SquiggleChart.js"; @@ -50,7 +50,7 @@ export const RootPathOverride: Story = { code: "{foo: 35 to 50, bar: [1,2,3]}", rootPathOverride: new SqValuePath({ root: "result", - items: [{ type: "string", value: "bar" }], + items: [SqPathItem.fromString("bar")], }), }, }; diff --git a/packages/components/src/stories/SquiggleChart/Basic.stories.tsx b/packages/components/src/stories/SquiggleChart/Basic.stories.tsx index 1c094c2316..5d120d5182 100644 --- a/packages/components/src/stories/SquiggleChart/Basic.stories.tsx +++ b/packages/components/src/stories/SquiggleChart/Basic.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { SqValuePath } from "@quri/squiggle-lang"; +import { SqPathItem, SqValuePath } from "@quri/squiggle-lang"; import { SquiggleChart } from "../../components/SquiggleChart.js"; @@ -43,10 +43,7 @@ export const WithPathOverride: Story = { `, rootPathOverride: new SqValuePath({ root: "bindings", - items: [ - { type: "string", value: "foo" }, - { type: "string", value: "bar" }, - ], + items: [SqPathItem.fromString("foo"), SqPathItem.fromString("bar")], }), }, }; From 9513f3f3f3159a2ab01038944df1e8db9a1b5c4d Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 14:10:35 -0600 Subject: [PATCH 23/40] disallow Foo.bar and Foo as variable names --- packages/squiggle-lang/src/ast/peggyParser.peggy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/squiggle-lang/src/ast/peggyParser.peggy b/packages/squiggle-lang/src/ast/peggyParser.peggy index 8eb36af8b3..1efea7810d 100644 --- a/packages/squiggle-lang/src/ast/peggyParser.peggy +++ b/packages/squiggle-lang/src/ast/peggyParser.peggy @@ -73,11 +73,11 @@ statement / defunStatement letStatement - = exported:("export" __nl)? variable:variable _ assignmentOp _nl value:zeroOMoreArgumentsBlockOrExpression + = exported:("export" __nl)? variable:dollarIdentifier _ assignmentOp _nl value:zeroOMoreArgumentsBlockOrExpression { return h.nodeLetStatement(variable, value, Boolean(exported), location()); } defunStatement - = exported:("export" __nl)? variable:variable '(' _nl args:functionParameters _nl ')' _ assignmentOp _nl body:innerBlockOrExpression + = exported:("export" __nl)? variable:dollarIdentifier '(' _nl args:functionParameters _nl ')' _ assignmentOp _nl body:innerBlockOrExpression { const value = h.nodeLambda(args, body, location(), variable); return h.nodeDefunStatement(variable, value, Boolean(exported), location()); From 24e0d18047dd372ccf5169470af58c50b049fc97 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 14:14:51 -0600 Subject: [PATCH 24/40] tests --- packages/squiggle-lang/__tests__/ast/parse_test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/squiggle-lang/__tests__/ast/parse_test.ts b/packages/squiggle-lang/__tests__/ast/parse_test.ts index 453ea28201..8cd3bf8f26 100644 --- a/packages/squiggle-lang/__tests__/ast/parse_test.ts +++ b/packages/squiggle-lang/__tests__/ast/parse_test.ts @@ -150,6 +150,8 @@ describe("Peggy parse", () => { testParse("x = 1", "(Program (LetStatement :x (Block 1)))"); testParse("x", "(Program :x)"); testParse("x = 1; x", "(Program (LetStatement :x (Block 1)) :x)"); + testEvalError("X = 1"); + testEvalError("Foo.bar = 1"); }); describe("functions", () => { From c413eb8fce2459f3607f3508dd0adec735f502bb Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 23:19:11 +0300 Subject: [PATCH 25/40] Create stupid-bananas-look.md --- .changeset/stupid-bananas-look.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stupid-bananas-look.md diff --git a/.changeset/stupid-bananas-look.md b/.changeset/stupid-bananas-look.md new file mode 100644 index 0000000000..0e7bdf4482 --- /dev/null +++ b/.changeset/stupid-bananas-look.md @@ -0,0 +1,5 @@ +--- +"@quri/squiggle-lang": patch +--- + +Disallow capitalized variable names that we allowed by accident in 0.9.0 From 28631209283e65cd791e1a0d1e1afbe7692f1ad2 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 14:45:34 -0600 Subject: [PATCH 26/40] extract reactAsDom function --- .../CodeEditor/languageSupport/autocomplete.tsx | 9 ++------- .../CodeEditor/useTooltipsExtension.tsx | 16 +++------------- .../src/components/CodeEditor/utils.tsx | 10 ++++++++++ 3 files changed, 15 insertions(+), 20 deletions(-) create mode 100644 packages/components/src/components/CodeEditor/utils.tsx diff --git a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx index 5e7702dda5..b90a539e58 100644 --- a/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx +++ b/packages/components/src/components/CodeEditor/languageSupport/autocomplete.tsx @@ -5,11 +5,11 @@ import { } from "@codemirror/autocomplete"; import { syntaxTree } from "@codemirror/language"; import { SyntaxNode, Tree } from "@lezer/common"; -import { createRoot } from "react-dom/client"; import { SqProject } from "@quri/squiggle-lang"; import { FnDocumentationFromName } from "../../ui/FnDocumentation.js"; +import { reactAsDom } from "../utils.js"; type NameNode = { node: SyntaxNode; @@ -78,12 +78,7 @@ export function makeCompletionSource(project: SqProject) { const decoratorCompletions: Completion[] = []; const getInfoFunction = (name: string): Completion["info"] => { - return () => { - const dom = document.createElement("div"); - const root = createRoot(dom); - root.render(); - return { dom }; - }; + return () => reactAsDom(); }; for (const [name, value] of project.getStdLib().entrySeq()) { diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index 0572584d8e..6aa33cfc0d 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -2,7 +2,6 @@ import { syntaxTree } from "@codemirror/language"; import { EditorView, hoverTooltip, repositionTooltips } from "@codemirror/view"; import { SyntaxNode } from "@lezer/common"; import { FC, PropsWithChildren, useEffect } from "react"; -import { createRoot } from "react-dom/client"; import { getFunctionDocumentation, @@ -18,6 +17,7 @@ import { } from "../SquiggleViewer/ViewerProvider.js"; import { FnDocumentation } from "../ui/FnDocumentation.js"; import { useReactiveExtension } from "./codemirrorHooks.js"; +import { reactAsDom } from "./utils.js"; type Hover = NonNullable>; @@ -100,12 +100,7 @@ function buildWordHoverExtension({ pos: node.from, end: node.to, above: true, - create() { - const dom = document.createElement("div"); - const root = createRoot(dom); - root.render(); - return { dom }; - }, + create: () => reactAsDom(), }; }; @@ -117,12 +112,7 @@ function buildWordHoverExtension({ pos: node.from, end: node.to, above: true, - create() { - const dom = document.createElement("div"); - const root = createRoot(dom); - root.render(); - return { dom }; - }, + create: () => reactAsDom(), }; }; diff --git a/packages/components/src/components/CodeEditor/utils.tsx b/packages/components/src/components/CodeEditor/utils.tsx new file mode 100644 index 0000000000..1bc2f17d1b --- /dev/null +++ b/packages/components/src/components/CodeEditor/utils.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from "react"; +import { createRoot } from "react-dom/client"; + +export function reactAsDom(node: ReactNode): { dom: HTMLDivElement } { + const dom = document.createElement("div"); + const root = createRoot(dom); + root.render(node); + // This is compatible with `CompletionInfo` and `TooltipView` CodeMirror types + return { dom }; +} From c10ef06f788da93de09a90ecc6eda77ece3c89d9 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 14:59:58 -0600 Subject: [PATCH 27/40] move SqValue tests --- .../{public => SqValue}/SqLambda_test.ts | 0 .../__tests__/SqValue/asJS_test.ts | 25 +++++++++++++++++++ .../docstring_test.ts} | 25 ------------------- 3 files changed, 25 insertions(+), 25 deletions(-) rename packages/squiggle-lang/__tests__/{public => SqValue}/SqLambda_test.ts (100%) create mode 100644 packages/squiggle-lang/__tests__/SqValue/asJS_test.ts rename packages/squiggle-lang/__tests__/{public/SqValue_test.ts => SqValue/docstring_test.ts} (83%) diff --git a/packages/squiggle-lang/__tests__/public/SqLambda_test.ts b/packages/squiggle-lang/__tests__/SqValue/SqLambda_test.ts similarity index 100% rename from packages/squiggle-lang/__tests__/public/SqLambda_test.ts rename to packages/squiggle-lang/__tests__/SqValue/SqLambda_test.ts diff --git a/packages/squiggle-lang/__tests__/SqValue/asJS_test.ts b/packages/squiggle-lang/__tests__/SqValue/asJS_test.ts new file mode 100644 index 0000000000..1990ba0404 --- /dev/null +++ b/packages/squiggle-lang/__tests__/SqValue/asJS_test.ts @@ -0,0 +1,25 @@ +import { testRun } from "../helpers/helpers.js"; + +describe("SqValue.asJS", () => { + test("SqDict -> Map", async () => { + const value = ( + await testRun('{ x: 5, y: [3, "foo", { dist: normal(5,2) } ] }') + ).asJS(); + + expect(value).toBeInstanceOf(Object); + }); + + test("Dict fields", async () => { + const value = (await testRun("{ x: 5 }")).asJS(); + + expect((value as any).value.x).toBe(5); + }); + + test("Deeply nested dist", async () => { + const value = ( + await testRun('{ x: 5, y: [3, "foo", { dist: normal(5,2) } ] }') + ).asJS(); + + expect((value as any).value.y[2].value.dist).toBeInstanceOf(Array); + }); +}); diff --git a/packages/squiggle-lang/__tests__/public/SqValue_test.ts b/packages/squiggle-lang/__tests__/SqValue/docstring_test.ts similarity index 83% rename from packages/squiggle-lang/__tests__/public/SqValue_test.ts rename to packages/squiggle-lang/__tests__/SqValue/docstring_test.ts index b9f1f553fa..a827b25c46 100644 --- a/packages/squiggle-lang/__tests__/public/SqValue_test.ts +++ b/packages/squiggle-lang/__tests__/SqValue/docstring_test.ts @@ -1,29 +1,4 @@ import { run, sq } from "../../src/index.js"; -import { testRun } from "../helpers/helpers.js"; - -describe("SqValue.asJS", () => { - test("SqDict -> Map", async () => { - const value = ( - await testRun('{ x: 5, y: [3, "foo", { dist: normal(5,2) } ] }') - ).asJS(); - - expect(value).toBeInstanceOf(Object); - }); - - test("Dict fields", async () => { - const value = (await testRun("{ x: 5 }")).asJS(); - - expect((value as any).value.x).toBe(5); - }); - - test("Deeply nested dist", async () => { - const value = ( - await testRun('{ x: 5, y: [3, "foo", { dist: normal(5,2) } ] }') - ).asJS(); - - expect((value as any).value.y[2].value.dist).toBeInstanceOf(Array); - }); -}); describe("docstrings", () => { const runToResult = async (code: string) => { From 44b4db911479e70b6ceeb37f0091b91d62d7c276 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 13:08:22 -0800 Subject: [PATCH 28/40] Testing SqValuePath --- .../src/components/SquiggleViewer/index.tsx | 2 +- .../src/components/SquiggleViewer/utils.ts | 7 +- .../__tests__/public/SqValuePath_test.ts | 128 ++++++++++++++++++ .../src/public/SqProject/index.ts | 6 +- .../src/public/SqValueContext.ts | 2 +- .../squiggle-lang/src/public/SqValuePath.ts | 111 ++++++++------- 6 files changed, 203 insertions(+), 53 deletions(-) create mode 100644 packages/squiggle-lang/__tests__/public/SqValuePath_test.ts diff --git a/packages/components/src/components/SquiggleViewer/index.tsx b/packages/components/src/components/SquiggleViewer/index.tsx index 57b910a6b2..4d417c59ae 100644 --- a/packages/components/src/components/SquiggleViewer/index.tsx +++ b/packages/components/src/components/SquiggleViewer/index.tsx @@ -62,7 +62,7 @@ const FocusedNavigation: FC<{ focus(path)} - text={path.items[i + rootPathFocusedAdjustment].toString()} + text={path.items[i + rootPathFocusedAdjustment].toDisplayString()} /> ))}
diff --git a/packages/components/src/components/SquiggleViewer/utils.ts b/packages/components/src/components/SquiggleViewer/utils.ts index 3bbdf7962b..63f3d136b0 100644 --- a/packages/components/src/components/SquiggleViewer/utils.ts +++ b/packages/components/src/components/SquiggleViewer/utils.ts @@ -16,7 +16,10 @@ function topLevelName(path: SqValuePath): string { } export function pathAsString(path: SqValuePath) { - return [topLevelName(path), ...path.items.map((p) => p.toString())].join("."); + return [ + topLevelName(path), + ...path.items.map((p) => p.toDisplayString()), + ].join("."); } export function pathToShortName(path: SqValuePath): string { @@ -24,7 +27,7 @@ export function pathToShortName(path: SqValuePath): string { return topLevelName(path); } else { const lastPathItem = path.items[path.items.length - 1]; - return lastPathItem.toString(); + return lastPathItem.toDisplayString(); } } diff --git a/packages/squiggle-lang/__tests__/public/SqValuePath_test.ts b/packages/squiggle-lang/__tests__/public/SqValuePath_test.ts new file mode 100644 index 0000000000..24ec3996bc --- /dev/null +++ b/packages/squiggle-lang/__tests__/public/SqValuePath_test.ts @@ -0,0 +1,128 @@ +import { SqPathItem, SqValuePath } from "../../src/index.js"; + +describe("SqPathItem", () => { + test("fromString creates a string item", () => { + const item = SqPathItem.fromString("test"); + expect(item.value).toEqual({ type: "string", value: "test" }); + }); +}); + +describe("SqValuePath", () => { + const path = new SqValuePath({ + root: "bindings", + items: [ + SqPathItem.fromString("foo"), + SqPathItem.fromNumber(2), + SqPathItem.fromCalculator(), + SqPathItem.fromCellAddress(1, 2), + ], + }); + + test("Serializes and deserializes complex paths correctly", () => { + const complexPath = new SqValuePath({ + root: "exports", + items: [ + SqPathItem.fromString("nested"), + SqPathItem.fromNumber(42), + SqPathItem.fromCellAddress(5, 10), + ], + }); + const serialized = complexPath.serializeToString(); + const deserialized = SqValuePath.deserialize(serialized); + expect(deserialized).toEqual(complexPath); + }); + + test("Handles empty paths", () => { + const emptyPath = new SqValuePath({ root: "imports", items: [] }); + const serialized = emptyPath.serializeToString(); + const deserialized = SqValuePath.deserialize(serialized); + expect(deserialized.items.length).toBe(0); + }); + + test("deserialize throws error on invalid input", () => { + expect(() => SqValuePath.deserialize("invalid")).toThrow(Error); + }); + + test("Equality", () => { + const path2 = new SqValuePath({ + root: "bindings", + items: [ + SqPathItem.fromString("foo"), + SqPathItem.fromNumber(2), + SqPathItem.fromCalculator(), + SqPathItem.fromCellAddress(1, 2), + ], + }); + expect(path.isEqual(path2)).toBe(true); + }); + + test("extends path correctly", () => { + const path = new SqValuePath({ + root: "bindings", + items: [SqPathItem.fromString("foo")], + }); + const extendedPath = path.extend(SqPathItem.fromNumber(2)); + expect(extendedPath.items.length).toBe(2); + expect(extendedPath.items[1].value).toEqual({ type: "number", value: 2 }); + }); + + describe("SqValuePath contains()", () => { + test("path fully contains another path", () => { + const basePath = new SqValuePath({ + root: "bindings", + items: [SqPathItem.fromString("foo"), SqPathItem.fromNumber(2)], + }); + const subPath = new SqValuePath({ + root: "bindings", + items: [SqPathItem.fromString("foo")], + }); + expect(basePath.contains(subPath)).toBe(true); + }); + + test("path partially contains another path", () => { + const basePath = new SqValuePath({ + root: "bindings", + items: [SqPathItem.fromString("foo")], + }); + const extendedPath = basePath.extend(SqPathItem.fromNumber(2)); + expect(basePath.contains(extendedPath)).toBe(false); + }); + + test("path does not contain another path", () => { + const path1 = new SqValuePath({ + root: "bindings", + items: [SqPathItem.fromString("foo")], + }); + const path2 = new SqValuePath({ + root: "imports", + items: [SqPathItem.fromString("bar")], + }); + expect(path1.contains(path2)).toBe(false); + }); + + test("empty path is contained in any path", () => { + const nonEmptyPath = new SqValuePath({ + root: "exports", + items: [SqPathItem.fromCalculator()], + }); + const emptyPath = new SqValuePath({ + root: "exports", + items: [], + }); + expect(nonEmptyPath.contains(emptyPath)).toBe(true); + }); + + test("equal paths contain each other", () => { + const path1 = new SqValuePath({ + root: "bindings", + items: [SqPathItem.fromString("test")], + }); + const path2 = new SqValuePath({ + root: "bindings", + items: [SqPathItem.fromString("test")], + }); + expect(path1.contains(path2)).toBe(true); + expect(path2.contains(path1)).toBe(true); + }); + }); +}); diff --git a/packages/squiggle-lang/src/public/SqProject/index.ts b/packages/squiggle-lang/src/public/SqProject/index.ts index 3c954ef8a3..4296126f53 100644 --- a/packages/squiggle-lang/src/public/SqProject/index.ts +++ b/packages/squiggle-lang/src/public/SqProject/index.ts @@ -13,7 +13,7 @@ import { SqLinker } from "../SqLinker.js"; import { SqValue, wrapValue } from "../SqValue/index.js"; import { SqDict } from "../SqValue/SqDict.js"; import { SqValueContext } from "../SqValueContext.js"; -import { Root, SqValuePath } from "../SqValuePath.js"; +import { RootPathItem, SqValuePath } from "../SqValuePath.js"; import { SqOutputResult } from "../types.js"; import { type Externals, @@ -237,7 +237,7 @@ export class SqProject { const hasEndExpression = !!lastStatement && !isBindingStatement(lastStatement); - const newContext = (root: Root) => { + const newContext = (root: RootPathItem) => { const isResult = root === "result"; return new SqValueContext({ project: this, @@ -253,7 +253,7 @@ export class SqProject { }); }; - const wrapSqDict = (innerDict: VDict, root: Root): SqDict => { + const wrapSqDict = (innerDict: VDict, root: RootPathItem): SqDict => { return new SqDict(innerDict, newContext(root)); }; diff --git a/packages/squiggle-lang/src/public/SqValueContext.ts b/packages/squiggle-lang/src/public/SqValueContext.ts index 2d33f1e9a3..954cbc97ce 100644 --- a/packages/squiggle-lang/src/public/SqValueContext.ts +++ b/packages/squiggle-lang/src/public/SqValueContext.ts @@ -77,7 +77,7 @@ export class SqValueContext { break; case "Array": if (pathItem.type === "number") { - const element = ast.elements[pathItem.value]; // Seems like this was broken before. + const element = ast.elements[pathItem.value]; if (element) { newAst = element; } diff --git a/packages/squiggle-lang/src/public/SqValuePath.ts b/packages/squiggle-lang/src/public/SqValuePath.ts index 92f99340bd..5b1dd70a10 100644 --- a/packages/squiggle-lang/src/public/SqValuePath.ts +++ b/packages/squiggle-lang/src/public/SqValuePath.ts @@ -1,6 +1,8 @@ import { ASTNode } from "../ast/parse.js"; import { locationContains } from "../ast/utils.js"; +export type RootPathItem = "result" | "bindings" | "imports" | "exports"; + export type PathItem = | { type: "string"; value: string } | { type: "number"; value: number } @@ -32,19 +34,6 @@ function pathItemIsEqual(a: PathItem, b: PathItem): boolean { } } -function pathItemToString(item: PathItem): string { - switch (item.type) { - case "string": - return item.value; - case "number": - return String(item.value); - case "cellAddress": - return `Cell (${item.value.row},${item.value.column})`; - case "calculator": - return "calculator"; - } -} - export class SqPathItem { private constructor(public value: PathItem) {} static fromString(str: string): SqPathItem { @@ -59,24 +48,44 @@ export class SqPathItem { static fromCellAddress(row: number, column: number): SqPathItem { return new SqPathItem({ type: "cellAddress", value: { row, column } }); } - toString() { - return pathItemToString(this.value); + + get type() { + return this.value.type; } + isEqual(other: SqPathItem) { return pathItemIsEqual(this.value, other.value); } - get type() { - return this.value.type; + + toDisplayString(): string { + const item = this.value; + switch (item.type) { + case "string": + return item.value; + case "number": + return String(item.value); + case "cellAddress": + return `Cell (${item.value.row},${item.value.column})`; + case "calculator": + return "calculator"; + } } -} -export type Root = "result" | "bindings" | "imports" | "exports"; + serialize(): string { + return JSON.stringify(this.value); + } + + static deserialize(str: string): SqPathItem { + const value = JSON.parse(str) as PathItem; + return new SqPathItem(value); + } +} export class SqValuePath { - public root: Root; + public root: RootPathItem; public items: SqPathItem[]; - constructor(props: { root: Root; items: SqPathItem[] }) { + constructor(props: { root: RootPathItem; items: SqPathItem[] }) { this.root = props.root; this.items = props.items; } @@ -88,12 +97,12 @@ export class SqValuePath { }); } - contains(other: SqValuePath) { - if (this.items.length > other.items.length) { + contains(smallerItem: SqValuePath) { + if (this.items.length < smallerItem.items.length) { return false; } - for (let i = 0; i < this.items.length; i++) { - if (!this.items[i].isEqual(other.items[i])) { + for (let i = 0; i < smallerItem.items.length; i++) { + if (!this.items[i].isEqual(smallerItem.items[i])) { return false; } } @@ -112,8 +121,37 @@ export class SqValuePath { return true; } - toString() { - return [this.root, ...this.items.map((f) => f.toString())].join("."); + serializeToString(): string { + const pathObject = { + root: this.root, + items: this.items.map((item) => item.serialize()), + }; + return JSON.stringify(pathObject); + } + + static deserialize(str: string): SqValuePath { + const parsed = JSON.parse(str); + const items = parsed.items.map(SqPathItem.deserialize); + return new SqValuePath({ root: parsed.root, items }); + } + + itemsAsValuePaths({ includeRoot = false }) { + const root = new SqValuePath({ + root: this.root, + items: [], + }); + const leafs = this.items.map( + (_, index) => + new SqValuePath({ + root: this.root, + items: this.items.slice(0, index + 1), + }) + ); + return includeRoot ? [root, ...leafs] : leafs; + } + + isRoot() { + return this.items.length === 0; } static findByOffset({ @@ -200,23 +238,4 @@ export class SqValuePath { items: findLoop(ast), }); } - - itemsAsValuePaths({ includeRoot = false }) { - const root = new SqValuePath({ - root: this.root, - items: [], - }); - const leafs = this.items.map( - (_, index) => - new SqValuePath({ - root: this.root, - items: this.items.slice(0, index + 1), - }) - ); - return includeRoot ? [root, ...leafs] : leafs; - } - - isRoot() { - return this.items.length === 0; - } } From 8361d0ccead607127723ec37d284e4f0c89ce421 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 13:19:30 -0800 Subject: [PATCH 29/40] Minor fix --- .../src/components/SquiggleViewer/utils.ts | 99 ++++++++----------- 1 file changed, 43 insertions(+), 56 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/utils.ts b/packages/components/src/components/SquiggleViewer/utils.ts index 63f3d136b0..5a368eab9f 100644 --- a/packages/components/src/components/SquiggleViewer/utils.ts +++ b/packages/components/src/components/SquiggleViewer/utils.ts @@ -1,5 +1,3 @@ -import isEqual from "lodash/isEqual.js"; - import { SqDict, SqValue, SqValuePath } from "@quri/squiggle-lang"; import { SHORT_STRING_LENGTH } from "../../lib/constants.js"; @@ -49,75 +47,64 @@ export function useGetSubvalueByPath() { return ( topValue: SqValue, - pathToSubvalue: SqValuePath + subValuePath: SqValuePath ): SqValue | undefined => { const { context } = topValue; if (!context) { return; } - if (!pathToSubvalue.contains(context.path)) { + if (!subValuePath.contains(context.path)) { return; } let currentValue = topValue; - for (let i = 0; i < pathToSubvalue.items.length; i++) { - if (i < context.path.items.length) { - // check that `path` is a subpath of `context.path` - if (!isEqual(context.path.items[i], pathToSubvalue.items[i])) { + for (let i = 0; i < subValuePath.items.length; i++) { + const pathItem = subValuePath.items[i]; + let nextValue: SqValue | undefined; + + if (currentValue.tag === "Array" && pathItem.value.type === "number") { + nextValue = currentValue.value.getValues()[pathItem.value.value]; + } else if ( + currentValue.tag === "Dict" && + pathItem.value.type === "string" + ) { + nextValue = currentValue.value.get(pathItem.value.value); + } else if ( + currentValue.tag === "TableChart" && + pathItem.value.type === "cellAddress" + ) { + // Maybe it would be better to get the environment in a different way. + const environment = context.project.getEnvironment(); + const item = currentValue.value.item( + pathItem.value.value.row, + pathItem.value.value.column, + environment + ); + if (item.ok) { + nextValue = item.value; + } else { return; } - continue; - } - - const pathItem = pathToSubvalue.items[i]; - - { - let nextValue: SqValue | undefined; - - if (currentValue.tag === "Array" && pathItem.value.type === "number") { - nextValue = currentValue.value.getValues()[pathItem.value.value]; - } else if ( - currentValue.tag === "Dict" && - pathItem.value.type === "string" - ) { - nextValue = currentValue.value.get(pathItem.value.value); - } else if ( - currentValue.tag === "TableChart" && - pathItem.value.type === "cellAddress" - ) { - // Maybe it would be better to get the environment in a different way. - const environment = context.project.getEnvironment(); - const item = currentValue.value.item( - pathItem.value.value.row, - pathItem.value.value.column, - environment - ); - if (item.ok) { - nextValue = item.value; - } else { - return; - } - } else if (pathItem.type === "calculator") { - // The previous path item is the one that is the parent of the calculator result. - // This is the one that we use in the ViewerContext to store information about the calculator. - const calculatorPath = new SqValuePath({ - root: pathToSubvalue.root, - items: pathToSubvalue.items.slice(0, i), - }); - const calculatorState = itemStore.getCalculator(calculatorPath); - const result = calculatorState?.calculatorResult; - if (!result?.ok) { - return; - } - nextValue = result.value; - } - - if (!nextValue) { + } else if (pathItem.type === "calculator") { + // The previous path item is the one that is the parent of the calculator result. + // This is the one that we use in the ViewerContext to store information about the calculator. + const calculatorPath = new SqValuePath({ + root: subValuePath.root, + items: subValuePath.items.slice(0, i), + }); + const calculatorState = itemStore.getCalculator(calculatorPath); + const result = calculatorState?.calculatorResult; + if (!result?.ok) { return; } - currentValue = nextValue; + nextValue = result.value; + } + + if (!nextValue) { + return; } + currentValue = nextValue; } return currentValue; }; From 12e6e6d79832726200bfef4e8f28f16e22d66304 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 15:22:27 -0600 Subject: [PATCH 30/40] SqValueContext tests; change testRun API --- .../squiggle-lang/__tests__/Jstat_test.ts | 8 ++-- .../__tests__/SqValue/asJS_test.ts | 6 +-- .../__tests__/SqValue/context_test.ts | 42 +++++++++++++++++++ .../__tests__/helpers/helpers.ts | 16 ++++++- .../__tests__/library/mixture_test.ts | 4 +- .../__tests__/library/pointset_test.ts | 16 +++---- .../__tests__/library/sampleSet_test.ts | 4 +- .../__tests__/library/scalars_test.ts | 10 ++--- .../__tests__/library/sym_test.ts | 42 ++++++++++++------- .../__tests__/public_smoke_test.ts | 12 +++--- .../__tests__/reducer/Parser_test.ts | 6 +-- packages/squiggle-lang/src/ast/parse.ts | 2 +- 12 files changed, 117 insertions(+), 51 deletions(-) create mode 100644 packages/squiggle-lang/__tests__/SqValue/context_test.ts diff --git a/packages/squiggle-lang/__tests__/Jstat_test.ts b/packages/squiggle-lang/__tests__/Jstat_test.ts index cceb1e395b..dd5fad039a 100644 --- a/packages/squiggle-lang/__tests__/Jstat_test.ts +++ b/packages/squiggle-lang/__tests__/Jstat_test.ts @@ -11,8 +11,8 @@ describe("cumulative density function of a normal distribution", () => { async (mean, stdev) => { const threeStdevsAboveMean = mean + 3 * stdev; const squiggleString = `cdf(Sym.normal(${mean}, ${stdev}), ${threeStdevsAboveMean})`; - const squiggleResult = await testRun(squiggleString); - expect(squiggleResult.value).toBeCloseTo(1); + const { result } = await testRun(squiggleString); + expect(result.value).toBeCloseTo(1); } ) ); @@ -26,8 +26,8 @@ describe("cumulative density function of a normal distribution", () => { async (mean, stdev) => { const threeStdevsBelowMean = mean - 3 * stdev; const squiggleString = `cdf(Sym.normal(${mean}, ${stdev}), ${threeStdevsBelowMean})`; - const squiggleResult = await testRun(squiggleString); - expect(squiggleResult.value).toBeCloseTo(0); + const { result } = await testRun(squiggleString); + expect(result.value).toBeCloseTo(0); } ) ); diff --git a/packages/squiggle-lang/__tests__/SqValue/asJS_test.ts b/packages/squiggle-lang/__tests__/SqValue/asJS_test.ts index 1990ba0404..15e1e37f66 100644 --- a/packages/squiggle-lang/__tests__/SqValue/asJS_test.ts +++ b/packages/squiggle-lang/__tests__/SqValue/asJS_test.ts @@ -4,13 +4,13 @@ describe("SqValue.asJS", () => { test("SqDict -> Map", async () => { const value = ( await testRun('{ x: 5, y: [3, "foo", { dist: normal(5,2) } ] }') - ).asJS(); + ).result.asJS(); expect(value).toBeInstanceOf(Object); }); test("Dict fields", async () => { - const value = (await testRun("{ x: 5 }")).asJS(); + const value = (await testRun("{ x: 5 }")).result.asJS(); expect((value as any).value.x).toBe(5); }); @@ -18,7 +18,7 @@ describe("SqValue.asJS", () => { test("Deeply nested dist", async () => { const value = ( await testRun('{ x: 5, y: [3, "foo", { dist: normal(5,2) } ] }') - ).asJS(); + ).result.asJS(); expect((value as any).value.y[2].value.dist).toBeInstanceOf(Array); }); diff --git a/packages/squiggle-lang/__tests__/SqValue/context_test.ts b/packages/squiggle-lang/__tests__/SqValue/context_test.ts new file mode 100644 index 0000000000..087846b7e1 --- /dev/null +++ b/packages/squiggle-lang/__tests__/SqValue/context_test.ts @@ -0,0 +1,42 @@ +import { nodeToString } from "../../src/ast/parse.js"; +import { assertTag, testRun } from "../helpers/helpers.js"; + +describe("SqValueContext", () => { + test("valueAst for nested nodes", async () => { + const { bindings } = await testRun(` +x = { foo: 5 } + +y = 6 +`); + + const x = bindings.get("x"); + assertTag(x, "Dict"); + + const foo = x.value.get("foo"); + expect(nodeToString(x.context!.valueAst)).toBe( + "(LetStatement :x (Block (Dict (KeyValue 'foo' 5))))" + ); + expect(nodeToString(foo!.context!.valueAst)).toBe("(KeyValue 'foo' 5)"); + + const y = bindings.get("y"); + assertTag(y, "Number"); + + expect(nodeToString(y.context!.valueAst)).toBe( + "(LetStatement :y (Block 6))" + ); + }); + + test.failing("valueAst for decorated statements", async () => { + const { bindings } = await testRun(` +@name("Z") +z = 5 +`); + + const z = bindings.get("z"); + assertTag(z, "Number"); + + expect(nodeToString(z.context!.valueAst)).toBe( + "(LetStatement :z (Block 7))" + ); + }); +}); diff --git a/packages/squiggle-lang/__tests__/helpers/helpers.ts b/packages/squiggle-lang/__tests__/helpers/helpers.ts index 052e967a09..a610708cc6 100644 --- a/packages/squiggle-lang/__tests__/helpers/helpers.ts +++ b/packages/squiggle-lang/__tests__/helpers/helpers.ts @@ -1,4 +1,4 @@ -import { run } from "../../src/index.js"; +import { run, SqValue } from "../../src/index.js"; export async function testRun(x: string) { const outputR = await run(x, { @@ -9,7 +9,7 @@ export async function testRun(x: string) { }); if (outputR.ok) { - return outputR.value.result; + return outputR.value; } else { throw new Error( `Expected squiggle expression to evaluate but got error: ${outputR.value}` @@ -39,3 +39,15 @@ export function expectErrorToBeBounded( const error = distance / normalizingDenom; expect(error).toBeLessThanOrEqual(epsilon); } + +export function assertTag( + value: SqValue | undefined, + tag: T +): asserts value is Extract { + if (!value) { + throw new Error("Undefined value"); + } + if (value.tag !== tag) { + throw new Error(`Expected ${tag} value, got ${value.tag}`); + } +} diff --git a/packages/squiggle-lang/__tests__/library/mixture_test.ts b/packages/squiggle-lang/__tests__/library/mixture_test.ts index a6f74d335e..ac4ae9770b 100644 --- a/packages/squiggle-lang/__tests__/library/mixture_test.ts +++ b/packages/squiggle-lang/__tests__/library/mixture_test.ts @@ -35,14 +35,14 @@ describe("mixture", () => { await testRun( "a = Sym.normal(0,1); m = mx(a, 3, [.999999,.00001]); stdev(a - m)" ) - ).value + ).result.value ).toBeGreaterThan(1); expect( ( await testRun( "a = normal(0,1); m = mx(a, 3, [.999999,.00001]); stdev(a - m)" ) - ).value + ).result.value ).toBeCloseTo(0); }); }); diff --git a/packages/squiggle-lang/__tests__/library/pointset_test.ts b/packages/squiggle-lang/__tests__/library/pointset_test.ts index 4fa9723dcd..a0d6d75e21 100644 --- a/packages/squiggle-lang/__tests__/library/pointset_test.ts +++ b/packages/squiggle-lang/__tests__/library/pointset_test.ts @@ -35,16 +35,16 @@ describe("Mean of mixture is weighted average of means", () => { async (normalMean, normalStdev, betaA, betaB, x, y) => { // normaalize is due to https://github.com/quantified-uncertainty/squiggle/issues/1400 bug const squiggleString = `mean(mixture(Sym.normal(${normalMean},${normalStdev}), Sym.beta(${betaA},${betaB}), [${x}, ${y}])->normalize)`; - const res = await testRun(squiggleString); + const { result } = await testRun(squiggleString); const weightDenom = x + y; const normalWeight = x / weightDenom; const betaWeight = y / weightDenom; const betaMean = betaA / (betaA + betaB); - if (res.tag !== "Number") { - throw new Error(`Expected number result, got: ${res.tag}`); + if (result.tag !== "Number") { + throw new Error(`Expected number result, got: ${result.tag}`); } expectErrorToBeBounded( - res.value, + result.value, normalWeight * normalMean + betaWeight * betaMean, // this is a huge allowed error, but it's the highest precision we can achieve because of this bug: https://github.com/quantified-uncertainty/squiggle/issues/1414, even on relatively high \alpha and \beta values { epsilon: 0.7 } @@ -65,11 +65,11 @@ describe("Discrete", () => { test("sample", async () => { for (let i = 0; i < 100; i++) { - const res = await testRun("mx(3,5) -> sample"); - if (res.tag !== "Number") { - throw new Error(`Expected number result, got: ${res.tag}`); + const { result } = await testRun("mx(3,5) -> sample"); + if (result.tag !== "Number") { + throw new Error(`Expected number result, got: ${result.tag}`); } - expect(res.value === 5 || res.value === 3).toBe(true); + expect(result.value === 5 || result.value === 3).toBe(true); } }); }); diff --git a/packages/squiggle-lang/__tests__/library/sampleSet_test.ts b/packages/squiggle-lang/__tests__/library/sampleSet_test.ts index 6aed3bad8c..a546214419 100644 --- a/packages/squiggle-lang/__tests__/library/sampleSet_test.ts +++ b/packages/squiggle-lang/__tests__/library/sampleSet_test.ts @@ -77,7 +77,7 @@ const arrayGen = () => async function makeSampleSet(samples: number[]) { const sampleList = samples.map((x) => x.toFixed(20)).join(","); - const result = await testRun(`SampleSet.fromList([${sampleList}])`); + const { result } = await testRun(`SampleSet.fromList([${sampleList}])`); if (result.tag === "Dist") { return result.value; } else { @@ -267,7 +267,7 @@ describe("fromSamples function", () => { const squiggleString = `x = fromSamples([${xsString}]); mean(x)`; const squiggleResult = await testRun(squiggleString); const mean = xs.reduce((a, b) => a + b, 0.0) / xs.length; - expect(squiggleResult.value).toBeCloseTo(mean, 4); + expect(squiggleResult.result.value).toBeCloseTo(mean, 4); }) ); }); diff --git a/packages/squiggle-lang/__tests__/library/scalars_test.ts b/packages/squiggle-lang/__tests__/library/scalars_test.ts index a60d84eac6..3feb6b753a 100644 --- a/packages/squiggle-lang/__tests__/library/scalars_test.ts +++ b/packages/squiggle-lang/__tests__/library/scalars_test.ts @@ -7,11 +7,11 @@ describe("Scalar manipulation is well-modeled by javascript math", () => { fc.assert( fc.asyncProperty(fc.nat(), async (x) => { const squiggleString = `log(${x})`; - const squiggleResult = await testRun(squiggleString); + const { result } = await testRun(squiggleString); if (x === 0) { - expect(squiggleResult.value).toEqual(-Infinity); + expect(result.value).toEqual(-Infinity); } else { - expect(squiggleResult.value).toEqual(Math.log(x)); + expect(result.value).toEqual(Math.log(x)); } }) ); @@ -25,8 +25,8 @@ describe("Scalar manipulation is well-modeled by javascript math", () => { fc.integer(), async (x, y, z) => { const squiggleString = `x = ${x}; y = ${y}; z = ${z}; x + y + z`; - const squiggleResult = await testRun(squiggleString); - expect(squiggleResult.value).toBeCloseTo(x + y + z); + const { result } = await testRun(squiggleString); + expect(result.value).toBeCloseTo(x + y + z); } ) ); diff --git a/packages/squiggle-lang/__tests__/library/sym_test.ts b/packages/squiggle-lang/__tests__/library/sym_test.ts index 08b677f013..ef7f61dfca 100644 --- a/packages/squiggle-lang/__tests__/library/sym_test.ts +++ b/packages/squiggle-lang/__tests__/library/sym_test.ts @@ -20,10 +20,12 @@ describe("Symbolic constructors", () => { test("5% inv", async () => { expect( - (await testRun("Sym.normal({p5: -2, p95: 4}) -> inv(0.05)")).value + (await testRun("Sym.normal({p5: -2, p95: 4}) -> inv(0.05)")).result + .value ).toBeCloseTo(-2); expect( - (await testRun("Sym.normal({p5: -2, p95: 4}) -> inv(0.95)")).value + (await testRun("Sym.normal({p5: -2, p95: 4}) -> inv(0.95)")).result + .value ).toBeCloseTo(4); }); @@ -33,10 +35,12 @@ describe("Symbolic constructors", () => { ); test("10% inv", async () => { expect( - (await testRun("Sym.normal({p10: -2, p90: 4}) -> inv(0.1)")).value + (await testRun("Sym.normal({p10: -2, p90: 4}) -> inv(0.1)")).result + .value ).toBeCloseTo(-2); expect( - (await testRun("Sym.normal({p10: -2, p90: 4}) -> inv(0.9)")).value + (await testRun("Sym.normal({p10: -2, p90: 4}) -> inv(0.9)")).result + .value ).toBeCloseTo(4); }); @@ -46,10 +50,12 @@ describe("Symbolic constructors", () => { ); test("25% inv", async () => { expect( - (await testRun("Sym.normal({p25: -2, p75: 4}) -> inv(0.25)")).value + (await testRun("Sym.normal({p25: -2, p75: 4}) -> inv(0.25)")).result + .value ).toBeCloseTo(-2); expect( - (await testRun("Sym.normal({p25: -2, p75: 4}) -> inv(0.75)")).value + (await testRun("Sym.normal({p25: -2, p75: 4}) -> inv(0.75)")).result + .value ).toBeCloseTo(4); }); }); @@ -63,10 +69,12 @@ describe("Symbolic constructors", () => { ); test("5% inv", async () => { expect( - (await testRun("Sym.lognormal({p5: 2, p95: 5}) -> inv(0.05)")).value + (await testRun("Sym.lognormal({p5: 2, p95: 5}) -> inv(0.05)")).result + .value ).toBeCloseTo(2); expect( - (await testRun("Sym.lognormal({p5: 2, p95: 5}) -> inv(0.95)")).value + (await testRun("Sym.lognormal({p5: 2, p95: 5}) -> inv(0.95)")).result + .value ).toBeCloseTo(5); }); @@ -76,10 +84,12 @@ describe("Symbolic constructors", () => { ); test("10% inv", async () => { expect( - (await testRun("Sym.lognormal({p10: 2, p90: 5}) -> inv(0.1)")).value + (await testRun("Sym.lognormal({p10: 2, p90: 5}) -> inv(0.1)")).result + .value ).toBeCloseTo(2); expect( - (await testRun("Sym.lognormal({p10: 2, p90: 5}) -> inv(0.9)")).value + (await testRun("Sym.lognormal({p10: 2, p90: 5}) -> inv(0.9)")).result + .value ).toBeCloseTo(5); }); @@ -89,10 +99,12 @@ describe("Symbolic constructors", () => { ); test("25% inv", async () => { expect( - (await testRun("Sym.lognormal({p25: 2, p75: 5}) -> inv(0.25)")).value + (await testRun("Sym.lognormal({p25: 2, p75: 5}) -> inv(0.25)")).result + .value ).toBeCloseTo(2); expect( - (await testRun("Sym.lognormal({p25: 2, p75: 5}) -> inv(0.75)")).value + (await testRun("Sym.lognormal({p25: 2, p75: 5}) -> inv(0.75)")).result + .value ).toBeCloseTo(5); }); }); @@ -143,7 +155,7 @@ describe("distribution functions", () => { testEvalToBe("10 - Sym.normal(5, 1)", "Normal(5,1)"); testEvalToBe("Sym.normal(5, 1) - 10", "Normal(-5,1)"); test("mean(1 - PointSet(Sym.normal(5, 2)))", async () => { - const result = await testRun("mean(1 - PointSet(Sym.normal(5, 2)))"); + const { result } = await testRun("mean(1 - PointSet(Sym.normal(5, 2)))"); if (result.tag !== "Number") { throw new Error(); } @@ -249,10 +261,10 @@ describe("Symbolic mean", () => { async (x, y, z) => { if (!(x < y && y < z)) { try { - const squiggleResult = await testRun( + const { result } = await testRun( `mean(triangular(${x},${y},${z}))` ); - expect(squiggleResult.value).toBeCloseTo((x + y + z) / 3); + expect(result.value).toBeCloseTo((x + y + z) / 3); } catch (err) { expect((err as Error).message).toEqual( "Expected squiggle expression to evaluate but got error: Distribution Math Error: Triangular values must be increasing order." diff --git a/packages/squiggle-lang/__tests__/public_smoke_test.ts b/packages/squiggle-lang/__tests__/public_smoke_test.ts index 9ee63f28a0..d61dbf7cc5 100644 --- a/packages/squiggle-lang/__tests__/public_smoke_test.ts +++ b/packages/squiggle-lang/__tests__/public_smoke_test.ts @@ -3,30 +3,30 @@ import { testRun } from "./helpers/helpers.js"; describe("Simple calculations and results", () => { test("mean(Sym.normal(5,2))", async () => { - const result = await testRun("mean(Sym.normal(5,2))"); // FIXME + const { result } = await testRun("mean(Sym.normal(5,2))"); // FIXME expect(result.toString()).toEqual("5"); }); test("10+10", async () => { - const result = await testRun("10 + 10"); + const { result } = await testRun("10 + 10"); expect(result.toString()).toEqual("20"); }); }); describe("Log function", () => { test("log(1) = 0", async () => { - const foo = await testRun("log(1)"); - expect(foo.toString()).toEqual("0"); + const { result } = await testRun("log(1)"); + expect(result.toString()).toEqual("0"); }); }); describe("Array", () => { test("nested Array", async () => { - expect((await testRun("[[ 1 ]]")).toString()).toEqual("[[1]]"); + expect((await testRun("[[ 1 ]]")).result.toString()).toEqual("[[1]]"); }); }); describe("Dict", () => { test("Return dict", async () => { - expect((await testRun("{a:1}")).toString()).toEqual("{a: 1}"); + expect((await testRun("{a:1}")).result.toString()).toEqual("{a: 1}"); }); }); diff --git a/packages/squiggle-lang/__tests__/reducer/Parser_test.ts b/packages/squiggle-lang/__tests__/reducer/Parser_test.ts index df84551c6f..987cc886e3 100644 --- a/packages/squiggle-lang/__tests__/reducer/Parser_test.ts +++ b/packages/squiggle-lang/__tests__/reducer/Parser_test.ts @@ -19,7 +19,7 @@ describe("Squiggle's parser is whitespace insensitive", () => { ): string => { return `theDist${a}=${b}beta(${c}4${d},${e}5e1)${f};${g}theDist${h}`; }; - const squiggleOutput = await testRun( + const { result } = await testRun( squiggleString("", "", "", "", "", "", "", "") ); @@ -40,8 +40,8 @@ describe("Squiggle's parser is whitespace insensitive", () => { whitespaceGen(), async (a, b, c, d, e, f, g, h) => { expect( - await testRun(squiggleString(a, b, c, d, e, f, g, h)) - ).toEqualSqValue(squiggleOutput); + (await testRun(squiggleString(a, b, c, d, e, f, g, h))).result + ).toEqualSqValue(result); } ) ); diff --git a/packages/squiggle-lang/src/ast/parse.ts b/packages/squiggle-lang/src/ast/parse.ts index e7374655b9..ef4c7487b9 100644 --- a/packages/squiggle-lang/src/ast/parse.ts +++ b/packages/squiggle-lang/src/ast/parse.ts @@ -48,7 +48,7 @@ export function parse(expr: string, source: string): ParseResult { // This function is just for the sake of tests. // For real generation of Squiggle code from AST try our prettier plugin. -function nodeToString(node: ASTNode): string { +export function nodeToString(node: ASTNode): string { const sExpr = (components: (ASTNode | string)[]) => "(" + node.type + From 29c5ae3296dfe3af62bdf5264a521999977fc1fb Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 13:26:22 -0800 Subject: [PATCH 31/40] useGetSubValueByPath cleanup --- .../src/components/SquiggleViewer/utils.ts | 22 +++++++++---------- .../squiggle-lang/src/public/SqValuePath.ts | 4 ++++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/utils.ts b/packages/components/src/components/SquiggleViewer/utils.ts index 5a368eab9f..69f3140d9e 100644 --- a/packages/components/src/components/SquiggleViewer/utils.ts +++ b/packages/components/src/components/SquiggleViewer/utils.ts @@ -24,8 +24,7 @@ export function pathToShortName(path: SqValuePath): string { if (path.isRoot()) { return topLevelName(path); } else { - const lastPathItem = path.items[path.items.length - 1]; - return lastPathItem.toDisplayString(); + return path.lastItem().toDisplayString(); } } @@ -50,17 +49,19 @@ export function useGetSubvalueByPath() { subValuePath: SqValuePath ): SqValue | undefined => { const { context } = topValue; - if (!context) { - return; - } - if (!subValuePath.contains(context.path)) { + if (!context || !subValuePath.contains(context.path)) { return; } let currentValue = topValue; - for (let i = 0; i < subValuePath.items.length; i++) { - const pathItem = subValuePath.items[i]; + const subValuePaths = subValuePath.itemsAsValuePaths({ + includeRoot: false, + }); + + for (const subValuePath of subValuePaths) { + const pathItem = subValuePath.lastItem(); + let nextValue: SqValue | undefined; if (currentValue.tag === "Array" && pathItem.value.type === "number") { @@ -89,10 +90,7 @@ export function useGetSubvalueByPath() { } else if (pathItem.type === "calculator") { // The previous path item is the one that is the parent of the calculator result. // This is the one that we use in the ViewerContext to store information about the calculator. - const calculatorPath = new SqValuePath({ - root: subValuePath.root, - items: subValuePath.items.slice(0, i), - }); + const calculatorPath = subValuePath; const calculatorState = itemStore.getCalculator(calculatorPath); const result = calculatorState?.calculatorResult; if (!result?.ok) { diff --git a/packages/squiggle-lang/src/public/SqValuePath.ts b/packages/squiggle-lang/src/public/SqValuePath.ts index 5b1dd70a10..2020b3d385 100644 --- a/packages/squiggle-lang/src/public/SqValuePath.ts +++ b/packages/squiggle-lang/src/public/SqValuePath.ts @@ -90,6 +90,10 @@ export class SqValuePath { this.items = props.items; } + lastItem() { + return this.items[this.items.length - 1]; + } + extend(item: SqPathItem) { return new SqValuePath({ root: this.root, From 671de40270a9c60edb7a8bbb93ffbdd99d121431 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 15:49:56 -0600 Subject: [PATCH 32/40] fix valueAst for decorated statements --- .../CodeEditor/useTooltipsExtension.tsx | 8 ++++++-- .../__tests__/SqValue/context_test.ts | 11 +++++++---- packages/squiggle-lang/src/ast/peggyHelpers.ts | 10 +++++++--- packages/squiggle-lang/src/ast/utils.ts | 16 ++++++++++++++-- packages/squiggle-lang/src/expression/compile.ts | 6 ++---- .../squiggle-lang/src/public/SqValueContext.ts | 2 ++ 6 files changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx index 6aa33cfc0d..a49926e1de 100644 --- a/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx +++ b/packages/components/src/components/CodeEditor/useTooltipsExtension.tsx @@ -153,11 +153,15 @@ function buildWordHoverExtension({ const value = bindings.value.get(name); if (!value) return null; - // Should be LetStatement or DefunStatement + // Should be a statement const valueAst = value.context?.valueAst; + if (!valueAst) { + return null; + } + if ( - valueAst && + // Note that `valueAst` can't be "DecoratedStatement", we skip those in `SqValueContext` and AST symbols (valueAst.type === "LetStatement" || valueAst.type === "DefunStatement") && // If these don't match then variable was probably shadowed by a later statement and we can't show its value. diff --git a/packages/squiggle-lang/__tests__/SqValue/context_test.ts b/packages/squiggle-lang/__tests__/SqValue/context_test.ts index 087846b7e1..2a7e0e6443 100644 --- a/packages/squiggle-lang/__tests__/SqValue/context_test.ts +++ b/packages/squiggle-lang/__tests__/SqValue/context_test.ts @@ -11,12 +11,14 @@ y = 6 const x = bindings.get("x"); assertTag(x, "Dict"); - - const foo = x.value.get("foo"); expect(nodeToString(x.context!.valueAst)).toBe( "(LetStatement :x (Block (Dict (KeyValue 'foo' 5))))" ); + expect(x.context?.valueAstIsPrecise).toBe(true); + + const foo = x.value.get("foo"); expect(nodeToString(foo!.context!.valueAst)).toBe("(KeyValue 'foo' 5)"); + expect(foo!.context!.valueAstIsPrecise).toBe(true); const y = bindings.get("y"); assertTag(y, "Number"); @@ -24,9 +26,10 @@ y = 6 expect(nodeToString(y.context!.valueAst)).toBe( "(LetStatement :y (Block 6))" ); + expect(y!.context!.valueAstIsPrecise).toBe(true); }); - test.failing("valueAst for decorated statements", async () => { + test("valueAst for decorated statements", async () => { const { bindings } = await testRun(` @name("Z") z = 5 @@ -36,7 +39,7 @@ z = 5 assertTag(z, "Number"); expect(nodeToString(z.context!.valueAst)).toBe( - "(LetStatement :z (Block 7))" + "(LetStatement :z (Block 5))" ); }); }); diff --git a/packages/squiggle-lang/src/ast/peggyHelpers.ts b/packages/squiggle-lang/src/ast/peggyHelpers.ts index fdad2743fd..0b7ff139f5 100644 --- a/packages/squiggle-lang/src/ast/peggyHelpers.ts +++ b/packages/squiggle-lang/src/ast/peggyHelpers.ts @@ -1,5 +1,7 @@ import { LocationRange } from "peggy"; +import { undecorated } from "./utils.js"; + export const infixFunctions = { "+": "add", "-": "subtract", @@ -50,6 +52,7 @@ type NodeProgram = N< statements: ASTNode[]; // Var name -> statement node, for faster path resolution. // Not used for evaluation. + // Note: symbols point to undecorated statements. symbols: { [k in string]: ASTNode }; } >; @@ -295,11 +298,12 @@ export function nodeProgram( ): NodeProgram { const symbols: NodeProgram["symbols"] = {}; for (const statement of statements) { + const _undecorated = undecorated(statement); if ( - statement.type === "LetStatement" || - statement.type === "DefunStatement" + _undecorated.type === "LetStatement" || + _undecorated.type === "DefunStatement" ) { - symbols[statement.variable.value] = statement; + symbols[_undecorated.variable.value] = _undecorated; } } return { type: "Program", imports, statements, symbols, location }; diff --git a/packages/squiggle-lang/src/ast/utils.ts b/packages/squiggle-lang/src/ast/utils.ts index 71b73c6c1a..1cda54f237 100644 --- a/packages/squiggle-lang/src/ast/utils.ts +++ b/packages/squiggle-lang/src/ast/utils.ts @@ -8,8 +8,20 @@ export function locationContains(location: LocationRange, offset: number) { export function isBindingStatement( statement: ASTNode -): statement is Extract { +): statement is Extract< + ASTNode, + { type: "LetStatement" | "DefunStatement" | "DecoratedStatement" } +> { return ( - statement.type === "LetStatement" || statement.type === "DefunStatement" + statement.type === "LetStatement" || + statement.type === "DefunStatement" || + statement.type === "DecoratedStatement" ); } + +export function undecorated(node: ASTNode) { + while (node.type === "DecoratedStatement") { + node = node.statement; + } + return node; +} diff --git a/packages/squiggle-lang/src/expression/compile.ts b/packages/squiggle-lang/src/expression/compile.ts index 8a8f23e0fc..60a9b29729 100644 --- a/packages/squiggle-lang/src/expression/compile.ts +++ b/packages/squiggle-lang/src/expression/compile.ts @@ -2,6 +2,7 @@ import { List as ImmutableList } from "immutable"; import { ASTNode } from "../ast/parse.js"; import { infixFunctions, unaryFunctions } from "../ast/peggyHelpers.js"; +import { undecorated } from "../ast/utils.js"; import { ICompileError } from "../errors/IError.js"; import { Bindings } from "../reducer/stack.js"; import { ImmutableMap } from "../utility/immutableMap.js"; @@ -108,10 +109,7 @@ function compileToContent( ); statements.push(statement); { - let maybeExportedStatement = astStatement; - while (maybeExportedStatement.type === "DecoratedStatement") { - maybeExportedStatement = maybeExportedStatement.statement; - } + const maybeExportedStatement = undecorated(astStatement); if ( (maybeExportedStatement.type === "LetStatement" || maybeExportedStatement.type === "DefunStatement") && diff --git a/packages/squiggle-lang/src/public/SqValueContext.ts b/packages/squiggle-lang/src/public/SqValueContext.ts index aac2a3b206..54a0045800 100644 --- a/packages/squiggle-lang/src/public/SqValueContext.ts +++ b/packages/squiggle-lang/src/public/SqValueContext.ts @@ -53,6 +53,8 @@ export class SqValueContext { ast = ast.statements[ast.statements.length - 1]; } else if (ast.type === "KeyValue") { ast = ast.value; + } else if (ast.type === "DecoratedStatement") { + ast = ast.statement; } else if (isBindingStatement(ast)) { ast = ast.value; } else { From 7963a45a40f057615a0dd736a4a1998ac45b86c3 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 13:51:05 -0800 Subject: [PATCH 33/40] Minor cleanup --- packages/components/src/components/SquiggleViewer/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/utils.ts b/packages/components/src/components/SquiggleViewer/utils.ts index 69f3140d9e..19beb18985 100644 --- a/packages/components/src/components/SquiggleViewer/utils.ts +++ b/packages/components/src/components/SquiggleViewer/utils.ts @@ -90,8 +90,7 @@ export function useGetSubvalueByPath() { } else if (pathItem.type === "calculator") { // The previous path item is the one that is the parent of the calculator result. // This is the one that we use in the ViewerContext to store information about the calculator. - const calculatorPath = subValuePath; - const calculatorState = itemStore.getCalculator(calculatorPath); + const calculatorState = itemStore.getCalculator(subValuePath); const result = calculatorState?.calculatorResult; if (!result?.ok) { return; From 0c3e04979ad432868aa26e5f994b329efd3e1738 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Jan 2024 16:52:04 -0600 Subject: [PATCH 34/40] fix components tests by converting ts-jest to jest with babel --- packages/components/babel.config.cjs | 7 + packages/components/jest.config.js | 13 +- packages/components/package.json | 8 +- pnpm-lock.yaml | 266 +++++++++++++++++++++------ 4 files changed, 223 insertions(+), 71 deletions(-) create mode 100644 packages/components/babel.config.cjs diff --git a/packages/components/babel.config.cjs b/packages/components/babel.config.cjs new file mode 100644 index 0000000000..a0c9eb23aa --- /dev/null +++ b/packages/components/babel.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ["@babel/preset-react", { runtime: "automatic" }], + ], +}; diff --git a/packages/components/jest.config.js b/packages/components/jest.config.js index df6b8bb320..e096edf8bc 100644 --- a/packages/components/jest.config.js +++ b/packages/components/jest.config.js @@ -1,20 +1,11 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ +/** @type {import('jest').Config} */ const jestConfig = { - preset: "ts-jest/presets/default-esm-legacy", testEnvironment: "jsdom", + extensionsToTreatAsEsm: [".ts", ".tsx"], setupFilesAfterEnv: ["/test/setup.ts"], moduleNameMapper: { "^(\\.{1,2}/.*)\\.js$": "$1", }, - transform: { - "^.+\\.tsx?$": [ - "ts-jest", - { - useESM: true, - tsconfig: "tsconfig.tests.json", - }, - ], - }, testPathIgnorePatterns: ["/node_modules/", "/dist"], }; diff --git a/packages/components/package.json b/packages/components/package.json index e32c5953ac..3c40ba7fad 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -22,8 +22,8 @@ "@hookform/resolvers": "^3.3.3", "@lezer/common": "^1.2.0", "@quri/prettier-plugin-squiggle": "workspace:*", - "@quri/squiggle-textmate-grammar": "workspace:*", "@quri/squiggle-lang": "workspace:*", + "@quri/squiggle-textmate-grammar": "workspace:*", "@quri/ui": "workspace:*", "@react-hook/size": "^2.1.2", "@tailwindcss/typography": "^0.5.10", @@ -45,12 +45,14 @@ "zod": "^3.22.4" }, "devDependencies": { - "@quri/configs": "workspace:*", + "@babel/preset-react": "^7.23.3", + "@babel/preset-typescript": "^7.23.3", "@jest/globals": "^29.7.0", "@juggle/resize-observer": "^3.4.0", "@lezer/generator": "^1.5.1", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.3.14", + "@quri/configs": "workspace:*", "@storybook/addon-actions": "^7.6.6", "@storybook/addon-docs": "^7.6.6", "@storybook/addon-essentials": "^7.6.6", @@ -73,6 +75,7 @@ "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "@wogns3623/eslint-plugin-better-exhaustive-deps": "^1.1.0", + "babel-jest": "^29.7.0", "canvas": "^2.11.2", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", @@ -88,7 +91,6 @@ "rollup-plugin-node-builtins": "^2.1.2", "storybook": "^7.6.7", "tailwindcss": "^3.4.0", - "ts-jest": "^29.1.1", "typescript": "^5.3.3", "vite": "^5.0.10" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de4f635183..297f635c2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,12 @@ importers: specifier: ^3.22.4 version: 3.22.4 devDependencies: + '@babel/preset-react': + specifier: ^7.23.3 + version: 7.23.3(@babel/core@7.23.7) + '@babel/preset-typescript': + specifier: ^7.23.3 + version: 7.23.3(@babel/core@7.23.7) '@jest/globals': specifier: ^29.7.0 version: 29.7.0 @@ -235,6 +241,9 @@ importers: '@wogns3623/eslint-plugin-better-exhaustive-deps': specifier: ^1.1.0 version: 1.1.0(eslint@8.56.0) + babel-jest: + specifier: ^29.7.0 + version: 29.7.0(@babel/core@7.23.7) canvas: specifier: ^2.11.2 version: 2.11.2 @@ -249,7 +258,7 @@ importers: version: 4.6.0(eslint@8.56.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.10.6)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.10.6) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0(canvas@2.11.2) @@ -277,9 +286,6 @@ importers: tailwindcss: specifier: ^3.4.0 version: 3.4.0 - ts-jest: - specifier: ^29.1.1 - version: 29.1.1(@babel/core@7.23.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.3.3) typescript: specifier: ^5.3.3 version: 5.3.3 @@ -1302,11 +1308,6 @@ packages: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.22.15: - resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-option@7.23.5: resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} @@ -2060,6 +2061,26 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-react-display-name@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.23.7): + resolution: {integrity: sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.7) + dev: true + /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.23.7): resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} engines: {node: '>=6.9.0'} @@ -2094,6 +2115,17 @@ packages: '@babel/types': 7.23.6 dev: true + /@babel/plugin-transform-react-pure-annotations@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} engines: {node: '>=6.9.0'} @@ -2336,6 +2368,21 @@ packages: esutils: 2.0.3 dev: true + /@babel/preset-react@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 + '@babel/plugin-transform-react-display-name': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.7) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.23.7) + '@babel/plugin-transform-react-pure-annotations': 7.23.3(@babel/core@7.23.7) + dev: true + /@babel/preset-typescript@7.23.3(@babel/core@7.23.7): resolution: {integrity: sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==} engines: {node: '>=6.9.0'} @@ -2344,7 +2391,7 @@ packages: dependencies: '@babel/core': 7.23.7 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.22.15 + '@babel/helper-validator-option': 7.23.5 '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7) '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.7) '@babel/plugin-transform-typescript': 7.23.5(@babel/core@7.23.7) @@ -4053,6 +4100,49 @@ packages: slash: 3.0.0 dev: true + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.6 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.10.6) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /@jest/core@29.7.0(ts-node@10.9.2): resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7184,7 +7274,7 @@ packages: chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.5.16 - jest: 29.7.0(@types/node@20.10.6)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.10.6) lodash: 4.17.21 redent: 3.0.0 dev: true @@ -8896,13 +8986,6 @@ packages: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) - /bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - dependencies: - fast-json-stable-stringify: 2.1.0 - dev: true - /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: @@ -9605,6 +9688,25 @@ packages: sha.js: 2.4.11 dev: true + /create-jest@29.7.0(@types/node@20.10.6): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.10.6) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /create-jest@29.7.0(@types/node@20.10.6)(ts-node@10.9.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -13226,6 +13328,34 @@ packages: - supports-color dev: true + /jest-cli@29.7.0(@types/node@20.10.6): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.10.6) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@20.10.6) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-cli@29.7.0(@types/node@20.10.6)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -13254,6 +13384,46 @@ packages: - ts-node dev: true + /jest-config@29.7.0(@types/node@20.10.6): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.23.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.10.6 + babel-jest: 29.7.0(@babel/core@7.23.7) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-config@29.7.0(@types/node@20.10.6)(ts-node@10.9.2): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -13610,6 +13780,27 @@ packages: supports-color: 8.1.1 dev: true + /jest@29.7.0(@types/node@20.10.6): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@20.10.6) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest@29.7.0(@types/node@20.10.6)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -14105,10 +14296,6 @@ packages: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} dev: false - /lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: true - /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -18883,41 +19070,6 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - /ts-jest@29.1.1(@babel/core@7.23.7)(esbuild@0.18.20)(jest@29.7.0)(typescript@5.3.3): - resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - '@babel/core': 7.23.7 - bs-logger: 0.2.6 - esbuild: 0.18.20 - fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.10.6)(ts-node@10.9.2) - jest-util: 29.7.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.5.4 - typescript: 5.3.3 - yargs-parser: 21.1.1 - dev: true - /ts-log@2.2.5: resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} dev: true From a8f8af60b3a4a2040da26d2953642161e82f8e2c Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 17:02:29 -0800 Subject: [PATCH 35/40] Test cleanup --- .../__tests__/public/SqValuePath_test.ts | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/squiggle-lang/__tests__/public/SqValuePath_test.ts b/packages/squiggle-lang/__tests__/public/SqValuePath_test.ts index 24ec3996bc..324ca374ab 100644 --- a/packages/squiggle-lang/__tests__/public/SqValuePath_test.ts +++ b/packages/squiggle-lang/__tests__/public/SqValuePath_test.ts @@ -18,32 +18,32 @@ describe("SqValuePath", () => { ], }); - test("Serializes and deserializes complex paths correctly", () => { - const complexPath = new SqValuePath({ - root: "exports", - items: [ - SqPathItem.fromString("nested"), - SqPathItem.fromNumber(42), - SqPathItem.fromCellAddress(5, 10), - ], + describe("serializeToString() and deserialize()", () => { + test("Works on complex paths", () => { + const complexPath = new SqValuePath({ + root: "exports", + items: [ + SqPathItem.fromString("nested"), + SqPathItem.fromNumber(42), + SqPathItem.fromCellAddress(5, 10), + ], + }); + const serialized = complexPath.serializeToString(); + const deserialized = SqValuePath.deserialize(serialized); + expect(deserialized).toEqual(complexPath); + }); + test("works on empty path", () => { + const emptyPath = new SqValuePath({ root: "imports", items: [] }); + const serialized = emptyPath.serializeToString(); + const deserialized = SqValuePath.deserialize(serialized); + expect(deserialized.items.length).toBe(0); + }); + test("throws error on invalid input", () => { + expect(() => SqValuePath.deserialize("invalid")).toThrow(Error); }); - const serialized = complexPath.serializeToString(); - const deserialized = SqValuePath.deserialize(serialized); - expect(deserialized).toEqual(complexPath); - }); - - test("Handles empty paths", () => { - const emptyPath = new SqValuePath({ root: "imports", items: [] }); - const serialized = emptyPath.serializeToString(); - const deserialized = SqValuePath.deserialize(serialized); - expect(deserialized.items.length).toBe(0); - }); - - test("deserialize throws error on invalid input", () => { - expect(() => SqValuePath.deserialize("invalid")).toThrow(Error); }); - test("Equality", () => { + test("isEqual()", () => { const path2 = new SqValuePath({ root: "bindings", items: [ @@ -56,7 +56,7 @@ describe("SqValuePath", () => { expect(path.isEqual(path2)).toBe(true); }); - test("extends path correctly", () => { + test("extend()", () => { const path = new SqValuePath({ root: "bindings", items: [SqPathItem.fromString("foo")], @@ -66,8 +66,8 @@ describe("SqValuePath", () => { expect(extendedPath.items[1].value).toEqual({ type: "number", value: 2 }); }); - describe("SqValuePath contains()", () => { - test("path fully contains another path", () => { + describe("contains()", () => { + test("path fully contains a shorter path", () => { const basePath = new SqValuePath({ root: "bindings", items: [SqPathItem.fromString("foo"), SqPathItem.fromNumber(2)], @@ -79,16 +79,16 @@ describe("SqValuePath", () => { expect(basePath.contains(subPath)).toBe(true); }); - test("path partially contains another path", () => { + test("path does not contain longer path", () => { const basePath = new SqValuePath({ root: "bindings", items: [SqPathItem.fromString("foo")], }); - const extendedPath = basePath.extend(SqPathItem.fromNumber(2)); - expect(basePath.contains(extendedPath)).toBe(false); + const longerPath = basePath.extend(SqPathItem.fromNumber(2)); + expect(basePath.contains(longerPath)).toBe(false); }); - test("path does not contain another path", () => { + test("path does not contain different path", () => { const path1 = new SqValuePath({ root: "bindings", items: [SqPathItem.fromString("foo")], @@ -100,7 +100,7 @@ describe("SqValuePath", () => { expect(path1.contains(path2)).toBe(false); }); - test("empty path is contained in any path", () => { + test("path contains empty path (with same root)", () => { const nonEmptyPath = new SqValuePath({ root: "exports", items: [SqPathItem.fromCalculator()], From f1005e916ee2ac14e737f920c5c69549fd9b3970 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 17:03:45 -0800 Subject: [PATCH 36/40] Added changset: --- .changeset/angry-bears-dress.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/angry-bears-dress.md diff --git a/.changeset/angry-bears-dress.md b/.changeset/angry-bears-dress.md new file mode 100644 index 0000000000..8f1c64ee01 --- /dev/null +++ b/.changeset/angry-bears-dress.md @@ -0,0 +1,6 @@ +--- +"@quri/squiggle-lang": patch +"@quri/squiggle-components": patch +--- + +Fixes error with "Find in editor" and tooltips not working for decorated values From 84e17788e0e0a0be022083ac5e6864e4d329c6ad Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 17:40:13 -0800 Subject: [PATCH 37/40] Further cleanup --- .../src/components/SquiggleViewer/index.tsx | 2 +- .../src/components/SquiggleViewer/utils.ts | 19 +- .../__tests__/public/SqValuePath_test.ts | 25 -- .../src/public/SqProject/index.ts | 2 +- .../squiggle-lang/src/public/SqValuePath.ts | 213 +++++++++--------- 5 files changed, 113 insertions(+), 148 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/index.tsx b/packages/components/src/components/SquiggleViewer/index.tsx index 4d417c59ae..7c3f27625c 100644 --- a/packages/components/src/components/SquiggleViewer/index.tsx +++ b/packages/components/src/components/SquiggleViewer/index.tsx @@ -56,7 +56,7 @@ const FocusedNavigation: FC<{ )} {focusedPath - .itemsAsValuePaths({ includeRoot: false }) + .allSqValuePathSubsets({ includeRoot: false }) .slice(rootPathFocusedAdjustment, -1) .map((path, i) => ( { ], }); - describe("serializeToString() and deserialize()", () => { - test("Works on complex paths", () => { - const complexPath = new SqValuePath({ - root: "exports", - items: [ - SqPathItem.fromString("nested"), - SqPathItem.fromNumber(42), - SqPathItem.fromCellAddress(5, 10), - ], - }); - const serialized = complexPath.serializeToString(); - const deserialized = SqValuePath.deserialize(serialized); - expect(deserialized).toEqual(complexPath); - }); - test("works on empty path", () => { - const emptyPath = new SqValuePath({ root: "imports", items: [] }); - const serialized = emptyPath.serializeToString(); - const deserialized = SqValuePath.deserialize(serialized); - expect(deserialized.items.length).toBe(0); - }); - test("throws error on invalid input", () => { - expect(() => SqValuePath.deserialize("invalid")).toThrow(Error); - }); - }); - test("isEqual()", () => { const path2 = new SqValuePath({ root: "bindings", diff --git a/packages/squiggle-lang/src/public/SqProject/index.ts b/packages/squiggle-lang/src/public/SqProject/index.ts index 4296126f53..e806384256 100644 --- a/packages/squiggle-lang/src/public/SqProject/index.ts +++ b/packages/squiggle-lang/src/public/SqProject/index.ts @@ -416,7 +416,7 @@ export class SqProject { if (!ast.ok) { return ast; } - const found = SqValuePath.findByOffset({ + const found = SqValuePath.findByAstOffset({ ast: ast.value, offset, }); diff --git a/packages/squiggle-lang/src/public/SqValuePath.ts b/packages/squiggle-lang/src/public/SqValuePath.ts index 2020b3d385..88b69a4b58 100644 --- a/packages/squiggle-lang/src/public/SqValuePath.ts +++ b/packages/squiggle-lang/src/public/SqValuePath.ts @@ -70,15 +70,85 @@ export class SqPathItem { return "calculator"; } } +} - serialize(): string { - return JSON.stringify(this.value); - } +// There might be a better place for this to go, nearer to the ASTNode type. +function astOffsetToPathItems(ast: ASTNode, offset: number): SqPathItem[] { + function buildRemainingPathItems(ast: ASTNode): SqPathItem[] { + switch (ast.type) { + case "Program": { + for (const statement of ast.statements) { + if (locationContains(statement.location, offset)) { + return buildRemainingPathItems(statement); + } + } + return []; + } + case "Dict": { + for (const pair of ast.elements) { + if ( + !locationContains( + { + source: ast.location.source, + start: pair.location.start, + end: pair.location.end, + }, + offset + ) + ) { + continue; + } - static deserialize(str: string): SqPathItem { - const value = JSON.parse(str) as PathItem; - return new SqPathItem(value); + if ( + pair.type === "KeyValue" && + pair.key.type === "String" // only string keys are supported + ) { + return [ + SqPathItem.fromString(pair.key.value), + ...buildRemainingPathItems(pair.value), + ]; + } else if (pair.type === "Identifier") { + return [SqPathItem.fromString(pair.value)]; // this is a final node, no need to buildRemainingPathItems recursively + } + } + return []; + } + case "Array": { + for (let i = 0; i < ast.elements.length; i++) { + const element = ast.elements[i]; + if (locationContains(element.location, offset)) { + return [ + SqPathItem.fromNumber(i), + ...buildRemainingPathItems(element), + ]; + } + } + return []; + } + case "LetStatement": { + return [ + SqPathItem.fromString(ast.variable.value), + ...buildRemainingPathItems(ast.value), + ]; + } + case "DefunStatement": { + return [ + SqPathItem.fromString(ast.variable.value), + ...buildRemainingPathItems(ast.value), + ]; + } + case "Block": { + if ( + ast.statements.length === 1 && + ["Array", "Dict"].includes(ast.statements[0].type) + ) { + return buildRemainingPathItems(ast.statements[0]); + } + } + } + return []; } + return buildRemainingPathItems(ast); } export class SqValuePath { @@ -90,7 +160,24 @@ export class SqValuePath { this.items = props.items; } - lastItem() { + static findByAstOffset({ + ast, + offset, + }: { + ast: ASTNode; + offset: number; + }): SqValuePath | undefined { + return new SqValuePath({ + root: "bindings", // not important, will probably be removed soon + items: astOffsetToPathItems(ast, offset), + }); + } + + isRoot() { + return this.items.length === 0; + } + + lastItem(): SqPathItem | undefined { return this.items[this.items.length - 1]; } @@ -101,7 +188,11 @@ export class SqValuePath { }); } + // Checks if this SqValuePath completely contains all of the nodes in this other one. contains(smallerItem: SqValuePath) { + if (this.root !== smallerItem.root) { + return false; + } if (this.items.length < smallerItem.items.length) { return false; } @@ -114,6 +205,9 @@ export class SqValuePath { } isEqual(other: SqValuePath) { + if (this.root !== other.root) { + return false; + } if (this.items.length !== other.items.length) { return false; } @@ -125,21 +219,7 @@ export class SqValuePath { return true; } - serializeToString(): string { - const pathObject = { - root: this.root, - items: this.items.map((item) => item.serialize()), - }; - return JSON.stringify(pathObject); - } - - static deserialize(str: string): SqValuePath { - const parsed = JSON.parse(str); - const items = parsed.items.map(SqPathItem.deserialize); - return new SqValuePath({ root: parsed.root, items }); - } - - itemsAsValuePaths({ includeRoot = false }) { + allSqValuePathSubsets({ includeRoot = false }) { const root = new SqValuePath({ root: this.root, items: [], @@ -153,93 +233,4 @@ export class SqValuePath { ); return includeRoot ? [root, ...leafs] : leafs; } - - isRoot() { - return this.items.length === 0; - } - - static findByOffset({ - ast, - offset, - }: { - ast: ASTNode; - offset: number; - }): SqValuePath | undefined { - const findLoop = (ast: ASTNode): SqPathItem[] => { - switch (ast.type) { - case "Program": { - for (const statement of ast.statements) { - if (locationContains(statement.location, offset)) { - return findLoop(statement); - } - } - return []; - } - case "Dict": { - for (const pair of ast.elements) { - if ( - !locationContains( - { - source: ast.location.source, - start: pair.location.start, - end: pair.location.end, - }, - offset - ) - ) { - continue; - } - - if ( - pair.type === "KeyValue" && - pair.key.type === "String" // only string keys are supported - ) { - return [ - SqPathItem.fromString(pair.key.value), - ...findLoop(pair.value), - ]; - } else if (pair.type === "Identifier") { - return [SqPathItem.fromString(pair.value)]; // this is a final node, no need to findLoop recursively - } - } - return []; - } - case "Array": { - for (let i = 0; i < ast.elements.length; i++) { - const element = ast.elements[i]; - if (locationContains(element.location, offset)) { - return [SqPathItem.fromNumber(i), ...findLoop(element)]; - } - } - return []; - } - case "LetStatement": { - return [ - SqPathItem.fromString(ast.variable.value), - ...findLoop(ast.value), - ]; - } - case "DefunStatement": { - return [ - SqPathItem.fromString(ast.variable.value), - ...findLoop(ast.value), - ]; - } - case "Block": { - if ( - ast.statements.length === 1 && - ["Array", "Dict"].includes(ast.statements[0].type) - ) { - return findLoop(ast.statements[0]); - } - } - } - return []; - }; - - return new SqValuePath({ - root: "bindings", // not important, will probably be removed soon - items: findLoop(ast), - }); - } } From 5a513a036cae675b61f229fb841ae49a153764c1 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 19:05:46 -0800 Subject: [PATCH 38/40] Minor cleanup --- .../squiggle-lang/src/public/SqValuePath.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/squiggle-lang/src/public/SqValuePath.ts b/packages/squiggle-lang/src/public/SqValuePath.ts index 88b69a4b58..d040fb509e 100644 --- a/packages/squiggle-lang/src/public/SqValuePath.ts +++ b/packages/squiggle-lang/src/public/SqValuePath.ts @@ -11,6 +11,12 @@ export type PathItem = type: "calculator"; }; +function isCellAddressPathItem( + item: PathItem +): item is { type: "cellAddress"; value: { row: number; column: number } } { + return item.type === "cellAddress"; +} + function pathItemIsEqual(a: PathItem, b: PathItem): boolean { if (a.type !== b.type) { return false; @@ -22,12 +28,9 @@ function pathItemIsEqual(a: PathItem, b: PathItem): boolean { return a.value === (b as { type: "number"; value: number }).value; case "cellAddress": return ( - a.value.row === - (b as { type: "cellAddress"; value: { row: number; column: number } }) - .value.row && - a.value.column === - (b as { type: "cellAddress"; value: { row: number; column: number } }) - .value.column + isCellAddressPathItem(b) && + a.value.row === b.value.row && + a.value.column === b.value.column ); case "calculator": return true; @@ -65,9 +68,9 @@ export class SqPathItem { case "number": return String(item.value); case "cellAddress": - return `Cell (${item.value.row},${item.value.column})`; + return `Cell(${item.value.row},${item.value.column})`; case "calculator": - return "calculator"; + return "Calculator"; } } } From e3bbc9911a1f11023f54ff58fa6c93002e2da12f Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 19:16:14 -0800 Subject: [PATCH 39/40] Changed SqPathItem to use 'dictKey' and 'arrayIndex' --- .../src/components/SquiggleViewer/utils.ts | 4 +-- .../src/stories/SquiggleChart.stories.tsx | 2 +- .../stories/SquiggleChart/Basic.stories.tsx | 2 +- .../__tests__/SqValue/SqValuePath_test.ts | 30 ++++++++-------- .../src/public/SqValue/SqArray.ts | 2 +- .../src/public/SqValue/SqDict.ts | 4 +-- .../src/public/SqValue/SqPlot.ts | 6 ++-- .../src/public/SqValueContext.ts | 6 ++-- .../squiggle-lang/src/public/SqValuePath.ts | 34 +++++++++---------- 9 files changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/components/src/components/SquiggleViewer/utils.ts b/packages/components/src/components/SquiggleViewer/utils.ts index c3c172c5c7..5f4a3b046e 100644 --- a/packages/components/src/components/SquiggleViewer/utils.ts +++ b/packages/components/src/components/SquiggleViewer/utils.ts @@ -66,9 +66,9 @@ export function useGetSubvalueByPath() { let nextValue: SqValue | undefined; - if (currentTag === "Array" && pathItemType === "number") { + if (currentTag === "Array" && pathItemType === "arrayIndex") { nextValue = currentValue.value.getValues()[pathItem.value.value]; - } else if (currentTag === "Dict" && pathItemType === "string") { + } else if (currentTag === "Dict" && pathItemType === "dictKey") { nextValue = currentValue.value.get(pathItem.value.value); } else if ( currentTag === "TableChart" && diff --git a/packages/components/src/stories/SquiggleChart.stories.tsx b/packages/components/src/stories/SquiggleChart.stories.tsx index a27c046b5a..c3294be7f4 100644 --- a/packages/components/src/stories/SquiggleChart.stories.tsx +++ b/packages/components/src/stories/SquiggleChart.stories.tsx @@ -50,7 +50,7 @@ export const RootPathOverride: Story = { code: "{foo: 35 to 50, bar: [1,2,3]}", rootPathOverride: new SqValuePath({ root: "result", - items: [SqPathItem.fromString("bar")], + items: [SqPathItem.fromDictKey("bar")], }), }, }; diff --git a/packages/components/src/stories/SquiggleChart/Basic.stories.tsx b/packages/components/src/stories/SquiggleChart/Basic.stories.tsx index 5d120d5182..e0ed530f03 100644 --- a/packages/components/src/stories/SquiggleChart/Basic.stories.tsx +++ b/packages/components/src/stories/SquiggleChart/Basic.stories.tsx @@ -43,7 +43,7 @@ export const WithPathOverride: Story = { `, rootPathOverride: new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("foo"), SqPathItem.fromString("bar")], + items: [SqPathItem.fromDictKey("foo"), SqPathItem.fromDictKey("bar")], }), }, }; diff --git a/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts b/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts index 5764819a66..2610d834d5 100644 --- a/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts +++ b/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts @@ -2,7 +2,7 @@ import { SqPathItem, SqValuePath } from "../../src/index.js"; describe("SqPathItem", () => { test("fromString creates a string item", () => { - const item = SqPathItem.fromString("test"); + const item = SqPathItem.fromDictKey("test"); expect(item.value).toEqual({ type: "string", value: "test" }); }); }); @@ -11,8 +11,8 @@ describe("SqValuePath", () => { const path = new SqValuePath({ root: "bindings", items: [ - SqPathItem.fromString("foo"), - SqPathItem.fromNumber(2), + SqPathItem.fromDictKey("foo"), + SqPathItem.fromArrayIndex(2), SqPathItem.fromCalculator(), SqPathItem.fromCellAddress(1, 2), ], @@ -22,8 +22,8 @@ describe("SqValuePath", () => { const path2 = new SqValuePath({ root: "bindings", items: [ - SqPathItem.fromString("foo"), - SqPathItem.fromNumber(2), + SqPathItem.fromDictKey("foo"), + SqPathItem.fromArrayIndex(2), SqPathItem.fromCalculator(), SqPathItem.fromCellAddress(1, 2), ], @@ -34,9 +34,9 @@ describe("SqValuePath", () => { test("extend()", () => { const path = new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("foo")], + items: [SqPathItem.fromDictKey("foo")], }); - const extendedPath = path.extend(SqPathItem.fromNumber(2)); + const extendedPath = path.extend(SqPathItem.fromArrayIndex(2)); expect(extendedPath.items.length).toBe(2); expect(extendedPath.items[1].value).toEqual({ type: "number", value: 2 }); }); @@ -45,11 +45,11 @@ describe("SqValuePath", () => { test("path fully contains a shorter path", () => { const basePath = new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("foo"), SqPathItem.fromNumber(2)], + items: [SqPathItem.fromDictKey("foo"), SqPathItem.fromArrayIndex(2)], }); const subPath = new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("foo")], + items: [SqPathItem.fromDictKey("foo")], }); expect(basePath.contains(subPath)).toBe(true); }); @@ -57,20 +57,20 @@ describe("SqValuePath", () => { test("path does not contain longer path", () => { const basePath = new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("foo")], + items: [SqPathItem.fromDictKey("foo")], }); - const longerPath = basePath.extend(SqPathItem.fromNumber(2)); + const longerPath = basePath.extend(SqPathItem.fromArrayIndex(2)); expect(basePath.contains(longerPath)).toBe(false); }); test("path does not contain different path", () => { const path1 = new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("foo")], + items: [SqPathItem.fromDictKey("foo")], }); const path2 = new SqValuePath({ root: "imports", - items: [SqPathItem.fromString("bar")], + items: [SqPathItem.fromDictKey("bar")], }); expect(path1.contains(path2)).toBe(false); }); @@ -90,11 +90,11 @@ describe("SqValuePath", () => { test("equal paths contain each other", () => { const path1 = new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("test")], + items: [SqPathItem.fromDictKey("test")], }); const path2 = new SqValuePath({ root: "bindings", - items: [SqPathItem.fromString("test")], + items: [SqPathItem.fromDictKey("test")], }); expect(path1.contains(path2)).toBe(true); expect(path2.contains(path1)).toBe(true); diff --git a/packages/squiggle-lang/src/public/SqValue/SqArray.ts b/packages/squiggle-lang/src/public/SqValue/SqArray.ts index f8252b1f5c..6dab6e76f0 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqArray.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqArray.ts @@ -11,7 +11,7 @@ export class SqArray { getValues() { return this._value.map((v, i) => - wrapValue(v, this.context?.extend(SqPathItem.fromNumber(i))) + wrapValue(v, this.context?.extend(SqPathItem.fromArrayIndex(i))) ); } } diff --git a/packages/squiggle-lang/src/public/SqValue/SqDict.ts b/packages/squiggle-lang/src/public/SqValue/SqDict.ts index a037a1c1c4..16d80e6a43 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqDict.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqDict.ts @@ -15,7 +15,7 @@ export class SqDict { ([key, v]) => [ key, - wrapValue(v, this.context?.extend(SqPathItem.fromString(key))), + wrapValue(v, this.context?.extend(SqPathItem.fromDictKey(key))), ] as const ); } @@ -25,7 +25,7 @@ export class SqDict { if (value === undefined) { return undefined; } - return wrapValue(value, this.context?.extend(SqPathItem.fromString(key))); + return wrapValue(value, this.context?.extend(SqPathItem.fromDictKey(key))); } toString() { diff --git a/packages/squiggle-lang/src/public/SqValue/SqPlot.ts b/packages/squiggle-lang/src/public/SqValue/SqPlot.ts index ee76c59421..0ac89b3aa4 100644 --- a/packages/squiggle-lang/src/public/SqValue/SqPlot.ts +++ b/packages/squiggle-lang/src/public/SqValue/SqPlot.ts @@ -160,7 +160,7 @@ export class SqNumericFnPlot extends SqAbstractPlot<"numericFn"> { this.context ? this.createdProgrammatically ? this.context - : this.context.extend(SqPathItem.fromString("fn")) + : this.context.extend(SqPathItem.fromDictKey("fn")) : undefined ); } @@ -228,7 +228,7 @@ export class SqDistFnPlot extends SqAbstractPlot<"distFn"> { this.context ? this.createdProgrammatically ? this.context - : this.context.extend(SqPathItem.fromString("fn")) + : this.context.extend(SqPathItem.fromDictKey("fn")) : undefined ); } @@ -315,7 +315,7 @@ export class SqRelativeValuesPlot extends SqAbstractPlot<"relativeValues"> { get fn(): SqLambda { return new SqLambda( this._value.fn, - this.context?.extend(SqPathItem.fromString("fn")) + this.context?.extend(SqPathItem.fromDictKey("fn")) ); } } diff --git a/packages/squiggle-lang/src/public/SqValueContext.ts b/packages/squiggle-lang/src/public/SqValueContext.ts index 3ff17dcc50..be32f99e11 100644 --- a/packages/squiggle-lang/src/public/SqValueContext.ts +++ b/packages/squiggle-lang/src/public/SqValueContext.ts @@ -66,19 +66,19 @@ export class SqValueContext { switch (ast.type) { case "Program": { - if (this.path.root === "bindings" && pathItem.type === "string") { + if (this.path.root === "bindings" && pathItem.type === "dictKey") { newAst = ast.symbols[pathItem.value]; break; } break; } case "Dict": - if (pathItem.type === "string") { + if (pathItem.type === "dictKey") { newAst = ast.symbols[pathItem.value]; } break; case "Array": - if (pathItem.type === "number") { + if (pathItem.type === "arrayIndex") { const element = ast.elements[pathItem.value]; if (element) { newAst = element; diff --git a/packages/squiggle-lang/src/public/SqValuePath.ts b/packages/squiggle-lang/src/public/SqValuePath.ts index d040fb509e..77eb07dd6e 100644 --- a/packages/squiggle-lang/src/public/SqValuePath.ts +++ b/packages/squiggle-lang/src/public/SqValuePath.ts @@ -4,8 +4,8 @@ import { locationContains } from "../ast/utils.js"; export type RootPathItem = "result" | "bindings" | "imports" | "exports"; export type PathItem = - | { type: "string"; value: string } - | { type: "number"; value: number } + | { type: "dictKey"; value: string } + | { type: "arrayIndex"; value: number } | { type: "cellAddress"; value: { row: number; column: number } } | { type: "calculator"; @@ -22,10 +22,10 @@ function pathItemIsEqual(a: PathItem, b: PathItem): boolean { return false; } switch (a.type) { - case "string": - return a.value === (b as { type: "string"; value: string }).value; - case "number": - return a.value === (b as { type: "number"; value: number }).value; + case "dictKey": + return a.value === (b as { type: "dictKey"; value: string }).value; + case "arrayIndex": + return a.value === (b as { type: "arrayIndex"; value: number }).value; case "cellAddress": return ( isCellAddressPathItem(b) && @@ -39,11 +39,11 @@ function pathItemIsEqual(a: PathItem, b: PathItem): boolean { export class SqPathItem { private constructor(public value: PathItem) {} - static fromString(str: string): SqPathItem { - return new SqPathItem({ type: "string", value: str }); + static fromDictKey(str: string): SqPathItem { + return new SqPathItem({ type: "dictKey", value: str }); } - static fromNumber(num: number): SqPathItem { - return new SqPathItem({ type: "number", value: num }); + static fromArrayIndex(num: number): SqPathItem { + return new SqPathItem({ type: "arrayIndex", value: num }); } static fromCalculator(): SqPathItem { return new SqPathItem({ type: "calculator" }); @@ -63,9 +63,9 @@ export class SqPathItem { toDisplayString(): string { const item = this.value; switch (item.type) { - case "string": + case "dictKey": return item.value; - case "number": + case "arrayIndex": return String(item.value); case "cellAddress": return `Cell(${item.value.row},${item.value.column})`; @@ -107,11 +107,11 @@ function astOffsetToPathItems(ast: ASTNode, offset: number): SqPathItem[] { pair.key.type === "String" // only string keys are supported ) { return [ - SqPathItem.fromString(pair.key.value), + SqPathItem.fromDictKey(pair.key.value), ...buildRemainingPathItems(pair.value), ]; } else if (pair.type === "Identifier") { - return [SqPathItem.fromString(pair.value)]; // this is a final node, no need to buildRemainingPathItems recursively + return [SqPathItem.fromDictKey(pair.value)]; // this is a final node, no need to buildRemainingPathItems recursively } } return []; @@ -121,7 +121,7 @@ function astOffsetToPathItems(ast: ASTNode, offset: number): SqPathItem[] { const element = ast.elements[i]; if (locationContains(element.location, offset)) { return [ - SqPathItem.fromNumber(i), + SqPathItem.fromArrayIndex(i), ...buildRemainingPathItems(element), ]; } @@ -130,13 +130,13 @@ function astOffsetToPathItems(ast: ASTNode, offset: number): SqPathItem[] { } case "LetStatement": { return [ - SqPathItem.fromString(ast.variable.value), + SqPathItem.fromDictKey(ast.variable.value), ...buildRemainingPathItems(ast.value), ]; } case "DefunStatement": { return [ - SqPathItem.fromString(ast.variable.value), + SqPathItem.fromDictKey(ast.variable.value), ...buildRemainingPathItems(ast.value), ]; } From 5da319e40eb6a52b037a7b7bdbe19cb8581b00a2 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 19 Jan 2024 19:26:47 -0800 Subject: [PATCH 40/40] Fixed tests --- .../squiggle-lang/__tests__/SqValue/SqValuePath_test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts b/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts index 2610d834d5..02cd8783d9 100644 --- a/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts +++ b/packages/squiggle-lang/__tests__/SqValue/SqValuePath_test.ts @@ -1,9 +1,9 @@ import { SqPathItem, SqValuePath } from "../../src/index.js"; describe("SqPathItem", () => { - test("fromString creates a string item", () => { + test("fromDictKey creates a string item", () => { const item = SqPathItem.fromDictKey("test"); - expect(item.value).toEqual({ type: "string", value: "test" }); + expect(item.value).toEqual({ type: "dictKey", value: "test" }); }); }); @@ -38,7 +38,10 @@ describe("SqValuePath", () => { }); const extendedPath = path.extend(SqPathItem.fromArrayIndex(2)); expect(extendedPath.items.length).toBe(2); - expect(extendedPath.items[1].value).toEqual({ type: "number", value: 2 }); + expect(extendedPath.items[1].value).toEqual({ + type: "arrayIndex", + value: 2, + }); }); describe("contains()", () => {