Merge pull request #902 from quantified-uncertainty/develop
Develop -> Master July 28 2022
This commit is contained in:
commit
3613a754c5
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -9,8 +9,8 @@
|
||||||
# This also holds true for GitHub teams.
|
# This also holds true for GitHub teams.
|
||||||
|
|
||||||
# Rescript
|
# Rescript
|
||||||
*.res @OAGr @quinn-dougherty
|
*.res @OAGr
|
||||||
*.resi @OAGr @quinn-dougherty
|
*.resi @OAGr
|
||||||
|
|
||||||
# Typescript
|
# Typescript
|
||||||
*.tsx @Hazelfire @OAGr
|
*.tsx @Hazelfire @OAGr
|
||||||
|
|
61
.github/workflows/ci.yml
vendored
61
.github/workflows/ci.yml
vendored
|
@ -19,6 +19,8 @@ jobs:
|
||||||
should_skip_lang: ${{ steps.skip_lang_check.outputs.should_skip }}
|
should_skip_lang: ${{ steps.skip_lang_check.outputs.should_skip }}
|
||||||
should_skip_components: ${{ steps.skip_components_check.outputs.should_skip }}
|
should_skip_components: ${{ steps.skip_components_check.outputs.should_skip }}
|
||||||
should_skip_website: ${{ steps.skip_website_check.outputs.should_skip }}
|
should_skip_website: ${{ steps.skip_website_check.outputs.should_skip }}
|
||||||
|
should_skip_vscodeext: ${{ steps.skip_vscodeext_check.outputs.should_skip }}
|
||||||
|
should_skip_cli: ${{ steps.skip_cli_check.outputs.should_skip }}
|
||||||
steps:
|
steps:
|
||||||
- id: skip_lang_check
|
- id: skip_lang_check
|
||||||
name: Check if the changes are about squiggle-lang src files
|
name: Check if the changes are about squiggle-lang src files
|
||||||
|
@ -35,6 +37,16 @@ jobs:
|
||||||
uses: fkirc/skip-duplicate-actions@v3.4.1
|
uses: fkirc/skip-duplicate-actions@v3.4.1
|
||||||
with:
|
with:
|
||||||
paths: '["packages/website/**"]'
|
paths: '["packages/website/**"]'
|
||||||
|
- id: skip_vscodeext_check
|
||||||
|
name: Check if the changes are about vscode extension src files
|
||||||
|
uses: fkirc/skip-duplicate-actions@v3.4.1
|
||||||
|
with:
|
||||||
|
paths: '["packages/vscode-ext/**"]'
|
||||||
|
- id: skip_cli_check
|
||||||
|
name: Check if the changes are about cli src files
|
||||||
|
uses: fkirc/skip-duplicate-actions@v3.4.1
|
||||||
|
with:
|
||||||
|
paths: '["packages/cli/**"]'
|
||||||
|
|
||||||
lang-lint:
|
lang-lint:
|
||||||
name: Language lint
|
name: Language lint
|
||||||
|
@ -158,3 +170,52 @@ jobs:
|
||||||
run: cd ../components && yarn build
|
run: cd ../components && yarn build
|
||||||
- name: Build website assets
|
- name: Build website assets
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
|
||||||
|
vscode-ext-lint:
|
||||||
|
name: VS Code extension lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: pre_check
|
||||||
|
if: ${{ needs.pre_check.outputs.should_skip_vscodeext != 'true' }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: packages/vscode-ext
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install dependencies from monorepo level
|
||||||
|
run: cd ../../ && yarn
|
||||||
|
- name: Lint the VSCode Extension source code
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
vscode-ext-build:
|
||||||
|
name: VS Code extension build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: pre_check
|
||||||
|
if: ${{ (needs.pre_check.outputs.should_skip_components != 'true') || (needs.pre_check.outputs.should_skip_lang != 'true') }} || (needs.pre_check.outputs.should_skip_vscodeext != 'true') }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: packages/vscode-ext
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install dependencies from monorepo level
|
||||||
|
run: cd ../../ && yarn
|
||||||
|
- name: Build
|
||||||
|
run: yarn compile
|
||||||
|
|
||||||
|
cli-lint:
|
||||||
|
name: CLI lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: pre_check
|
||||||
|
if: ${{ needs.pre_check.outputs.should_skip_cli != 'true' }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: packages/cli
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Check javascript, typescript, and markdown lint
|
||||||
|
uses: creyD/prettier_action@v4.2
|
||||||
|
with:
|
||||||
|
dry: true
|
||||||
|
prettier_options: --check packages/cli
|
||||||
|
|
|
@ -12,3 +12,4 @@ packages/squiggle-lang/coverage/
|
||||||
packages/squiggle-lang/.cache/
|
packages/squiggle-lang/.cache/
|
||||||
packages/website/build/
|
packages/website/build/
|
||||||
packages/squiggle-lang/src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
|
packages/squiggle-lang/src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
|
||||||
|
packages/vscode-ext/media/vendor/
|
||||||
|
|
|
@ -17,6 +17,7 @@ _An estimation language_.
|
||||||
- [Known bugs](https://www.squiggle-language.com/docs/Discussions/Bugs)
|
- [Known bugs](https://www.squiggle-language.com/docs/Discussions/Bugs)
|
||||||
- [Original lesswrong sequence](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3)
|
- [Original lesswrong sequence](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3)
|
||||||
- [Author your squiggle models as Observable notebooks](https://observablehq.com/@hazelfire/squiggle)
|
- [Author your squiggle models as Observable notebooks](https://observablehq.com/@hazelfire/squiggle)
|
||||||
|
- [Use squiggle in VS Code](https://marketplace.visualstudio.com/items?itemName=QURI.vscode-squiggle)
|
||||||
|
|
||||||
## Our deployments
|
## Our deployments
|
||||||
|
|
||||||
|
@ -39,6 +40,8 @@ the packages can be found in `packages`.
|
||||||
of the calculation.
|
of the calculation.
|
||||||
- `packages/website` is the main descriptive website for squiggle,
|
- `packages/website` is the main descriptive website for squiggle,
|
||||||
it is hosted at `squiggle-language.com`.
|
it is hosted at `squiggle-language.com`.
|
||||||
|
- `packages/vscode-ext` is the VS Code extension for writing estimation functions.
|
||||||
|
- `packages/cli` is an experimental way of using imports in squiggle, which is also on [npm](https://www.npmjs.com/package/squiggle-cli-experimental).
|
||||||
|
|
||||||
# Develop
|
# Develop
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"lint:all": "prettier --check . && cd packages/squiggle-lang && yarn lint:rescript"
|
"lint:all": "prettier --check . && cd packages/squiggle-lang && yarn lint:rescript"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.6.2"
|
"prettier": "^2.7.1"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
4
packages/cli/.gitignore
vendored
Normal file
4
packages/cli/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
## Artifacts
|
||||||
|
*.swp
|
||||||
|
/node_modules/
|
||||||
|
yarn-error.log
|
22
packages/cli/README.md
Normal file
22
packages/cli/README.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
## Squiggle CLI
|
||||||
|
|
||||||
|
This package can be used to incorporate a very simple `import` system into Squiggle.
|
||||||
|
|
||||||
|
To use, write special files with a `.squiggleU` file type. In these files, you can write lines like,
|
||||||
|
|
||||||
|
```
|
||||||
|
@import(models/gdp_over_time.squiggle, gdpOverTime)
|
||||||
|
gdpOverTime(2.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
The imports will be replaced with the contents of the file in `models/gdp_over_time.squiggle` upon compilation. The `.squiggleU` file will be converted into a `.squiggle` file with the `import` statement having this replacement.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
### `npx squiggle-cli-experimental compile`
|
||||||
|
|
||||||
|
Runs compilation in the current directory and all of its subdirectories.
|
||||||
|
|
||||||
|
### `npx squiggle-cli-experimental watch`
|
||||||
|
|
||||||
|
Watches `.squiggleU` files in the current directory (and subdirectories) and rebuilds them when they are saved. Note that this will _not_ rebuild files when their dependencies are changed, just when they are changed directly.
|
96
packages/cli/index.js
Executable file
96
packages/cli/index.js
Executable file
|
@ -0,0 +1,96 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import indentString from "indent-string";
|
||||||
|
import chokidar from "chokidar";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { Command } from "commander";
|
||||||
|
import glob from "glob";
|
||||||
|
|
||||||
|
const processFile = (fileName, seen = []) => {
|
||||||
|
const normalizedFileName = path.resolve(fileName);
|
||||||
|
if (seen.includes(normalizedFileName)) {
|
||||||
|
throw new Error(`Recursive dependency for file ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContents = fs.readFileSync(fileName, "utf-8");
|
||||||
|
if (!fileName.endsWith(".squiggleU")) {
|
||||||
|
return fileContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /\@import\(\s*([^)]+?)\s*\)/g;
|
||||||
|
const matches = Array.from(fileContents.matchAll(regex)).map((r) =>
|
||||||
|
r[1].split(/\s*,\s*/)
|
||||||
|
);
|
||||||
|
const newContent = fileContents.replaceAll(regex, "");
|
||||||
|
const appendings = [];
|
||||||
|
|
||||||
|
matches.forEach((r) => {
|
||||||
|
const importFileName = r[0];
|
||||||
|
const rename = r[1];
|
||||||
|
const item = fs.statSync(importFileName);
|
||||||
|
if (item.isFile()) {
|
||||||
|
const data = processFile(importFileName, [...seen, normalizedFileName]);
|
||||||
|
if (data) {
|
||||||
|
const importString = `${rename} = {\n${indentString(data, 2)}\n}\n`;
|
||||||
|
appendings.push(importString);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
chalk.red(`Import Error`) +
|
||||||
|
`: ` +
|
||||||
|
chalk.cyan(importFileName) +
|
||||||
|
` not found in file ` +
|
||||||
|
chalk.cyan(fileName) +
|
||||||
|
`. Make sure the @import file names all exist in this repo.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const imports = appendings.join("\n");
|
||||||
|
|
||||||
|
const newerContent = imports.concat(newContent);
|
||||||
|
return newerContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const run = (fileName) => {
|
||||||
|
const content = processFile(fileName);
|
||||||
|
const parsedPath = path.parse(path.resolve(fileName));
|
||||||
|
const newFilename = `${parsedPath.dir}/${parsedPath.name}.squiggle`;
|
||||||
|
fs.writeFileSync(newFilename, content);
|
||||||
|
console.log(chalk.cyan(`Updated ${fileName} -> ${newFilename}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const compile = () => {
|
||||||
|
glob("**/*.squiggleU", (_err, files) => {
|
||||||
|
files.forEach(run);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const watch = () => {
|
||||||
|
chokidar
|
||||||
|
.watch("**.squiggleU")
|
||||||
|
.on("ready", () => console.log(chalk.green("Ready!")))
|
||||||
|
.on("change", (event, _) => {
|
||||||
|
run(event);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name("squiggle-utils")
|
||||||
|
.description("CLI to transform squiggle files with @imports")
|
||||||
|
.version("0.0.1");
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("watch")
|
||||||
|
.description("watch files and compile on the fly")
|
||||||
|
.action(watch);
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("compile")
|
||||||
|
.description("compile all .squiggleU files into .squiggle files")
|
||||||
|
.action(compile);
|
||||||
|
|
||||||
|
program.parse();
|
21
packages/cli/package.json
Normal file
21
packages/cli/package.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "squiggle-cli-experimental",
|
||||||
|
"version": "0.0.3",
|
||||||
|
"main": "index.js",
|
||||||
|
"homepage": "https://squiggle-language.com",
|
||||||
|
"author": "Quantified Uncertainty Research Institute",
|
||||||
|
"bin": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ."
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.0.1",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
|
"commander": "^9.4.0",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"glob": "^8.0.3",
|
||||||
|
"indent-string": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,11 +20,47 @@ Add to `App.js`:
|
||||||
```jsx
|
```jsx
|
||||||
import { SquiggleEditor } from "@quri/squiggle-components";
|
import { SquiggleEditor } from "@quri/squiggle-components";
|
||||||
<SquiggleEditor
|
<SquiggleEditor
|
||||||
initialSquiggleString="x = beta($alpha, 10); x + $shift"
|
defaultCode="x = beta($alpha, 10); x + $shift"
|
||||||
jsImports={{ alpha: 3, shift: 20 }}
|
jsImports={{ alpha: 3, shift: 20 }}
|
||||||
/>;
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Usage in a Nextjs project
|
||||||
|
|
||||||
|
For now, `squiggle-components` requires the `window` property, so using the package in nextjs requires dynamic loading:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { SquiggleChart } from "@quri/squiggle-components";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
const SquiggleChart = dynamic(
|
||||||
|
() => import("@quri/squiggle-components").then((mod) => mod.SquiggleChart),
|
||||||
|
{
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function DynamicSquiggleChart({ squiggleString }) {
|
||||||
|
if (squiggleString == "") {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<SquiggleChart
|
||||||
|
defaultCode={squiggleString}
|
||||||
|
width={445}
|
||||||
|
height={200}
|
||||||
|
showSummary={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
# Build storybook for development
|
# Build storybook for development
|
||||||
|
|
||||||
We assume that you had run `yarn` at monorepo level, installing dependencies.
|
We assume that you had run `yarn` at monorepo level, installing dependencies.
|
||||||
|
|
|
@ -1,65 +1,75 @@
|
||||||
{
|
{
|
||||||
"name": "@quri/squiggle-components",
|
"name": "@quri/squiggle-components",
|
||||||
"version": "0.2.20",
|
"version": "0.2.24",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.6.4",
|
"@floating-ui/react-dom": "^0.7.2",
|
||||||
|
"@floating-ui/react-dom-interactions": "^0.6.6",
|
||||||
|
"@headlessui/react": "^1.6.6",
|
||||||
"@heroicons/react": "^1.0.6",
|
"@heroicons/react": "^1.0.6",
|
||||||
"@hookform/resolvers": "^2.9.1",
|
"@hookform/resolvers": "^2.9.6",
|
||||||
"@quri/squiggle-lang": "^0.2.8",
|
"@quri/squiggle-lang": "^0.2.8",
|
||||||
"@react-hook/size": "^2.1.2",
|
"@react-hook/size": "^2.1.2",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.2.1",
|
||||||
|
"framer-motion": "^6.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-hook-form": "^7.33.1",
|
||||||
"react-hook-form": "^7.32.0",
|
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"react-vega": "^7.5.1",
|
"react-vega": "^7.6.0",
|
||||||
"vega": "^5.22.1",
|
"vega": "^5.22.1",
|
||||||
"vega-embed": "^6.20.6",
|
"vega-embed": "^6.21.0",
|
||||||
"vega-lite": "^5.2.0",
|
"vega-lite": "^5.3.0",
|
||||||
|
"vscode-uri": "^3.0.3",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.17.12",
|
"@babel/plugin-proposal-private-property-in-object": "^7.18.6",
|
||||||
"@storybook/addon-actions": "^6.5.8",
|
"@storybook/addon-actions": "^6.5.9",
|
||||||
"@storybook/addon-essentials": "^6.5.8",
|
"@storybook/addon-essentials": "^6.5.9",
|
||||||
"@storybook/addon-links": "^6.5.8",
|
"@storybook/addon-links": "^6.5.9",
|
||||||
"@storybook/builder-webpack5": "^6.5.8",
|
"@storybook/builder-webpack5": "^6.5.9",
|
||||||
"@storybook/manager-webpack5": "^6.5.8",
|
"@storybook/manager-webpack5": "^6.5.9",
|
||||||
"@storybook/node-logger": "^6.5.6",
|
"@storybook/node-logger": "^6.5.9",
|
||||||
"@storybook/preset-create-react-app": "^4.1.2",
|
"@storybook/preset-create-react-app": "^4.1.2",
|
||||||
"@storybook/react": "^6.5.8",
|
"@storybook/react": "^6.5.9",
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^14.2.0",
|
"@testing-library/user-event": "^14.3.0",
|
||||||
"@types/jest": "^27.5.0",
|
"@types/jest": "^27.5.0",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/node": "^17.0.42",
|
"@types/node": "^18.6.1",
|
||||||
"@types/react": "^18.0.9",
|
"@types/react": "^18.0.9",
|
||||||
"@types/react-dom": "^18.0.5",
|
|
||||||
"@types/styled-components": "^5.1.24",
|
"@types/styled-components": "^5.1.24",
|
||||||
"@types/webpack": "^5.28.0",
|
"@types/webpack": "^5.28.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"mini-css-extract-plugin": "^2.6.0",
|
"mini-css-extract-plugin": "^2.6.1",
|
||||||
"postcss-cli": "^9.1.0",
|
"postcss-cli": "^10.0.0",
|
||||||
"postcss-import": "^14.1.0",
|
"postcss-import": "^14.1.0",
|
||||||
"postcss-loader": "^7.0.0",
|
"postcss-loader": "^7.0.1",
|
||||||
|
"react": "^18.1.0",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"tailwindcss": "^3.1.2",
|
"tailwindcss": "^3.1.6",
|
||||||
"ts-loader": "^9.3.0",
|
"ts-loader": "^9.3.0",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||||
"typescript": "^4.7.3",
|
"typescript": "^4.7.4",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^4.10.0",
|
"webpack-cli": "^4.10.0",
|
||||||
"webpack-dev-server": "^4.9.2"
|
"webpack-dev-server": "^4.9.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18",
|
||||||
|
"react-dom": "^16.8.0 || ^17 || ^18"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
|
"start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
|
||||||
"build": "tsc -b && postcss ./src/styles/main.css -o ./dist/main.css && build-storybook -s public",
|
"build:cjs": "tsc -b",
|
||||||
|
"build:css": "postcss ./src/styles/main.css -o ./dist/main.css",
|
||||||
|
"build:storybook": "build-storybook -s public",
|
||||||
|
"build": "yarn run build:cjs && yarn run build:css && yarn run build:storybook",
|
||||||
"bundle": "webpack",
|
"bundle": "webpack",
|
||||||
"all": "yarn bundle && yarn build",
|
"all": "yarn bundle && yarn build",
|
||||||
"lint": "prettier --check .",
|
"lint": "prettier --check .",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { FC } from "react";
|
import React, { FC, useMemo, useRef } from "react";
|
||||||
import AceEditor from "react-ace";
|
import AceEditor from "react-ace";
|
||||||
|
|
||||||
import "ace-builds/src-noconflict/mode-golang";
|
import "ace-builds/src-noconflict/mode-golang";
|
||||||
|
@ -8,6 +8,7 @@ import "ace-builds/src-noconflict/theme-github";
|
||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
onSubmit?: () => void;
|
||||||
oneLine?: boolean;
|
oneLine?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
@ -17,18 +18,24 @@ interface CodeEditorProps {
|
||||||
export const CodeEditor: FC<CodeEditorProps> = ({
|
export const CodeEditor: FC<CodeEditorProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
onSubmit,
|
||||||
oneLine = false,
|
oneLine = false,
|
||||||
showGutter = false,
|
showGutter = false,
|
||||||
height,
|
height,
|
||||||
}) => {
|
}) => {
|
||||||
let lineCount = value.split("\n").length;
|
const lineCount = value.split("\n").length;
|
||||||
let id = _.uniqueId();
|
const id = useMemo(() => _.uniqueId(), []);
|
||||||
|
|
||||||
|
// this is necessary because AceEditor binds commands on mount, see https://github.com/securingsincity/react-ace/issues/684
|
||||||
|
const onSubmitRef = useRef<typeof onSubmit | null>(null);
|
||||||
|
onSubmitRef.current = onSubmit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AceEditor
|
<AceEditor
|
||||||
value={value}
|
value={value}
|
||||||
mode="golang"
|
mode="golang"
|
||||||
theme="github"
|
theme="github"
|
||||||
width={"100%"}
|
width="100%"
|
||||||
fontSize={14}
|
fontSize={14}
|
||||||
height={String(height) + "px"}
|
height={String(height) + "px"}
|
||||||
minLines={oneLine ? lineCount : undefined}
|
minLines={oneLine ? lineCount : undefined}
|
||||||
|
@ -45,6 +52,13 @@ export const CodeEditor: FC<CodeEditorProps> = ({
|
||||||
enableBasicAutocompletion: false,
|
enableBasicAutocompletion: false,
|
||||||
enableLiveAutocompletion: false,
|
enableLiveAutocompletion: false,
|
||||||
}}
|
}}
|
||||||
|
commands={[
|
||||||
|
{
|
||||||
|
name: "submit",
|
||||||
|
bindKey: { mac: "Cmd-Enter", win: "Ctrl-Enter" },
|
||||||
|
exec: () => onSubmitRef.current?.(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,39 +5,38 @@ import {
|
||||||
distributionError,
|
distributionError,
|
||||||
distributionErrorToString,
|
distributionErrorToString,
|
||||||
} from "@quri/squiggle-lang";
|
} from "@quri/squiggle-lang";
|
||||||
import { Vega, VisualizationSpec } from "react-vega";
|
import { Vega } from "react-vega";
|
||||||
import * as chartSpecification from "../vega-specs/spec-distributions.json";
|
|
||||||
import { ErrorAlert } from "./Alert";
|
import { ErrorAlert } from "./Alert";
|
||||||
import { useSize } from "react-use";
|
import { useSize } from "react-use";
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
linearXScale,
|
buildVegaSpec,
|
||||||
logXScale,
|
DistributionChartSpecOptions,
|
||||||
linearYScale,
|
} from "../lib/distributionSpecBuilder";
|
||||||
expYScale,
|
|
||||||
} from "./DistributionVegaScales";
|
|
||||||
import { NumberShower } from "./NumberShower";
|
import { NumberShower } from "./NumberShower";
|
||||||
|
import { hasMassBelowZero } from "../lib/distributionUtils";
|
||||||
|
|
||||||
type DistributionChartProps = {
|
export type DistributionPlottingSettings = {
|
||||||
|
/** Whether to show a summary of means, stdev, percentiles etc */
|
||||||
|
showSummary: boolean;
|
||||||
|
actions?: boolean;
|
||||||
|
} & DistributionChartSpecOptions;
|
||||||
|
|
||||||
|
export type DistributionChartProps = {
|
||||||
distribution: Distribution;
|
distribution: Distribution;
|
||||||
width?: number;
|
width?: number;
|
||||||
height: number;
|
height: number;
|
||||||
/** Whether to show a summary of means, stdev, percentiles etc */
|
} & DistributionPlottingSettings;
|
||||||
showSummary: boolean;
|
|
||||||
/** Whether to show the user graph controls (scale etc) */
|
|
||||||
showControls?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DistributionChart: React.FC<DistributionChartProps> = ({
|
export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
||||||
|
const {
|
||||||
distribution,
|
distribution,
|
||||||
height,
|
height,
|
||||||
showSummary,
|
showSummary,
|
||||||
width,
|
width,
|
||||||
showControls = false,
|
logX,
|
||||||
}) => {
|
actions = false,
|
||||||
const [isLogX, setLogX] = React.useState(false);
|
} = props;
|
||||||
const [isExpY, setExpY] = React.useState(false);
|
|
||||||
const shape = distribution.pointSet();
|
const shape = distribution.pointSet();
|
||||||
const [sized] = useSize((size) => {
|
const [sized] = useSize((size) => {
|
||||||
if (shape.tag === "Error") {
|
if (shape.tag === "Error") {
|
||||||
|
@ -48,10 +47,7 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const massBelow0 =
|
const spec = buildVegaSpec(props);
|
||||||
shape.value.continuous.some((x) => x.x <= 0) ||
|
|
||||||
shape.value.discrete.some((x) => x.x <= 0);
|
|
||||||
const spec = buildVegaSpec(isLogX, isExpY);
|
|
||||||
|
|
||||||
let widthProp = width ? width : size.width;
|
let widthProp = width ? width : size.width;
|
||||||
if (widthProp < 20) {
|
if (widthProp < 20) {
|
||||||
|
@ -63,79 +59,28 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: widthProp }}>
|
<div style={{ width: widthProp }}>
|
||||||
|
{logX && hasMassBelowZero(shape.value) ? (
|
||||||
|
<ErrorAlert heading="Log Domain Error">
|
||||||
|
Cannot graph distribution with negative values on logarithmic scale.
|
||||||
|
</ErrorAlert>
|
||||||
|
) : (
|
||||||
<Vega
|
<Vega
|
||||||
spec={spec}
|
spec={spec}
|
||||||
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
|
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
|
||||||
width={widthProp - 10}
|
width={widthProp - 10}
|
||||||
height={height}
|
height={height}
|
||||||
actions={false}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{showSummary && <SummaryTable distribution={distribution} />}
|
{showSummary && <SummaryTable distribution={distribution} />}
|
||||||
</div>
|
</div>
|
||||||
{showControls && (
|
|
||||||
<div>
|
|
||||||
<CheckBox
|
|
||||||
label="Log X scale"
|
|
||||||
value={isLogX}
|
|
||||||
onChange={setLogX}
|
|
||||||
// Check whether we should disable the checkbox
|
|
||||||
{...(massBelow0
|
|
||||||
? {
|
|
||||||
disabled: true,
|
|
||||||
tooltip:
|
|
||||||
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.",
|
|
||||||
}
|
|
||||||
: {})}
|
|
||||||
/>
|
|
||||||
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return sized;
|
return sized;
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildVegaSpec(isLogX: boolean, isExpY: boolean): VisualizationSpec {
|
|
||||||
return {
|
|
||||||
...chartSpecification,
|
|
||||||
scales: [
|
|
||||||
isLogX ? logXScale : linearXScale,
|
|
||||||
isExpY ? expYScale : linearYScale,
|
|
||||||
],
|
|
||||||
} as VisualizationSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CheckBoxProps {
|
|
||||||
label: string;
|
|
||||||
onChange: (x: boolean) => void;
|
|
||||||
value: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
tooltip?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CheckBox: React.FC<CheckBoxProps> = ({
|
|
||||||
label,
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
disabled = false,
|
|
||||||
tooltip,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<span title={tooltip}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
value={value + ""}
|
|
||||||
onChange={() => onChange(!value)}
|
|
||||||
disabled={disabled}
|
|
||||||
className="form-checkbox"
|
|
||||||
/>
|
|
||||||
<label className={clsx(disabled && "text-slate-400")}> {label}</label>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
|
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => (
|
}) => (
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { lambdaValue, environment, runForeign } from "@quri/squiggle-lang";
|
import {
|
||||||
|
lambdaValue,
|
||||||
|
environment,
|
||||||
|
runForeign,
|
||||||
|
errorValueToString,
|
||||||
|
} from "@quri/squiggle-lang";
|
||||||
import { FunctionChart1Dist } from "./FunctionChart1Dist";
|
import { FunctionChart1Dist } from "./FunctionChart1Dist";
|
||||||
import { FunctionChart1Number } from "./FunctionChart1Number";
|
import { FunctionChart1Number } from "./FunctionChart1Number";
|
||||||
|
import { DistributionPlottingSettings } from "./DistributionChart";
|
||||||
import { ErrorAlert, MessageAlert } from "./Alert";
|
import { ErrorAlert, MessageAlert } from "./Alert";
|
||||||
|
|
||||||
export type FunctionChartSettings = {
|
export type FunctionChartSettings = {
|
||||||
|
@ -13,6 +19,7 @@ export type FunctionChartSettings = {
|
||||||
interface FunctionChartProps {
|
interface FunctionChartProps {
|
||||||
fn: lambdaValue;
|
fn: lambdaValue;
|
||||||
chartSettings: FunctionChartSettings;
|
chartSettings: FunctionChartSettings;
|
||||||
|
distributionPlotSettings: DistributionPlottingSettings;
|
||||||
environment: environment;
|
environment: environment;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +28,7 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
|
||||||
fn,
|
fn,
|
||||||
chartSettings,
|
chartSettings,
|
||||||
environment,
|
environment,
|
||||||
|
distributionPlotSettings,
|
||||||
height,
|
height,
|
||||||
}) => {
|
}) => {
|
||||||
if (fn.parameters.length > 1) {
|
if (fn.parameters.length > 1) {
|
||||||
|
@ -42,10 +50,16 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const validResult = getValidResult();
|
const validResult = getValidResult();
|
||||||
const resultType =
|
|
||||||
validResult.tag === "Ok" ? validResult.value.tag : ("Error" as const);
|
|
||||||
|
|
||||||
switch (resultType) {
|
if (validResult.tag === "Error") {
|
||||||
|
return (
|
||||||
|
<ErrorAlert heading="Error">
|
||||||
|
{errorValueToString(validResult.value)}
|
||||||
|
</ErrorAlert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (validResult.value.tag) {
|
||||||
case "distribution":
|
case "distribution":
|
||||||
return (
|
return (
|
||||||
<FunctionChart1Dist
|
<FunctionChart1Dist
|
||||||
|
@ -53,6 +67,7 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
|
||||||
chartSettings={chartSettings}
|
chartSettings={chartSettings}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
height={height}
|
height={height}
|
||||||
|
distributionPlotSettings={distributionPlotSettings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "number":
|
case "number":
|
||||||
|
@ -64,15 +79,11 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
|
||||||
height={height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "Error":
|
|
||||||
return (
|
|
||||||
<ErrorAlert heading="Error">The function failed to be run</ErrorAlert>
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<MessageAlert heading="Function Display Not Supported">
|
<MessageAlert heading="Function Display Not Supported">
|
||||||
There is no function visualization for this type of output:{" "}
|
There is no function visualization for this type of output:{" "}
|
||||||
<span className="font-bold">{resultType}</span>
|
<span className="font-bold">{validResult.value.tag}</span>
|
||||||
</MessageAlert>
|
</MessageAlert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,10 @@ import {
|
||||||
} from "@quri/squiggle-lang";
|
} from "@quri/squiggle-lang";
|
||||||
import { createClassFromSpec } from "react-vega";
|
import { createClassFromSpec } from "react-vega";
|
||||||
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
|
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
|
||||||
import { DistributionChart } from "./DistributionChart";
|
import {
|
||||||
|
DistributionChart,
|
||||||
|
DistributionPlottingSettings,
|
||||||
|
} from "./DistributionChart";
|
||||||
import { NumberShower } from "./NumberShower";
|
import { NumberShower } from "./NumberShower";
|
||||||
import { ErrorAlert } from "./Alert";
|
import { ErrorAlert } from "./Alert";
|
||||||
|
|
||||||
|
@ -44,6 +47,7 @@ export type FunctionChartSettings = {
|
||||||
interface FunctionChart1DistProps {
|
interface FunctionChart1DistProps {
|
||||||
fn: lambdaValue;
|
fn: lambdaValue;
|
||||||
chartSettings: FunctionChartSettings;
|
chartSettings: FunctionChartSettings;
|
||||||
|
distributionPlotSettings: DistributionPlottingSettings;
|
||||||
environment: environment;
|
environment: environment;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +88,7 @@ let getPercentiles = ({ chartSettings, fn, environment }) => {
|
||||||
let chartPointsData: point[] = chartPointsToRender.map((x) => {
|
let chartPointsData: point[] = chartPointsToRender.map((x) => {
|
||||||
let result = runForeign(fn, [x], environment);
|
let result = runForeign(fn, [x], environment);
|
||||||
if (result.tag === "Ok") {
|
if (result.tag === "Ok") {
|
||||||
if (result.value.tag == "distribution") {
|
if (result.value.tag === "distribution") {
|
||||||
return { x, value: { tag: "Ok", value: result.value.value } };
|
return { x, value: { tag: "Ok", value: result.value.value } };
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
@ -150,6 +154,7 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
|
||||||
fn,
|
fn,
|
||||||
chartSettings,
|
chartSettings,
|
||||||
environment,
|
environment,
|
||||||
|
distributionPlotSettings,
|
||||||
height,
|
height,
|
||||||
}) => {
|
}) => {
|
||||||
let [mouseOverlay, setMouseOverlay] = React.useState(0);
|
let [mouseOverlay, setMouseOverlay] = React.useState(0);
|
||||||
|
@ -160,12 +165,14 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
|
||||||
setMouseOverlay(NaN);
|
setMouseOverlay(NaN);
|
||||||
}
|
}
|
||||||
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
|
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
|
||||||
|
|
||||||
|
//TODO: This custom error handling is a bit hacky and should be improved.
|
||||||
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
|
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
|
||||||
? runForeign(fn, [mouseOverlay], environment)
|
? runForeign(fn, [mouseOverlay], environment)
|
||||||
: {
|
: {
|
||||||
tag: "Error",
|
tag: "Error",
|
||||||
value: {
|
value: {
|
||||||
tag: "REExpectedType",
|
tag: "RETodo",
|
||||||
value: "Hover x-coordinate returned NaN. Expected a number.",
|
value: "Hover x-coordinate returned NaN. Expected a number.",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -175,7 +182,7 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
|
||||||
distribution={mouseItem.value.value}
|
distribution={mouseItem.value.value}
|
||||||
width={400}
|
width={400}
|
||||||
height={50}
|
height={50}
|
||||||
showSummary={false}
|
{...distributionPlotSettings}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
run,
|
|
||||||
errorValueToString,
|
|
||||||
squiggleExpression,
|
squiggleExpression,
|
||||||
bindings,
|
bindings,
|
||||||
environment,
|
environment,
|
||||||
|
@ -9,265 +7,27 @@ import {
|
||||||
defaultImports,
|
defaultImports,
|
||||||
defaultBindings,
|
defaultBindings,
|
||||||
defaultEnvironment,
|
defaultEnvironment,
|
||||||
declaration,
|
|
||||||
} from "@quri/squiggle-lang";
|
} from "@quri/squiggle-lang";
|
||||||
import { NumberShower } from "./NumberShower";
|
import { useSquiggle } from "../lib/hooks";
|
||||||
import { DistributionChart } from "./DistributionChart";
|
import { SquiggleViewer } from "./SquiggleViewer";
|
||||||
import { ErrorAlert } from "./Alert";
|
|
||||||
import { FunctionChart, FunctionChartSettings } from "./FunctionChart";
|
|
||||||
|
|
||||||
function getRange<a>(x: declaration<a>) {
|
|
||||||
let first = x.args[0];
|
|
||||||
switch (first.tag) {
|
|
||||||
case "Float": {
|
|
||||||
return { floats: { min: first.value.min, max: first.value.max } };
|
|
||||||
}
|
|
||||||
case "Date": {
|
|
||||||
return { time: { min: first.value.min, max: first.value.max } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
|
|
||||||
let range = getRange(x);
|
|
||||||
let min = range.floats ? range.floats.min : 0;
|
|
||||||
let max = range.floats ? range.floats.max : 10;
|
|
||||||
return {
|
|
||||||
start: min,
|
|
||||||
stop: max,
|
|
||||||
count: 20,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VariableBoxProps {
|
|
||||||
heading: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
showTypes: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VariableBox: React.FC<VariableBoxProps> = ({
|
|
||||||
heading = "Error",
|
|
||||||
children,
|
|
||||||
showTypes = false,
|
|
||||||
}) => {
|
|
||||||
if (showTypes) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-grey-200 m-2">
|
|
||||||
<div className="border-b border-grey-200 p-3">
|
|
||||||
<header className="font-mono">{heading}</header>
|
|
||||||
</div>
|
|
||||||
<div className="p-3">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <div>{children}</div>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SquiggleItemProps {
|
|
||||||
/** The input string for squiggle */
|
|
||||||
expression: squiggleExpression;
|
|
||||||
width?: number;
|
|
||||||
height: number;
|
|
||||||
/** Whether to show a summary of statistics for distributions */
|
|
||||||
showSummary: boolean;
|
|
||||||
/** Whether to show type information */
|
|
||||||
showTypes: boolean;
|
|
||||||
/** Whether to show users graph controls (scale etc) */
|
|
||||||
showControls: boolean;
|
|
||||||
/** Settings for displaying functions */
|
|
||||||
chartSettings: FunctionChartSettings;
|
|
||||||
/** Environment for further function executions */
|
|
||||||
environment: environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SquiggleItem: React.FC<SquiggleItemProps> = ({
|
|
||||||
expression,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
showSummary,
|
|
||||||
showTypes = false,
|
|
||||||
showControls = false,
|
|
||||||
chartSettings,
|
|
||||||
environment,
|
|
||||||
}) => {
|
|
||||||
switch (expression.tag) {
|
|
||||||
case "number":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Number" showTypes={showTypes}>
|
|
||||||
<div className="font-semibold text-slate-600">
|
|
||||||
<NumberShower precision={3} number={expression.value} />
|
|
||||||
</div>
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "distribution": {
|
|
||||||
let distType = expression.value.type();
|
|
||||||
return (
|
|
||||||
<VariableBox
|
|
||||||
heading={`Distribution (${distType})`}
|
|
||||||
showTypes={showTypes}
|
|
||||||
>
|
|
||||||
{distType === "Symbolic" && showTypes ? (
|
|
||||||
<div>{expression.value.toString()}</div>
|
|
||||||
) : null}
|
|
||||||
<DistributionChart
|
|
||||||
distribution={expression.value}
|
|
||||||
height={height}
|
|
||||||
width={width}
|
|
||||||
showSummary={showSummary}
|
|
||||||
showControls={showControls}
|
|
||||||
/>
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "string":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="String" showTypes={showTypes}>
|
|
||||||
<span className="text-slate-400">"</span>
|
|
||||||
<span className="text-slate-600 font-semibold">
|
|
||||||
{expression.value}
|
|
||||||
</span>
|
|
||||||
<span className="text-slate-400">"</span>
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "boolean":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Boolean" showTypes={showTypes}>
|
|
||||||
{expression.value.toString()}
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "symbol":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Symbol" showTypes={showTypes}>
|
|
||||||
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
|
|
||||||
<span className="text-slate-600">{expression.value}</span>
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "call":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Call" showTypes={showTypes}>
|
|
||||||
{expression.value}
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "array":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Array" showTypes={showTypes}>
|
|
||||||
{expression.value.map((r, i) => (
|
|
||||||
<div key={i} className="flex pt-1">
|
|
||||||
<div className="flex-none bg-slate-100 rounded-sm px-1">
|
|
||||||
<header className="text-slate-400 font-mono">{i}</header>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 mb-2 grow">
|
|
||||||
<SquiggleItem
|
|
||||||
key={i}
|
|
||||||
expression={r}
|
|
||||||
width={width !== undefined ? width - 20 : width}
|
|
||||||
height={50}
|
|
||||||
showTypes={showTypes}
|
|
||||||
showControls={showControls}
|
|
||||||
chartSettings={chartSettings}
|
|
||||||
environment={environment}
|
|
||||||
showSummary={showSummary}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "record":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Record" showTypes={showTypes}>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{Object.entries(expression.value).map(([key, r]) => (
|
|
||||||
<div key={key} className="flex space-x-2">
|
|
||||||
<div className="flex-none">
|
|
||||||
<header className="text-slate-500 font-mono">{key}:</header>
|
|
||||||
</div>
|
|
||||||
<div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
|
|
||||||
<SquiggleItem
|
|
||||||
expression={r}
|
|
||||||
width={width !== undefined ? width - 20 : width}
|
|
||||||
height={height / 3}
|
|
||||||
showTypes={showTypes}
|
|
||||||
showSummary={showSummary}
|
|
||||||
showControls={showControls}
|
|
||||||
chartSettings={chartSettings}
|
|
||||||
environment={environment}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "arraystring":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Array String" showTypes={showTypes}>
|
|
||||||
{expression.value.map((r) => `"${r}"`).join(", ")}
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "date":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Date" showTypes={showTypes}>
|
|
||||||
{expression.value.toDateString()}
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "timeDuration": {
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Time Duration" showTypes={showTypes}>
|
|
||||||
<NumberShower precision={3} number={expression.value} />
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "lambda":
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Function" showTypes={showTypes}>
|
|
||||||
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
|
|
||||||
","
|
|
||||||
)})`}</div>
|
|
||||||
<FunctionChart
|
|
||||||
fn={expression.value}
|
|
||||||
chartSettings={chartSettings}
|
|
||||||
height={height}
|
|
||||||
environment={{
|
|
||||||
sampleCount: environment.sampleCount / 10,
|
|
||||||
xyPointLength: environment.xyPointLength / 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
case "lambdaDeclaration": {
|
|
||||||
return (
|
|
||||||
<VariableBox heading="Function Declaration" showTypes={showTypes}>
|
|
||||||
<FunctionChart
|
|
||||||
fn={expression.value.fn}
|
|
||||||
chartSettings={getChartSettings(expression.value)}
|
|
||||||
height={height}
|
|
||||||
environment={{
|
|
||||||
sampleCount: environment.sampleCount / 10,
|
|
||||||
xyPointLength: environment.xyPointLength / 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</VariableBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return <>Should be unreachable</>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SquiggleChartProps {
|
export interface SquiggleChartProps {
|
||||||
/** The input string for squiggle */
|
/** The input string for squiggle */
|
||||||
squiggleString?: 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 */
|
/** If the output requires monte carlo sampling, the amount of samples */
|
||||||
sampleCount?: number;
|
sampleCount?: number;
|
||||||
/** The amount of points returned to draw the distribution */
|
/** The amount of points returned to draw the distribution */
|
||||||
environment?: environment;
|
environment?: environment;
|
||||||
/** If the result is a function, where the function starts, ends and the amount of stops */
|
/** If the result is a function, where the function domain starts */
|
||||||
chartSettings?: FunctionChartSettings;
|
diagramStart?: number;
|
||||||
/** When the environment changes */
|
/** If the result is a function, where the function domain ends */
|
||||||
onChange?(expr: squiggleExpression): void;
|
diagramStop?: number;
|
||||||
|
/** If the result is a function, the amount of stops sampled */
|
||||||
|
diagramCount?: number;
|
||||||
|
/** When the squiggle code gets reevaluated */
|
||||||
|
onChange?(expr: squiggleExpression | undefined): void;
|
||||||
/** CSS width of the element */
|
/** CSS width of the element */
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
@ -275,51 +35,90 @@ export interface SquiggleChartProps {
|
||||||
bindings?: bindings;
|
bindings?: bindings;
|
||||||
/** JS imported parameters */
|
/** JS imported parameters */
|
||||||
jsImports?: jsImports;
|
jsImports?: jsImports;
|
||||||
/** Whether to show a summary of the distirbution */
|
/** Whether to show a summary of the distribution */
|
||||||
showSummary?: boolean;
|
showSummary?: boolean;
|
||||||
/** Whether to show type information about returns, default false */
|
/** Set the x scale to be logarithmic by deault */
|
||||||
showTypes?: boolean;
|
logX?: boolean;
|
||||||
/** Whether to show graph controls (scale etc)*/
|
/** Set the y scale to be exponential by deault */
|
||||||
showControls?: boolean;
|
expY?: boolean;
|
||||||
|
/** How to format numbers on the x axis */
|
||||||
|
tickFormat?: string;
|
||||||
|
/** Title of the graphed distribution */
|
||||||
|
title?: string;
|
||||||
|
/** Color of the graphed distribution */
|
||||||
|
color?: string;
|
||||||
|
/** Specify the lower bound of the x scale */
|
||||||
|
minX?: number;
|
||||||
|
/** Specify the upper bound of the x scale */
|
||||||
|
maxX?: number;
|
||||||
|
/** Whether to show vega actions to the user, so they can copy the chart spec */
|
||||||
|
distributionChartActions?: boolean;
|
||||||
|
enableLocalSettings?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultChartSettings = { start: 0, stop: 10, count: 20 };
|
const defaultOnChange = () => {};
|
||||||
|
|
||||||
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
||||||
squiggleString = "",
|
({
|
||||||
|
code = "",
|
||||||
|
executionId = 0,
|
||||||
environment,
|
environment,
|
||||||
onChange = () => {},
|
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
|
||||||
height = 200,
|
height = 200,
|
||||||
bindings = defaultBindings,
|
bindings = defaultBindings,
|
||||||
jsImports = defaultImports,
|
jsImports = defaultImports,
|
||||||
showSummary = false,
|
showSummary = false,
|
||||||
width,
|
width,
|
||||||
showTypes = false,
|
logX = false,
|
||||||
showControls = false,
|
expY = false,
|
||||||
chartSettings = defaultChartSettings,
|
diagramStart = 0,
|
||||||
|
diagramStop = 10,
|
||||||
|
diagramCount = 100,
|
||||||
|
tickFormat,
|
||||||
|
minX,
|
||||||
|
maxX,
|
||||||
|
color,
|
||||||
|
title,
|
||||||
|
distributionChartActions,
|
||||||
|
enableLocalSettings = false,
|
||||||
}) => {
|
}) => {
|
||||||
let expressionResult = run(squiggleString, bindings, environment, jsImports);
|
const result = useSquiggle({
|
||||||
if (expressionResult.tag !== "Ok") {
|
code,
|
||||||
return (
|
bindings,
|
||||||
<ErrorAlert heading={"Parse Error"}>
|
environment,
|
||||||
{errorValueToString(expressionResult.value)}
|
jsImports,
|
||||||
</ErrorAlert>
|
onChange,
|
||||||
);
|
executionId,
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const distributionPlotSettings = {
|
||||||
|
showSummary,
|
||||||
|
logX,
|
||||||
|
expY,
|
||||||
|
format: tickFormat,
|
||||||
|
minX,
|
||||||
|
maxX,
|
||||||
|
color,
|
||||||
|
title,
|
||||||
|
actions: distributionChartActions,
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartSettings = {
|
||||||
|
start: diagramStart,
|
||||||
|
stop: diagramStop,
|
||||||
|
count: diagramCount,
|
||||||
|
};
|
||||||
|
|
||||||
let e = environment ?? defaultEnvironment;
|
|
||||||
let expression = expressionResult.value;
|
|
||||||
onChange(expression);
|
|
||||||
return (
|
return (
|
||||||
<SquiggleItem
|
<SquiggleViewer
|
||||||
expression={expression}
|
result={result}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
showSummary={showSummary}
|
distributionPlotSettings={distributionPlotSettings}
|
||||||
showTypes={showTypes}
|
|
||||||
showControls={showControls}
|
|
||||||
chartSettings={chartSettings}
|
chartSettings={chartSettings}
|
||||||
environment={e}
|
environment={environment ?? defaultEnvironment}
|
||||||
|
enableLocalSettings={enableLocalSettings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -13,6 +13,7 @@ const SquiggleContext = React.createContext<SquiggleContextShape>({
|
||||||
|
|
||||||
export const SquiggleContainer: React.FC<Props> = ({ children }) => {
|
export const SquiggleContainer: React.FC<Props> = ({ children }) => {
|
||||||
const context = useContext(SquiggleContext);
|
const context = useContext(SquiggleContext);
|
||||||
|
|
||||||
if (context.containerized) {
|
if (context.containerized) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,212 +1,92 @@
|
||||||
import * as React from "react";
|
import React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import { SquiggleChart } from "./SquiggleChart";
|
|
||||||
import { CodeEditor } from "./CodeEditor";
|
import { CodeEditor } from "./CodeEditor";
|
||||||
import type {
|
import { environment, bindings, jsImports } from "@quri/squiggle-lang";
|
||||||
squiggleExpression,
|
import { defaultImports, defaultBindings } from "@quri/squiggle-lang";
|
||||||
environment,
|
|
||||||
bindings,
|
|
||||||
jsImports,
|
|
||||||
} from "@quri/squiggle-lang";
|
|
||||||
import {
|
|
||||||
runPartial,
|
|
||||||
errorValueToString,
|
|
||||||
defaultImports,
|
|
||||||
defaultBindings,
|
|
||||||
} from "@quri/squiggle-lang";
|
|
||||||
import { ErrorAlert } from "./Alert";
|
|
||||||
import { SquiggleContainer } from "./SquiggleContainer";
|
import { SquiggleContainer } from "./SquiggleContainer";
|
||||||
|
import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart";
|
||||||
|
import { useSquigglePartial, useMaybeControlledValue } from "../lib/hooks";
|
||||||
|
import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
|
||||||
|
|
||||||
export interface SquiggleEditorProps {
|
const WrappedCodeEditor: React.FC<{
|
||||||
/** The input string for squiggle */
|
code: string;
|
||||||
initialSquiggleString?: string;
|
setCode: (code: string) => void;
|
||||||
/** If the output requires monte carlo sampling, the amount of samples */
|
}> = ({ code, setCode }) => (
|
||||||
environment?: environment;
|
|
||||||
/** 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;
|
|
||||||
/** when the environment changes. Used again for notebook magic*/
|
|
||||||
onChange?(expr: squiggleExpression): void;
|
|
||||||
/** The width of the element */
|
|
||||||
width?: number;
|
|
||||||
/** Previous variable declarations */
|
|
||||||
bindings?: bindings;
|
|
||||||
/** JS Imports */
|
|
||||||
jsImports?: jsImports;
|
|
||||||
/** Whether to show detail about types of the returns, default false */
|
|
||||||
showTypes?: boolean;
|
|
||||||
/** Whether to give users access to graph controls */
|
|
||||||
showControls?: boolean;
|
|
||||||
/** Whether to show a summary table */
|
|
||||||
showSummary?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
|
|
||||||
initialSquiggleString = "",
|
|
||||||
width,
|
|
||||||
environment,
|
|
||||||
diagramStart = 0,
|
|
||||||
diagramStop = 10,
|
|
||||||
diagramCount = 20,
|
|
||||||
onChange,
|
|
||||||
bindings = defaultBindings,
|
|
||||||
jsImports = defaultImports,
|
|
||||||
showTypes = false,
|
|
||||||
showControls = false,
|
|
||||||
showSummary = false,
|
|
||||||
}: SquiggleEditorProps) => {
|
|
||||||
const [expression, setExpression] = React.useState(initialSquiggleString);
|
|
||||||
const chartSettings = {
|
|
||||||
start: diagramStart,
|
|
||||||
stop: diagramStop,
|
|
||||||
count: diagramCount,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<SquiggleContainer>
|
|
||||||
<div>
|
|
||||||
<div className="border border-grey-200 p-2 m-4">
|
<div className="border border-grey-200 p-2 m-4">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={expression}
|
value={code}
|
||||||
onChange={setExpression}
|
onChange={setCode}
|
||||||
oneLine={true}
|
oneLine={true}
|
||||||
showGutter={false}
|
showGutter={false}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SquiggleChart
|
);
|
||||||
width={width}
|
|
||||||
environment={environment}
|
export type SquiggleEditorProps = SquiggleChartProps & {
|
||||||
squiggleString={expression}
|
defaultCode?: string;
|
||||||
chartSettings={chartSettings}
|
onCodeChange?: (code: string) => void;
|
||||||
onChange={onChange}
|
};
|
||||||
bindings={bindings}
|
|
||||||
jsImports={jsImports}
|
export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
|
||||||
showTypes={showTypes}
|
const [code, setCode] = useMaybeControlledValue({
|
||||||
showControls={showControls}
|
value: props.code,
|
||||||
showSummary={showSummary}
|
defaultValue: props.defaultCode ?? "",
|
||||||
/>
|
onChange: props.onCodeChange,
|
||||||
</div>
|
});
|
||||||
|
|
||||||
|
let chartProps = { ...props, code };
|
||||||
|
return (
|
||||||
|
<SquiggleContainer>
|
||||||
|
<WrappedCodeEditor code={code} setCode={setCode} />
|
||||||
|
<SquiggleChart {...chartProps} />
|
||||||
</SquiggleContainer>
|
</SquiggleContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
|
|
||||||
let parent = document.createElement("div");
|
|
||||||
ReactDOM.render(
|
|
||||||
<SquiggleEditor
|
|
||||||
{...props}
|
|
||||||
onChange={(expr) => {
|
|
||||||
// Typescript complains on two levels here.
|
|
||||||
// - Div elements don't have a value property
|
|
||||||
// - Even if it did (like it was an input element), it would have to
|
|
||||||
// be a string
|
|
||||||
//
|
|
||||||
// Which are reasonable in most web contexts.
|
|
||||||
//
|
|
||||||
// However we're using observable, neither of those things have to be
|
|
||||||
// true there. div elements can contain the value property, and can have
|
|
||||||
// the value be any datatype they wish.
|
|
||||||
//
|
|
||||||
// This is here to get the 'viewof' part of:
|
|
||||||
// viewof env = cell('normal(0,1)')
|
|
||||||
// to work
|
|
||||||
// @ts-ignore
|
|
||||||
parent.value = expr;
|
|
||||||
|
|
||||||
parent.dispatchEvent(new CustomEvent("input"));
|
|
||||||
if (props.onChange) props.onChange(expr);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
parent
|
|
||||||
);
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SquigglePartialProps {
|
export interface SquigglePartialProps {
|
||||||
/** The input string for squiggle */
|
/** The text inside the input (controlled) */
|
||||||
initialSquiggleString?: string;
|
code?: string;
|
||||||
/** If the output requires monte carlo sampling, the amount of samples */
|
/** The default text inside the input (unControlled) */
|
||||||
environment?: environment;
|
defaultCode?: string;
|
||||||
/** 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;
|
|
||||||
/** when the environment changes. Used again for notebook magic*/
|
/** when the environment changes. Used again for notebook magic*/
|
||||||
onChange?(expr: bindings): void;
|
onChange?(expr: bindings | undefined): void;
|
||||||
|
/** When the code changes */
|
||||||
|
onCodeChange?(code: string): void;
|
||||||
/** Previously declared variables */
|
/** Previously declared variables */
|
||||||
bindings?: bindings;
|
bindings?: bindings;
|
||||||
|
/** If the output requires monte carlo sampling, the amount of samples */
|
||||||
|
environment?: environment;
|
||||||
/** Variables imported from js */
|
/** Variables imported from js */
|
||||||
jsImports?: jsImports;
|
jsImports?: jsImports;
|
||||||
/** Whether to give users access to graph controls */
|
|
||||||
showControls?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export let SquigglePartial: React.FC<SquigglePartialProps> = ({
|
export const SquigglePartial: React.FC<SquigglePartialProps> = ({
|
||||||
initialSquiggleString = "",
|
code: controlledCode,
|
||||||
|
defaultCode = "",
|
||||||
onChange,
|
onChange,
|
||||||
|
onCodeChange,
|
||||||
bindings = defaultBindings,
|
bindings = defaultBindings,
|
||||||
environment,
|
environment,
|
||||||
jsImports = defaultImports,
|
jsImports = defaultImports,
|
||||||
}: SquigglePartialProps) => {
|
}: SquigglePartialProps) => {
|
||||||
const [expression, setExpression] = React.useState(initialSquiggleString);
|
const [code, setCode] = useMaybeControlledValue<string>({
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
value: controlledCode,
|
||||||
|
defaultValue: defaultCode,
|
||||||
|
onChange: onCodeChange,
|
||||||
|
});
|
||||||
|
|
||||||
const runSquiggleAndUpdateBindings = () => {
|
const result = useSquigglePartial({
|
||||||
const squiggleResult = runPartial(
|
code,
|
||||||
expression,
|
|
||||||
bindings,
|
bindings,
|
||||||
environment,
|
environment,
|
||||||
jsImports
|
jsImports,
|
||||||
);
|
onChange,
|
||||||
if (squiggleResult.tag === "Ok") {
|
});
|
||||||
if (onChange) onChange(squiggleResult.value);
|
|
||||||
setError(null);
|
|
||||||
} else {
|
|
||||||
setError(errorValueToString(squiggleResult.value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(runSquiggleAndUpdateBindings, [expression]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SquiggleContainer>
|
<SquiggleContainer>
|
||||||
<div>
|
<WrappedCodeEditor code={code} setCode={setCode} />
|
||||||
<div className="border border-grey-200 p-2 m-4">
|
{result.tag !== "Ok" ? <SquiggleErrorAlert error={result.value} /> : null}
|
||||||
<CodeEditor
|
|
||||||
value={expression}
|
|
||||||
onChange={setExpression}
|
|
||||||
oneLine={true}
|
|
||||||
showGutter={false}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{error !== null ? (
|
|
||||||
<ErrorAlert heading="Error">{error}</ErrorAlert>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</SquiggleContainer>
|
</SquiggleContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderSquigglePartialToDom(props: SquigglePartialProps) {
|
|
||||||
let parent = document.createElement("div");
|
|
||||||
ReactDOM.render(
|
|
||||||
<SquigglePartial
|
|
||||||
{...props}
|
|
||||||
onChange={(bindings) => {
|
|
||||||
// @ts-ignore
|
|
||||||
parent.value = bindings;
|
|
||||||
|
|
||||||
parent.dispatchEvent(new CustomEvent("input"));
|
|
||||||
if (props.onChange) props.onChange(bindings);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
parent
|
|
||||||
);
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import React from "react";
|
||||||
|
import { SquiggleEditor } from "./SquiggleEditor";
|
||||||
|
import type { SquiggleEditorProps } from "./SquiggleEditor";
|
||||||
|
import { runPartial, defaultBindings } from "@quri/squiggle-lang";
|
||||||
|
import type {
|
||||||
|
result,
|
||||||
|
errorValue,
|
||||||
|
bindings as bindingsType,
|
||||||
|
} from "@quri/squiggle-lang";
|
||||||
|
|
||||||
|
function resultDefault(x: result<bindingsType, errorValue>): bindingsType {
|
||||||
|
switch (x.tag) {
|
||||||
|
case "Ok":
|
||||||
|
return x.value;
|
||||||
|
case "Error":
|
||||||
|
return defaultBindings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SquiggleEditorWithImportedBindingsProps = SquiggleEditorProps & {
|
||||||
|
bindingsImportUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SquiggleEditorWithImportedBindings: React.FC<
|
||||||
|
SquiggleEditorWithImportedBindingsProps
|
||||||
|
> = (props) => {
|
||||||
|
const { bindingsImportUrl, ...editorProps } = props;
|
||||||
|
const [bindingsResult, setBindingsResult] = React.useState({
|
||||||
|
tag: "Ok",
|
||||||
|
value: defaultBindings,
|
||||||
|
} as result<bindingsType, errorValue>);
|
||||||
|
React.useEffect(() => {
|
||||||
|
async function retrieveBindings(fileName: string) {
|
||||||
|
let contents = await fetch(fileName).then((response) => {
|
||||||
|
return response.text();
|
||||||
|
});
|
||||||
|
setBindingsResult(
|
||||||
|
runPartial(
|
||||||
|
contents,
|
||||||
|
editorProps.bindings,
|
||||||
|
editorProps.environment,
|
||||||
|
editorProps.jsImports
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
retrieveBindings(bindingsImportUrl);
|
||||||
|
}, [bindingsImportUrl]);
|
||||||
|
const deliveredBindings = resultDefault(bindingsResult);
|
||||||
|
return (
|
||||||
|
<SquiggleEditor {...{ ...editorProps, bindings: deliveredBindings }} />
|
||||||
|
);
|
||||||
|
};
|
11
packages/components/src/components/SquiggleErrorAlert.tsx
Normal file
11
packages/components/src/components/SquiggleErrorAlert.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { errorValue, errorValueToString } from "@quri/squiggle-lang";
|
||||||
|
import React from "react";
|
||||||
|
import { ErrorAlert } from "./Alert";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error: errorValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => {
|
||||||
|
return <ErrorAlert heading="Error">{errorValueToString(error)}</ErrorAlert>;
|
||||||
|
};
|
|
@ -1,40 +1,62 @@
|
||||||
import React, { FC, Fragment, useState } from "react";
|
import React, {
|
||||||
import ReactDOM from "react-dom";
|
FC,
|
||||||
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
} from "react";
|
||||||
|
import { useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import { useMaybeControlledValue, useRunnerState } from "../lib/hooks";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import {
|
import {
|
||||||
ChartSquareBarIcon,
|
ChartSquareBarIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
CurrencyDollarIcon,
|
CurrencyDollarIcon,
|
||||||
|
EyeIcon,
|
||||||
|
PauseIcon,
|
||||||
|
PlayIcon,
|
||||||
|
RefreshIcon,
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { defaultBindings, environment } from "@quri/squiggle-lang";
|
import { defaultBindings, environment } from "@quri/squiggle-lang";
|
||||||
|
|
||||||
import { SquiggleChart } from "./SquiggleChart";
|
import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart";
|
||||||
import { CodeEditor } from "./CodeEditor";
|
import { CodeEditor } from "./CodeEditor";
|
||||||
import { JsonEditor } from "./JsonEditor";
|
import { JsonEditor } from "./JsonEditor";
|
||||||
import { ErrorAlert, SuccessAlert } from "./Alert";
|
import { ErrorAlert, SuccessAlert } from "./Alert";
|
||||||
import { SquiggleContainer } from "./SquiggleContainer";
|
import { SquiggleContainer } from "./SquiggleContainer";
|
||||||
|
import { Toggle } from "./ui/Toggle";
|
||||||
|
import { StyledTab } from "./ui/StyledTab";
|
||||||
|
import { InputItem } from "./ui/InputItem";
|
||||||
|
import { Text } from "./ui/Text";
|
||||||
|
import { ViewSettings, viewSettingsSchema } from "./ViewSettings";
|
||||||
|
import { HeadedSection } from "./ui/HeadedSection";
|
||||||
|
import {
|
||||||
|
defaultColor,
|
||||||
|
defaultTickFormat,
|
||||||
|
} from "../lib/distributionSpecBuilder";
|
||||||
|
import { Button } from "./ui/Button";
|
||||||
|
|
||||||
interface PlaygroundProps {
|
type PlaygroundProps = SquiggleChartProps & {
|
||||||
/** The initial squiggle string to put in the playground */
|
/** The initial squiggle string to put in the playground */
|
||||||
initialSquiggleString?: string;
|
defaultCode?: string;
|
||||||
/** How many pixels high is the playground */
|
onCodeChange?(expr: string): void;
|
||||||
height?: number;
|
/* When settings change */
|
||||||
/** Whether to show the types of outputs in the playground */
|
onSettingsChange?(settings: any): void;
|
||||||
showTypes?: boolean;
|
/** Should we show the editor? */
|
||||||
/** Whether to show the log scale controls in the playground */
|
showEditor?: boolean;
|
||||||
showControls?: boolean;
|
/** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */
|
||||||
/** Whether to show the summary table in the playground */
|
showShareButton?: boolean;
|
||||||
showSummary?: boolean;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const schema = yup
|
const schema = yup
|
||||||
.object()
|
.object({})
|
||||||
.shape({
|
.shape({
|
||||||
sampleCount: yup
|
sampleCount: yup
|
||||||
.number()
|
.number()
|
||||||
|
@ -52,194 +74,14 @@ const schema = yup
|
||||||
.default(1000)
|
.default(1000)
|
||||||
.min(10)
|
.min(10)
|
||||||
.max(10000),
|
.max(10000),
|
||||||
chartHeight: yup.number().required().positive().integer().default(350),
|
|
||||||
leftSizePercent: yup
|
|
||||||
.number()
|
|
||||||
.required()
|
|
||||||
.positive()
|
|
||||||
.integer()
|
|
||||||
.min(10)
|
|
||||||
.max(100)
|
|
||||||
.default(50),
|
|
||||||
showTypes: yup.boolean(),
|
|
||||||
showControls: yup.boolean(),
|
|
||||||
showSummary: yup.boolean(),
|
|
||||||
showSettingsPage: yup.boolean().default(false),
|
|
||||||
diagramStart: yup
|
|
||||||
.number()
|
|
||||||
.required()
|
|
||||||
.positive()
|
|
||||||
.integer()
|
|
||||||
.default(0)
|
|
||||||
.min(0),
|
|
||||||
diagramStop: yup
|
|
||||||
.number()
|
|
||||||
.required()
|
|
||||||
.positive()
|
|
||||||
.integer()
|
|
||||||
.default(10)
|
|
||||||
.min(0),
|
|
||||||
diagramCount: yup
|
|
||||||
.number()
|
|
||||||
.required()
|
|
||||||
.positive()
|
|
||||||
.integer()
|
|
||||||
.default(20)
|
|
||||||
.min(2),
|
|
||||||
})
|
})
|
||||||
.required();
|
.concat(viewSettingsSchema);
|
||||||
|
|
||||||
type StyledTabProps = {
|
type FormFields = yup.InferType<typeof schema>;
|
||||||
name: string;
|
|
||||||
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => {
|
const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
|
||||||
return (
|
register,
|
||||||
<Tab key={name} as={Fragment}>
|
|
||||||
{({ selected }) => (
|
|
||||||
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
"p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium",
|
|
||||||
selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={clsx(
|
|
||||||
"-ml-0.5 mr-2 h-4 w-4",
|
|
||||||
selected
|
|
||||||
? "text-slate-500"
|
|
||||||
: "text-gray-400 group-hover:text-gray-900"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
selected
|
|
||||||
? "text-gray-900"
|
|
||||||
: "text-gray-600 group-hover:text-gray-900"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tab>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
}) => (
|
}) => (
|
||||||
<div>
|
|
||||||
<header className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
{title}
|
|
||||||
</header>
|
|
||||||
<div className="mt-4">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Text: FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
||||||
<p className="text-sm text-gray-500">{children}</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
function InputItem<T>({
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
type,
|
|
||||||
register,
|
|
||||||
}: {
|
|
||||||
name: Path<T>;
|
|
||||||
label: string;
|
|
||||||
type: "number";
|
|
||||||
register: UseFormRegister<T>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<label className="block">
|
|
||||||
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
{...register(name)}
|
|
||||||
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Checkbox<T>({
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
register,
|
|
||||||
}: {
|
|
||||||
name: Path<T>;
|
|
||||||
label: string;
|
|
||||||
register: UseFormRegister<T>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<label className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
{...register(name)}
|
|
||||||
className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */}
|
|
||||||
<div className="ml-3 text-sm font-medium text-gray-700">{label}</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SquigglePlayground: FC<PlaygroundProps> = ({
|
|
||||||
initialSquiggleString = "",
|
|
||||||
height = 500,
|
|
||||||
showTypes = false,
|
|
||||||
showControls = false,
|
|
||||||
showSummary = false,
|
|
||||||
}) => {
|
|
||||||
const [squiggleString, setSquiggleString] = useState(initialSquiggleString);
|
|
||||||
const [importString, setImportString] = useState("{}");
|
|
||||||
const [imports, setImports] = useState({});
|
|
||||||
const [importsAreValid, setImportsAreValid] = useState(true);
|
|
||||||
const { register, control } = useForm({
|
|
||||||
resolver: yupResolver(schema),
|
|
||||||
defaultValues: {
|
|
||||||
sampleCount: 1000,
|
|
||||||
xyPointLength: 1000,
|
|
||||||
chartHeight: 150,
|
|
||||||
showTypes: showTypes,
|
|
||||||
showControls: showControls,
|
|
||||||
showSummary: showSummary,
|
|
||||||
leftSizePercent: 50,
|
|
||||||
showSettingsPage: false,
|
|
||||||
diagramStart: 0,
|
|
||||||
diagramStop: 10,
|
|
||||||
diagramCount: 20,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const vars = useWatch({
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
const chartSettings = {
|
|
||||||
start: Number(vars.diagramStart),
|
|
||||||
stop: Number(vars.diagramStop),
|
|
||||||
count: Number(vars.diagramCount),
|
|
||||||
};
|
|
||||||
const env: environment = {
|
|
||||||
sampleCount: Number(vars.sampleCount),
|
|
||||||
xyPointLength: Number(vars.xyPointLength),
|
|
||||||
};
|
|
||||||
const getChangeJson = (r: string) => {
|
|
||||||
setImportString(r);
|
|
||||||
try {
|
|
||||||
setImports(JSON.parse(r));
|
|
||||||
setImportsAreValid(true);
|
|
||||||
} catch (e) {
|
|
||||||
setImportsAreValid(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const samplingSettings = (
|
|
||||||
<div className="space-y-6 p-3 max-w-xl">
|
<div className="space-y-6 p-3 max-w-xl">
|
||||||
<div>
|
<div>
|
||||||
<InputItem
|
<InputItem
|
||||||
|
@ -264,85 +106,36 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
|
||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Text>
|
<Text>
|
||||||
When distributions are converted into PointSet shapes, we need to
|
When distributions are converted into PointSet shapes, we need to know
|
||||||
know how many coordinates to use.
|
how many coordinates to use.
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewSettings = (
|
const InputVariablesSettings: React.FC<{
|
||||||
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
|
initialImports: any; // TODO - any json type
|
||||||
<HeadedSection title="General Display Settings">
|
setImports: (imports: any) => void;
|
||||||
<div className="space-y-4">
|
}> = ({ initialImports, setImports }) => {
|
||||||
<InputItem
|
const [importString, setImportString] = useState(() =>
|
||||||
name="chartHeight"
|
JSON.stringify(initialImports)
|
||||||
type="number"
|
|
||||||
register={register}
|
|
||||||
label="Chart Height (in pixels)"
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
name="showTypes"
|
|
||||||
register={register}
|
|
||||||
label="Show information about displayed types"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</HeadedSection>
|
|
||||||
|
|
||||||
<div className="pt-8">
|
|
||||||
<HeadedSection title="Distribution Display Settings">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Checkbox
|
|
||||||
register={register}
|
|
||||||
name="showControls"
|
|
||||||
label="Show toggles to adjust scale of x and y axes"
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
register={register}
|
|
||||||
name="showSummary"
|
|
||||||
label="Show summary statistics"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</HeadedSection>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-8">
|
|
||||||
<HeadedSection title="Function Display Settings">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Text>
|
|
||||||
When displaying functions of single variables that return numbers
|
|
||||||
or distributions, we need to use defaults for the x-axis. We need
|
|
||||||
to select a minimum and maximum value of x to sample, and a number
|
|
||||||
n of the number of points to sample.
|
|
||||||
</Text>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<InputItem
|
|
||||||
type="number"
|
|
||||||
name="diagramStart"
|
|
||||||
register={register}
|
|
||||||
label="Min X Value"
|
|
||||||
/>
|
|
||||||
<InputItem
|
|
||||||
type="number"
|
|
||||||
name="diagramStop"
|
|
||||||
register={register}
|
|
||||||
label="Max X Value"
|
|
||||||
/>
|
|
||||||
<InputItem
|
|
||||||
type="number"
|
|
||||||
name="diagramCount"
|
|
||||||
register={register}
|
|
||||||
label="Points between X min and X max to sample"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HeadedSection>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
const [importsAreValid, setImportsAreValid] = useState(true);
|
||||||
|
|
||||||
const inputVariableSettings = (
|
const onChange = (value: string) => {
|
||||||
|
setImportString(value);
|
||||||
|
let imports = {} as any;
|
||||||
|
try {
|
||||||
|
imports = JSON.parse(value);
|
||||||
|
setImportsAreValid(true);
|
||||||
|
} catch (e) {
|
||||||
|
setImportsAreValid(false);
|
||||||
|
}
|
||||||
|
setImports(imports);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="p-3 max-w-3xl">
|
<div className="p-3 max-w-3xl">
|
||||||
<HeadedSection title="Import Variables from JSON">
|
<HeadedSection title="Import Variables from JSON">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
@ -354,7 +147,7 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
|
||||||
<div className="border border-slate-200 mt-6 mb-2">
|
<div className="border border-slate-200 mt-6 mb-2">
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
value={importString}
|
value={importString}
|
||||||
onChange={getChangeJson}
|
onChange={onChange}
|
||||||
oneLine={false}
|
oneLine={false}
|
||||||
showGutter={true}
|
showGutter={true}
|
||||||
height={150}
|
height={150}
|
||||||
|
@ -373,62 +166,253 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
|
||||||
</HeadedSection>
|
</HeadedSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RunControls: React.FC<{
|
||||||
|
autorunMode: boolean;
|
||||||
|
isRunning: boolean;
|
||||||
|
isStale: boolean;
|
||||||
|
onAutorunModeChange: (value: boolean) => void;
|
||||||
|
run: () => void;
|
||||||
|
}> = ({ autorunMode, isRunning, isStale, onAutorunModeChange, run }) => {
|
||||||
|
const CurrentPlayIcon = isRunning ? RefreshIcon : PlayIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SquiggleContainer>
|
<div className="flex space-x-1 items-center">
|
||||||
<Tab.Group>
|
{autorunMode ? null : (
|
||||||
<div className="pb-4">
|
<button onClick={run}>
|
||||||
<Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
|
<CurrentPlayIcon
|
||||||
<StyledTab name="Code" icon={CodeIcon} />
|
className={clsx(
|
||||||
<StyledTab name="Sampling Settings" icon={CogIcon} />
|
"w-8 h-8",
|
||||||
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
|
isRunning && "animate-spin",
|
||||||
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
|
isStale ? "text-indigo-500" : "text-gray-400"
|
||||||
</Tab.List>
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<Toggle
|
||||||
|
texts={["Autorun", "Paused"]}
|
||||||
|
icons={[CheckCircleIcon, PauseIcon]}
|
||||||
|
status={autorunMode}
|
||||||
|
onChange={onAutorunModeChange}
|
||||||
|
spinIcon={autorunMode && isRunning}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex" style={{ height }}>
|
);
|
||||||
<div className="w-1/2">
|
};
|
||||||
<Tab.Panels>
|
|
||||||
<Tab.Panel>
|
const ShareButton: React.FC = () => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard.writeText((window.top || window).location.href);
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 1000);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="w-36">
|
||||||
|
<Button onClick={copy} wide>
|
||||||
|
{isCopied ? (
|
||||||
|
"Copied to clipboard!"
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<ClipboardCopyIcon className="w-4 h-4" />
|
||||||
|
<span>Copy share link</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlaygroundContextShape = {
|
||||||
|
getLeftPanelElement: () => HTMLDivElement | undefined;
|
||||||
|
};
|
||||||
|
export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
|
||||||
|
getLeftPanelElement: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SquigglePlayground: FC<PlaygroundProps> = ({
|
||||||
|
defaultCode = "",
|
||||||
|
height = 500,
|
||||||
|
showSummary = false,
|
||||||
|
logX = false,
|
||||||
|
expY = false,
|
||||||
|
title,
|
||||||
|
minX,
|
||||||
|
maxX,
|
||||||
|
color = defaultColor,
|
||||||
|
tickFormat = defaultTickFormat,
|
||||||
|
distributionChartActions,
|
||||||
|
code: controlledCode,
|
||||||
|
onCodeChange,
|
||||||
|
onSettingsChange,
|
||||||
|
showEditor = true,
|
||||||
|
showShareButton = false,
|
||||||
|
}) => {
|
||||||
|
const [code, setCode] = useMaybeControlledValue({
|
||||||
|
value: controlledCode,
|
||||||
|
defaultValue: defaultCode,
|
||||||
|
onChange: onCodeChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [imports, setImports] = useState({});
|
||||||
|
|
||||||
|
const { register, control } = useForm({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
sampleCount: 1000,
|
||||||
|
xyPointLength: 1000,
|
||||||
|
chartHeight: 150,
|
||||||
|
logX,
|
||||||
|
expY,
|
||||||
|
title,
|
||||||
|
minX,
|
||||||
|
maxX,
|
||||||
|
color,
|
||||||
|
tickFormat,
|
||||||
|
distributionChartActions,
|
||||||
|
showSummary,
|
||||||
|
showEditor,
|
||||||
|
diagramStart: 0,
|
||||||
|
diagramStop: 10,
|
||||||
|
diagramCount: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const vars = useWatch({
|
||||||
|
control,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSettingsChange?.(vars);
|
||||||
|
}, [vars, onSettingsChange]);
|
||||||
|
|
||||||
|
const env: environment = useMemo(
|
||||||
|
() => ({
|
||||||
|
sampleCount: Number(vars.sampleCount),
|
||||||
|
xyPointLength: Number(vars.xyPointLength),
|
||||||
|
}),
|
||||||
|
[vars.sampleCount, vars.xyPointLength]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
run,
|
||||||
|
autorunMode,
|
||||||
|
setAutorunMode,
|
||||||
|
isRunning,
|
||||||
|
renderedCode,
|
||||||
|
executionId,
|
||||||
|
} = useRunnerState(code);
|
||||||
|
|
||||||
|
const squiggleChart =
|
||||||
|
renderedCode === "" ? null : (
|
||||||
|
<div className="relative">
|
||||||
|
{isRunning ? (
|
||||||
|
<div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
|
||||||
|
) : null}
|
||||||
|
<SquiggleChart
|
||||||
|
code={renderedCode}
|
||||||
|
executionId={executionId}
|
||||||
|
environment={env}
|
||||||
|
{...vars}
|
||||||
|
bindings={defaultBindings}
|
||||||
|
jsImports={imports}
|
||||||
|
enableLocalSettings={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstTab = vars.showEditor ? (
|
||||||
<div className="border border-slate-200">
|
<div className="border border-slate-200">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={squiggleString}
|
value={code}
|
||||||
onChange={setSquiggleString}
|
onChange={setCode}
|
||||||
|
onSubmit={run}
|
||||||
oneLine={false}
|
oneLine={false}
|
||||||
showGutter={true}
|
showGutter={true}
|
||||||
height={height - 1}
|
height={height - 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
) : (
|
||||||
<Tab.Panel>{samplingSettings}</Tab.Panel>
|
squiggleChart
|
||||||
<Tab.Panel>{viewSettings}</Tab.Panel>
|
);
|
||||||
<Tab.Panel>{inputVariableSettings}</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-1/2 p-2 pl-4">
|
const tabs = (
|
||||||
<div style={{ maxHeight: height }}>
|
<StyledTab.Panels>
|
||||||
<SquiggleChart
|
<StyledTab.Panel>{firstTab}</StyledTab.Panel>
|
||||||
squiggleString={squiggleString}
|
<StyledTab.Panel>
|
||||||
environment={env}
|
<SamplingSettings register={register} />
|
||||||
chartSettings={chartSettings}
|
</StyledTab.Panel>
|
||||||
height={vars.chartHeight}
|
<StyledTab.Panel>
|
||||||
showTypes={vars.showTypes}
|
<ViewSettings
|
||||||
showControls={vars.showControls}
|
register={
|
||||||
bindings={defaultBindings}
|
// This is dangerous, but doesn't cause any problems.
|
||||||
jsImports={imports}
|
// I tried to make `ViewSettings` generic (to allow it to accept any extension of a settings schema), but it didn't work.
|
||||||
showSummary={vars.showSummary}
|
register as unknown as UseFormRegister<
|
||||||
|
yup.InferType<typeof viewSettingsSchema>
|
||||||
|
>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</StyledTab.Panel>
|
||||||
|
<StyledTab.Panel>
|
||||||
|
<InputVariablesSettings
|
||||||
|
initialImports={imports}
|
||||||
|
setImports={setImports}
|
||||||
|
/>
|
||||||
|
</StyledTab.Panel>
|
||||||
|
</StyledTab.Panels>
|
||||||
|
);
|
||||||
|
|
||||||
|
const leftPanelRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const withEditor = (
|
||||||
|
<div className="flex mt-2">
|
||||||
|
<div
|
||||||
|
className="w-1/2 relative"
|
||||||
|
style={{ minHeight: height }}
|
||||||
|
ref={leftPanelRef}
|
||||||
|
>
|
||||||
|
{tabs}
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2 p-2 pl-4">{squiggleChart}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const withoutEditor = <div className="mt-3">{tabs}</div>;
|
||||||
|
|
||||||
|
const getLeftPanelElement = useCallback(() => {
|
||||||
|
return leftPanelRef.current ?? undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SquiggleContainer>
|
||||||
|
<PlaygroundContext.Provider value={{ getLeftPanelElement }}>
|
||||||
|
<StyledTab.Group>
|
||||||
|
<div className="pb-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<StyledTab.List>
|
||||||
|
<StyledTab
|
||||||
|
name={vars.showEditor ? "Code" : "Display"}
|
||||||
|
icon={vars.showEditor ? CodeIcon : EyeIcon}
|
||||||
|
/>
|
||||||
|
<StyledTab name="Sampling Settings" icon={CogIcon} />
|
||||||
|
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
|
||||||
|
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
|
||||||
|
</StyledTab.List>
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<RunControls
|
||||||
|
autorunMode={autorunMode}
|
||||||
|
isStale={renderedCode !== code}
|
||||||
|
run={run}
|
||||||
|
isRunning={isRunning}
|
||||||
|
onAutorunModeChange={setAutorunMode}
|
||||||
|
/>
|
||||||
|
{showShareButton && <ShareButton />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{vars.showEditor ? withEditor : withoutEditor}
|
||||||
</div>
|
</div>
|
||||||
</Tab.Group>
|
</StyledTab.Group>
|
||||||
|
</PlaygroundContext.Provider>
|
||||||
</SquiggleContainer>
|
</SquiggleContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SquigglePlayground;
|
|
||||||
export function renderSquigglePlaygroundToDom(props: PlaygroundProps) {
|
|
||||||
const parent = document.createElement("div");
|
|
||||||
ReactDOM.render(<SquigglePlayground {...props} />, parent);
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,291 @@
|
||||||
|
import React from "react";
|
||||||
|
import { squiggleExpression, declaration } from "@quri/squiggle-lang";
|
||||||
|
import { NumberShower } from "../NumberShower";
|
||||||
|
import { DistributionChart } from "../DistributionChart";
|
||||||
|
import { FunctionChart, FunctionChartSettings } from "../FunctionChart";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { VariableBox } from "./VariableBox";
|
||||||
|
import { ItemSettingsMenu } from "./ItemSettingsMenu";
|
||||||
|
import { hasMassBelowZero } from "../../lib/distributionUtils";
|
||||||
|
import { MergedItemSettings } from "./utils";
|
||||||
|
|
||||||
|
function getRange<a>(x: declaration<a>) {
|
||||||
|
const first = x.args[0];
|
||||||
|
switch (first.tag) {
|
||||||
|
case "Float": {
|
||||||
|
return { floats: { min: first.value.min, max: first.value.max } };
|
||||||
|
}
|
||||||
|
case "Date": {
|
||||||
|
return { time: { min: first.value.min, max: first.value.max } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
|
||||||
|
const range = getRange(x);
|
||||||
|
const min = range.floats ? range.floats.min : 0;
|
||||||
|
const max = range.floats ? range.floats.max : 10;
|
||||||
|
return {
|
||||||
|
start: min,
|
||||||
|
stop: max,
|
||||||
|
count: 20,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableList: React.FC<{
|
||||||
|
path: string[];
|
||||||
|
heading: string;
|
||||||
|
children: (settings: MergedItemSettings) => React.ReactNode;
|
||||||
|
}> = ({ path, heading, children }) => (
|
||||||
|
<VariableBox path={path} heading={heading}>
|
||||||
|
{(settings) => (
|
||||||
|
<div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}>
|
||||||
|
{children(settings)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
/** The output of squiggle's run */
|
||||||
|
expression: squiggleExpression;
|
||||||
|
/** Path to the current item, e.g. `['foo', 'bar', '3']` for `foo.bar[3]`; can be empty on the top-level item. */
|
||||||
|
path: string[];
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExpressionViewer: React.FC<Props> = ({
|
||||||
|
path,
|
||||||
|
expression,
|
||||||
|
width,
|
||||||
|
}) => {
|
||||||
|
switch (expression.tag) {
|
||||||
|
case "number":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Number">
|
||||||
|
{() => (
|
||||||
|
<div className="font-semibold text-slate-600">
|
||||||
|
<NumberShower precision={3} number={expression.value} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "distribution": {
|
||||||
|
const distType = expression.value.type();
|
||||||
|
return (
|
||||||
|
<VariableBox
|
||||||
|
path={path}
|
||||||
|
heading={`Distribution (${distType})\n${
|
||||||
|
distType === "Symbolic" ? expression.value.toString() : ""
|
||||||
|
}`}
|
||||||
|
renderSettingsMenu={({ onChange }) => {
|
||||||
|
const shape = expression.value.pointSet();
|
||||||
|
return (
|
||||||
|
<ItemSettingsMenu
|
||||||
|
path={path}
|
||||||
|
onChange={onChange}
|
||||||
|
disableLogX={
|
||||||
|
shape.tag === "Ok" && hasMassBelowZero(shape.value)
|
||||||
|
}
|
||||||
|
withFunctionSettings={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(settings) => {
|
||||||
|
return (
|
||||||
|
<DistributionChart
|
||||||
|
distribution={expression.value}
|
||||||
|
{...settings.distributionPlotSettings}
|
||||||
|
height={settings.height}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "string":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="String">
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
<span className="text-slate-400">"</span>
|
||||||
|
<span className="text-slate-600 font-semibold font-mono">
|
||||||
|
{expression.value}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-400">"</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "boolean":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Boolean">
|
||||||
|
{() => expression.value.toString()}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "symbol":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Symbol">
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
|
||||||
|
<span className="text-slate-600">{expression.value}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "call":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Call">
|
||||||
|
{() => expression.value}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "arraystring":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Array String">
|
||||||
|
{() => expression.value.map((r) => `"${r}"`).join(", ")}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "date":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Date">
|
||||||
|
{() => expression.value.toDateString()}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "void":
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Void">
|
||||||
|
{() => "Void"}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "timeDuration": {
|
||||||
|
return (
|
||||||
|
<VariableBox path={path} heading="Time Duration">
|
||||||
|
{() => <NumberShower precision={3} number={expression.value} />}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "lambda":
|
||||||
|
return (
|
||||||
|
<VariableBox
|
||||||
|
path={path}
|
||||||
|
heading="Function"
|
||||||
|
renderSettingsMenu={({ onChange }) => {
|
||||||
|
return (
|
||||||
|
<ItemSettingsMenu
|
||||||
|
path={path}
|
||||||
|
onChange={onChange}
|
||||||
|
withFunctionSettings={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(settings) => (
|
||||||
|
<>
|
||||||
|
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
|
||||||
|
","
|
||||||
|
)})`}</div>
|
||||||
|
<FunctionChart
|
||||||
|
fn={expression.value}
|
||||||
|
chartSettings={settings.chartSettings}
|
||||||
|
distributionPlotSettings={settings.distributionPlotSettings}
|
||||||
|
height={settings.height}
|
||||||
|
environment={{
|
||||||
|
sampleCount: settings.environment.sampleCount / 10,
|
||||||
|
xyPointLength: settings.environment.xyPointLength / 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
case "lambdaDeclaration": {
|
||||||
|
return (
|
||||||
|
<VariableBox
|
||||||
|
path={path}
|
||||||
|
heading="Function Declaration"
|
||||||
|
renderSettingsMenu={({ onChange }) => {
|
||||||
|
return (
|
||||||
|
<ItemSettingsMenu
|
||||||
|
onChange={onChange}
|
||||||
|
path={path}
|
||||||
|
withFunctionSettings={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(settings) => (
|
||||||
|
<FunctionChart
|
||||||
|
fn={expression.value.fn}
|
||||||
|
chartSettings={getChartSettings(expression.value)}
|
||||||
|
distributionPlotSettings={settings.distributionPlotSettings}
|
||||||
|
height={settings.height}
|
||||||
|
environment={{
|
||||||
|
sampleCount: settings.environment.sampleCount / 10,
|
||||||
|
xyPointLength: settings.environment.xyPointLength / 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</VariableBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "module": {
|
||||||
|
return (
|
||||||
|
<VariableList path={path} heading="Module">
|
||||||
|
{(settings) =>
|
||||||
|
Object.entries(expression.value)
|
||||||
|
.filter(([key, r]) => !key.match(/^(Math|System)\./))
|
||||||
|
.map(([key, r]) => (
|
||||||
|
<ExpressionViewer
|
||||||
|
key={key}
|
||||||
|
path={[...path, key]}
|
||||||
|
expression={r}
|
||||||
|
width={width !== undefined ? width - 20 : width}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</VariableList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "record":
|
||||||
|
return (
|
||||||
|
<VariableList path={path} heading="Record">
|
||||||
|
{(settings) =>
|
||||||
|
Object.entries(expression.value).map(([key, r]) => (
|
||||||
|
<ExpressionViewer
|
||||||
|
key={key}
|
||||||
|
path={[...path, key]}
|
||||||
|
expression={r}
|
||||||
|
width={width !== undefined ? width - 20 : width}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</VariableList>
|
||||||
|
);
|
||||||
|
case "array":
|
||||||
|
return (
|
||||||
|
<VariableList path={path} heading="Array">
|
||||||
|
{(settings) =>
|
||||||
|
expression.value.map((r, i) => (
|
||||||
|
<ExpressionViewer
|
||||||
|
key={i}
|
||||||
|
path={[...path, String(i)]}
|
||||||
|
expression={r}
|
||||||
|
width={width !== undefined ? width - 20 : width}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</VariableList>
|
||||||
|
);
|
||||||
|
default: {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>No display for type: </span>{" "}
|
||||||
|
<span className="font-semibold text-slate-600">{expression.tag}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { CogIcon } from "@heroicons/react/solid";
|
||||||
|
import React, { useContext, useRef, useState, useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { Modal } from "../ui/Modal";
|
||||||
|
import { ViewSettings, viewSettingsSchema } from "../ViewSettings";
|
||||||
|
import { Path, pathAsString } from "./utils";
|
||||||
|
import { ViewerContext } from "./ViewerContext";
|
||||||
|
import {
|
||||||
|
defaultColor,
|
||||||
|
defaultTickFormat,
|
||||||
|
} from "../../lib/distributionSpecBuilder";
|
||||||
|
import { PlaygroundContext } from "../SquigglePlayground";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
path: Path;
|
||||||
|
onChange: () => void;
|
||||||
|
disableLogX?: boolean;
|
||||||
|
withFunctionSettings: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemSettingsModal: React.FC<
|
||||||
|
Props & { close: () => void; resetScroll: () => void }
|
||||||
|
> = ({
|
||||||
|
path,
|
||||||
|
onChange,
|
||||||
|
disableLogX,
|
||||||
|
withFunctionSettings,
|
||||||
|
close,
|
||||||
|
resetScroll,
|
||||||
|
}) => {
|
||||||
|
const { setSettings, getSettings, getMergedSettings } =
|
||||||
|
useContext(ViewerContext);
|
||||||
|
|
||||||
|
const mergedSettings = getMergedSettings(path);
|
||||||
|
|
||||||
|
const { register, watch } = useForm({
|
||||||
|
resolver: yupResolver(viewSettingsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
// this is a mess and should be fixed
|
||||||
|
showEditor: true, // doesn't matter
|
||||||
|
chartHeight: mergedSettings.height,
|
||||||
|
showSummary: mergedSettings.distributionPlotSettings.showSummary,
|
||||||
|
logX: mergedSettings.distributionPlotSettings.logX,
|
||||||
|
expY: mergedSettings.distributionPlotSettings.expY,
|
||||||
|
tickFormat:
|
||||||
|
mergedSettings.distributionPlotSettings.format || defaultTickFormat,
|
||||||
|
title: mergedSettings.distributionPlotSettings.title,
|
||||||
|
color: mergedSettings.distributionPlotSettings.color || defaultColor,
|
||||||
|
minX: mergedSettings.distributionPlotSettings.minX,
|
||||||
|
maxX: mergedSettings.distributionPlotSettings.maxX,
|
||||||
|
distributionChartActions: mergedSettings.distributionPlotSettings.actions,
|
||||||
|
diagramStart: mergedSettings.chartSettings.start,
|
||||||
|
diagramStop: mergedSettings.chartSettings.stop,
|
||||||
|
diagramCount: mergedSettings.chartSettings.count,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = watch((vars) => {
|
||||||
|
const settings = getSettings(path); // get the latest version
|
||||||
|
setSettings(path, {
|
||||||
|
...settings,
|
||||||
|
distributionPlotSettings: {
|
||||||
|
showSummary: vars.showSummary,
|
||||||
|
logX: vars.logX,
|
||||||
|
expY: vars.expY,
|
||||||
|
format: vars.tickFormat,
|
||||||
|
title: vars.title,
|
||||||
|
color: vars.color,
|
||||||
|
minX: vars.minX,
|
||||||
|
maxX: vars.maxX,
|
||||||
|
actions: vars.distributionChartActions,
|
||||||
|
},
|
||||||
|
chartSettings: {
|
||||||
|
start: vars.diagramStart,
|
||||||
|
stop: vars.diagramStop,
|
||||||
|
count: vars.diagramCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [getSettings, setSettings, onChange, path, watch]);
|
||||||
|
|
||||||
|
const { getLeftPanelElement } = useContext(PlaygroundContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal container={getLeftPanelElement()} close={close}>
|
||||||
|
<Modal.Header>
|
||||||
|
Chart settings
|
||||||
|
{path.length ? (
|
||||||
|
<>
|
||||||
|
{" for "}
|
||||||
|
<span
|
||||||
|
title="Scroll to item"
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={resetScroll}
|
||||||
|
>
|
||||||
|
{pathAsString(path)}
|
||||||
|
</span>{" "}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<ViewSettings
|
||||||
|
register={register}
|
||||||
|
withShowEditorSetting={false}
|
||||||
|
withFunctionSettings={withFunctionSettings}
|
||||||
|
disableLogXSetting={disableLogX}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ItemSettingsMenu: React.FC<Props> = (props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { enableLocalSettings, setSettings, getSettings } =
|
||||||
|
useContext(ViewerContext);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
if (!enableLocalSettings) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const settings = getSettings(props.path);
|
||||||
|
|
||||||
|
const resetScroll = () => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
window.scroll({
|
||||||
|
top: ref.current.getBoundingClientRect().y + window.scrollY,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2" ref={ref}>
|
||||||
|
<CogIcon
|
||||||
|
className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
/>
|
||||||
|
{settings.distributionPlotSettings || settings.chartSettings ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSettings(props.path, {
|
||||||
|
...settings,
|
||||||
|
distributionPlotSettings: undefined,
|
||||||
|
chartSettings: undefined,
|
||||||
|
});
|
||||||
|
props.onChange();
|
||||||
|
}}
|
||||||
|
className="text-xs px-1 py-0.5 rounded bg-slate-300"
|
||||||
|
>
|
||||||
|
Reset settings
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{isOpen ? (
|
||||||
|
<ItemSettingsModal
|
||||||
|
{...props}
|
||||||
|
close={() => setIsOpen(false)}
|
||||||
|
resetScroll={resetScroll}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { useContext, useReducer } from "react";
|
||||||
|
import { Tooltip } from "../ui/Tooltip";
|
||||||
|
import { LocalItemSettings, MergedItemSettings } from "./utils";
|
||||||
|
import { ViewerContext } from "./ViewerContext";
|
||||||
|
|
||||||
|
type SettingsMenuParams = {
|
||||||
|
onChange: () => void; // used to notify VariableBox that settings have changed, so that VariableBox could re-render itself
|
||||||
|
};
|
||||||
|
|
||||||
|
type VariableBoxProps = {
|
||||||
|
path: string[];
|
||||||
|
heading: string;
|
||||||
|
renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode;
|
||||||
|
children: (settings: MergedItemSettings) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VariableBox: React.FC<VariableBoxProps> = ({
|
||||||
|
path,
|
||||||
|
heading = "Error",
|
||||||
|
renderSettingsMenu,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const { setSettings, getSettings, getMergedSettings } =
|
||||||
|
useContext(ViewerContext);
|
||||||
|
|
||||||
|
// Since ViewerContext doesn't keep the actual settings, VariableBox won't rerender when setSettings is called.
|
||||||
|
// So we use `forceUpdate` to force rerendering.
|
||||||
|
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
const settings = getSettings(path);
|
||||||
|
|
||||||
|
const setSettingsAndUpdate = (newSettings: LocalItemSettings) => {
|
||||||
|
setSettings(path, newSettings);
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCollapsed = () => {
|
||||||
|
setSettingsAndUpdate({ ...settings, collapsed: !settings.collapsed });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTopLevel = path.length === 0;
|
||||||
|
const name = isTopLevel ? "Result" : path[path.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header className="inline-flex space-x-1">
|
||||||
|
<Tooltip text={heading}>
|
||||||
|
<span
|
||||||
|
className="text-slate-500 font-mono text-sm cursor-pointer"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
>
|
||||||
|
{name}:
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{settings.collapsed ? (
|
||||||
|
<span
|
||||||
|
className="rounded p-0.5 bg-slate-200 text-slate-500 font-mono text-xs cursor-pointer"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
) : renderSettingsMenu ? (
|
||||||
|
renderSettingsMenu({ onChange: forceUpdate })
|
||||||
|
) : null}
|
||||||
|
</header>
|
||||||
|
{settings.collapsed ? null : (
|
||||||
|
<div className="flex w-full">
|
||||||
|
{path.length ? (
|
||||||
|
<div
|
||||||
|
className="border-l-2 border-slate-200 hover:border-indigo-600 w-4 cursor-pointer"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
></div>
|
||||||
|
) : null}
|
||||||
|
<div className="grow">{children(getMergedSettings(path))}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { defaultEnvironment } from "@quri/squiggle-lang";
|
||||||
|
import React from "react";
|
||||||
|
import { LocalItemSettings, MergedItemSettings, Path } from "./utils";
|
||||||
|
|
||||||
|
type ViewerContextShape = {
|
||||||
|
// Note that we don't store settings themselves in the context (that would cause rerenders of the entire tree on each settings update).
|
||||||
|
// Instead, we keep settings in local state and notify the global context via setSettings to pass them down the component tree again if it got rebuilt from scratch.
|
||||||
|
// See ./SquiggleViewer.tsx and ./VariableBox.tsx for other implementation details on this.
|
||||||
|
getSettings(path: Path): LocalItemSettings;
|
||||||
|
getMergedSettings(path: Path): MergedItemSettings;
|
||||||
|
setSettings(path: Path, value: LocalItemSettings): void;
|
||||||
|
enableLocalSettings: boolean; // show local settings icon in the UI
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewerContext = React.createContext<ViewerContextShape>({
|
||||||
|
getSettings: () => ({ collapsed: false }),
|
||||||
|
getMergedSettings: () => ({
|
||||||
|
collapsed: false,
|
||||||
|
// copy-pasted from SquiggleChart
|
||||||
|
chartSettings: {
|
||||||
|
start: 0,
|
||||||
|
stop: 10,
|
||||||
|
count: 100,
|
||||||
|
},
|
||||||
|
distributionPlotSettings: {
|
||||||
|
showSummary: false,
|
||||||
|
logX: false,
|
||||||
|
expY: false,
|
||||||
|
},
|
||||||
|
environment: defaultEnvironment,
|
||||||
|
height: 150,
|
||||||
|
}),
|
||||||
|
setSettings() {},
|
||||||
|
enableLocalSettings: false,
|
||||||
|
});
|
100
packages/components/src/components/SquiggleViewer/index.tsx
Normal file
100
packages/components/src/components/SquiggleViewer/index.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import React, { useCallback, useRef } from "react";
|
||||||
|
import { environment } from "@quri/squiggle-lang";
|
||||||
|
import { DistributionPlottingSettings } from "../DistributionChart";
|
||||||
|
import { FunctionChartSettings } from "../FunctionChart";
|
||||||
|
import { ExpressionViewer } from "./ExpressionViewer";
|
||||||
|
import { ViewerContext } from "./ViewerContext";
|
||||||
|
import {
|
||||||
|
LocalItemSettings,
|
||||||
|
MergedItemSettings,
|
||||||
|
Path,
|
||||||
|
pathAsString,
|
||||||
|
} from "./utils";
|
||||||
|
import { useSquiggle } from "../../lib/hooks";
|
||||||
|
import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/** The output of squiggle's run */
|
||||||
|
result: ReturnType<typeof useSquiggle>;
|
||||||
|
width?: number;
|
||||||
|
height: number;
|
||||||
|
distributionPlotSettings: DistributionPlottingSettings;
|
||||||
|
/** Settings for displaying functions */
|
||||||
|
chartSettings: FunctionChartSettings;
|
||||||
|
/** Environment for further function executions */
|
||||||
|
environment: environment;
|
||||||
|
enableLocalSettings?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Settings = {
|
||||||
|
[k: string]: LocalItemSettings;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSettings: LocalItemSettings = { collapsed: false };
|
||||||
|
|
||||||
|
export const SquiggleViewer: React.FC<Props> = ({
|
||||||
|
result,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
distributionPlotSettings,
|
||||||
|
chartSettings,
|
||||||
|
environment,
|
||||||
|
enableLocalSettings = false,
|
||||||
|
}) => {
|
||||||
|
// can't store settings in the state because we don't want to rerender the entire tree on every change
|
||||||
|
const settingsRef = useRef<Settings>({});
|
||||||
|
|
||||||
|
const getSettings = useCallback(
|
||||||
|
(path: Path) => {
|
||||||
|
return settingsRef.current[pathAsString(path)] || defaultSettings;
|
||||||
|
},
|
||||||
|
[settingsRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSettings = useCallback(
|
||||||
|
(path: Path, value: LocalItemSettings) => {
|
||||||
|
settingsRef.current[pathAsString(path)] = value;
|
||||||
|
},
|
||||||
|
[settingsRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMergedSettings = useCallback(
|
||||||
|
(path: Path) => {
|
||||||
|
const localSettings = getSettings(path);
|
||||||
|
const result: MergedItemSettings = {
|
||||||
|
distributionPlotSettings: {
|
||||||
|
...distributionPlotSettings,
|
||||||
|
...(localSettings.distributionPlotSettings || {}),
|
||||||
|
},
|
||||||
|
chartSettings: {
|
||||||
|
...chartSettings,
|
||||||
|
...(localSettings.chartSettings || {}),
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
...environment,
|
||||||
|
...(localSettings.environment || {}),
|
||||||
|
},
|
||||||
|
height: localSettings.height || height,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[distributionPlotSettings, chartSettings, environment, height, getSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewerContext.Provider
|
||||||
|
value={{
|
||||||
|
getSettings,
|
||||||
|
setSettings,
|
||||||
|
getMergedSettings,
|
||||||
|
enableLocalSettings,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{result.tag === "Ok" ? (
|
||||||
|
<ExpressionViewer path={[]} expression={result.value} width={width} />
|
||||||
|
) : (
|
||||||
|
<SquiggleErrorAlert error={result.value} />
|
||||||
|
)}
|
||||||
|
</ViewerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
22
packages/components/src/components/SquiggleViewer/utils.ts
Normal file
22
packages/components/src/components/SquiggleViewer/utils.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { DistributionPlottingSettings } from "../DistributionChart";
|
||||||
|
import { FunctionChartSettings } from "../FunctionChart";
|
||||||
|
import { environment } from "@quri/squiggle-lang";
|
||||||
|
|
||||||
|
export type LocalItemSettings = {
|
||||||
|
collapsed: boolean;
|
||||||
|
distributionPlotSettings?: Partial<DistributionPlottingSettings>;
|
||||||
|
chartSettings?: Partial<FunctionChartSettings>;
|
||||||
|
height?: number;
|
||||||
|
environment?: Partial<environment>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MergedItemSettings = {
|
||||||
|
distributionPlotSettings: DistributionPlottingSettings;
|
||||||
|
chartSettings: FunctionChartSettings;
|
||||||
|
height: number;
|
||||||
|
environment: environment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Path = string[];
|
||||||
|
|
||||||
|
export const pathAsString = (path: Path) => path.join(".");
|
163
packages/components/src/components/ViewSettings.tsx
Normal file
163
packages/components/src/components/ViewSettings.tsx
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { UseFormRegister } from "react-hook-form";
|
||||||
|
import { InputItem } from "./ui/InputItem";
|
||||||
|
import { Checkbox } from "./ui/Checkbox";
|
||||||
|
import { HeadedSection } from "./ui/HeadedSection";
|
||||||
|
import { Text } from "./ui/Text";
|
||||||
|
import {
|
||||||
|
defaultColor,
|
||||||
|
defaultTickFormat,
|
||||||
|
} from "../lib/distributionSpecBuilder";
|
||||||
|
|
||||||
|
export const viewSettingsSchema = yup.object({}).shape({
|
||||||
|
chartHeight: yup.number().required().positive().integer().default(350),
|
||||||
|
showSummary: yup.boolean().required(),
|
||||||
|
showEditor: yup.boolean().required(),
|
||||||
|
logX: yup.boolean().required(),
|
||||||
|
expY: yup.boolean().required(),
|
||||||
|
tickFormat: yup.string().default(defaultTickFormat),
|
||||||
|
title: yup.string(),
|
||||||
|
color: yup.string().default(defaultColor).required(),
|
||||||
|
minX: yup.number(),
|
||||||
|
maxX: yup.number(),
|
||||||
|
distributionChartActions: yup.boolean(),
|
||||||
|
diagramStart: yup.number().required().positive().integer().default(0).min(0),
|
||||||
|
diagramStop: yup.number().required().positive().integer().default(10).min(0),
|
||||||
|
diagramCount: yup.number().required().positive().integer().default(20).min(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormFields = yup.InferType<typeof viewSettingsSchema>;
|
||||||
|
|
||||||
|
// This component is used in two places: for global settings in SquigglePlayground, and for item-specific settings in modal dialogs.
|
||||||
|
export const ViewSettings: React.FC<{
|
||||||
|
withShowEditorSetting?: boolean;
|
||||||
|
withFunctionSettings?: boolean;
|
||||||
|
disableLogXSetting?: boolean;
|
||||||
|
register: UseFormRegister<FormFields>;
|
||||||
|
}> = ({
|
||||||
|
withShowEditorSetting = true,
|
||||||
|
withFunctionSettings = true,
|
||||||
|
disableLogXSetting,
|
||||||
|
register,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
|
||||||
|
<HeadedSection title="General Display Settings">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{withShowEditorSetting ? (
|
||||||
|
<Checkbox
|
||||||
|
name="showEditor"
|
||||||
|
register={register}
|
||||||
|
label="Show code editor on left"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<InputItem
|
||||||
|
name="chartHeight"
|
||||||
|
type="number"
|
||||||
|
register={register}
|
||||||
|
label="Chart Height (in pixels)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HeadedSection>
|
||||||
|
|
||||||
|
<div className="pt-8">
|
||||||
|
<HeadedSection title="Distribution Display Settings">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Checkbox
|
||||||
|
register={register}
|
||||||
|
name="logX"
|
||||||
|
label="Show x scale logarithmically"
|
||||||
|
disabled={disableLogXSetting}
|
||||||
|
tooltip={
|
||||||
|
disableLogXSetting
|
||||||
|
? "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
register={register}
|
||||||
|
name="expY"
|
||||||
|
label="Show y scale exponentially"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
register={register}
|
||||||
|
name="distributionChartActions"
|
||||||
|
label="Show vega chart controls"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
register={register}
|
||||||
|
name="showSummary"
|
||||||
|
label="Show summary statistics"
|
||||||
|
/>
|
||||||
|
<InputItem
|
||||||
|
name="minX"
|
||||||
|
type="number"
|
||||||
|
register={register}
|
||||||
|
label="Min X Value"
|
||||||
|
/>
|
||||||
|
<InputItem
|
||||||
|
name="maxX"
|
||||||
|
type="number"
|
||||||
|
register={register}
|
||||||
|
label="Max X Value"
|
||||||
|
/>
|
||||||
|
<InputItem
|
||||||
|
name="title"
|
||||||
|
type="text"
|
||||||
|
register={register}
|
||||||
|
label="Title"
|
||||||
|
/>
|
||||||
|
<InputItem
|
||||||
|
name="tickFormat"
|
||||||
|
type="text"
|
||||||
|
register={register}
|
||||||
|
label="Tick Format"
|
||||||
|
/>
|
||||||
|
<InputItem
|
||||||
|
name="color"
|
||||||
|
type="color"
|
||||||
|
register={register}
|
||||||
|
label="Color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HeadedSection>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{withFunctionSettings ? (
|
||||||
|
<div className="pt-8">
|
||||||
|
<HeadedSection title="Function Display Settings">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Text>
|
||||||
|
When displaying functions of single variables that return
|
||||||
|
numbers or distributions, we need to use defaults for the
|
||||||
|
x-axis. We need to select a minimum and maximum value of x to
|
||||||
|
sample, and a number n of the number of points to sample.
|
||||||
|
</Text>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<InputItem
|
||||||
|
type="number"
|
||||||
|
name="diagramStart"
|
||||||
|
register={register}
|
||||||
|
label="Min X Value"
|
||||||
|
/>
|
||||||
|
<InputItem
|
||||||
|
type="number"
|
||||||
|
name="diagramStop"
|
||||||
|
register={register}
|
||||||
|
label="Max X Value"
|
||||||
|
/>
|
||||||
|
<InputItem
|
||||||
|
type="number"
|
||||||
|
name="diagramCount"
|
||||||
|
register={register}
|
||||||
|
label="Points between X min and X max to sample"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HeadedSection>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
22
packages/components/src/components/ui/Button.tsx
Normal file
22
packages/components/src/components/ui/Button.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
wide?: boolean; // stretch the button horizontally
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Button: React.FC<Props> = ({ onClick, wide, children }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"rounded-md py-1.5 px-2 bg-slate-500 text-white text-xs font-semibold flex items-center justify-center space-x-1",
|
||||||
|
wide && "w-full"
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
37
packages/components/src/components/ui/Checkbox.tsx
Normal file
37
packages/components/src/components/ui/Checkbox.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
import { Path, UseFormRegister } from "react-hook-form";
|
||||||
|
|
||||||
|
export function Checkbox<T>({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
register,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
}: {
|
||||||
|
name: Path<T>;
|
||||||
|
label: string;
|
||||||
|
register: UseFormRegister<T>;
|
||||||
|
disabled?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="flex items-center" title={tooltip}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
disabled={disabled}
|
||||||
|
{...register(name)}
|
||||||
|
className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */}
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"ml-3 text-sm font-medium",
|
||||||
|
disabled ? "text-gray-400" : "text-gray-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
13
packages/components/src/components/ui/HeadedSection.tsx
Normal file
13
packages/components/src/components/ui/HeadedSection.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const HeadedSection: React.FC<{
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ title, children }) => (
|
||||||
|
<div>
|
||||||
|
<header className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{title}
|
||||||
|
</header>
|
||||||
|
<div className="mt-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
25
packages/components/src/components/ui/InputItem.tsx
Normal file
25
packages/components/src/components/ui/InputItem.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Path, UseFormRegister } from "react-hook-form";
|
||||||
|
|
||||||
|
export function InputItem<T>({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
register,
|
||||||
|
}: {
|
||||||
|
name: Path<T>;
|
||||||
|
label: string;
|
||||||
|
type: "number" | "text" | "color";
|
||||||
|
register: UseFormRegister<T>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="block">
|
||||||
|
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
{...register(name, { valueAsNumber: type === "number" })}
|
||||||
|
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
184
packages/components/src/components/ui/Modal.tsx
Normal file
184
packages/components/src/components/ui/Modal.tsx
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useWindowScroll, useWindowSize } from "react-use";
|
||||||
|
|
||||||
|
type ModalContextShape = {
|
||||||
|
close: () => void;
|
||||||
|
};
|
||||||
|
const ModalContext = React.createContext<ModalContextShape>({
|
||||||
|
close: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Overlay: React.FC = () => {
|
||||||
|
const { close } = useContext(ModalContext);
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 -z-10 bg-black"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 0.1 }}
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalHeader: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ children }) => {
|
||||||
|
const { close } = useContext(ModalContext);
|
||||||
|
return (
|
||||||
|
<header className="px-5 py-3 border-b border-gray-200 font-bold flex items-center justify-between">
|
||||||
|
<div>{children}</div>
|
||||||
|
<button
|
||||||
|
className="px-1 bg-transparent cursor-pointer text-gray-700 hover:text-accent-500"
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
<XIcon className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO - get rid of forwardRef, support `focus` and `{...hotkeys}` via smart props
|
||||||
|
const ModalBody = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
JSX.IntrinsicElements["div"]
|
||||||
|
>(function ModalBody(props, ref) {
|
||||||
|
return <div ref={ref} className="px-5 py-3 overflow-auto" {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ModalFooter: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<div className="px-5 py-3 border-t border-gray-200">{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModalWindow: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
container?: HTMLElement;
|
||||||
|
}> = ({ children, container }) => {
|
||||||
|
// This component works in two possible modes:
|
||||||
|
// 1. container mode - the modal is rendered inside a container element
|
||||||
|
// 2. centered mode - the modal is rendered in the middle of the screen
|
||||||
|
// The mode is determined by the presence of the `container` prop and by whether the available space is large enough to fit the modal.
|
||||||
|
|
||||||
|
// Necessary for container mode - need to reposition the modal on scroll and resize events.
|
||||||
|
useWindowSize();
|
||||||
|
useWindowScroll();
|
||||||
|
|
||||||
|
let position:
|
||||||
|
| {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
maxWidth: number;
|
||||||
|
maxHeight: number;
|
||||||
|
transform: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
// If available space in `visibleRect` is smaller than these, fallback to positioning in the middle of the screen.
|
||||||
|
const minWidth = 384;
|
||||||
|
const minHeight = 300;
|
||||||
|
const offset = 8;
|
||||||
|
const naturalWidth = 576; // maximum possible width; modal tries to take this much space, but can be smaller
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
const { clientWidth: screenWidth, clientHeight: screenHeight } =
|
||||||
|
document.documentElement;
|
||||||
|
const rect = container?.getBoundingClientRect();
|
||||||
|
|
||||||
|
const visibleRect = {
|
||||||
|
left: Math.max(rect.left, 0),
|
||||||
|
right: Math.min(rect.right, screenWidth),
|
||||||
|
top: Math.max(rect.top, 0),
|
||||||
|
bottom: Math.min(rect.bottom, screenHeight),
|
||||||
|
};
|
||||||
|
const maxWidth = visibleRect.right - visibleRect.left - 2 * offset;
|
||||||
|
const maxHeight = visibleRect.bottom - visibleRect.top - 2 * offset;
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
left: visibleRect.left + (visibleRect.right - visibleRect.left) / 2,
|
||||||
|
top: visibleRect.top + (visibleRect.bottom - visibleRect.top) / 2,
|
||||||
|
};
|
||||||
|
position = {
|
||||||
|
left: center.left,
|
||||||
|
top: center.top,
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
};
|
||||||
|
if (maxWidth < minWidth || maxHeight < minHeight) {
|
||||||
|
position = undefined; // modal is hard to fit in the container, fallback to positioning it in the middle of the screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-white rounded-md shadow-toast flex flex-col overflow-auto border",
|
||||||
|
position ? "fixed" : null
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: naturalWidth,
|
||||||
|
...(position ?? {
|
||||||
|
maxHeight: "calc(100% - 20px)",
|
||||||
|
maxWidth: "calc(100% - 20px)",
|
||||||
|
width: naturalWidth,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModalType = React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
container?: HTMLElement; // if specified, modal will be positioned over the visible part of the container, if it's not too small
|
||||||
|
close: () => void;
|
||||||
|
}> & {
|
||||||
|
Body: typeof ModalBody;
|
||||||
|
Footer: typeof ModalFooter;
|
||||||
|
Header: typeof ModalHeader;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Modal: ModalType = ({ children, container, close }) => {
|
||||||
|
const [el] = React.useState(() => document.createElement("div"));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(el);
|
||||||
|
};
|
||||||
|
}, [el]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
};
|
||||||
|
}, [close]);
|
||||||
|
|
||||||
|
const modal = (
|
||||||
|
<ModalContext.Provider value={{ close }}>
|
||||||
|
<div className="squiggle">
|
||||||
|
<div className="fixed inset-0 z-40 flex justify-center items-center">
|
||||||
|
<Overlay />
|
||||||
|
<ModalWindow container={container}>{children}</ModalWindow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(modal, container || el);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.Body = ModalBody;
|
||||||
|
Modal.Footer = ModalFooter;
|
||||||
|
Modal.Header = ModalHeader;
|
60
packages/components/src/components/ui/StyledTab.tsx
Normal file
60
packages/components/src/components/ui/StyledTab.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { Fragment } from "react";
|
||||||
|
import { Tab } from "@headlessui/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type StyledTabProps = {
|
||||||
|
name: string;
|
||||||
|
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StyledTabType = React.FC<StyledTabProps> & {
|
||||||
|
List: React.FC<{ children: React.ReactNode }>;
|
||||||
|
Group: typeof Tab.Group;
|
||||||
|
Panels: typeof Tab.Panels;
|
||||||
|
Panel: typeof Tab.Panel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StyledTab: StyledTabType = ({ name, icon: Icon }) => {
|
||||||
|
return (
|
||||||
|
<Tab as={Fragment}>
|
||||||
|
{({ selected }) => (
|
||||||
|
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium",
|
||||||
|
selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={clsx(
|
||||||
|
"-ml-0.5 mr-2 h-4 w-4",
|
||||||
|
selected
|
||||||
|
? "text-slate-500"
|
||||||
|
: "text-gray-400 group-hover:text-gray-900"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
selected
|
||||||
|
? "text-gray-900"
|
||||||
|
: "text-gray-600 group-hover:text-gray-900"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StyledTab.List = ({ children }) => (
|
||||||
|
<Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
|
||||||
|
{children}
|
||||||
|
</Tab.List>
|
||||||
|
);
|
||||||
|
|
||||||
|
StyledTab.Group = Tab.Group;
|
||||||
|
StyledTab.Panels = Tab.Panels;
|
||||||
|
StyledTab.Panel = Tab.Panel;
|
5
packages/components/src/components/ui/Text.tsx
Normal file
5
packages/components/src/components/ui/Text.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const Text: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<p className="text-sm text-gray-500">{children}</p>
|
||||||
|
);
|
47
packages/components/src/components/ui/Toggle.tsx
Normal file
47
packages/components/src/components/ui/Toggle.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { RefreshIcon } from "@heroicons/react/solid";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type IconType = (props: React.ComponentProps<"svg">) => JSX.Element;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: boolean;
|
||||||
|
onChange: (status: boolean) => void;
|
||||||
|
texts: [string, string];
|
||||||
|
icons: [IconType, IconType];
|
||||||
|
spinIcon?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Toggle: React.FC<Props> = ({
|
||||||
|
status,
|
||||||
|
onChange,
|
||||||
|
texts: [onText, offText],
|
||||||
|
icons: [OnIcon, OffIcon],
|
||||||
|
spinIcon,
|
||||||
|
}) => {
|
||||||
|
const CurrentIcon = status ? OnIcon : OffIcon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"rounded-md py-0.5 bg-slate-500 text-white text-xs font-semibold flex items-center space-x-1",
|
||||||
|
status ? "bg-slate-500" : "bg-gray-400",
|
||||||
|
status ? "pl-1 pr-3" : "pl-3 pr-1",
|
||||||
|
!status && "flex-row-reverse space-x-reverse"
|
||||||
|
)}
|
||||||
|
onClick={() => onChange(!status)}
|
||||||
|
>
|
||||||
|
<div className="relative w-6 h-6" key={String(spinIcon)}>
|
||||||
|
<CurrentIcon
|
||||||
|
className={clsx(
|
||||||
|
"w-6 h-6 absolute opacity-100",
|
||||||
|
spinIcon && "animate-hide"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{spinIcon && (
|
||||||
|
<RefreshIcon className="w-6 h-6 absolute opacity-0 animate-appear-and-spin" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span>{status ? onText : offText}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
64
packages/components/src/components/ui/Tooltip.tsx
Normal file
64
packages/components/src/components/ui/Tooltip.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React, { cloneElement, useState } from "react";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
flip,
|
||||||
|
shift,
|
||||||
|
useDismiss,
|
||||||
|
useFloating,
|
||||||
|
useHover,
|
||||||
|
useInteractions,
|
||||||
|
useRole,
|
||||||
|
} from "@floating-ui/react-dom-interactions";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip: React.FC<Props> = ({ text, children }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { x, y, reference, floating, strategy, context } = useFloating({
|
||||||
|
placement: "top",
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
middleware: [shift(), flip()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
|
useHover(context),
|
||||||
|
useRole(context, { role: "tooltip" }),
|
||||||
|
useDismiss(context),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cloneElement(
|
||||||
|
children,
|
||||||
|
getReferenceProps({ ref: reference, ...children.props })
|
||||||
|
)}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
{...getFloatingProps({
|
||||||
|
ref: floating,
|
||||||
|
className:
|
||||||
|
"text-xs p-2 border border-gray-300 rounded bg-white z-10",
|
||||||
|
style: {
|
||||||
|
position: strategy,
|
||||||
|
top: y ?? 0,
|
||||||
|
left: x ?? 0,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="font-mono whitespace-pre">{text}</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +1,7 @@
|
||||||
export { SquiggleChart } from "./components/SquiggleChart";
|
export { SquiggleChart } from "./components/SquiggleChart";
|
||||||
export {
|
export { SquiggleEditor, SquigglePartial } from "./components/SquiggleEditor";
|
||||||
SquiggleEditor,
|
export { SquigglePlayground } from "./components/SquigglePlayground";
|
||||||
SquigglePartial,
|
|
||||||
renderSquiggleEditorToDom,
|
|
||||||
renderSquigglePartialToDom,
|
|
||||||
} from "./components/SquiggleEditor";
|
|
||||||
export {
|
|
||||||
default as SquigglePlayground,
|
|
||||||
renderSquigglePlaygroundToDom,
|
|
||||||
} from "./components/SquigglePlayground";
|
|
||||||
export { SquiggleContainer } from "./components/SquiggleContainer";
|
export { SquiggleContainer } from "./components/SquiggleContainer";
|
||||||
|
export { SquiggleEditorWithImportedBindings } from "./components/SquiggleEditorWithImportedBindings";
|
||||||
|
|
||||||
export { mergeBindings } from "@quri/squiggle-lang";
|
export { mergeBindings } from "@quri/squiggle-lang";
|
||||||
|
|
259
packages/components/src/lib/distributionSpecBuilder.ts
Normal file
259
packages/components/src/lib/distributionSpecBuilder.ts
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
import { VisualizationSpec } from "react-vega";
|
||||||
|
import type { LogScale, LinearScale, PowScale } from "vega";
|
||||||
|
|
||||||
|
export type DistributionChartSpecOptions = {
|
||||||
|
/** Set the x scale to be logarithmic by deault */
|
||||||
|
logX: boolean;
|
||||||
|
/** Set the y scale to be exponential by deault */
|
||||||
|
expY: boolean;
|
||||||
|
/** The minimum x coordinate shown on the chart */
|
||||||
|
minX?: number;
|
||||||
|
/** The maximum x coordinate shown on the chart */
|
||||||
|
maxX?: number;
|
||||||
|
/** The color of the chart */
|
||||||
|
color?: string;
|
||||||
|
/** The title of the chart */
|
||||||
|
title?: string;
|
||||||
|
/** The formatting of the ticks */
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let linearXScale: LinearScale = {
|
||||||
|
name: "xscale",
|
||||||
|
clamp: true,
|
||||||
|
type: "linear",
|
||||||
|
range: "width",
|
||||||
|
zero: false,
|
||||||
|
nice: false,
|
||||||
|
domain: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
data: "con",
|
||||||
|
field: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "dis",
|
||||||
|
field: "x",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export let linearYScale: LinearScale = {
|
||||||
|
name: "yscale",
|
||||||
|
type: "linear",
|
||||||
|
range: "height",
|
||||||
|
zero: true,
|
||||||
|
domain: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
data: "con",
|
||||||
|
field: "y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "dis",
|
||||||
|
field: "y",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export let logXScale: LogScale = {
|
||||||
|
name: "xscale",
|
||||||
|
type: "log",
|
||||||
|
range: "width",
|
||||||
|
zero: false,
|
||||||
|
base: 10,
|
||||||
|
nice: false,
|
||||||
|
clamp: true,
|
||||||
|
domain: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
data: "con",
|
||||||
|
field: "x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "dis",
|
||||||
|
field: "x",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export let expYScale: PowScale = {
|
||||||
|
name: "yscale",
|
||||||
|
type: "pow",
|
||||||
|
exponent: 0.1,
|
||||||
|
range: "height",
|
||||||
|
zero: true,
|
||||||
|
nice: false,
|
||||||
|
domain: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
data: "con",
|
||||||
|
field: "y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "dis",
|
||||||
|
field: "y",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTickFormat = ".9~s";
|
||||||
|
export const defaultColor = "#739ECC";
|
||||||
|
|
||||||
|
export function buildVegaSpec(
|
||||||
|
specOptions: DistributionChartSpecOptions
|
||||||
|
): VisualizationSpec {
|
||||||
|
let {
|
||||||
|
format = defaultTickFormat,
|
||||||
|
color = defaultColor,
|
||||||
|
title,
|
||||||
|
minX,
|
||||||
|
maxX,
|
||||||
|
logX,
|
||||||
|
expY,
|
||||||
|
} = specOptions;
|
||||||
|
|
||||||
|
let xScale = logX ? logXScale : linearXScale;
|
||||||
|
if (minX !== undefined && Number.isFinite(minX)) {
|
||||||
|
xScale = { ...xScale, domainMin: minX };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxX !== undefined && Number.isFinite(maxX)) {
|
||||||
|
xScale = { ...xScale, domainMax: maxX };
|
||||||
|
}
|
||||||
|
|
||||||
|
let spec: VisualizationSpec = {
|
||||||
|
$schema: "https://vega.github.io/schema/vega/v5.json",
|
||||||
|
description: "A basic area chart example",
|
||||||
|
width: 500,
|
||||||
|
height: 100,
|
||||||
|
padding: 5,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: "con",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dis",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
signals: [],
|
||||||
|
scales: [xScale, expY ? expYScale : linearYScale],
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
orient: "bottom",
|
||||||
|
scale: "xscale",
|
||||||
|
labelColor: "#727d93",
|
||||||
|
tickColor: "#fff",
|
||||||
|
tickOpacity: 0.0,
|
||||||
|
domainColor: "#fff",
|
||||||
|
domainOpacity: 0.0,
|
||||||
|
format: format,
|
||||||
|
tickCount: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
marks: [
|
||||||
|
{
|
||||||
|
type: "area",
|
||||||
|
from: {
|
||||||
|
data: "con",
|
||||||
|
},
|
||||||
|
encode: {
|
||||||
|
update: {
|
||||||
|
interpolate: { value: "linear" },
|
||||||
|
x: {
|
||||||
|
scale: "xscale",
|
||||||
|
field: "x",
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
scale: "yscale",
|
||||||
|
field: "y",
|
||||||
|
},
|
||||||
|
y2: {
|
||||||
|
scale: "yscale",
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
value: color,
|
||||||
|
},
|
||||||
|
fillOpacity: {
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rect",
|
||||||
|
from: {
|
||||||
|
data: "dis",
|
||||||
|
},
|
||||||
|
encode: {
|
||||||
|
enter: {
|
||||||
|
width: {
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
x: {
|
||||||
|
scale: "xscale",
|
||||||
|
field: "x",
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
scale: "yscale",
|
||||||
|
field: "y",
|
||||||
|
},
|
||||||
|
y2: {
|
||||||
|
scale: "yscale",
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
value: "#2f65a7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "symbol",
|
||||||
|
from: {
|
||||||
|
data: "dis",
|
||||||
|
},
|
||||||
|
encode: {
|
||||||
|
enter: {
|
||||||
|
shape: {
|
||||||
|
value: "circle",
|
||||||
|
},
|
||||||
|
size: [{ value: 100 }],
|
||||||
|
tooltip: {
|
||||||
|
signal: "{ probability: datum.y, value: datum.x }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
x: {
|
||||||
|
scale: "xscale",
|
||||||
|
field: "x",
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
scale: "yscale",
|
||||||
|
field: "y",
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
value: "#1e4577",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
if (title) {
|
||||||
|
spec = {
|
||||||
|
...spec,
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec;
|
||||||
|
}
|
5
packages/components/src/lib/distributionUtils.ts
Normal file
5
packages/components/src/lib/distributionUtils.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { shape } from "@quri/squiggle-lang";
|
||||||
|
|
||||||
|
export const hasMassBelowZero = (shape: shape) =>
|
||||||
|
shape.continuous.some((x) => x.x <= 0) ||
|
||||||
|
shape.discrete.some((x) => x.x <= 0);
|
3
packages/components/src/lib/hooks/index.ts
Normal file
3
packages/components/src/lib/hooks/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { useMaybeControlledValue } from "./useMaybeControlledValue";
|
||||||
|
export { useSquiggle, useSquigglePartial } from "./useSquiggle";
|
||||||
|
export { useRunnerState } from "./useRunnerState";
|
22
packages/components/src/lib/hooks/useMaybeControlledValue.ts
Normal file
22
packages/components/src/lib/hooks/useMaybeControlledValue.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type ControlledValueArgs<T> = {
|
||||||
|
value?: T;
|
||||||
|
defaultValue: T;
|
||||||
|
onChange?: (x: T) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useMaybeControlledValue<T>(
|
||||||
|
args: ControlledValueArgs<T>
|
||||||
|
): [T, (x: T) => void] {
|
||||||
|
let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue);
|
||||||
|
let value = args.value ?? uncontrolledValue;
|
||||||
|
let onChange = (newValue: T) => {
|
||||||
|
if (args.value === undefined) {
|
||||||
|
// uncontrolled mode
|
||||||
|
setUncontrolledValue(newValue);
|
||||||
|
}
|
||||||
|
args.onChange?.(newValue);
|
||||||
|
};
|
||||||
|
return [value, onChange];
|
||||||
|
}
|
100
packages/components/src/lib/hooks/useRunnerState.ts
Normal file
100
packages/components/src/lib/hooks/useRunnerState.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { useLayoutEffect, useReducer } from "react";
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
autorunMode: boolean;
|
||||||
|
renderedCode: string;
|
||||||
|
// "prepared" is for rendering a spinner; "run" for executing squiggle code; then it gets back to "none" on the next render
|
||||||
|
runningState: "none" | "prepared" | "run";
|
||||||
|
executionId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildInitialState = (code: string): State => ({
|
||||||
|
autorunMode: true,
|
||||||
|
renderedCode: "",
|
||||||
|
runningState: "none",
|
||||||
|
executionId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: "SET_AUTORUN_MODE";
|
||||||
|
value: boolean;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "PREPARE_RUN";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "RUN";
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "STOP_RUN";
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "SET_AUTORUN_MODE":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
autorunMode: action.value,
|
||||||
|
};
|
||||||
|
case "PREPARE_RUN":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
runningState: "prepared",
|
||||||
|
};
|
||||||
|
case "RUN":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
runningState: "run",
|
||||||
|
renderedCode: action.code,
|
||||||
|
executionId: state.executionId + 1,
|
||||||
|
};
|
||||||
|
case "STOP_RUN":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
runningState: "none",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRunnerState = (code: string) => {
|
||||||
|
const [state, dispatch] = useReducer(reducer, buildInitialState(code));
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (state.runningState === "prepared") {
|
||||||
|
// this is necessary for async playground loading - otherwise it executes the code synchronously on the initial load
|
||||||
|
// (it's surprising that this is necessary, but empirically it _is_ necessary, both with `useEffect` and `useLayoutEffect`)
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch({ type: "RUN", code });
|
||||||
|
}, 0);
|
||||||
|
} else if (state.runningState === "run") {
|
||||||
|
dispatch({ type: "STOP_RUN" });
|
||||||
|
}
|
||||||
|
}, [state.runningState, code]);
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
// The rest will be handled by dispatches above on following renders, but we need to update the spinner first.
|
||||||
|
dispatch({ type: "PREPARE_RUN" });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.autorunMode &&
|
||||||
|
state.renderedCode !== code &&
|
||||||
|
state.runningState === "none"
|
||||||
|
) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
autorunMode: state.autorunMode,
|
||||||
|
renderedCode: state.renderedCode,
|
||||||
|
isRunning: state.runningState !== "none",
|
||||||
|
executionId: state.executionId,
|
||||||
|
setAutorunMode: (newValue: boolean) => {
|
||||||
|
dispatch({ type: "SET_AUTORUN_MODE", value: newValue, code });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
53
packages/components/src/lib/hooks/useSquiggle.ts
Normal file
53
packages/components/src/lib/hooks/useSquiggle.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import {
|
||||||
|
bindings,
|
||||||
|
environment,
|
||||||
|
jsImports,
|
||||||
|
run,
|
||||||
|
runPartial,
|
||||||
|
} from "@quri/squiggle-lang";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
|
||||||
|
code: string;
|
||||||
|
executionId?: number;
|
||||||
|
bindings?: bindings;
|
||||||
|
jsImports?: jsImports;
|
||||||
|
environment?: environment;
|
||||||
|
onChange?: (expr: Extract<T, { tag: "Ok" }>["value"] | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSquiggleAny = <T extends ReturnType<typeof run | typeof runPartial>>(
|
||||||
|
args: SquiggleArgs<T>,
|
||||||
|
f: (...args: Parameters<typeof run>) => T
|
||||||
|
) => {
|
||||||
|
const result: T = useMemo<T>(
|
||||||
|
() => f(args.code, args.bindings, args.environment, args.jsImports),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[
|
||||||
|
f,
|
||||||
|
args.code,
|
||||||
|
args.bindings,
|
||||||
|
args.environment,
|
||||||
|
args.jsImports,
|
||||||
|
args.executionId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { onChange } = args;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange?.(result.tag === "Ok" ? result.value : undefined);
|
||||||
|
}, [result, onChange]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSquigglePartial = (
|
||||||
|
args: SquiggleArgs<ReturnType<typeof runPartial>>
|
||||||
|
) => {
|
||||||
|
return useSquiggleAny(args, runPartial);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => {
|
||||||
|
return useSquiggleAny(args, run);
|
||||||
|
};
|
|
@ -3,7 +3,7 @@ import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
||||||
|
|
||||||
<Meta title="Squiggle/SquiggleChart" component={SquiggleChart} />
|
<Meta title="Squiggle/SquiggleChart" component={SquiggleChart} />
|
||||||
|
|
||||||
export const Template = SquiggleChart;
|
export const Template = (props) => <SquiggleChart {...props} />;
|
||||||
/*
|
/*
|
||||||
We have to hardcode a width here, because otherwise some interaction with
|
We have to hardcode a width here, because otherwise some interaction with
|
||||||
Storybook creates an infinite loop with the internal width
|
Storybook creates an infinite loop with the internal width
|
||||||
|
@ -29,7 +29,7 @@ could be continuous, discrete or mixed.
|
||||||
<Story
|
<Story
|
||||||
name="Continuous Symbolic"
|
name="Continuous Symbolic"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "normal(5,2)",
|
code: "normal(5,2)",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -43,7 +43,7 @@ could be continuous, discrete or mixed.
|
||||||
<Story
|
<Story
|
||||||
name="Continuous Pointset"
|
name="Continuous Pointset"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "toPointSet(normal(5,2))",
|
code: "PointSet.fromDist(normal(5,2))",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -57,7 +57,7 @@ could be continuous, discrete or mixed.
|
||||||
<Story
|
<Story
|
||||||
name="Continuous SampleSet"
|
name="Continuous SampleSet"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "toSampleSet(normal(5,2), 1000)",
|
code: "SampleSet.fromDist(normal(5,2))",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -71,7 +71,7 @@ could be continuous, discrete or mixed.
|
||||||
<Story
|
<Story
|
||||||
name="Discrete"
|
name="Discrete"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "mx(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])",
|
code: "mx(0, 1, 3, 5, 8, 10, [0.1, 0.8, 0.5, 0.3, 0.2, 0.1])",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -85,8 +85,7 @@ could be continuous, discrete or mixed.
|
||||||
<Story
|
<Story
|
||||||
name="Mixed"
|
name="Mixed"
|
||||||
args={{
|
args={{
|
||||||
squiggleString:
|
code: "mx(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])",
|
||||||
"mx(0, 1, 3, 5, 8, normal(8, 1), [0.1, 0.3, 0.4, 0.35, 0.2, 0.8])",
|
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -103,7 +102,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="Constant"
|
name="Constant"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "500000000",
|
code: "500000000",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -117,7 +116,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="Array"
|
name="Array"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "[normal(5,2), normal(10,1), normal(40,2), 400000]",
|
code: "[normal(5,2), normal(10,1), normal(40,2), 400000]",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -131,7 +130,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="Error"
|
name="Error"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "f(x) = normal(",
|
code: "f(x) = normal(",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -145,7 +144,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="Boolean"
|
name="Boolean"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "3 == 3",
|
code: "3 == 3",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -159,7 +158,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="Function to Distribution"
|
name="Function to Distribution"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "foo(t) = normal(t,2)*normal(5,3); foo",
|
code: "foo(t) = normal(t,2)*normal(5,3); foo",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -173,7 +172,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="Function to Number"
|
name="Function to Number"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "foo(t) = t^2; foo",
|
code: "foo(t) = t^2; foo",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -187,7 +186,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="Record"
|
name="Record"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: "{foo: 35 to 50, bar: [1,2,3]}",
|
code: "{foo: 35 to 50, bar: [1,2,3]}",
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -201,7 +200,7 @@ to allow large and small numbers being printed cleanly.
|
||||||
<Story
|
<Story
|
||||||
name="String"
|
name="String"
|
||||||
args={{
|
args={{
|
||||||
squiggleString: '"Lucky day!"',
|
code: '"Lucky day!"',
|
||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -14,7 +14,20 @@ the distribution.
|
||||||
<Story
|
<Story
|
||||||
name="Normal"
|
name="Normal"
|
||||||
args={{
|
args={{
|
||||||
initialSquiggleString: "normal(5,2)",
|
defaultCode: "normal(5,2)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
It's also possible to create a controlled version of the same component
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Controlled"
|
||||||
|
args={{
|
||||||
|
code: "normal(5,2)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Template.bind({})}
|
{Template.bind({})}
|
||||||
|
@ -27,7 +40,7 @@ You can also name variables like so:
|
||||||
<Story
|
<Story
|
||||||
name="Variables"
|
name="Variables"
|
||||||
args={{
|
args={{
|
||||||
initialSquiggleString: "x = 2\nnormal(x,2)",
|
defaultCode: "x = 2\nnormal(x,2)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Template.bind({})}
|
{Template.bind({})}
|
||||||
|
|
|
@ -15,7 +15,7 @@ instead returns bindings that can be used by further Squiggle Editors.
|
||||||
<Story
|
<Story
|
||||||
name="Standalone"
|
name="Standalone"
|
||||||
args={{
|
args={{
|
||||||
initialSquiggleString: "x = normal(5,2)",
|
defaultCode: "x = normal(5,2)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Template.bind({})}
|
{Template.bind({})}
|
||||||
|
@ -36,12 +36,12 @@ instead returns bindings that can be used by further Squiggle Editors.
|
||||||
<>
|
<>
|
||||||
<SquigglePartial
|
<SquigglePartial
|
||||||
{...props}
|
{...props}
|
||||||
initialSquiggleString={props.initialPartialString}
|
defaultCode={props.initialPartialString}
|
||||||
onChange={setBindings}
|
onChange={setBindings}
|
||||||
/>
|
/>
|
||||||
<SquiggleEditor
|
<SquiggleEditor
|
||||||
{...props}
|
{...props}
|
||||||
initialSquiggleString={props.initialEditorString}
|
defaultCode={props.initialEditorString}
|
||||||
bindings={bindings}
|
bindings={bindings}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SquigglePlayground from "../components/SquigglePlayground";
|
import { SquigglePlayground } from "../components/SquigglePlayground";
|
||||||
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
||||||
|
|
||||||
<Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} />
|
<Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} />
|
||||||
|
@ -14,10 +14,23 @@ including sampling settings, in squiggle.
|
||||||
<Story
|
<Story
|
||||||
name="Normal"
|
name="Normal"
|
||||||
args={{
|
args={{
|
||||||
initialSquiggleString: "normal(5,2)",
|
defaultCode: "normal(5,2)",
|
||||||
height: 800,
|
height: 800,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Template.bind({})}
|
{Template.bind({})}
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="With share button"
|
||||||
|
args={{
|
||||||
|
defaultCode: "normal(5,2)",
|
||||||
|
height: 800,
|
||||||
|
showShareButton: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Template.bind({})}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
|
@ -19,7 +19,23 @@ This file contains:
|
||||||
-webkit-text-size-adjust: 100%; /* 2 */
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
-moz-tab-size: 4; /* 3 */
|
-moz-tab-size: 4; /* 3 */
|
||||||
tab-size: 4; /* 3 */
|
tab-size: 4; /* 3 */
|
||||||
font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */
|
font-family: theme(
|
||||||
|
"fontFamily.sans",
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
"Helvetica Neue",
|
||||||
|
Arial,
|
||||||
|
"Noto Sans",
|
||||||
|
sans-serif,
|
||||||
|
"Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji",
|
||||||
|
"Segoe UI Symbol",
|
||||||
|
"Noto Color Emoji"
|
||||||
|
); /* 4 */
|
||||||
/* } */
|
/* } */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -32,7 +48,6 @@ This file contains:
|
||||||
line-height: inherit; /* 2 */
|
line-height: inherit; /* 2 */
|
||||||
/* } */
|
/* } */
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||||
|
@ -44,12 +59,12 @@ This file contains:
|
||||||
box-sizing: border-box; /* 1 */
|
box-sizing: border-box; /* 1 */
|
||||||
border-width: 0; /* 2 */
|
border-width: 0; /* 2 */
|
||||||
border-style: solid; /* 2 */
|
border-style: solid; /* 2 */
|
||||||
border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */
|
border-color: theme("borderColor.DEFAULT", currentColor); /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
::before,
|
::before,
|
||||||
::after {
|
::after {
|
||||||
--tw-content: '';
|
--tw-content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -113,7 +128,17 @@ code,
|
||||||
kbd,
|
kbd,
|
||||||
samp,
|
samp,
|
||||||
pre {
|
pre {
|
||||||
font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); /* 1 */
|
font-family: theme(
|
||||||
|
"fontFamily.mono",
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
Menlo,
|
||||||
|
Monaco,
|
||||||
|
Consolas,
|
||||||
|
"Liberation Mono",
|
||||||
|
"Courier New",
|
||||||
|
monospace
|
||||||
|
); /* 1 */
|
||||||
font-size: 1em; /* 2 */
|
font-size: 1em; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,9 +217,9 @@ select {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
[type='button'],
|
[type="button"],
|
||||||
[type='reset'],
|
[type="reset"],
|
||||||
[type='submit'] {
|
[type="submit"] {
|
||||||
-webkit-appearance: button; /* 1 */
|
-webkit-appearance: button; /* 1 */
|
||||||
background-color: transparent; /* 2 */
|
background-color: transparent; /* 2 */
|
||||||
background-image: none; /* 2 */
|
background-image: none; /* 2 */
|
||||||
|
@ -238,7 +263,7 @@ Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
2. Correct the outline style in Safari.
|
2. Correct the outline style in Safari.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
[type='search'] {
|
[type="search"] {
|
||||||
-webkit-appearance: textfield; /* 1 */
|
-webkit-appearance: textfield; /* 1 */
|
||||||
outline-offset: -2px; /* 2 */
|
outline-offset: -2px; /* 2 */
|
||||||
}
|
}
|
||||||
|
@ -322,7 +347,7 @@ textarea {
|
||||||
input::placeholder,
|
input::placeholder,
|
||||||
textarea::placeholder {
|
textarea::placeholder {
|
||||||
opacity: 1; /* 1 */
|
opacity: 1; /* 1 */
|
||||||
color: theme('colors.gray.400', #9ca3af); /* 2 */
|
color: theme("colors.gray.400", #9ca3af); /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
/* Fork of https://github.com/tailwindlabs/tailwindcss-forms styles, see the comment in main.css for details. */
|
/* Fork of https://github.com/tailwindlabs/tailwindcss-forms styles, see the comment in main.css for details. */
|
||||||
.squiggle {
|
.squiggle {
|
||||||
.form-input,.form-textarea,.form-select,.form-multiselect {
|
.form-input,
|
||||||
|
.form-textarea,
|
||||||
|
.form-select,
|
||||||
|
.form-multiselect {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-color: #6b7280;
|
border-color: #6b7280;
|
||||||
|
@ -14,19 +17,26 @@ font-size: 1rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
--tw-shadow: 0 0 #0000;
|
--tw-shadow: 0 0 #0000;
|
||||||
}
|
}
|
||||||
.form-input:focus, .form-textarea:focus, .form-select:focus, .form-multiselect:focus {
|
.form-input:focus,
|
||||||
|
.form-textarea:focus,
|
||||||
|
.form-select:focus,
|
||||||
|
.form-multiselect:focus {
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
|
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
|
||||||
--tw-ring-offset-width: 0px;
|
--tw-ring-offset-width: 0px;
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-color: #2563eb;
|
--tw-ring-color: #2563eb;
|
||||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
|
||||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0
|
||||||
|
calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
|
||||||
|
var(--tw-shadow);
|
||||||
border-color: #2563eb;
|
border-color: #2563eb;
|
||||||
}
|
}
|
||||||
.form-input::placeholder,.form-textarea::placeholder {
|
.form-input::placeholder,
|
||||||
|
.form-textarea::placeholder {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@ -36,11 +46,20 @@ padding: 0;
|
||||||
.form-input::-webkit-date-and-time-value {
|
.form-input::-webkit-date-and-time-value {
|
||||||
min-height: 1.5em;
|
min-height: 1.5em;
|
||||||
}
|
}
|
||||||
.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-year-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-meridiem-field {
|
.form-input::-webkit-datetime-edit,
|
||||||
|
.form-input::-webkit-datetime-edit-year-field,
|
||||||
|
.form-input::-webkit-datetime-edit-month-field,
|
||||||
|
.form-input::-webkit-datetime-edit-day-field,
|
||||||
|
.form-input::-webkit-datetime-edit-hour-field,
|
||||||
|
.form-input::-webkit-datetime-edit-minute-field,
|
||||||
|
.form-input::-webkit-datetime-edit-second-field,
|
||||||
|
.form-input::-webkit-datetime-edit-millisecond-field,
|
||||||
|
.form-input::-webkit-datetime-edit-meridiem-field {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
.form-checkbox,.form-radio {
|
.form-checkbox,
|
||||||
|
.form-radio {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
-webkit-print-color-adjust: exact;
|
-webkit-print-color-adjust: exact;
|
||||||
|
@ -62,18 +81,23 @@ border-width: 1px;
|
||||||
.form-checkbox {
|
.form-checkbox {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
.form-checkbox:focus,.form-radio:focus {
|
.form-checkbox:focus,
|
||||||
|
.form-radio:focus {
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
|
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
|
||||||
--tw-ring-offset-width: 2px;
|
--tw-ring-offset-width: 2px;
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-color: #2563eb;
|
--tw-ring-color: #2563eb;
|
||||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
|
||||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0
|
||||||
|
calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
|
||||||
|
var(--tw-shadow);
|
||||||
}
|
}
|
||||||
.form-checkbox:checked,.form-radio:checked {
|
.form-checkbox:checked,
|
||||||
|
.form-radio:checked {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
|
@ -83,7 +107,10 @@ background-repeat: no-repeat;
|
||||||
.form-checkbox:checked {
|
.form-checkbox:checked {
|
||||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||||
}
|
}
|
||||||
.form-checkbox:checked:hover,.form-checkbox:checked:focus,.form-radio:checked:hover,.form-radio:checked:focus {
|
.form-checkbox:checked:hover,
|
||||||
|
.form-checkbox:checked:focus,
|
||||||
|
.form-radio:checked:hover,
|
||||||
|
.form-radio:checked:focus {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
}
|
}
|
||||||
|
@ -95,7 +122,8 @@ background-size: 100% 100%;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
.form-checkbox:indeterminate:hover,.form-checkbox:indeterminate:focus {
|
.form-checkbox:indeterminate:hover,
|
||||||
|
.form-checkbox:indeterminate:focus {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,27 @@ module.exports = {
|
||||||
},
|
},
|
||||||
important: ".squiggle",
|
important: ".squiggle",
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
animation: {
|
||||||
|
"appear-and-spin":
|
||||||
|
"spin 1s linear infinite, squiggle-appear 0.2s forwards",
|
||||||
|
"semi-appear": "squiggle-semi-appear 0.2s forwards",
|
||||||
|
hide: "squiggle-hide 0.2s forwards",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"squiggle-appear": {
|
||||||
|
from: { opacity: 0 },
|
||||||
|
to: { opacity: 1 },
|
||||||
|
},
|
||||||
|
"squiggle-semi-appear": {
|
||||||
|
from: { opacity: 0 },
|
||||||
|
to: { opacity: 0.5 },
|
||||||
|
},
|
||||||
|
"squiggle-hide": {
|
||||||
|
from: { opacity: 1 },
|
||||||
|
to: { opacity: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "production",
|
mode: "production",
|
||||||
devtool: "source-map",
|
devtool: "source-map",
|
||||||
profile: true,
|
profile: true,
|
||||||
entry: ["./src/index.ts", "./src/styles/main.css"],
|
entry: "./src/index.ts",
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
@ -14,13 +13,8 @@ module.exports = {
|
||||||
options: { projectReferences: true },
|
options: { projectReferences: true },
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [new MiniCssExtractPlugin()],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".js", ".tsx", ".ts"],
|
extensions: [".js", ".tsx", ".ts"],
|
||||||
alias: {
|
alias: {
|
||||||
|
@ -42,4 +36,18 @@ module.exports = {
|
||||||
compress: true,
|
compress: true,
|
||||||
port: 9000,
|
port: 9000,
|
||||||
},
|
},
|
||||||
|
externals: {
|
||||||
|
react: {
|
||||||
|
commonjs: "react",
|
||||||
|
commonjs2: "react",
|
||||||
|
amd: "react",
|
||||||
|
root: "React",
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
commonjs: "react-dom",
|
||||||
|
commonjs2: "react-dom",
|
||||||
|
amd: "react-dom",
|
||||||
|
root: "ReactDOM",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
1
packages/squiggle-lang/.gitignore
vendored
1
packages/squiggle-lang/.gitignore
vendored
|
@ -22,3 +22,4 @@ _coverage
|
||||||
coverage
|
coverage
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
|
src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
|
||||||
|
src/rescript/Reducer/Reducer_Peggy/helpers.js
|
||||||
|
|
|
@ -14,4 +14,16 @@ describe("Combining Continuous and Discrete Distributions", () => {
|
||||||
), // Multiply distribution by -1
|
), // Multiply distribution by -1
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
makeTest(
|
||||||
|
"keep order of xs when first number is discrete and adding",
|
||||||
|
AlgebraicShapeCombination.isOrdered(
|
||||||
|
AlgebraicShapeCombination.combineShapesContinuousDiscrete(
|
||||||
|
#Add,
|
||||||
|
{xs: [0., 1.], ys: [1., 1.]},
|
||||||
|
{xs: [1.], ys: [1.]},
|
||||||
|
~discretePosition=First,
|
||||||
|
),
|
||||||
|
), // 1 + distribution
|
||||||
|
true,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
open Jest
|
open Jest
|
||||||
open Expect
|
open Expect
|
||||||
|
|
||||||
let env: DistributionOperation.env = {
|
let env: GenericDist.env = {
|
||||||
sampleCount: 100,
|
sampleCount: 100,
|
||||||
xyPointLength: 100,
|
xyPointLength: 100,
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ describe("sparkline", () => {
|
||||||
expected: DistributionOperation.outputType,
|
expected: DistributionOperation.outputType,
|
||||||
) => {
|
) => {
|
||||||
test(name, () => {
|
test(name, () => {
|
||||||
let result = DistributionOperation.run(~env, FromDist(ToString(ToSparkline(20)), dist))
|
let result = DistributionOperation.run(~env, FromDist(#ToString(ToSparkline(20)), dist))
|
||||||
expect(result)->toEqual(expected)
|
expect(result)->toEqual(expected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -81,8 +81,8 @@ describe("sparkline", () => {
|
||||||
describe("toPointSet", () => {
|
describe("toPointSet", () => {
|
||||||
test("on symbolic normal distribution", () => {
|
test("on symbolic normal distribution", () => {
|
||||||
let result =
|
let result =
|
||||||
run(FromDist(ToDist(ToPointSet), normalDist5))
|
run(FromDist(#ToDist(ToPointSet), normalDist5))
|
||||||
->outputMap(FromDist(ToFloat(#Mean)))
|
->outputMap(FromDist(#ToFloat(#Mean)))
|
||||||
->toFloat
|
->toFloat
|
||||||
->toExt
|
->toExt
|
||||||
expect(result)->toBeSoCloseTo(5.0, ~digits=0)
|
expect(result)->toBeSoCloseTo(5.0, ~digits=0)
|
||||||
|
@ -90,10 +90,10 @@ describe("toPointSet", () => {
|
||||||
|
|
||||||
test("on sample set", () => {
|
test("on sample set", () => {
|
||||||
let result =
|
let result =
|
||||||
run(FromDist(ToDist(ToPointSet), normalDist5))
|
run(FromDist(#ToDist(ToPointSet), normalDist5))
|
||||||
->outputMap(FromDist(ToDist(ToSampleSet(1000))))
|
->outputMap(FromDist(#ToDist(ToSampleSet(1000))))
|
||||||
->outputMap(FromDist(ToDist(ToPointSet)))
|
->outputMap(FromDist(#ToDist(ToPointSet)))
|
||||||
->outputMap(FromDist(ToFloat(#Mean)))
|
->outputMap(FromDist(#ToFloat(#Mean)))
|
||||||
->toFloat
|
->toFloat
|
||||||
->toExt
|
->toExt
|
||||||
expect(result)->toBeSoCloseTo(5.0, ~digits=-1)
|
expect(result)->toBeSoCloseTo(5.0, ~digits=-1)
|
||||||
|
|
|
@ -19,7 +19,6 @@ exception MixtureFailed
|
||||||
let float1 = 1.0
|
let float1 = 1.0
|
||||||
let float2 = 2.0
|
let float2 = 2.0
|
||||||
let float3 = 3.0
|
let float3 = 3.0
|
||||||
let {mkDelta} = module(TestHelpers)
|
let point1 = TestHelpers.mkDelta(float1)
|
||||||
let point1 = mkDelta(float1)
|
let point2 = TestHelpers.mkDelta(float2)
|
||||||
let point2 = mkDelta(float2)
|
let point3 = TestHelpers.mkDelta(float3)
|
||||||
let point3 = mkDelta(float3)
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ describe("mixture", () => {
|
||||||
let (mean1, mean2) = tup
|
let (mean1, mean2) = tup
|
||||||
let meanValue = {
|
let meanValue = {
|
||||||
run(Mixture([(mkNormal(mean1, 9e-1), 0.5), (mkNormal(mean2, 9e-1), 0.5)]))->outputMap(
|
run(Mixture([(mkNormal(mean1, 9e-1), 0.5), (mkNormal(mean2, 9e-1), 0.5)]))->outputMap(
|
||||||
FromDist(ToFloat(#Mean)),
|
FromDist(#ToFloat(#Mean)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
meanValue->unpackFloat->expect->toBeSoCloseTo((mean1 +. mean2) /. 2.0, ~digits=-1)
|
meanValue->unpackFloat->expect->toBeSoCloseTo((mean1 +. mean2) /. 2.0, ~digits=-1)
|
||||||
|
@ -28,7 +28,7 @@ describe("mixture", () => {
|
||||||
let meanValue = {
|
let meanValue = {
|
||||||
run(
|
run(
|
||||||
Mixture([(mkBeta(alpha, beta), betaWeight), (mkExponential(rate), exponentialWeight)]),
|
Mixture([(mkBeta(alpha, beta), betaWeight), (mkExponential(rate), exponentialWeight)]),
|
||||||
)->outputMap(FromDist(ToFloat(#Mean)))
|
)->outputMap(FromDist(#ToFloat(#Mean)))
|
||||||
}
|
}
|
||||||
let betaMean = 1.0 /. (1.0 +. beta /. alpha)
|
let betaMean = 1.0 /. (1.0 +. beta /. alpha)
|
||||||
let exponentialMean = 1.0 /. rate
|
let exponentialMean = 1.0 /. rate
|
||||||
|
@ -52,7 +52,7 @@ describe("mixture", () => {
|
||||||
(mkUniform(low, high), uniformWeight),
|
(mkUniform(low, high), uniformWeight),
|
||||||
(mkLognormal(mu, sigma), lognormalWeight),
|
(mkLognormal(mu, sigma), lognormalWeight),
|
||||||
]),
|
]),
|
||||||
)->outputMap(FromDist(ToFloat(#Mean)))
|
)->outputMap(FromDist(#ToFloat(#Mean)))
|
||||||
}
|
}
|
||||||
let uniformMean = (low +. high) /. 2.0
|
let uniformMean = (low +. high) /. 2.0
|
||||||
let lognormalMean = mu +. sigma ** 2.0 /. 2.0
|
let lognormalMean = mu +. sigma ** 2.0 /. 2.0
|
||||||
|
|
|
@ -3,6 +3,7 @@ open Expect
|
||||||
open TestHelpers
|
open TestHelpers
|
||||||
open GenericDist_Fixtures
|
open GenericDist_Fixtures
|
||||||
|
|
||||||
|
let klDivergence = DistributionOperation.Constructors.LogScore.distEstimateDistAnswer(~env)
|
||||||
// integral from low to high of 1 / (high - low) log(normal(mean, stdev)(x) / (1 / (high - low))) dx
|
// integral from low to high of 1 / (high - low) log(normal(mean, stdev)(x) / (1 / (high - low))) dx
|
||||||
let klNormalUniform = (mean, stdev, low, high): float =>
|
let klNormalUniform = (mean, stdev, low, high): float =>
|
||||||
-.Js.Math.log((high -. low) /. Js.Math.sqrt(2.0 *. MagicNumbers.Math.pi *. stdev ** 2.0)) +.
|
-.Js.Math.log((high -. low) /. Js.Math.sqrt(2.0 *. MagicNumbers.Math.pi *. stdev ** 2.0)) +.
|
||||||
|
@ -11,8 +12,6 @@ let klNormalUniform = (mean, stdev, low, high): float =>
|
||||||
(mean ** 2.0 -. (high +. low) *. mean +. (low ** 2.0 +. high *. low +. high ** 2.0) /. 3.0)
|
(mean ** 2.0 -. (high +. low) *. mean +. (low ** 2.0 +. high *. low +. high ** 2.0) /. 3.0)
|
||||||
|
|
||||||
describe("klDivergence: continuous -> continuous -> float", () => {
|
describe("klDivergence: continuous -> continuous -> float", () => {
|
||||||
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
|
|
||||||
|
|
||||||
let testUniform = (lowAnswer, highAnswer, lowPrediction, highPrediction) => {
|
let testUniform = (lowAnswer, highAnswer, lowPrediction, highPrediction) => {
|
||||||
test("of two uniforms is equal to the analytic expression", () => {
|
test("of two uniforms is equal to the analytic expression", () => {
|
||||||
let answer =
|
let answer =
|
||||||
|
@ -58,7 +57,7 @@ describe("klDivergence: continuous -> continuous -> float", () => {
|
||||||
let kl = E.R.liftJoin2(klDivergence, prediction, answer)
|
let kl = E.R.liftJoin2(klDivergence, prediction, answer)
|
||||||
|
|
||||||
switch kl {
|
switch kl {
|
||||||
| Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=3)
|
| Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=2)
|
||||||
| Error(err) => {
|
| Error(err) => {
|
||||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||||
raise(KlFailed)
|
raise(KlFailed)
|
||||||
|
@ -82,7 +81,6 @@ describe("klDivergence: continuous -> continuous -> float", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("klDivergence: discrete -> discrete -> float", () => {
|
describe("klDivergence: discrete -> discrete -> float", () => {
|
||||||
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
|
|
||||||
let mixture = a => DistributionTypes.DistributionOperation.Mixture(a)
|
let mixture = a => DistributionTypes.DistributionOperation.Mixture(a)
|
||||||
let a' = [(point1, 1e0), (point2, 1e0)]->mixture->run
|
let a' = [(point1, 1e0), (point2, 1e0)]->mixture->run
|
||||||
let b' = [(point1, 1e0), (point2, 1e0), (point3, 1e0)]->mixture->run
|
let b' = [(point1, 1e0), (point2, 1e0), (point3, 1e0)]->mixture->run
|
||||||
|
@ -117,7 +115,6 @@ describe("klDivergence: discrete -> discrete -> float", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("klDivergence: mixed -> mixed -> float", () => {
|
describe("klDivergence: mixed -> mixed -> float", () => {
|
||||||
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
|
|
||||||
let mixture' = a => DistributionTypes.DistributionOperation.Mixture(a)
|
let mixture' = a => DistributionTypes.DistributionOperation.Mixture(a)
|
||||||
let mixture = a => {
|
let mixture = a => {
|
||||||
let dist' = a->mixture'->run
|
let dist' = a->mixture'->run
|
||||||
|
@ -189,15 +186,15 @@ describe("combineAlongSupportOfSecondArgument0", () => {
|
||||||
uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap(s => DistributionTypes.ArgumentError(
|
uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap(s => DistributionTypes.ArgumentError(
|
||||||
s,
|
s,
|
||||||
))
|
))
|
||||||
let answerWrapped = E.R.fmap(a => run(FromDist(ToDist(ToPointSet), a)), answer)
|
let answerWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), answer)
|
||||||
let predictionWrapped = E.R.fmap(a => run(FromDist(ToDist(ToPointSet), a)), prediction)
|
let predictionWrapped = E.R.fmap(a => run(FromDist(#ToDist(ToPointSet), a)), prediction)
|
||||||
|
|
||||||
let interpolator = XYShape.XtoY.continuousInterpolator(#Stepwise, #UseZero)
|
let interpolator = XYShape.XtoY.continuousInterpolator(#Stepwise, #UseZero)
|
||||||
let integrand = PointSetDist_Scoring.KLDivergence.integrand
|
let integrand = PointSetDist_Scoring.WithDistAnswer.integrand
|
||||||
|
|
||||||
let result = switch (answerWrapped, predictionWrapped) {
|
let result = switch (answerWrapped, predictionWrapped) {
|
||||||
| (Ok(Dist(PointSet(Continuous(a)))), Ok(Dist(PointSet(Continuous(b))))) =>
|
| (Ok(Dist(PointSet(Continuous(a)))), Ok(Dist(PointSet(Continuous(b))))) =>
|
||||||
Some(combineAlongSupportOfSecondArgument(integrand, interpolator, a.xyShape, b.xyShape))
|
Some(combineAlongSupportOfSecondArgument(interpolator, integrand, a.xyShape, b.xyShape))
|
||||||
| _ => None
|
| _ => None
|
||||||
}
|
}
|
||||||
result
|
result
|
|
@ -0,0 +1,68 @@
|
||||||
|
open Jest
|
||||||
|
open Expect
|
||||||
|
open TestHelpers
|
||||||
|
open GenericDist_Fixtures
|
||||||
|
exception ScoreFailed
|
||||||
|
|
||||||
|
describe("WithScalarAnswer: discrete -> scalar -> score", () => {
|
||||||
|
let mixture = a => DistributionTypes.DistributionOperation.Mixture(a)
|
||||||
|
let pointA = mkDelta(3.0)
|
||||||
|
let pointB = mkDelta(2.0)
|
||||||
|
let pointC = mkDelta(1.0)
|
||||||
|
let pointD = mkDelta(0.0)
|
||||||
|
|
||||||
|
test("score: agrees with analytical answer when finite", () => {
|
||||||
|
let prediction' = [(pointA, 0.25), (pointB, 0.25), (pointC, 0.25), (pointD, 0.25)]->mixture->run
|
||||||
|
let prediction = switch prediction' {
|
||||||
|
| Dist(PointSet(p)) => p
|
||||||
|
| _ => raise(MixtureFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let answer = 2.0 // So this is: assigning 100% probability to 2.0
|
||||||
|
let result = PointSetDist_Scoring.WithScalarAnswer.score(~estimate=prediction, ~answer)
|
||||||
|
switch result {
|
||||||
|
| Ok(x) => x->expect->toEqual(-.Js.Math.log(0.25 /. 1.0))
|
||||||
|
| _ => raise(ScoreFailed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("score: agrees with analytical answer when finite", () => {
|
||||||
|
let prediction' = [(pointA, 0.75), (pointB, 0.25)]->mixture->run
|
||||||
|
let prediction = switch prediction' {
|
||||||
|
| Dist(PointSet(p)) => p
|
||||||
|
| _ => raise(MixtureFailed)
|
||||||
|
}
|
||||||
|
let answer = 3.0 // So this is: assigning 100% probability to 2.0
|
||||||
|
let result = PointSetDist_Scoring.WithScalarAnswer.score(~estimate=prediction, ~answer)
|
||||||
|
switch result {
|
||||||
|
| Ok(x) => x->expect->toEqual(-.Js.Math.log(0.75 /. 1.0))
|
||||||
|
| _ => raise(ScoreFailed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("scoreWithPrior: agrees with analytical answer when finite", () => {
|
||||||
|
let prior' = [(pointA, 0.5), (pointB, 0.5)]->mixture->run
|
||||||
|
let prediction' = [(pointA, 0.75), (pointB, 0.25)]->mixture->run
|
||||||
|
|
||||||
|
let prediction = switch prediction' {
|
||||||
|
| Dist(PointSet(p)) => p
|
||||||
|
| _ => raise(MixtureFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let prior = switch prior' {
|
||||||
|
| Dist(PointSet(p)) => p
|
||||||
|
| _ => raise(MixtureFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let answer = 3.0 // So this is: assigning 100% probability to 2.0
|
||||||
|
let result = PointSetDist_Scoring.WithScalarAnswer.scoreWithPrior(
|
||||||
|
~estimate=prediction,
|
||||||
|
~answer,
|
||||||
|
~prior,
|
||||||
|
)
|
||||||
|
switch result {
|
||||||
|
| Ok(x) => x->expect->toEqual(-.Js.Math.log(0.75 /. 1.0) -. -.Js.Math.log(0.5 /. 1.0))
|
||||||
|
| _ => raise(ScoreFailed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -8,34 +8,34 @@ let mkNormal = (mean, stdev) => DistributionTypes.Symbolic(#Normal({mean: mean,
|
||||||
describe("(Symbolic) normalize", () => {
|
describe("(Symbolic) normalize", () => {
|
||||||
testAll("has no impact on normal distributions", list{-1e8, -1e-2, 0.0, 1e-4, 1e16}, mean => {
|
testAll("has no impact on normal distributions", list{-1e8, -1e-2, 0.0, 1e-4, 1e16}, mean => {
|
||||||
let normalValue = mkNormal(mean, 2.0)
|
let normalValue = mkNormal(mean, 2.0)
|
||||||
let normalizedValue = run(FromDist(ToDist(Normalize), normalValue))
|
let normalizedValue = run(FromDist(#ToDist(Normalize), normalValue))
|
||||||
normalizedValue->unpackDist->expect->toEqual(normalValue)
|
normalizedValue->unpackDist->expect->toEqual(normalValue)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("(Symbolic) mean", () => {
|
describe("(Symbolic) mean", () => {
|
||||||
testAll("of normal distributions", list{-1e8, -16.0, -1e-2, 0.0, 1e-4, 32.0, 1e16}, mean => {
|
testAll("of normal distributions", list{-1e8, -16.0, -1e-2, 0.0, 1e-4, 32.0, 1e16}, mean => {
|
||||||
run(FromDist(ToFloat(#Mean), mkNormal(mean, 4.0)))->unpackFloat->expect->toBeCloseTo(mean)
|
run(FromDist(#ToFloat(#Mean), mkNormal(mean, 4.0)))->unpackFloat->expect->toBeCloseTo(mean)
|
||||||
})
|
})
|
||||||
|
|
||||||
Skip.test("of normal(0, -1) (it NaNs out)", () => {
|
Skip.test("of normal(0, -1) (it NaNs out)", () => {
|
||||||
run(FromDist(ToFloat(#Mean), mkNormal(1e1, -1e0)))->unpackFloat->expect->ExpectJs.toBeFalsy
|
run(FromDist(#ToFloat(#Mean), mkNormal(1e1, -1e0)))->unpackFloat->expect->ExpectJs.toBeFalsy
|
||||||
})
|
})
|
||||||
|
|
||||||
test("of normal(0, 1e-8) (it doesn't freak out at tiny stdev)", () => {
|
test("of normal(0, 1e-8) (it doesn't freak out at tiny stdev)", () => {
|
||||||
run(FromDist(ToFloat(#Mean), mkNormal(0.0, 1e-8)))->unpackFloat->expect->toBeCloseTo(0.0)
|
run(FromDist(#ToFloat(#Mean), mkNormal(0.0, 1e-8)))->unpackFloat->expect->toBeCloseTo(0.0)
|
||||||
})
|
})
|
||||||
|
|
||||||
testAll("of exponential distributions", list{1e-7, 2.0, 10.0, 100.0}, rate => {
|
testAll("of exponential distributions", list{1e-7, 2.0, 10.0, 100.0}, rate => {
|
||||||
let meanValue = run(
|
let meanValue = run(
|
||||||
FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Exponential({rate: rate}))),
|
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Exponential({rate: rate}))),
|
||||||
)
|
)
|
||||||
meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. rate) // https://en.wikipedia.org/wiki/Exponential_distribution#Mean,_variance,_moments,_and_median
|
meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. rate) // https://en.wikipedia.org/wiki/Exponential_distribution#Mean,_variance,_moments,_and_median
|
||||||
})
|
})
|
||||||
|
|
||||||
test("of a cauchy distribution", () => {
|
test("of a cauchy distribution", () => {
|
||||||
let meanValue = run(
|
let meanValue = run(
|
||||||
FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Cauchy({local: 1.0, scale: 1.0}))),
|
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Cauchy({local: 1.0, scale: 1.0}))),
|
||||||
)
|
)
|
||||||
meanValue->unpackFloat->expect->toBeSoCloseTo(1.0098094001641797, ~digits=5)
|
meanValue->unpackFloat->expect->toBeSoCloseTo(1.0098094001641797, ~digits=5)
|
||||||
//-> toBe(GenDistError(Other("Cauchy distributions may have no mean value.")))
|
//-> toBe(GenDistError(Other("Cauchy distributions may have no mean value.")))
|
||||||
|
@ -48,7 +48,7 @@ describe("(Symbolic) mean", () => {
|
||||||
let (low, medium, high) = tup
|
let (low, medium, high) = tup
|
||||||
let meanValue = run(
|
let meanValue = run(
|
||||||
FromDist(
|
FromDist(
|
||||||
ToFloat(#Mean),
|
#ToFloat(#Mean),
|
||||||
DistributionTypes.Symbolic(#Triangular({low: low, medium: medium, high: high})),
|
DistributionTypes.Symbolic(#Triangular({low: low, medium: medium, high: high})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -63,7 +63,7 @@ describe("(Symbolic) mean", () => {
|
||||||
tup => {
|
tup => {
|
||||||
let (alpha, beta) = tup
|
let (alpha, beta) = tup
|
||||||
let meanValue = run(
|
let meanValue = run(
|
||||||
FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha: alpha, beta: beta}))),
|
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha: alpha, beta: beta}))),
|
||||||
)
|
)
|
||||||
meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. (1.0 +. beta /. alpha)) // https://en.wikipedia.org/wiki/Beta_distribution#Mean
|
meanValue->unpackFloat->expect->toBeCloseTo(1.0 /. (1.0 +. beta /. alpha)) // https://en.wikipedia.org/wiki/Beta_distribution#Mean
|
||||||
},
|
},
|
||||||
|
@ -72,18 +72,35 @@ describe("(Symbolic) mean", () => {
|
||||||
// TODO: When we have our theory of validators we won't want this to be NaN but to be an error.
|
// TODO: When we have our theory of validators we won't want this to be NaN but to be an error.
|
||||||
test("of beta(0, 0)", () => {
|
test("of beta(0, 0)", () => {
|
||||||
let meanValue = run(
|
let meanValue = run(
|
||||||
FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha: 0.0, beta: 0.0}))),
|
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Beta({alpha: 0.0, beta: 0.0}))),
|
||||||
)
|
)
|
||||||
meanValue->unpackFloat->expect->ExpectJs.toBeFalsy
|
meanValue->unpackFloat->expect->ExpectJs.toBeFalsy
|
||||||
})
|
})
|
||||||
|
|
||||||
|
testAll(
|
||||||
|
"of beta distributions from mean and standard dev",
|
||||||
|
list{(0.39, 0.1), (0.08, 0.1), (0.8, 0.3)},
|
||||||
|
tup => {
|
||||||
|
let (mean, stdev) = tup
|
||||||
|
let betaDistribution = SymbolicDist.Beta.fromMeanAndStdev(mean, stdev)
|
||||||
|
let meanValue =
|
||||||
|
betaDistribution->E.R2.fmap(d =>
|
||||||
|
run(FromDist(#ToFloat(#Mean), d->DistributionTypes.Symbolic))
|
||||||
|
)
|
||||||
|
switch meanValue {
|
||||||
|
| Ok(value) => value->unpackFloat->expect->toBeCloseTo(mean)
|
||||||
|
| Error(err) => err->expect->toBe("shouldn't happen")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
testAll(
|
testAll(
|
||||||
"of lognormal distributions",
|
"of lognormal distributions",
|
||||||
list{(2.0, 4.0), (1e-7, 1e-2), (-1e6, 10.0), (1e3, -1e2), (-1e8, -1e4), (1e2, 1e-5)},
|
list{(2.0, 4.0), (1e-7, 1e-2), (-1e6, 10.0), (1e3, -1e2), (-1e8, -1e4), (1e2, 1e-5)},
|
||||||
tup => {
|
tup => {
|
||||||
let (mu, sigma) = tup
|
let (mu, sigma) = tup
|
||||||
let meanValue = run(
|
let meanValue = run(
|
||||||
FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Lognormal({mu: mu, sigma: sigma}))),
|
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Lognormal({mu: mu, sigma: sigma}))),
|
||||||
)
|
)
|
||||||
meanValue->unpackFloat->expect->toBeCloseTo(Js.Math.exp(mu +. sigma ** 2.0 /. 2.0)) // https://brilliant.org/wiki/log-normal-distribution/
|
meanValue->unpackFloat->expect->toBeCloseTo(Js.Math.exp(mu +. sigma ** 2.0 /. 2.0)) // https://brilliant.org/wiki/log-normal-distribution/
|
||||||
},
|
},
|
||||||
|
@ -95,14 +112,14 @@ describe("(Symbolic) mean", () => {
|
||||||
tup => {
|
tup => {
|
||||||
let (low, high) = tup
|
let (low, high) = tup
|
||||||
let meanValue = run(
|
let meanValue = run(
|
||||||
FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Uniform({low: low, high: high}))),
|
FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Uniform({low: low, high: high}))),
|
||||||
)
|
)
|
||||||
meanValue->unpackFloat->expect->toBeCloseTo((low +. high) /. 2.0) // https://en.wikipedia.org/wiki/Continuous_uniform_distribution#Moments
|
meanValue->unpackFloat->expect->toBeCloseTo((low +. high) /. 2.0) // https://en.wikipedia.org/wiki/Continuous_uniform_distribution#Moments
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
test("of a float", () => {
|
test("of a float", () => {
|
||||||
let meanValue = run(FromDist(ToFloat(#Mean), DistributionTypes.Symbolic(#Float(7.7))))
|
let meanValue = run(FromDist(#ToFloat(#Mean), DistributionTypes.Symbolic(#Float(7.7))))
|
||||||
meanValue->unpackFloat->expect->toBeCloseTo(7.7)
|
meanValue->unpackFloat->expect->toBeCloseTo(7.7)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
open Jest
|
||||||
|
open Expect
|
||||||
|
|
||||||
|
test("todo", () => expect("1")->toBe("1"))
|
|
@ -19,23 +19,23 @@ describe("bindStatement", () => {
|
||||||
testMacro(
|
testMacro(
|
||||||
[],
|
[],
|
||||||
eBindStatement(eBindings([]), exampleStatementY),
|
eBindStatement(eBindings([]), exampleStatementY),
|
||||||
"Ok((:$_setBindings_$ {} :y 1) context: {})",
|
"Ok((:$_setBindings_$ @{} :y 1) context: @{})",
|
||||||
)
|
)
|
||||||
// Then it answers the bindings for the next statement when reduced
|
// Then it answers the bindings for the next statement when reduced
|
||||||
testMacroEval([], eBindStatement(eBindings([]), exampleStatementY), "Ok({y: 1})")
|
testMacroEval([], eBindStatement(eBindings([]), exampleStatementY), "Ok(@{y: 1})")
|
||||||
// Now let's feed a binding to see what happens
|
// Now let's feed a binding to see what happens
|
||||||
testMacro(
|
testMacro(
|
||||||
[],
|
[],
|
||||||
eBindStatement(eBindings([("x", EvNumber(2.))]), exampleStatementX),
|
eBindStatement(eBindings([("x", IEvNumber(2.))]), exampleStatementX),
|
||||||
"Ok((:$_setBindings_$ {x: 2} :y 2) context: {x: 2})",
|
"Ok((:$_setBindings_$ @{x: 2} :y 2) context: @{x: 2})",
|
||||||
)
|
)
|
||||||
// An expression does not return a binding, thus error
|
// An expression does not return a binding, thus error
|
||||||
testMacro([], eBindStatement(eBindings([]), exampleExpression), "Assignment expected")
|
testMacro([], eBindStatement(eBindings([]), exampleExpression), "Assignment expected")
|
||||||
// When bindings from previous statement are missing the context is injected. This must be the first statement of a block
|
// When bindings from previous statement are missing the context is injected. This must be the first statement of a block
|
||||||
testMacro(
|
testMacro(
|
||||||
[("z", EvNumber(99.))],
|
[("z", IEvNumber(99.))],
|
||||||
eBindStatementDefault(exampleStatementY),
|
eBindStatementDefault(exampleStatementY),
|
||||||
"Ok((:$_setBindings_$ {z: 99} :y 1) context: {z: 99})",
|
"Ok((:$_setBindings_$ @{z: 99} :y 1) context: @{z: 99})",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -43,26 +43,26 @@ describe("bindExpression", () => {
|
||||||
// x is simply bound in the expression
|
// x is simply bound in the expression
|
||||||
testMacro(
|
testMacro(
|
||||||
[],
|
[],
|
||||||
eBindExpression(eBindings([("x", EvNumber(2.))]), eSymbol("x")),
|
eBindExpression(eBindings([("x", IEvNumber(2.))]), eSymbol("x")),
|
||||||
"Ok(2 context: {x: 2})",
|
"Ok(2 context: @{x: 2})",
|
||||||
)
|
)
|
||||||
// When an let statement is the end expression then bindings are returned
|
// When an let statement is the end expression then bindings are returned
|
||||||
testMacro(
|
testMacro(
|
||||||
[],
|
[],
|
||||||
eBindExpression(eBindings([("x", EvNumber(2.))]), exampleStatementY),
|
eBindExpression(eBindings([("x", IEvNumber(2.))]), exampleStatementY),
|
||||||
"Ok((:$_exportBindings_$ (:$_setBindings_$ {x: 2} :y 1)) context: {x: 2})",
|
"Ok((:$_exportBindings_$ (:$_setBindings_$ @{x: 2} :y 1)) context: @{x: 2})",
|
||||||
)
|
)
|
||||||
// Now let's reduce that expression
|
// Now let's reduce that expression
|
||||||
testMacroEval(
|
testMacroEval(
|
||||||
[],
|
[],
|
||||||
eBindExpression(eBindings([("x", EvNumber(2.))]), exampleStatementY),
|
eBindExpression(eBindings([("x", IEvNumber(2.))]), exampleStatementY),
|
||||||
"Ok({x: 2,y: 1})",
|
"Ok(@{x: 2,y: 1})",
|
||||||
)
|
)
|
||||||
// When bindings are missing the context is injected. This must be the first and last statement of a block
|
// When bindings are missing the context is injected. This must be the first and last statement of a block
|
||||||
testMacroEval(
|
testMacroEval(
|
||||||
[("z", EvNumber(99.))],
|
[("z", IEvNumber(99.))],
|
||||||
eBindExpressionDefault(exampleStatementY),
|
eBindExpressionDefault(exampleStatementY),
|
||||||
"Ok({y: 1,z: 99})",
|
"Ok(@{y: 1,z: 99})",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ describe("block", () => {
|
||||||
testMacroEval([], eBlock(list{exampleExpression}), "Ok(1)")
|
testMacroEval([], eBlock(list{exampleExpression}), "Ok(1)")
|
||||||
// Block with a single statement
|
// Block with a single statement
|
||||||
testMacro([], eBlock(list{exampleStatementY}), "Ok((:$$_bindExpression_$$ (:$_let_$ :y 1)))")
|
testMacro([], eBlock(list{exampleStatementY}), "Ok((:$$_bindExpression_$$ (:$_let_$ :y 1)))")
|
||||||
testMacroEval([], eBlock(list{exampleStatementY}), "Ok({y: 1})")
|
testMacroEval([], eBlock(list{exampleStatementY}), "Ok(@{y: 1})")
|
||||||
// Block with a statement and an expression
|
// Block with a statement and an expression
|
||||||
testMacro(
|
testMacro(
|
||||||
[],
|
[],
|
||||||
|
@ -86,7 +86,7 @@ describe("block", () => {
|
||||||
eBlock(list{exampleStatementY, exampleStatementZ}),
|
eBlock(list{exampleStatementY, exampleStatementZ}),
|
||||||
"Ok((:$$_bindExpression_$$ (:$$_bindStatement_$$ (:$_let_$ :y 1)) (:$_let_$ :z :y)))",
|
"Ok((:$$_bindExpression_$$ (:$$_bindStatement_$$ (:$_let_$ :y 1)) (:$_let_$ :z :y)))",
|
||||||
)
|
)
|
||||||
testMacroEval([], eBlock(list{exampleStatementY, exampleStatementZ}), "Ok({y: 1,z: 1})")
|
testMacroEval([], eBlock(list{exampleStatementY, exampleStatementZ}), "Ok(@{y: 1,z: 1})")
|
||||||
// Block inside a block
|
// Block inside a block
|
||||||
testMacro([], eBlock(list{eBlock(list{exampleExpression})}), "Ok((:$$_bindExpression_$$ {1}))")
|
testMacro([], eBlock(list{eBlock(list{exampleExpression})}), "Ok((:$$_bindExpression_$$ {1}))")
|
||||||
testMacroEval([], eBlock(list{eBlock(list{exampleExpression})}), "Ok(1)")
|
testMacroEval([], eBlock(list{eBlock(list{exampleExpression})}), "Ok(1)")
|
||||||
|
@ -99,7 +99,7 @@ describe("block", () => {
|
||||||
testMacroEval(
|
testMacroEval(
|
||||||
[],
|
[],
|
||||||
eBlock(list{eLetStatement("z", eBlock(list{eBlock(list{exampleExpressionY})}))}),
|
eBlock(list{eLetStatement("z", eBlock(list{eBlock(list{exampleExpressionY})}))}),
|
||||||
"Ok({z: :y})",
|
"Ok(@{z: :y})",
|
||||||
)
|
)
|
||||||
// Empty block
|
// Empty block
|
||||||
testMacro([], eBlock(list{}), "Ok(:undefined block)") //TODO: should be an error
|
testMacro([], eBlock(list{}), "Ok(:undefined block)") //TODO: should be an error
|
||||||
|
@ -115,7 +115,7 @@ describe("block", () => {
|
||||||
"Ok((:$$_bindExpression_$$ {(:$_let_$ :y (:add :x 1)); :y}))",
|
"Ok((:$$_bindExpression_$$ {(:$_let_$ :y (:add :x 1)); :y}))",
|
||||||
)
|
)
|
||||||
testMacroEval(
|
testMacroEval(
|
||||||
[("x", EvNumber(1.))],
|
[("x", IEvNumber(1.))],
|
||||||
eBlock(list{
|
eBlock(list{
|
||||||
eBlock(list{
|
eBlock(list{
|
||||||
eLetStatement("y", eFunction("add", list{eSymbol("x"), eNumber(1.)})),
|
eLetStatement("y", eFunction("add", list{eSymbol("x"), eNumber(1.)})),
|
||||||
|
@ -135,12 +135,12 @@ describe("lambda", () => {
|
||||||
testMacro([], callLambdaExpression, "Ok(((:$$_lambda_$$ [y] :y) 1))")
|
testMacro([], callLambdaExpression, "Ok(((:$$_lambda_$$ [y] :y) 1))")
|
||||||
testMacroEval([], callLambdaExpression, "Ok(1)")
|
testMacroEval([], callLambdaExpression, "Ok(1)")
|
||||||
// Parameters shadow the outer scope
|
// Parameters shadow the outer scope
|
||||||
testMacroEval([("y", EvNumber(666.))], callLambdaExpression, "Ok(1)")
|
testMacroEval([("y", IEvNumber(666.))], callLambdaExpression, "Ok(1)")
|
||||||
// When not shadowed by the parameters, the outer scope variables are available
|
// When not shadowed by the parameters, the outer scope variables are available
|
||||||
let lambdaExpression = eFunction(
|
let lambdaExpression = eFunction(
|
||||||
"$$_lambda_$$",
|
"$$_lambda_$$",
|
||||||
list{eArrayString(["z"]), eFunction("add", list{eSymbol("y"), eSymbol("z")})},
|
list{eArrayString(["z"]), eFunction("add", list{eSymbol("y"), eSymbol("z")})},
|
||||||
)
|
)
|
||||||
let callLambdaExpression = eList(list{lambdaExpression, eNumber(1.)})
|
let callLambdaExpression = eList(list{lambdaExpression, eNumber(1.)})
|
||||||
testMacroEval([("y", EvNumber(666.))], callLambdaExpression, "Ok(667)")
|
testMacroEval([("y", IEvNumber(666.))], callLambdaExpression, "Ok(667)")
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module ExpressionValue = ReducerInterface.ExpressionValue
|
module ExpressionValue = ReducerInterface.ExternalExpressionValue
|
||||||
|
|
||||||
open Jest
|
open Jest
|
||||||
open Expect
|
open Expect
|
||||||
|
@ -17,10 +17,6 @@ describe("builtin", () => {
|
||||||
testEval("1-1", "Ok(0)")
|
testEval("1-1", "Ok(0)")
|
||||||
testEval("2>1", "Ok(true)")
|
testEval("2>1", "Ok(true)")
|
||||||
testEval("concat('a','b')", "Ok('ab')")
|
testEval("concat('a','b')", "Ok('ab')")
|
||||||
testEval(
|
|
||||||
"addOne(t)=t+1; toList(mapSamples(fromSamples([1,2,3,4,5,6]), addOne))",
|
|
||||||
"Ok([2,3,4,5,6,7])",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("builtin exception", () => {
|
describe("builtin exception", () => {
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
module ExpressionT = Reducer_Expression_T
|
// Reducer_Helpers
|
||||||
module ExpressionValue = ReducerInterface.ExpressionValue
|
|
||||||
module ErrorValue = Reducer_ErrorValue
|
module ErrorValue = Reducer_ErrorValue
|
||||||
module Bindings = Reducer_Category_Bindings
|
module ExternalExpressionValue = ReducerInterface.ExternalExpressionValue
|
||||||
|
module InternalExpressionValue = ReducerInterface.InternalExpressionValue
|
||||||
|
module Bindings = Reducer_Bindings
|
||||||
|
|
||||||
let removeDefaults = (ev: ExpressionT.expressionValue): ExpressionT.expressionValue =>
|
let removeDefaultsInternal = (iev: InternalExpressionValue.t) => {
|
||||||
switch ev {
|
switch iev {
|
||||||
| EvRecord(extbindings) => {
|
| InternalExpressionValue.IEvBindings(nameSpace) =>
|
||||||
let bindings: Bindings.t = Bindings.fromRecord(extbindings)
|
Bindings.removeOther(
|
||||||
let keys = Js.Dict.keys(Reducer.defaultExternalBindings)
|
nameSpace,
|
||||||
Belt.Map.String.keep(bindings, (key, _value) => {
|
ReducerInterface.StdLib.internalStdLib,
|
||||||
let removeThis = Js.Array2.includes(keys, key)
|
)->InternalExpressionValue.IEvBindings
|
||||||
!removeThis
|
|
||||||
})->Bindings.toExpressionValue
|
|
||||||
}
|
|
||||||
| value => value
|
| value => value
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let rRemoveDefaults = r => Belt.Result.map(r, ev => removeDefaults(ev))
|
let removeDefaultsExternal = (ev: ExternalExpressionValue.t): ExternalExpressionValue.t =>
|
||||||
|
ev->InternalExpressionValue.toInternal->removeDefaultsInternal->InternalExpressionValue.toExternal
|
||||||
|
|
||||||
|
let rRemoveDefaultsInternal = r => Belt.Result.map(r, removeDefaultsInternal)
|
||||||
|
let rRemoveDefaultsExternal = r => Belt.Result.map(r, removeDefaultsExternal)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
open ReducerInterface.ExpressionValue
|
|
||||||
module MathJs = Reducer_MathJs
|
module MathJs = Reducer_MathJs
|
||||||
module ErrorValue = Reducer.ErrorValue
|
module ErrorValue = Reducer.ErrorValue
|
||||||
|
|
||||||
|
@ -6,14 +5,14 @@ open Jest
|
||||||
open ExpectJs
|
open ExpectJs
|
||||||
|
|
||||||
describe("eval", () => {
|
describe("eval", () => {
|
||||||
test("Number", () => expect(MathJs.Eval.eval("1"))->toEqual(Ok(EvNumber(1.))))
|
test("Number", () => expect(MathJs.Eval.eval("1"))->toEqual(Ok(IEvNumber(1.))))
|
||||||
test("Number expr", () => expect(MathJs.Eval.eval("1-1"))->toEqual(Ok(EvNumber(0.))))
|
test("Number expr", () => expect(MathJs.Eval.eval("1-1"))->toEqual(Ok(IEvNumber(0.))))
|
||||||
test("String", () => expect(MathJs.Eval.eval("'hello'"))->toEqual(Ok(EvString("hello"))))
|
test("String", () => expect(MathJs.Eval.eval("'hello'"))->toEqual(Ok(IEvString("hello"))))
|
||||||
test("String expr", () =>
|
test("String expr", () =>
|
||||||
expect(MathJs.Eval.eval("concat('hello ','world')"))->toEqual(Ok(EvString("hello world")))
|
expect(MathJs.Eval.eval("concat('hello ','world')"))->toEqual(Ok(IEvString("hello world")))
|
||||||
)
|
)
|
||||||
test("Boolean", () => expect(MathJs.Eval.eval("true"))->toEqual(Ok(EvBool(true))))
|
test("Boolean", () => expect(MathJs.Eval.eval("true"))->toEqual(Ok(IEvBool(true))))
|
||||||
test("Boolean expr", () => expect(MathJs.Eval.eval("2>1"))->toEqual(Ok(EvBool(true))))
|
test("Boolean expr", () => expect(MathJs.Eval.eval("2>1"))->toEqual(Ok(IEvBool(true))))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("errors", () => {
|
describe("errors", () => {
|
||||||
|
|
|
@ -236,7 +236,8 @@ describe("Peggy parse", () => {
|
||||||
testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}")
|
testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}")
|
||||||
})
|
})
|
||||||
describe("Module", () => {
|
describe("Module", () => {
|
||||||
testParse("Math.pi", "{(::$_atIndex_$ @Math 'pi')}")
|
testParse("x", "{:x}")
|
||||||
|
testParse("Math.pi", "{:Math.pi}")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ describe("Peggy parse type", () => {
|
||||||
"{(::$_typeOf_$ :f (::$_typeFunction_$ (::$_constructArray_$ (#number #number #number))))}",
|
"{(::$_typeOf_$ :f (::$_typeFunction_$ (::$_constructArray_$ (#number #number #number))))}",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
describe("high priority modifier", () => {
|
describe("high priority contract", () => {
|
||||||
testParse(
|
testParse(
|
||||||
"answer: number<-min<-max(100)|string",
|
"answer: number<-min<-max(100)|string",
|
||||||
"{(::$_typeOf_$ :answer (::$_typeOr_$ (::$_constructArray_$ ((::$_typeModifier_max_$ (::$_typeModifier_min_$ #number) 100) #string))))}",
|
"{(::$_typeOf_$ :answer (::$_typeOr_$ (::$_constructArray_$ ((::$_typeModifier_max_$ (::$_typeModifier_min_$ #number) 100) #string))))}",
|
||||||
|
@ -30,7 +30,7 @@ describe("Peggy parse type", () => {
|
||||||
"{(::$_typeOf_$ :answer (::$_typeModifier_memberOf_$ #number (::$_constructArray_$ (1 3 5))))}",
|
"{(::$_typeOf_$ :answer (::$_typeModifier_memberOf_$ #number (::$_constructArray_$ (1 3 5))))}",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
describe("low priority modifier", () => {
|
describe("low priority contract", () => {
|
||||||
testParse(
|
testParse(
|
||||||
"answer: number | string $ opaque",
|
"answer: number | string $ opaque",
|
||||||
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
|
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
|
||||||
|
@ -63,14 +63,14 @@ describe("Peggy parse type", () => {
|
||||||
"{(::$_typeOf_$ :weekend (::$_typeOr_$ (::$_constructArray_$ ((::$_typeConstructor_$ #Saturday (::$_constructArray_$ ())) (::$_typeConstructor_$ #Sunday (::$_constructArray_$ ()))))))}",
|
"{(::$_typeOf_$ :weekend (::$_typeOr_$ (::$_constructArray_$ ((::$_typeConstructor_$ #Saturday (::$_constructArray_$ ())) (::$_typeConstructor_$ #Sunday (::$_constructArray_$ ()))))))}",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
describe("type paranthesis", () => {
|
describe("type parenthesis", () => {
|
||||||
//$ is introduced to avoid paranthesis
|
//$ is introduced to avoid parenthesis
|
||||||
testParse(
|
testParse(
|
||||||
"answer: (number|string)<-opaque",
|
"answer: (number|string)<-opaque",
|
||||||
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
|
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
describe("squiggle expressions in type modifiers", () => {
|
describe("squiggle expressions in type contracts", () => {
|
||||||
testParse(
|
testParse(
|
||||||
"odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))",
|
"odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))",
|
||||||
"{:odds1 = {(::$_constructArray_$ (1 3 5))}; :odds2 = {(::$_constructArray_$ (7 9))}; (::$_typeAlias_$ #odds (::$_typeModifier_memberOf_$ #number (::concat :odds1 :odds2)))}",
|
"{:odds1 = {(::$_constructArray_$ (1 3 5))}; :odds2 = {(::$_constructArray_$ (7 9))}; (::$_typeAlias_$ #odds (::$_typeModifier_memberOf_$ #number (::concat :odds1 :odds2)))}",
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
module Expression = Reducer_Expression
|
module Expression = Reducer_Expression
|
||||||
module ExpressionT = Reducer_Expression_T
|
module ExpressionT = Reducer_Expression_T
|
||||||
module ExpressionValue = ReducerInterface_ExpressionValue
|
module ExpressionValue = ReducerInterface.InternalExpressionValue
|
||||||
module Parse = Reducer_Peggy_Parse
|
module Parse = Reducer_Peggy_Parse
|
||||||
module Result = Belt.Result
|
module Result = Belt.Result
|
||||||
module ToExpression = Reducer_Peggy_ToExpression
|
module ToExpression = Reducer_Peggy_ToExpression
|
||||||
|
module Bindings = Reducer_Bindings
|
||||||
|
|
||||||
open Jest
|
open Jest
|
||||||
open Expect
|
open Expect
|
||||||
|
@ -29,7 +30,7 @@ let expectToExpressionToBe = (expr, answer, ~v="_", ()) => {
|
||||||
ExpressionValue.defaultEnvironment,
|
ExpressionValue.defaultEnvironment,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
->Reducer_Helpers.rRemoveDefaults
|
->Reducer_Helpers.rRemoveDefaultsInternal
|
||||||
->ExpressionValue.toStringResultOkless
|
->ExpressionValue.toStringResultOkless
|
||||||
(a1, a2)->expect->toEqual((answer, v))
|
(a1, a2)->expect->toEqual((answer, v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
module Bindings = Reducer_Bindings
|
||||||
|
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
|
||||||
|
|
||||||
open Jest
|
open Jest
|
||||||
open Reducer_Peggy_TestHelpers
|
open Reducer_Peggy_TestHelpers
|
||||||
|
|
||||||
describe("Peggy to Expression", () => {
|
describe("Peggy to Expression", () => {
|
||||||
describe("literals operators parenthesis", () => {
|
describe("literals operators parenthesis", () => {
|
||||||
// Note that there is always an outer block. Otherwise, external bindings are ignrored at the first statement
|
// Note that there is always an outer block. Otherwise, external bindings are ignored at the first statement
|
||||||
testToExpression("1", "{1}", ~v="1", ())
|
testToExpression("1", "{1}", ~v="1", ())
|
||||||
testToExpression("'hello'", "{'hello'}", ~v="'hello'", ())
|
testToExpression("'hello'", "{'hello'}", ~v="'hello'", ())
|
||||||
testToExpression("true", "{true}", ~v="true", ())
|
testToExpression("true", "{true}", ~v="true", ())
|
||||||
|
@ -22,11 +25,11 @@ describe("Peggy to Expression", () => {
|
||||||
|
|
||||||
describe("multi-line", () => {
|
describe("multi-line", () => {
|
||||||
testToExpression("x=1; 2", "{(:$_let_$ :x {1}); 2}", ~v="2", ())
|
testToExpression("x=1; 2", "{(:$_let_$ :x {1}); 2}", ~v="2", ())
|
||||||
testToExpression("x=1; y=2", "{(:$_let_$ :x {1}); (:$_let_$ :y {2})}", ~v="{x: 1,y: 2}", ())
|
testToExpression("x=1; y=2", "{(:$_let_$ :x {1}); (:$_let_$ :y {2})}", ~v="@{x: 1,y: 2}", ())
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("variables", () => {
|
describe("variables", () => {
|
||||||
testToExpression("x = 1", "{(:$_let_$ :x {1})}", ~v="{x: 1}", ())
|
testToExpression("x = 1", "{(:$_let_$ :x {1})}", ~v="@{x: 1}", ())
|
||||||
testToExpression("x", "{:x}", ~v=":x", ()) //TODO: value should return error
|
testToExpression("x", "{:x}", ~v=":x", ()) //TODO: value should return error
|
||||||
testToExpression("x = 1; x", "{(:$_let_$ :x {1}); :x}", ~v="1", ())
|
testToExpression("x = 1; x", "{(:$_let_$ :x {1}); :x}", ~v="1", ())
|
||||||
})
|
})
|
||||||
|
@ -35,7 +38,7 @@ describe("Peggy to Expression", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"identity(x) = x",
|
"identity(x) = x",
|
||||||
"{(:$_let_$ :identity (:$$_lambda_$$ [x] {:x}))}",
|
"{(:$_let_$ :identity (:$$_lambda_$$ [x] {:x}))}",
|
||||||
~v="{identity: lambda(x=>internal code)}",
|
~v="@{identity: lambda(x=>internal code)}",
|
||||||
(),
|
(),
|
||||||
) // Function definitions become lambda assignments
|
) // Function definitions become lambda assignments
|
||||||
testToExpression("identity(x)", "{(:identity :x)}", ()) // Note value returns error properly
|
testToExpression("identity(x)", "{(:identity :x)}", ()) // Note value returns error properly
|
||||||
|
@ -155,7 +158,7 @@ describe("Peggy to Expression", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"y=99; x={y=1; y}",
|
"y=99; x={y=1; y}",
|
||||||
"{(:$_let_$ :y {99}); (:$_let_$ :x {(:$_let_$ :y {1}); :y})}",
|
"{(:$_let_$ :y {99}); (:$_let_$ :x {(:$_let_$ :y {1}); :y})}",
|
||||||
~v="{x: 1,y: 99}",
|
~v="@{x: 1,y: 99}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -165,24 +168,32 @@ describe("Peggy to Expression", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"f={|x| x}",
|
"f={|x| x}",
|
||||||
"{(:$_let_$ :f {(:$$_lambda_$$ [x] {:x})})}",
|
"{(:$_let_$ :f {(:$$_lambda_$$ [x] {:x})})}",
|
||||||
~v="{f: lambda(x=>internal code)}",
|
~v="@{f: lambda(x=>internal code)}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"f(x)=x",
|
"f(x)=x",
|
||||||
"{(:$_let_$ :f (:$$_lambda_$$ [x] {:x}))}",
|
"{(:$_let_$ :f (:$$_lambda_$$ [x] {:x}))}",
|
||||||
~v="{f: lambda(x=>internal code)}",
|
~v="@{f: lambda(x=>internal code)}",
|
||||||
(),
|
(),
|
||||||
) // Function definitions are lambda assignments
|
) // Function definitions are lambda assignments
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"f(x)=x ? 1 : 0",
|
"f(x)=x ? 1 : 0",
|
||||||
"{(:$_let_$ :f (:$$_lambda_$$ [x] {(:$$_ternary_$$ :x 1 0)}))}",
|
"{(:$_let_$ :f (:$$_lambda_$$ [x] {(:$$_ternary_$$ :x 1 0)}))}",
|
||||||
~v="{f: lambda(x=>internal code)}",
|
~v="@{f: lambda(x=>internal code)}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("module", () => {
|
describe("module", () => {
|
||||||
testToExpression("Math.pi", "{(:$_atIndex_$ :Math 'pi')}", ~v="3.141592653589793", ())
|
// testToExpression("Math.pi", "{:Math.pi}", ~v="3.141592653589793", ())
|
||||||
|
// Only.test("stdlibrary", () => {
|
||||||
|
// ReducerInterface_StdLib.internalStdLib
|
||||||
|
// ->IEvBindings
|
||||||
|
// ->InternalExpressionValue.toString
|
||||||
|
// ->expect
|
||||||
|
// ->toBe("")
|
||||||
|
// })
|
||||||
|
testToExpression("Math.pi", "{:Math.pi}", ~v="3.141592653589793", ())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,7 +6,7 @@ describe("Peggy Types to Expression", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"p: number",
|
"p: number",
|
||||||
"{(:$_typeOf_$ :p #number)}",
|
"{(:$_typeOf_$ :p #number)}",
|
||||||
~v="{_typeReferences_: {p: #number}}",
|
~v="@{_typeReferences_: {p: #number}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -14,7 +14,7 @@ describe("Peggy Types to Expression", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"type index=number",
|
"type index=number",
|
||||||
"{(:$_typeAlias_$ #index #number)}",
|
"{(:$_typeAlias_$ #index #number)}",
|
||||||
~v="{_typeAliases_: {index: #number}}",
|
~v="@{_typeAliases_: {index: #number}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -22,7 +22,7 @@ describe("Peggy Types to Expression", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number|string|distribution",
|
"answer: number|string|distribution",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ (#number #string #distribution))))}",
|
"{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ (#number #string #distribution))))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeOr',typeOr: [#number,#string,#distribution]}}}",
|
~v="@{_typeReferences_: {answer: {typeOr: [#number,#string,#distribution],typeTag: 'typeOr'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -30,67 +30,67 @@ describe("Peggy Types to Expression", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"f: number=>number=>number",
|
"f: number=>number=>number",
|
||||||
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number #number))))}",
|
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number #number))))}",
|
||||||
~v="{_typeReferences_: {f: {typeTag: 'typeFunction',inputs: [#number,#number],output: #number}}}",
|
~v="@{_typeReferences_: {f: {inputs: [#number,#number],output: #number,typeTag: 'typeFunction'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"f: number=>number",
|
"f: number=>number",
|
||||||
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number))))}",
|
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number))))}",
|
||||||
~v="{_typeReferences_: {f: {typeTag: 'typeFunction',inputs: [#number],output: #number}}}",
|
~v="@{_typeReferences_: {f: {inputs: [#number],output: #number,typeTag: 'typeFunction'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
describe("high priority modifier", () => {
|
describe("high priority contract", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number<-min(1)<-max(100)|string",
|
"answer: number<-min(1)<-max(100)|string",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ ((:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 100) #string))))}",
|
"{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ ((:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 100) #string))))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeOr',typeOr: [{typeTag: 'typeIdentifier',typeIdentifier: #number,min: 1,max: 100},#string]}}}",
|
~v="@{_typeReferences_: {answer: {typeOr: [{max: 100,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'},#string],typeTag: 'typeOr'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number<-memberOf([1,3,5])",
|
"answer: number<-memberOf([1,3,5])",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeModifier_memberOf_$ #number (:$_constructArray_$ (1 3 5))))}",
|
"{(:$_typeOf_$ :answer (:$_typeModifier_memberOf_$ #number (:$_constructArray_$ (1 3 5))))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,memberOf: [1,3,5]}}}",
|
~v="@{_typeReferences_: {answer: {memberOf: [1,3,5],typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number<-min(1)",
|
"answer: number<-min(1)",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ #number 1))}",
|
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ #number 1))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,min: 1}}}",
|
~v="@{_typeReferences_: {answer: {min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number<-max(10)",
|
"answer: number<-max(10)",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ #number 10))}",
|
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ #number 10))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,max: 10}}}",
|
~v="@{_typeReferences_: {answer: {max: 10,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number<-min(1)<-max(10)",
|
"answer: number<-min(1)<-max(10)",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 10))}",
|
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 10))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,min: 1,max: 10}}}",
|
~v="@{_typeReferences_: {answer: {max: 10,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number<-max(10)<-min(1)",
|
"answer: number<-max(10)<-min(1)",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ (:$_typeModifier_max_$ #number 10) 1))}",
|
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ (:$_typeModifier_max_$ #number 10) 1))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,max: 10,min: 1}}}",
|
~v="@{_typeReferences_: {answer: {max: 10,min: 1,typeIdentifier: #number,typeTag: 'typeIdentifier'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
describe("low priority modifier", () => {
|
describe("low priority contract", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"answer: number | string $ opaque",
|
"answer: number | string $ opaque",
|
||||||
"{(:$_typeOf_$ :answer (:$_typeModifier_opaque_$ (:$_typeOr_$ (:$_constructArray_$ (#number #string)))))}",
|
"{(:$_typeOf_$ :answer (:$_typeModifier_opaque_$ (:$_typeOr_$ (:$_constructArray_$ (#number #string)))))}",
|
||||||
~v="{_typeReferences_: {answer: {typeTag: 'typeOr',typeOr: [#number,#string],opaque: true}}}",
|
~v="@{_typeReferences_: {answer: {opaque: true,typeOr: [#number,#string],typeTag: 'typeOr'}}}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
describe("squiggle expressions in type modifiers", () => {
|
describe("squiggle expressions in type contracts", () => {
|
||||||
testToExpression(
|
testToExpression(
|
||||||
"odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))",
|
"odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))",
|
||||||
"{(:$_let_$ :odds1 {(:$_constructArray_$ (1 3 5))}); (:$_let_$ :odds2 {(:$_constructArray_$ (7 9))}); (:$_typeAlias_$ #odds (:$_typeModifier_memberOf_$ #number (:concat :odds1 :odds2)))}",
|
"{(:$_let_$ :odds1 {(:$_constructArray_$ (1 3 5))}); (:$_let_$ :odds2 {(:$_constructArray_$ (7 9))}); (:$_typeAlias_$ #odds (:$_typeModifier_memberOf_$ #number (:concat :odds1 :odds2)))}",
|
||||||
~v="{_typeAliases_: {odds: {typeTag: 'typeIdentifier',typeIdentifier: #number,memberOf: [1,3,5,7,9]}},odds1: [1,3,5],odds2: [7,9]}",
|
~v="@{_typeAliases_: {odds: {memberOf: [1,3,5,7,9],typeIdentifier: #number,typeTag: 'typeIdentifier'}},odds1: [1,3,5],odds2: [7,9]}",
|
||||||
(),
|
(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
open Jest
|
||||||
|
open Reducer_Peggy_TestHelpers
|
||||||
|
|
||||||
|
describe("Peggy void", () => {
|
||||||
|
//literal
|
||||||
|
testToExpression("()", "{()}", ~v="()", ())
|
||||||
|
testToExpression(
|
||||||
|
"fn()=1",
|
||||||
|
"{(:$_let_$ :fn (:$$_lambda_$$ [_] {1}))}",
|
||||||
|
~v="@{fn: lambda(_=>internal code)}",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
testToExpression("fn()=1; fn()", "{(:$_let_$ :fn (:$$_lambda_$$ [_] {1})); (:fn ())}", ~v="1", ())
|
||||||
|
testToExpression(
|
||||||
|
"fn(a)=(); call fn(1)",
|
||||||
|
"{(:$_let_$ :fn (:$$_lambda_$$ [a] {()})); (:$_let_$ :_ {(:fn 1)})}",
|
||||||
|
~v="@{_: (),fn: lambda(a=>internal code)}",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
})
|
|
@ -1,7 +1,6 @@
|
||||||
module ExpressionT = Reducer_Expression_T
|
module ExpressionT = Reducer_Expression_T
|
||||||
module ExpressionValue = ReducerInterface.ExpressionValue
|
module ExternalExpressionValue = ReducerInterface.ExternalExpressionValue
|
||||||
module ErrorValue = Reducer_ErrorValue
|
module ErrorValue = Reducer_ErrorValue
|
||||||
module Bindings = Reducer_Category_Bindings
|
|
||||||
|
|
||||||
open Jest
|
open Jest
|
||||||
open Expect
|
open Expect
|
||||||
|
@ -9,7 +8,7 @@ open Expect
|
||||||
let unwrapRecord = rValue =>
|
let unwrapRecord = rValue =>
|
||||||
rValue->Belt.Result.flatMap(value =>
|
rValue->Belt.Result.flatMap(value =>
|
||||||
switch value {
|
switch value {
|
||||||
| ExpressionValue.EvRecord(aRecord) => Ok(aRecord)
|
| ExternalExpressionValue.EvRecord(aRecord) => Ok(aRecord)
|
||||||
| _ => ErrorValue.RETodo("TODO: External bindings must be returned")->Error
|
| _ => ErrorValue.RETodo("TODO: External bindings must be returned")->Error
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -19,18 +18,18 @@ let expectParseToBe = (expr: string, answer: string) =>
|
||||||
|
|
||||||
let expectEvalToBe = (expr: string, answer: string) =>
|
let expectEvalToBe = (expr: string, answer: string) =>
|
||||||
Reducer.evaluate(expr)
|
Reducer.evaluate(expr)
|
||||||
->Reducer_Helpers.rRemoveDefaults
|
->Reducer_Helpers.rRemoveDefaultsExternal
|
||||||
->ExpressionValue.toStringResult
|
->ExternalExpressionValue.toStringResult
|
||||||
->expect
|
->expect
|
||||||
->toBe(answer)
|
->toBe(answer)
|
||||||
|
|
||||||
let expectEvalError = (expr: string) =>
|
let expectEvalError = (expr: string) =>
|
||||||
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toMatch("Error\(")
|
Reducer.evaluate(expr)->ExternalExpressionValue.toStringResult->expect->toMatch("Error\(")
|
||||||
|
|
||||||
let expectEvalBindingsToBe = (expr: string, bindings: Reducer.externalBindings, answer: string) =>
|
let expectEvalBindingsToBe = (expr: string, bindings: Reducer.externalBindings, answer: string) =>
|
||||||
Reducer.evaluateUsingOptions(expr, ~externalBindings=Some(bindings), ~environment=None)
|
Reducer.evaluateUsingOptions(expr, ~externalBindings=Some(bindings), ~environment=None)
|
||||||
->Reducer_Helpers.rRemoveDefaults
|
->Reducer_Helpers.rRemoveDefaultsExternal
|
||||||
->ExpressionValue.toStringResult
|
->ExternalExpressionValue.toStringResult
|
||||||
->expect
|
->expect
|
||||||
->toBe(answer)
|
->toBe(answer)
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
open Jest
|
open Jest
|
||||||
open Expect
|
open Expect
|
||||||
|
|
||||||
module Bindings = Reducer_Expression_Bindings
|
module BindingsReplacer = Reducer_Expression_BindingsReplacer
|
||||||
module Expression = Reducer_Expression
|
module Expression = Reducer_Expression
|
||||||
module ExpressionValue = ReducerInterface_ExpressionValue
|
// module ExpressionValue = ReducerInterface.ExpressionValue
|
||||||
|
module InternalExpressionValue = ReducerInterface.InternalExpressionValue
|
||||||
module ExpressionWithContext = Reducer_ExpressionWithContext
|
module ExpressionWithContext = Reducer_ExpressionWithContext
|
||||||
module Macro = Reducer_Expression_Macro
|
module Macro = Reducer_Expression_Macro
|
||||||
module T = Reducer_Expression_T
|
module T = Reducer_Expression_T
|
||||||
|
module Bindings = Reducer_Bindings
|
||||||
|
|
||||||
let testMacro_ = (
|
let testMacro_ = (
|
||||||
tester,
|
tester,
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedCode: string,
|
expectedCode: string,
|
||||||
) => {
|
) => {
|
||||||
let bindings = Belt.Map.String.fromArray(bindArray)
|
let bindings = Bindings.fromArray(bindArray)
|
||||||
tester(expr->T.toString, () =>
|
tester(expr->T.toString, () =>
|
||||||
expr
|
expr
|
||||||
->Macro.expandMacroCall(
|
->Macro.expandMacroCall(
|
||||||
bindings,
|
bindings,
|
||||||
ExpressionValue.defaultEnvironment,
|
InternalExpressionValue.defaultEnvironment,
|
||||||
Expression.reduceExpression,
|
Expression.reduceExpression,
|
||||||
)
|
)
|
||||||
->ExpressionWithContext.toStringResult
|
->ExpressionWithContext.toStringResult
|
||||||
|
@ -30,39 +32,43 @@ let testMacro_ = (
|
||||||
|
|
||||||
let testMacroEval_ = (
|
let testMacroEval_ = (
|
||||||
tester,
|
tester,
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedValue: string,
|
expectedValue: string,
|
||||||
) => {
|
) => {
|
||||||
let bindings = Belt.Map.String.fromArray(bindArray)
|
let bindings = Bindings.fromArray(bindArray)
|
||||||
tester(expr->T.toString, () =>
|
tester(expr->T.toString, () =>
|
||||||
expr
|
expr
|
||||||
->Macro.doMacroCall(bindings, ExpressionValue.defaultEnvironment, Expression.reduceExpression)
|
->Macro.doMacroCall(
|
||||||
->ExpressionValue.toStringResult
|
bindings,
|
||||||
|
InternalExpressionValue.defaultEnvironment,
|
||||||
|
Expression.reduceExpression,
|
||||||
|
)
|
||||||
|
->InternalExpressionValue.toStringResult
|
||||||
->expect
|
->expect
|
||||||
->toEqual(expectedValue)
|
->toEqual(expectedValue)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let testMacro = (
|
let testMacro = (
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedExpr: string,
|
expectedExpr: string,
|
||||||
) => testMacro_(test, bindArray, expr, expectedExpr)
|
) => testMacro_(test, bindArray, expr, expectedExpr)
|
||||||
let testMacroEval = (
|
let testMacroEval = (
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedValue: string,
|
expectedValue: string,
|
||||||
) => testMacroEval_(test, bindArray, expr, expectedValue)
|
) => testMacroEval_(test, bindArray, expr, expectedValue)
|
||||||
|
|
||||||
module MySkip = {
|
module MySkip = {
|
||||||
let testMacro = (
|
let testMacro = (
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedExpr: string,
|
expectedExpr: string,
|
||||||
) => testMacro_(Skip.test, bindArray, expr, expectedExpr)
|
) => testMacro_(Skip.test, bindArray, expr, expectedExpr)
|
||||||
let testMacroEval = (
|
let testMacroEval = (
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedValue: string,
|
expectedValue: string,
|
||||||
) => testMacroEval_(Skip.test, bindArray, expr, expectedValue)
|
) => testMacroEval_(Skip.test, bindArray, expr, expectedValue)
|
||||||
|
@ -70,12 +76,12 @@ module MySkip = {
|
||||||
|
|
||||||
module MyOnly = {
|
module MyOnly = {
|
||||||
let testMacro = (
|
let testMacro = (
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedExpr: string,
|
expectedExpr: string,
|
||||||
) => testMacro_(Only.test, bindArray, expr, expectedExpr)
|
) => testMacro_(Only.test, bindArray, expr, expectedExpr)
|
||||||
let testMacroEval = (
|
let testMacroEval = (
|
||||||
bindArray: array<(string, ExpressionValue.expressionValue)>,
|
bindArray: array<(string, InternalExpressionValue.t)>,
|
||||||
expr: T.expression,
|
expr: T.expression,
|
||||||
expectedValue: string,
|
expectedValue: string,
|
||||||
) => testMacroEval_(Only.test, bindArray, expr, expectedValue)
|
) => testMacroEval_(Only.test, bindArray, expr, expectedValue)
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
module Expression = Reducer_Expression
|
||||||
|
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
|
||||||
|
module Bindings = Reducer_Bindings
|
||||||
|
module T = Reducer_Type_T
|
||||||
|
module TypeCompile = Reducer_Type_Compile
|
||||||
|
|
||||||
|
open Jest
|
||||||
|
open Expect
|
||||||
|
|
||||||
|
let myIevEval = (aTypeSourceCode: string) =>
|
||||||
|
TypeCompile.ievFromTypeExpression(aTypeSourceCode, Expression.reduceExpression)
|
||||||
|
let myIevEvalToString = (aTypeSourceCode: string) =>
|
||||||
|
myIevEval(aTypeSourceCode)->InternalExpressionValue.toStringResult
|
||||||
|
|
||||||
|
let myIevExpectEqual = (aTypeSourceCode, answer) =>
|
||||||
|
expect(myIevEvalToString(aTypeSourceCode))->toEqual(answer)
|
||||||
|
|
||||||
|
let myIevTest = (test, aTypeSourceCode, answer) =>
|
||||||
|
test(aTypeSourceCode, () => myIevExpectEqual(aTypeSourceCode, answer))
|
||||||
|
|
||||||
|
let myTypeEval = (aTypeSourceCode: string) =>
|
||||||
|
TypeCompile.fromTypeExpression(aTypeSourceCode, Expression.reduceExpression)
|
||||||
|
let myTypeEvalToString = (aTypeSourceCode: string) => myTypeEval(aTypeSourceCode)->T.toStringResult
|
||||||
|
|
||||||
|
let myTypeExpectEqual = (aTypeSourceCode, answer) =>
|
||||||
|
expect(myTypeEvalToString(aTypeSourceCode))->toEqual(answer)
|
||||||
|
|
||||||
|
let myTypeTest = (test, aTypeSourceCode, answer) =>
|
||||||
|
test(aTypeSourceCode, () => myTypeExpectEqual(aTypeSourceCode, answer))
|
||||||
|
|
||||||
|
// | ItTypeIdentifier(string)
|
||||||
|
myTypeTest(test, "number", "number")
|
||||||
|
myTypeTest(test, "(number)", "number")
|
||||||
|
// | ItModifiedType({modifiedType: iType})
|
||||||
|
myIevTest(test, "number<-min(0)", "Ok({min: 0,typeIdentifier: #number,typeTag: 'typeIdentifier'})")
|
||||||
|
myTypeTest(test, "number<-min(0)", "number<-min(0)")
|
||||||
|
// | ItTypeOr({typeOr: array<iType>})
|
||||||
|
myTypeTest(test, "number | string", "(number | string)")
|
||||||
|
// | ItTypeFunction({inputs: array<iType>, output: iType})
|
||||||
|
myTypeTest(test, "number => number => number", "(number => number => number)")
|
||||||
|
// | ItTypeArray({element: iType})
|
||||||
|
myIevTest(test, "[number]", "Ok({element: #number,typeTag: 'typeArray'})")
|
||||||
|
myTypeTest(test, "[number]", "[number]")
|
||||||
|
// | ItTypeTuple({elements: array<iType>})
|
||||||
|
myTypeTest(test, "[number, string]", "[number, string]")
|
||||||
|
// | ItTypeRecord({properties: Belt.Map.String.t<iType>})
|
||||||
|
myIevTest(
|
||||||
|
test,
|
||||||
|
"{age: number, name: string}",
|
||||||
|
"Ok({properties: {age: #number,name: #string},typeTag: 'typeRecord'})",
|
||||||
|
)
|
||||||
|
myTypeTest(test, "{age: number, name: string}", "{age: number, name: string}")
|
|
@ -0,0 +1,41 @@
|
||||||
|
module Expression = Reducer_Expression
|
||||||
|
module ExpressionT = Reducer_Expression_T
|
||||||
|
module ErrorValue = Reducer_ErrorValue
|
||||||
|
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
|
||||||
|
module Bindings = Reducer_Bindings
|
||||||
|
module T = Reducer_Type_T
|
||||||
|
module TypeChecker = Reducer_Type_TypeChecker
|
||||||
|
|
||||||
|
open Jest
|
||||||
|
open Expect
|
||||||
|
|
||||||
|
let checkArgumentsSourceCode = (aTypeSourceCode: string, sourceCode: string): result<
|
||||||
|
'v,
|
||||||
|
ErrorValue.t,
|
||||||
|
> => {
|
||||||
|
let reducerFn = Expression.reduceExpression
|
||||||
|
let rResult =
|
||||||
|
Reducer.parse(sourceCode)->Belt.Result.flatMap(expr =>
|
||||||
|
reducerFn(expr, Bindings.emptyBindings, InternalExpressionValue.defaultEnvironment)
|
||||||
|
)
|
||||||
|
rResult->Belt.Result.flatMap(result =>
|
||||||
|
switch result {
|
||||||
|
| IEvArray(args) => TypeChecker.checkArguments(aTypeSourceCode, args, reducerFn)
|
||||||
|
| _ => Js.Exn.raiseError("Arguments has to be an array")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let myCheckArguments = (aTypeSourceCode: string, sourceCode: string): string =>
|
||||||
|
switch checkArgumentsSourceCode(aTypeSourceCode, sourceCode) {
|
||||||
|
| Ok(_) => "Ok"
|
||||||
|
| Error(error) => ErrorValue.errorToString(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
let myCheckArgumentsExpectEqual = (aTypeSourceCode, sourceCode, answer) =>
|
||||||
|
expect(myCheckArguments(aTypeSourceCode, sourceCode))->toEqual(answer)
|
||||||
|
|
||||||
|
let myCheckArgumentsTest = (test, aTypeSourceCode, sourceCode, answer) =>
|
||||||
|
test(aTypeSourceCode, () => myCheckArgumentsExpectEqual(aTypeSourceCode, sourceCode, answer))
|
||||||
|
|
||||||
|
myCheckArgumentsTest(test, "number=>number=>number", "[1,2]", "Ok")
|
|
@ -0,0 +1,70 @@
|
||||||
|
module Expression = Reducer_Expression
|
||||||
|
module ExpressionT = Reducer_Expression_T
|
||||||
|
module ErrorValue = Reducer_ErrorValue
|
||||||
|
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
|
||||||
|
module Bindings = Reducer_Bindings
|
||||||
|
module T = Reducer_Type_T
|
||||||
|
module TypeChecker = Reducer_Type_TypeChecker
|
||||||
|
|
||||||
|
open Jest
|
||||||
|
open Expect
|
||||||
|
|
||||||
|
// In development, you are expected to use TypeChecker.isTypeOf(aTypeSourceCode, result, reducerFn).
|
||||||
|
// isTypeOfSourceCode is written to use strings instead of expression values.
|
||||||
|
|
||||||
|
let isTypeOfSourceCode = (aTypeSourceCode: string, sourceCode: string): result<
|
||||||
|
'v,
|
||||||
|
ErrorValue.t,
|
||||||
|
> => {
|
||||||
|
let reducerFn = Expression.reduceExpression
|
||||||
|
let rResult =
|
||||||
|
Reducer.parse(sourceCode)->Belt.Result.flatMap(expr =>
|
||||||
|
reducerFn(expr, Bindings.emptyBindings, InternalExpressionValue.defaultEnvironment)
|
||||||
|
)
|
||||||
|
rResult->Belt.Result.flatMap(result => TypeChecker.isTypeOf(aTypeSourceCode, result, reducerFn))
|
||||||
|
}
|
||||||
|
|
||||||
|
let myTypeCheck = (aTypeSourceCode: string, sourceCode: string): string =>
|
||||||
|
switch isTypeOfSourceCode(aTypeSourceCode, sourceCode) {
|
||||||
|
| Ok(_) => "Ok"
|
||||||
|
| Error(error) => ErrorValue.errorToString(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
let myTypeCheckExpectEqual = (aTypeSourceCode, sourceCode, answer) =>
|
||||||
|
expect(myTypeCheck(aTypeSourceCode, sourceCode))->toEqual(answer)
|
||||||
|
|
||||||
|
let myTypeCheckTest = (test, aTypeSourceCode, sourceCode, answer) =>
|
||||||
|
test(aTypeSourceCode, () => myTypeCheckExpectEqual(aTypeSourceCode, sourceCode, answer))
|
||||||
|
|
||||||
|
myTypeCheckTest(test, "number", "1", "Ok")
|
||||||
|
myTypeCheckTest(test, "number", "'2'", "Expected type: number but got: '2'")
|
||||||
|
myTypeCheckTest(test, "string", "3", "Expected type: string but got: 3")
|
||||||
|
myTypeCheckTest(test, "string", "'a'", "Ok")
|
||||||
|
myTypeCheckTest(test, "[number]", "[1,2,3]", "Ok")
|
||||||
|
myTypeCheckTest(test, "[number]", "['a','a','a']", "Expected type: number but got: 'a'")
|
||||||
|
myTypeCheckTest(test, "[number]", "[1,'a',3]", "Expected type: number but got: 'a'")
|
||||||
|
myTypeCheckTest(test, "[number, string]", "[1,'a']", "Ok")
|
||||||
|
myTypeCheckTest(test, "[number, string]", "[1, 2]", "Expected type: string but got: 2")
|
||||||
|
myTypeCheckTest(
|
||||||
|
test,
|
||||||
|
"[number, string, string]",
|
||||||
|
"[1,'a']",
|
||||||
|
"Expected type: [number, string, string] but got: [1,'a']",
|
||||||
|
)
|
||||||
|
myTypeCheckTest(
|
||||||
|
test,
|
||||||
|
"[number, string]",
|
||||||
|
"[1,'a', 3]",
|
||||||
|
"Expected type: [number, string] but got: [1,'a',3]",
|
||||||
|
)
|
||||||
|
myTypeCheckTest(test, "{age: number, name: string}", "{age: 1, name: 'a'}", "Ok")
|
||||||
|
myTypeCheckTest(
|
||||||
|
test,
|
||||||
|
"{age: number, name: string}",
|
||||||
|
"{age: 1, name: 'a', job: 'IT'}",
|
||||||
|
"Expected type: {age: number, name: string} but got: {age: 1,job: 'IT',name: 'a'}",
|
||||||
|
)
|
||||||
|
myTypeCheckTest(test, "number | string", "1", "Ok")
|
||||||
|
myTypeCheckTest(test, "date | string", "1", "Expected type: (date | string) but got: 1")
|
||||||
|
myTypeCheckTest(test, "number<-min(10)", "10", "Ok")
|
||||||
|
myTypeCheckTest(test, "number<-min(10)", "0", "Expected type: number<-min(10) but got: 0")
|
|
@ -1,11 +1,14 @@
|
||||||
// TODO: Reimplement with usual parse
|
|
||||||
open Jest
|
open Jest
|
||||||
open Reducer_TestHelpers
|
open Reducer_TestHelpers
|
||||||
|
|
||||||
describe("Eval with Bindings", () => {
|
describe("Eval with Bindings", () => {
|
||||||
testEvalBindingsToBe("x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(1)")
|
testEvalBindingsToBe("x", list{("x", ExternalExpressionValue.EvNumber(1.))}, "Ok(1)")
|
||||||
testEvalBindingsToBe("x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
|
testEvalBindingsToBe("x+1", list{("x", ExternalExpressionValue.EvNumber(1.))}, "Ok(2)")
|
||||||
testParseToBe("y = x+1; y", "Ok({(:$_let_$ :y {(:add :x 1)}); :y})")
|
testParseToBe("y = x+1; y", "Ok({(:$_let_$ :y {(:add :x 1)}); :y})")
|
||||||
testEvalBindingsToBe("y = x+1; y", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
|
testEvalBindingsToBe("y = x+1; y", list{("x", ExternalExpressionValue.EvNumber(1.))}, "Ok(2)")
|
||||||
testEvalBindingsToBe("y = x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1,y: 2})")
|
testEvalBindingsToBe(
|
||||||
|
"y = x+1",
|
||||||
|
list{("x", ExternalExpressionValue.EvNumber(1.))},
|
||||||
|
"Ok(@{x: 1,y: 2})",
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -39,15 +39,15 @@ describe("symbol not defined", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("call and bindings", () => {
|
describe("call and bindings", () => {
|
||||||
testEvalToBe("f(x)=x+1", "Ok({f: lambda(x=>internal code)})")
|
testEvalToBe("f(x)=x+1", "Ok(@{f: lambda(x=>internal code)})")
|
||||||
testEvalToBe("f(x)=x+1; f(1)", "Ok(2)")
|
testEvalToBe("f(x)=x+1; f(1)", "Ok(2)")
|
||||||
testEvalToBe("f=1;y=2", "Ok({f: 1,y: 2})")
|
testEvalToBe("f=1;y=2", "Ok(@{f: 1,y: 2})")
|
||||||
testEvalToBe("f(x)=x+1; y=f(1)", "Ok({f: lambda(x=>internal code),y: 2})")
|
testEvalToBe("f(x)=x+1; y=f(1)", "Ok(@{f: lambda(x=>internal code),y: 2})")
|
||||||
testEvalToBe("f(x)=x+1; y=f(1); f(1)", "Ok(2)")
|
testEvalToBe("f(x)=x+1; y=f(1); f(1)", "Ok(2)")
|
||||||
testEvalToBe("f(x)=x+1; y=f(1); z=f(1)", "Ok({f: lambda(x=>internal code),y: 2,z: 2})")
|
testEvalToBe("f(x)=x+1; y=f(1); z=f(1)", "Ok(@{f: lambda(x=>internal code),y: 2,z: 2})")
|
||||||
testEvalToBe(
|
testEvalToBe(
|
||||||
"f(x)=x+1; g(x)=f(x)+1",
|
"f(x)=x+1; g(x)=f(x)+1",
|
||||||
"Ok({f: lambda(x=>internal code),g: lambda(x=>internal code)})",
|
"Ok(@{f: lambda(x=>internal code),g: lambda(x=>internal code)})",
|
||||||
)
|
)
|
||||||
testParseToBe(
|
testParseToBe(
|
||||||
"f=99; g(x)=f; g(2)",
|
"f=99; g(x)=f; g(2)",
|
||||||
|
@ -57,7 +57,7 @@ describe("call and bindings", () => {
|
||||||
testEvalToBe("f(x)=x; g(x)=f(x); g(2)", "Ok(2)")
|
testEvalToBe("f(x)=x; g(x)=f(x); g(2)", "Ok(2)")
|
||||||
testEvalToBe(
|
testEvalToBe(
|
||||||
"f(x)=x+1; g(x)=f(x)+1; y=g(2)",
|
"f(x)=x+1; g(x)=f(x)+1; y=g(2)",
|
||||||
"Ok({f: lambda(x=>internal code),g: lambda(x=>internal code),y: 4})",
|
"Ok(@{f: lambda(x=>internal code),g: lambda(x=>internal code),y: 4})",
|
||||||
)
|
)
|
||||||
testEvalToBe("f(x)=x+1; g(x)=f(x)+1; g(2)", "Ok(4)")
|
testEvalToBe("f(x)=x+1; g(x)=f(x)+1; g(2)", "Ok(4)")
|
||||||
})
|
})
|
||||||
|
@ -65,7 +65,7 @@ describe("call and bindings", () => {
|
||||||
describe("function tricks", () => {
|
describe("function tricks", () => {
|
||||||
testEvalError("f(x)=f(y)=2; f(2)") //Error because chain assignment is not allowed
|
testEvalError("f(x)=f(y)=2; f(2)") //Error because chain assignment is not allowed
|
||||||
testEvalToBe("y=2;g(x)=y+1;g(2)", "Ok(3)")
|
testEvalToBe("y=2;g(x)=y+1;g(2)", "Ok(3)")
|
||||||
testEvalToBe("y=2;g(x)=inspect(y)+1", "Ok({g: lambda(x=>internal code),y: 2})")
|
testEvalToBe("y=2;g(x)=inspect(y)+1", "Ok(@{g: lambda(x=>internal code),y: 2})")
|
||||||
MySkip.testEvalToBe("f(x) = x(x); f(f)", "????") // TODO: Infinite loop. Any solution? Catching proper exception or timeout?
|
MySkip.testEvalToBe("f(x) = x(x); f(f)", "????") // TODO: Infinite loop. Any solution? Catching proper exception or timeout?
|
||||||
MySkip.testEvalToBe("f(x, x)=x+x; f(1,2)", "????") // TODO: Duplicate parameters
|
MySkip.testEvalToBe("f(x, x)=x+x; f(1,2)", "????") // TODO: Duplicate parameters
|
||||||
testEvalToBe("myadd(x,y)=x+y; z=myadd; z", "Ok(lambda(x,y=>internal code))")
|
testEvalToBe("myadd(x,y)=x+y; z=myadd; z", "Ok(lambda(x,y=>internal code))")
|
||||||
|
@ -75,7 +75,7 @@ describe("function tricks", () => {
|
||||||
describe("lambda in structures", () => {
|
describe("lambda in structures", () => {
|
||||||
testEvalToBe(
|
testEvalToBe(
|
||||||
"myadd(x,y)=x+y; z=[myadd]",
|
"myadd(x,y)=x+y; z=[myadd]",
|
||||||
"Ok({myadd: lambda(x,y=>internal code),z: [lambda(x,y=>internal code)]})",
|
"Ok(@{myadd: lambda(x,y=>internal code),z: [lambda(x,y=>internal code)]})",
|
||||||
)
|
)
|
||||||
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0]", "Ok(lambda(x,y=>internal code))")
|
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0]", "Ok(lambda(x,y=>internal code))")
|
||||||
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0](3,2)", "Ok(5)")
|
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0](3,2)", "Ok(5)")
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
open Jest
|
open Jest
|
||||||
open Reducer_TestHelpers
|
open Reducer_TestHelpers
|
||||||
|
|
||||||
describe("map reduce", () => {
|
|
||||||
testEvalToBe("double(x)=2*x; arr=[1,2,3]; map(arr, double)", "Ok([2,4,6])")
|
|
||||||
testEvalToBe("myadd(acc,x)=acc+x; arr=[1,2,3]; reduce(arr, 0, myadd)", "Ok(6)")
|
|
||||||
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; reduce(arr, 0, change)", "Ok(15)")
|
|
||||||
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; reduceReverse(arr, 0, change)", "Ok(9)")
|
|
||||||
testEvalToBe("arr=[1,2,3]; reverse(arr)", "Ok([3,2,1])")
|
|
||||||
testEvalToBe("check(x)=(x==2);arr=[1,2,3]; filter(arr,check)", "Ok([2])")
|
|
||||||
})
|
|
||||||
|
|
||||||
Skip.describe("map reduce (sam)", () => {
|
Skip.describe("map reduce (sam)", () => {
|
||||||
testEvalToBe("addone(x)=x+1; map(2, addone)", "Error???")
|
testEvalToBe("addone(x)=x+1; map(2, addone)", "Error???")
|
||||||
testEvalToBe("addone(x)=x+1; map(2, {x: addone})", "Error???")
|
testEvalToBe("addone(x)=x+1; map(2, {x: addone})", "Error???")
|
||||||
|
|
|
@ -10,5 +10,5 @@ describe("Evaluate ternary operator", () => {
|
||||||
testEvalToBe("false ? 'YES' : 'NO'", "Ok('NO')")
|
testEvalToBe("false ? 'YES' : 'NO'", "Ok('NO')")
|
||||||
testEvalToBe("2 > 1 ? 'YES' : 'NO'", "Ok('YES')")
|
testEvalToBe("2 > 1 ? 'YES' : 'NO'", "Ok('YES')")
|
||||||
testEvalToBe("2 <= 1 ? 'YES' : 'NO'", "Ok('NO')")
|
testEvalToBe("2 <= 1 ? 'YES' : 'NO'", "Ok('NO')")
|
||||||
testEvalToBe("1+1 ? 'YES' : 'NO'", "Error(Expected type: Boolean)")
|
testEvalToBe("1+1 ? 'YES' : 'NO'", "Error(Expected type: Boolean but got: )")
|
||||||
})
|
})
|
||||||
|
|
|
@ -39,7 +39,7 @@ describe("eval", () => {
|
||||||
testEvalToBe("x=1; y=x+1; y+1", "Ok(3)")
|
testEvalToBe("x=1; y=x+1; y+1", "Ok(3)")
|
||||||
testEvalError("1; x=1")
|
testEvalError("1; x=1")
|
||||||
testEvalError("1; 1")
|
testEvalError("1; 1")
|
||||||
testEvalToBe("x=1; x=1", "Ok({x: 1})")
|
testEvalToBe("x=1; x=1", "Ok(@{x: 1})")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -41,12 +41,6 @@ describe("eval on distribution functions", () => {
|
||||||
describe("normalize", () => {
|
describe("normalize", () => {
|
||||||
testEval("normalize(normal(5,2))", "Ok(Normal(5,2))")
|
testEval("normalize(normal(5,2))", "Ok(Normal(5,2))")
|
||||||
})
|
})
|
||||||
describe("toPointSet", () => {
|
|
||||||
testEval("toPointSet(normal(5,2))", "Ok(Point Set Distribution)")
|
|
||||||
})
|
|
||||||
describe("toSampleSet", () => {
|
|
||||||
testEval("toSampleSet(normal(5,2), 100)", "Ok(Sample Set Distribution)")
|
|
||||||
})
|
|
||||||
describe("add", () => {
|
describe("add", () => {
|
||||||
testEval("add(normal(5,2), normal(10,2))", "Ok(Normal(15,2.8284271247461903))")
|
testEval("add(normal(5,2), normal(10,2))", "Ok(Normal(15,2.8284271247461903))")
|
||||||
testEval("add(normal(5,2), lognormal(10,2))", "Ok(Sample Set Distribution)")
|
testEval("add(normal(5,2), lognormal(10,2))", "Ok(Sample Set Distribution)")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
open ReducerInterface.ExpressionValue
|
open ReducerInterface.ExternalExpressionValue
|
||||||
open Jest
|
open Jest
|
||||||
open Expect
|
open Expect
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
open Jest
|
||||||
|
open Expect
|
||||||
|
open Reducer_TestHelpers
|
||||||
|
|
||||||
|
let expectEvalToBeOk = (expr: string) =>
|
||||||
|
Reducer.evaluate(expr)->Reducer_Helpers.rRemoveDefaultsExternal->E.R.isOk->expect->toBe(true)
|
||||||
|
|
||||||
|
let registry = FunctionRegistry_Library.registry
|
||||||
|
let examples = E.A.to_list(FunctionRegistry_Core.Registry.allExamples(registry))
|
||||||
|
|
||||||
|
describe("FunctionRegistry Library", () => {
|
||||||
|
describe("Regular tests", () => {
|
||||||
|
testEvalToBe("List.make(3, 'HI')", "Ok(['HI','HI','HI'])")
|
||||||
|
testEvalToBe("make(3, 'HI')", "Error(Function not found: make(Number,String))")
|
||||||
|
testEvalToBe("List.upTo(1,3)", "Ok([1,2,3])")
|
||||||
|
testEvalToBe("List.first([3,5,8])", "Ok(3)")
|
||||||
|
testEvalToBe("List.last([3,5,8])", "Ok(8)")
|
||||||
|
testEvalToBe("List.reverse([3,5,8])", "Ok([8,5,3])")
|
||||||
|
testEvalToBe("double(x)=2*x; arr=[1,2,3]; List.map(arr, double)", "Ok([2,4,6])")
|
||||||
|
testEvalToBe("double(x)=2*x; arr=[1,2,3]; map(arr, double)", "Ok([2,4,6])")
|
||||||
|
testEvalToBe("myadd(acc,x)=acc+x; arr=[1,2,3]; List.reduce(arr, 0, myadd)", "Ok(6)")
|
||||||
|
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; List.reduce(arr, 0, change)", "Ok(15)")
|
||||||
|
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; List.reduceReverse(arr, 0, change)", "Ok(9)")
|
||||||
|
testEvalToBe("check(x)=(x==2);arr=[1,2,3]; List.filter(arr,check)", "Ok([2])")
|
||||||
|
testEvalToBe("arr=[1,2,3]; List.reverse(arr)", "Ok([3,2,1])")
|
||||||
|
testEvalToBe("Dist.normal(5,2)", "Ok(Normal(5,2))")
|
||||||
|
testEvalToBe("normal(5,2)", "Ok(Normal(5,2))")
|
||||||
|
testEvalToBe("normal({mean:5,stdev:2})", "Ok(Normal(5,2))")
|
||||||
|
testEvalToBe("-2 to 4", "Ok(Normal(1,1.8238704957353074))")
|
||||||
|
testEvalToBe("pointMass(5)", "Ok(PointMass(5))")
|
||||||
|
testEvalToBe("Number.floor(5.5)", "Ok(5)")
|
||||||
|
testEvalToBe("Number.ceil(5.5)", "Ok(6)")
|
||||||
|
testEvalToBe("floor(5.5)", "Ok(5)")
|
||||||
|
testEvalToBe("ceil(5.5)", "Ok(6)")
|
||||||
|
testEvalToBe("Number.abs(5.5)", "Ok(5.5)")
|
||||||
|
testEvalToBe("abs(5.5)", "Ok(5.5)")
|
||||||
|
testEvalToBe("Number.exp(10)", "Ok(22026.465794806718)")
|
||||||
|
testEvalToBe("Number.log10(10)", "Ok(1)")
|
||||||
|
testEvalToBe("Number.log2(10)", "Ok(3.321928094887362)")
|
||||||
|
testEvalToBe("Number.sum([2,5,3])", "Ok(10)")
|
||||||
|
testEvalToBe("sum([2,5,3])", "Ok(10)")
|
||||||
|
testEvalToBe("Number.product([2,5,3])", "Ok(30)")
|
||||||
|
testEvalToBe("Number.min([2,5,3])", "Ok(2)")
|
||||||
|
testEvalToBe("Number.max([2,5,3])", "Ok(5)")
|
||||||
|
testEvalToBe("Number.mean([0,5,10])", "Ok(5)")
|
||||||
|
testEvalToBe("Number.geomean([1,5,18])", "Ok(4.481404746557164)")
|
||||||
|
testEvalToBe("Number.stdev([0,5,10,15])", "Ok(5.5901699437494745)")
|
||||||
|
testEvalToBe("Number.variance([0,5,10,15])", "Ok(31.25)")
|
||||||
|
testEvalToBe("Number.sort([10,0,15,5])", "Ok([0,5,10,15])")
|
||||||
|
testEvalToBe("Number.cumsum([1,5,3])", "Ok([1,6,9])")
|
||||||
|
testEvalToBe("Number.cumprod([1,5,3])", "Ok([1,5,15])")
|
||||||
|
testEvalToBe("Number.diff([1,5,3])", "Ok([4,-2])")
|
||||||
|
testEvalToBe(
|
||||||
|
"Dist.logScore({estimate: normal(5,2), answer: normal(5.2,1), prior: normal(5.5,3)})",
|
||||||
|
"Ok(-0.33591375663884876)",
|
||||||
|
)
|
||||||
|
testEvalToBe(
|
||||||
|
"Dist.logScore({estimate: normal(5,2), answer: normal(5.2,1)})",
|
||||||
|
"Ok(0.32244107041564646)",
|
||||||
|
)
|
||||||
|
testEvalToBe("Dist.logScore({estimate: normal(5,2), answer: 4.5})", "Ok(1.6433360626394853)")
|
||||||
|
testEvalToBe("Dist.klDivergence(normal(5,2), normal(5,1.5))", "Ok(0.06874342818671068)")
|
||||||
|
testEvalToBe("SampleSet.fromList([3,5,2,3,5,2,3,5,2,3,3,5])", "Ok(Sample Set Distribution)")
|
||||||
|
testEvalToBe("SampleSet.fromList([3,5,2,3,5,2,3,5,2,3,3,5])", "Ok(Sample Set Distribution)")
|
||||||
|
testEvalToBe("SampleSet.fromFn({|| sample(normal(5,2))})", "Ok(Sample Set Distribution)")
|
||||||
|
testEvalToBe(
|
||||||
|
"addOne(t)=t+1; SampleSet.toList(SampleSet.map(SampleSet.fromList([1,2,3,4,5,6]), addOne))",
|
||||||
|
"Ok([2,3,4,5,6,7])",
|
||||||
|
)
|
||||||
|
testEvalToBe(
|
||||||
|
"SampleSet.toList(SampleSet.mapN([SampleSet.fromList([1,2,3,4,5,6]), SampleSet.fromList([6, 5, 4, 3, 2, 1])], {|x| x[0] > x[1] ? x[0] : x[1]}))",
|
||||||
|
"Ok([6,5,4,4,5,6])",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Fn auto-testing", () => {
|
||||||
|
testAll("tests of validity", examples, r => {
|
||||||
|
expectEvalToBeOk(r)
|
||||||
|
})
|
||||||
|
|
||||||
|
testAll(
|
||||||
|
"tests of type",
|
||||||
|
E.A.to_list(
|
||||||
|
FunctionRegistry_Core.Registry.allExamplesWithFns(registry)->E.A2.filter(((fn, _)) =>
|
||||||
|
E.O.isSome(fn.output)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
((fn, example)) => {
|
||||||
|
let responseType =
|
||||||
|
example
|
||||||
|
->Reducer.evaluate
|
||||||
|
->E.R2.fmap(ReducerInterface_InternalExpressionValue.externalValueToValueType)
|
||||||
|
let expectedOutputType = fn.output |> E.O.toExn("")
|
||||||
|
expect(responseType)->toEqual(Ok(expectedOutputType))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -5,7 +5,7 @@ import { testRun } from "./TestHelpers";
|
||||||
describe("cumulative density function of a normal distribution", () => {
|
describe("cumulative density function of a normal distribution", () => {
|
||||||
test("at 3 stdevs to the right of the mean is near 1", () => {
|
test("at 3 stdevs to the right of the mean is near 1", () => {
|
||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(fc.float(), fc.float({ min: 1e-7 }), (mean, stdev) => {
|
fc.property(fc.integer(), fc.integer({ min: 1 }), (mean, stdev) => {
|
||||||
let threeStdevsAboveMean = mean + 3 * stdev;
|
let threeStdevsAboveMean = mean + 3 * stdev;
|
||||||
let squiggleString = `cdf(normal(${mean}, ${stdev}), ${threeStdevsAboveMean})`;
|
let squiggleString = `cdf(normal(${mean}, ${stdev}), ${threeStdevsAboveMean})`;
|
||||||
let squiggleResult = testRun(squiggleString);
|
let squiggleResult = testRun(squiggleString);
|
||||||
|
@ -16,7 +16,7 @@ describe("cumulative density function of a normal distribution", () => {
|
||||||
|
|
||||||
test("at 3 stdevs to the left of the mean is near 0", () => {
|
test("at 3 stdevs to the left of the mean is near 0", () => {
|
||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(fc.float(), fc.float({ min: 1e-7 }), (mean, stdev) => {
|
fc.property(fc.integer(), fc.integer({ min: 1 }), (mean, stdev) => {
|
||||||
let threeStdevsBelowMean = mean - 3 * stdev;
|
let threeStdevsBelowMean = mean - 3 * stdev;
|
||||||
let squiggleString = `cdf(normal(${mean}, ${stdev}), ${threeStdevsBelowMean})`;
|
let squiggleString = `cdf(normal(${mean}, ${stdev}), ${threeStdevsBelowMean})`;
|
||||||
let squiggleResult = testRun(squiggleString);
|
let squiggleResult = testRun(squiggleString);
|
||||||
|
|
|
@ -4,13 +4,16 @@ import * as fc from "fast-check";
|
||||||
|
|
||||||
// Beware: float64Array makes it appear in an infinite loop.
|
// Beware: float64Array makes it appear in an infinite loop.
|
||||||
let arrayGen = () =>
|
let arrayGen = () =>
|
||||||
fc.float32Array({
|
fc
|
||||||
|
.float32Array({
|
||||||
minLength: 10,
|
minLength: 10,
|
||||||
maxLength: 10000,
|
maxLength: 10000,
|
||||||
noDefaultInfinity: true,
|
noDefaultInfinity: true,
|
||||||
noNaN: true,
|
noNaN: true,
|
||||||
});
|
})
|
||||||
|
.filter(
|
||||||
|
(xs_) => Math.min(...Array.from(xs_)) != Math.max(...Array.from(xs_))
|
||||||
|
);
|
||||||
describe("cumulative density function", () => {
|
describe("cumulative density function", () => {
|
||||||
let n = 10000;
|
let n = 10000;
|
||||||
|
|
||||||
|
@ -119,11 +122,7 @@ describe("cumulative density function", () => {
|
||||||
{ sampleCount: n, xyPointLength: 100 }
|
{ sampleCount: n, xyPointLength: 100 }
|
||||||
);
|
);
|
||||||
let cdfValue = dist.cdf(x).value;
|
let cdfValue = dist.cdf(x).value;
|
||||||
if (x < Math.min(...xs)) {
|
expect(cdfValue).toBeGreaterThanOrEqual(0);
|
||||||
expect(cdfValue).toEqual(0);
|
|
||||||
} else {
|
|
||||||
expect(cdfValue).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,15 +5,11 @@ import * as fc from "fast-check";
|
||||||
describe("Scalar manipulation is well-modeled by javascript math", () => {
|
describe("Scalar manipulation is well-modeled by javascript math", () => {
|
||||||
test("in the case of natural logarithms", () => {
|
test("in the case of natural logarithms", () => {
|
||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(fc.float(), (x) => {
|
fc.property(fc.integer(), (x) => {
|
||||||
let squiggleString = `log(${x})`;
|
let squiggleString = `log(${x})`;
|
||||||
let squiggleResult = testRun(squiggleString);
|
let squiggleResult = testRun(squiggleString);
|
||||||
if (x == 0) {
|
if (x == 0) {
|
||||||
expect(squiggleResult.value).toEqual(-Infinity);
|
expect(squiggleResult.value).toEqual(-Infinity);
|
||||||
} else if (x < 0) {
|
|
||||||
expect(squiggleResult.value).toEqual(
|
|
||||||
"somemessage (confused why a test case hasn't pointed out to me that this message is bogus)"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
expect(squiggleResult.value).toEqual(Math.log(x));
|
expect(squiggleResult.value).toEqual(Math.log(x));
|
||||||
}
|
}
|
||||||
|
@ -23,7 +19,7 @@ describe("Scalar manipulation is well-modeled by javascript math", () => {
|
||||||
|
|
||||||
test("in the case of addition (with assignment)", () => {
|
test("in the case of addition (with assignment)", () => {
|
||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(fc.float(), fc.float(), fc.float(), (x, y, z) => {
|
fc.property(fc.integer(), fc.integer(), fc.integer(), (x, y, z) => {
|
||||||
let squiggleString = `x = ${x}; y = ${y}; z = ${z}; x + y + z`;
|
let squiggleString = `x = ${x}; y = ${y}; z = ${z}; x + y + z`;
|
||||||
let squiggleResult = testRun(squiggleString);
|
let squiggleResult = testRun(squiggleString);
|
||||||
expect(squiggleResult.value).toBeCloseTo(x + y + z);
|
expect(squiggleResult.value).toBeCloseTo(x + y + z);
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { errorValueToString } from "../../src/js/index";
|
|
||||||
import { testRun } from "./TestHelpers";
|
import { testRun } from "./TestHelpers";
|
||||||
import * as fc from "fast-check";
|
import * as fc from "fast-check";
|
||||||
|
|
||||||
describe("Symbolic mean", () => {
|
describe("Symbolic mean", () => {
|
||||||
test("mean(triangular(x,y,z))", () => {
|
test("mean(triangular(x,y,z))", () => {
|
||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(fc.float(), fc.float(), fc.float(), (x, y, z) => {
|
fc.property(fc.integer(), fc.integer(), fc.integer(), (x, y, z) => {
|
||||||
if (!(x < y && y < z)) {
|
if (!(x < y && y < z)) {
|
||||||
try {
|
try {
|
||||||
let squiggleResult = testRun(`mean(triangular(${x},${y},${z}))`);
|
let squiggleResult = testRun(`mean(triangular(${x},${y},${z}))`);
|
||||||
|
|
|
@ -29,7 +29,7 @@ let {toFloat, toDist, toString, toError, fmap} = module(DistributionOperation.Ou
|
||||||
|
|
||||||
let fnImage = (theFn, inps) => Js.Array.map(theFn, inps)
|
let fnImage = (theFn, inps) => Js.Array.map(theFn, inps)
|
||||||
|
|
||||||
let env: DistributionOperation.env = {
|
let env: GenericDist.env = {
|
||||||
sampleCount: MagicNumbers.Environment.defaultSampleCount,
|
sampleCount: MagicNumbers.Environment.defaultSampleCount,
|
||||||
xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
|
xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"basic": false
|
"basic": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"external-stdlib": "@rescript/std",
|
||||||
"refmt": 3,
|
"refmt": 3,
|
||||||
"warnings": {
|
"warnings": {
|
||||||
"number": "+A-42-48-9-30-4"
|
"number": "+A-42-48-9-30-4"
|
||||||
|
|
|
@ -19,7 +19,7 @@ do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
files=`ls src/rescript/**/**/*.resi src/rescript/**/*.resi` # src/rescript/*/resi
|
files=`ls src/rescript/**/*.resi` # src/rescript/*/resi
|
||||||
for file in $files
|
for file in $files
|
||||||
do
|
do
|
||||||
current=`cat $file`
|
current=`cat $file`
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "@quri/squiggle-lang",
|
"name": "@quri/squiggle-lang",
|
||||||
"version": "0.2.9",
|
"version": "0.2.11",
|
||||||
"homepage": "https://squiggle-language.com",
|
"homepage": "https://squiggle-language.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"peggy": "peggy --cache",
|
"peggy": "peggy --cache",
|
||||||
|
"rescript": "rescript",
|
||||||
"build": "yarn build:peggy && yarn build:rescript && yarn build:typescript",
|
"build": "yarn build:peggy && yarn build:rescript && yarn build:typescript",
|
||||||
"build:peggy": "find . -type f -name *.peggy -exec yarn peggy {} \\;",
|
"build:peggy:helpers": "tsc --module commonjs --outDir src/rescript/Reducer/Reducer_Peggy/ src/rescript/Reducer/Reducer_Peggy/helpers.ts",
|
||||||
|
"build:peggy": "yarn build:peggy:helpers && find . -type f -name *.peggy -exec yarn peggy {} \\;",
|
||||||
"build:rescript": "rescript build -with-deps",
|
"build:rescript": "rescript build -with-deps",
|
||||||
"build:typescript": "tsc",
|
"build:typescript": "tsc",
|
||||||
"bundle": "webpack",
|
"bundle": "webpack",
|
||||||
|
@ -18,6 +20,7 @@
|
||||||
"test:ts": "jest __tests__/TS/",
|
"test:ts": "jest __tests__/TS/",
|
||||||
"test:rescript": "jest --modulePathIgnorePatterns=__tests__/TS/*",
|
"test:rescript": "jest --modulePathIgnorePatterns=__tests__/TS/*",
|
||||||
"test:watch": "jest --watchAll",
|
"test:watch": "jest --watchAll",
|
||||||
|
"test:fnRegistry": "jest __tests__/SquiggleLibrary/SquiggleLibrary_FunctionRegistryLibrary_test.bs.js",
|
||||||
"coverage:rescript": "rm -f *.coverage && yarn clean && BISECT_ENABLE=yes yarn build && yarn test:rescript && bisect-ppx-report html",
|
"coverage:rescript": "rm -f *.coverage && yarn clean && BISECT_ENABLE=yes yarn build && yarn test:rescript && bisect-ppx-report html",
|
||||||
"coverage:ts": "yarn clean && yarn build && nyc --reporter=lcov yarn test:ts",
|
"coverage:ts": "yarn clean && yarn build && nyc --reporter=lcov yarn test:ts",
|
||||||
"coverage:rescript:ci": "yarn clean && BISECT_ENABLE=yes yarn build:rescript && yarn test:rescript && bisect-ppx-report send-to Codecov",
|
"coverage:rescript:ci": "yarn clean && BISECT_ENABLE=yes yarn build:rescript && yarn test:rescript && bisect-ppx-report send-to Codecov",
|
||||||
|
@ -29,6 +32,7 @@
|
||||||
"format:prettier": "prettier --write .",
|
"format:prettier": "prettier --write .",
|
||||||
"format": "yarn format:rescript && yarn format:prettier",
|
"format": "yarn format:rescript && yarn format:prettier",
|
||||||
"prepack": "yarn build && yarn test && yarn bundle",
|
"prepack": "yarn build && yarn test && yarn bundle",
|
||||||
|
"all:rescript": "yarn build:rescript && yarn test:rescript && yarn format:rescript",
|
||||||
"all": "yarn build && yarn bundle && yarn test"
|
"all": "yarn build && yarn bundle && yarn test"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -36,11 +40,12 @@
|
||||||
],
|
],
|
||||||
"author": "Quantified Uncertainty Research Institute",
|
"author": "Quantified Uncertainty Research Institute",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@rescript/std": "^9.1.4",
|
||||||
"@stdlib/stats": "^0.0.13",
|
"@stdlib/stats": "^0.0.13",
|
||||||
"jstat": "^1.9.5",
|
"jstat": "^1.9.5",
|
||||||
"mathjs": "^10.6.0",
|
"lodash": "^4.17.21",
|
||||||
"pdfast": "^0.2.0",
|
"mathjs": "^11.0.1",
|
||||||
"rescript": "^9.1.4"
|
"pdfast": "^0.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@glennsl/rescript-jest": "^0.9.0",
|
"@glennsl/rescript-jest": "^0.9.0",
|
||||||
|
@ -50,20 +55,21 @@
|
||||||
"bisect_ppx": "^2.7.1",
|
"bisect_ppx": "^2.7.1",
|
||||||
"chalk": "^5.0.1",
|
"chalk": "^5.0.1",
|
||||||
"codecov": "^3.8.3",
|
"codecov": "^3.8.3",
|
||||||
"fast-check": "^2.25.0",
|
"fast-check": "^3.1.0",
|
||||||
"gentype": "^4.4.0",
|
"gentype": "^4.5.0",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"moduleserve": "^0.9.1",
|
"moduleserve": "^0.9.1",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"peggy": "^2.0.1",
|
"peggy": "^2.0.1",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
"reanalyze": "^2.23.0",
|
"reanalyze": "^2.23.0",
|
||||||
|
"rescript": "^9.1.4",
|
||||||
"rescript-fast-check": "^1.1.1",
|
"rescript-fast-check": "^1.1.1",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^27.1.4",
|
||||||
"ts-loader": "^9.3.0",
|
"ts-loader": "^9.3.0",
|
||||||
"ts-node": "^10.8.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.7.3",
|
"typescript": "^4.7.4",
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^4.10.0"
|
"webpack-cli": "^4.10.0"
|
||||||
},
|
},
|
||||||
"source": "./src/js/index.ts",
|
"source": "./src/js/index.ts",
|
||||||
|
|
|
@ -37,6 +37,8 @@ import { Distribution, shape } from "./distribution";
|
||||||
export { Distribution, resultMap, defaultEnvironment };
|
export { Distribution, resultMap, defaultEnvironment };
|
||||||
export type { result, shape, environment, lambdaValue, squiggleExpression };
|
export type { result, shape, environment, lambdaValue, squiggleExpression };
|
||||||
|
|
||||||
|
export { parse } from "./parse";
|
||||||
|
|
||||||
export let defaultSamplingInputs: environment = {
|
export let defaultSamplingInputs: environment = {
|
||||||
sampleCount: 10000,
|
sampleCount: 10000,
|
||||||
xyPointLength: 10000,
|
xyPointLength: 10000,
|
||||||
|
@ -118,6 +120,10 @@ function createTsExport(
|
||||||
x: expressionValue,
|
x: expressionValue,
|
||||||
environment: environment
|
environment: environment
|
||||||
): squiggleExpression {
|
): squiggleExpression {
|
||||||
|
switch (x) {
|
||||||
|
case "EvVoid":
|
||||||
|
return tag("void", "");
|
||||||
|
default: {
|
||||||
switch (x.tag) {
|
switch (x.tag) {
|
||||||
case "EvArray":
|
case "EvArray":
|
||||||
// genType doesn't convert anything more than 2 layers down into {tag: x, value: x}
|
// genType doesn't convert anything more than 2 layers down into {tag: x, value: x}
|
||||||
|
@ -129,31 +135,14 @@ function createTsExport(
|
||||||
// case
|
// case
|
||||||
return tag(
|
return tag(
|
||||||
"array",
|
"array",
|
||||||
x.value.map((arrayItem): squiggleExpression => {
|
x.value.map(
|
||||||
switch (arrayItem.tag) {
|
(arrayItem): squiggleExpression =>
|
||||||
case "EvRecord":
|
|
||||||
return tag(
|
|
||||||
"record",
|
|
||||||
_.mapValues(arrayItem.value, (recordValue: unknown) =>
|
|
||||||
convertRawToTypescript(
|
convertRawToTypescript(
|
||||||
recordValue as rescriptExport,
|
arrayItem as unknown as rescriptExport,
|
||||||
environment
|
environment
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
case "EvArray":
|
|
||||||
let y = arrayItem.value as unknown as rescriptExport[];
|
|
||||||
return tag(
|
|
||||||
"array",
|
|
||||||
y.map((childArrayItem) =>
|
|
||||||
convertRawToTypescript(childArrayItem, environment)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return createTsExport(arrayItem, environment);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
case "EvArrayString":
|
case "EvArrayString":
|
||||||
return tag("arraystring", x.value);
|
return tag("arraystring", x.value);
|
||||||
case "EvBool":
|
case "EvBool":
|
||||||
|
@ -168,7 +157,8 @@ function createTsExport(
|
||||||
return tag("number", x.value);
|
return tag("number", x.value);
|
||||||
case "EvRecord":
|
case "EvRecord":
|
||||||
// genType doesn't support records, so we have to do the raw conversion ourself
|
// genType doesn't support records, so we have to do the raw conversion ourself
|
||||||
let result: tagged<"record", { [key: string]: squiggleExpression }> = tag(
|
let result: tagged<"record", { [key: string]: squiggleExpression }> =
|
||||||
|
tag(
|
||||||
"record",
|
"record",
|
||||||
_.mapValues(x.value, (x: unknown) =>
|
_.mapValues(x.value, (x: unknown) =>
|
||||||
convertRawToTypescript(x as rescriptExport, environment)
|
convertRawToTypescript(x as rescriptExport, environment)
|
||||||
|
@ -187,6 +177,17 @@ function createTsExport(
|
||||||
return tag("lambdaDeclaration", x.value);
|
return tag("lambdaDeclaration", x.value);
|
||||||
case "EvTypeIdentifier":
|
case "EvTypeIdentifier":
|
||||||
return tag("typeIdentifier", x.value);
|
return tag("typeIdentifier", x.value);
|
||||||
|
case "EvType":
|
||||||
|
let typeResult: tagged<
|
||||||
|
"type",
|
||||||
|
{ [key: string]: squiggleExpression }
|
||||||
|
> = tag(
|
||||||
|
"type",
|
||||||
|
_.mapValues(x.value, (x: unknown) =>
|
||||||
|
convertRawToTypescript(x as rescriptExport, environment)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return typeResult;
|
||||||
case "EvModule":
|
case "EvModule":
|
||||||
let moduleResult: tagged<
|
let moduleResult: tagged<
|
||||||
"module",
|
"module",
|
||||||
|
@ -200,3 +201,5 @@ function createTsExport(
|
||||||
return moduleResult;
|
return moduleResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
23
packages/squiggle-lang/src/js/parse.ts
Normal file
23
packages/squiggle-lang/src/js/parse.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import {
|
||||||
|
errorValue,
|
||||||
|
parse as parseRescript,
|
||||||
|
} from "../rescript/TypescriptInterface.gen";
|
||||||
|
import { result } from "./types";
|
||||||
|
import { AnyPeggyNode } from "../rescript/Reducer/Reducer_Peggy/helpers";
|
||||||
|
|
||||||
|
export function parse(
|
||||||
|
squiggleString: string
|
||||||
|
): result<AnyPeggyNode, Extract<errorValue, { tag: "RESyntaxError" }>> {
|
||||||
|
const maybeExpression = parseRescript(squiggleString);
|
||||||
|
if (maybeExpression.tag === "Ok") {
|
||||||
|
return { tag: "Ok", value: maybeExpression.value as AnyPeggyNode };
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
typeof maybeExpression.value !== "object" ||
|
||||||
|
maybeExpression.value.tag !== "RESyntaxError"
|
||||||
|
) {
|
||||||
|
throw new Error("Expected syntax error");
|
||||||
|
}
|
||||||
|
return { tag: "Error", value: maybeExpression.value };
|
||||||
|
}
|
||||||
|
}
|
|
@ -129,8 +129,10 @@ export type squiggleExpression =
|
||||||
| tagged<"timeDuration", number>
|
| tagged<"timeDuration", number>
|
||||||
| tagged<"lambdaDeclaration", lambdaDeclaration>
|
| tagged<"lambdaDeclaration", lambdaDeclaration>
|
||||||
| tagged<"record", { [key: string]: squiggleExpression }>
|
| tagged<"record", { [key: string]: squiggleExpression }>
|
||||||
|
| tagged<"type", { [key: string]: squiggleExpression }>
|
||||||
| tagged<"typeIdentifier", string>
|
| tagged<"typeIdentifier", string>
|
||||||
| tagged<"module", { [key: string]: squiggleExpression }>;
|
| tagged<"module", { [key: string]: squiggleExpression }>
|
||||||
|
| tagged<"void", string>;
|
||||||
|
|
||||||
export { lambdaValue };
|
export { lambdaValue };
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,9 @@ type error = DistributionTypes.error
|
||||||
|
|
||||||
// TODO: It could be great to use a cache for some calculations (basically, do memoization). Also, better analytics/tracking could go a long way.
|
// TODO: It could be great to use a cache for some calculations (basically, do memoization). Also, better analytics/tracking could go a long way.
|
||||||
|
|
||||||
type env = {
|
type env = GenericDist.env
|
||||||
sampleCount: int,
|
|
||||||
xyPointLength: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
let defaultEnv = {
|
let defaultEnv: env = {
|
||||||
sampleCount: MagicNumbers.Environment.defaultSampleCount,
|
sampleCount: MagicNumbers.Environment.defaultSampleCount,
|
||||||
xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
|
xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
|
||||||
}
|
}
|
||||||
|
@ -93,7 +90,7 @@ module OutputLocal = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
let rec run = (~env: env, functionCallInfo: functionCallInfo): outputType => {
|
||||||
let {sampleCount, xyPointLength} = env
|
let {sampleCount, xyPointLength} = env
|
||||||
|
|
||||||
let reCall = (~env=env, ~functionCallInfo=functionCallInfo, ()) => {
|
let reCall = (~env=env, ~functionCallInfo=functionCallInfo, ()) => {
|
||||||
|
@ -101,14 +98,14 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let toPointSetFn = r => {
|
let toPointSetFn = r => {
|
||||||
switch reCall(~functionCallInfo=FromDist(ToDist(ToPointSet), r), ()) {
|
switch reCall(~functionCallInfo=FromDist(#ToDist(ToPointSet), r), ()) {
|
||||||
| Dist(PointSet(p)) => Ok(p)
|
| Dist(PointSet(p)) => Ok(p)
|
||||||
| e => Error(OutputLocal.toErrorOrUnreachable(e))
|
| e => Error(OutputLocal.toErrorOrUnreachable(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let toSampleSetFn = r => {
|
let toSampleSetFn = r => {
|
||||||
switch reCall(~functionCallInfo=FromDist(ToDist(ToSampleSet(sampleCount)), r), ()) {
|
switch reCall(~functionCallInfo=FromDist(#ToDist(ToSampleSet(sampleCount)), r), ()) {
|
||||||
| Dist(SampleSet(p)) => Ok(p)
|
| Dist(SampleSet(p)) => Ok(p)
|
||||||
| e => Error(OutputLocal.toErrorOrUnreachable(e))
|
| e => Error(OutputLocal.toErrorOrUnreachable(e))
|
||||||
}
|
}
|
||||||
|
@ -116,13 +113,13 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
||||||
|
|
||||||
let scaleMultiply = (r, weight) =>
|
let scaleMultiply = (r, weight) =>
|
||||||
reCall(
|
reCall(
|
||||||
~functionCallInfo=FromDist(ToDistCombination(Pointwise, #Multiply, #Float(weight)), r),
|
~functionCallInfo=FromDist(#ToDistCombination(Pointwise, #Multiply, #Float(weight)), r),
|
||||||
(),
|
(),
|
||||||
)->OutputLocal.toDistR
|
)->OutputLocal.toDistR
|
||||||
|
|
||||||
let pointwiseAdd = (r1, r2) =>
|
let pointwiseAdd = (r1, r2) =>
|
||||||
reCall(
|
reCall(
|
||||||
~functionCallInfo=FromDist(ToDistCombination(Pointwise, #Add, #Dist(r2)), r1),
|
~functionCallInfo=FromDist(#ToDistCombination(Pointwise, #Add, #Dist(r2)), r1),
|
||||||
(),
|
(),
|
||||||
)->OutputLocal.toDistR
|
)->OutputLocal.toDistR
|
||||||
|
|
||||||
|
@ -131,49 +128,40 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
||||||
dist: genericDist,
|
dist: genericDist,
|
||||||
): outputType => {
|
): outputType => {
|
||||||
let response = switch subFnName {
|
let response = switch subFnName {
|
||||||
| ToFloat(distToFloatOperation) =>
|
| #ToFloat(distToFloatOperation) =>
|
||||||
GenericDist.toFloatOperation(dist, ~toPointSetFn, ~distToFloatOperation)
|
GenericDist.toFloatOperation(dist, ~toPointSetFn, ~distToFloatOperation)
|
||||||
->E.R2.fmap(r => Float(r))
|
->E.R2.fmap(r => Float(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToString(ToString) => dist->GenericDist.toString->String
|
| #ToString(ToString) => dist->GenericDist.toString->String
|
||||||
| ToString(ToSparkline(bucketCount)) =>
|
| #ToString(ToSparkline(bucketCount)) =>
|
||||||
GenericDist.toSparkline(dist, ~sampleCount, ~bucketCount, ())
|
GenericDist.toSparkline(dist, ~sampleCount, ~bucketCount, ())
|
||||||
->E.R2.fmap(r => String(r))
|
->E.R2.fmap(r => String(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDist(Inspect) => {
|
| #ToDist(Inspect) => {
|
||||||
Js.log2("Console log requested: ", dist)
|
Js.log2("Console log requested: ", dist)
|
||||||
Dist(dist)
|
Dist(dist)
|
||||||
}
|
}
|
||||||
| ToDist(Normalize) => dist->GenericDist.normalize->Dist
|
| #ToDist(Normalize) => dist->GenericDist.normalize->Dist
|
||||||
| ToScore(KLDivergence(t2)) =>
|
| #ToScore(LogScore(answer, prior)) =>
|
||||||
GenericDist.Score.klDivergence(dist, t2, ~toPointSetFn)
|
GenericDist.Score.logScore(~estimate=dist, ~answer, ~prior, ~env)
|
||||||
->E.R2.fmap(r => Float(r))
|
->E.R2.fmap(s => Float(s))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToScore(LogScore(answer, prior)) =>
|
| #ToBool(IsNormalized) => dist->GenericDist.isNormalized->Bool
|
||||||
GenericDist.Score.logScoreWithPointResolution(
|
| #ToDist(Truncate(leftCutoff, rightCutoff)) =>
|
||||||
~prediction=dist,
|
|
||||||
~answer,
|
|
||||||
~prior,
|
|
||||||
~toPointSetFn,
|
|
||||||
)
|
|
||||||
->E.R2.fmap(r => Float(r))
|
|
||||||
->OutputLocal.fromResult
|
|
||||||
| ToBool(IsNormalized) => dist->GenericDist.isNormalized->Bool
|
|
||||||
| ToDist(Truncate(leftCutoff, rightCutoff)) =>
|
|
||||||
GenericDist.truncate(~toPointSetFn, ~leftCutoff, ~rightCutoff, dist, ())
|
GenericDist.truncate(~toPointSetFn, ~leftCutoff, ~rightCutoff, dist, ())
|
||||||
->E.R2.fmap(r => Dist(r))
|
->E.R2.fmap(r => Dist(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDist(ToSampleSet(n)) =>
|
| #ToDist(ToSampleSet(n)) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.toSampleSetDist(n)
|
->GenericDist.toSampleSetDist(n)
|
||||||
->E.R2.fmap(r => Dist(SampleSet(r)))
|
->E.R2.fmap(r => Dist(SampleSet(r)))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDist(ToPointSet) =>
|
| #ToDist(ToPointSet) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.toPointSet(~xyPointLength, ~sampleCount, ())
|
->GenericDist.toPointSet(~xyPointLength, ~sampleCount, ())
|
||||||
->E.R2.fmap(r => Dist(PointSet(r)))
|
->E.R2.fmap(r => Dist(PointSet(r)))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDist(Scale(#LogarithmWithThreshold(eps), f)) =>
|
| #ToDist(Scale(#LogarithmWithThreshold(eps), f)) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.pointwiseCombinationFloat(
|
->GenericDist.pointwiseCombinationFloat(
|
||||||
~toPointSetFn,
|
~toPointSetFn,
|
||||||
|
@ -182,18 +170,23 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
||||||
)
|
)
|
||||||
->E.R2.fmap(r => Dist(r))
|
->E.R2.fmap(r => Dist(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDist(Scale(#Logarithm, f)) =>
|
| #ToDist(Scale(#Multiply, f)) =>
|
||||||
|
dist
|
||||||
|
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Multiply, ~f)
|
||||||
|
->E.R2.fmap(r => Dist(r))
|
||||||
|
->OutputLocal.fromResult
|
||||||
|
| #ToDist(Scale(#Logarithm, f)) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Logarithm, ~f)
|
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Logarithm, ~f)
|
||||||
->E.R2.fmap(r => Dist(r))
|
->E.R2.fmap(r => Dist(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDist(Scale(#Power, f)) =>
|
| #ToDist(Scale(#Power, f)) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Power, ~f)
|
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Power, ~f)
|
||||||
->E.R2.fmap(r => Dist(r))
|
->E.R2.fmap(r => Dist(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDistCombination(Algebraic(_), _, #Float(_)) => GenDistError(NotYetImplemented)
|
| #ToDistCombination(Algebraic(_), _, #Float(_)) => GenDistError(NotYetImplemented)
|
||||||
| ToDistCombination(Algebraic(strategy), arithmeticOperation, #Dist(t2)) =>
|
| #ToDistCombination(Algebraic(strategy), arithmeticOperation, #Dist(t2)) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.algebraicCombination(
|
->GenericDist.algebraicCombination(
|
||||||
~strategy,
|
~strategy,
|
||||||
|
@ -204,12 +197,12 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
||||||
)
|
)
|
||||||
->E.R2.fmap(r => Dist(r))
|
->E.R2.fmap(r => Dist(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDistCombination(Pointwise, algebraicCombination, #Dist(t2)) =>
|
| #ToDistCombination(Pointwise, algebraicCombination, #Dist(t2)) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.pointwiseCombination(~toPointSetFn, ~algebraicCombination, ~t2)
|
->GenericDist.pointwiseCombination(~toPointSetFn, ~algebraicCombination, ~t2)
|
||||||
->E.R2.fmap(r => Dist(r))
|
->E.R2.fmap(r => Dist(r))
|
||||||
->OutputLocal.fromResult
|
->OutputLocal.fromResult
|
||||||
| ToDistCombination(Pointwise, algebraicCombination, #Float(f)) =>
|
| #ToDistCombination(Pointwise, algebraicCombination, #Float(f)) =>
|
||||||
dist
|
dist
|
||||||
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination, ~f)
|
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination, ~f)
|
||||||
->E.R2.fmap(r => Dist(r))
|
->E.R2.fmap(r => Dist(r))
|
||||||
|
@ -220,8 +213,7 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
||||||
|
|
||||||
switch functionCallInfo {
|
switch functionCallInfo {
|
||||||
| FromDist(subFnName, dist) => fromDistFn(subFnName, dist)
|
| FromDist(subFnName, dist) => fromDistFn(subFnName, dist)
|
||||||
| FromFloat(subFnName, float) =>
|
| FromFloat(subFnName, x) => reCall(~functionCallInfo=FromFloat(subFnName, x), ())
|
||||||
reCall(~functionCallInfo=FromDist(subFnName, GenericDist.fromFloat(float)), ())
|
|
||||||
| Mixture(dists) =>
|
| Mixture(dists) =>
|
||||||
dists
|
dists
|
||||||
->GenericDist.mixture(~scaleMultiplyFn=scaleMultiply, ~pointwiseAddFn=pointwiseAdd)
|
->GenericDist.mixture(~scaleMultiplyFn=scaleMultiply, ~pointwiseAddFn=pointwiseAdd)
|
||||||
|
@ -273,13 +265,16 @@ module Constructors = {
|
||||||
let pdf = (~env, dist, f) => C.pdf(dist, f)->run(~env)->toFloatR
|
let pdf = (~env, dist, f) => C.pdf(dist, f)->run(~env)->toFloatR
|
||||||
let normalize = (~env, dist) => C.normalize(dist)->run(~env)->toDistR
|
let normalize = (~env, dist) => C.normalize(dist)->run(~env)->toDistR
|
||||||
let isNormalized = (~env, dist) => C.isNormalized(dist)->run(~env)->toBoolR
|
let isNormalized = (~env, dist) => C.isNormalized(dist)->run(~env)->toBoolR
|
||||||
let klDivergence = (~env, dist1, dist2) => C.klDivergence(dist1, dist2)->run(~env)->toFloatR
|
module LogScore = {
|
||||||
let logScoreWithPointResolution = (
|
let distEstimateDistAnswer = (~env, estimate, answer) =>
|
||||||
~env,
|
C.LogScore.distEstimateDistAnswer(estimate, answer)->run(~env)->toFloatR
|
||||||
~prediction: DistributionTypes.genericDist,
|
let distEstimateDistAnswerWithPrior = (~env, estimate, answer, prior) =>
|
||||||
~answer: float,
|
C.LogScore.distEstimateDistAnswerWithPrior(estimate, answer, prior)->run(~env)->toFloatR
|
||||||
~prior: option<DistributionTypes.genericDist>,
|
let distEstimateScalarAnswer = (~env, estimate, answer) =>
|
||||||
) => C.logScoreWithPointResolution(~prediction, ~answer, ~prior)->run(~env)->toFloatR
|
C.LogScore.distEstimateScalarAnswer(estimate, answer)->run(~env)->toFloatR
|
||||||
|
let distEstimateScalarAnswerWithPrior = (~env, estimate, answer, prior) =>
|
||||||
|
C.LogScore.distEstimateScalarAnswerWithPrior(estimate, answer, prior)->run(~env)->toFloatR
|
||||||
|
}
|
||||||
let toPointSet = (~env, dist) => C.toPointSet(dist)->run(~env)->toDistR
|
let toPointSet = (~env, dist) => C.toPointSet(dist)->run(~env)->toDistR
|
||||||
let toSampleSet = (~env, dist, n) => C.toSampleSet(dist, n)->run(~env)->toDistR
|
let toSampleSet = (~env, dist, n) => C.toSampleSet(dist, n)->run(~env)->toDistR
|
||||||
let fromSamples = (~env, xs) => C.fromSamples(xs)->run(~env)->toDistR
|
let fromSamples = (~env, xs) => C.fromSamples(xs)->run(~env)->toDistR
|
||||||
|
@ -298,6 +293,7 @@ module Constructors = {
|
||||||
let algebraicLogarithm = (~env, dist1, dist2) =>
|
let algebraicLogarithm = (~env, dist1, dist2) =>
|
||||||
C.algebraicLogarithm(dist1, dist2)->run(~env)->toDistR
|
C.algebraicLogarithm(dist1, dist2)->run(~env)->toDistR
|
||||||
let algebraicPower = (~env, dist1, dist2) => C.algebraicPower(dist1, dist2)->run(~env)->toDistR
|
let algebraicPower = (~env, dist1, dist2) => C.algebraicPower(dist1, dist2)->run(~env)->toDistR
|
||||||
|
let scaleMultiply = (~env, dist, n) => C.scaleMultiply(dist, n)->run(~env)->toDistR
|
||||||
let scalePower = (~env, dist, n) => C.scalePower(dist, n)->run(~env)->toDistR
|
let scalePower = (~env, dist, n) => C.scalePower(dist, n)->run(~env)->toDistR
|
||||||
let scaleLogarithm = (~env, dist, n) => C.scaleLogarithm(dist, n)->run(~env)->toDistR
|
let scaleLogarithm = (~env, dist, n) => C.scaleLogarithm(dist, n)->run(~env)->toDistR
|
||||||
let pointwiseAdd = (~env, dist1, dist2) => C.pointwiseAdd(dist1, dist2)->run(~env)->toDistR
|
let pointwiseAdd = (~env, dist1, dist2) => C.pointwiseAdd(dist1, dist2)->run(~env)->toDistR
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
@genType
|
@genType
|
||||||
type env = {
|
let defaultEnv: GenericDist.env
|
||||||
sampleCount: int,
|
|
||||||
xyPointLength: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
@genType
|
|
||||||
let defaultEnv: env
|
|
||||||
|
|
||||||
open DistributionTypes
|
open DistributionTypes
|
||||||
|
|
||||||
|
@ -19,15 +13,18 @@ type outputType =
|
||||||
| GenDistError(error)
|
| GenDistError(error)
|
||||||
|
|
||||||
@genType
|
@genType
|
||||||
let run: (~env: env, DistributionTypes.DistributionOperation.genericFunctionCallInfo) => outputType
|
let run: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
DistributionTypes.DistributionOperation.genericFunctionCallInfo,
|
||||||
|
) => outputType
|
||||||
let runFromDist: (
|
let runFromDist: (
|
||||||
~env: env,
|
~env: GenericDist.env,
|
||||||
~functionCallInfo: DistributionTypes.DistributionOperation.fromDist,
|
~functionCallInfo: DistributionTypes.DistributionOperation.fromDist,
|
||||||
genericDist,
|
genericDist,
|
||||||
) => outputType
|
) => outputType
|
||||||
let runFromFloat: (
|
let runFromFloat: (
|
||||||
~env: env,
|
~env: GenericDist.env,
|
||||||
~functionCallInfo: DistributionTypes.DistributionOperation.fromDist,
|
~functionCallInfo: DistributionTypes.DistributionOperation.fromFloat,
|
||||||
float,
|
float,
|
||||||
) => outputType
|
) => outputType
|
||||||
|
|
||||||
|
@ -42,77 +39,147 @@ module Output: {
|
||||||
let toBool: t => option<bool>
|
let toBool: t => option<bool>
|
||||||
let toBoolR: t => result<bool, error>
|
let toBoolR: t => result<bool, error>
|
||||||
let toError: t => option<error>
|
let toError: t => option<error>
|
||||||
let fmap: (~env: env, t, DistributionTypes.DistributionOperation.singleParamaterFunction) => t
|
let fmap: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
t,
|
||||||
|
DistributionTypes.DistributionOperation.singleParamaterFunction,
|
||||||
|
) => t
|
||||||
}
|
}
|
||||||
|
|
||||||
module Constructors: {
|
module Constructors: {
|
||||||
@genType
|
@genType
|
||||||
let mean: (~env: env, genericDist) => result<float, error>
|
let mean: (~env: GenericDist.env, genericDist) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let stdev: (~env: env, genericDist) => result<float, error>
|
let stdev: (~env: GenericDist.env, genericDist) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let variance: (~env: env, genericDist) => result<float, error>
|
let variance: (~env: GenericDist.env, genericDist) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let sample: (~env: env, genericDist) => result<float, error>
|
let sample: (~env: GenericDist.env, genericDist) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let cdf: (~env: env, genericDist, float) => result<float, error>
|
let cdf: (~env: GenericDist.env, genericDist, float) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let inv: (~env: env, genericDist, float) => result<float, error>
|
let inv: (~env: GenericDist.env, genericDist, float) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let pdf: (~env: env, genericDist, float) => result<float, error>
|
let pdf: (~env: GenericDist.env, genericDist, float) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let normalize: (~env: env, genericDist) => result<genericDist, error>
|
let normalize: (~env: GenericDist.env, genericDist) => result<genericDist, error>
|
||||||
@genType
|
@genType
|
||||||
let isNormalized: (~env: env, genericDist) => result<bool, error>
|
let isNormalized: (~env: GenericDist.env, genericDist) => result<bool, error>
|
||||||
|
module LogScore: {
|
||||||
@genType
|
@genType
|
||||||
let klDivergence: (~env: env, genericDist, genericDist) => result<float, error>
|
let distEstimateDistAnswer: (
|
||||||
@genType
|
~env: GenericDist.env,
|
||||||
let logScoreWithPointResolution: (
|
genericDist,
|
||||||
~env: env,
|
genericDist,
|
||||||
~prediction: genericDist,
|
|
||||||
~answer: float,
|
|
||||||
~prior: option<genericDist>,
|
|
||||||
) => result<float, error>
|
) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let toPointSet: (~env: env, genericDist) => result<genericDist, error>
|
let distEstimateDistAnswerWithPrior: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let toSampleSet: (~env: env, genericDist, int) => result<genericDist, error>
|
let distEstimateScalarAnswer: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
float,
|
||||||
|
) => result<float, error>
|
||||||
@genType
|
@genType
|
||||||
let fromSamples: (~env: env, SampleSetDist.t) => result<genericDist, error>
|
let distEstimateScalarAnswerWithPrior: (
|
||||||
@genType
|
~env: GenericDist.env,
|
||||||
let truncate: (~env: env, genericDist, option<float>, option<float>) => result<genericDist, error>
|
genericDist,
|
||||||
@genType
|
float,
|
||||||
let inspect: (~env: env, genericDist) => result<genericDist, error>
|
genericDist,
|
||||||
@genType
|
) => result<float, error>
|
||||||
let toString: (~env: env, genericDist) => result<string, error>
|
}
|
||||||
@genType
|
@genType
|
||||||
let toSparkline: (~env: env, genericDist, int) => result<string, error>
|
let toPointSet: (~env: GenericDist.env, genericDist) => result<genericDist, error>
|
||||||
@genType
|
@genType
|
||||||
let algebraicAdd: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
let toSampleSet: (~env: GenericDist.env, genericDist, int) => result<genericDist, error>
|
||||||
@genType
|
@genType
|
||||||
let algebraicMultiply: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
let fromSamples: (~env: GenericDist.env, SampleSetDist.t) => result<genericDist, error>
|
||||||
@genType
|
@genType
|
||||||
let algebraicDivide: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
let truncate: (
|
||||||
@genType
|
~env: GenericDist.env,
|
||||||
let algebraicSubtract: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
genericDist,
|
||||||
@genType
|
option<float>,
|
||||||
let algebraicLogarithm: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
option<float>,
|
||||||
@genType
|
) => result<genericDist, error>
|
||||||
let algebraicPower: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
@genType
|
||||||
@genType
|
let inspect: (~env: GenericDist.env, genericDist) => result<genericDist, error>
|
||||||
let scaleLogarithm: (~env: env, genericDist, float) => result<genericDist, error>
|
@genType
|
||||||
@genType
|
let toString: (~env: GenericDist.env, genericDist) => result<string, error>
|
||||||
let scalePower: (~env: env, genericDist, float) => result<genericDist, error>
|
@genType
|
||||||
@genType
|
let toSparkline: (~env: GenericDist.env, genericDist, int) => result<string, error>
|
||||||
let pointwiseAdd: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
@genType
|
||||||
@genType
|
let algebraicAdd: (~env: GenericDist.env, genericDist, genericDist) => result<genericDist, error>
|
||||||
let pointwiseMultiply: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
@genType
|
||||||
@genType
|
let algebraicMultiply: (
|
||||||
let pointwiseDivide: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
~env: GenericDist.env,
|
||||||
@genType
|
genericDist,
|
||||||
let pointwiseSubtract: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
genericDist,
|
||||||
@genType
|
) => result<genericDist, error>
|
||||||
let pointwiseLogarithm: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
@genType
|
||||||
@genType
|
let algebraicDivide: (
|
||||||
let pointwisePower: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let algebraicSubtract: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let algebraicLogarithm: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let algebraicPower: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let scaleLogarithm: (~env: GenericDist.env, genericDist, float) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let scaleMultiply: (~env: GenericDist.env, genericDist, float) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let scalePower: (~env: GenericDist.env, genericDist, float) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let pointwiseAdd: (~env: GenericDist.env, genericDist, genericDist) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let pointwiseMultiply: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let pointwiseDivide: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let pointwiseSubtract: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let pointwiseLogarithm: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
|
@genType
|
||||||
|
let pointwisePower: (
|
||||||
|
~env: GenericDist.env,
|
||||||
|
genericDist,
|
||||||
|
genericDist,
|
||||||
|
) => result<genericDist, error>
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ module DistributionOperation = {
|
||||||
]
|
]
|
||||||
|
|
||||||
type toScaleFn = [
|
type toScaleFn = [
|
||||||
|
| #Multiply
|
||||||
| #Power
|
| #Power
|
||||||
| #Logarithm
|
| #Logarithm
|
||||||
| #LogarithmWithThreshold(float)
|
| #LogarithmWithThreshold(float)
|
||||||
|
@ -97,60 +98,86 @@ module DistributionOperation = {
|
||||||
| ToString
|
| ToString
|
||||||
| ToSparkline(int)
|
| ToSparkline(int)
|
||||||
|
|
||||||
type toScore = KLDivergence(genericDist) | LogScore(float, option<genericDist>)
|
type genericDistOrScalar = Score_Dist(genericDist) | Score_Scalar(float)
|
||||||
|
|
||||||
type fromDist =
|
type toScore = LogScore(genericDistOrScalar, option<genericDist>)
|
||||||
| ToFloat(toFloat)
|
|
||||||
| ToDist(toDist)
|
type fromFloat = [
|
||||||
| ToScore(toScore)
|
| #ToFloat(toFloat)
|
||||||
| ToDistCombination(direction, Operation.Algebraic.t, [#Dist(genericDist) | #Float(float)])
|
| #ToDist(toDist)
|
||||||
| ToString(toString)
|
| #ToDistCombination(direction, Operation.Algebraic.t, [#Dist(genericDist) | #Float(float)])
|
||||||
| ToBool(toBool)
|
| #ToString(toString)
|
||||||
|
| #ToBool(toBool)
|
||||||
|
]
|
||||||
|
|
||||||
|
type fromDist = [
|
||||||
|
| fromFloat
|
||||||
|
| #ToScore(toScore)
|
||||||
|
]
|
||||||
|
|
||||||
type singleParamaterFunction =
|
type singleParamaterFunction =
|
||||||
| FromDist(fromDist)
|
| FromDist(fromDist)
|
||||||
| FromFloat(fromDist)
|
| FromFloat(fromFloat)
|
||||||
|
|
||||||
type genericFunctionCallInfo =
|
type genericFunctionCallInfo =
|
||||||
| FromDist(fromDist, genericDist)
|
| FromDist(fromDist, genericDist)
|
||||||
| FromFloat(fromDist, float)
|
| FromFloat(fromFloat, float)
|
||||||
| FromSamples(array<float>)
|
| FromSamples(array<float>)
|
||||||
| Mixture(array<(genericDist, float)>)
|
| Mixture(array<(genericDist, float)>)
|
||||||
|
|
||||||
let distCallToString = (distFunction: fromDist): string =>
|
let floatCallToString = (floatFunction: fromFloat): string =>
|
||||||
switch distFunction {
|
switch floatFunction {
|
||||||
| ToFloat(#Cdf(r)) => `cdf(${E.Float.toFixed(r)})`
|
| #ToFloat(#Cdf(r)) => `cdf(${E.Float.toFixed(r)})`
|
||||||
| ToFloat(#Inv(r)) => `inv(${E.Float.toFixed(r)})`
|
| #ToFloat(#Inv(r)) => `inv(${E.Float.toFixed(r)})`
|
||||||
| ToFloat(#Mean) => `mean`
|
| #ToFloat(#Mean) => `mean`
|
||||||
| ToFloat(#Min) => `min`
|
| #ToFloat(#Min) => `min`
|
||||||
| ToFloat(#Max) => `max`
|
| #ToFloat(#Max) => `max`
|
||||||
| ToFloat(#Stdev) => `stdev`
|
| #ToFloat(#Stdev) => `stdev`
|
||||||
| ToFloat(#Variance) => `variance`
|
| #ToFloat(#Variance) => `variance`
|
||||||
| ToFloat(#Mode) => `mode`
|
| #ToFloat(#Mode) => `mode`
|
||||||
| ToFloat(#Pdf(r)) => `pdf(${E.Float.toFixed(r)})`
|
| #ToFloat(#Pdf(r)) => `pdf(${E.Float.toFixed(r)})`
|
||||||
| ToFloat(#Sample) => `sample`
|
| #ToFloat(#Sample) => `sample`
|
||||||
| ToFloat(#IntegralSum) => `integralSum`
|
| #ToFloat(#IntegralSum) => `integralSum`
|
||||||
| ToScore(KLDivergence(_)) => `klDivergence`
|
| #ToDist(Normalize) => `normalize`
|
||||||
| ToScore(LogScore(x, _)) => `logScore against ${E.Float.toFixed(x)}`
|
| #ToDist(ToPointSet) => `toPointSet`
|
||||||
| ToDist(Normalize) => `normalize`
|
| #ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})`
|
||||||
| ToDist(ToPointSet) => `toPointSet`
|
| #ToDist(Truncate(_, _)) => `truncate`
|
||||||
| ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})`
|
| #ToDist(Inspect) => `inspect`
|
||||||
| ToDist(Truncate(_, _)) => `truncate`
|
| #ToDist(Scale(#Power, r)) => `scalePower(${E.Float.toFixed(r)})`
|
||||||
| ToDist(Inspect) => `inspect`
|
| #ToDist(Scale(#Multiply, r)) => `scaleMultiply(${E.Float.toFixed(r)})`
|
||||||
| ToDist(Scale(#Power, r)) => `scalePower(${E.Float.toFixed(r)})`
|
| #ToDist(Scale(#Logarithm, r)) => `scaleLog(${E.Float.toFixed(r)})`
|
||||||
| ToDist(Scale(#Logarithm, r)) => `scaleLog(${E.Float.toFixed(r)})`
|
| #ToDist(Scale(#LogarithmWithThreshold(eps), r)) =>
|
||||||
| ToDist(Scale(#LogarithmWithThreshold(eps), r)) =>
|
|
||||||
`scaleLogWithThreshold(${E.Float.toFixed(r)}, epsilon=${E.Float.toFixed(eps)})`
|
`scaleLogWithThreshold(${E.Float.toFixed(r)}, epsilon=${E.Float.toFixed(eps)})`
|
||||||
| ToString(ToString) => `toString`
|
| #ToString(ToString) => `toString`
|
||||||
| ToString(ToSparkline(n)) => `sparkline(${E.I.toString(n)})`
|
| #ToString(ToSparkline(n)) => `sparkline(${E.I.toString(n)})`
|
||||||
| ToBool(IsNormalized) => `isNormalized`
|
| #ToBool(IsNormalized) => `isNormalized`
|
||||||
| ToDistCombination(Algebraic(_), _, _) => `algebraic`
|
| #ToDistCombination(Algebraic(_), _, _) => `algebraic`
|
||||||
| ToDistCombination(Pointwise, _, _) => `pointwise`
|
| #ToDistCombination(Pointwise, _, _) => `pointwise`
|
||||||
|
}
|
||||||
|
|
||||||
|
let distCallToString = (
|
||||||
|
distFunction: [
|
||||||
|
| #ToFloat(toFloat)
|
||||||
|
| #ToDist(toDist)
|
||||||
|
| #ToDistCombination(direction, Operation.Algebraic.t, [#Dist(genericDist) | #Float(float)])
|
||||||
|
| #ToString(toString)
|
||||||
|
| #ToBool(toBool)
|
||||||
|
| #ToScore(toScore)
|
||||||
|
],
|
||||||
|
): string =>
|
||||||
|
switch distFunction {
|
||||||
|
| #ToScore(_) => `logScore`
|
||||||
|
| #ToFloat(x) => floatCallToString(#ToFloat(x))
|
||||||
|
| #ToDist(x) => floatCallToString(#ToDist(x))
|
||||||
|
| #ToString(x) => floatCallToString(#ToString(x))
|
||||||
|
| #ToBool(x) => floatCallToString(#ToBool(x))
|
||||||
|
| #ToDistCombination(x, y, z) => floatCallToString(#ToDistCombination(x, y, z))
|
||||||
}
|
}
|
||||||
|
|
||||||
let toString = (d: genericFunctionCallInfo): string =>
|
let toString = (d: genericFunctionCallInfo): string =>
|
||||||
switch d {
|
switch d {
|
||||||
| FromDist(f, _) | FromFloat(f, _) => distCallToString(f)
|
| FromDist(f, _) => distCallToString(f)
|
||||||
|
| FromFloat(f, _) => floatCallToString(f)
|
||||||
| Mixture(_) => `mixture`
|
| Mixture(_) => `mixture`
|
||||||
| FromSamples(_) => `fromSamples`
|
| FromSamples(_) => `fromSamples`
|
||||||
}
|
}
|
||||||
|
@ -160,79 +187,93 @@ module Constructors = {
|
||||||
|
|
||||||
module UsingDists = {
|
module UsingDists = {
|
||||||
@genType
|
@genType
|
||||||
let mean = (dist): t => FromDist(ToFloat(#Mean), dist)
|
let mean = (dist): t => FromDist(#ToFloat(#Mean), dist)
|
||||||
let stdev = (dist): t => FromDist(ToFloat(#Stdev), dist)
|
let stdev = (dist): t => FromDist(#ToFloat(#Stdev), dist)
|
||||||
let variance = (dist): t => FromDist(ToFloat(#Variance), dist)
|
let variance = (dist): t => FromDist(#ToFloat(#Variance), dist)
|
||||||
let sample = (dist): t => FromDist(ToFloat(#Sample), dist)
|
let sample = (dist): t => FromDist(#ToFloat(#Sample), dist)
|
||||||
let cdf = (dist, x): t => FromDist(ToFloat(#Cdf(x)), dist)
|
let cdf = (dist, x): t => FromDist(#ToFloat(#Cdf(x)), dist)
|
||||||
let inv = (dist, x): t => FromDist(ToFloat(#Inv(x)), dist)
|
let inv = (dist, x): t => FromDist(#ToFloat(#Inv(x)), dist)
|
||||||
let pdf = (dist, x): t => FromDist(ToFloat(#Pdf(x)), dist)
|
let pdf = (dist, x): t => FromDist(#ToFloat(#Pdf(x)), dist)
|
||||||
let normalize = (dist): t => FromDist(ToDist(Normalize), dist)
|
let normalize = (dist): t => FromDist(#ToDist(Normalize), dist)
|
||||||
let isNormalized = (dist): t => FromDist(ToBool(IsNormalized), dist)
|
let isNormalized = (dist): t => FromDist(#ToBool(IsNormalized), dist)
|
||||||
let toPointSet = (dist): t => FromDist(ToDist(ToPointSet), dist)
|
let toPointSet = (dist): t => FromDist(#ToDist(ToPointSet), dist)
|
||||||
let toSampleSet = (dist, r): t => FromDist(ToDist(ToSampleSet(r)), dist)
|
let toSampleSet = (dist, r): t => FromDist(#ToDist(ToSampleSet(r)), dist)
|
||||||
let fromSamples = (xs): t => FromSamples(xs)
|
let fromSamples = (xs): t => FromSamples(xs)
|
||||||
let truncate = (dist, left, right): t => FromDist(ToDist(Truncate(left, right)), dist)
|
let truncate = (dist, left, right): t => FromDist(#ToDist(Truncate(left, right)), dist)
|
||||||
let inspect = (dist): t => FromDist(ToDist(Inspect), dist)
|
let inspect = (dist): t => FromDist(#ToDist(Inspect), dist)
|
||||||
let klDivergence = (dist1, dist2): t => FromDist(ToScore(KLDivergence(dist2)), dist1)
|
module LogScore = {
|
||||||
let logScoreWithPointResolution = (~prediction, ~answer, ~prior): t => FromDist(
|
let distEstimateDistAnswer = (estimate, answer): t => FromDist(
|
||||||
ToScore(LogScore(answer, prior)),
|
#ToScore(LogScore(Score_Dist(answer), None)),
|
||||||
prediction,
|
estimate,
|
||||||
)
|
)
|
||||||
let scalePower = (dist, n): t => FromDist(ToDist(Scale(#Power, n)), dist)
|
let distEstimateDistAnswerWithPrior = (estimate, answer, prior): t => FromDist(
|
||||||
let scaleLogarithm = (dist, n): t => FromDist(ToDist(Scale(#Logarithm, n)), dist)
|
#ToScore(LogScore(Score_Dist(answer), Some(prior))),
|
||||||
|
estimate,
|
||||||
|
)
|
||||||
|
let distEstimateScalarAnswer = (estimate, answer): t => FromDist(
|
||||||
|
#ToScore(LogScore(Score_Scalar(answer), None)),
|
||||||
|
estimate,
|
||||||
|
)
|
||||||
|
let distEstimateScalarAnswerWithPrior = (estimate, answer, prior): t => FromDist(
|
||||||
|
#ToScore(LogScore(Score_Scalar(answer), Some(prior))),
|
||||||
|
estimate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let scaleMultiply = (dist, n): t => FromDist(#ToDist(Scale(#Multiply, n)), dist)
|
||||||
|
let scalePower = (dist, n): t => FromDist(#ToDist(Scale(#Power, n)), dist)
|
||||||
|
let scaleLogarithm = (dist, n): t => FromDist(#ToDist(Scale(#Logarithm, n)), dist)
|
||||||
let scaleLogarithmWithThreshold = (dist, n, eps): t => FromDist(
|
let scaleLogarithmWithThreshold = (dist, n, eps): t => FromDist(
|
||||||
ToDist(Scale(#LogarithmWithThreshold(eps), n)),
|
#ToDist(Scale(#LogarithmWithThreshold(eps), n)),
|
||||||
dist,
|
dist,
|
||||||
)
|
)
|
||||||
let toString = (dist): t => FromDist(ToString(ToString), dist)
|
let toString = (dist): t => FromDist(#ToString(ToString), dist)
|
||||||
let toSparkline = (dist, n): t => FromDist(ToString(ToSparkline(n)), dist)
|
let toSparkline = (dist, n): t => FromDist(#ToString(ToSparkline(n)), dist)
|
||||||
let algebraicAdd = (dist1, dist2: genericDist): t => FromDist(
|
let algebraicAdd = (dist1, dist2: genericDist): t => FromDist(
|
||||||
ToDistCombination(Algebraic(AsDefault), #Add, #Dist(dist2)),
|
#ToDistCombination(Algebraic(AsDefault), #Add, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let algebraicMultiply = (dist1, dist2): t => FromDist(
|
let algebraicMultiply = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Algebraic(AsDefault), #Multiply, #Dist(dist2)),
|
#ToDistCombination(Algebraic(AsDefault), #Multiply, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let algebraicDivide = (dist1, dist2): t => FromDist(
|
let algebraicDivide = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Algebraic(AsDefault), #Divide, #Dist(dist2)),
|
#ToDistCombination(Algebraic(AsDefault), #Divide, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let algebraicSubtract = (dist1, dist2): t => FromDist(
|
let algebraicSubtract = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Algebraic(AsDefault), #Subtract, #Dist(dist2)),
|
#ToDistCombination(Algebraic(AsDefault), #Subtract, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let algebraicLogarithm = (dist1, dist2): t => FromDist(
|
let algebraicLogarithm = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Algebraic(AsDefault), #Logarithm, #Dist(dist2)),
|
#ToDistCombination(Algebraic(AsDefault), #Logarithm, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let algebraicPower = (dist1, dist2): t => FromDist(
|
let algebraicPower = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Algebraic(AsDefault), #Power, #Dist(dist2)),
|
#ToDistCombination(Algebraic(AsDefault), #Power, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let pointwiseAdd = (dist1, dist2): t => FromDist(
|
let pointwiseAdd = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Pointwise, #Add, #Dist(dist2)),
|
#ToDistCombination(Pointwise, #Add, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let pointwiseMultiply = (dist1, dist2): t => FromDist(
|
let pointwiseMultiply = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Pointwise, #Multiply, #Dist(dist2)),
|
#ToDistCombination(Pointwise, #Multiply, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let pointwiseDivide = (dist1, dist2): t => FromDist(
|
let pointwiseDivide = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Pointwise, #Divide, #Dist(dist2)),
|
#ToDistCombination(Pointwise, #Divide, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let pointwiseSubtract = (dist1, dist2): t => FromDist(
|
let pointwiseSubtract = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Pointwise, #Subtract, #Dist(dist2)),
|
#ToDistCombination(Pointwise, #Subtract, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let pointwiseLogarithm = (dist1, dist2): t => FromDist(
|
let pointwiseLogarithm = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Pointwise, #Logarithm, #Dist(dist2)),
|
#ToDistCombination(Pointwise, #Logarithm, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
let pointwisePower = (dist1, dist2): t => FromDist(
|
let pointwisePower = (dist1, dist2): t => FromDist(
|
||||||
ToDistCombination(Pointwise, #Power, #Dist(dist2)),
|
#ToDistCombination(Pointwise, #Power, #Dist(dist2)),
|
||||||
dist1,
|
dist1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,11 @@ type toSampleSetFn = t => result<SampleSetDist.t, error>
|
||||||
type scaleMultiplyFn = (t, float) => result<t, error>
|
type scaleMultiplyFn = (t, float) => result<t, error>
|
||||||
type pointwiseAddFn = (t, t) => result<t, error>
|
type pointwiseAddFn = (t, t) => result<t, error>
|
||||||
|
|
||||||
|
type env = {
|
||||||
|
sampleCount: int,
|
||||||
|
xyPointLength: int,
|
||||||
|
}
|
||||||
|
|
||||||
let isPointSet = (t: t) =>
|
let isPointSet = (t: t) =>
|
||||||
switch t {
|
switch t {
|
||||||
| PointSet(_) => true
|
| PointSet(_) => true
|
||||||
|
@ -61,46 +66,6 @@ let integralEndY = (t: t): float =>
|
||||||
|
|
||||||
let isNormalized = (t: t): bool => Js.Math.abs_float(integralEndY(t) -. 1.0) < 1e-7
|
let isNormalized = (t: t): bool => Js.Math.abs_float(integralEndY(t) -. 1.0) < 1e-7
|
||||||
|
|
||||||
module Score = {
|
|
||||||
let klDivergence = (prediction, answer, ~toPointSetFn: toPointSetFn): result<float, error> => {
|
|
||||||
let pointSets = E.R.merge(toPointSetFn(prediction), toPointSetFn(answer))
|
|
||||||
pointSets |> E.R2.bind(((predi, ans)) =>
|
|
||||||
PointSetDist.T.klDivergence(predi, ans)->E.R2.errMap(x => DistributionTypes.OperationError(x))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let logScoreWithPointResolution = (
|
|
||||||
~prediction: DistributionTypes.genericDist,
|
|
||||||
~answer: float,
|
|
||||||
~prior: option<DistributionTypes.genericDist>,
|
|
||||||
~toPointSetFn: toPointSetFn,
|
|
||||||
): result<float, error> => {
|
|
||||||
switch prior {
|
|
||||||
| Some(prior') =>
|
|
||||||
E.R.merge(toPointSetFn(prior'), toPointSetFn(prediction))->E.R.bind(((
|
|
||||||
prior'',
|
|
||||||
prediction'',
|
|
||||||
)) =>
|
|
||||||
PointSetDist.T.logScoreWithPointResolution(
|
|
||||||
~prediction=prediction'',
|
|
||||||
~answer,
|
|
||||||
~prior=prior''->Some,
|
|
||||||
)->E.R2.errMap(x => DistributionTypes.OperationError(x))
|
|
||||||
)
|
|
||||||
| None =>
|
|
||||||
prediction
|
|
||||||
->toPointSetFn
|
|
||||||
->E.R.bind(x =>
|
|
||||||
PointSetDist.T.logScoreWithPointResolution(
|
|
||||||
~prediction=x,
|
|
||||||
~answer,
|
|
||||||
~prior=None,
|
|
||||||
)->E.R2.errMap(x => DistributionTypes.OperationError(x))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let toFloatOperation = (
|
let toFloatOperation = (
|
||||||
t,
|
t,
|
||||||
~toPointSetFn: toPointSetFn,
|
~toPointSetFn: toPointSetFn,
|
||||||
|
@ -171,6 +136,70 @@ let toPointSet = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module Score = {
|
||||||
|
type genericDistOrScalar = DistributionTypes.DistributionOperation.genericDistOrScalar
|
||||||
|
|
||||||
|
let argsMake = (~esti: t, ~answ: genericDistOrScalar, ~prior: option<t>, ~env: env): result<
|
||||||
|
PointSetDist_Scoring.scoreArgs,
|
||||||
|
error,
|
||||||
|
> => {
|
||||||
|
let toPointSetFn = t =>
|
||||||
|
toPointSet(
|
||||||
|
t,
|
||||||
|
~xyPointLength=env.xyPointLength,
|
||||||
|
~sampleCount=env.sampleCount,
|
||||||
|
~xSelection=#ByWeight,
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
let prior': option<result<PointSetTypes.pointSetDist, error>> = switch prior {
|
||||||
|
| None => None
|
||||||
|
| Some(d) => toPointSetFn(d)->Some
|
||||||
|
}
|
||||||
|
let twoDists = (~toPointSetFn, esti': t, answ': t): result<
|
||||||
|
(PointSetTypes.pointSetDist, PointSetTypes.pointSetDist),
|
||||||
|
error,
|
||||||
|
> => E.R.merge(toPointSetFn(esti'), toPointSetFn(answ'))
|
||||||
|
switch (esti, answ, prior') {
|
||||||
|
| (esti', Score_Dist(answ'), None) =>
|
||||||
|
twoDists(~toPointSetFn, esti', answ')->E.R2.fmap(((esti'', answ'')) =>
|
||||||
|
{estimate: esti'', answer: answ'', prior: None}->PointSetDist_Scoring.DistAnswer
|
||||||
|
)
|
||||||
|
| (esti', Score_Dist(answ'), Some(Ok(prior''))) =>
|
||||||
|
twoDists(~toPointSetFn, esti', answ')->E.R2.fmap(((esti'', answ'')) =>
|
||||||
|
{
|
||||||
|
estimate: esti'',
|
||||||
|
answer: answ'',
|
||||||
|
prior: Some(prior''),
|
||||||
|
}->PointSetDist_Scoring.DistAnswer
|
||||||
|
)
|
||||||
|
| (esti', Score_Scalar(answ'), None) =>
|
||||||
|
toPointSetFn(esti')->E.R2.fmap(esti'' =>
|
||||||
|
{
|
||||||
|
estimate: esti'',
|
||||||
|
answer: answ',
|
||||||
|
prior: None,
|
||||||
|
}->PointSetDist_Scoring.ScalarAnswer
|
||||||
|
)
|
||||||
|
| (esti', Score_Scalar(answ'), Some(Ok(prior''))) =>
|
||||||
|
toPointSetFn(esti')->E.R2.fmap(esti'' =>
|
||||||
|
{
|
||||||
|
estimate: esti'',
|
||||||
|
answer: answ',
|
||||||
|
prior: Some(prior''),
|
||||||
|
}->PointSetDist_Scoring.ScalarAnswer
|
||||||
|
)
|
||||||
|
| (_, _, Some(Error(err))) => err->Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let logScore = (~estimate: t, ~answer: genericDistOrScalar, ~prior: option<t>, ~env: env): result<
|
||||||
|
float,
|
||||||
|
error,
|
||||||
|
> =>
|
||||||
|
argsMake(~esti=estimate, ~answ=answer, ~prior, ~env)->E.R.bind(x =>
|
||||||
|
x->PointSetDist.logScore->E.R2.errMap(y => DistributionTypes.OperationError(y))
|
||||||
|
)
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
PointSetDist.toSparkline calls "downsampleEquallyOverX", which downsamples it to n=bucketCount.
|
PointSetDist.toSparkline calls "downsampleEquallyOverX", which downsamples it to n=bucketCount.
|
||||||
It first needs a pointSetDist, so we convert to a pointSetDist. In this process we want the
|
It first needs a pointSetDist, so we convert to a pointSetDist. In this process we want the
|
||||||
|
|
|
@ -5,6 +5,9 @@ type toSampleSetFn = t => result<SampleSetDist.t, error>
|
||||||
type scaleMultiplyFn = (t, float) => result<t, error>
|
type scaleMultiplyFn = (t, float) => result<t, error>
|
||||||
type pointwiseAddFn = (t, t) => result<t, error>
|
type pointwiseAddFn = (t, t) => result<t, error>
|
||||||
|
|
||||||
|
@genType
|
||||||
|
type env = {sampleCount: int, xyPointLength: int}
|
||||||
|
|
||||||
let sampleN: (t, int) => array<float>
|
let sampleN: (t, int) => array<float>
|
||||||
let sample: t => float
|
let sample: t => float
|
||||||
|
|
||||||
|
@ -25,12 +28,11 @@ let toFloatOperation: (
|
||||||
) => result<float, error>
|
) => result<float, error>
|
||||||
|
|
||||||
module Score: {
|
module Score: {
|
||||||
let klDivergence: (t, t, ~toPointSetFn: toPointSetFn) => result<float, error>
|
let logScore: (
|
||||||
let logScoreWithPointResolution: (
|
~estimate: t,
|
||||||
~prediction: t,
|
~answer: DistributionTypes.DistributionOperation.genericDistOrScalar,
|
||||||
~answer: float,
|
|
||||||
~prior: option<t>,
|
~prior: option<t>,
|
||||||
~toPointSetFn: toPointSetFn,
|
~env: env,
|
||||||
) => result<float, error>
|
) => result<float, error>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user