Merge pull request #902 from quantified-uncertainty/develop

Develop -> Master July 28 2022
This commit is contained in:
Ozzie Gooen 2022-07-28 16:36:24 -07:00 committed by GitHub
commit 3613a754c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
224 changed files with 12011 additions and 5954 deletions

4
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -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

View File

@ -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/

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,4 @@
## Artifacts
*.swp
/node_modules/
yarn-error.log

22
packages/cli/README.md Normal file
View 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
View 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
View 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"
}
}

View File

@ -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.

View File

@ -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 .",

View File

@ -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?.(),
},
]}
/> />
); );
}; };

View File

@ -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) => {
distribution, const {
height, distribution,
showSummary, height,
width, showSummary,
showControls = false, width,
}) => { logX,
const [isLogX, setLogX] = React.useState(false); actions = false,
const [isExpY, setExpY] = React.useState(false); } = props;
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 }}>
<Vega {logX && hasMassBelowZero(shape.value) ? (
spec={spec} <ErrorAlert heading="Log Domain Error">
data={{ con: shape.value.continuous, dis: shape.value.discrete }} Cannot graph distribution with negative values on logarithmic scale.
width={widthProp - 10} </ErrorAlert>
height={height} ) : (
actions={false} <Vega
/> spec={spec}
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
width={widthProp - 10}
height={height}
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,
}) => ( }) => (

View File

@ -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>
); );
} }

View File

@ -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;

View File

@ -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> = React.memo(
({
code = "",
executionId = 0,
environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false,
width,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 100,
tickFormat,
minX,
maxX,
color,
title,
distributionChartActions,
enableLocalSettings = false,
}) => {
const result = useSquiggle({
code,
bindings,
environment,
jsImports,
onChange,
executionId,
});
const distributionPlotSettings = {
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "",
environment,
onChange = () => {},
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false,
width,
showTypes = false,
showControls = false,
chartSettings = defaultChartSettings,
}) => {
let expressionResult = run(squiggleString, bindings, environment, jsImports);
if (expressionResult.tag !== "Ok") {
return ( return (
<ErrorAlert heading={"Parse Error"}> <SquiggleViewer
{errorValueToString(expressionResult.value)} result={result}
</ErrorAlert> width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
enableLocalSettings={enableLocalSettings}
/>
); );
} }
);
let e = environment ?? defaultEnvironment;
let expression = expressionResult.value;
onChange(expression);
return (
<SquiggleItem
expression={expression}
width={width}
height={height}
showSummary={showSummary}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={e}
/>
);
};

View File

