From f122b5fd7faa56683ccdcabe599a37c9a194a780 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 22 Mar 2022 13:33:28 +1100 Subject: [PATCH] Add squiggle notebooks --- packages/components/package.json | 5 +- packages/components/src/SquiggleChart.tsx | 22 +- packages/components/src/SquiggleEditor.tsx | 103 ++++++++ packages/components/src/index.ts | 1 + packages/components/webpack.config.js | 8 + packages/squiggle-lang/.gitignore | 1 + packages/squiggle-lang/__tests__/JS__Test.ts | 18 +- packages/squiggle-lang/src/js/index.ts | 10 +- .../src/rescript/ProgramEvaluator.res | 228 +++++++++--------- .../src/rescript/interpreter/ASTTypes.res | 3 +- .../symbolicDist/SymbolicDistTypes.res | 1 + .../src/rescript/utility/Operation.res | 4 +- packages/squiggle-lang/tsconfig.json | 1 + yarn.lock | 2 +- 14 files changed, 271 insertions(+), 136 deletions(-) create mode 100644 packages/components/src/SquiggleEditor.tsx diff --git a/packages/components/package.json b/packages/components/package.json index 3b6eb5b8..5648f69f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@quri/squiggle-components", - "version": "0.1.5", + "version": "0.1.6", "dependencies": { "@quri/squiggle-lang": "0.2.2", "@testing-library/jest-dom": "^5.16.2", @@ -72,7 +72,8 @@ "react-codejar": "^1.1.2", "ts-loader": "^9.2.8", "webpack": "^5.70.0", - "webpack-cli": "^4.9.2" + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4" }, "resolutions": { "@types/react": "17.0.39" diff --git a/packages/components/src/SquiggleChart.tsx b/packages/components/src/SquiggleChart.tsx index 277e55c5..a2527dcb 100644 --- a/packages/components/src/SquiggleChart.tsx +++ b/packages/components/src/SquiggleChart.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import * as _ from 'lodash'; +import _ from 'lodash'; import type { Spec } from 'vega'; -import { run } from '@squiggle/lang'; -import type { DistPlus, SamplingInputs } from '@squiggle/lang'; +import { run } from '@quri/squiggle-lang'; +import type { DistPlus, SamplingInputs, exportEnv, exportDistribution } from '@quri/squiggle-lang'; import { createClassFromSpec } from 'react-vega'; import * as chartSpecification from './spec-distributions.json' import * as percentilesSpec from './spec-pertentiles.json' @@ -26,7 +26,11 @@ export interface SquiggleChartProps { /** If the result is a function, where the function ends */ diagramStop? : number, /** If the result is a function, how many points along the function it samples */ - diagramCount? : number + diagramCount? : number, + /** variables declared before this expression */ + environment? : exportEnv, + /** When the environment changes */ + onEnvChange?(env: exportEnv): void } export const SquiggleChart : React.FC = props => { @@ -38,11 +42,13 @@ export const SquiggleChart : React.FC = props => { } - let result = run(props.squiggleString, samplingInputs); - console.log(result) + let result = run(props.squiggleString, samplingInputs, props.environment); if (result.tag === "Ok") { - let chartResults = result.value.map(chartResult => { - console.log(chartResult) + let environment = result.value.environment + let exports = result.value.exports + if(props.onEnvChange) + props.onEnvChange(environment) + let chartResults = exports.map((chartResult:exportDistribution )=> { if(chartResult["NAME"] === "Float"){ return ; } diff --git a/packages/components/src/SquiggleEditor.tsx b/packages/components/src/SquiggleEditor.tsx new file mode 100644 index 00000000..68e1d877 --- /dev/null +++ b/packages/components/src/SquiggleEditor.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { SquiggleChart } from './SquiggleChart' +import { ReactCodeJar } from "react-codejar"; +import type { exportEnv } from '@quri/squiggle-lang' + + +export interface SquiggleEditorProps { + /** The input string for squiggle */ + initialSquiggleString? : string, + + /** If the output requires monte carlo sampling, the amount of samples */ + sampleCount? : number, + /** The amount of points returned to draw the distribution */ + outputXYPoints? : number, + kernelWidth? : number, + pointDistLength? : number, + /** If the result is a function, where the function starts */ + diagramStart? : number, + /** If the result is a function, where the function ends */ + diagramStop? : number, + /** If the result is a function, how many points along the function it samples */ + diagramCount? : number, + /** The environment, other variables that were already declared */ + environment?: exportEnv, + /** when the environment changes. Used again for notebook magic*/ + onEnvChange?(env: exportEnv) : void +} + +const highlight = (editor: HTMLInputElement) => { + let code = editor.textContent; + code = code.replace(/\((\w+?)(\b)/g, '($1$2'); + editor.innerHTML = code; +}; + +interface SquiggleEditorState { + expression: string, + env: exportEnv +} + +export class SquiggleEditor extends React.Component{ + constructor(props: SquiggleEditorProps) { + super(props) + let code = props.initialSquiggleString ? props.initialSquiggleString : "" + this.state = {expression: code, env: props.environment } + + } + render() { + let {expression, env} = this.state + let props = this.props + return ( +
+ { + this.setState({expression: e}) + }} + style={{ + borderRadius: "6px", + width: "530px", + border: "1px solid grey", + fontFamily: "'Source Code Pro', monospace", + fontSize: "14px", + fontWeight: "400", + letterSpacing: "normal", + lineHeight: "20px", + padding: "10px", + tabSize: "4" + }} + highlight={highlight} + lineNumbers={false} + /> + +
+ ) + } +} + +export function renderSquiggleEditor(props : SquiggleEditorProps) { + let parent = document.createElement("div") + ReactDOM.render( { + + // I can set the value here because I need it for creating notebooks + // @ts-ignore + parent.value = env + + parent.dispatchEvent(new CustomEvent("input")) + if(props.onEnvChange) + props.onEnvChange(env) + }} /> , parent) + return parent +} diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 5c7c48df..16ded094 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1 +1,2 @@ export { SquiggleChart } from './SquiggleChart'; +export { SquiggleEditor, renderSquiggleEditor } from './SquiggleEditor'; diff --git a/packages/components/webpack.config.js b/packages/components/webpack.config.js index cdbd7df5..0e8fcd54 100644 --- a/packages/components/webpack.config.js +++ b/packages/components/webpack.config.js @@ -2,6 +2,7 @@ const path = require('path'); module.exports = { mode: 'production', + devtool: 'source-map', entry: './src/index.ts', module: { rules: [ @@ -23,4 +24,11 @@ module.exports = { type: 'umd', }, }, + devServer: { + static: { + directory: path.join(__dirname, 'public'), + }, + compress: true, + port: 9000, + }, }; diff --git a/packages/squiggle-lang/.gitignore b/packages/squiggle-lang/.gitignore index 34ac7ad8..ce7ea182 100644 --- a/packages/squiggle-lang/.gitignore +++ b/packages/squiggle-lang/.gitignore @@ -14,5 +14,6 @@ yarn-error.log .netlify .idea *.gen.ts +*.gen.tsx *.gen.js dist diff --git a/packages/squiggle-lang/__tests__/JS__Test.ts b/packages/squiggle-lang/__tests__/JS__Test.ts index 577476c1..ef80d367 100644 --- a/packages/squiggle-lang/__tests__/JS__Test.ts +++ b/packages/squiggle-lang/__tests__/JS__Test.ts @@ -1,19 +1,29 @@ import { run } from '../src/js/index'; +let testRun = (x: string) => { + let result = run(x) + if(result.tag == 'Ok'){ + return { tag: 'Ok', value: result.value.exports } + } + else { + return result + } +} + describe("A simple result", () => { test("mean(normal(5,2))", () => { - expect(run("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] }) + expect(testRun("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] }) }) test("10+10", () => { - let foo = run("10 + 10") + let foo = testRun("10 + 10") expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 20 } ] }) }) test("log(1) = 0", () => { - let foo = run("log(1)") + let foo = testRun("log(1)") expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 0} ]}) }) test("mm(0,0,[0,0,0])", () => { - let foo = run("mm(0,0,[0,0,0])") + let foo = testRun("mm(0,0,[0,0,0])") expect(foo).toEqual({ "tag": "Error", "value": "Function multimodal error: Too many weights provided" }) }) }); diff --git a/packages/squiggle-lang/src/js/index.ts b/packages/squiggle-lang/src/js/index.ts index dc54c532..ad148bdd 100644 --- a/packages/squiggle-lang/src/js/index.ts +++ b/packages/squiggle-lang/src/js/index.ts @@ -1,6 +1,6 @@ import {runAll} from '../rescript/ProgramEvaluator.gen'; -import type { Inputs_SamplingInputs_t as SamplingInputs } from '../rescript/ProgramEvaluator.gen'; -export type { SamplingInputs } +import type { Inputs_SamplingInputs_t as SamplingInputs,exportEnv, exportType, exportDistribution} from '../rescript/ProgramEvaluator.gen'; +export type { SamplingInputs , exportEnv, exportDistribution } export type {t as DistPlus} from '../rescript/pointSetDist/DistPlus.gen'; export let defaultSamplingInputs : SamplingInputs = { @@ -9,7 +9,9 @@ export let defaultSamplingInputs : SamplingInputs = { pointDistLength : 1000 } -export function run(squiggleString : string, samplingInputs? : SamplingInputs) { +export function run(squiggleString : string, samplingInputs? : SamplingInputs, environment?: exportEnv) : { tag: "Ok"; value: exportType } + | { tag: "Error"; value: string } { let si : SamplingInputs = samplingInputs ? samplingInputs : defaultSamplingInputs - return runAll(squiggleString, si) + let env : exportEnv = environment ? environment : [] + return runAll(squiggleString, si, env) } diff --git a/packages/squiggle-lang/src/rescript/ProgramEvaluator.res b/packages/squiggle-lang/src/rescript/ProgramEvaluator.res index 8ab8b4c8..b2317050 100644 --- a/packages/squiggle-lang/src/rescript/ProgramEvaluator.res +++ b/packages/squiggle-lang/src/rescript/ProgramEvaluator.res @@ -36,12 +36,20 @@ module Inputs = { } } -type exportType = [ +type exportDistribution = [ | #DistPlus(DistPlus.t) | #Float(float) | #Function((float) => Belt.Result.t) ] +type exportEnv = array<(string, ASTTypes.node)> + +type exportType = { + environment : exportEnv, + exports: array +} + + module Internals = { let addVariable = ( {samplingInputs, squiggleString, environment}: Inputs.inputs, @@ -71,135 +79,125 @@ module Internals = { let runNode = (inputs, node) => AST.toLeaf(makeInputs(inputs), inputs.environment, node) + let renderIfNeeded = (inputs: Inputs.inputs, node: ASTTypes.node): result< + ASTTypes.node, + string, + > => + node |> ( + x => + switch x { + | #Normalize(_) as n + | #SymbolicDist(_) as n => + #Render(n) + |> runNode(inputs) + |> ( + x => + switch x { + | Ok(#RenderedDist(_)) as r => r + | Error(r) => Error(r) + | _ => Error("Didn't render, but intended to") + } + ) + + | n => Ok(n) + } + ) + + let outputToDistPlus = (inputs: Inputs.inputs, pointSetDist: PointSetTypes.pointSetDist) => + DistPlus.make(~pointSetDist, ~squiggleString=Some(inputs.squiggleString), ()) + + let rec returnDist = (functionInfo : (array, ASTTypes.node), + inputs : Inputs.inputs, + env : ASTTypes.environment) => { + (input : float) => { + let foo: Inputs.inputs = {...inputs, environment: env}; + evaluateFunction( + foo, + functionInfo, + [#SymbolicDist(#Float(input))], + ) |> E.R.bind(_, a => + switch a { + | #DistPlus(d) => Ok(DistPlus.T.normalize(d)) + | n => + Js.log2("Error here", n) + Error("wrong type") + } + ) + } + } + // TODO: Consider using ExpressionTypes.ExpressionTree.getFloat or similar in this function + and coersionToExportedTypes = ( + inputs, + env: ASTTypes.environment, + ex: ASTTypes.node, + ): result => + ex + |> renderIfNeeded(inputs) + |> E.R.bind(_, x => + switch x { + | #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Ok(#Float(x)) + | #SymbolicDist(#Float(x)) => Ok(#Float(x)) + | #RenderedDist(n) => Ok(#DistPlus(outputToDistPlus(inputs, n))) + | #Function(n) => Ok(#Function(returnDist(n, inputs, env))) + | n => Error("Didn't output a rendered distribution. Format:" ++ AST.toString(n)) + } + ) + + and evaluateFunction = ( + inputs: Inputs.inputs, + fn: (array, ASTTypes.node), + fnInputs, + ) => { + let output = AST.runFunction( + makeInputs(inputs), + inputs.environment, + fnInputs, + fn, + ) + output |> E.R.bind(_, coersionToExportedTypes(inputs, inputs.environment)) + } + let runProgram = (inputs: Inputs.inputs, p: ASTTypes.program) => { let ins = ref(inputs) p - |> E.A.fmap(x => - switch x { - | #Assignment(name, node) => - ins := addVariable(ins.contents, name, node) - None - | #Expression(node) => - Some(runNode(ins.contents, node) |> E.R.fmap(r => (ins.contents.environment, r))) - } - ) - |> E.A.O.concatSomes - |> E.A.R.firstErrorOrOpen + |> E.A.fmap(x => + switch x { + | #Assignment(name, node) => + ins := addVariable(ins.contents, name, node) + None + | #Expression(node) => + Some(runNode(ins.contents, node)) + } + ) + |> E.A.O.concatSomes + |> E.A.R.firstErrorOrOpen + |> E.R.bind(_, d => + d + |> E.A.fmap(x => coersionToExportedTypes(inputs, ins.contents.environment, x)) + |> E.A.R.firstErrorOrOpen + ) + |> E.R.fmap(ex => + { + environment: Belt.Map.String.toArray(ins.contents.environment), + exports: ex + } + ) } let inputsToLeaf = (inputs: Inputs.inputs) => Parser.fromString(inputs.squiggleString) |> E.R.bind(_, g => runProgram(inputs, g)) - let outputToDistPlus = (inputs: Inputs.inputs, pointSetDist: PointSetTypes.pointSetDist) => - DistPlus.make(~pointSetDist, ~squiggleString=Some(inputs.squiggleString), ()) } -let renderIfNeeded = (inputs: Inputs.inputs, node: ASTTypes.node): result< - ASTTypes.node, - string, -> => - node |> ( - x => - switch x { - | #Normalize(_) as n - | #SymbolicDist(_) as n => - #Render(n) - |> Internals.runNode(inputs) - |> ( - x => - switch x { - | Ok(#RenderedDist(_)) as r => r - | Error(r) => Error(r) - | _ => Error("Didn't render, but intended to") - } - ) - - | n => Ok(n) - } - ) - -let rec returnDist = (functionInfo : (array, ASTTypes.node), - inputs : Inputs.inputs, - env : ASTTypes.environment) => { - (input : float) => { - let foo: Inputs.inputs = {...inputs, environment: env}; - evaluateFunction( - foo, - functionInfo, - [#SymbolicDist(#Float(input))], - ) |> E.R.bind(_, a => - switch a { - | #DistPlus(d) => Ok(DistPlus.T.normalize(d)) - | n => - Js.log2("Error here", n) - Error("wrong type") - } - ) - } -} -// TODO: Consider using ExpressionTypes.ExpressionTree.getFloat or similar in this function -and coersionToExportedTypes = ( - inputs, - env: ASTTypes.environment, - node: ASTTypes.node, -): result => - node - |> renderIfNeeded(inputs) - |> E.R.bind(_, x => - switch x { - | #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Ok(#Float(x)) - | #SymbolicDist(#Float(x)) => Ok(#Float(x)) - | #RenderedDist(n) => Ok(#DistPlus(Internals.outputToDistPlus(inputs, n))) - | #Function(n) => Ok(#Function(returnDist(n, inputs, env))) - | n => Error("Didn't output a rendered distribution. Format:" ++ AST.toString(n)) - } - ) - -and evaluateFunction = ( - inputs: Inputs.inputs, - fn: (array, ASTTypes.node), - fnInputs, -) => { - let output = AST.runFunction( - Internals.makeInputs(inputs), - inputs.environment, - fnInputs, - fn, - ) - output |> E.R.bind(_, coersionToExportedTypes(inputs, inputs.environment)) -} - - - - -let rec mapM = (f, xs) => - switch xs { - | [] => Ok([]) - | arr => - switch f(arr[0]) { - | Error(err) => Error(err) - | Ok(val) => - switch mapM(f, Belt.Array.sliceToEnd(arr, 1)) { - | Error(err) => Error(err) - | Ok(restList) => Ok(Belt.Array.concat([val], restList)) - } - } - } - -let evaluateProgram = (inputs: Inputs.inputs) => - inputs - |> Internals.inputsToLeaf - |> E.R.bind(_, xs => mapM(((a, b)) => coersionToExportedTypes(inputs, a, b), xs)) - @genType -let runAll = (squiggleString: string, samplingInputs: Inputs.SamplingInputs.t) => { +let runAll : (string, Inputs.SamplingInputs.t, exportEnv) => result = + (squiggleString, samplingInputs, environment) => { let inputs = Inputs.make( ~samplingInputs, ~squiggleString, - ~environment=[]->Belt.Map.String.fromArray, + ~environment=Belt.Map.String.fromArray(environment), (), ) - let response1 = evaluateProgram(inputs); - response1 + Internals.inputsToLeaf(inputs) } diff --git a/packages/squiggle-lang/src/rescript/interpreter/ASTTypes.res b/packages/squiggle-lang/src/rescript/interpreter/ASTTypes.res index 67bc4844..31217374 100644 --- a/packages/squiggle-lang/src/rescript/interpreter/ASTTypes.res +++ b/packages/squiggle-lang/src/rescript/interpreter/ASTTypes.res @@ -1,3 +1,4 @@ +@genType type rec hash = array<(string, node)> and node = [ | #SymbolicDist(SymbolicDistTypes.symbolicDist) @@ -229,4 +230,4 @@ module SamplingDistribution = { pointSetDist |> E.R.fmap(r => #Normalize(#RenderedDist(r))) }) } -} \ No newline at end of file +} diff --git a/packages/squiggle-lang/src/rescript/symbolicDist/SymbolicDistTypes.res b/packages/squiggle-lang/src/rescript/symbolicDist/SymbolicDistTypes.res index d2a6603e..4625867d 100644 --- a/packages/squiggle-lang/src/rescript/symbolicDist/SymbolicDistTypes.res +++ b/packages/squiggle-lang/src/rescript/symbolicDist/SymbolicDistTypes.res @@ -31,6 +31,7 @@ type triangular = { high: float, } +@genType type symbolicDist = [ | #Normal(normal) | #Beta(beta) diff --git a/packages/squiggle-lang/src/rescript/utility/Operation.res b/packages/squiggle-lang/src/rescript/utility/Operation.res index bbe69ef4..4eb2c3cd 100644 --- a/packages/squiggle-lang/src/rescript/utility/Operation.res +++ b/packages/squiggle-lang/src/rescript/utility/Operation.res @@ -1,5 +1,6 @@ // This file has no dependencies. It's used outside of the interpreter, but the interpreter depends on it. +@genType type algebraicOperation = [ | #Add | #Multiply @@ -7,6 +8,7 @@ type algebraicOperation = [ | #Divide | #Exponentiate ] +@genType type pointwiseOperation = [#Add | #Multiply | #Exponentiate] type scaleOperation = [#Multiply | #Exponentiate | #Log] type distToFloatOperation = [ @@ -109,4 +111,4 @@ module Truncate = { let right = right |> E.O.dimap(Js.Float.toString, () => "inf") j`truncate($nodeToString, $left, $right)` } -} \ No newline at end of file +} diff --git a/packages/squiggle-lang/tsconfig.json b/packages/squiggle-lang/tsconfig.json index 62422899..63f896f0 100644 --- a/packages/squiggle-lang/tsconfig.json +++ b/packages/squiggle-lang/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "jsx": "react", "allowJs": true, "noImplicitAny": true, "removeComments": true, diff --git a/yarn.lock b/yarn.lock index 23a9954e..e4d064e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18696,7 +18696,7 @@ webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.6.0, webpack-dev-server@^4.7.1: +webpack-dev-server@^4.6.0, webpack-dev-server@^4.7.1, webpack-dev-server@^4.7.4: version "4.7.4" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz#d0ef7da78224578384e795ac228d8efb63d5f945" integrity sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==