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 ( = ({ onSubmit={() => runnerState.run()} /> - {hideViewer || !squiggleOutput?.code ? null : ( + {hideViewer || !squiggleOutput ? null : ( = ({ output, mode, isRunning }) => { + if (!output.ok) { + return ; + } + + const sqOutput = output.value; + let usedValue: SqValue | undefined; + switch (mode) { + case "Result": + usedValue = 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..7ab57fc4c6 --- /dev/null +++ b/packages/components/src/components/SquiggleOutputViewer/ViewerMenu.tsx @@ -0,0 +1,115 @@ +import clsx from "clsx"; +import { FC } from "react"; + +import { + Button, + CodeBracketIcon, + Dropdown, + DropdownMenu, + DropdownMenuActionItem, + TriangleIcon, +} from "@quri/ui"; + +import { SqOutputResult } from "../../../../squiggle-lang/src/public/types.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: SqOutputResult; +}; + +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; + const exportsCount = output.ok ? output.value.exports.size() : 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 6ab49d329e..2eac328c0a 100644 --- a/packages/components/src/components/SquiggleOutputViewer/index.tsx +++ b/packages/components/src/components/SquiggleOutputViewer/index.tsx @@ -1,51 +1,17 @@ -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 { SqOutputResult } from "../../../../squiggle-lang/src/public/types.js"; import { SquiggleOutput } from "../../lib/hooks/useSquiggle.js"; -import { - getResultExports, - getResultImports, - getResultValue, - getResultVariables, -} 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, 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; @@ -53,63 +19,32 @@ type Props = { editor?: CodeEditorHandle; } & PartialPlaygroundSettings; -/* 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; +export type ViewerMode = "Imports" | "Exports" | "Variables" | "Result"; - const [mode, setMode] = useState< - "Imports" | "Exports" | "Variables" | "Result" - >(hasResult ? "Result" : exportsCount > 0 ? "Exports" : "Variables"); +function useMode(outputResult: SqOutputResult) { + return useState(() => { + // Pick the initial mode value - 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 (!outputResult.ok) { + return "Variables"; + } - 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 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 { output } = squiggleOutput; + const [mode, setMode] = useMode(output); return ( ( ref={viewerRef} > ( - - {Boolean(importsCount) && ( - - } - onClick={() => { - setMode("Imports"); - close(); - }} - /> - )} - {Boolean(variablesCount) && ( - - } - onClick={() => { - setMode("Variables"); - close(); - }} - /> - )} - {Boolean(exportsCount) && ( - - } - onClick={() => { - setMode("Exports"); - close(); - }} - /> - )} - - } - onClick={() => { - setMode("Result"); - close(); - }} - /> - - )} - > - - - } + menu={} indicator={ } - viewer={squiggleViewer} + viewer={ + + } /> ); } ); -SquiggleOutputViewer.displayName = "DynamicSquiggleViewer"; +SquiggleOutputViewer.displayName = "SquiggleOutputViewer"; diff --git a/packages/components/src/lib/hooks/useSquiggle.ts b/packages/components/src/lib/hooks/useSquiggle.ts index c3558e3922..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; @@ -53,7 +40,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/components/src/lib/utility.ts b/packages/components/src/lib/utility.ts index ef0056e15d..a7c6071af6 100644 --- a/packages/components/src/lib/utility.ts +++ b/packages/components/src/lib/utility.ts @@ -1,10 +1,4 @@ -import { - result, - resultMap, - SqDictValue, - SqError, - SqValue, -} from "@quri/squiggle-lang"; +import { result, SqValue } from "@quri/squiggle-lang"; import { SquiggleOutput } from "./hooks/useSquiggle.js"; @@ -42,35 +36,6 @@ export function some(arr: boolean[]): boolean { return arr.reduce((x, y) => 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/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"; 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)* 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; 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);