@ -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 {

View File

@ -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; <div className="border border-grey-200 p-2 m-4">
/** If the result is a function, where the function starts */ <CodeEditor
diagramStart?: number; value={code}
/** If the result is a function, where the function ends */ onChange={setCode}
diagramStop?: number; oneLine={true}
/** If the result is a function, how many points along the function it samples */ showGutter={false}
diagramCount?: number; height={20}
/** when the environment changes. Used again for notebook magic*/ />
onChange?(expr: squiggleExpression): void; </div>
/** 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> = ({ export type SquiggleEditorProps = SquiggleChartProps & {
initialSquiggleString = "", defaultCode?: string;
width, onCodeChange?: (code: string) => void;
environment, };
diagramStart = 0,
diagramStop = 10, export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
diagramCount = 20, const [code, setCode] = useMaybeControlledValue({
onChange, value: props.code,
bindings = defaultBindings, defaultValue: props.defaultCode ?? "",
jsImports = defaultImports, onChange: props.onCodeChange,
showTypes = false, });
showControls = false,
showSummary = false, let chartProps = { ...props, code };
}: SquiggleEditorProps) => {
const [expression, setExpression] = React.useState(initialSquiggleString);
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return ( return (
<SquiggleContainer> <SquiggleContainer>
<div> <WrappedCodeEditor code={code} setCode={setCode} />
<div className="border border-grey-200 p-2 m-4"> <SquiggleChart {...chartProps} />
<CodeEditor
value={expression}
onChange={setExpression}
oneLine={true}
showGutter={false}
height={20}
/>
</div>
<SquiggleChart
width={width}
environment={environment}
squiggleString={expression}
chartSettings={chartSettings}
onChange={onChange}
bindings={bindings}
jsImports={jsImports}
showTypes={showTypes}
showControls={showControls}
showSummary={showSummary}
/>
</div>
</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;
}

View File

@ -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 }} />
);
};

View 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>;
};

View File

@ -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,297 +74,68 @@ 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> <div className="space-y-6 p-3 max-w-xl">
<header className="text-lg leading-6 font-medium text-gray-900"> <div>
{title} <InputItem
</header> name="sampleCount"
<div className="mt-4">{children}</div> type="number"
label="Sample Count"
register={register}
/>
<div className="mt-2">
<Text>
How many samples to use for Monte Carlo simulations. This can
occasionally be overridden by specific Squiggle programs.
</Text>
</div>
</div>
<div>
<InputItem
name="xyPointLength"
type="number"
register={register}
label="Coordinate Count (For PointSet Shapes)"
/>
<div className="mt-2">
<Text>
When distributions are converted into PointSet shapes, we need to know
how many coordinates to use.
</Text>
</div>
</div>
</div> </div>
); );
const Text: FC<{ children: React.ReactNode }> = ({ children }) => ( const InputVariablesSettings: React.FC<{
<p className="text-sm text-gray-500">{children}</p> initialImports: any; // TODO - any json type
); setImports: (imports: any) => void;
}> = ({ initialImports, setImports }) => {
function InputItem<T>({ const [importString, setImportString] = useState(() =>
name, JSON.stringify(initialImports)
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 [importsAreValid, setImportsAreValid] = useState(true);
const { register, control } = useForm({
resolver: yupResolver(schema), const onChange = (value: string) => {
defaultValues: { setImportString(value);
sampleCount: 1000, let imports = {} as any;
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 { try {
setImports(JSON.parse(r)); imports = JSON.parse(value);
setImportsAreValid(true); setImportsAreValid(true);
} catch (e) { } catch (e) {
setImportsAreValid(false); setImportsAreValid(false);
} }
setImports(imports);
}; };
const samplingSettings = ( return (
<div className="space-y-6 p-3 max-w-xl">
<div>
<InputItem
name="sampleCount"
type="number"
label="Sample Count"
register={register}
/>
<div className="mt-2">
<Text>
How many samples to use for Monte Carlo simulations. This can
occasionally be overridden by specific Squiggle programs.
</Text>
</div>
</div>
<div>
<InputItem
name="xyPointLength"
type="number"
register={register}
label="Coordinate Count (For PointSet Shapes)"
/>
<div className="mt-2">
<Text>
When distributions are converted into PointSet shapes, we need to
know how many coordinates to use.
</Text>
</div>
</div>
</div>
);
const viewSettings = (
<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">
<InputItem
name="chartHeight"
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 inputVariableSettings = (
<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> )}
</div> />
<div className="flex" style={{ height }}> </button>
<div className="w-1/2"> )}
<Tab.Panels> <Toggle
<Tab.Panel> texts={["Autorun", "Paused"]}
<div className="border border-slate-200"> icons={[CheckCircleIcon, PauseIcon]}
<CodeEditor status={autorunMode}
value={squiggleString} onChange={onAutorunModeChange}
onChange={setSquiggleString} spinIcon={autorunMode && isRunning}
oneLine={false} />
showGutter={true} </div>
height={height - 1}
/>
</div>
</Tab.Panel>
<Tab.Panel>{samplingSettings}</Tab.Panel>
<Tab.Panel>{viewSettings}</Tab.Panel>
<Tab.Panel>{inputVariableSettings}</Tab.Panel>
</Tab.Panels>
</div>
<div className="w-1/2 p-2 pl-4">
<div style={{ maxHeight: height }}>
<SquiggleChart
squiggleString={squiggleString}
environment={env}
chartSettings={chartSettings}
height={vars.chartHeight}
showTypes={vars.showTypes}
showControls={vars.showControls}
bindings={defaultBindings}
jsImports={imports}
showSummary={vars.showSummary}
/>
</div>
</div>
</div>
</Tab.Group>
</SquiggleContainer>
); );
}; };
export default SquigglePlayground; const ShareButton: React.FC = () => {
export function renderSquigglePlaygroundToDom(props: PlaygroundProps) { const [isCopied, setIsCopied] = useState(false);
const parent = document.createElement("div"); const copy = () => {
ReactDOM.render(<SquigglePlayground {...props} />, parent); navigator.clipboard.writeText((window.top || window).location.href);
return parent; 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">
<CodeEditor
value={code}
onChange={setCode}
onSubmit={run}
oneLine={false}
showGutter={true}
height={height - 1}
/>
</div>
) : (
squiggleChart
);
const tabs = (
<StyledTab.Panels>
<StyledTab.Panel>{firstTab}</StyledTab.Panel>
<StyledTab.Panel>
<SamplingSettings register={register} />
</StyledTab.Panel>
<StyledTab.Panel>
<ViewSettings
register={
// This is dangerous, but doesn't cause any problems.
// I tried to make `ViewSettings` generic (to allow it to accept any extension of a settings schema), but it didn't work.
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>
{vars.showEditor ? withEditor : withoutEditor}
</div>
</StyledTab.Group>
</PlaygroundContext.Provider>
</SquiggleContainer>
);
};

View File

@ -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>
);
}
}
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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,
});

View 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>
);
};

View 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(".");

View 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>
);
};

View 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>
);
};

View 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>
);
}

View 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>
);

View 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>
);
}

View 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;

View 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;

View 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>
);

View 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>
);
};

View 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>
</>
);
};

View File

@ -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";

View 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;
}

View 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);

View File

@ -0,0 +1,3 @@
export { useMaybeControlledValue } from "./useMaybeControlledValue";
export { useSquiggle, useSquigglePartial } from "./useSquiggle";
export { useRunnerState } from "./useRunnerState";

View 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];
}

View 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 });
},
};
};

View 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);
};

View File

@ -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,
}} }}
> >

View File

@ -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({})}

View File

@ -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}
/> />
</> </>

View File

@ -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>

View File

@ -1,5 +1,5 @@
.squiggle { .squiggle {
/* /*
This file contains: This file contains:
1) Base Tailwind preflight styles 1) Base Tailwind preflight styles
2) Base https://github.com/tailwindlabs/tailwindcss-forms styles 2) Base https://github.com/tailwindlabs/tailwindcss-forms styles
@ -7,365 +7,390 @@ This file contains:
(Both are wrapped in .squiggle) (Both are wrapped in .squiggle)
*/ */
/* /*
1. Use a consistent sensible line-height in all browsers. 1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS. 2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size. 3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default. 4. Use the user's configured `sans` font-family by default.
*/ */
/* html { */ /* html { */
line-height: 1.5; /* 1 */ line-height: 1.5; /* 1 */
-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 */
/* } */
/* /*
1. Remove the margin in all browsers. 1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/ */
/* body { */ /* body { */
margin: 0; /* 1 */ margin: 0; /* 1 */
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)
*/ */
*, *,
::before, ::before,
::after { ::after {
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: "";
} }
/* /*
1. Add the correct height in Firefox. 1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default. 3. Ensure horizontal rules are visible by default.
*/ */
hr { hr {
height: 0; /* 1 */ height: 0; /* 1 */
color: inherit; /* 2 */ color: inherit; /* 2 */
border-top-width: 1px; /* 3 */ border-top-width: 1px; /* 3 */
} }
/* /*
Add the correct text decoration in Chrome, Edge, and Safari. Add the correct text decoration in Chrome, Edge, and Safari.
*/ */
abbr:where([title]) { abbr:where([title]) {
text-decoration: underline dotted; text-decoration: underline dotted;
} }
/* /*
Remove the default font size and weight for headings. Remove the default font size and weight for headings.
*/ */
h1, h1,
h2, h2,
h3, h3,
h4, h4,
h5, h5,
h6 { h6 {
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
} }
/* /*
Reset links to optimize for opt-in styling instead of opt-out. Reset links to optimize for opt-in styling instead of opt-out.
*/ */
a { a {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
} }
/* /*
Add the correct font weight in Edge and Safari. Add the correct font weight in Edge and Safari.
*/ */
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
/* /*
1. Use the user's configured `mono` font family by default. 1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers. 2. Correct the odd `em` font sizing in all browsers.
*/ */
code, 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(
font-size: 1em; /* 2 */ "fontFamily.mono",
} ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
"Liberation Mono",
"Courier New",
monospace
); /* 1 */
font-size: 1em; /* 2 */
}
/* /*
Add the correct font size in all browsers. Add the correct font size in all browsers.
*/ */
small { small {
font-size: 80%; font-size: 80%;
} }
/* /*
Prevent `sub` and `sup` elements from affecting the line height in all browsers. Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/ */
sub, sub,
sup { sup {
font-size: 75%; font-size: 75%;
line-height: 0; line-height: 0;
position: relative; position: relative;
vertical-align: baseline; vertical-align: baseline;
} }
sub { sub {
bottom: -0.25em; bottom: -0.25em;
} }
sup { sup {
top: -0.5em; top: -0.5em;
} }
/* /*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default. 3. Remove gaps between table borders by default.
*/ */
table { table {
text-indent: 0; /* 1 */ text-indent: 0; /* 1 */
border-color: inherit; /* 2 */ border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */ border-collapse: collapse; /* 3 */
} }
/* /*
1. Change the font styles in all browsers. 1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari. 2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers. 3. Remove default padding in all browsers.
*/ */
button, button,
input, input,
optgroup, optgroup,
select, select,
textarea { textarea {
font-family: inherit; /* 1 */ font-family: inherit; /* 1 */
font-size: 100%; /* 1 */ font-size: 100%; /* 1 */
font-weight: inherit; /* 1 */ font-weight: inherit; /* 1 */
line-height: inherit; /* 1 */ line-height: inherit; /* 1 */
color: inherit; /* 1 */ color: inherit; /* 1 */
margin: 0; /* 2 */ margin: 0; /* 2 */
padding: 0; /* 3 */ padding: 0; /* 3 */
} }
/* /*
Remove the inheritance of text transform in Edge and Firefox. Remove the inheritance of text transform in Edge and Firefox.
*/ */
button, button,
select { select {
text-transform: none; text-transform: none;
} }
/* /*
1. Correct the inability to style clickable types in iOS and Safari. 1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles. 2. Remove default button styles.
*/ */
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 */
} }
/* /*
Use the modern Firefox focus style for all focusable elements. Use the modern Firefox focus style for all focusable elements.
*/ */
:-moz-focusring { :-moz-focusring {
outline: auto; outline: auto;
} }
/* /*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/ */
:-moz-ui-invalid { :-moz-ui-invalid {
box-shadow: none; box-shadow: none;
} }
/* /*
Add the correct vertical alignment in Chrome and Firefox. Add the correct vertical alignment in Chrome and Firefox.
*/ */
progress { progress {
vertical-align: baseline; vertical-align: baseline;
} }
/* /*
Correct the cursor style of increment and decrement buttons in Safari. Correct the cursor style of increment and decrement buttons in Safari.
*/ */
::-webkit-inner-spin-button, ::-webkit-inner-spin-button,
::-webkit-outer-spin-button { ::-webkit-outer-spin-button {
height: auto; height: auto;
} }
/* /*
1. Correct the odd appearance in Chrome and Safari. 1. Correct the odd appearance in Chrome and 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 */
} }
/* /*
Remove the inner padding in Chrome and Safari on macOS. Remove the inner padding in Chrome and Safari on macOS.
*/ */
::-webkit-search-decoration { ::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
/* /*
1. Correct the inability to style clickable types in iOS and Safari. 1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari. 2. Change font properties to `inherit` in Safari.
*/ */
::-webkit-file-upload-button { ::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */ -webkit-appearance: button; /* 1 */
font: inherit; /* 2 */ font: inherit; /* 2 */
} }
/* /*
Add the correct display in Chrome and Safari. Add the correct display in Chrome and Safari.
*/ */
summary { summary {
display: list-item; display: list-item;
} }
/* /*
Removes the default spacing and border for appropriate elements. Removes the default spacing and border for appropriate elements.
*/ */
blockquote, blockquote,
dl, dl,
dd, dd,
h1, h1,
h2, h2,
h3, h3,
h4, h4,
h5, h5,
h6, h6,
hr, hr,
figure, figure,
p, p,
pre { pre {
margin: 0; margin: 0;
} }
fieldset { fieldset {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
legend { legend {
padding: 0; padding: 0;
} }
ol, ol,
ul, ul,
menu { menu {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
/* /*
Prevent resizing textareas horizontally by default. Prevent resizing textareas horizontally by default.
*/ */
textarea { textarea {
resize: vertical; resize: vertical;
} }
/* /*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color. 2. Set the default placeholder color to the user's configured gray 400 color.
*/ */
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 */
} }
/* /*
Set the default cursor for buttons. Set the default cursor for buttons.
*/ */
button, button,
[role="button"] { [role="button"] {
cursor: pointer; cursor: pointer;
} }
/* /*
Make sure disabled buttons don't get the pointer cursor. Make sure disabled buttons don't get the pointer cursor.
*/ */
:disabled { :disabled {
cursor: default; cursor: default;
} }
/* /*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design. This can trigger a poorly considered lint error in some tools but is included by design.
*/ */
img, img,
svg, svg,
video, video,
canvas, canvas,
audio, audio,
iframe, iframe,
embed, embed,
object { object {
display: block; /* 1 */ display: block; /* 1 */
vertical-align: middle; /* 2 */ vertical-align: middle; /* 2 */
} }
/* /*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/ */
img, img,
video { video {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
} }

View File

@ -1,102 +1,130 @@
/* 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,
appearance: none; .form-textarea,
background-color: #fff; .form-select,
border-color: #6b7280; .form-multiselect {
border-width: 1px; appearance: none;
border-radius: 0px; background-color: #fff;
padding-top: 0.5rem; border-color: #6b7280;
padding-right: 0.75rem; border-width: 1px;
padding-bottom: 0.5rem; border-radius: 0px;
padding-left: 0.75rem; padding-top: 0.5rem;
font-size: 1rem; padding-right: 0.75rem;
line-height: 1.5rem; padding-bottom: 0.5rem;
--tw-shadow: 0 0 #0000; padding-left: 0.75rem;
} font-size: 1rem;
.form-input:focus, .form-textarea:focus, .form-select:focus, .form-multiselect:focus { line-height: 1.5rem;
outline: 2px solid transparent; --tw-shadow: 0 0 #0000;
outline-offset: 2px; }
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); .form-input:focus,
--tw-ring-offset-width: 0px; .form-textarea:focus,
--tw-ring-offset-color: #fff; .form-select:focus,
--tw-ring-color: #2563eb; .form-multiselect:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); outline: 2px solid transparent;
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); outline-offset: 2px;
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
border-color: #2563eb; --tw-ring-offset-width: 0px;
} --tw-ring-offset-color: #fff;
.form-input::placeholder,.form-textarea::placeholder { --tw-ring-color: #2563eb;
color: #6b7280; --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
opacity: 1; var(--tw-ring-offset-width) var(--tw-ring-offset-color);
} --tw-ring-shadow: var(--tw-ring-inset) 0 0 0
.form-input::-webkit-datetime-edit-fields-wrapper { calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
padding: 0; box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
} var(--tw-shadow);
.form-input::-webkit-date-and-time-value { border-color: #2563eb;
min-height: 1.5em; }
} .form-input::placeholder,
.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-textarea::placeholder {
padding-top: 0; color: #6b7280;
padding-bottom: 0; opacity: 1;
} }
.form-checkbox,.form-radio { .form-input::-webkit-datetime-edit-fields-wrapper {
appearance: none; padding: 0;
padding: 0; }
-webkit-print-color-adjust: exact; .form-input::-webkit-date-and-time-value {
print-color-adjust: exact; min-height: 1.5em;
display: inline-block; }
vertical-align: middle; .form-input::-webkit-datetime-edit,
background-origin: border-box; .form-input::-webkit-datetime-edit-year-field,
-webkit-user-select: none; .form-input::-webkit-datetime-edit-month-field,
user-select: none; .form-input::-webkit-datetime-edit-day-field,
flex-shrink: 0; .form-input::-webkit-datetime-edit-hour-field,
height: 1rem; .form-input::-webkit-datetime-edit-minute-field,
width: 1rem; .form-input::-webkit-datetime-edit-second-field,
color: #2563eb; .form-input::-webkit-datetime-edit-millisecond-field,
background-color: #fff; .form-input::-webkit-datetime-edit-meridiem-field {
border-color: #6b7280; padding-top: 0;
border-width: 1px; padding-bottom: 0;
--tw-shadow: 0 0 #0000; }
} .form-checkbox,
.form-checkbox { .form-radio {
border-radius: 0px; appearance: none;
} padding: 0;
.form-checkbox:focus,.form-radio:focus { -webkit-print-color-adjust: exact;
outline: 2px solid transparent; print-color-adjust: exact;
outline-offset: 2px; display: inline-block;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); vertical-align: middle;
--tw-ring-offset-width: 2px; background-origin: border-box;
--tw-ring-offset-color: #fff; -webkit-user-select: none;
--tw-ring-color: #2563eb; user-select: none;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); flex-shrink: 0;
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); height: 1rem;
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); width: 1rem;
} color: #2563eb;
.form-checkbox:checked,.form-radio:checked { background-color: #fff;
border-color: transparent; border-color: #6b7280;
background-color: currentColor; border-width: 1px;
background-size: 100% 100%; --tw-shadow: 0 0 #0000;
background-position: center; }
background-repeat: no-repeat; .form-checkbox {
} border-radius: 0px;
.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"); .form-checkbox:focus,
} .form-radio:focus {
.form-checkbox:checked:hover,.form-checkbox:checked:focus,.form-radio:checked:hover,.form-radio:checked:focus { outline: 2px solid transparent;
border-color: transparent; outline-offset: 2px;
background-color: currentColor; --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
} --tw-ring-offset-width: 2px;
.form-checkbox:indeterminate { --tw-ring-offset-color: #fff;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); --tw-ring-color: #2563eb;
border-color: transparent; --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
background-color: currentColor; var(--tw-ring-offset-width) var(--tw-ring-offset-color);
background-size: 100% 100%; --tw-ring-shadow: var(--tw-ring-inset) 0 0 0
background-position: center; calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
background-repeat: no-repeat; box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
} var(--tw-shadow);
.form-checkbox:indeterminate:hover,.form-checkbox:indeterminate:focus { }
border-color: transparent; .form-checkbox:checked,
background-color: currentColor; .form-radio:checked {
} border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.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");
}
.form-checkbox:checked:hover,
.form-checkbox:checked:focus,
.form-radio:checked:hover,
.form-radio:checked:focus {
border-color: transparent;
background-color: currentColor;
}
.form-checkbox:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.form-checkbox:indeterminate:hover,
.form-checkbox:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
} }

View File

@ -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 },
},
},
},
}, },
}; };

View File

@ -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",
},
},
}; };

View File

@ -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

View File

@ -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,
)
}) })

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}
})
})

View File

@ -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)
}) })
}) })

View File

@ -0,0 +1,4 @@
open Jest
open Expect
test("todo", () => expect("1")->toBe("1"))

View File

@ -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)")
}) })

View File

@ -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", () => {

View File

@ -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)

View File

@ -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", () => {

View File

@ -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}")
}) })
}) })

View File

@ -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)))}",

View File

@ -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))
} }

View File

@ -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", ())
}) })
}) })

View File

@ -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]}",
(), (),
) )
}) })

View File

@ -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)}",
(),
)
})

View File

@ -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)

View File

@ -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)

View File

@ -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}")

View File

@ -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")

View File

@ -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")

View File

@ -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})",
)
}) })

View File

@ -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)")

View File

@ -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???")

View File

@ -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: )")
}) })

View File

@ -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})")
}) })
}) })

View File

@ -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)")

View File

@ -1,4 +1,4 @@
open ReducerInterface.ExpressionValue open ReducerInterface.ExternalExpressionValue
open Jest open Jest
open Expect open Expect

View File

@ -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))
},
)
})
})

View File

@ -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);

View File

@ -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
minLength: 10, .float32Array({
maxLength: 10000, minLength: 10,
noDefaultInfinity: true, maxLength: 10000,
noNaN: true, noDefaultInfinity: 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);
}
}) })
); );
}); });

View File

@ -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);

View File

@ -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}))`);

View File

@ -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,
} }

View File

@ -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"

View File

@ -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`

View 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",

View File

@ -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,85 +120,86 @@ function createTsExport(
x: expressionValue, x: expressionValue,
environment: environment environment: environment
): squiggleExpression { ): squiggleExpression {
switch (x.tag) { switch (x) {
case "EvArray": case "EvVoid":
// genType doesn't convert anything more than 2 layers down into {tag: x, value: x} return tag("void", "");
// format, leaving it as the raw values. This converts the raw values default: {
// directly into typescript values. switch (x.tag) {
// case "EvArray":
// The casting here is because genType is about the types of the returned // genType doesn't convert anything more than 2 layers down into {tag: x, value: x}
// values, claiming they are fully recursive when that's not actually the // format, leaving it as the raw values. This converts the raw values
// case // directly into typescript values.
return tag( //
"array", // The casting here is because genType is about the types of the returned
x.value.map((arrayItem): squiggleExpression => { // values, claiming they are fully recursive when that's not actually the
switch (arrayItem.tag) { // case
case "EvRecord": return tag(
return tag( "array",
"record", x.value.map(
_.mapValues(arrayItem.value, (recordValue: unknown) => (arrayItem): squiggleExpression =>
convertRawToTypescript( convertRawToTypescript(
recordValue as rescriptExport, arrayItem as unknown as rescriptExport,
environment environment
)
) )
); )
case "EvArray": );
let y = arrayItem.value as unknown as rescriptExport[]; case "EvArrayString":
return tag( return tag("arraystring", x.value);
"array", case "EvBool":
y.map((childArrayItem) => return tag("boolean", x.value);
convertRawToTypescript(childArrayItem, environment) case "EvCall":
) return tag("call", x.value);
); case "EvLambda":
default: return tag("lambda", x.value);
return createTsExport(arrayItem, environment); case "EvDistribution":
} return tag("distribution", new Distribution(x.value, environment));
}) case "EvNumber":
); return tag("number", x.value);
case "EvArrayString": case "EvRecord":
return tag("arraystring", x.value); // genType doesn't support records, so we have to do the raw conversion ourself
case "EvBool": let result: tagged<"record", { [key: string]: squiggleExpression }> =
return tag("boolean", x.value); tag(
case "EvCall": "record",
return tag("call", x.value); _.mapValues(x.value, (x: unknown) =>
case "EvLambda": convertRawToTypescript(x as rescriptExport, environment)
return tag("lambda", x.value); )
case "EvDistribution": );
return tag("distribution", new Distribution(x.value, environment)); return result;
case "EvNumber": case "EvString":
return tag("number", x.value); return tag("string", x.value);
case "EvRecord": case "EvSymbol":
// genType doesn't support records, so we have to do the raw conversion ourself return tag("symbol", x.value);
let result: tagged<"record", { [key: string]: squiggleExpression }> = tag( case "EvDate":
"record", return tag("date", x.value);
_.mapValues(x.value, (x: unknown) => case "EvTimeDuration":
convertRawToTypescript(x as rescriptExport, environment) return tag("timeDuration", x.value);
) case "EvDeclaration":
); return tag("lambdaDeclaration", x.value);
return result; case "EvTypeIdentifier":
case "EvString": return tag("typeIdentifier", x.value);
return tag("string", x.value); case "EvType":
case "EvSymbol": let typeResult: tagged<
return tag("symbol", x.value); "type",
case "EvDate": { [key: string]: squiggleExpression }
return tag("date", x.value); > = tag(
case "EvTimeDuration": "type",
return tag("timeDuration", x.value); _.mapValues(x.value, (x: unknown) =>
case "EvDeclaration": convertRawToTypescript(x as rescriptExport, environment)
return tag("lambdaDeclaration", x.value); )
case "EvTypeIdentifier": );
return tag("typeIdentifier", x.value); return typeResult;
case "EvModule": case "EvModule":
let moduleResult: tagged< let moduleResult: tagged<
"module", "module",
{ [key: string]: squiggleExpression } { [key: string]: squiggleExpression }
> = tag( > = tag(
"module", "module",
_.mapValues(x.value, (x: unknown) => _.mapValues(x.value, (x: unknown) =>
convertRawToTypescript(x as rescriptExport, environment) convertRawToTypescript(x as rescriptExport, environment)
) )
); );
return moduleResult; return moduleResult;
}
}
} }
} }

View 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 };
}
}

View File

@ -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 };

View File

@ -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

View File

@ -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
let distEstimateDistAnswer: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<float, error>
@genType
let distEstimateDistAnswerWithPrior: (
~env: GenericDist.env,
genericDist,
genericDist,
genericDist,
) => result<float, error>
@genType
let distEstimateScalarAnswer: (
~env: GenericDist.env,
genericDist,
float,
) => result<float, error>
@genType
let distEstimateScalarAnswerWithPrior: (
~env: GenericDist.env,
genericDist,
float,
genericDist,
) => result<float, error>
}
@genType @genType
let klDivergence: (~env: env, genericDist, genericDist) => result<float, error> let toPointSet: (~env: GenericDist.env, genericDist) => result<genericDist, error>
@genType @genType
let logScoreWithPointResolution: ( let toSampleSet: (~env: GenericDist.env, genericDist, int) => result<genericDist, error>
~env: env,
~prediction: genericDist,
~answer: float,
~prior: option<genericDist>,
) => result<float, error>
@genType @genType
let toPointSet: (~env: env, genericDist) => result<genericDist, error> let fromSamples: (~env: GenericDist.env, SampleSetDist.t) => result<genericDist, error>
@genType @genType
let toSampleSet: (~env: env, genericDist, int) => result<genericDist, error> let truncate: (
~env: GenericDist.env,
genericDist,
option<float>,
option<float>,
) => result<genericDist, error>
@genType @genType
let fromSamples: (~env: env, SampleSetDist.t) => result<genericDist, error> let inspect: (~env: GenericDist.env, genericDist) => result<genericDist, error>
@genType @genType
let truncate: (~env: env, genericDist, option<float>, option<float>) => result<genericDist, error> let toString: (~env: GenericDist.env, genericDist) => result<string, error>
@genType @genType
let inspect: (~env: env, genericDist) => result<genericDist, error> let toSparkline: (~env: GenericDist.env, genericDist, int) => result<string, error>
@genType @genType
let toString: (~env: env, genericDist) => result<string, error> let algebraicAdd: (~env: GenericDist.env, genericDist, genericDist) => result<genericDist, error>
@genType @genType
let toSparkline: (~env: env, genericDist, int) => result<string, error> let algebraicMultiply: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let algebraicAdd: (~env: env, genericDist, genericDist) => result<genericDist, error> let algebraicDivide: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let algebraicMultiply: (~env: env, genericDist, genericDist) => result<genericDist, error> let algebraicSubtract: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let algebraicDivide: (~env: env, genericDist, genericDist) => result<genericDist, error> let algebraicLogarithm: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let algebraicSubtract: (~env: env, genericDist, genericDist) => result<genericDist, error> let algebraicPower: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let algebraicLogarithm: (~env: env, genericDist, genericDist) => result<genericDist, error> let scaleLogarithm: (~env: GenericDist.env, genericDist, float) => result<genericDist, error>
@genType @genType
let algebraicPower: (~env: env, genericDist, genericDist) => result<genericDist, error> let scaleMultiply: (~env: GenericDist.env, genericDist, float) => result<genericDist, error>
@genType @genType
let scaleLogarithm: (~env: env, genericDist, float) => result<genericDist, error> let scalePower: (~env: GenericDist.env, genericDist, float) => result<genericDist, error>
@genType @genType
let scalePower: (~env: env, genericDist, float) => result<genericDist, error> let pointwiseAdd: (~env: GenericDist.env, genericDist, genericDist) => result<genericDist, error>
@genType @genType
let pointwiseAdd: (~env: env, genericDist, genericDist) => result<genericDist, error> let pointwiseMultiply: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let pointwiseMultiply: (~env: env, genericDist, genericDist) => result<genericDist, error> let pointwiseDivide: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let pointwiseDivide: (~env: env, genericDist, genericDist) => result<genericDist, error> let pointwiseSubtract: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let pointwiseSubtract: (~env: env, genericDist, genericDist) => result<genericDist, error> let pointwiseLogarithm: (
~env: GenericDist.env,
genericDist,
genericDist,
) => result<genericDist, error>
@genType @genType
let pointwiseLogarithm: (~env: env, genericDist, genericDist) => result<genericDist, error> let pointwisePower: (
@genType ~env: GenericDist.env,
let pointwisePower: (~env: env, genericDist, genericDist) => result<genericDist, error> genericDist,
genericDist,
) => result<genericDist, error>
} }

View File

@ -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,
) )
} }

View File

@ -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

View File

@ -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