diff --git a/.gitignore b/.gitignore index 8f538f09..0712e779 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ yarn-error.log .vscode todo.txt result +shell.nix diff --git a/packages/components/package.json b/packages/components/package.json index e0a1abd2..6e0cba3f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -10,6 +10,7 @@ "@hookform/resolvers": "^2.9.8", "@quri/squiggle-lang": "^0.5.0", "@react-hook/size": "^2.1.2", + "@types/uuid": "^8.3.4", "clsx": "^1.2.1", "framer-motion": "^7.5.1", "lodash": "^4.17.21", @@ -18,6 +19,7 @@ "react-hook-form": "^7.36.1", "react-use": "^17.4.0", "react-vega": "^7.6.0", + "uuid": "^9.0.0", "vega": "^5.22.1", "vega-embed": "^6.21.0", "vega-lite": "^5.5.0", @@ -42,6 +44,7 @@ "@types/node": "^18.8.0", "@types/react": "^18.0.21", "@types/styled-components": "^5.1.26", + "@types/uuid": "^8.3.4", "@types/webpack": "^5.28.0", "canvas": "^2.10.1", "cross-env": "^7.0.3", @@ -82,7 +85,8 @@ "format": "prettier --write .", "prepack": "yarn run build:cjs && yarn run bundle", "test": "jest", - "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand" + "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand", + "test:profile": "node --cpu-prof node_modules/.bin/jest --runInBand" }, "eslintConfig": { "extends": [ diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 4dd57a1f..08c0c4ff 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -2,23 +2,21 @@ import * as React from "react"; import { SqValue, environment, - defaultEnvironment, resultMap, SqValueTag, + SqProject, } from "@quri/squiggle-lang"; import { useSquiggle } from "../lib/hooks"; import { SquiggleViewer } from "./SquiggleViewer"; import { JsImports } from "../lib/jsImports"; -export interface SquiggleChartProps { +export type SquiggleChartProps = { /** The input string for squiggle */ - code?: string; + code: string; /** Allows to re-run the code if code hasn't changed */ executionId?: number; /** If the output requires monte carlo sampling, the amount of samples */ sampleCount?: number; - /** The amount of points returned to draw the distribution */ - environment?: environment; /** If the result is a function, where the function domain starts */ diagramStart?: number; /** If the result is a function, where the function domain ends */ @@ -26,7 +24,7 @@ export interface SquiggleChartProps { /** If the result is a function, the amount of stops sampled */ diagramCount?: number; /** When the squiggle code gets reevaluated */ - onChange?(expr: SqValue | undefined): void; + onChange?(expr: SqValue | undefined, sourceName: string): void; /** CSS width of the element */ width?: number; height?: number; @@ -53,38 +51,69 @@ export interface SquiggleChartProps { /** Whether to show vega actions to the user, so they can copy the chart spec */ distributionChartActions?: boolean; enableLocalSettings?: boolean; -} +} & (StandaloneExecutionProps | ProjectExecutionProps); +// Props needed for a standalone execution +type StandaloneExecutionProps = { + project?: undefined; + continues?: undefined; + /** The amount of points returned to draw the distribution, not needed if using a project */ + environment?: environment; +}; + +// Props needed when executing inside a project. +type ProjectExecutionProps = { + environment?: undefined; + /** The project that this execution is part of */ + project: SqProject; + /** What other squiggle sources from the project to continue. Default [] */ + continues?: string[]; +}; const defaultOnChange = () => {}; const defaultImports: JsImports = {}; export const SquiggleChart: React.FC = React.memo( - ({ - code = "", - executionId = 0, - environment, - onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here - height = 200, - jsImports = defaultImports, - showSummary = false, - width, - logX = false, - expY = false, - diagramStart = 0, - diagramStop = 10, - diagramCount = 20, - tickFormat, - minX, - maxX, - color, - title, - xAxisType = "number", - distributionChartActions, - enableLocalSettings = false, - }) => { - const { result, bindings } = useSquiggle({ + (props: SquiggleChartProps) => { + const { + executionId = 0, + onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here + height = 200, + jsImports = defaultImports, + showSummary = false, + width, + logX = false, + expY = false, + diagramStart = 0, + diagramStop = 10, + diagramCount = 20, + tickFormat, + minX, + maxX, + color, + title, + xAxisType = "number", + distributionChartActions, + enableLocalSettings = false, + code, + continues = [], + } = props; + + const p = React.useMemo(() => { + if (props.project) { + return props.project; + } else { + const p = SqProject.create(); + if (props.environment) { + p.setEnvironment(props.environment); + } + return p; + } + }, [props.project, props.environment]); + + const { result, bindings } = useSquiggle({ + continues, + project: p, code, - environment, jsImports, onChange, executionId, @@ -120,7 +149,7 @@ export const SquiggleChart: React.FC = React.memo( height={height} distributionPlotSettings={distributionPlotSettings} chartSettings={chartSettings} - environment={environment ?? defaultEnvironment} + environment={p.getEnvironment()} enableLocalSettings={enableLocalSettings} /> ); diff --git a/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx index cb40c509..1c041dbf 100644 --- a/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx +++ b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx @@ -2,7 +2,7 @@ import React, { useContext } from "react"; import { SqDistributionTag, SqValue, SqValueTag } from "@quri/squiggle-lang"; import { NumberShower } from "../NumberShower"; import { DistributionChart, defaultPlot, makePlot } from "../DistributionChart"; -import { FunctionChart, FunctionChartSettings } from "../FunctionChart"; +import { FunctionChart } from "../FunctionChart"; import clsx from "clsx"; import { VariableBox } from "./VariableBox"; import { ItemSettingsMenu } from "./ItemSettingsMenu"; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index bbd0d178..b8a6e385 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,3 +1,4 @@ +export { SqProject } from "@quri/squiggle-lang/"; export { SquiggleChart } from "./components/SquiggleChart"; export { SquiggleEditor } from "./components/SquiggleEditor"; export { SquigglePlayground } from "./components/SquigglePlayground"; diff --git a/packages/components/src/lib/hooks/useSquiggle.ts b/packages/components/src/lib/hooks/useSquiggle.ts index 5916ac54..7f6fed96 100644 --- a/packages/components/src/lib/hooks/useSquiggle.ts +++ b/packages/components/src/lib/hooks/useSquiggle.ts @@ -1,42 +1,72 @@ -import { environment, SqProject, SqValue } from "@quri/squiggle-lang"; +import { SqProject, SqValue } from "@quri/squiggle-lang"; import { useEffect, useMemo } from "react"; import { JsImports, jsImportsToSquiggleCode } from "../jsImports"; +import * as uuid from "uuid"; type SquiggleArgs = { code: string; executionId?: number; jsImports?: JsImports; - environment?: environment; - onChange?: (expr: SqValue | undefined) => void; + project: SqProject; + continues: string[]; + onChange?: (expr: SqValue | undefined, sourceName: string) => void; }; +const importSourceName = (sourceName: string) => "imports-" + sourceName; + export const useSquiggle = (args: SquiggleArgs) => { + const sourceName = useMemo(() => uuid.v4(), []); + + const env = args.project.getEnvironment(); + const result = useMemo( () => { - const project = SqProject.create(); - project.setSource("main", args.code); - if (args.environment) { - project.setEnvironment(args.environment); - } + const project = args.project; + + project.setSource(sourceName, args.code); + let continues = args.continues; if (args.jsImports && Object.keys(args.jsImports).length) { const importsSource = jsImportsToSquiggleCode(args.jsImports); - project.setSource("imports", importsSource); - project.setContinues("main", ["imports"]); + project.setSource(importSourceName(sourceName), importsSource); + continues = args.continues.concat(importSourceName(sourceName)); } - project.run("main"); - const result = project.getResult("main"); - const bindings = project.getBindings("main"); + project.setContinues(sourceName, continues); + project.run(sourceName); + const result = project.getResult(sourceName); + const bindings = project.getBindings(sourceName); return { result, bindings }; }, + // This complains about executionId not being used inside the function body. + // This is on purpose, as executionId simply allows you to run the squiggle + // code again // eslint-disable-next-line react-hooks/exhaustive-deps - [args.code, args.environment, args.jsImports, args.executionId] + [ + args.code, + args.jsImports, + args.executionId, + sourceName, + args.continues, + args.project, + env, + ] ); const { onChange } = args; useEffect(() => { - onChange?.(result.result.tag === "Ok" ? result.result.value : undefined); - }, [result, onChange]); + onChange?.( + result.result.tag === "Ok" ? result.result.value : undefined, + sourceName + ); + }, [result, onChange, sourceName]); + + useEffect(() => { + return () => { + args.project.removeSource(sourceName); + if (args.project.getSource(importSourceName(sourceName))) + args.project.removeSource(importSourceName(sourceName)); + }; + }, [args.project, sourceName]); return result; }; diff --git a/packages/components/test/basic.test.tsx b/packages/components/test/basic.test.tsx index 9eb4973a..e77f104b 100644 --- a/packages/components/test/basic.test.tsx +++ b/packages/components/test/basic.test.tsx @@ -3,11 +3,11 @@ import React from "react"; import "@testing-library/jest-dom"; import { SquiggleChart } from "../src/index"; -test("Logs no warnings or errors", async () => { - debugger; +test("Logs nothing on render", async () => { const { unmount } = render(); unmount(); + expect(console.log).not.toBeCalled(); expect(console.warn).not.toBeCalled(); expect(console.error).not.toBeCalled(); }); diff --git a/packages/components/test/cleanup.test.tsx b/packages/components/test/cleanup.test.tsx new file mode 100644 index 00000000..d2be427a --- /dev/null +++ b/packages/components/test/cleanup.test.tsx @@ -0,0 +1,39 @@ +import { render } from "@testing-library/react"; +import React from "react"; +import "@testing-library/jest-dom"; +import { SquiggleChart } from "../src/index"; +import { SqProject } from "@quri/squiggle-lang"; + +test("Creates and cleans up source", async () => { + const project = SqProject.create(); + + const { unmount } = render( + + ); + + expect(project.getSourceIds().length).toBe(1); + + const sourceId = project.getSourceIds()[0]; + expect(project.getSource(sourceId)).toBe("normal(0, 1)"); + + unmount(); + expect(project.getSourceIds().length).toBe(0); + expect(project.getSource(sourceId)).toBe(undefined); +}); + +test("Creates and cleans up source and imports", async () => { + const project = SqProject.create(); + + const { unmount } = render( + + ); + + expect(project.getSourceIds().length).toBe(2); + + unmount(); + expect(project.getSourceIds()).toStrictEqual([]); +}); diff --git a/yarn.lock b/yarn.lock index 9e4d2db1..2292e288 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5186,6 +5186,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/vscode@^1.70.0": version "1.71.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.71.0.tgz#a8d9bb7aca49b0455060e6eb978711b510bdd2e2" @@ -18723,6 +18728,11 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"