Merge remote-tracking branch 'origin/develop' into scoring-cleanup-three

This commit is contained in:
Quinn Dougherty 2022-06-20 08:50:59 -04:00
commit f5366540f7
137 changed files with 9748 additions and 3291 deletions

View File

@ -100,7 +100,7 @@ jobs:
uses: creyD/prettier_action@v4.2 uses: creyD/prettier_action@v4.2
with: with:
dry: true dry: true
prettier_options: --check packages/components prettier_options: --check packages/components --ignore-path packages/components/.prettierignore
components-bundle-build: components-bundle-build:
name: Components bundle and build name: Components bundle and build
@ -154,5 +154,7 @@ jobs:
run: cd ../../ && yarn run: cd ../../ && yarn
- name: Build rescript in squiggle-lang - name: Build rescript in squiggle-lang
run: cd ../squiggle-lang && yarn build run: cd ../squiggle-lang && yarn build
- name: Build components
run: cd ../components && yarn build
- name: Build website assets - name: Build website assets
run: yarn build run: yarn build

4
packages/cli/.gitignore vendored Normal file
View File

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

20
packages/cli/README.md Normal file
View File

@ -0,0 +1,20 @@
## 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 Normal 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();

22
packages/cli/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"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.3.0",
"fs": "^0.0.1-security",
"glob": "^8.0.3",
"indent-string": "^5.0.0"
}
}

View File

@ -1,2 +1,4 @@
dist/ dist/
storybook-static storybook-static
src/styles/base.css
src/styles/forms.css

View File

@ -1,3 +1,15 @@
import "../src/styles/main.css";
import "!style-loader!css-loader!postcss-loader!../src/styles/main.css";
import { SquiggleContainer } from "../src/components/SquiggleContainer";
export const decorators = [
(Story) => (
<SquiggleContainer>
<Story />
</SquiggleContainer>
),
];
export const parameters = { export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" }, actions: { argTypesRegex: "^on[A-Z].*" },
controls: { controls: {

View File

@ -3,53 +3,71 @@
"version": "0.2.20", "version": "0.2.20",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@headlessui/react": "^1.6.4",
"@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^2.9.1",
"@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",
"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.32.0",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"react-vega": "^7.5.1", "react-vega": "^7.5.1",
"styled-components": "^5.3.5",
"vega": "^5.22.1", "vega": "^5.22.1",
"vega-embed": "^6.20.6", "vega-embed": "^6.20.6",
"vega-lite": "^5.2.0" "vega-lite": "^5.2.0",
"yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.17.12", "@babel/plugin-proposal-private-property-in-object": "^7.17.12",
"@storybook/addon-actions": "^6.5.3", "@storybook/addon-actions": "^6.5.8",
"@storybook/addon-essentials": "^6.5.4", "@storybook/addon-essentials": "^6.5.8",
"@storybook/addon-links": "^6.5.4", "@storybook/addon-links": "^6.5.8",
"@storybook/builder-webpack5": "^6.5.4", "@storybook/builder-webpack5": "^6.5.8",
"@storybook/manager-webpack5": "^6.5.4", "@storybook/manager-webpack5": "^6.5.8",
"@storybook/node-logger": "^6.5.4", "@storybook/node-logger": "^6.5.6",
"@storybook/preset-create-react-app": "^4.1.1", "@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.4", "@storybook/react": "^6.5.8",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.2.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0", "@testing-library/user-event": "^14.2.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.35", "@types/node": "^17.0.42",
"@types/react": "^18.0.9", "@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4", "@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",
"postcss-cli": "^9.1.0",
"postcss-import": "^14.1.0",
"postcss-loader": "^7.0.0",
"react": "^18.1.0",
"react-dom": "^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",
"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.6.3", "typescript": "^4.7.3",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"webpack": "^5.72.1", "webpack": "^5.73.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.0" "webpack-dev-server": "^4.9.2"
},
"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 && 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

@ -0,0 +1,9 @@
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
cssnano: {},
},
};

View File

@ -0,0 +1,86 @@
import * as React from "react";
import {
XCircleIcon,
InformationCircleIcon,
CheckCircleIcon,
} from "@heroicons/react/solid";
import clsx from "clsx";
export const Alert: React.FC<{
heading: string;
backgroundColor: string;
headingColor: string;
bodyColor: string;
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
iconColor: string;
children?: React.ReactNode;
}> = ({
heading = "Error",
backgroundColor,
headingColor,
bodyColor,
icon: Icon,
iconColor,
children,
}) => {
return (
<div className={clsx("rounded-md p-4", backgroundColor)}>
<div className="flex">
<Icon
className={clsx("h-5 w-5 flex-shrink-0", iconColor)}
aria-hidden="true"
/>
<div className="ml-3">
<header className={clsx("text-sm font-medium", headingColor)}>
{heading}
</header>
{children && React.Children.count(children) ? (
<div className={clsx("mt-2 text-sm", bodyColor)}>{children}</div>
) : null}
</div>
</div>
</div>
);
};
export const ErrorAlert: React.FC<{
heading: string;
children?: React.ReactNode;
}> = (props) => (
<Alert
{...props}
backgroundColor="bg-red-100"
headingColor="text-red-800"
bodyColor="text-red-700"
icon={XCircleIcon}
iconColor="text-red-400"
/>
);
export const MessageAlert: React.FC<{
heading: string;
children?: React.ReactNode;
}> = (props) => (
<Alert
{...props}
backgroundColor="bg-slate-100"
headingColor="text-slate-700"
bodyColor="text-slate-700"
icon={InformationCircleIcon}
iconColor="text-slate-400"
/>
);
export const SuccessAlert: React.FC<{
heading: string;
children?: React.ReactNode;
}> = (props) => (
<Alert
{...props}
backgroundColor="bg-green-50"
headingColor="text-green-800"
bodyColor="text-green-700"
icon={CheckCircleIcon}
iconColor="text-green-400"
/>
);

View File

@ -1,5 +1,5 @@
import _ from "lodash"; import _ from "lodash";
import React, { FC } from "react"; import React, { FC, useMemo } 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";
@ -14,21 +14,23 @@ interface CodeEditorProps {
showGutter?: boolean; showGutter?: boolean;
} }
export let CodeEditor: FC<CodeEditorProps> = ({ export const CodeEditor: FC<CodeEditorProps> = ({
value, value,
onChange, onChange,
oneLine = false, oneLine = false,
showGutter = false, showGutter = false,
height, height,
}: CodeEditorProps) => { }) => {
let lineCount = value.split("\n").length; const lineCount = value.split("\n").length;
let id = _.uniqueId(); const id = useMemo(() => _.uniqueId(), []);
return ( return (
<AceEditor <AceEditor
value={value} value={value}
mode="golang" mode="golang"
theme="github" theme="github"
width={"100%"} width="100%"
fontSize={14}
height={String(height) + "px"} height={String(height) + "px"}
minLines={oneLine ? lineCount : undefined} minLines={oneLine ? lineCount : undefined}
maxLines={oneLine ? lineCount : undefined} maxLines={oneLine ? lineCount : undefined}
@ -47,4 +49,3 @@ export let CodeEditor: FC<CodeEditorProps> = ({
/> />
); );
}; };
export default CodeEditor;

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash";
import { import {
Distribution, Distribution,
result, result,
@ -8,15 +7,16 @@ import {
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { Vega, VisualizationSpec } from "react-vega"; import { Vega, VisualizationSpec } from "react-vega";
import * as chartSpecification from "../vega-specs/spec-distributions.json"; import * as chartSpecification from "../vega-specs/spec-distributions.json";
import { ErrorBox } from "./ErrorBox"; import { ErrorAlert } from "./Alert";
import { useSize } from "react-use"; import { useSize } from "react-use";
import clsx from "clsx";
import { import {
linearXScale, linearXScale,
logXScale, logXScale,
linearYScale, linearYScale,
expYScale, expYScale,
} from "./DistributionVegaScales"; } from "./DistributionVegaScales";
import styled from "styled-components";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
type DistributionChartProps = { type DistributionChartProps = {
@ -35,38 +35,34 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
showSummary, showSummary,
width, width,
showControls = false, showControls = false,
}: DistributionChartProps) => { }) => {
let [isLogX, setLogX] = React.useState(false); const [isLogX, setLogX] = React.useState(false);
let [isExpY, setExpY] = React.useState(false); const [isExpY, setExpY] = React.useState(false);
let shape = distribution.pointSet(); const shape = distribution.pointSet();
const [sized, _] = useSize((size) => { const [sized] = useSize((size) => {
if (shape.tag === "Ok") { if (shape.tag === "Error") {
let massBelow0 = return (
<ErrorAlert heading="Distribution Error">
{distributionErrorToString(shape.value)}
</ErrorAlert>
);
}
const massBelow0 =
shape.value.continuous.some((x) => x.x <= 0) || shape.value.continuous.some((x) => x.x <= 0) ||
shape.value.discrete.some((x) => x.x <= 0); shape.value.discrete.some((x) => x.x <= 0);
let spec = buildVegaSpec(isLogX, isExpY); const spec = buildVegaSpec(isLogX, isExpY);
let widthProp = width ? width : size.width; let widthProp = width ? width : size.width;
if (widthProp < 20) {
// Check whether we should disable the checkbox console.warn(
var logCheckbox = ( `Width of Distribution is set to ${widthProp}, which is too small`
<CheckBox label="Log X scale" value={isLogX} onChange={setLogX} />
);
if (massBelow0) {
logCheckbox = (
<CheckBox
label="Log X scale"
value={isLogX}
onChange={setLogX}
disabled={true}
tooltip={
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
}
/>
); );
widthProp = 20;
} }
var result = ( return (
<ChartContainer width={widthProp + "px"}> <div style={{ width: widthProp }}>
<Vega <Vega
spec={spec} spec={spec}
data={{ con: shape.value.continuous, dis: shape.value.discrete }} data={{ con: shape.value.continuous, dis: shape.value.discrete }}
@ -74,34 +70,33 @@ export const DistributionChart: React.FC<DistributionChartProps> = ({
height={height} height={height}
actions={false} actions={false}
/> />
<div className="flex justify-center">
{showSummary && <SummaryTable distribution={distribution} />} {showSummary && <SummaryTable distribution={distribution} />}
</div>
{showControls && ( {showControls && (
<div> <div>
{logCheckbox} <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} /> <CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
</div> </div>
)} )}
</ChartContainer> </div>
); );
} else {
var result = (
<ErrorBox heading="Distribution Error">
{distributionErrorToString(shape.value)}
</ErrorBox>
);
}
return result;
}); });
return sized; return sized;
}; };
type ChartContainerProps = { width: string };
let ChartContainer = styled.div<ChartContainerProps>`
width: ${(props) => props.width};
`;
function buildVegaSpec(isLogX: boolean, isExpY: boolean): VisualizationSpec { function buildVegaSpec(isLogX: boolean, isExpY: boolean): VisualizationSpec {
return { return {
...chartSpecification, ...chartSpecification,
@ -120,17 +115,13 @@ interface CheckBoxProps {
tooltip?: string; tooltip?: string;
} }
const Label = styled.label<{ disabled: boolean }>` export const CheckBox: React.FC<CheckBoxProps> = ({
${(props) => props.disabled && "color: #999;"}
`;
export const CheckBox = ({
label, label,
onChange, onChange,
value, value,
disabled = false, disabled = false,
tooltip, tooltip,
}: CheckBoxProps) => { }) => {
return ( return (
<span title={tooltip}> <span title={tooltip}>
<input <input
@ -138,74 +129,65 @@ export const CheckBox = ({
value={value + ""} value={value + ""}
onChange={() => onChange(!value)} onChange={() => onChange(!value)}
disabled={disabled} disabled={disabled}
className="form-checkbox"
/> />
<Label disabled={disabled}>{label}</Label> <label className={clsx(disabled && "text-slate-400")}> {label}</label>
</span> </span>
); );
}; };
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<th className="border border-slate-200 bg-slate-50 py-1 px-2 text-slate-500 font-semibold">
{children}
</th>
);
const Cell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<td className="border border-slate-200 py-1 px-2 text-slate-900">
{children}
</td>
);
type SummaryTableProps = { type SummaryTableProps = {
distribution: Distribution; distribution: Distribution;
}; };
const Table = styled.table` const SummaryTable: React.FC<SummaryTableProps> = ({ distribution }) => {
margin-left: auto; const mean = distribution.mean();
margin-right: auto; const stdev = distribution.stdev();
border-collapse: collapse; const p5 = distribution.inv(0.05);
text-align: center; const p10 = distribution.inv(0.1);
border-style: hidden; const p25 = distribution.inv(0.25);
`; const p50 = distribution.inv(0.5);
const p75 = distribution.inv(0.75);
const p90 = distribution.inv(0.9);
const p95 = distribution.inv(0.95);
const TableHead = styled.thead` const hasResult = (x: result<number, distributionError>): boolean =>
border-bottom: 1px solid rgb(141 149 167); x.tag === "Ok";
`;
const TableHeadCell = styled.th` const unwrapResult = (
border-right: 1px solid rgb(141 149 167);
border-left: 1px solid rgb(141 149 167);
padding: 0.3em;
`;
const TableBody = styled.tbody``;
const Row = styled.tr``;
const Cell = styled.td`
padding: 0.3em;
border-right: 1px solid rgb(141 149 167);
border-left: 1px solid rgb(141 149 167);
`;
const SummaryTable: React.FC<SummaryTableProps> = ({
distribution,
}: SummaryTableProps) => {
let mean = distribution.mean();
let p5 = distribution.inv(0.05);
let p10 = distribution.inv(0.1);
let p25 = distribution.inv(0.25);
let p50 = distribution.inv(0.5);
let p75 = distribution.inv(0.75);
let p90 = distribution.inv(0.9);
let p95 = distribution.inv(0.95);
let unwrapResult = (
x: result<number, distributionError> x: result<number, distributionError>
): React.ReactNode => { ): React.ReactNode => {
if (x.tag === "Ok") { if (x.tag === "Ok") {
return <NumberShower number={x.value} />; return <NumberShower number={x.value} />;
} else { } else {
return ( return (
<ErrorBox heading="Distribution Error"> <ErrorAlert heading="Distribution Error">
{distributionErrorToString(x.value)} {distributionErrorToString(x.value)}
</ErrorBox> </ErrorAlert>
); );
} }
}; };
return ( return (
<Table> <table className="border border-collapse border-slate-400">
<TableHead> <thead className="bg-slate-50">
<Row> <tr>
<TableHeadCell>{"Mean"}</TableHeadCell> <TableHeadCell>{"Mean"}</TableHeadCell>
{hasResult(stdev) && <TableHeadCell>{"Stdev"}</TableHeadCell>}
<TableHeadCell>{"5%"}</TableHeadCell> <TableHeadCell>{"5%"}</TableHeadCell>
<TableHeadCell>{"10%"}</TableHeadCell> <TableHeadCell>{"10%"}</TableHeadCell>
<TableHeadCell>{"25%"}</TableHeadCell> <TableHeadCell>{"25%"}</TableHeadCell>
@ -213,11 +195,12 @@ const SummaryTable: React.FC<SummaryTableProps> = ({
<TableHeadCell>{"75%"}</TableHeadCell> <TableHeadCell>{"75%"}</TableHeadCell>
<TableHeadCell>{"90%"}</TableHeadCell> <TableHeadCell>{"90%"}</TableHeadCell>
<TableHeadCell>{"95%"}</TableHeadCell> <TableHeadCell>{"95%"}</TableHeadCell>
</Row> </tr>
</TableHead> </thead>
<TableBody> <tbody>
<Row> <tr>
<Cell>{unwrapResult(mean)}</Cell> <Cell>{unwrapResult(mean)}</Cell>
{hasResult(stdev) && <Cell>{unwrapResult(stdev)}</Cell>}
<Cell>{unwrapResult(p5)}</Cell> <Cell>{unwrapResult(p5)}</Cell>
<Cell>{unwrapResult(p10)}</Cell> <Cell>{unwrapResult(p10)}</Cell>
<Cell>{unwrapResult(p25)}</Cell> <Cell>{unwrapResult(p25)}</Cell>
@ -225,8 +208,8 @@ const SummaryTable: React.FC<SummaryTableProps> = ({
<Cell>{unwrapResult(p75)}</Cell> <Cell>{unwrapResult(p75)}</Cell>
<Cell>{unwrapResult(p90)}</Cell> <Cell>{unwrapResult(p90)}</Cell>
<Cell>{unwrapResult(p95)}</Cell> <Cell>{unwrapResult(p95)}</Cell>
</Row> </tr>
</TableBody> </tbody>
</Table> </table>
); );
}; };

View File

@ -1,20 +0,0 @@
import * as React from "react";
import styled from "styled-components";
const ShowError = styled.div`
border: 1px solid #792e2e;
background: #eee2e2;
padding: 0.4em 0.8em;
`;
export const ErrorBox: React.FC<{
heading: string;
children: React.ReactNode;
}> = ({ heading = "Error", children }) => {
return (
<ShowError>
<h3>{heading}</h3>
{children}
</ShowError>
);
};

View File

@ -1,40 +1,9 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash"; import { lambdaValue, environment, runForeign } from "@quri/squiggle-lang";
import type { Spec } from "vega"; import { FunctionChart1Dist } from "./FunctionChart1Dist";
import { import { FunctionChart1Number } from "./FunctionChart1Number";
Distribution, import { ErrorAlert, MessageAlert } from "./Alert";
result,
lambdaValue,
environment,
runForeign,
squiggleExpression,
errorValue,
errorValueToString,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
import { DistributionChart } from "./DistributionChart";
import { NumberShower } from "./NumberShower";
import { ErrorBox } from "./ErrorBox";
let SquigglePercentilesChart = createClassFromSpec({
spec: percentilesSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
function unwrap<a, b>(x: result<a, b>): a {
if (x.tag === "Ok") {
return x.value;
} else {
throw Error("FAILURE TO UNWRAP");
}
}
export type FunctionChartSettings = { export type FunctionChartSettings = {
start: number; start: number;
stop: number; stop: number;
@ -45,167 +14,66 @@ interface FunctionChartProps {
fn: lambdaValue; fn: lambdaValue;
chartSettings: FunctionChartSettings; chartSettings: FunctionChartSettings;
environment: environment; environment: environment;
height: number;
} }
type percentiles = {
x: number;
p1: number;
p5: number;
p10: number;
p20: number;
p30: number;
p40: number;
p50: number;
p60: number;
p70: number;
p80: number;
p90: number;
p95: number;
p99: number;
}[];
type errors = _.Dictionary<
{
x: number;
value: string;
}[]
>;
type point = { x: number; value: result<Distribution, string> };
let getPercentiles = ({ chartSettings, fn, environment }) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
if (result.value.tag == "distribution") {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value:
"Cannot currently render functions that don't return distributions",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: errorValueToString(result.value) },
};
}
});
let initialPartition: [
{ x: number; value: Distribution }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
let percentiles: percentiles = functionImage.map(({ x, value }) => {
// We convert it to to a pointSet distribution first, so that in case its a sample set
// distribution, it doesn't internally convert it to a pointSet distribution for every
// single inv() call.
let toPointSet: Distribution = unwrap(value.toPointSet());
return {
x: x,
p1: unwrap(toPointSet.inv(0.01)),
p5: unwrap(toPointSet.inv(0.05)),
p10: unwrap(toPointSet.inv(0.1)),
p20: unwrap(toPointSet.inv(0.2)),
p30: unwrap(toPointSet.inv(0.3)),
p40: unwrap(toPointSet.inv(0.4)),
p50: unwrap(toPointSet.inv(0.5)),
p60: unwrap(toPointSet.inv(0.6)),
p70: unwrap(toPointSet.inv(0.7)),
p80: unwrap(toPointSet.inv(0.8)),
p90: unwrap(toPointSet.inv(0.9)),
p95: unwrap(toPointSet.inv(0.95)),
p99: unwrap(toPointSet.inv(0.99)),
};
});
return { percentiles, errors: groupedErrors };
};
export const FunctionChart: React.FC<FunctionChartProps> = ({ export const FunctionChart: React.FC<FunctionChartProps> = ({
fn, fn,
chartSettings, chartSettings,
environment, environment,
}: FunctionChartProps) => { height,
let [mouseOverlay, setMouseOverlay] = React.useState(0); }) => {
function handleHover(_name: string, value: unknown) { if (fn.parameters.length > 1) {
setMouseOverlay(value as number);
}
function handleOut() {
setMouseOverlay(NaN);
}
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? runForeign(fn, [mouseOverlay], environment)
: {
tag: "Error",
value: {
tag: "REExpectedType",
value: "Hover x-coordinate returned NaN. Expected a number.",
},
};
let showChart =
mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? (
<DistributionChart
distribution={mouseItem.value.value}
width={400}
height={140}
showSummary={false}
/>
) : (
<></>
);
let getPercentilesMemoized = React.useMemo(
() => getPercentiles({ chartSettings, fn, environment }),
[environment, fn]
);
return ( return (
<> <MessageAlert heading="Function Display Not Supported">
<SquigglePercentilesChart Only functions with one parameter are displayed.
data={{ facet: getPercentilesMemoized.percentiles }} </MessageAlert>
actions={false}
signalListeners={signalListeners}
/>
{showChart}
{_.entries(getPercentilesMemoized.errors).map(
([errorName, errorPoints]) => (
<ErrorBox key={errorName} heading={errorName}>
Values:{" "}
{errorPoints
.map((r, i) => <NumberShower key={i} number={r.x} />)
.reduce((a, b) => (
<>
{a}, {b}
</>
))}
</ErrorBox>
)
)}
</>
); );
}
const result1 = runForeign(fn, [chartSettings.start], environment);
const result2 = runForeign(fn, [chartSettings.stop], environment);
const getValidResult = () => {
if (result1.tag === "Ok") {
return result1;
} else if (result2.tag === "Ok") {
return result2;
} else {
return result1;
}
};
const validResult = getValidResult();
const resultType =
validResult.tag === "Ok" ? validResult.value.tag : ("Error" as const);
switch (resultType) {
case "distribution":
return (
<FunctionChart1Dist
fn={fn}
chartSettings={chartSettings}
environment={environment}
height={height}
/>
);
case "number":
return (
<FunctionChart1Number
fn={fn}
chartSettings={chartSettings}
environment={environment}
height={height}
/>
);
case "Error":
return (
<ErrorAlert heading="Error">The function failed to be run</ErrorAlert>
);
default:
return (
<MessageAlert heading="Function Display Not Supported">
There is no function visualization for this type of output:{" "}
<span className="font-bold">{resultType}</span>
</MessageAlert>
);
}
}; };

View File

@ -0,0 +1,212 @@
import * as React from "react";
import _ from "lodash";
import type { Spec } from "vega";
import {
Distribution,
result,
lambdaValue,
environment,
runForeign,
squiggleExpression,
errorValue,
errorValueToString,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
import { DistributionChart } from "./DistributionChart";
import { NumberShower } from "./NumberShower";
import { ErrorAlert } from "./Alert";
let SquigglePercentilesChart = createClassFromSpec({
spec: percentilesSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
function unwrap<a, b>(x: result<a, b>): a {
if (x.tag === "Ok") {
return x.value;
} else {
throw Error("FAILURE TO UNWRAP");
}
}
export type FunctionChartSettings = {
start: number;
stop: number;
count: number;
};
interface FunctionChart1DistProps {
fn: lambdaValue;
chartSettings: FunctionChartSettings;
environment: environment;
height: number;
}
type percentiles = {
x: number;
p1: number;
p5: number;
p10: number;
p20: number;
p30: number;
p40: number;
p50: number;
p60: number;
p70: number;
p80: number;
p90: number;
p95: number;
p99: number;
}[];
type errors = _.Dictionary<
{
x: number;
value: string;
}[]
>;
type point = { x: number; value: result<Distribution, string> };
let getPercentiles = ({ chartSettings, fn, environment }) => {
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
chartSettings.count
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
if (result.value.tag == "distribution") {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value:
"Cannot currently render functions that don't return distributions",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: errorValueToString(result.value) },
};
}
});
let initialPartition: [
{ x: number; value: Distribution }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
let percentiles: percentiles = functionImage.map(({ x, value }) => {
// We convert it to to a pointSet distribution first, so that in case its a sample set
// distribution, it doesn't internally convert it to a pointSet distribution for every
// single inv() call.
let toPointSet: Distribution = unwrap(value.toPointSet());
return {
x: x,
p1: unwrap(toPointSet.inv(0.01)),
p5: unwrap(toPointSet.inv(0.05)),
p10: unwrap(toPointSet.inv(0.1)),
p20: unwrap(toPointSet.inv(0.2)),
p30: unwrap(toPointSet.inv(0.3)),
p40: unwrap(toPointSet.inv(0.4)),
p50: unwrap(toPointSet.inv(0.5)),
p60: unwrap(toPointSet.inv(0.6)),
p70: unwrap(toPointSet.inv(0.7)),
p80: unwrap(toPointSet.inv(0.8)),
p90: unwrap(toPointSet.inv(0.9)),
p95: unwrap(toPointSet.inv(0.95)),
p99: unwrap(toPointSet.inv(0.99)),
};
});
return { percentiles, errors: groupedErrors };
};
export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
fn,
chartSettings,
environment,
height,
}) => {
let [mouseOverlay, setMouseOverlay] = React.useState(0);
function handleHover(_name: string, value: unknown) {
setMouseOverlay(value as number);
}
function handleOut() {
setMouseOverlay(NaN);
}
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? runForeign(fn, [mouseOverlay], environment)
: {
tag: "Error",
value: {
tag: "REExpectedType",
value: "Hover x-coordinate returned NaN. Expected a number.",
},
};
let showChart =
mouseItem.tag === "Ok" && mouseItem.value.tag === "distribution" ? (
<DistributionChart
distribution={mouseItem.value.value}
width={400}
height={50}
showSummary={false}
/>
) : null;
let getPercentilesMemoized = React.useMemo(
() => getPercentiles({ chartSettings, fn, environment }),
[environment, fn]
);
return (
<>
<SquigglePercentilesChart
data={{ facet: getPercentilesMemoized.percentiles }}
height={height}
actions={false}
signalListeners={signalListeners}
/>
{showChart}
{_.entries(getPercentilesMemoized.errors).map(
([errorName, errorPoints]) => (
<ErrorAlert key={errorName} heading={errorName}>
Values:{" "}
{errorPoints
.map((r, i) => <NumberShower key={i} number={r.x} />)
.reduce((a, b) => (
<>
{a}, {b}
</>
))}
</ErrorAlert>
)
)}
</>
);
};

View File

@ -0,0 +1,119 @@
import * as React from "react";
import _ from "lodash";
import type { Spec } from "vega";
import {
result,
lambdaValue,
environment,
runForeign,
errorValueToString,
} from "@quri/squiggle-lang";
import { createClassFromSpec } from "react-vega";
import * as lineChartSpec from "../vega-specs/spec-line-chart.json";
import { ErrorAlert } from "./Alert";
let SquiggleLineChart = createClassFromSpec({
spec: lineChartSpec as Spec,
});
const _rangeByCount = (start: number, stop: number, count: number) => {
const step = (stop - start) / (count - 1);
const items = _.range(start, stop, step);
const result = items.concat([stop]);
return result;
};
export type FunctionChartSettings = {
start: number;
stop: number;
count: number;
};
interface FunctionChart1NumberProps {
fn: lambdaValue;
chartSettings: FunctionChartSettings;
environment: environment;
height: number;
}
type point = { x: number; value: result<number, string> };
let getFunctionImage = ({ chartSettings, fn, environment }) => {
//We adjust the count, because the count is made for distributions, which are much more expensive to estimate
let adjustedCount = chartSettings.count * 20;
let chartPointsToRender = _rangeByCount(
chartSettings.start,
chartSettings.stop,
adjustedCount
);
let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") {
if (result.value.tag == "number") {
return { x, value: { tag: "Ok", value: result.value.value } };
} else {
return {
x,
value: {
tag: "Error",
value: "This component expected number outputs",
},
};
}
} else {
return {
x,
value: { tag: "Error", value: errorValueToString(result.value) },
};
}
});
let initialPartition: [
{ x: number; value: number }[],
{ x: number; value: string }[]
] = [[], []];
let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
if (current.value.tag === "Ok") {
acc[0].push({ x: current.x, value: current.value.value });
} else {
acc[1].push({ x: current.x, value: current.value.value });
}
return acc;
}, initialPartition);
return { errors, functionImage };
};
export const FunctionChart1Number: React.FC<FunctionChart1NumberProps> = ({
fn,
chartSettings,
environment,
height,
}: FunctionChart1NumberProps) => {
let getFunctionImageMemoized = React.useMemo(
() => getFunctionImage({ chartSettings, fn, environment }),
[environment, fn]
);
let data = getFunctionImageMemoized.functionImage.map(({ x, value }) => ({
x,
y: value,
}));
return (
<>
<SquiggleLineChart
data={{ facet: data }}
height={height}
actions={false}
/>
{getFunctionImageMemoized.errors.map(({ x, value }) => (
<ErrorAlert key={x} heading={value}>
Error at point ${x}
</ErrorAlert>
))}
</>
);
};

View File

@ -0,0 +1,49 @@
import _ from "lodash";
import React, { FC } from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
oneLine?: boolean;
width?: number;
height: number;
showGutter?: boolean;
}
export const JsonEditor: FC<CodeEditorProps> = ({
value,
onChange,
oneLine = false,
showGutter = false,
height,
}) => {
const lineCount = value.split("\n").length;
const id = _.uniqueId();
return (
<AceEditor
value={value}
mode="json"
theme="github"
width={"100%"}
height={String(height) + "px"}
minLines={oneLine ? lineCount : undefined}
maxLines={oneLine ? lineCount : undefined}
showGutter={showGutter}
highlightActiveLine={false}
showPrintMargin={false}
onChange={onChange}
name={id}
editorProps={{
$blockScrolling: true,
}}
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
}}
/>
);
};

View File

@ -1,5 +1,4 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash";
const orderOfMagnitudeNum = (n: number) => { const orderOfMagnitudeNum = (n: number) => {
return Math.pow(10, n); return Math.pow(10, n);
@ -74,25 +73,23 @@ export interface NumberShowerProps {
precision?: number; precision?: number;
} }
export let NumberShower: React.FC<NumberShowerProps> = ({ export const NumberShower: React.FC<NumberShowerProps> = ({
number, number,
precision = 2, precision = 2,
}: NumberShowerProps) => { }) => {
let numberWithPresentation = numberShow(number, precision); const numberWithPresentation = numberShow(number, precision);
return ( return (
<span> <span>
{numberWithPresentation.value} {numberWithPresentation.value}
{numberWithPresentation.symbol} {numberWithPresentation.symbol}
{numberWithPresentation.power ? ( {numberWithPresentation.power ? (
<span> <span>
{"\u00b710"} {"\u00b7" /* dot symbol */}10
<span style={{ fontSize: "0.6em", verticalAlign: "super" }}> <span style={{ fontSize: "0.6em", verticalAlign: "super" }}>
{numberWithPresentation.power} {numberWithPresentation.power}
</span> </span>
</span> </span>
) : ( ) : null}
<></>
)}
</span> </span>
); );
}; };

View File

@ -1,9 +1,5 @@
import * as React from "react"; import * as React from "react";
import _ from "lodash";
import styled from "styled-components";
import { import {
run,
errorValueToString,
squiggleExpression, squiggleExpression,
bindings, bindings,
environment, environment,
@ -12,209 +8,10 @@ import {
defaultBindings, defaultBindings,
defaultEnvironment, defaultEnvironment,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower"; import { FunctionChartSettings } from "./FunctionChart";
import { DistributionChart } from "./DistributionChart"; import { useSquiggle } from "../lib/hooks";
import { ErrorBox } from "./ErrorBox"; import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; import { SquiggleItem } from "./SquiggleItem";
const variableBox = {
Component: styled.div`
background: white;
border: 1px solid #eee;
border-radius: 2px;
margin-bottom: 0.4em;
`,
Heading: styled.div`
border-bottom: 1px solid #eee;
padding-left: 0.8em;
padding-right: 0.8em;
padding-top: 0.1em;
`,
Body: styled.div`
padding: 0.4em 0.8em;
`,
};
interface VariableBoxProps {
heading: string;
children: React.ReactNode;
showTypes: boolean;
}
export const VariableBox: React.FC<VariableBoxProps> = ({
heading = "Error",
children,
showTypes = false,
}: VariableBoxProps) => {
if (showTypes) {
return (
<variableBox.Component>
<variableBox.Heading>
<h3>{heading}</h3>
</variableBox.Heading>
<variableBox.Body>{children}</variableBox.Body>
</variableBox.Component>
);
} else {
return <div>{children}</div>;
}
};
let RecordKeyHeader = styled.h3``;
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,
}: SquiggleItemProps) => {
switch (expression.tag) {
case "number":
return (
<VariableBox heading="Number" showTypes={showTypes}>
<NumberShower precision={3} number={expression.value} />
</VariableBox>
);
case "distribution": {
let distType = expression.value.type();
return (
<VariableBox
heading={`Distribution (${distType})`}
showTypes={showTypes}
>
{distType === "Symbolic" && showTypes ? (
<>
<div>{expression.value.toString()}</div>
</>
) : (
<></>
)}
<DistributionChart
distribution={expression.value}
height={height}
width={width}
showSummary={showSummary}
showControls={showControls}
/>
</VariableBox>
);
}
case "string":
return (
<VariableBox
heading="String"
showTypes={showTypes}
>{`"${expression.value}"`}</VariableBox>
);
case "boolean":
return (
<VariableBox heading="Boolean" showTypes={showTypes}>
{expression.value.toString()}
</VariableBox>
);
case "symbol":
return (
<VariableBox heading="Symbol" showTypes={showTypes}>
{expression.value}
</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) => (
<SquiggleItem
key={i}
expression={r}
width={width !== undefined ? width - 20 : width}
height={50}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
showSummary={showSummary}
/>
))}
</VariableBox>
);
case "record":
return (
<VariableBox heading="Record" showTypes={showTypes}>
{Object.entries(expression.value).map(([key, r]) => (
<div key={key}>
<RecordKeyHeader>{key}</RecordKeyHeader>
<SquiggleItem
expression={r}
width={width !== undefined ? width - 20 : width}
height={50}
showTypes={showTypes}
showSummary={showSummary}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/>
</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 (
<FunctionChart
fn={expression.value}
chartSettings={chartSettings}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
);
}
};
export interface SquiggleChartProps { export interface SquiggleChartProps {
/** The input string for squiggle */ /** The input string for squiggle */
@ -225,8 +22,8 @@ export interface SquiggleChartProps {
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 starts, ends and the amount of stops */
chartSettings?: FunctionChartSettings; chartSettings?: FunctionChartSettings;
/** When the environment changes */ /** When the squiggle code gets reevaluated */
onChange?(expr: squiggleExpression): void; onChange?(expr: squiggleExpression | undefined): void;
/** CSS width of the element */ /** CSS width of the element */
width?: number; width?: number;
height?: number; height?: number;
@ -234,7 +31,7 @@ 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 */ /** Whether to show type information about returns, default false */
showTypes?: boolean; showTypes?: boolean;
@ -242,19 +39,14 @@ export interface SquiggleChartProps {
showControls?: boolean; showControls?: boolean;
} }
const ChartWrapper = styled.div` const defaultOnChange = () => {};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, const defaultChartSettings = { start: 0, stop: 10, count: 20 };
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
`;
let defaultChartSettings = { start: 0, stop: 10, count: 20 };
export const SquiggleChart: React.FC<SquiggleChartProps> = ({ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "", squiggleString = "",
environment, environment,
onChange = () => {}, onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 60, height = 200,
bindings = defaultBindings, bindings = defaultBindings,
jsImports = defaultImports, jsImports = defaultImports,
showSummary = false, showSummary = false,
@ -262,31 +54,29 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
showTypes = false, showTypes = false,
showControls = false, showControls = false,
chartSettings = defaultChartSettings, chartSettings = defaultChartSettings,
}: SquiggleChartProps) => { }) => {
let expressionResult = run(squiggleString, bindings, environment, jsImports); const { result } = useSquiggle({
let e = environment ? environment : defaultEnvironment; code: squiggleString,
let internal: JSX.Element; bindings,
if (expressionResult.tag === "Ok") { environment,
let expression = expressionResult.value; jsImports,
onChange(expression); onChange,
internal = ( });
if (result.tag !== "Ok") {
return <SquiggleErrorAlert error={result.value} />;
}
return (
<SquiggleItem <SquiggleItem
expression={expression} expression={result.value}
width={width} width={width}
height={height} height={height}
showSummary={showSummary} showSummary={showSummary}
showTypes={showTypes} showTypes={showTypes}
showControls={showControls} showControls={showControls}
chartSettings={chartSettings} chartSettings={chartSettings}
environment={e} environment={environment ?? defaultEnvironment}
/> />
); );
} else {
internal = (
<ErrorBox heading={"Parse Error"}>
{errorValueToString(expressionResult.value)}
</ErrorBox>
);
}
return <ChartWrapper>{internal}</ChartWrapper>;
}; };

View File

@ -0,0 +1,25 @@
import React, { useContext } from "react";
type Props = {
children: React.ReactNode;
};
type SquiggleContextShape = {
containerized: boolean;
};
const SquiggleContext = React.createContext<SquiggleContextShape>({
containerized: false,
});
export const SquiggleContainer: React.FC<Props> = ({ children }) => {
const context = useContext(SquiggleContext);
if (context.containerized) {
return <>{children}</>;
} else {
return (
<SquiggleContext.Provider value={{ containerized: true }}>
<div className="squiggle">{children}</div>
</SquiggleContext.Provider>
);
}
};

View File

@ -1,39 +1,51 @@
import * as React from "react"; import React, { useState } from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import styled from "styled-components"; import {
import type {
squiggleExpression, squiggleExpression,
environment, environment,
bindings, bindings,
jsImports, jsImports,
defaultEnvironment,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { import { defaultImports, defaultBindings } from "@quri/squiggle-lang";
runPartial, import { SquiggleContainer } from "./SquiggleContainer";
errorValueToString, import { useSquiggle, useSquigglePartial } from "../lib/hooks";
defaultImports, import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
defaultBindings, import { SquiggleItem } from "./SquiggleItem";
} from "@quri/squiggle-lang";
import { ErrorBox } from "./ErrorBox"; const WrappedCodeEditor: React.FC<{
code: string;
setCode: (code: string) => void;
}> = ({ code, setCode }) => (
<div className="border border-grey-200 p-2 m-4">
<CodeEditor
value={code}
onChange={setCode}
oneLine={true}
showGutter={false}
height={20}
/>
</div>
);
export interface SquiggleEditorProps { export interface SquiggleEditorProps {
/** The input string for squiggle */ /** The input string for squiggle */
initialSquiggleString?: string; initialSquiggleString?: string;
/** If the output requires monte carlo sampling, the amount of samples */ /** The width of the element */
environment?: environment; width?: number;
/** If the result is a function, where the function starts */ /** If the result is a function, where the function starts */
diagramStart?: number; diagramStart?: number;
/** If the result is a function, where the function ends */ /** If the result is a function, where the function ends */
diagramStop?: number; diagramStop?: number;
/** If the result is a function, how many points along the function it samples */ /** If the result is a function, how many points along the function it samples */
diagramCount?: number; diagramCount?: number;
/** when the environment changes. Used again for notebook magic*/ /** When the environment changes. Used again for notebook magic */
onChange?(expr: squiggleExpression): void; onChange?(expr: squiggleExpression | undefined): void;
/** The width of the element */
width?: number;
/** Previous variable declarations */ /** Previous variable declarations */
bindings?: bindings; bindings?: bindings;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** JS Imports */ /** JS Imports */
jsImports?: jsImports; jsImports?: jsImports;
/** Whether to show detail about types of the returns, default false */ /** Whether to show detail about types of the returns, default false */
@ -44,169 +56,109 @@ export interface SquiggleEditorProps {
showSummary?: boolean; showSummary?: boolean;
} }
const Input = styled.div` export const SquiggleEditor: React.FC<SquiggleEditorProps> = ({
border: 1px solid #ddd;
padding: 0.3em 0.3em;
margin-bottom: 1em;
`;
export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
width, width,
environment,
diagramStart = 0, diagramStart = 0,
diagramStop = 10, diagramStop = 10,
diagramCount = 20, diagramCount = 20,
onChange, onChange,
bindings = defaultBindings, bindings = defaultBindings,
environment,
jsImports = defaultImports, jsImports = defaultImports,
showTypes = false, showTypes = false,
showControls = false, showControls = false,
showSummary = false, showSummary = false,
}: SquiggleEditorProps) => { }: SquiggleEditorProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString); const [code, setCode] = useState(initialSquiggleString);
let chartSettings = {
const { result, observableRef } = useSquiggle({
code,
bindings,
environment,
jsImports,
onChange,
});
const chartSettings = {
start: diagramStart, start: diagramStart,
stop: diagramStop, stop: diagramStop,
count: diagramCount, count: diagramCount,
}; };
return ( return (
<div> <div ref={observableRef}>
<Input> <SquiggleContainer>
<CodeEditor <WrappedCodeEditor code={code} setCode={setCode} />
value={expression} {result.tag === "Ok" ? (
onChange={setExpression} <SquiggleItem
oneLine={true} expression={result.value}
showGutter={false}
height={20}
/>
</Input>
<SquiggleChart
width={width} width={width}
environment={environment} height={200}
squiggleString={expression} showSummary={showSummary}
chartSettings={chartSettings}
onChange={onChange}
bindings={bindings}
jsImports={jsImports}
showTypes={showTypes} showTypes={showTypes}
showControls={showControls} showControls={showControls}
showSummary={showSummary} chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
/> />
) : (
<SquiggleErrorAlert error={result.value} />
)}
</SquiggleContainer>
</div> </div>
); );
}; };
export function renderSquiggleEditorToDom(props: SquiggleEditorProps) { export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
let parent = document.createElement("div"); const parent = document.createElement("div");
ReactDOM.render( ReactDOM.render(<SquiggleEditor {...props} />, parent);
<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; return parent;
} }
export interface SquigglePartialProps { export interface SquigglePartialProps {
/** The input string for squiggle */ /** The input string for squiggle */
initialSquiggleString?: string; initialSquiggleString?: string;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** If the result is a function, where the function starts */
diagramStart?: number;
/** If the result is a function, where the function ends */
diagramStop?: number;
/** If the result is a function, how many points along the function it samples */
diagramCount?: number;
/** when the environment changes. Used again for notebook magic*/ /** when the environment changes. Used again for notebook magic*/
onChange?(expr: bindings): void; onChange?(expr: bindings | undefined): 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 = "", initialSquiggleString = "",
onChange, onChange,
bindings = defaultBindings, bindings = defaultBindings,
environment, environment,
jsImports = defaultImports, jsImports = defaultImports,
}: SquigglePartialProps) => { }: SquigglePartialProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString); const [code, setCode] = useState(initialSquiggleString);
let [error, setError] = React.useState<string | null>(null);
let runSquiggleAndUpdateBindings = () => { const { result, observableRef } = useSquigglePartial({
let 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 (
<div> <div ref={observableRef}>
<Input> <SquiggleContainer>
<CodeEditor <WrappedCodeEditor code={code} setCode={setCode} />
value={expression} {result.tag !== "Ok" ? (
onChange={setExpression} <SquiggleErrorAlert error={result.value} />
oneLine={true} ) : null}
showGutter={false} </SquiggleContainer>
height={20}
/>
</Input>
{error !== null ? <ErrorBox heading="Error">{error}</ErrorBox> : <></>}
</div> </div>
); );
}; };
export function renderSquigglePartialToDom(props: SquigglePartialProps) { export function renderSquigglePartialToDom(props: SquigglePartialProps) {
let parent = document.createElement("div"); const parent = document.createElement("div");
ReactDOM.render( ReactDOM.render(<SquigglePartial {...props} />, parent);
<SquigglePartial
{...props}
onChange={(bindings) => {
// @ts-ignore
parent.value = bindings;
parent.dispatchEvent(new CustomEvent("input"));
if (props.onChange) props.onChange(bindings);
}}
/>,
parent
);
return parent; return parent;
} }

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

@ -0,0 +1,262 @@
import * as React from "react";
import {
squiggleExpression,
environment,
declaration,
} from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower";
import { DistributionChart } from "./DistributionChart";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart";
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,
};
}
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;
}
export 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": {
const 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>
);
}
case "module": {
return (
<VariableBox heading="Module" showTypes={showTypes}>
<span className="text-slate-600 font-semibold">Internal Module</span>
</VariableBox>
);
}
default: {
return (
<div>
<span>No display for type: </span>{" "}
<span className="font-semibold text-slate-600">{expression.tag}</span>
</div>
);
}
}
};

View File

@ -1,78 +1,25 @@
import _ from "lodash"; import React, { FC, Fragment, useState } from "react";
import React, { FC, ReactElement, useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart"; import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
import CodeEditor from "./CodeEditor"; import * as yup from "yup";
import styled from "styled-components"; import { yupResolver } from "@hookform/resolvers/yup";
import { Tab } from "@headlessui/react";
import { import {
defaultBindings, ChartSquareBarIcon,
environment, CodeIcon,
defaultImports, CogIcon,
} from "@quri/squiggle-lang"; CurrencyDollarIcon,
EyeIcon,
} from "@heroicons/react/solid";
import clsx from "clsx";
interface FieldFloatProps { import { defaultBindings, environment } from "@quri/squiggle-lang";
label: string;
className?: string;
value: number;
onChange: (value: number) => void;
}
const Input = styled.input``; import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor";
const FormItem = (props: { label: string; children: ReactElement }) => ( import { JsonEditor } from "./JsonEditor";
<div> import { ErrorAlert, SuccessAlert } from "./Alert";
<label>{props.label}</label> import { SquiggleContainer } from "./SquiggleContainer";
{props.children}
</div>
);
function FieldFloat(Props: FieldFloatProps) {
let [contents, setContents] = useState(Props.value + "");
return (
<FormItem label={Props.label}>
<Input
value={contents}
className={Props.className ? Props.className : ""}
onChange={(e) => {
setContents(e.target.value);
let result = parseFloat(contents);
if (_.isFinite(result)) {
Props.onChange(result);
}
}}
/>
</FormItem>
);
}
interface ShowBoxProps {
height: number;
}
const ShowBox = styled.div<ShowBoxProps>`
border: 1px solid #eee;
border-radius: 2px;
height: ${(props) => props.height};
`;
interface TitleProps {
readonly maxHeight: number;
}
const Display = styled.div<TitleProps>`
background: #f6f6f6;
border-left: 1px solid #eee;
height: 100vh;
padding: 3px;
overflow-y: auto;
max-height: ${(props) => props.maxHeight}px;
`;
const Row = styled.div`
display: grid;
grid-template-columns: 50% 50%;
`;
const Col = styled.div``;
interface PlaygroundProps { interface PlaygroundProps {
/** The initial squiggle string to put in the playground */ /** The initial squiggle string to put in the playground */
@ -85,65 +32,442 @@ interface PlaygroundProps {
showControls?: boolean; showControls?: boolean;
/** Whether to show the summary table in the playground */ /** Whether to show the summary table in the playground */
showSummary?: boolean; showSummary?: boolean;
/** If code is set, component becomes controlled */
code?: string;
onCodeChange?(expr: string): void;
/** Should we show the editor? */
showEditor?: boolean;
} }
let SquigglePlayground: FC<PlaygroundProps> = ({ const schema = yup
.object()
.shape({
sampleCount: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.max(1000000),
xyPointLength: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.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(),
showEditor: 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();
type StyledTabProps = {
name: string;
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
};
const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => {
return (
<Tab key={name} as={Fragment}>
{({ selected }) => (
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
<span
className={clsx(
"p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium",
selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5"
)}
>
<Icon
className={clsx(
"-ml-0.5 mr-2 h-4 w-4",
selected
? "text-slate-500"
: "text-gray-400 group-hover:text-gray-900"
)}
/>
<span
className={clsx(
selected
? "text-gray-900"
: "text-gray-600 group-hover:text-gray-900"
)}
>
{name}
</span>
</span>
</button>
)}
</Tab>
);
};
const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({
title,
children,
}) => (
<div>
<header className="text-lg leading-6 font-medium text-gray-900">
{title}
</header>
<div className="mt-4">{children}</div>
</div>
);
const Text: FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);
function InputItem<T>({
name,
label,
type,
register,
}: {
name: Path<T>;
label: string;
type: "number";
register: UseFormRegister<T>;
}) {
return (
<label className="block">
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input
type={type}
{...register(name)}
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</label>
);
}
function Checkbox<T>({
name,
label,
register,
}: {
name: Path<T>;
label: string;
register: UseFormRegister<T>;
}) {
return (
<label className="flex items-center">
<input
type="checkbox"
{...register(name)}
className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */}
<div className="ml-3 text-sm font-medium text-gray-700">{label}</div>
</label>
);
}
export const SquigglePlayground: FC<PlaygroundProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
height = 300, height = 500,
showTypes = false, showTypes = false,
showControls = false, showControls = false,
showSummary = false, showSummary = false,
}: PlaygroundProps) => { code: controlledCode,
let [squiggleString, setSquiggleString] = useState(initialSquiggleString); onCodeChange,
let [sampleCount, setSampleCount] = useState(1000); showEditor = true,
let [outputXYPoints, setOutputXYPoints] = useState(1000); }) => {
let [pointDistLength, setPointDistLength] = useState(1000); const [uncontrolledCode, setUncontrolledCode] = useState(
let [diagramStart, setDiagramStart] = useState(0); initialSquiggleString
let [diagramStop, setDiagramStop] = useState(10); );
let [diagramCount, setDiagramCount] = useState(20); const [importString, setImportString] = useState("{}");
let chartSettings = { const [imports, setImports] = useState({});
start: diagramStart, const [importsAreValid, setImportsAreValid] = useState(true);
stop: diagramStop, const { register, control } = useForm({
count: diagramCount, resolver: yupResolver(schema),
defaultValues: {
sampleCount: 1000,
xyPointLength: 1000,
chartHeight: 150,
showTypes: showTypes,
showControls: showControls,
showSummary: showSummary,
showEditor: showEditor,
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),
}; };
let env: environment = { const env: environment = {
sampleCount: sampleCount, sampleCount: Number(vars.sampleCount),
xyPointLength: outputXYPoints, xyPointLength: Number(vars.xyPointLength),
}; };
return ( const getChangeJson = (r: string) => {
<ShowBox height={height}> setImportString(r);
<Row> try {
<Col> setImports(JSON.parse(r));
<CodeEditor setImportsAreValid(true);
value={squiggleString} } catch (e) {
onChange={setSquiggleString} setImportsAreValid(false);
}
};
const code = controlledCode ?? uncontrolledCode;
const samplingSettings = (
<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">
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
<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">
<HeadedSection title="Import Variables from JSON">
<div className="space-y-6">
<Text>
You can import variables from JSON into your Squiggle code.
Variables are accessed with dollar signs. For example, "timeNow"
would be accessed as "$timeNow".
</Text>
<div className="border border-slate-200 mt-6 mb-2">
<JsonEditor
value={importString}
onChange={getChangeJson}
oneLine={false} oneLine={false}
showGutter={true} showGutter={true}
height={height - 3} height={150}
/> />
</Col> </div>
<Col> <div className="p-1 pt-2">
<Display maxHeight={height - 3}> {importsAreValid ? (
<SuccessAlert heading="Valid JSON" />
) : (
<ErrorAlert heading="Invalid JSON">
You must use valid JSON in this editor.
</ErrorAlert>
)}
</div>
</div>
</HeadedSection>
</div>
);
const squiggleChart = (
<SquiggleChart <SquiggleChart
squiggleString={squiggleString} squiggleString={code}
environment={env} environment={env}
chartSettings={chartSettings} chartSettings={chartSettings}
height={150} height={vars.chartHeight}
showTypes={showTypes} showTypes={vars.showTypes}
showControls={showControls} showControls={vars.showControls}
showSummary={vars.showSummary}
bindings={defaultBindings} bindings={defaultBindings}
jsImports={defaultImports} jsImports={imports}
showSummary={showSummary}
/> />
</Display> );
</Col>
</Row> const firstTab = vars.showEditor ? (
</ShowBox> <div className="border border-slate-200">
<CodeEditor
value={code}
onChange={(newCode) => {
if (controlledCode === undefined) {
// uncontrolled mode
setUncontrolledCode(newCode);
}
onCodeChange?.(newCode);
}}
oneLine={false}
showGutter={true}
height={height - 1}
/>
</div>
) : (
squiggleChart
);
const tabs = (
<Tab.Panels>
<Tab.Panel>{firstTab}</Tab.Panel>
<Tab.Panel>{samplingSettings}</Tab.Panel>
<Tab.Panel>{viewSettings}</Tab.Panel>
<Tab.Panel>{inputVariableSettings}</Tab.Panel>
</Tab.Panels>
);
const withEditor = (
<div className="flex mt-1">
<div className="w-1/2">{tabs}</div>
<div className="w-1/2 p-2 pl-4">{squiggleChart}</div>
</div>
);
const withoutEditor = <div className="mt-3">{tabs}</div>;
return (
<SquiggleContainer>
<Tab.Group>
<div className="pb-4">
<Tab.List className="flex w-fit p-0.5 mt-2 rounded-md bg-slate-100 hover:bg-slate-200">
<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} />
</Tab.List>
{vars.showEditor ? withEditor : withoutEditor}
</div>
</Tab.Group>
</SquiggleContainer>
); );
}; };
export default SquigglePlayground;
export function renderSquigglePlaygroundToDom(props: PlaygroundProps) { export function renderSquigglePlaygroundToDom(props: PlaygroundProps) {
let parent = document.createElement("div"); const parent = document.createElement("div");
ReactDOM.render(<SquigglePlayground {...props} />, parent); ReactDOM.render(<SquigglePlayground {...props} />, parent);
return parent; return parent;
} }

View File

@ -5,9 +5,10 @@ export {
renderSquiggleEditorToDom, renderSquiggleEditorToDom,
renderSquigglePartialToDom, renderSquigglePartialToDom,
} from "./components/SquiggleEditor"; } from "./components/SquiggleEditor";
import SquigglePlayground, { export {
SquigglePlayground,
renderSquigglePlaygroundToDom, renderSquigglePlaygroundToDom,
} from "./components/SquigglePlayground"; } from "./components/SquigglePlayground";
export { SquigglePlayground, renderSquigglePlaygroundToDom }; export { SquiggleContainer } from "./components/SquiggleContainer";
export { mergeBindings } from "@quri/squiggle-lang"; export { mergeBindings } from "@quri/squiggle-lang";

View File

@ -0,0 +1,63 @@
import {
bindings,
environment,
jsImports,
run,
runPartial,
} from "@quri/squiggle-lang";
import { useEffect, useMemo, useRef } from "react";
type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
code: string;
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
) => {
// We're using observable, where div elements can have a `value` property:
// https://observablehq.com/@observablehq/introduction-to-views
//
// This is here to get the 'viewof' part of:
// viewof env = cell('normal(0,1)')
// to work
const ref = useRef<
HTMLDivElement & { value?: Extract<T, { tag: "Ok" }>["value"] }
>(null);
const result: T = useMemo<T>(
() => f(args.code, args.bindings, args.environment, args.jsImports),
[f, args.code, args.bindings, args.environment, args.jsImports]
);
useEffect(() => {
if (!ref.current) return;
ref.current.value = result.tag === "Ok" ? result.value : undefined;
ref.current.dispatchEvent(new CustomEvent("input"));
}, [result]);
const { onChange } = args;
useEffect(() => {
onChange?.(result.tag === "Ok" ? result.value : undefined);
}, [result, onChange]);
return {
result, // squiggleExpression or externalBindings
observableRef: ref, // can be passed to outermost <div> if you want to use your component as an observablehq's view
};
};
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

@ -1 +0,0 @@
{"version":3,"file":"SquiggleChart.stories.js","sourceRoot":"","sources":["SquiggleChart.stories.tsx"],"names":[],"mappings":";;;AAAA,6BAA8B;AAC9B,iDAA+C;AAG/C,qBAAe;IACb,KAAK,EAAE,uBAAuB;IAC9B,SAAS,EAAE,6BAAa;CACzB,CAAA;AAED,IAAM,QAAQ,GAAG,UAAC,EAAgB;QAAf,cAAc,oBAAA;IAAM,OAAA,oBAAC,6BAAa,IAAC,cAAc,EAAE,cAAc,GAAI;AAAjD,CAAiD,CAAA;AAE3E,QAAA,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACxC,eAAO,CAAC,IAAI,GAAG;IACb,cAAc,EAAE,cAAc;CAC/B,CAAC"}

View File

@ -153,11 +153,11 @@ to allow large and small numbers being printed cleanly.
</Story> </Story>
</Canvas> </Canvas>
## Functions ## Functions (Distribution Output)
<Canvas> <Canvas>
<Story <Story
name="Function" name="Function to Distribution"
args={{ args={{
squiggleString: "foo(t) = normal(t,2)*normal(5,3); foo", squiggleString: "foo(t) = normal(t,2)*normal(5,3); foo",
width, width,
@ -167,6 +167,20 @@ to allow large and small numbers being printed cleanly.
</Story> </Story>
</Canvas> </Canvas>
## Functions (Number Output)
<Canvas>
<Story
name="Function to Number"
args={{
squiggleString: "foo(t) = t^2; foo",
width,
}}
>
{Template.bind({})}
</Story>
</Canvas>
## Records ## Records
<Canvas> <Canvas>

View File

@ -1,6 +1,5 @@
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";
import styled from "styled-components";
<Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} /> <Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} />
@ -16,7 +15,7 @@ including sampling settings, in squiggle.
name="Normal" name="Normal"
args={{ args={{
initialSquiggleString: "normal(5,2)", initialSquiggleString: "normal(5,2)",
height: 500, height: 800,
}} }}
> >
{Template.bind({})} {Template.bind({})}

View File

@ -0,0 +1,371 @@
.squiggle {
/*
This file contains:
1) Base Tailwind preflight styles
2) Base https://github.com/tailwindlabs/tailwindcss-forms styles
(Both are wrapped in .squiggle)
*/
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
*/
/* html { */
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-moz-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 */
/* } */
/*
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.
*/
/* body { */
margin: 0; /* 1 */
line-height: inherit; /* 2 */
/* } */
/*
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)
*/
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
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)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: theme('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.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
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)
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.
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
font-weight: inherit; /* 1 */
line-height: inherit; /* 1 */
color: inherit; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button; /* 1 */
background-color: transparent; /* 2 */
background-image: none; /* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
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 {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
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.
*/
input::placeholder,
textarea::placeholder {
opacity: 1; /* 1 */
color: theme('colors.gray.400', #9ca3af); /* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
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)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block; /* 1 */
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)
*/
img,
video {
max-width: 100%;
height: auto;
}
}

View File

@ -0,0 +1,102 @@
/* Fork of https://github.com/tailwindlabs/tailwindcss-forms styles, see the comment in main.css for details. */
.squiggle {
.form-input,.form-textarea,.form-select,.form-multiselect {
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
.form-input:focus, .form-textarea:focus, .form-select:focus, .form-multiselect:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
.form-input::placeholder,.form-textarea::placeholder {
color: #6b7280;
opacity: 1;
}
.form-input::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
.form-input::-webkit-date-and-time-value {
min-height: 1.5em;
}
.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-year-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
.form-checkbox,.form-radio {
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
-webkit-user-select: none;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: #2563eb;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
--tw-shadow: 0 0 #0000;
}
.form-checkbox {
border-radius: 0px;
}
.form-checkbox:focus,.form-radio:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.form-checkbox:checked,.form-radio:checked {
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

@ -0,0 +1,24 @@
/*
Fork of tailwind's preflight with `.squiggle` scoping.
*/
@import "./base.css";
/*
Fork of https://github.com/tailwindlabs/tailwindcss-forms styles (with strategy: "class"), but with `.squiggle` scoping.
This is necessary because tailwindcss-forms's css specificity is lower than preflight's specificity (`padding: 0` and so on),
so unscoped forms css with scoped preflight css, and there's no setting for scoping.
*/
@import "./forms.css";
/*
This doesn't include tailwind's original preflight (it's disabled in tailwind.config.js),
but this line is still necessary for proper initialization of `--tw-*` variables.
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Necessary hack because scoped preflight in ./base.css has higher specificity. */
.ace_cursor {
border-left: 2px solid !important;
}

View File

@ -23,7 +23,7 @@
"tickOpacity": 0.0, "tickOpacity": 0.0,
"domainColor": "#fff", "domainColor": "#fff",
"domainOpacity": 0.0, "domainOpacity": 0.0,
"format": "~g", "format": ".9~s",
"tickCount": 10 "tickCount": 10
} }
], ],
@ -33,6 +33,7 @@
"from": { "from": {
"data": "con" "data": "con"
}, },
"interpolate": "linear",
"encode": { "encode": {
"update": { "update": {
"x": { "x": {
@ -48,10 +49,10 @@
"value": 0 "value": 0
}, },
"fill": { "fill": {
"value": "#4C78A8" "value": "#739ECC"
}, },
"interpolate": { "interpolate": {
"value": "monotone" "value": "linear"
}, },
"fillOpacity": { "fillOpacity": {
"value": 1 "value": 1
@ -82,6 +83,9 @@
"y2": { "y2": {
"scale": "yscale", "scale": "yscale",
"value": 0 "value": 0
},
"fill": {
"value": "#2f65a7"
} }
} }
} }

View File

@ -0,0 +1,91 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 500,
"height": 200,
"padding": 5,
"data": [
{
"name": "facet",
"values": [],
"format": {
"type": "json",
"parse": {
"timestamp": "date"
}
}
}
],
"scales": [
{
"name": "x",
"type": "linear",
"nice": true,
"zero": false,
"domain": {
"data": "facet",
"field": "x"
},
"range": "width"
},
{
"name": "y",
"type": "linear",
"range": "height",
"nice": true,
"zero": false,
"domain": {
"data": "facet",
"field": "y"
}
}
],
"signals": [
{
"name": "mousemove",
"on": [{ "events": "mousemove", "update": "invert('x', x())" }]
},
{
"name": "mouseout",
"on": [{ "events": "mouseout", "update": "invert('x', x())" }]
}
],
"axes": [
{
"orient": "bottom",
"scale": "x",
"grid": false,
"labelColor": "#727d93",
"tickColor": "#fff",
"tickOpacity": 0.0,
"domainColor": "#727d93",
"domainOpacity": 0.1,
"format": ".9~s",
"tickCount": 5
},
{
"orient": "left",
"scale": "y",
"grid": false,
"labelColor": "#727d93",
"tickColor": "#fff",
"tickOpacity": 0.0,
"domainColor": "#727d93",
"domainOpacity": 0.1,
"format": ".9~s",
"tickCount": 5
}
],
"marks": [
{
"type": "line",
"from": { "data": "facet" },
"encode": {
"enter": {
"x": { "scale": "x", "field": "x" },
"y": { "scale": "y", "field": "y" },
"strokeWidth": { "value": 2 }
}
}
}
]
}

View File

@ -75,6 +75,7 @@
"name": "xscale", "name": "xscale",
"type": "linear", "type": "linear",
"nice": true, "nice": true,
"zero": false,
"domain": { "domain": {
"data": "facet", "data": "facet",
"field": "x" "field": "x"
@ -86,10 +87,10 @@
"type": "linear", "type": "linear",
"range": "height", "range": "height",
"nice": true, "nice": true,
"zero": true, "zero": false,
"domain": { "domain": {
"data": "facet", "data": "facet",
"field": "p99" "fields": ["p1", "p99"]
} }
} }
], ],
@ -113,12 +114,14 @@
"tickOpacity": 0.0, "tickOpacity": 0.0,
"domainColor": "#727d93", "domainColor": "#727d93",
"domainOpacity": 0.1, "domainOpacity": 0.1,
"format": ".9~s",
"tickCount": 5 "tickCount": 5
}, },
{ {
"orient": "left", "orient": "left",
"scale": "yscale", "scale": "yscale",
"grid": false, "grid": false,
"format": ".9~s",
"labelColor": "#727d93", "labelColor": "#727d93",
"tickColor": "#fff", "tickColor": "#fff",
"tickOpacity": 0.0, "tickOpacity": 0.0,

View File

@ -0,0 +1,10 @@
module.exports = {
content: ["./src/**/*.{html,tsx,ts,js,jsx}"],
corePlugins: {
preflight: false,
},
important: ".squiggle",
theme: {
extend: {},
},
};

View File

@ -20,7 +20,8 @@
}, },
"files": [ "files": [
"src/vega-specs/spec-distributions.json", "src/vega-specs/spec-distributions.json",
"src/vega-specs/spec-percentiles.json" "src/vega-specs/spec-percentiles.json",
"src/vega-specs/spec-line-chart.json"
], ],
"target": "ES6", "target": "ES6",
"include": ["src/**/*", "src/*"], "include": ["src/**/*", "src/*"],

View File

@ -10,13 +10,9 @@ module.exports = {
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
loader: "ts-loader", loader: "ts-loader",
options: { projectReferences: true, transpileOnly: true }, options: { projectReferences: true },
exclude: /node_modules/, exclude: /node_modules/,
}, },
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
], ],
}, },
resolve: { resolve: {
@ -40,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

@ -3,6 +3,6 @@ lib
*.bs.js *.bs.js
*.gen.tsx *.gen.tsx
.nyc_output/ .nyc_output/
coverage/ _coverage/
.cache/ .cache/
Reducer_Peggy_GeneratedParser.js Reducer_Peggy_GeneratedParser.js

View File

@ -88,17 +88,13 @@ describe("block", () => {
) )
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( testMacro([], eBlock(list{eBlock(list{exampleExpression})}), "Ok((:$$_bindExpression_$$ {1}))")
[],
eBlock(list{eBlock(list{exampleExpression})}),
"Ok((:$$_bindExpression_$$ (:$$_block_$$ 1)))",
)
testMacroEval([], eBlock(list{eBlock(list{exampleExpression})}), "Ok(1)") testMacroEval([], eBlock(list{eBlock(list{exampleExpression})}), "Ok(1)")
// Block assigned to a variable // Block assigned to a variable
testMacro( testMacro(
[], [],
eBlock(list{eLetStatement("z", eBlock(list{eBlock(list{exampleExpressionY})}))}), eBlock(list{eLetStatement("z", eBlock(list{eBlock(list{exampleExpressionY})}))}),
"Ok((:$$_bindExpression_$$ (:$_let_$ :z (:$$_block_$$ (:$$_block_$$ :y)))))", "Ok((:$$_bindExpression_$$ (:$_let_$ :z {{:y}})))",
) )
testMacroEval( testMacroEval(
[], [],
@ -116,7 +112,7 @@ describe("block", () => {
eSymbol("y"), eSymbol("y"),
}), }),
}), }),
"Ok((:$$_bindExpression_$$ (:$$_block_$$ (:$_let_$ :y (:add :x 1)) :y)))", "Ok((:$$_bindExpression_$$ {(:$_let_$ :y (:add :x 1)); :y}))",
) )
testMacroEval( testMacroEval(
[("x", EvNumber(1.))], [("x", EvNumber(1.))],

View File

@ -18,7 +18,7 @@ describe("builtin", () => {
testEval("2>1", "Ok(true)") testEval("2>1", "Ok(true)")
testEval("concat('a','b')", "Ok('ab')") testEval("concat('a','b')", "Ok('ab')")
testEval( testEval(
"addOne(t)=t+1; toInternalSampleArray(mapSamples(fromSamples([1,2,3,4,5,6]), addOne))", "addOne(t)=t+1; toList(mapSamples(fromSamples([1,2,3,4,5,6]), addOne))",
"Ok([2,3,4,5,6,7])", "Ok([2,3,4,5,6,7])",
) )
}) })

View File

@ -0,0 +1,19 @@
module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.ExpressionValue
module ErrorValue = Reducer_ErrorValue
module Bindings = Reducer_Category_Bindings
let removeDefaults = (ev: ExpressionT.expressionValue): ExpressionT.expressionValue =>
switch ev {
| EvRecord(extbindings) => {
let bindings: Bindings.t = Bindings.fromRecord(extbindings)
let keys = Js.Dict.keys(Reducer.defaultExternalBindings)
Belt.Map.String.keep(bindings, (key, _value) => {
let removeThis = Js.Array2.includes(keys, key)
!removeThis
})->Bindings.toExpressionValue
}
| value => value
}
let rRemoveDefaults = r => Belt.Result.map(r, ev => removeDefaults(ev))

View File

@ -1,26 +1,5 @@
module Parse = Reducer_Peggy_Parse
module Result = Belt.Result
open Jest open Jest
open Expect open Reducer_Peggy_TestHelpers
let expectParseToBe = (expr, answer) =>
Parse.parse(expr)->Parse.toStringResult->expect->toBe(answer)
let testParse = (expr, answer) => test(expr, () => expectParseToBe(expr, answer))
module MySkip = {
let testParse = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer))
let testDescriptionParse = (desc, expr, answer) =>
Skip.test(desc, () => expectParseToBe(expr, answer))
}
module MyOnly = {
let testParse = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer))
let testDescriptionParse = (desc, expr, answer) =>
Only.test(desc, () => expectParseToBe(expr, answer))
}
describe("Peggy parse", () => { describe("Peggy parse", () => {
describe("float", () => { describe("float", () => {
@ -253,8 +232,12 @@ describe("Peggy parse", () => {
}) })
describe("unit", () => { describe("unit", () => {
testParse("1m", "{(::fromUnit_m 1)}") testParse("1m", "{(::fromUnit_m 1)}")
testParse("1M", "{(::fromUnit_M 1)}")
testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}") testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}")
}) })
describe("Module", () => {
testParse("Math.pi", "{(::$_atIndex_$ @Math 'pi')}")
})
}) })
describe("parsing new line", () => { describe("parsing new line", () => {

View File

@ -0,0 +1,79 @@
open Jest
open Reducer_Peggy_TestHelpers
describe("Peggy parse type", () => {
describe("type of", () => {
testParse("p: number", "{(::$_typeOf_$ :p #number)}")
})
describe("type alias", () => {
testParse("type index=number", "{(::$_typeAlias_$ #index #number)}")
})
describe("type or", () => {
testParse(
"answer: number|string",
"{(::$_typeOf_$ :answer (::$_typeOr_$ (::$_constructArray_$ (#number #string))))}",
)
})
describe("type function", () => {
testParse(
"f: number=>number=>number",
"{(::$_typeOf_$ :f (::$_typeFunction_$ (::$_constructArray_$ (#number #number #number))))}",
)
})
describe("high priority modifier", () => {
testParse(
"answer: number<-min<-max(100)|string",
"{(::$_typeOf_$ :answer (::$_typeOr_$ (::$_constructArray_$ ((::$_typeModifier_max_$ (::$_typeModifier_min_$ #number) 100) #string))))}",
)
testParse(
"answer: number<-memberOf([1,3,5])",
"{(::$_typeOf_$ :answer (::$_typeModifier_memberOf_$ #number (::$_constructArray_$ (1 3 5))))}",
)
})
describe("low priority modifier", () => {
testParse(
"answer: number | string $ opaque",
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
)
})
describe("type array", () => {
testParse("answer: [number]", "{(::$_typeOf_$ :answer (::$_typeArray_$ #number))}")
})
describe("type record", () => {
testParse(
"answer: {a: number, b: string}",
"{(::$_typeOf_$ :answer (::$_typeRecord_$ (::$_constructRecord_$ ('a': #number 'b': #string))))}",
)
})
describe("type constructor", () => {
testParse(
"answer: Age(number)",
"{(::$_typeOf_$ :answer (::$_typeConstructor_$ #Age (::$_constructArray_$ (#number))))}",
)
testParse(
"answer: Complex(number, number)",
"{(::$_typeOf_$ :answer (::$_typeConstructor_$ #Complex (::$_constructArray_$ (#number #number))))}",
)
testParse(
"answer: Person({age: number, name: string})",
"{(::$_typeOf_$ :answer (::$_typeConstructor_$ #Person (::$_constructArray_$ ((::$_typeRecord_$ (::$_constructRecord_$ ('age': #number 'name': #string)))))))}",
)
testParse(
"weekend: Saturday | Sunday",
"{(::$_typeOf_$ :weekend (::$_typeOr_$ (::$_constructArray_$ ((::$_typeConstructor_$ #Saturday (::$_constructArray_$ ())) (::$_typeConstructor_$ #Sunday (::$_constructArray_$ ()))))))}",
)
})
describe("type paranthesis", () => {
//$ is introduced to avoid paranthesis
testParse(
"answer: (number|string)<-opaque",
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
)
})
describe("squiggle expressions in type modifiers", () => {
testParse(
"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)))}",
)
})
})

View File

@ -0,0 +1,51 @@
module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface_ExpressionValue
module Parse = Reducer_Peggy_Parse
module Result = Belt.Result
module ToExpression = Reducer_Peggy_ToExpression
open Jest
open Expect
let expectParseToBe = (expr, answer) =>
Parse.parse(expr)->Parse.toStringResult->expect->toBe(answer)
let testParse = (expr, answer) => test(expr, () => expectParseToBe(expr, answer))
let expectToExpressionToBe = (expr, answer, ~v="_", ()) => {
let rExpr = Parse.parse(expr)->Result.map(ToExpression.fromNode)
let a1 = rExpr->ExpressionT.toStringResultOkless
if v == "_" {
a1->expect->toBe(answer)
} else {
let a2 =
rExpr
->Result.flatMap(expr =>
Expression.reduceExpression(
expr,
ReducerInterface_StdLib.internalStdLib,
ExpressionValue.defaultEnvironment,
)
)
->Reducer_Helpers.rRemoveDefaults
->ExpressionValue.toStringResultOkless
(a1, a2)->expect->toEqual((answer, v))
}
}
let testToExpression = (expr, answer, ~v="_", ()) =>
test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
module MyOnly = {
let testParse = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer))
let testToExpression = (expr, answer, ~v="_", ()) =>
Only.test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
}
module MySkip = {
let testParse = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer))
let testToExpression = (expr, answer, ~v="_", ()) =>
Skip.test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
}

View File

@ -1,181 +1,150 @@
module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface_ExpressionValue
module Parse = Reducer_Peggy_Parse
module ToExpression = Reducer_Peggy_ToExpression
module Result = Belt.Result
open Jest open Jest
open Expect open Reducer_Peggy_TestHelpers
let expectToExpressionToBe = (expr, answer, ~v="_", ()) => {
let rExpr = Parse.parse(expr)->Result.map(ToExpression.fromNode)
let a1 = rExpr->ExpressionT.toStringResultOkless
if v == "_" {
a1->expect->toBe(answer)
} else {
let a2 =
rExpr
->Result.flatMap(expr =>
Expression.reduceExpression(expr, Belt.Map.String.empty, ExpressionValue.defaultEnvironment)
)
->ExpressionValue.toStringResultOkless
(a1, a2)->expect->toEqual((answer, v))
}
}
let testToExpression = (expr, answer, ~v="_", ()) =>
test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
module MySkip = {
let testToExpression = (expr, answer, ~v="_", ()) =>
Skip.test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
}
module MyOnly = {
let testToExpression = (expr, answer, ~v="_", ()) =>
Only.test(expr, () => expectToExpressionToBe(expr, answer, ~v, ()))
}
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 ignrored at the first statement
testToExpression("1", "(:$$_block_$$ 1)", ~v="1", ()) testToExpression("1", "{1}", ~v="1", ())
testToExpression("'hello'", "(:$$_block_$$ 'hello')", ~v="'hello'", ()) testToExpression("'hello'", "{'hello'}", ~v="'hello'", ())
testToExpression("true", "(:$$_block_$$ true)", ~v="true", ()) testToExpression("true", "{true}", ~v="true", ())
testToExpression("1+2", "(:$$_block_$$ (:add 1 2))", ~v="3", ()) testToExpression("1+2", "{(:add 1 2)}", ~v="3", ())
testToExpression("add(1,2)", "(:$$_block_$$ (:add 1 2))", ~v="3", ()) testToExpression("add(1,2)", "{(:add 1 2)}", ~v="3", ())
testToExpression("(1)", "(:$$_block_$$ 1)", ()) testToExpression("(1)", "{1}", ())
testToExpression("(1+2)", "(:$$_block_$$ (:add 1 2))", ()) testToExpression("(1+2)", "{(:add 1 2)}", ())
}) })
describe("unary", () => { describe("unary", () => {
testToExpression("-1", "(:$$_block_$$ (:unaryMinus 1))", ~v="-1", ()) testToExpression("-1", "{(:unaryMinus 1)}", ~v="-1", ())
testToExpression("!true", "(:$$_block_$$ (:not true))", ~v="false", ()) testToExpression("!true", "{(:not true)}", ~v="false", ())
testToExpression("1 + -1", "(:$$_block_$$ (:add 1 (:unaryMinus 1)))", ~v="0", ()) testToExpression("1 + -1", "{(:add 1 (:unaryMinus 1))}", ~v="0", ())
testToExpression("-a[0]", "(:$$_block_$$ (:unaryMinus (:$_atIndex_$ :a 0)))", ()) testToExpression("-a[0]", "{(:unaryMinus (:$_atIndex_$ :a 0))}", ())
}) })
describe("multi-line", () => { describe("multi-line", () => {
testToExpression("x=1; 2", "(:$$_block_$$ (:$_let_$ :x (:$$_block_$$ 1)) 2)", ~v="2", ()) testToExpression("x=1; 2", "{(:$_let_$ :x {1}); 2}", ~v="2", ())
testToExpression( testToExpression("x=1; y=2", "{(:$_let_$ :x {1}); (:$_let_$ :y {2})}", ~v="{x: 1,y: 2}", ())
"x=1; y=2",
"(:$$_block_$$ (:$_let_$ :x (:$$_block_$$ 1)) (:$_let_$ :y (:$$_block_$$ 2)))",
~v="{x: 1,y: 2}",
(),
)
}) })
describe("variables", () => { describe("variables", () => {
testToExpression("x = 1", "(:$$_block_$$ (:$_let_$ :x (:$$_block_$$ 1)))", ~v="{x: 1}", ()) testToExpression("x = 1", "{(:$_let_$ :x {1})}", ~v="{x: 1}", ())
testToExpression("x", "(:$$_block_$$ :x)", ~v=":x", ()) //TODO: value should return error testToExpression("x", "{:x}", ~v=":x", ()) //TODO: value should return error
testToExpression("x = 1; x", "(:$$_block_$$ (:$_let_$ :x (:$$_block_$$ 1)) :x)", ~v="1", ()) testToExpression("x = 1; x", "{(:$_let_$ :x {1}); :x}", ~v="1", ())
}) })
describe("functions", () => { describe("functions", () => {
testToExpression( testToExpression(
"identity(x) = x", "identity(x) = x",
"(:$$_block_$$ (:$_let_$ :identity (:$$_lambda_$$ [x] (:$$_block_$$ :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)", "(:$$_block_$$ (:identity :x))", ()) // Note value returns error properly testToExpression("identity(x)", "{(:identity :x)}", ()) // Note value returns error properly
testToExpression( testToExpression(
"f(x) = x> 2 ? 0 : 1; f(3)", "f(x) = x> 2 ? 0 : 1; f(3)",
"(:$$_block_$$ (:$_let_$ :f (:$$_lambda_$$ [x] (:$$_block_$$ (:$$_ternary_$$ (:larger :x 2) 0 1)))) (:f 3))", "{(:$_let_$ :f (:$$_lambda_$$ [x] {(:$$_ternary_$$ (:larger :x 2) 0 1)})); (:f 3)}",
~v="0", ~v="0",
(), (),
) )
}) })
describe("arrays", () => { describe("arrays", () => {
testToExpression("[]", "(:$$_block_$$ (:$_constructArray_$ ()))", ~v="[]", ()) testToExpression("[]", "{(:$_constructArray_$ ())}", ~v="[]", ())
testToExpression("[0, 1, 2]", "(:$$_block_$$ (:$_constructArray_$ (0 1 2)))", ~v="[0,1,2]", ()) testToExpression("[0, 1, 2]", "{(:$_constructArray_$ (0 1 2))}", ~v="[0,1,2]", ())
testToExpression( testToExpression(
"['hello', 'world']", "['hello', 'world']",
"(:$$_block_$$ (:$_constructArray_$ ('hello' 'world')))", "{(:$_constructArray_$ ('hello' 'world'))}",
~v="['hello','world']", ~v="['hello','world']",
(), (),
) )
testToExpression( testToExpression("([0,1,2])[1]", "{(:$_atIndex_$ (:$_constructArray_$ (0 1 2)) 1)}", ~v="1", ())
"([0,1,2])[1]",
"(:$$_block_$$ (:$_atIndex_$ (:$_constructArray_$ (0 1 2)) 1))",
~v="1",
(),
)
}) })
describe("records", () => { describe("records", () => {
testToExpression( testToExpression(
"{a: 1, b: 2}", "{a: 1, b: 2}",
"(:$$_block_$$ (:$_constructRecord_$ (('a' 1) ('b' 2))))", "{(:$_constructRecord_$ (('a' 1) ('b' 2)))}",
~v="{a: 1,b: 2}", ~v="{a: 1,b: 2}",
(), (),
) )
testToExpression( testToExpression(
"{1+0: 1, 2+0: 2}", "{1+0: 1, 2+0: 2}",
"(:$$_block_$$ (:$_constructRecord_$ (((:add 1 0) 1) ((:add 2 0) 2))))", "{(:$_constructRecord_$ (((:add 1 0) 1) ((:add 2 0) 2)))}",
(), (),
) // key can be any expression ) // key can be any expression
testToExpression("record.property", "(:$$_block_$$ (:$_atIndex_$ :record 'property'))", ()) testToExpression("record.property", "{(:$_atIndex_$ :record 'property')}", ())
testToExpression( testToExpression(
"record={property: 1}; record.property", "record={property: 1}; record.property",
"(:$$_block_$$ (:$_let_$ :record (:$$_block_$$ (:$_constructRecord_$ (('property' 1))))) (:$_atIndex_$ :record 'property'))", "{(:$_let_$ :record {(:$_constructRecord_$ (('property' 1)))}); (:$_atIndex_$ :record 'property')}",
~v="1", ~v="1",
(), (),
) )
}) })
describe("comments", () => { describe("comments", () => {
testToExpression("1 # This is a line comment", "(:$$_block_$$ 1)", ~v="1", ()) testToExpression("1 # This is a line comment", "{1}", ~v="1", ())
testToExpression("1 // This is a line comment", "(:$$_block_$$ 1)", ~v="1", ()) testToExpression("1 // This is a line comment", "{1}", ~v="1", ())
testToExpression("1 /* This is a multi line comment */", "(:$$_block_$$ 1)", ~v="1", ()) testToExpression("1 /* This is a multi line comment */", "{1}", ~v="1", ())
testToExpression("/* This is a multi line comment */ 1", "(:$$_block_$$ 1)", ~v="1", ()) testToExpression("/* This is a multi line comment */ 1", "{1}", ~v="1", ())
}) })
describe("ternary operator", () => { describe("ternary operator", () => {
testToExpression("true ? 1 : 0", "(:$$_block_$$ (:$$_ternary_$$ true 1 0))", ~v="1", ()) testToExpression("true ? 1 : 0", "{(:$$_ternary_$$ true 1 0)}", ~v="1", ())
testToExpression("false ? 1 : 0", "(:$$_block_$$ (:$$_ternary_$$ false 1 0))", ~v="0", ()) testToExpression("false ? 1 : 0", "{(:$$_ternary_$$ false 1 0)}", ~v="0", ())
testToExpression( testToExpression(
"true ? 1 : false ? 2 : 0", "true ? 1 : false ? 2 : 0",
"(:$$_block_$$ (:$$_ternary_$$ true 1 (:$$_ternary_$$ false 2 0)))", "{(:$$_ternary_$$ true 1 (:$$_ternary_$$ false 2 0))}",
~v="1", ~v="1",
(), (),
) // nested ternary ) // nested ternary
testToExpression( testToExpression(
"false ? 1 : false ? 2 : 0", "false ? 1 : false ? 2 : 0",
"(:$$_block_$$ (:$$_ternary_$$ false 1 (:$$_ternary_$$ false 2 0)))", "{(:$$_ternary_$$ false 1 (:$$_ternary_$$ false 2 0))}",
~v="0", ~v="0",
(), (),
) // nested ternary ) // nested ternary
describe("ternary bindings", () => {
testToExpression(
// expression binding
"f(a) = a > 5 ? 1 : 0; f(6)",
"{(:$_let_$ :f (:$$_lambda_$$ [a] {(:$$_ternary_$$ (:larger :a 5) 1 0)})); (:f 6)}",
~v="1",
(),
)
testToExpression(
// when true binding
"f(a) = a > 5 ? a : 0; f(6)",
"{(:$_let_$ :f (:$$_lambda_$$ [a] {(:$$_ternary_$$ (:larger :a 5) :a 0)})); (:f 6)}",
~v="6",
(),
)
testToExpression(
// when false binding
"f(a) = a < 5 ? 1 : a; f(6)",
"{(:$_let_$ :f (:$$_lambda_$$ [a] {(:$$_ternary_$$ (:smaller :a 5) 1 :a)})); (:f 6)}",
~v="6",
(),
)
})
}) })
describe("if then else", () => { describe("if then else", () => {
testToExpression( testToExpression("if true then 2 else 3", "{(:$$_ternary_$$ true {2} {3})}", ())
"if true then 2 else 3", testToExpression("if true then {2} else {3}", "{(:$$_ternary_$$ true {2} {3})}", ())
"(:$$_block_$$ (:$$_ternary_$$ true (:$$_block_$$ 2) (:$$_block_$$ 3)))",
(),
)
testToExpression(
"if true then {2} else {3}",
"(:$$_block_$$ (:$$_ternary_$$ true (:$$_block_$$ 2) (:$$_block_$$ 3)))",
(),
)
testToExpression( testToExpression(
"if false then {2} else if false then {4} else {5}", "if false then {2} else if false then {4} else {5}",
"(:$$_block_$$ (:$$_ternary_$$ false (:$$_block_$$ 2) (:$$_ternary_$$ false (:$$_block_$$ 4) (:$$_block_$$ 5))))", "{(:$$_ternary_$$ false {2} (:$$_ternary_$$ false {4} {5}))}",
(), (),
) //nested if ) //nested if
}) })
describe("pipe", () => { describe("pipe", () => {
testToExpression("1 -> add(2)", "(:$$_block_$$ (:add 1 2))", ~v="3", ()) testToExpression("1 -> add(2)", "{(:add 1 2)}", ~v="3", ())
testToExpression("-1 -> add(2)", "(:$$_block_$$ (:add (:unaryMinus 1) 2))", ~v="1", ()) // note that unary has higher priority naturally testToExpression("-1 -> add(2)", "{(:add (:unaryMinus 1) 2)}", ~v="1", ()) // note that unary has higher priority naturally
testToExpression("1 -> add(2) * 3", "(:$$_block_$$ (:multiply (:add 1 2) 3))", ~v="9", ()) testToExpression("1 -> add(2) * 3", "{(:multiply (:add 1 2) 3)}", ~v="9", ())
}) })
describe("elixir pipe", () => { describe("elixir pipe", () => {
testToExpression("1 |> add(2)", "(:$$_block_$$ (:add 1 2))", ~v="3", ()) testToExpression("1 |> add(2)", "{(:add 1 2)}", ~v="3", ())
}) })
// see testParse for priorities of to and credibleIntervalToDistribution // see testParse for priorities of to and credibleIntervalToDistribution
@ -185,36 +154,35 @@ describe("Peggy to Expression", () => {
// Like lambdas they have a local scope. // Like lambdas they have a local scope.
testToExpression( testToExpression(
"y=99; x={y=1; y}", "y=99; x={y=1; y}",
"(:$$_block_$$ (:$_let_$ :y (:$$_block_$$ 99)) (:$_let_$ :x (:$$_block_$$ (:$_let_$ :y (:$$_block_$$ 1)) :y)))", "{(:$_let_$ :y {99}); (:$_let_$ :x {(:$_let_$ :y {1}); :y})}",
~v="{x: 1,y: 99}", ~v="{x: 1,y: 99}",
(), (),
) )
}) })
describe("lambda", () => { describe("lambda", () => {
testToExpression( testToExpression("{|x| x}", "{(:$$_lambda_$$ [x] {:x})}", ~v="lambda(x=>internal code)", ())
"{|x| x}",
"(:$$_block_$$ (:$$_lambda_$$ [x] (:$$_block_$$ :x)))",
~v="lambda(x=>internal code)",
(),
)
testToExpression( testToExpression(
"f={|x| x}", "f={|x| x}",
"(:$$_block_$$ (:$_let_$ :f (:$$_block_$$ (:$$_lambda_$$ [x] (:$$_block_$$ :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",
"(:$$_block_$$ (:$_let_$ :f (:$$_lambda_$$ [x] (:$$_block_$$ :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",
"(:$$_block_$$ (:$_let_$ :f (:$$_lambda_$$ [x] (:$$_block_$$ (:$$_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", () => {
testToExpression("Math.pi", "{(:$_atIndex_$ :Math 'pi')}", ~v="3.141592653589793", ())
})
}) })

View File

@ -0,0 +1,99 @@
open Jest
open Reducer_Peggy_TestHelpers
describe("Peggy Types to Expression", () => {
describe("type of", () => {
testToExpression(
"p: number",
"{(:$_typeOf_$ :p #number)}",
~v="{_typeReferences_: {p: #number}}",
(),
)
})
describe("type alias", () => {
testToExpression(
"type index=number",
"{(:$_typeAlias_$ #index #number)}",
~v="{_typeAliases_: {index: #number}}",
(),
)
})
describe("type or", () => {
testToExpression(
"answer: number|string|distribution",
"{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ (#number #string #distribution))))}",
~v="{_typeReferences_: {answer: {typeTag: 'typeOr',typeOr: [#number,#string,#distribution]}}}",
(),
)
})
describe("type function", () => {
testToExpression(
"f: number=>number=>number",
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number #number))))}",
~v="{_typeReferences_: {f: {typeTag: 'typeFunction',inputs: [#number,#number],output: #number}}}",
(),
)
testToExpression(
"f: number=>number",
"{(:$_typeOf_$ :f (:$_typeFunction_$ (:$_constructArray_$ (#number #number))))}",
~v="{_typeReferences_: {f: {typeTag: 'typeFunction',inputs: [#number],output: #number}}}",
(),
)
})
describe("high priority modifier", () => {
testToExpression(
"answer: number<-min(1)<-max(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]}}}",
(),
)
testToExpression(
"answer: number<-memberOf([1,3,5])",
"{(:$_typeOf_$ :answer (:$_typeModifier_memberOf_$ #number (:$_constructArray_$ (1 3 5))))}",
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,memberOf: [1,3,5]}}}",
(),
)
testToExpression(
"answer: number<-min(1)",
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ #number 1))}",
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,min: 1}}}",
(),
)
testToExpression(
"answer: number<-max(10)",
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ #number 10))}",
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,max: 10}}}",
(),
)
testToExpression(
"answer: number<-min(1)<-max(10)",
"{(:$_typeOf_$ :answer (:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 10))}",
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,min: 1,max: 10}}}",
(),
)
testToExpression(
"answer: number<-max(10)<-min(1)",
"{(:$_typeOf_$ :answer (:$_typeModifier_min_$ (:$_typeModifier_max_$ #number 10) 1))}",
~v="{_typeReferences_: {answer: {typeTag: 'typeIdentifier',typeIdentifier: #number,max: 10,min: 1}}}",
(),
)
})
describe("low priority modifier", () => {
testToExpression(
"answer: number | string $ opaque",
"{(:$_typeOf_$ :answer (:$_typeModifier_opaque_$ (:$_typeOr_$ (:$_constructArray_$ (#number #string)))))}",
~v="{_typeReferences_: {answer: {typeTag: 'typeOr',typeOr: [#number,#string],opaque: true}}}",
(),
)
})
describe("squiggle expressions in type modifiers", () => {
testToExpression(
"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)))}",
~v="{_typeAliases_: {odds: {typeTag: 'typeIdentifier',typeIdentifier: #number,memberOf: [1,3,5,7,9]}},odds1: [1,3,5],odds2: [7,9]}",
(),
)
})
// TODO: type bindings. Type bindings are not yet supported.
// TODO: type constructor
})

View File

@ -1,6 +1,7 @@
module ExpressionT = Reducer_Expression_T module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.ExpressionValue module ExpressionValue = ReducerInterface.ExpressionValue
module ErrorValue = Reducer_ErrorValue module ErrorValue = Reducer_ErrorValue
module Bindings = Reducer_Category_Bindings
open Jest open Jest
open Expect open Expect
@ -17,13 +18,18 @@ let expectParseToBe = (expr: string, answer: string) =>
Reducer.parse(expr)->ExpressionT.toStringResult->expect->toBe(answer) Reducer.parse(expr)->ExpressionT.toStringResult->expect->toBe(answer)
let expectEvalToBe = (expr: string, answer: string) => let expectEvalToBe = (expr: string, answer: string) =>
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toBe(answer) Reducer.evaluate(expr)
->Reducer_Helpers.rRemoveDefaults
->ExpressionValue.toStringResult
->expect
->toBe(answer)
let expectEvalError = (expr: string) => let expectEvalError = (expr: string) =>
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toMatch("Error\(") Reducer.evaluate(expr)->ExpressionValue.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
->ExpressionValue.toStringResult ->ExpressionValue.toStringResult
->expect ->expect
->toBe(answer) ->toBe(answer)

View File

@ -5,7 +5,7 @@ open Reducer_TestHelpers
describe("Eval with Bindings", () => { describe("Eval with Bindings", () => {
testEvalBindingsToBe("x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(1)") testEvalBindingsToBe("x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(1)")
testEvalBindingsToBe("x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)") testEvalBindingsToBe("x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
testParseToBe("y = x+1; y", "Ok((:$$_block_$$ (:$_let_$ :y (:$$_block_$$ (: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", ExpressionValue.EvNumber(1.))}, "Ok(2)")
testEvalBindingsToBe("y = x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1,y: 2})") testEvalBindingsToBe("y = x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1,y: 2})")
}) })

View File

@ -2,11 +2,8 @@ open Jest
open Reducer_TestHelpers open Reducer_TestHelpers
describe("Parse function assignment", () => { describe("Parse function assignment", () => {
testParseToBe("f(x)=x", "Ok((:$$_block_$$ (:$_let_$ :f (:$$_lambda_$$ [x] (:$$_block_$$ :x)))))") testParseToBe("f(x)=x", "Ok({(:$_let_$ :f (:$$_lambda_$$ [x] {:x}))})")
testParseToBe( testParseToBe("f(x)=2*x", "Ok({(:$_let_$ :f (:$$_lambda_$$ [x] {(:multiply 2 :x)}))})")
"f(x)=2*x",
"Ok((:$$_block_$$ (:$_let_$ :f (:$$_lambda_$$ [x] (:$$_block_$$ (:multiply 2 :x))))))",
)
//MathJs does not allow blocks in function definitions //MathJs does not allow blocks in function definitions
}) })

View File

@ -51,7 +51,7 @@ describe("call and bindings", () => {
) )
testParseToBe( testParseToBe(
"f=99; g(x)=f; g(2)", "f=99; g(x)=f; g(2)",
"Ok((:$$_block_$$ (:$_let_$ :f (:$$_block_$$ 99)) (:$_let_$ :g (:$$_lambda_$$ [x] (:$$_block_$$ :f))) (:g 2)))", "Ok({(:$_let_$ :f {99}); (:$_let_$ :g (:$$_lambda_$$ [x] {:f})); (:g 2)})",
) )
testEvalToBe("f=99; g(x)=f; g(2)", "Ok(99)") testEvalToBe("f=99; g(x)=f; g(2)", "Ok(99)")
testEvalToBe("f(x)=x; g(x)=f(x); g(2)", "Ok(2)") testEvalToBe("f(x)=x; g(x)=f(x); g(2)", "Ok(2)")

View File

@ -7,7 +7,7 @@ describe("map reduce", () => {
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]; reduce(arr, 0, change)", "Ok(15)")
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; reduceReverse(arr, 0, change)", "Ok(9)") 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("arr=[1,2,3]; reverse(arr)", "Ok([3,2,1])")
testEvalToBe("check(x)=(x==2);arr=[1,2,3]; keep(arr,check)", "Ok([2])") testEvalToBe("check(x)=(x==2);arr=[1,2,3]; filter(arr,check)", "Ok([2])")
}) })
Skip.describe("map reduce (sam)", () => { Skip.describe("map reduce (sam)", () => {

View File

@ -2,7 +2,7 @@ open Jest
open Reducer_TestHelpers open Reducer_TestHelpers
describe("Parse ternary operator", () => { describe("Parse ternary operator", () => {
testParseToBe("true ? 'YES' : 'NO'", "Ok((:$$_block_$$ (:$$_ternary_$$ true 'YES' 'NO')))") testParseToBe("true ? 'YES' : 'NO'", "Ok({(:$$_ternary_$$ true 'YES' 'NO')})")
}) })
describe("Evaluate ternary operator", () => { describe("Evaluate ternary operator", () => {

View File

@ -80,6 +80,7 @@ describe("eval on distribution functions", () => {
testEval("truncateLeft(normal(5,2), 3)", "Ok(Point Set Distribution)") testEval("truncateLeft(normal(5,2), 3)", "Ok(Point Set Distribution)")
testEval("truncateRight(normal(5,2), 3)", "Ok(Point Set Distribution)") testEval("truncateRight(normal(5,2), 3)", "Ok(Point Set Distribution)")
testEval("truncate(normal(5,2), 3, 8)", "Ok(Point Set Distribution)") testEval("truncate(normal(5,2), 3, 8)", "Ok(Point Set Distribution)")
testEval("isNormalized(truncate(normal(5,2), 3, 8))", "Ok(true)")
}) })
describe("exp", () => { describe("exp", () => {
@ -123,13 +124,13 @@ describe("eval on distribution functions", () => {
describe("parse on distribution functions", () => { describe("parse on distribution functions", () => {
describe("power", () => { describe("power", () => {
testParse("normal(5,2) ^ normal(5,1)", "Ok((:$$_block_$$ (:pow (:normal 5 2) (:normal 5 1))))") testParse("normal(5,2) ^ normal(5,1)", "Ok({(:pow (:normal 5 2) (:normal 5 1))})")
testParse("3 ^ normal(5,1)", "Ok((:$$_block_$$ (:pow 3 (:normal 5 1))))") testParse("3 ^ normal(5,1)", "Ok({(:pow 3 (:normal 5 1))})")
testParse("normal(5,2) ^ 3", "Ok((:$$_block_$$ (:pow (:normal 5 2) 3)))") testParse("normal(5,2) ^ 3", "Ok({(:pow (:normal 5 2) 3)})")
}) })
describe("subtraction", () => { describe("subtraction", () => {
testParse("10 - normal(5,1)", "Ok((:$$_block_$$ (:subtract 10 (:normal 5 1))))") testParse("10 - normal(5,1)", "Ok({(:subtract 10 (:normal 5 1))})")
testParse("normal(5,1) - 10", "Ok((:$$_block_$$ (:subtract (:normal 5 1) 10)))") testParse("normal(5,1) - 10", "Ok({(:subtract (:normal 5 1) 10)})")
}) })
describe("pointwise arithmetic expressions", () => { describe("pointwise arithmetic expressions", () => {
testParse(~skip=true, "normal(5,2) .+ normal(5,1)", "Ok((:dotAdd (:normal 5 2) (:normal 5 1)))") testParse(~skip=true, "normal(5,2) .+ normal(5,1)", "Ok((:dotAdd (:normal 5 2) (:normal 5 1)))")
@ -137,23 +138,14 @@ describe("parse on distribution functions", () => {
~skip=true, ~skip=true,
"normal(5,2) .- normal(5,1)", "normal(5,2) .- normal(5,1)",
"Ok((:$$_block_$$ (:dotSubtract (:normal 5 2) (:normal 5 1))))", "Ok((:$$_block_$$ (:dotSubtract (:normal 5 2) (:normal 5 1))))",
// TODO: !!! returns "Ok((:$$_block_$$ (:dotPow (:normal 5 2) (:normal 5 1))))" // TODO: !!! returns "Ok({(:dotPow (:normal 5 2) (:normal 5 1))})"
)
testParse(
"normal(5,2) .* normal(5,1)",
"Ok((:$$_block_$$ (:dotMultiply (:normal 5 2) (:normal 5 1))))",
)
testParse(
"normal(5,2) ./ normal(5,1)",
"Ok((:$$_block_$$ (:dotDivide (:normal 5 2) (:normal 5 1))))",
)
testParse(
"normal(5,2) .^ normal(5,1)",
"Ok((:$$_block_$$ (:dotPow (:normal 5 2) (:normal 5 1))))",
) )
testParse("normal(5,2) .* normal(5,1)", "Ok({(:dotMultiply (:normal 5 2) (:normal 5 1))})")
testParse("normal(5,2) ./ normal(5,1)", "Ok({(:dotDivide (:normal 5 2) (:normal 5 1))})")
testParse("normal(5,2) .^ normal(5,1)", "Ok({(:dotPow (:normal 5 2) (:normal 5 1))})")
}) })
describe("equality", () => { describe("equality", () => {
testParse("5 == normal(5,2)", "Ok((:$$_block_$$ (:equal 5 (:normal 5 2))))") testParse("5 == normal(5,2)", "Ok({(:equal 5 (:normal 5 2))})")
}) })
describe("pointwise adding two normals", () => { describe("pointwise adding two normals", () => {
testParse(~skip=true, "normal(5,2) .+ normal(5,1)", "Ok((:dotAdd (:normal 5 2) (:normal 5 1)))") testParse(~skip=true, "normal(5,2) .+ normal(5,1)", "Ok((:dotAdd (:normal 5 2) (:normal 5 1)))")

View File

@ -0,0 +1,7 @@
open Jest
open Reducer_TestHelpers
describe("Math Library", () => {
testEvalToBe("Math.e", "Ok(2.718281828459045)")
testEvalToBe("Math.pi", "Ok(3.141592653589793)")
})

View File

@ -1,6 +1,6 @@
{ {
"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": {
@ -38,7 +38,8 @@
"dependencies": { "dependencies": {
"@stdlib/stats": "^0.0.13", "@stdlib/stats": "^0.0.13",
"jstat": "^1.9.5", "jstat": "^1.9.5",
"mathjs": "^10.5.2", "lodash": "^4.17.21",
"mathjs": "^10.6.0",
"pdfast": "^0.2.0", "pdfast": "^0.2.0",
"rescript": "^9.1.4" "rescript": "^9.1.4"
}, },
@ -51,20 +52,19 @@
"chalk": "^5.0.1", "chalk": "^5.0.1",
"codecov": "^3.8.3", "codecov": "^3.8.3",
"fast-check": "^2.25.0", "fast-check": "^2.25.0",
"gentype": "^4.3.0", "gentype": "^4.4.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": "^1.2.0", "peggy": "^2.0.1",
"reanalyze": "^2.19.0", "reanalyze": "^2.23.0",
"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.0", "ts-node": "^10.8.1",
"typescript": "^4.6.3", "typescript": "^4.7.3",
"webpack": "^5.72.1", "webpack": "^5.73.0",
"webpack-cli": "^4.9.2" "webpack-cli": "^4.10.0"
}, },
"source": "./src/js/index.ts", "source": "./src/js/index.ts",
"main": "./dist/src/js/index.js", "main": "./dist/src/js/index.js",

View File

@ -11,6 +11,7 @@ import {
import { result, resultMap, Ok } from "./types"; import { result, resultMap, Ok } from "./types";
import { import {
Constructors_mean, Constructors_mean,
Constructors_stdev,
Constructors_sample, Constructors_sample,
Constructors_pdf, Constructors_pdf,
Constructors_cdf, Constructors_cdf,
@ -69,6 +70,10 @@ export class Distribution {
return Constructors_mean({ env: this.env }, this.t); return Constructors_mean({ env: this.env }, this.t);
} }
stdev(): result<number, distributionError> {
return Constructors_stdev({ env: this.env }, this.t);
}
sample(): result<number, distributionError> { sample(): result<number, distributionError> {
return Constructors_sample({ env: this.env }, this.t); return Constructors_sample({ env: this.env }, this.t);
} }

View File

@ -1,19 +1,25 @@
import * as _ from "lodash"; import * as _ from "lodash";
import { import type {
environment, environment,
expressionValue,
externalBindings,
errorValue,
} from "../rescript/TypescriptInterface.gen";
import {
defaultEnvironment, defaultEnvironment,
evaluatePartialUsingExternalBindings, evaluatePartialUsingExternalBindings,
evaluateUsingOptions, evaluateUsingOptions,
externalBindings,
expressionValue,
errorValue,
foreignFunctionInterface, foreignFunctionInterface,
} from "../rescript/TypescriptInterface.gen"; } from "../rescript/TypescriptInterface.gen";
export { export {
makeSampleSetDist, makeSampleSetDist,
errorValueToString, errorValueToString,
distributionErrorToString, distributionErrorToString,
} from "../rescript/TypescriptInterface.gen";
export type {
distributionError, distributionError,
declarationArg,
declaration,
} from "../rescript/TypescriptInterface.gen"; } from "../rescript/TypescriptInterface.gen";
export type { errorValue, externalBindings as bindings, jsImports }; export type { errorValue, externalBindings as bindings, jsImports };
import { import {
@ -28,16 +34,8 @@ import {
import { result, resultMap, tag, tagged } from "./types"; import { result, resultMap, tag, tagged } from "./types";
import { Distribution, shape } from "./distribution"; import { Distribution, shape } from "./distribution";
export { export { Distribution, resultMap, defaultEnvironment };
Distribution, export type { result, shape, environment, lambdaValue, squiggleExpression };
squiggleExpression,
result,
resultMap,
shape,
lambdaValue,
environment,
defaultEnvironment,
};
export let defaultSamplingInputs: environment = { export let defaultSamplingInputs: environment = {
sampleCount: 10000, sampleCount: 10000,
@ -185,5 +183,20 @@ function createTsExport(
return tag("date", x.value); return tag("date", x.value);
case "EvTimeDuration": case "EvTimeDuration":
return tag("timeDuration", x.value); return tag("timeDuration", x.value);
case "EvDeclaration":
return tag("lambdaDeclaration", x.value);
case "EvTypeIdentifier":
return tag("typeIdentifier", x.value);
case "EvModule":
let moduleResult: tagged<
"module",
{ [key: string]: squiggleExpression }
> = tag(
"module",
_.mapValues(x.value, (x: unknown) =>
convertRawToTypescript(x as rescriptExport, environment)
)
);
return moduleResult;
} }
} }

View File

@ -1,5 +1,5 @@
import * as _ from "lodash"; import * as _ from "lodash";
import { import type {
expressionValue, expressionValue,
mixedShape, mixedShape,
sampleSetDist, sampleSetDist,
@ -9,6 +9,8 @@ import {
discreteShape, discreteShape,
continuousShape, continuousShape,
lambdaValue, lambdaValue,
lambdaDeclaration,
declarationArg,
} from "../rescript/TypescriptInterface.gen"; } from "../rescript/TypescriptInterface.gen";
import { Distribution } from "./distribution"; import { Distribution } from "./distribution";
import { tagged, tag } from "./types"; import { tagged, tag } from "./types";
@ -63,6 +65,18 @@ export type rescriptExport =
| { | {
TAG: 11; // EvTimeDuration TAG: 11; // EvTimeDuration
_0: number; _0: number;
}
| {
TAG: 12; // EvDeclaration
_0: rescriptLambdaDeclaration;
}
| {
TAG: 13; // EvTypeIdentifier
_0: string;
}
| {
TAG: 14; // EvModule
_0: { [key: string]: rescriptExport };
}; };
type rescriptDist = type rescriptDist =
@ -84,6 +98,23 @@ type rescriptPointSetDist =
_0: continuousShape; _0: continuousShape;
}; };
type rescriptLambdaDeclaration = {
readonly fn: lambdaValue;
readonly args: rescriptDeclarationArg[];
};
type rescriptDeclarationArg =
| {
TAG: 0; // Float
min: number;
max: number;
}
| {
TAG: 1; // Date
min: Date;
max: Date;
};
export type squiggleExpression = export type squiggleExpression =
| tagged<"symbol", string> | tagged<"symbol", string>
| tagged<"string", string> | tagged<"string", string>
@ -96,7 +127,10 @@ export type squiggleExpression =
| tagged<"number", number> | tagged<"number", number>
| tagged<"date", Date> | tagged<"date", Date>
| tagged<"timeDuration", number> | tagged<"timeDuration", number>
| tagged<"record", { [key: string]: squiggleExpression }>; | tagged<"lambdaDeclaration", lambdaDeclaration>
| tagged<"record", { [key: string]: squiggleExpression }>
| tagged<"typeIdentifier", string>
| tagged<"module", { [key: string]: squiggleExpression }>;
export { lambdaValue }; export { lambdaValue };
@ -141,6 +175,29 @@ export function convertRawToTypescript(
return tag("date", result._0); return tag("date", result._0);
case 11: // EvTimeDuration case 11: // EvTimeDuration
return tag("number", result._0); return tag("number", result._0);
case 12: // EvDeclaration
return tag("lambdaDeclaration", {
fn: result._0.fn,
args: result._0.args.map(convertDeclaration),
});
case 13: // EvSymbol
return tag("typeIdentifier", result._0);
case 14: // EvModule
return tag(
"module",
_.mapValues(result._0, (x) => convertRawToTypescript(x, environment))
);
}
}
function convertDeclaration(
declarationArg: rescriptDeclarationArg
): declarationArg {
switch (declarationArg.TAG) {
case 0: // Float
return tag("Float", { min: declarationArg.min, max: declarationArg.max });
case 1: // Date
return tag("Date", { min: declarationArg.min, max: declarationArg.max });
} }
} }

View File

@ -173,6 +173,11 @@ 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(#Multiply, f)) =>
dist
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Multiply, ~f)
->E.R2.fmap(r => Dist(r))
->OutputLocal.fromResult
| ToDist(Scale(#Logarithm, f)) => | ToDist(Scale(#Logarithm, f)) =>
dist dist
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Logarithm, ~f) ->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Logarithm, ~f)
@ -256,6 +261,8 @@ module Constructors = {
module C = DistributionTypes.Constructors.UsingDists module C = DistributionTypes.Constructors.UsingDists
open OutputLocal open OutputLocal
let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR
let stdev = (~env, dist) => C.stdev(dist)->run(~env)->toFloatR
let variance = (~env, dist) => C.variance(dist)->run(~env)->toFloatR
let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR
let cdf = (~env, dist, f) => C.cdf(dist, f)->run(~env)->toFloatR let cdf = (~env, dist, f) => C.cdf(dist, f)->run(~env)->toFloatR
let inv = (~env, dist, f) => C.inv(dist, f)->run(~env)->toFloatR let inv = (~env, dist, f) => C.inv(dist, f)->run(~env)->toFloatR
@ -298,6 +305,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

@ -49,6 +49,10 @@ module Constructors: {
@genType @genType
let mean: (~env: env, genericDist) => result<float, error> let mean: (~env: env, genericDist) => result<float, error>
@genType @genType
let stdev: (~env: env, genericDist) => result<float, error>
@genType
let variance: (~env: env, genericDist) => result<float, error>
@genType
let sample: (~env: env, genericDist) => result<float, error> let sample: (~env: env, genericDist) => result<float, error>
@genType @genType
let cdf: (~env: env, genericDist, float) => result<float, error> let cdf: (~env: env, genericDist, float) => result<float, error>
@ -127,6 +131,8 @@ module Constructors: {
@genType @genType
let scaleLogarithm: (~env: env, genericDist, float) => result<genericDist, error> let scaleLogarithm: (~env: env, genericDist, float) => result<genericDist, error>
@genType @genType
let scaleMultiply: (~env: env, genericDist, float) => result<genericDist, error>
@genType
let scalePower: (~env: env, genericDist, float) => result<genericDist, error> let scalePower: (~env: env, genericDist, float) => result<genericDist, error>
@genType @genType
let pointwiseAdd: (~env: env, genericDist, genericDist) => result<genericDist, error> let pointwiseAdd: (~env: env, genericDist, genericDist) => result<genericDist, error>

View File

@ -30,9 +30,9 @@ module Error = {
@genType @genType
let toString = (err: error): string => let toString = (err: error): string =>
switch err { switch err {
| NotYetImplemented => "Function Not Yet Implemented" | NotYetImplemented => "Function not yet implemented"
| Unreachable => "Unreachable" | Unreachable => "Unreachable"
| DistributionVerticalShiftIsInvalid => "Distribution Vertical Shift is Invalid" | DistributionVerticalShiftIsInvalid => "Distribution vertical shift is invalid"
| ArgumentError(s) => `Argument Error ${s}` | ArgumentError(s) => `Argument Error ${s}`
| LogarithmOfDistributionError(s) => `Logarithm of input error: ${s}` | LogarithmOfDistributionError(s) => `Logarithm of input error: ${s}`
| SampleSetError(TooFewSamples) => "Too Few Samples" | SampleSetError(TooFewSamples) => "Too Few Samples"
@ -68,9 +68,15 @@ module DistributionOperation = {
| #Mean | #Mean
| #Sample | #Sample
| #IntegralSum | #IntegralSum
| #Mode
| #Stdev
| #Min
| #Max
| #Variance
] ]
type toScaleFn = [ type toScaleFn = [
| #Multiply
| #Power | #Power
| #Logarithm | #Logarithm
| #LogarithmWithThreshold(float) | #LogarithmWithThreshold(float)
@ -119,6 +125,11 @@ module DistributionOperation = {
| 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(#Max) => `max`
| ToFloat(#Stdev) => `stdev`
| ToFloat(#Variance) => `variance`
| 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`
@ -129,11 +140,12 @@ module DistributionOperation = {
| ToDist(Truncate(_, _)) => `truncate` | ToDist(Truncate(_, _)) => `truncate`
| ToDist(Inspect) => `inspect` | ToDist(Inspect) => `inspect`
| ToDist(Scale(#Power, r)) => `scalePower(${E.Float.toFixed(r)})` | ToDist(Scale(#Power, r)) => `scalePower(${E.Float.toFixed(r)})`
| ToDist(Scale(#Multiply, r)) => `scaleMultiply(${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)) => `toSparkline(${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`
@ -152,6 +164,8 @@ 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 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)
@ -197,6 +211,7 @@ module Constructors = {
estimate, estimate,
) )
} }
let scaleMultiply = (dist, n): t => FromDist(ToDist(Scale(#Multiply, n)), dist)
let scalePower = (dist, n): t => FromDist(ToDist(Scale(#Power, 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 scaleLogarithm = (dist, n): t => FromDist(ToDist(Scale(#Logarithm, n)), dist)
let scaleLogarithmWithThreshold = (dist, n, eps): t => FromDist( let scaleLogarithmWithThreshold = (dist, n, eps): t => FromDist(

View File

@ -68,7 +68,7 @@ let toFloatOperation = (
) => { ) => {
switch distToFloatOperation { switch distToFloatOperation {
| #IntegralSum => Ok(integralEndY(t)) | #IntegralSum => Ok(integralEndY(t))
| (#Pdf(_) | #Cdf(_) | #Inv(_) | #Mean | #Sample) as op => { | (#Pdf(_) | #Cdf(_) | #Inv(_) | #Mean | #Sample | #Min | #Max) as op => {
let trySymbolicSolution = switch (t: t) { let trySymbolicSolution = switch (t: t) {
| Symbolic(r) => SymbolicDist.T.operate(op, r)->E.R.toOption | Symbolic(r) => SymbolicDist.T.operate(op, r)->E.R.toOption
| _ => None | _ => None
@ -78,6 +78,8 @@ let toFloatOperation = (
| (SampleSet(sampleSet), #Mean) => SampleSetDist.mean(sampleSet)->Some | (SampleSet(sampleSet), #Mean) => SampleSetDist.mean(sampleSet)->Some
| (SampleSet(sampleSet), #Sample) => SampleSetDist.sample(sampleSet)->Some | (SampleSet(sampleSet), #Sample) => SampleSetDist.sample(sampleSet)->Some
| (SampleSet(sampleSet), #Inv(r)) => SampleSetDist.percentile(sampleSet, r)->Some | (SampleSet(sampleSet), #Inv(r)) => SampleSetDist.percentile(sampleSet, r)->Some
| (SampleSet(sampleSet), #Min) => SampleSetDist.min(sampleSet)->Some
| (SampleSet(sampleSet), #Max) => SampleSetDist.max(sampleSet)->Some
| _ => None | _ => None
} }
@ -90,6 +92,16 @@ let toFloatOperation = (
} }
} }
} }
| (#Stdev | #Variance | #Mode) as op =>
switch t {
| SampleSet(s) =>
switch op {
| #Stdev => SampleSetDist.stdev(s)->Ok
| #Variance => SampleSetDist.variance(s)->Ok
| #Mode => SampleSetDist.mode(s)->Ok
}
| _ => Error(DistributionTypes.NotYetImplemented)
}
} }
} }
@ -244,7 +256,9 @@ module Truncate = {
| Some(r) => Ok(r) | Some(r) => Ok(r)
| None => | None =>
toPointSetFn(t)->E.R2.fmap(t => { toPointSetFn(t)->E.R2.fmap(t => {
DistributionTypes.PointSet(PointSetDist.T.truncate(leftCutoff, rightCutoff, t)) DistributionTypes.PointSet(
PointSetDist.T.truncate(leftCutoff, rightCutoff, t)->PointSetDist.T.normalize,
)
}) })
} }
} }
@ -323,7 +337,7 @@ module AlgebraicCombination = {
let fn = Operation.Algebraic.toFn(arithmeticOperation) let fn = Operation.Algebraic.toFn(arithmeticOperation)
E.R.merge(toSampleSet(t1), toSampleSet(t2)) E.R.merge(toSampleSet(t1), toSampleSet(t2))
->E.R.bind(((t1, t2)) => { ->E.R.bind(((t1, t2)) => {
SampleSetDist.map2(~fn, ~t1, ~t2)->E.R2.errMap(x => DistributionTypes.OperationError(x)) SampleSetDist.map2(~fn, ~t1, ~t2)->E.R2.errMap(x => DistributionTypes.SampleSetError(x))
}) })
->E.R2.fmap(r => DistributionTypes.SampleSet(r)) ->E.R2.fmap(r => DistributionTypes.SampleSet(r))
} }

View File

@ -225,9 +225,8 @@ let doN = (n, fn) => {
} }
let sample = (t: t): float => { let sample = (t: t): float => {
let randomItem = Random.float(1.) let randomItem = Random.float(1.0)
let bar = t |> T.Integral.yToX(randomItem) t |> T.Integral.yToX(randomItem)
bar
} }
let isFloat = (t: t) => let isFloat = (t: t) =>
@ -249,6 +248,8 @@ let operate = (distToFloatOp: Operation.distToFloatOperation, s): float =>
| #Inv(f) => inv(f, s) | #Inv(f) => inv(f, s)
| #Sample => sample(s) | #Sample => sample(s)
| #Mean => T.mean(s) | #Mean => T.mean(s)
| #Min => T.minX(s)
| #Max => T.maxX(s)
} }
let toSparkline = (t: t, bucketCount): result<string, PointSetTypes.sparklineError> => let toSparkline = (t: t, bucketCount): result<string, PointSetTypes.sparklineError> =>

View File

@ -20,6 +20,14 @@ module Error = {
} }
let fromOperationError = e => OperationError(e) let fromOperationError = e => OperationError(e)
let toString = (err: sampleSetError) => {
switch err {
| TooFewSamples => "Too few samples when constructing sample set"
| NonNumericInput(err) => `Found a non-number in input: ${err}`
| OperationError(err) => Operation.Error.toString(err)
}
}
} }
include Error include Error
@ -87,30 +95,27 @@ let sampleN = (t: t, n) => {
} }
} }
let _fromSampleResultArray = (samples: array<result<float, QuriSquiggleLang.Operation.Error.t>>) =>
E.A.R.firstErrorOrOpen(samples)->E.R2.errMap(Error.fromOperationError) |> E.R2.bind(make)
let samplesMap = (~fn: float => result<float, Operation.Error.t>, t: t): result< let samplesMap = (~fn: float => result<float, Operation.Error.t>, t: t): result<
t, t,
sampleSetError, sampleSetError,
> => { > => T.get(t)->E.A2.fmap(fn)->_fromSampleResultArray
let samples = T.get(t)->E.A2.fmap(fn)
E.A.R.firstErrorOrOpen(samples)->E.R2.errMap(Error.fromOperationError) |> E.R2.bind(make)
}
//TODO: Figure out what to do if distributions are different lengths. ``zip`` is kind of inelegant for this. //TODO: Figure out what to do if distributions are different lengths. ``zip`` is kind of inelegant for this.
let map2 = (~fn: (float, float) => result<float, Operation.Error.t>, ~t1: t, ~t2: t): result< let map2 = (~fn: (float, float) => result<float, Operation.Error.t>, ~t1: t, ~t2: t): result<
t, t,
Operation.Error.t, sampleSetError,
> => { > => E.A.zip(get(t1), get(t2))->E.A2.fmap(E.Tuple2.toFnCall(fn))->_fromSampleResultArray
let samples = Belt.Array.zip(get(t1), get(t2))->E.A2.fmap(((a, b)) => fn(a, b))
// This assertion should never be reached. In order for it to be reached, one let map3 = (
// of the input parameters would need to be a sample set distribution with less ~fn: (float, float, float) => result<float, Operation.Error.t>,
// than 6 samples. Which should be impossible due to the smart constructor. ~t1: t,
// I could prove this to the type system (say, creating a {first: float, second: float, ..., fifth: float, rest: array<float>} ~t2: t,
// But doing so would take too much time, so I'll leave it as an assertion ~t3: t,
E.A.R.firstErrorOrOpen(samples)->E.R2.fmap(x => ): result<t, sampleSetError> =>
E.R.toExnFnString(Error.sampleSetErrorToString, make(x)) E.A.zip3(get(t1), get(t2), get(t3))->E.A2.fmap(E.Tuple3.toFnCall(fn))->_fromSampleResultArray
)
}
let mean = t => T.get(t)->E.A.Floats.mean let mean = t => T.get(t)->E.A.Floats.mean
let geomean = t => T.get(t)->E.A.Floats.geomean let geomean = t => T.get(t)->E.A.Floats.geomean

View File

@ -295,7 +295,7 @@ module Float = {
let inv = (p, t: t) => p < t ? 0.0 : 1.0 let inv = (p, t: t) => p < t ? 0.0 : 1.0
let mean = (t: t) => Ok(t) let mean = (t: t) => Ok(t)
let sample = (t: t) => t let sample = (t: t) => t
let toString = (t: t) => j`Delta($t)` let toString = (t: t) => j`PointMass($t)`
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Discrete( let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Discrete(
Discrete.make(~integralSumCache=Some(1.0), {xs: [t], ys: [1.0]}), Discrete.make(~integralSumCache=Some(1.0), {xs: [t], ys: [1.0]}),
) )
@ -449,6 +449,8 @@ module T = {
| #Cdf(f) => Ok(cdf(f, s)) | #Cdf(f) => Ok(cdf(f, s))
| #Pdf(f) => Ok(pdf(f, s)) | #Pdf(f) => Ok(pdf(f, s))
| #Inv(f) => Ok(inv(f, s)) | #Inv(f) => Ok(inv(f, s))
| #Min => Ok(min(s))
| #Max => Ok(max(s))
| #Sample => Ok(sample(s)) | #Sample => Ok(sample(s))
| #Mean => mean(s) | #Mean => mean(s)
} }

View File

@ -8,9 +8,13 @@ type rec frType =
| FRTypeNumber | FRTypeNumber
| FRTypeNumeric | FRTypeNumeric
| FRTypeDistOrNumber | FRTypeDistOrNumber
| FRTypeLambda
| FRTypeRecord(frTypeRecord) | FRTypeRecord(frTypeRecord)
| FRTypeArray(array<frType>) | FRTypeDict(frType)
| FRTypeOption(frType) | FRTypeArray(frType)
| FRTypeString
| FRTypeAny
| FRTypeVariant(array<string>)
and frTypeRecord = array<frTypeRecordParam> and frTypeRecord = array<frTypeRecordParam>
and frTypeRecordParam = (string, frType) and frTypeRecordParam = (string, frType)
@ -21,11 +25,17 @@ and frTypeRecordParam = (string, frType)
type rec frValue = type rec frValue =
| FRValueNumber(float) | FRValueNumber(float)
| FRValueDist(DistributionTypes.genericDist) | FRValueDist(DistributionTypes.genericDist)
| FRValueOption(option<frValue>) | FRValueArray(array<frValue>)
| FRValueDistOrNumber(frValueDistOrNumber) | FRValueDistOrNumber(frValueDistOrNumber)
| FRValueRecord(frValueRecord) | FRValueRecord(frValueRecord)
| FRValueLambda(ReducerInterface_ExpressionValue.lambdaValue)
| FRValueString(string)
| FRValueVariant(string)
| FRValueAny(frValue)
| FRValueDict(Js.Dict.t<frValue>)
and frValueRecord = array<frValueRecordParam> and frValueRecord = array<frValueRecordParam>
and frValueRecordParam = (string, frValue) and frValueRecordParam = (string, frValue)
and frValueDictParam = (string, frValue)
and frValueDistOrNumber = FRValueNumber(float) | FRValueDist(DistributionTypes.genericDist) and frValueDistOrNumber = FRValueNumber(float) | FRValueDist(DistributionTypes.genericDist)
type fnDefinition = { type fnDefinition = {
@ -37,6 +47,9 @@ type fnDefinition = {
type function = { type function = {
name: string, name: string,
definitions: array<fnDefinition>, definitions: array<fnDefinition>,
examples: option<string>,
description: option<string>,
isExperimental: bool,
} }
type registry = array<function> type registry = array<function>
@ -47,17 +60,39 @@ module FRType = {
switch t { switch t {
| FRTypeNumber => "number" | FRTypeNumber => "number"
| FRTypeNumeric => "numeric" | FRTypeNumeric => "numeric"
| FRTypeDistOrNumber => "frValueDistOrNumber" | FRTypeDistOrNumber => "distribution|number"
| FRTypeRecord(r) => { | FRTypeRecord(r) => {
let input = ((name, frType): frTypeRecordParam) => `${name}: ${toString(frType)}` let input = ((name, frType): frTypeRecordParam) => `${name}: ${toString(frType)}`
`record({${r->E.A2.fmap(input)->E.A2.joinWith(", ")}})` `{${r->E.A2.fmap(input)->E.A2.joinWith(", ")}}`
} }
| FRTypeArray(r) => `record(${r->E.A2.fmap(toString)->E.A2.joinWith(", ")})` | FRTypeArray(r) => `list(${toString(r)})`
| FRTypeOption(v) => `option(${toString(v)})` | FRTypeLambda => `lambda`
| FRTypeString => `string`
| FRTypeVariant(_) => "variant"
| FRTypeDict(r) => `dict(${toString(r)})`
| FRTypeAny => `any`
}
let rec toFrValue = (r: expressionValue): option<frValue> =>
switch r {
| EvNumber(f) => Some(FRValueNumber(f))
| EvString(f) => Some(FRValueString(f))
| EvDistribution(f) => Some(FRValueDistOrNumber(FRValueDist(f)))
| EvLambda(f) => Some(FRValueLambda(f))
| EvArray(elements) =>
elements->E.A2.fmap(toFrValue)->E.A.O.openIfAllSome->E.O2.fmap(r => FRValueArray(r))
| EvRecord(record) =>
Js.Dict.entries(record)
->E.A2.fmap(((key, item)) => item->toFrValue->E.O2.fmap(o => (key, o)))
->E.A.O.openIfAllSome
->E.O2.fmap(r => FRValueRecord(r))
| _ => None
} }
let rec matchWithExpressionValue = (t: t, r: expressionValue): option<frValue> => let rec matchWithExpressionValue = (t: t, r: expressionValue): option<frValue> =>
switch (t, r) { switch (t, r) {
| (FRTypeAny, f) => toFrValue(f)
| (FRTypeString, EvString(f)) => Some(FRValueString(f))
| (FRTypeNumber, EvNumber(f)) => Some(FRValueNumber(f)) | (FRTypeNumber, EvNumber(f)) => Some(FRValueNumber(f))
| (FRTypeDistOrNumber, EvNumber(f)) => Some(FRValueDistOrNumber(FRValueNumber(f))) | (FRTypeDistOrNumber, EvNumber(f)) => Some(FRValueDistOrNumber(FRValueNumber(f)))
| (FRTypeDistOrNumber, EvDistribution(Symbolic(#Float(f)))) => | (FRTypeDistOrNumber, EvDistribution(Symbolic(#Float(f)))) =>
@ -65,7 +100,17 @@ module FRType = {
| (FRTypeDistOrNumber, EvDistribution(f)) => Some(FRValueDistOrNumber(FRValueDist(f))) | (FRTypeDistOrNumber, EvDistribution(f)) => Some(FRValueDistOrNumber(FRValueDist(f)))
| (FRTypeNumeric, EvNumber(f)) => Some(FRValueNumber(f)) | (FRTypeNumeric, EvNumber(f)) => Some(FRValueNumber(f))
| (FRTypeNumeric, EvDistribution(Symbolic(#Float(f)))) => Some(FRValueNumber(f)) | (FRTypeNumeric, EvDistribution(Symbolic(#Float(f)))) => Some(FRValueNumber(f))
| (FRTypeOption(v), _) => Some(FRValueOption(matchWithExpressionValue(v, r))) | (FRTypeLambda, EvLambda(f)) => Some(FRValueLambda(f))
| (FRTypeArray(intendedType), EvArray(elements)) => {
let el = elements->E.A2.fmap(matchWithExpressionValue(intendedType))
E.A.O.openIfAllSome(el)->E.O2.fmap(r => FRValueArray(r))
}
| (FRTypeDict(r), EvRecord(record)) =>
record
->Js.Dict.entries
->E.A2.fmap(((key, item)) => matchWithExpressionValue(r, item)->E.O2.fmap(o => (key, o)))
->E.A.O.openIfAllSome
->E.O2.fmap(r => FRValueDict(Js.Dict.fromArray(r)))
| (FRTypeRecord(recordParams), EvRecord(record)) => { | (FRTypeRecord(recordParams), EvRecord(record)) => {
let getAndMatch = (name, input) => let getAndMatch = (name, input) =>
E.Dict.get(record, name)->E.O.bind(matchWithExpressionValue(input)) E.Dict.get(record, name)->E.O.bind(matchWithExpressionValue(input))
@ -80,6 +125,32 @@ module FRType = {
| _ => None | _ => None
} }
let rec matchReverse = (e: frValue): expressionValue =>
switch e {
| FRValueNumber(f) => EvNumber(f)
| FRValueDistOrNumber(FRValueNumber(n)) => EvNumber(n)
| FRValueDistOrNumber(FRValueDist(n)) => EvDistribution(n)
| FRValueDist(dist) => EvDistribution(dist)
| FRValueArray(elements) => EvArray(elements->E.A2.fmap(matchReverse))
| FRValueRecord(frValueRecord) => {
let record =
frValueRecord->E.A2.fmap(((name, value)) => (name, matchReverse(value)))->E.Dict.fromArray
EvRecord(record)
}
| FRValueDict(frValueRecord) => {
let record =
frValueRecord
->Js.Dict.entries
->E.A2.fmap(((name, value)) => (name, matchReverse(value)))
->E.Dict.fromArray
EvRecord(record)
}
| FRValueLambda(l) => EvLambda(l)
| FRValueString(string) => EvString(string)
| FRValueVariant(string) => EvString(string)
| FRValueAny(f) => matchReverse(f)
}
let matchWithExpressionValueArray = (inputs: array<t>, args: array<expressionValue>): option< let matchWithExpressionValueArray = (inputs: array<t>, args: array<expressionValue>): option<
array<frValue>, array<frValue>,
> => { > => {
@ -263,13 +334,34 @@ module FnDefinition = {
module Function = { module Function = {
type t = function type t = function
let make = (~name, ~definitions): t => { type functionJson = {
name: string,
definitions: array<string>,
examples: option<string>,
description: option<string>,
isExperimental: bool,
}
let make = (~name, ~definitions, ~examples=?, ~description=?, ~isExperimental=false, ()): t => {
name: name, name: name,
definitions: definitions, definitions: definitions,
examples: examples,
isExperimental: isExperimental,
description: description,
}
let toJson = (t: t): functionJson => {
name: t.name,
definitions: t.definitions->E.A2.fmap(FnDefinition.toString),
examples: t.examples,
description: t.description,
isExperimental: t.isExperimental,
} }
} }
module Registry = { module Registry = {
let toJson = (r: registry) => r->E.A2.fmap(Function.toJson)
/* /*
There's a (potential+minor) bug here: If a function definition is called outside of the calls There's a (potential+minor) bug here: If a function definition is called outside of the calls
to the registry, then it's possible that there could be a match after the registry is to the registry, then it's possible that there could be a match after the registry is
@ -282,6 +374,7 @@ module Registry = {
~env: DistributionOperation.env, ~env: DistributionOperation.env,
) => { ) => {
let matchToDef = m => Matcher.Registry.matchToDef(registry, m) let matchToDef = m => Matcher.Registry.matchToDef(registry, m)
//Js.log(toSimple(registry))
let showNameMatchDefinitions = matches => { let showNameMatchDefinitions = matches => {
let defs = let defs =
matches matches

View File

@ -1,58 +0,0 @@
type expressionValue = ReducerInterface_ExpressionValue.expressionValue
type rec frType =
| FRTypeNumber
| FRTypeNumeric
| FRTypeDistOrNumber
| FRTypeRecord(frTypeRecord)
| FRTypeArray(array<frType>)
| FRTypeOption(frType)
and frTypeRecord = array<frTypeRecordParam>
and frTypeRecordParam = (string, frType)
type rec frValue =
| FRValueNumber(float)
| FRValueDist(DistributionTypes.genericDist)
| FRValueOption(option<frValue>)
| FRValueDistOrNumber(frValueDistOrNumber)
| FRValueRecord(frValueRecord)
and frValueRecord = array<frValueRecordParam>
and frValueRecordParam = (string, frValue)
and frValueDistOrNumber = FRValueNumber(float) | FRValueDist(DistributionTypes.genericDist)
type fnDefinition = {
name: string,
inputs: array<frType>,
run: (array<frValue>, DistributionOperation.env) => result<expressionValue, string>,
}
type function = {
name: string,
definitions: array<fnDefinition>,
}
type registry = array<function>
// Note: The function "name" is just used for documentation purposes
module Function: {
type t = function
let make: (~name: string, ~definitions: array<fnDefinition>) => t
}
module FnDefinition: {
type t = fnDefinition
let make: (
~name: string,
~inputs: array<frType>,
~run: (array<frValue>, DistributionOperation.env) => result<expressionValue, string>,
) => t
}
module Registry: {
let matchAndRun: (
~registry: registry,
~fnName: string,
~args: array<expressionValue>,
~env: QuriSquiggleLang.DistributionOperation.env,
) => option<result<expressionValue, string>>
}

View File

@ -5,10 +5,17 @@ let impossibleError = "Wrong inputs / Logically impossible"
module Wrappers = { module Wrappers = {
let symbolic = r => DistributionTypes.Symbolic(r) let symbolic = r => DistributionTypes.Symbolic(r)
let evDistribution = r => ReducerInterface_ExpressionValue.EvDistribution(r) let evDistribution = r => ReducerInterface_ExpressionValue.EvDistribution(r)
let evNumber = r => ReducerInterface_ExpressionValue.EvNumber(r)
let evArray = r => ReducerInterface_ExpressionValue.EvArray(r)
let evRecord = r => ReducerInterface_ExpressionValue.EvRecord(r)
let evString = r => ReducerInterface_ExpressionValue.EvString(r)
let symbolicEvDistribution = r => r->DistributionTypes.Symbolic->evDistribution let symbolicEvDistribution = r => r->DistributionTypes.Symbolic->evDistribution
} }
let getOrError = (a, g) => E.A.get(a, g) |> E.O.toResult(impossibleError)
module Prepare = { module Prepare = {
type t = frValue
type ts = array<frValue> type ts = array<frValue>
type err = string type err = string
@ -19,6 +26,26 @@ module Prepare = {
| [FRValueRecord([(_, n1), (_, n2)])] => Ok([n1, n2]) | [FRValueRecord([(_, n1), (_, n2)])] => Ok([n1, n2])
| _ => Error(impossibleError) | _ => Error(impossibleError)
} }
let toArgs = (inputs: ts): result<ts, err> =>
switch inputs {
| [FRValueRecord(args)] => args->E.A2.fmap(((_, b)) => b)->Ok
| _ => Error(impossibleError)
}
}
module Array = {
let openA = (inputs: t): result<ts, err> =>
switch inputs {
| FRValueArray(n) => Ok(n)
| _ => Error(impossibleError)
}
let arrayOfArrays = (inputs: t): result<array<ts>, err> =>
switch inputs {
| FRValueArray(n) => n->E.A2.fmap(openA)->E.A.R.firstErrorOrOpen
| _ => Error(impossibleError)
}
} }
} }
@ -30,6 +57,20 @@ module Prepare = {
} }
} }
let twoNumbers = (values: ts): result<(float, float), err> => {
switch values {
| [FRValueNumber(a1), FRValueNumber(a2)] => Ok(a1, a2)
| _ => Error(impossibleError)
}
}
let threeNumbers = (values: ts): result<(float, float, float), err> => {
switch values {
| [FRValueNumber(a1), FRValueNumber(a2), FRValueNumber(a3)] => Ok(a1, a2, a3)
| _ => Error(impossibleError)
}
}
let oneDistOrNumber = (values: ts): result<frValueDistOrNumber, err> => { let oneDistOrNumber = (values: ts): result<frValueDistOrNumber, err> => {
switch values { switch values {
| [FRValueDistOrNumber(a1)] => Ok(a1) | [FRValueDistOrNumber(a1)] => Ok(a1)
@ -42,6 +83,46 @@ module Prepare = {
values->ToValueArray.Record.twoArgs->E.R.bind(twoDistOrNumber) values->ToValueArray.Record.twoArgs->E.R.bind(twoDistOrNumber)
} }
} }
module ToArrayRecordPairs = {
let twoArgs = (input: t): result<array<ts>, err> => {
let array = input->ToValueArray.Array.openA
let pairs =
array->E.R.bind(pairs =>
pairs
->E.A2.fmap(xyCoord => [xyCoord]->ToValueArray.Record.twoArgs)
->E.A.R.firstErrorOrOpen
)
pairs
}
}
let oneNumber = (values: t): result<float, err> => {
switch values {
| FRValueNumber(a1) => Ok(a1)
| _ => Error(impossibleError)
}
}
let oneDict = (values: t): result<Js.Dict.t<frValue>, err> => {
switch values {
| FRValueDict(a1) => Ok(a1)
| _ => Error(impossibleError)
}
}
module ToTypedArray = {
let numbers = (inputs: ts): result<array<float>, err> => {
let openNumbers = (elements: array<t>) =>
elements->E.A2.fmap(oneNumber)->E.A.R.firstErrorOrOpen
inputs->getOrError(0)->E.R.bind(ToValueArray.Array.openA)->E.R.bind(openNumbers)
}
let dicts = (inputs: ts): Belt.Result.t<array<Js.Dict.t<frValue>>, err> => {
let openDicts = (elements: array<t>) => elements->E.A2.fmap(oneDict)->E.A.R.firstErrorOrOpen
inputs->getOrError(0)->E.R.bind(ToValueArray.Array.openA)->E.R.bind(openDicts)
}
}
} }
module Process = { module Process = {
@ -75,7 +156,7 @@ module Process = {
| Ok((t1, t2)) => | Ok((t1, t2)) =>
switch SampleSetDist.map2(~fn=altFn, ~t1, ~t2) { switch SampleSetDist.map2(~fn=altFn, ~t1, ~t2) {
| Ok(r) => Ok(DistributionTypes.SampleSet(r)) | Ok(r) => Ok(DistributionTypes.SampleSet(r))
| Error(r) => Error(Operation.Error.toString(r)) | Error(r) => Error(SampleSetDist.Error.toString(r))
} }
| Error(r) => Error(DistributionTypes.Error.toString(r)) | Error(r) => Error(DistributionTypes.Error.toString(r))
} }
@ -148,9 +229,36 @@ module OneArgDist = {
->E.R.bind(Process.DistOrNumberToDist.oneValueUsingSymbolicDist(~fn, ~value=_, ~env)) ->E.R.bind(Process.DistOrNumberToDist.oneValueUsingSymbolicDist(~fn, ~value=_, ~env))
->E.R2.fmap(Wrappers.evDistribution) ->E.R2.fmap(Wrappers.evDistribution)
let make = (name, fn) => { let make = (name, fn) =>
FnDefinition.make(~name, ~inputs=[FRTypeDistOrNumber], ~run=(inputs, env) => FnDefinition.make(~name, ~inputs=[FRTypeDistOrNumber], ~run=(inputs, env) =>
inputs->Prepare.ToValueTuple.oneDistOrNumber->process(~fn, ~env) inputs->Prepare.ToValueTuple.oneDistOrNumber->process(~fn, ~env)
) )
}
module ArrayNumberDist = {
let make = (name, fn) => {
FnDefinition.make(~name, ~inputs=[FRTypeArray(FRTypeNumber)], ~run=(inputs, _) =>
Prepare.ToTypedArray.numbers(inputs)
->E.R.bind(r => E.A.length(r) === 0 ? Error("List is empty") : Ok(r))
->E.R.bind(fn)
)
}
let make2 = (name, fn) => {
FnDefinition.make(~name, ~inputs=[FRTypeArray(FRTypeAny)], ~run=(inputs, _) =>
Prepare.ToTypedArray.numbers(inputs)
->E.R.bind(r => E.A.length(r) === 0 ? Error("List is empty") : Ok(r))
->E.R.bind(fn)
)
} }
} }
module NumberToNumber = {
let make = (name, fn) =>
FnDefinition.make(~name, ~inputs=[FRTypeNumber], ~run=(inputs, _) => {
inputs
->getOrError(0)
->E.R.bind(Prepare.oneNumber)
->E.R2.fmap(fn)
->E.R2.fmap(Wrappers.evNumber)
})
}

View File

@ -3,9 +3,87 @@ open FunctionRegistry_Helpers
let twoArgs = E.Tuple2.toFnCall let twoArgs = E.Tuple2.toFnCall
module Declaration = {
let frType = FRTypeRecord([
("fn", FRTypeLambda),
("inputs", FRTypeArray(FRTypeRecord([("min", FRTypeNumber), ("max", FRTypeNumber)]))),
])
let fromExpressionValue = (e: frValue): result<expressionValue, string> => {
switch FunctionRegistry_Helpers.Prepare.ToValueArray.Record.twoArgs([e]) {
| Ok([FRValueLambda(lambda), FRValueArray(inputs)]) => {
open FunctionRegistry_Helpers.Prepare
let getMinMax = arg =>
ToValueArray.Record.toArgs([arg])
->E.R.bind(ToValueTuple.twoNumbers)
->E.R2.fmap(((min, max)) => Declaration.ContinuousFloatArg.make(min, max))
inputs
->E.A2.fmap(getMinMax)
->E.A.R.firstErrorOrOpen
->E.R2.fmap(args => ReducerInterface_ExpressionValue.EvDeclaration(
Declaration.make(lambda, args),
))
}
| Error(r) => Error(r)
| Ok(_) => Error(FunctionRegistry_Helpers.impossibleError)
}
}
}
let inputsTodist = (inputs: array<FunctionRegistry_Core.frValue>, makeDist) => {
let array = inputs->getOrError(0)->E.R.bind(Prepare.ToValueArray.Array.openA)
let xyCoords =
array->E.R.bind(xyCoords =>
xyCoords
->E.A2.fmap(xyCoord =>
[xyCoord]->Prepare.ToValueArray.Record.twoArgs->E.R.bind(Prepare.ToValueTuple.twoNumbers)
)
->E.A.R.firstErrorOrOpen
)
let expressionValue =
xyCoords
->E.R.bind(r => r->XYShape.T.makeFromZipped->E.R2.errMap(XYShape.Error.toString))
->E.R2.fmap(r => ReducerInterface_ExpressionValue.EvDistribution(PointSet(makeDist(r))))
expressionValue
}
let registry = [ let registry = [
Function.make(
~name="toContinuousPointSet",
~definitions=[
FnDefinition.make(
~name="toContinuousPointSet",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(inputs, _) => inputsTodist(inputs, r => Continuous(Continuous.make(r))),
),
],
(),
),
Function.make(
~name="toDiscretePointSet",
~definitions=[
FnDefinition.make(
~name="toDiscretePointSet",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(inputs, _) => inputsTodist(inputs, r => Discrete(Discrete.make(r))),
),
],
(),
),
Function.make(
~name="Declaration",
~definitions=[
FnDefinition.make(~name="declareFn", ~inputs=[Declaration.frType], ~run=(inputs, _) => {
inputs->getOrError(0)->E.R.bind(Declaration.fromExpressionValue)
}),
],
(),
),
Function.make( Function.make(
~name="Normal", ~name="Normal",
~examples=`normal(5,1)
normal({p5: 4, p95: 10})
normal({mean: 5, stdev: 2})`,
~definitions=[ ~definitions=[
TwoArgDist.make("normal", twoArgs(SymbolicDist.Normal.make)), TwoArgDist.make("normal", twoArgs(SymbolicDist.Normal.make)),
TwoArgDist.makeRecordP5P95("normal", r => TwoArgDist.makeRecordP5P95("normal", r =>
@ -13,9 +91,13 @@ let registry = [
), ),
TwoArgDist.makeRecordMeanStdev("normal", twoArgs(SymbolicDist.Normal.make)), TwoArgDist.makeRecordMeanStdev("normal", twoArgs(SymbolicDist.Normal.make)),
], ],
(),
), ),
Function.make( Function.make(
~name="Lognormal", ~name="Lognormal",
~examples=`lognormal(0.5, 0.8)
lognormal({p5: 4, p95: 10})
lognormal({mean: 5, stdev: 2})`,
~definitions=[ ~definitions=[
TwoArgDist.make("lognormal", twoArgs(SymbolicDist.Lognormal.make)), TwoArgDist.make("lognormal", twoArgs(SymbolicDist.Lognormal.make)),
TwoArgDist.makeRecordP5P95("lognormal", r => TwoArgDist.makeRecordP5P95("lognormal", r =>
@ -23,29 +105,43 @@ let registry = [
), ),
TwoArgDist.makeRecordMeanStdev("lognormal", twoArgs(SymbolicDist.Lognormal.fromMeanAndStdev)), TwoArgDist.makeRecordMeanStdev("lognormal", twoArgs(SymbolicDist.Lognormal.fromMeanAndStdev)),
], ],
(),
), ),
Function.make( Function.make(
~name="Uniform", ~name="Uniform",
~examples=`uniform(10, 12)`,
~definitions=[TwoArgDist.make("uniform", twoArgs(SymbolicDist.Uniform.make))], ~definitions=[TwoArgDist.make("uniform", twoArgs(SymbolicDist.Uniform.make))],
(),
), ),
Function.make( Function.make(
~name="Beta", ~name="Beta",
~examples=`beta(20, 25)`,
~definitions=[TwoArgDist.make("beta", twoArgs(SymbolicDist.Beta.make))], ~definitions=[TwoArgDist.make("beta", twoArgs(SymbolicDist.Beta.make))],
(),
), ),
Function.make( Function.make(
~name="Cauchy", ~name="Cauchy",
~examples=`cauchy(5, 1)`,
~definitions=[TwoArgDist.make("cauchy", twoArgs(SymbolicDist.Cauchy.make))], ~definitions=[TwoArgDist.make("cauchy", twoArgs(SymbolicDist.Cauchy.make))],
(),
), ),
Function.make( Function.make(
~name="Gamma", ~name="Gamma",
~examples=`gamma(5, 1)`,
~definitions=[TwoArgDist.make("gamma", twoArgs(SymbolicDist.Gamma.make))], ~definitions=[TwoArgDist.make("gamma", twoArgs(SymbolicDist.Gamma.make))],
(),
), ),
Function.make( Function.make(
~name="Logistic", ~name="Logistic",
~examples=`gamma(5, 1)`,
~definitions=[TwoArgDist.make("logistic", twoArgs(SymbolicDist.Logistic.make))], ~definitions=[TwoArgDist.make("logistic", twoArgs(SymbolicDist.Logistic.make))],
(),
), ),
Function.make( Function.make(
~name="To", ~name="To (Distribution)",
~examples=`5 to 10
to(5,10)
-5 to 5`,
~definitions=[ ~definitions=[
TwoArgDist.make("to", twoArgs(SymbolicDist.From90thPercentile.make)), TwoArgDist.make("to", twoArgs(SymbolicDist.From90thPercentile.make)),
TwoArgDist.make( TwoArgDist.make(
@ -53,13 +149,357 @@ let registry = [
twoArgs(SymbolicDist.From90thPercentile.make), twoArgs(SymbolicDist.From90thPercentile.make),
), ),
], ],
(),
), ),
Function.make( Function.make(
~name="Exponential", ~name="Exponential",
~examples=`exponential(2)`,
~definitions=[OneArgDist.make("exponential", SymbolicDist.Exponential.make)], ~definitions=[OneArgDist.make("exponential", SymbolicDist.Exponential.make)],
(),
), ),
Function.make( Function.make(
~name="Bernoulli", ~name="Bernoulli",
~examples=`bernoulli(0.5)`,
~definitions=[OneArgDist.make("bernoulli", SymbolicDist.Bernoulli.make)], ~definitions=[OneArgDist.make("bernoulli", SymbolicDist.Bernoulli.make)],
(),
),
Function.make(
~name="PointMass",
~examples=`pointMass(0.5)`,
~definitions=[OneArgDist.make("pointMass", SymbolicDist.Float.makeSafe)],
(),
),
Function.make(
~name="toContinuousPointSet",
~description="Converts a set of points to a continuous distribution",
~examples=`toContinuousPointSet([
{x: 0, y: 0.1},
{x: 1, y: 0.2},
{x: 2, y: 0.15},
{x: 3, y: 0.1}
])`,
~definitions=[
FnDefinition.make(
~name="toContinuousPointSet",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(inputs, _) => inputsTodist(inputs, r => Continuous(Continuous.make(r))),
),
],
(),
),
Function.make(
~name="toDiscretePointSet",
~description="Converts a set of points to a discrete distribution",
~examples=`toDiscretePointSet([
{x: 0, y: 0.1},
{x: 1, y: 0.2},
{x: 2, y: 0.15},
{x: 3, y: 0.1}
])`,
~definitions=[
FnDefinition.make(
~name="toDiscretePointSet",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(inputs, _) => inputsTodist(inputs, r => Discrete(Discrete.make(r))),
),
],
(),
),
Function.make(
~name="Declaration (Continuous Function)",
~description="Adds metadata to a function of the input ranges. Works now for numeric and date inputs. This is useful when making predictions. It allows you to limit the domain that your prediction will be used and scored within.",
~examples=`declareFn({
fn: {|a,b| a },
inputs: [
{min: 0, max: 100},
{min: 30, max: 50}
]
})`,
~definitions=[
FnDefinition.make(~name="declareFn", ~inputs=[Declaration.frType], ~run=(inputs, _) => {
inputs->E.A.unsafe_get(0)->Declaration.fromExpressionValue
}),
],
~isExperimental=true,
(),
),
Function.make(
~name="Floor",
~definitions=[NumberToNumber.make("floor", Js.Math.floor_float)],
(),
),
Function.make(
~name="Ceiling",
~definitions=[NumberToNumber.make("ceil", Js.Math.ceil_float)],
(),
),
Function.make(
~name="Absolute Value",
~definitions=[NumberToNumber.make("abs", Js.Math.abs_float)],
(),
),
Function.make(~name="Exponent", ~definitions=[NumberToNumber.make("exp", Js.Math.exp)], ()),
Function.make(~name="Log", ~definitions=[NumberToNumber.make("log", Js.Math.log)], ()),
Function.make(
~name="Log Base 10",
~definitions=[NumberToNumber.make("log10", Js.Math.log10)],
(),
),
Function.make(~name="Log Base 2", ~definitions=[NumberToNumber.make("log2", Js.Math.log2)], ()),
Function.make(~name="Round", ~definitions=[NumberToNumber.make("round", Js.Math.round)], ()),
Function.make(
~name="Sum",
~definitions=[ArrayNumberDist.make("sum", r => r->E.A.Floats.sum->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Product",
~definitions=[
ArrayNumberDist.make("product", r => r->E.A.Floats.product->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="Min",
~definitions=[ArrayNumberDist.make("min", r => r->E.A.Floats.min->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Max",
~definitions=[ArrayNumberDist.make("max", r => r->E.A.Floats.max->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Mean",
~definitions=[ArrayNumberDist.make("mean", r => r->E.A.Floats.mean->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Geometric Mean",
~definitions=[
ArrayNumberDist.make("geomean", r => r->E.A.Floats.geomean->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="Standard Deviation",
~definitions=[ArrayNumberDist.make("stdev", r => r->E.A.Floats.stdev->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Variance",
~definitions=[
ArrayNumberDist.make("variance", r => r->E.A.Floats.stdev->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="First",
~definitions=[
ArrayNumberDist.make2("first", r =>
r->E.A.first |> E.O.toResult(impossibleError) |> E.R.fmap(Wrappers.evNumber)
),
],
(),
),
Function.make(
~name="Last",
~definitions=[
ArrayNumberDist.make2("last", r =>
r->E.A.last |> E.O.toResult(impossibleError) |> E.R.fmap(Wrappers.evNumber)
),
],
(),
),
Function.make(
~name="Sort",
~definitions=[
ArrayNumberDist.make("sort", r =>
r->E.A.Floats.sort->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Reverse",
~definitions=[
ArrayNumberDist.make("reverse", r =>
r->Belt_Array.reverse->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Cumulative Sum",
~definitions=[
ArrayNumberDist.make("cumsum", r =>
r->E.A.Floats.cumsum->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Cumulative Prod",
~definitions=[
ArrayNumberDist.make("cumprod", r =>
r->E.A.Floats.cumsum->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Diff",
~definitions=[
ArrayNumberDist.make("diff", r =>
r->E.A.Floats.diff->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Dict.merge",
~definitions=[
FnDefinition.make(
~name="merge",
~inputs=[FRTypeDict(FRTypeAny), FRTypeDict(FRTypeAny)],
~run=(inputs, _) => {
switch inputs {
| [FRValueDict(d1), FRValueDict(d2)] => {
let newDict =
E.Dict.concat(d1, d2) |> Js.Dict.map((. r) =>
FunctionRegistry_Core.FRType.matchReverse(r)
)
newDict->Wrappers.evRecord->Ok
}
| _ => Error(impossibleError)
}
},
),
],
(),
),
//TODO: Make sure that two functions can't have the same name. This causes chaos elsewhere.
Function.make(
~name="Dict.mergeMany",
~definitions=[
FnDefinition.make(~name="mergeMany", ~inputs=[FRTypeArray(FRTypeDict(FRTypeAny))], ~run=(
inputs,
_,
) =>
inputs
->Prepare.ToTypedArray.dicts
->E.R2.fmap(E.Dict.concatMany)
->E.R2.fmap(Js.Dict.map((. r) => FunctionRegistry_Core.FRType.matchReverse(r)))
->E.R2.fmap(Wrappers.evRecord)
),
],
(),
),
Function.make(
~name="Dict.keys",
~definitions=[
FnDefinition.make(~name="keys", ~inputs=[FRTypeDict(FRTypeAny)], ~run=(inputs, _) =>
switch inputs {
| [FRValueDict(d1)] => Js.Dict.keys(d1)->E.A2.fmap(Wrappers.evString)->Wrappers.evArray->Ok
| _ => Error(impossibleError)
}
),
],
(),
),
Function.make(
~name="Dict.values",
~definitions=[
FnDefinition.make(~name="values", ~inputs=[FRTypeDict(FRTypeAny)], ~run=(inputs, _) =>
switch inputs {
| [FRValueDict(d1)] =>
Js.Dict.values(d1)
->E.A2.fmap(FunctionRegistry_Core.FRType.matchReverse)
->Wrappers.evArray
->Ok
| _ => Error(impossibleError)
}
),
],
(),
),
Function.make(
~name="Dict.toList",
~definitions=[
FnDefinition.make(~name="dictToList", ~inputs=[FRTypeDict(FRTypeAny)], ~run=(inputs, _) =>
switch inputs {
| [FRValueDict(dict)] =>
dict
->Js.Dict.entries
->E.A2.fmap(((key, value)) =>
Wrappers.evArray([
Wrappers.evString(key),
FunctionRegistry_Core.FRType.matchReverse(value),
])
)
->Wrappers.evArray
->Ok
| _ => Error(impossibleError)
}
),
],
(),
),
Function.make(
~name="Dict.fromList",
~definitions=[
FnDefinition.make(~name="dictFromList", ~inputs=[FRTypeArray(FRTypeArray(FRTypeAny))], ~run=(
inputs,
_,
) => {
let convertInternalItems = items =>
items
->E.A2.fmap(item => {
switch item {
| [FRValueString(string), value] =>
(string, FunctionRegistry_Core.FRType.matchReverse(value))->Ok
| _ => Error(impossibleError)
}
})
->E.A.R.firstErrorOrOpen
->E.R2.fmap(Js.Dict.fromArray)
->E.R2.fmap(Wrappers.evRecord)
inputs->getOrError(0)->E.R.bind(Prepare.ToValueArray.Array.arrayOfArrays)
|> E.R2.bind(convertInternalItems)
}),
],
(),
),
Function.make(
~name="List.make",
~definitions=[
//Todo: If the second item is a function with no args, it could be nice to run this function and return the result.
FnDefinition.make(~name="listMake", ~inputs=[FRTypeNumber, FRTypeAny], ~run=(inputs, _) => {
switch inputs {
| [FRValueNumber(number), value] =>
Belt.Array.make(E.Float.toInt(number), value)
->E.A2.fmap(FunctionRegistry_Core.FRType.matchReverse)
->Wrappers.evArray
->Ok
| _ => Error(impossibleError)
}
}),
],
(),
),
Function.make(
~name="upTo",
~definitions=[
FnDefinition.make(~name="upTo", ~inputs=[FRTypeNumber, FRTypeNumber], ~run=(inputs, _) =>
inputs
->Prepare.ToValueTuple.twoNumbers
->E.R2.fmap(((low, high)) =>
E.A.Floats.range(low, high, (high -. low +. 1.0)->E.Float.toInt)
->E.A2.fmap(Wrappers.evNumber)
->Wrappers.evArray
)
),
],
(),
), ),
] ]

View File

@ -24,4 +24,4 @@ let foreignFunctionInterface = (
let defaultEnvironment = ExpressionValue.defaultEnvironment let defaultEnvironment = ExpressionValue.defaultEnvironment
let defaultExternalBindings = ExpressionValue.defaultExternalBindings let defaultExternalBindings = ReducerInterface_StdLib.externalStdLib

View File

@ -0,0 +1,12 @@
include Reducer_Category_Module // Bindings inherit from Module
open ReducerInterface_ExpressionValue
let emptyBindings = emptyModule
let toExpressionValue = (container: t): expressionValue => EvRecord(toRecord(container))
let fromExpressionValue = (aValue: expressionValue): t =>
switch aValue {
| EvRecord(r) => fromRecord(r)
| _ => emptyBindings
}

View File

@ -0,0 +1,33 @@
module ExpressionT = Reducer_Expression_T
open ReducerInterface_ExpressionValue
let expressionValueToString = toString
type t = ExpressionT.bindings
let typeAliasesKey = "_typeAliases_"
let typeReferencesKey = "_typeReferences_"
let emptyModule: t = Belt.Map.String.empty
let cloneRecord = (r: record): record => r->Js.Dict.entries->Js.Dict.fromArray
let fromRecord = (r: record): t => Js.Dict.entries(r)->Belt.Map.String.fromArray
let toRecord = (container: t): record => Belt.Map.String.toArray(container)->Js.Dict.fromArray
let toExpressionValue = (container: t): expressionValue => EvModule(toRecord(container))
let fromExpressionValue = (aValue: expressionValue): t =>
switch aValue {
| EvModule(r) => fromRecord(r)
| _ => emptyModule
}
let toString = (container: t): string => container->toRecord->EvRecord->expressionValueToString
// -- Module definition
let define = (container: t, identifier: string, ev: expressionValue): t =>
Belt.Map.String.set(container, identifier, ev) // TODO build lambda for polymorphic functions here
let defineNumber = (container: t, identifier: string, value: float): t =>
container->define(identifier, EvNumber(value))
let defineModule = (container: t, identifier: string, value: t): t =>
container->define(identifier, toExpressionValue(value))

View File

@ -52,6 +52,16 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
| None => RERecordPropertyNotFound("Record property not found", sIndex)->Error | None => RERecordPropertyNotFound("Record property not found", sIndex)->Error
} }
let doAddArray = (originalA, b) => {
let a = originalA->Js.Array2.copy
let _ = Js.Array2.pushMany(a, b)
a->EvArray->Ok
}
let doAddString = (a, b) => {
let answer = Js.String2.concat(a, b)
answer->EvString->Ok
}
let inspect = (value: expressionValue) => { let inspect = (value: expressionValue) => {
Js.log(value->toString) Js.log(value->toString)
value->Ok value->Ok
@ -74,6 +84,40 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
->Ok ->Ok
} }
let doSetBindingsInNamespace = (
externalBindings: externalBindings,
symbol: string,
value: expressionValue,
namespace: string,
) => {
let bindings = Bindings.fromExternalBindings(externalBindings)
let evAliases = bindings->Belt.Map.String.getWithDefault(namespace, EvRecord(Js.Dict.empty()))
let newEvAliases = switch evAliases {
| EvRecord(dict) => {
Js.Dict.set(dict, symbol, value)
dict->EvRecord
}
| _ => Js.Dict.empty()->EvRecord
}
bindings
->Belt.Map.String.set(namespace, newEvAliases)
->Bindings.toExternalBindings
->EvRecord
->Ok
}
let doSetTypeAliasBindings = (
externalBindings: externalBindings,
symbol: string,
value: expressionValue,
) => doSetBindingsInNamespace(externalBindings, symbol, value, Bindings.typeAliasesKey)
let doSetTypeOfBindings = (
externalBindings: externalBindings,
symbol: string,
value: expressionValue,
) => doSetBindingsInNamespace(externalBindings, symbol, value, Bindings.typeReferencesKey)
let doExportBindings = (externalBindings: externalBindings) => EvRecord(externalBindings)->Ok let doExportBindings = (externalBindings: externalBindings) => EvRecord(externalBindings)->Ok
let doKeepArray = (aValueArray, aLambdaValue) => { let doKeepArray = (aValueArray, aLambdaValue) => {
@ -101,16 +145,34 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
rMappedList->Result.map(mappedList => mappedList->Belt.List.toArray->EvArray) rMappedList->Result.map(mappedList => mappedList->Belt.List.toArray->EvArray)
} }
let doMapSampleSetDist = (sampleSetDist: SampleSetDist.t, aLambdaValue) => { module SampleMap = {
let fn = r => type t = SampleSetDist.t
switch Lambda.doLambdaCall(aLambdaValue, list{EvNumber(r)}, environment, reducer) { let doLambdaCall = (aLambdaValue, list) =>
switch Lambda.doLambdaCall(aLambdaValue, list, environment, reducer) {
| Ok(EvNumber(f)) => Ok(f) | Ok(EvNumber(f)) => Ok(f)
| _ => Error(Operation.SampleMapNeedsNtoNFunction) | _ => Error(Operation.SampleMapNeedsNtoNFunction)
} }
switch SampleSetDist.samplesMap(~fn, sampleSetDist) {
let toType = r =>
switch r {
| Ok(r) => Ok(EvDistribution(SampleSet(r))) | Ok(r) => Ok(EvDistribution(SampleSet(r)))
| Error(r) => Error(REDistributionError(SampleSetError(r))) | Error(r) => Error(REDistributionError(SampleSetError(r)))
} }
let map1 = (sampleSetDist: t, aLambdaValue) => {
let fn = r => doLambdaCall(aLambdaValue, list{EvNumber(r)})
toType(SampleSetDist.samplesMap(~fn, sampleSetDist))
}
let map2 = (t1: t, t2: t, aLambdaValue) => {
let fn = (a, b) => doLambdaCall(aLambdaValue, list{EvNumber(a), EvNumber(b)})
SampleSetDist.map2(~fn, ~t1, ~t2)->toType
}
let map3 = (t1: t, t2: t, t3: t, aLambdaValue) => {
let fn = (a, b, c) => doLambdaCall(aLambdaValue, list{EvNumber(a), EvNumber(b), EvNumber(c)})
SampleSetDist.map3(~fn, ~t1, ~t2, ~t3)->toType
}
} }
let doReduceArray = (aValueArray, initialValue, aLambdaValue) => { let doReduceArray = (aValueArray, initialValue, aLambdaValue) => {
@ -129,21 +191,122 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
) )
} }
let typeModifier_memberOf = (aType, anArray) => {
let newRecord = Js.Dict.fromArray([
("typeTag", EvString("typeIdentifier")),
("typeIdentifier", aType),
])
newRecord->Js.Dict.set("memberOf", anArray)
newRecord->EvRecord->Ok
}
let typeModifier_memberOf_update = (aRecord, anArray) => {
let newRecord = aRecord->Js.Dict.entries->Js.Dict.fromArray
newRecord->Js.Dict.set("memberOf", anArray)
newRecord->EvRecord->Ok
}
let typeModifier_min = (aType, value) => {
let newRecord = Js.Dict.fromArray([
("typeTag", EvString("typeIdentifier")),
("typeIdentifier", aType),
])
newRecord->Js.Dict.set("min", value)
newRecord->EvRecord->Ok
}
let typeModifier_min_update = (aRecord, value) => {
let newRecord = aRecord->Js.Dict.entries->Js.Dict.fromArray
newRecord->Js.Dict.set("min", value)
newRecord->EvRecord->Ok
}
let typeModifier_max = (aType, value) => {
let newRecord = Js.Dict.fromArray([
("typeTag", EvString("typeIdentifier")),
("typeIdentifier", aType),
])
newRecord->Js.Dict.set("max", value)
newRecord->EvRecord->Ok
}
let typeModifier_max_update = (aRecord, value) => {
let newRecord = aRecord->Js.Dict.entries->Js.Dict.fromArray
newRecord->Js.Dict.set("max", value)
newRecord->EvRecord->Ok
}
let typeModifier_opaque_update = aRecord => {
let newRecord = aRecord->Js.Dict.entries->Js.Dict.fromArray
newRecord->Js.Dict.set("opaque", EvBool(true))
newRecord->EvRecord->Ok
}
let typeOr = evArray => {
let newRecord = Js.Dict.fromArray([("typeTag", EvString("typeOr")), ("typeOr", evArray)])
newRecord->EvRecord->Ok
}
let typeFunction = anArray => {
let output = Belt.Array.getUnsafe(anArray, Js.Array2.length(anArray) - 1)
let inputs = Js.Array2.slice(anArray, ~start=0, ~end_=-1)
let newRecord = Js.Dict.fromArray([
("typeTag", EvString("typeFunction")),
("inputs", EvArray(inputs)),
("output", output),
])
newRecord->EvRecord->Ok
}
switch call { switch call {
| ("$_atIndex_$", [EvArray(aValueArray), EvNumber(fIndex)]) => arrayAtIndex(aValueArray, fIndex) | ("$_atIndex_$", [EvArray(aValueArray), EvNumber(fIndex)]) => arrayAtIndex(aValueArray, fIndex)
| ("$_atIndex_$", [EvModule(dict), EvString(sIndex)]) => recordAtIndex(dict, sIndex)
| ("$_atIndex_$", [EvRecord(dict), EvString(sIndex)]) => recordAtIndex(dict, sIndex) | ("$_atIndex_$", [EvRecord(dict), EvString(sIndex)]) => recordAtIndex(dict, sIndex)
| ("$_constructArray_$", [EvArray(aValueArray)]) => EvArray(aValueArray)->Ok | ("$_constructArray_$", [EvArray(aValueArray)]) => EvArray(aValueArray)->Ok
| ("$_constructRecord_$", [EvArray(arrayOfPairs)]) => constructRecord(arrayOfPairs) | ("$_constructRecord_$", [EvArray(arrayOfPairs)]) => constructRecord(arrayOfPairs)
| ("$_exportBindings_$", [EvRecord(externalBindings)]) => doExportBindings(externalBindings) | ("$_exportBindings_$", [EvRecord(externalBindings)]) => doExportBindings(externalBindings)
| ("$_setBindings_$", [EvRecord(externalBindings), EvSymbol(symbol), value]) => | ("$_setBindings_$", [EvRecord(externalBindings), EvSymbol(symbol), value]) =>
doSetBindings(externalBindings, symbol, value) doSetBindings(externalBindings, symbol, value)
| ("$_setTypeAliasBindings_$", [EvRecord(externalBindings), EvTypeIdentifier(symbol), value]) =>
doSetTypeAliasBindings(externalBindings, symbol, value)
| ("$_setTypeOfBindings_$", [EvRecord(externalBindings), EvSymbol(symbol), value]) =>
doSetTypeOfBindings(externalBindings, symbol, value)
| ("$_typeModifier_memberOf_$", [EvTypeIdentifier(typeIdentifier), EvArray(arr)]) =>
typeModifier_memberOf(EvTypeIdentifier(typeIdentifier), EvArray(arr))
| ("$_typeModifier_memberOf_$", [EvRecord(typeRecord), EvArray(arr)]) =>
typeModifier_memberOf_update(typeRecord, EvArray(arr))
| ("$_typeModifier_min_$", [EvTypeIdentifier(typeIdentifier), value]) =>
typeModifier_min(EvTypeIdentifier(typeIdentifier), value)
| ("$_typeModifier_min_$", [EvRecord(typeRecord), value]) =>
typeModifier_min_update(typeRecord, value)
| ("$_typeModifier_max_$", [EvTypeIdentifier(typeIdentifier), value]) =>
typeModifier_max(EvTypeIdentifier(typeIdentifier), value)
| ("$_typeModifier_max_$", [EvRecord(typeRecord), value]) =>
typeModifier_max_update(typeRecord, value)
| ("$_typeModifier_opaque_$", [EvRecord(typeRecord)]) => typeModifier_opaque_update(typeRecord)
| ("$_typeOr_$", [EvArray(arr)]) => typeOr(EvArray(arr))
| ("$_typeFunction_$", [EvArray(arr)]) => typeFunction(arr)
| ("concat", [EvArray(aValueArray), EvArray(bValueArray)]) => doAddArray(aValueArray, bValueArray)
| ("concat", [EvString(aValueString), EvString(bValueString)]) =>
doAddString(aValueString, bValueString)
| ("inspect", [value, EvString(label)]) => inspectLabel(value, label) | ("inspect", [value, EvString(label)]) => inspectLabel(value, label)
| ("inspect", [value]) => inspect(value) | ("inspect", [value]) => inspect(value)
| ("keep", [EvArray(aValueArray), EvLambda(aLambdaValue)]) => | ("filter", [EvArray(aValueArray), EvLambda(aLambdaValue)]) =>
doKeepArray(aValueArray, aLambdaValue) doKeepArray(aValueArray, aLambdaValue)
| ("map", [EvArray(aValueArray), EvLambda(aLambdaValue)]) => doMapArray(aValueArray, aLambdaValue) | ("map", [EvArray(aValueArray), EvLambda(aLambdaValue)]) => doMapArray(aValueArray, aLambdaValue)
| ("mapSamples", [EvDistribution(SampleSet(dist)), EvLambda(aLambdaValue)]) => | ("mapSamples", [EvDistribution(SampleSet(dist)), EvLambda(aLambdaValue)]) =>
doMapSampleSetDist(dist, aLambdaValue) SampleMap.map1(dist, aLambdaValue)
| (
"mapSamples2",
[EvDistribution(SampleSet(dist1)), EvDistribution(SampleSet(dist2)), EvLambda(aLambdaValue)],
) =>
SampleMap.map2(dist1, dist2, aLambdaValue)
| (
"mapSamples3",
[
EvDistribution(SampleSet(dist1)),
EvDistribution(SampleSet(dist2)),
EvDistribution(SampleSet(dist3)),
EvLambda(aLambdaValue),
],
) =>
SampleMap.map3(dist1, dist2, dist3, aLambdaValue)
| ("reduce", [EvArray(aValueArray), initialValue, EvLambda(aLambdaValue)]) => | ("reduce", [EvArray(aValueArray), initialValue, EvLambda(aLambdaValue)]) =>
doReduceArray(aValueArray, initialValue, aLambdaValue) doReduceArray(aValueArray, initialValue, aLambdaValue)
| ("reduceReverse", [EvArray(aValueArray), initialValue, EvLambda(aLambdaValue)]) => | ("reduceReverse", [EvArray(aValueArray), initialValue, EvLambda(aLambdaValue)]) =>

View File

@ -4,6 +4,7 @@
Macros are used to define language building blocks. They are like Lisp macros. Macros are used to define language building blocks. They are like Lisp macros.
*/ */
module Bindings = Reducer_Expression_Bindings module Bindings = Reducer_Expression_Bindings
module ErrorValue = Reducer_ErrorValue
module ExpressionBuilder = Reducer_Expression_ExpressionBuilder module ExpressionBuilder = Reducer_Expression_ExpressionBuilder
module ExpressionT = Reducer_Expression_T module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.ExpressionValue module ExpressionValue = ReducerInterface.ExpressionValue
@ -12,7 +13,7 @@ module Result = Belt.Result
open Reducer_Expression_ExpressionBuilder open Reducer_Expression_ExpressionBuilder
type environment = ExpressionValue.environment type environment = ExpressionValue.environment
type errorValue = Reducer_ErrorValue.errorValue type errorValue = ErrorValue.errorValue
type expression = ExpressionT.expression type expression = ExpressionT.expression
type expressionValue = ExpressionValue.expressionValue type expressionValue = ExpressionValue.expressionValue
type expressionWithContext = ExpressionWithContext.expressionWithContext type expressionWithContext = ExpressionWithContext.expressionWithContext
@ -23,82 +24,76 @@ let dispatchMacroCall = (
environment, environment,
reduceExpression: ExpressionT.reducerFn, reduceExpression: ExpressionT.reducerFn,
): result<expressionWithContext, errorValue> => { ): result<expressionWithContext, errorValue> => {
let doBindStatement = (bindingExpr: expression, statement: expression, environment) => let useExpressionToSetBindings = (bindingExpr: expression, environment, statement, newCode) => {
switch statement {
| ExpressionT.EList(list{ExpressionT.EValue(EvCall("$_let_$")), symbolExpr, statement}) => {
let rExternalBindingsValue = reduceExpression(bindingExpr, bindings, environment) let rExternalBindingsValue = reduceExpression(bindingExpr, bindings, environment)
rExternalBindingsValue->Result.flatMap(externalBindingsValue => { rExternalBindingsValue->Result.flatMap(externalBindingsValue => {
let newBindings = Bindings.fromValue(externalBindingsValue) let newBindings = Bindings.fromValue(externalBindingsValue)
// Js.log(
// `bindStatement ${Bindings.toString(newBindings)}<==${ExpressionT.toString(
// bindingExpr,
// )} statement: $_let_$ ${ExpressionT.toString(symbolExpr)}=${ExpressionT.toString(
// statement,
// )}`,
// )
let rNewStatement = Bindings.replaceSymbols(newBindings, statement) let rNewStatement = Bindings.replaceSymbols(newBindings, statement)
rNewStatement->Result.map(newStatement => rNewStatement->Result.map(boundStatement =>
ExpressionWithContext.withContext( ExpressionWithContext.withContext(
eFunction( newCode(newBindings->Bindings.toExternalBindings->eRecord, boundStatement),
"$_setBindings_$",
list{newBindings->Bindings.toExternalBindings->eRecord, symbolExpr, newStatement},
),
newBindings, newBindings,
) )
) )
}) })
} }
| _ => REAssignmentExpected->Error
let correspondingSetBindingsFn = (fnName: string): string =>
switch fnName {
| "$_let_$" => "$_setBindings_$"
| "$_typeOf_$" => "$_setTypeOfBindings_$"
| "$_typeAlias_$" => "$_setTypeAliasBindings_$"
| _ => ""
}
let doBindStatement = (bindingExpr: expression, statement: expression, environment) => {
let defaultStatement = ErrorValue.REAssignmentExpected->Error
switch statement {
| ExpressionT.EList(list{ExpressionT.EValue(EvCall(callName)), symbolExpr, statement}) => {
let setBindingsFn = correspondingSetBindingsFn(callName)
if setBindingsFn !== "" {
useExpressionToSetBindings(bindingExpr, environment, statement, (
newBindingsExpr,
boundStatement,
) => eFunction(setBindingsFn, list{newBindingsExpr, symbolExpr, boundStatement}))
} else {
defaultStatement
}
}
| _ => defaultStatement
}
} }
let doBindExpression = (bindingExpr: expression, statement: expression, environment): result< let doBindExpression = (bindingExpr: expression, statement: expression, environment): result<
expressionWithContext, expressionWithContext,
errorValue, errorValue,
> => > => {
switch statement { let defaultStatement = () =>
| ExpressionT.EList(list{ExpressionT.EValue(EvCall("$_let_$")), symbolExpr, statement}) => { useExpressionToSetBindings(bindingExpr, environment, statement, (
let rExternalBindingsValue = reduceExpression(bindingExpr, bindings, environment) _newBindingsExpr,
boundStatement,
) => boundStatement)
rExternalBindingsValue->Result.flatMap(externalBindingsValue => { switch statement {
let newBindings = Bindings.fromValue(externalBindingsValue) | ExpressionT.EList(list{ExpressionT.EValue(EvCall(callName)), symbolExpr, statement}) => {
let rNewStatement = Bindings.replaceSymbols(newBindings, statement) let setBindingsFn = correspondingSetBindingsFn(callName)
rNewStatement->Result.map(newStatement => if setBindingsFn !== "" {
ExpressionWithContext.withContext( useExpressionToSetBindings(bindingExpr, environment, statement, (
newBindingsExpr,
boundStatement,
) =>
eFunction( eFunction(
"$_exportBindings_$", "$_exportBindings_$",
list{ list{eFunction(setBindingsFn, list{newBindingsExpr, symbolExpr, boundStatement})},
eFunction(
"$_setBindings_$",
list{
newBindings->Bindings.toExternalBindings->eRecord,
symbolExpr,
newStatement,
},
),
},
),
newBindings,
) )
) )
}) } else {
defaultStatement()
} }
| _ => { }
let rExternalBindingsValue: result<expressionValue, errorValue> = reduceExpression( | _ => defaultStatement()
bindingExpr,
bindings,
environment,
)
rExternalBindingsValue->Result.flatMap(externalBindingsValue => {
let newBindings = Bindings.fromValue(externalBindingsValue)
let rNewStatement = Bindings.replaceSymbols(newBindings, statement)
rNewStatement->Result.map(newStatement =>
ExpressionWithContext.withContext(newStatement, newBindings)
)
})
} }
} }
@ -144,8 +139,14 @@ let dispatchMacroCall = (
let rCondition = reduceExpression(blockCondition, bindings, environment) let rCondition = reduceExpression(blockCondition, bindings, environment)
rCondition->Result.flatMap(conditionValue => rCondition->Result.flatMap(conditionValue =>
switch conditionValue { switch conditionValue {
| ExpressionValue.EvBool(false) => ExpressionWithContext.noContext(ifFalse)->Ok | ExpressionValue.EvBool(false) => {
| ExpressionValue.EvBool(true) => ExpressionWithContext.noContext(ifTrue)->Ok let ifFalseBlock = eBlock(list{ifFalse})
ExpressionWithContext.withContext(ifFalseBlock, bindings)->Ok
}
| ExpressionValue.EvBool(true) => {
let ifTrueBlock = eBlock(list{ifTrue})
ExpressionWithContext.withContext(ifTrueBlock, bindings)->Ok
}
| _ => REExpectedType("Boolean")->Error | _ => REExpectedType("Boolean")->Error
} }
) )

View File

@ -75,9 +75,9 @@ and reduceValueList = (valueList: list<expressionValue>, environment): result<
> => > =>
switch valueList { switch valueList {
| list{EvCall(fName), ...args} => { | list{EvCall(fName), ...args} => {
let rCheckedArgs = switch fName == "$_setBindings_$" { let rCheckedArgs = switch fName {
| false => args->Lambda.checkIfReduced | "$_setBindings_$" | "$_setTypeOfBindings_$" | "$_setTypeAliasBindings_$" => args->Ok
| true => args->Ok | _ => args->Lambda.checkIfReduced
} }
rCheckedArgs->Result.flatMap(checkedArgs => rCheckedArgs->Result.flatMap(checkedArgs =>
@ -116,14 +116,20 @@ let evaluateUsingOptions = (
~externalBindings: option<ReducerInterface_ExpressionValue.externalBindings>, ~externalBindings: option<ReducerInterface_ExpressionValue.externalBindings>,
code: string, code: string,
): result<expressionValue, errorValue> => { ): result<expressionValue, errorValue> => {
let anEnvironment = switch environment { let anEnvironment = Belt.Option.getWithDefault(
| Some(env) => env environment,
| None => ReducerInterface_ExpressionValue.defaultEnvironment ReducerInterface_ExpressionValue.defaultEnvironment,
} )
let anExternalBindings = switch externalBindings { let anExternalBindings = switch externalBindings {
| Some(bindings) => bindings | Some(bindings) => {
| None => ReducerInterface_ExpressionValue.defaultExternalBindings let cloneLib = ReducerInterface_StdLib.externalStdLib->Reducer_Category_Bindings.cloneRecord
Js.Dict.entries(bindings)->Js.Array2.reduce((acc, (key, value)) => {
acc->Js.Dict.set(key, value)
acc
}, cloneLib)
}
| None => ReducerInterface_StdLib.externalStdLib
} }
let bindings = anExternalBindings->Bindings.fromExternalBindings let bindings = anExternalBindings->Bindings.fromExternalBindings

View File

@ -22,11 +22,16 @@ let callReducer = (
bindings: bindings, bindings: bindings,
environment: environment, environment: environment,
reducer: reducerFn, reducer: reducerFn,
): result<expressionValue, errorValue> => ): result<expressionValue, errorValue> => {
switch expressionWithContext { switch expressionWithContext {
| ExpressionNoContext(expr) => reducer(expr, bindings, environment) | ExpressionNoContext(expr) =>
| ExpressionWithContext(expr, context) => reducer(expr, context, environment) // Js.log(`callReducer: bindings ${Bindings.toString(bindings)} expr ${ExpressionT.toString(expr)}`)
reducer(expr, bindings, environment)
| ExpressionWithContext(expr, context) =>
// Js.log(`callReducer: context ${Bindings.toString(context)} expr ${ExpressionT.toString(expr)}`)
reducer(expr, context, environment)
} }
}
let withContext = (expression, context) => ExpressionWithContext(expression, context) let withContext = (expression, context) => ExpressionWithContext(expression, context)
let noContext = expression => ExpressionNoContext(expression) let noContext = expression => ExpressionNoContext(expression)

View File

@ -2,38 +2,25 @@ module ErrorValue = Reducer_ErrorValue
module ExpressionT = Reducer_Expression_T module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.ExpressionValue module ExpressionValue = ReducerInterface.ExpressionValue
module Result = Belt.Result module Result = Belt.Result
module Bindings = Reducer_Category_Bindings
type errorValue = Reducer_ErrorValue.errorValue type errorValue = Reducer_ErrorValue.errorValue
type expression = ExpressionT.expression type expression = ExpressionT.expression
type expressionValue = ExpressionValue.expressionValue type expressionValue = ExpressionValue.expressionValue
type externalBindings = ReducerInterface_ExpressionValue.externalBindings type externalBindings = ReducerInterface_ExpressionValue.externalBindings
let defaultBindings: ExpressionT.bindings = Belt.Map.String.empty let emptyBindings = Reducer_Category_Bindings.emptyBindings
let fromExternalBindings = (externalBindings: externalBindings): ExpressionT.bindings => { let typeAliasesKey = Bindings.typeAliasesKey
let keys = Js.Dict.keys(externalBindings) let typeReferencesKey = Bindings.typeReferencesKey
keys->Belt.Array.reduce(defaultBindings, (acc, key) => {
let value = Js.Dict.unsafeGet(externalBindings, key)
acc->Belt.Map.String.set(key, value)
})
}
let toExternalBindings = (bindings: ExpressionT.bindings): externalBindings => { let toExternalBindings = (bindings: ExpressionT.bindings): externalBindings =>
let keys = Belt.Map.String.keysToArray(bindings) Bindings.toRecord(bindings)
keys->Belt.Array.reduce(Js.Dict.empty(), (acc, key) => {
let value = bindings->Belt.Map.String.getExn(key)
Js.Dict.set(acc, key, value)
acc
})
}
let fromValue = (aValue: expressionValue) => let fromExternalBindings = (externalBindings: externalBindings): ExpressionT.bindings =>
switch aValue { Bindings.fromRecord(externalBindings)
| EvRecord(externalBindings) => fromExternalBindings(externalBindings)
| _ => defaultBindings
}
let externalFromArray = anArray => Js.Dict.fromArray(anArray) let fromValue = (aValue: expressionValue) => Bindings.fromExpressionValue(aValue)
let isMacroName = (fName: string): bool => fName->Js.String2.startsWith("$$") let isMacroName = (fName: string): bool => fName->Js.String2.startsWith("$$")

View File

@ -64,3 +64,8 @@ let eBindExpression = (bindingExpr: expression, expression: expression): express
let eBindExpressionDefault = (expression: expression): expression => let eBindExpressionDefault = (expression: expression): expression =>
eFunction("$$_bindExpression_$$", list{expression}) eFunction("$$_bindExpression_$$", list{expression})
let eIdentifier = (name: string): expression => name->BExpressionValue.EvSymbol->BExpressionT.EValue
let eTypeIdentifier = (name: string): expression =>
name->BExpressionValue.EvTypeIdentifier->BExpressionT.EValue

View File

@ -28,6 +28,11 @@ type reducerFn = (
*/ */
let rec toString = expression => let rec toString = expression =>
switch expression { switch expression {
| EList(list{EValue(EvCall("$$_block_$$")), ...statements}) =>
`{${Belt.List.map(statements, aValue => toString(aValue))
->Extra.List.interperse("; ")
->Belt.List.toArray
->Js.String.concatMany("")}}`
| EList(aList) => | EList(aList) =>
`(${Belt.List.map(aList, aValue => toString(aValue)) `(${Belt.List.map(aList, aValue => toString(aValue))
->Extra.List.interperse(" ") ->Extra.List.interperse(" ")

View File

@ -36,11 +36,6 @@
'[]': '$_atIndex_$', '[]': '$_atIndex_$',
} }
function nodeBlock(statements) {return{type: 'Block', statements: statements}}
function nodeBoolean(value) {return {type: 'Boolean', value: value}}
function nodeCallIndentifier(value) {return {type: 'CallIdentifier', value: value}}
function nodeExpression(args) {return {type: 'Expression', nodes: args}}
function nodeFloat(value) {return {type: 'Float', value: value}}
function makeFunctionCall(fn, args) { function makeFunctionCall(fn, args) {
if (fn === '$$_applyAll_$$') { if (fn === '$$_applyAll_$$') {
// Any list of values is applied from left to right anyway. // Any list of values is applied from left to right anyway.
@ -52,6 +47,16 @@
return nodeExpression([nodeCallIndentifier(fn), ...args]) return nodeExpression([nodeCallIndentifier(fn), ...args])
} }
} }
function apply(fn, arg) { return makeFunctionCall(fn, [arg]); }
function constructArray(elems) { return apply('$_constructArray_$', nodeExpression(elems)); }
function constructRecord(elems) { return apply('$_constructRecord_$', nodeExpression(elems)); }
function nodeBlock(statements) {return{type: 'Block', statements: statements}}
function nodeBoolean(value) {return {type: 'Boolean', value: value}}
function nodeCallIndentifier(value) {return {type: 'CallIdentifier', value: value}}
function nodeExpression(args) {return {type: 'Expression', nodes: args}}
function nodeFloat(value) {return {type: 'Float', value: value}}
function nodeIdentifier(value) {return {type: 'Identifier', value: value}} function nodeIdentifier(value) {return {type: 'Identifier', value: value}}
function nodeInteger(value) {return {type: 'Integer', value: value}} function nodeInteger(value) {return {type: 'Integer', value: value}}
function nodeKeyValue(key, value) { function nodeKeyValue(key, value) {
@ -59,11 +64,15 @@
return {type: 'KeyValue', key: key, value: value}} return {type: 'KeyValue', key: key, value: value}}
function nodeLambda(args, body) {return {type: 'Lambda', args: args, body: body}} function nodeLambda(args, body) {return {type: 'Lambda', args: args, body: body}}
function nodeLetStatment(variable, value) {return {type: 'LetStatement', variable: variable, value: value}} function nodeLetStatment(variable, value) {return {type: 'LetStatement', variable: variable, value: value}}
function nodeModuleIdentifier(value) {return {type: 'ModuleIdentifier', value: value}}
function nodeString(value) {return {type: 'String', value: value}} function nodeString(value) {return {type: 'String', value: value}}
function nodeTernary(condition, trueExpression, falseExpression) {return {type: 'Ternary', condition: condition, trueExpression: trueExpression, falseExpression: falseExpression}} function nodeTernary(condition, trueExpression, falseExpression) {return {type: 'Ternary', condition: condition, trueExpression: trueExpression, falseExpression: falseExpression}}
function nodeTypeIdentifier(typeValue) {return {type: 'TypeIdentifier', value: typeValue}}
}} }}
start start
// = _nl start:typeExpression _nl finalComment? {return start}
= _nl start:outerBlock _nl finalComment? {return start} = _nl start:outerBlock _nl finalComment? {return start}
zeroOMoreArgumentsBlockOrExpression = innerBlockOrExpression / lambda zeroOMoreArgumentsBlockOrExpression = innerBlockOrExpression / lambda
@ -96,6 +105,7 @@ array_statements
statement statement
= letStatement = letStatement
/ defunStatement / defunStatement
/ typeStatement
letStatement letStatement
= variable:identifier _ assignmentOp _nl value:zeroOMoreArgumentsBlockOrExpression = variable:identifier _ assignmentOp _nl value:zeroOMoreArgumentsBlockOrExpression
@ -205,7 +215,7 @@ chainFunctionCall
unary unary
= unaryOperator:unaryOperator _nl right:(unary/postOperator) = unaryOperator:unaryOperator _nl right:(unary/postOperator)
{ return makeFunctionCall(unaryToFunction[unaryOperator], [right])} { return apply(unaryToFunction[unaryOperator], right)}
/ postOperator / postOperator
unaryOperator "unary operator" unaryOperator "unary operator"
@ -215,10 +225,9 @@ postOperator = indexedValue
indexedValue indexedValue
= collectionElement = collectionElement
/ recordElement
/ atom / atom
collectionElement collectionElement
= head:atom &('['/'('/'.') = head:atom &('['/'('/'.')
tail:( tail:(
_ '[' _nl arg:expression _nl ']' {return {fn: postOperatorToFunction['[]'], args: [arg]}} _ '[' _nl arg:expression _nl ']' {return {fn: postOperatorToFunction['[]'], args: [arg]}}
@ -233,13 +242,6 @@ indexedValue
= head:expression tail:(_ ',' _nl @expression)* = head:expression tail:(_ ',' _nl @expression)*
{ return [head, ...tail]; } { return [head, ...tail]; }
recordElement
= head:dollarIdentifier &'.'
tail:(_ '.' _nl arg:$dollarIdentifier {return {fn: postOperatorToFunction['.'], args: [nodeString(arg)]}})*
{ return tail.reduce(function(result, element) {
return makeFunctionCall(element.fn, [result, ...element.args])
}, head)}
atom atom
= '(' _nl expression:expression _nl ')' {return expression} = '(' _nl expression:expression _nl ')' {return expression}
/ basicValue / basicValue
@ -250,24 +252,41 @@ basicLiteral
= string = string
/ number / number
/ boolean / boolean
/ dollarIdentifierWithModule
/ dollarIdentifier / dollarIdentifier
dollarIdentifierWithModule 'identifier'
= head:moduleIdentifier
tail:('.' _nl @$moduleIdentifier)* '.' _nl
final:$dollarIdentifier
{ tail.push(final);
return tail.reduce(function(result, element) {
return makeFunctionCall(postOperatorToFunction['[]'], [result, nodeString(element)])
}, head)}
identifier 'identifier' identifier 'identifier'
= ([_a-z]+[_a-z0-9]i*) {return nodeIdentifier(text())} = ([_a-z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
unitIdentifier 'identifier'
= ([_a-zA-Z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
dollarIdentifier '$identifier' dollarIdentifier '$identifier'
= ([\$_a-z]+[\$_a-z0-9]i*) {return nodeIdentifier(text())} = ([\$_a-z]+[\$_a-z0-9]i*) {return nodeIdentifier(text())}
moduleIdentifier 'identifier'
= ([A-Z]+[_a-z0-9]i*) {return nodeModuleIdentifier(text())}
string 'string' string 'string'
= characters:("'" @([^'])* "'") {return nodeString(characters.join(''))} = characters:("'" @([^'])* "'") {return nodeString(characters.join(''))}
/ characters:('"' @([^"])* '"') {return nodeString(characters.join(''))} / characters:('"' @([^"])* '"') {return nodeString(characters.join(''))}
number = number:(float / integer) unit:identifier? number = number:(float / integer) unit:unitIdentifier?
{ {
if (unit === null) if (unit === null)
{ return number } { return number }
else else
{ return makeFunctionCall('fromUnit_'+unit.value, [number]) { return apply('fromUnit_'+unit.value, number)
} }
} }
@ -301,9 +320,9 @@ lambda
arrayConstructor 'array' arrayConstructor 'array'
= '[' _nl ']' = '[' _nl ']'
{ return makeFunctionCall('$_constructArray_$', [nodeExpression([])])} { return constructArray([]); }
/ '[' _nl args:array_elements _nl ']' / '[' _nl args:array_elements _nl ']'
{ return makeFunctionCall('$_constructArray_$', [nodeExpression(args)])} { return constructArray(args); }
array_elements array_elements
= head:expression tail:(_ ',' _nl @expression)* = head:expression tail:(_ ',' _nl @expression)*
@ -311,7 +330,7 @@ arrayConstructor 'array'
recordConstructor 'record' recordConstructor 'record'
= '{' _nl args:array_recordArguments _nl '}' = '{' _nl args:array_recordArguments _nl '}'
{ return makeFunctionCall('$_constructRecord_$', [nodeExpression(args)])} { return constructRecord(args); }
array_recordArguments array_recordArguments
= head:keyValuePair tail:(_ ',' _nl @keyValuePair)* = head:keyValuePair tail:(_ ',' _nl @keyValuePair)*
@ -321,10 +340,12 @@ recordConstructor 'record'
= key:expression _ ':' _nl value:expression = key:expression _ ':' _nl value:expression
{ return nodeKeyValue(key, value)} { return nodeKeyValue(key, value)}
// Separators
_ 'whitespace' _ 'whitespace'
= whiteSpaceCharactersOrComment* = whiteSpaceCharactersOrComment*
_nl 'optional whitespace or newline' _nl 'whitespace or newline'
= (whiteSpaceCharactersOrComment / commentOrNewLine)* = (whiteSpaceCharactersOrComment / commentOrNewLine)*
__ 'whitespace' __ 'whitespace'
@ -351,4 +372,79 @@ statementSeparator 'statement separator'
newLine "newline" newLine "newline"
= [\n\r] = [\n\r]
// Types
noArguments = ('(' _nl ')' )?
typeIdentifier 'type identifier'
= ([a-z]+[_a-z0-9]i*) {return nodeTypeIdentifier(text())}
typeConstructorIdentifier 'type constructor identifier'
= ([A-Z]+[_a-z0-9]i*) {return nodeTypeIdentifier(text())}
typeExpression = typePostModifierExpression
typePostModifierExpression = head:typeOr tail:(_ '$' _nl @typeModifier)*
{
return tail.reduce((result, element) => {
return makeFunctionCall('$_typeModifier_'+element.modifier.value+'_$', [result, ...element.args])
}, head)
}
typeOr = head:typeFunction tail:(_ '|' _nl @typeFunction)*
{ return tail.length === 0 ? head : apply('$_typeOr_$', constructArray([head, ...tail])); }
typeFunction = head:typeModifierExpression tail:(_ '=>' _nl @typeModifierExpression)*
{ return tail.length === 0 ? head : apply( '$_typeFunction_$', constructArray([head, ...tail])); }
typeModifierExpression = head:basicType tail:(_ '<-' _nl @typeModifier)*
{
return tail.reduce((result, element) => {
return makeFunctionCall('$_typeModifier_'+element.modifier.value+'_$', [result, ...element.args])
}, head)
}
typeModifier
= modifier:identifier _ '(' _nl args:array_elements _nl ')'
{ return {modifier: modifier, args: args}; }
/ modifier:identifier _ noArguments
{ return {modifier: modifier, args: []}; }
basicType = typeConstructor / typeArray / typeRecord / typeInParanthesis / typeIdentifier
typeArray = '[' _nl elem:typeExpression _nl ']'
{return apply('$_typeArray_$', elem)}
typeRecord = '{' _nl elems:array_typeRecordArguments _nl '}'
{ return apply('$_typeRecord_$', constructRecord(elems)); }
array_typeRecordArguments
= head:typeKeyValuePair tail:(_ ',' _nl @typeKeyValuePair)*
{ return [head, ...tail]; }
typeKeyValuePair
= key:identifier _ ':' _nl value:typeExpression
{ return nodeKeyValue(key, value)}
typeConstructor
= constructor:typeConstructorIdentifier _ '(' _nl args:array_types _nl ')'
{ return makeFunctionCall('$_typeConstructor_$', [constructor, constructArray(args)]); }
/ constructor:typeConstructorIdentifier _ noArguments
{ return makeFunctionCall('$_typeConstructor_$', [constructor, constructArray([])]); }
array_types = head:typeExpression tail:(_ ',' _nl @typeExpression)*
{ return [head, ...tail]; }
typeStatement = typeAliasStatement / typeOfStatement
typeAliasStatement = 'type' __nl typeIdentifier:typeIdentifier _nl '=' _nl typeExpression:typeExpression
{ return makeFunctionCall('$_typeAlias_$', [typeIdentifier, typeExpression])}
typeOfStatement = identifier:identifier _ ':' _nl typeExpression:typeExpression
{ return makeFunctionCall('$_typeOf_$', [identifier, typeExpression])}
typeInParanthesis = '(' _nl typeExpression:typeExpression _nl ')' {return typeExpression}
// TODO: min max example
// TODO: Example of foo = {a: 2, b: 5}; type fooKeys = string $ memberOf(foo->keys)
// TODO: Example of memberOf( [1,2,3] )
// TODO: Example of $
// TODO: Cons(a, list) | EmptyList

View File

@ -22,8 +22,10 @@ type nodeInteger = {...node, "value": int}
type nodeKeyValue = {...node, "key": node, "value": node} type nodeKeyValue = {...node, "key": node, "value": node}
type nodeLambda = {...node, "args": array<nodeIdentifier>, "body": nodeBlock} type nodeLambda = {...node, "args": array<nodeIdentifier>, "body": nodeBlock}
type nodeLetStatement = {...node, "variable": nodeIdentifier, "value": node} type nodeLetStatement = {...node, "variable": nodeIdentifier, "value": node}
type nodeModuleIdentifier = {...node, "value": string}
type nodeString = {...node, "value": string} type nodeString = {...node, "value": string}
type nodeTernary = {...node, "condition": node, "trueExpression": node, "falseExpression": node} type nodeTernary = {...node, "condition": node, "trueExpression": node, "falseExpression": node}
type nodeTypeIdentifier = {...node, "value": string}
type peggyNode = type peggyNode =
| PgNodeBlock(nodeBlock) | PgNodeBlock(nodeBlock)
@ -36,8 +38,10 @@ type peggyNode =
| PgNodeKeyValue(nodeKeyValue) | PgNodeKeyValue(nodeKeyValue)
| PgNodeLambda(nodeLambda) | PgNodeLambda(nodeLambda)
| PgNodeLetStatement(nodeLetStatement) | PgNodeLetStatement(nodeLetStatement)
| PgNodeModuleIdentifier(nodeModuleIdentifier)
| PgNodeString(nodeString) | PgNodeString(nodeString)
| PgNodeTernary(nodeTernary) | PgNodeTernary(nodeTernary)
| PgNodeTypeIdentifier(nodeTypeIdentifier)
external castNodeBlock: node => nodeBlock = "%identity" external castNodeBlock: node => nodeBlock = "%identity"
external castNodeBoolean: node => nodeBoolean = "%identity" external castNodeBoolean: node => nodeBoolean = "%identity"
@ -49,8 +53,10 @@ external castNodeInteger: node => nodeInteger = "%identity"
external castNodeKeyValue: node => nodeKeyValue = "%identity" external castNodeKeyValue: node => nodeKeyValue = "%identity"
external castNodeLambda: node => nodeLambda = "%identity" external castNodeLambda: node => nodeLambda = "%identity"
external castNodeLetStatement: node => nodeLetStatement = "%identity" external castNodeLetStatement: node => nodeLetStatement = "%identity"
external castNodeModuleIdentifier: node => nodeModuleIdentifier = "%identity"
external castNodeString: node => nodeString = "%identity" external castNodeString: node => nodeString = "%identity"
external castNodeTernary: node => nodeTernary = "%identity" external castNodeTernary: node => nodeTernary = "%identity"
external castNodeTypeIdentifier: node => nodeTypeIdentifier = "%identity"
exception UnsupportedPeggyNodeType(string) // This should never happen; programming error exception UnsupportedPeggyNodeType(string) // This should never happen; programming error
let castNodeType = (node: node) => let castNodeType = (node: node) =>
@ -65,8 +71,10 @@ let castNodeType = (node: node) =>
| "KeyValue" => node->castNodeKeyValue->PgNodeKeyValue | "KeyValue" => node->castNodeKeyValue->PgNodeKeyValue
| "Lambda" => node->castNodeLambda->PgNodeLambda | "Lambda" => node->castNodeLambda->PgNodeLambda
| "LetStatement" => node->castNodeLetStatement->PgNodeLetStatement | "LetStatement" => node->castNodeLetStatement->PgNodeLetStatement
| "ModuleIdentifier" => node->castNodeModuleIdentifier->PgNodeModuleIdentifier
| "String" => node->castNodeString->PgNodeString | "String" => node->castNodeString->PgNodeString
| "Ternary" => node->castNodeTernary->PgNodeTernary | "Ternary" => node->castNodeTernary->PgNodeTernary
| "TypeIdentifier" => node->castNodeTypeIdentifier->PgNodeTypeIdentifier
| _ => raise(UnsupportedPeggyNodeType(node["type"])) | _ => raise(UnsupportedPeggyNodeType(node["type"]))
} }
@ -90,6 +98,7 @@ let rec pgToString = (peggyNode: peggyNode): string => {
"{|" ++ node["args"]->argsToString ++ "| " ++ pgToString(PgNodeBlock(node["body"])) ++ "}" "{|" ++ node["args"]->argsToString ++ "| " ++ pgToString(PgNodeBlock(node["body"])) ++ "}"
| PgNodeLetStatement(node) => | PgNodeLetStatement(node) =>
pgToString(PgNodeIdentifier(node["variable"])) ++ " = " ++ toString(node["value"]) pgToString(PgNodeIdentifier(node["variable"])) ++ " = " ++ toString(node["value"])
| PgNodeModuleIdentifier(node) => `@${node["value"]}`
| PgNodeString(node) => `'${node["value"]->Js.String.make}'` | PgNodeString(node) => `'${node["value"]->Js.String.make}'`
| PgNodeTernary(node) => | PgNodeTernary(node) =>
"(::$$_ternary_$$ " ++ "(::$$_ternary_$$ " ++
@ -98,6 +107,7 @@ let rec pgToString = (peggyNode: peggyNode): string => {
toString(node["trueExpression"]) ++ toString(node["trueExpression"]) ++
" " ++ " " ++
toString(node["falseExpression"]) ++ ")" toString(node["falseExpression"]) ++ ")"
| PgNodeTypeIdentifier(node) => `#${node["value"]}`
} }
} }
and toString = (node: node): string => node->castNodeType->pgToString and toString = (node: node): string => node->castNodeType->pgToString

View File

@ -34,6 +34,8 @@ let rec fromNode = (node: Parse.node): expression => {
nodeLetStatement["variable"]["value"], nodeLetStatement["variable"]["value"],
fromNode(nodeLetStatement["value"]), fromNode(nodeLetStatement["value"]),
) )
| PgNodeModuleIdentifier(nodeModuleIdentifier) =>
ExpressionBuilder.eIdentifier(nodeModuleIdentifier["value"])
| PgNodeString(nodeString) => ExpressionBuilder.eString(nodeString["value"]) | PgNodeString(nodeString) => ExpressionBuilder.eString(nodeString["value"])
| PgNodeTernary(nodeTernary) => | PgNodeTernary(nodeTernary) =>
ExpressionBuilder.eFunction( ExpressionBuilder.eFunction(
@ -44,5 +46,7 @@ let rec fromNode = (node: Parse.node): expression => {
fromNode(nodeTernary["falseExpression"]), fromNode(nodeTernary["falseExpression"]),
}, },
) )
| PgNodeTypeIdentifier(nodeTypeIdentifier) =>
ExpressionBuilder.eTypeIdentifier(nodeTypeIdentifier["value"])
} }
} }

View File

@ -0,0 +1,27 @@
module EV = ReducerInterface_ExpressionValue
type expressionValue = EV.expressionValue
let dispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| ("toString", [EvDate(t)]) => EV.EvString(DateTime.Date.toString(t))->Ok->Some
| ("makeDateFromYear", [EvNumber(year)]) =>
switch DateTime.Date.makeFromYear(year) {
| Ok(t) => EV.EvDate(t)->Ok->Some
| Error(e) => Reducer_ErrorValue.RETodo(e)->Error->Some
}
| ("dateFromNumber", [EvNumber(f)]) => EV.EvDate(DateTime.Date.fromFloat(f))->Ok->Some
| ("toNumber", [EvDate(f)]) => EV.EvNumber(DateTime.Date.toFloat(f))->Ok->Some
| ("subtract", [EvDate(d1), EvDate(d2)]) =>
switch DateTime.Date.subtract(d1, d2) {
| Ok(d) => EV.EvTimeDuration(d)->Ok
| Error(e) => Error(RETodo(e))
}->Some
| ("subtract", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.subtractDuration(d1, d2))->Ok->Some
| ("add", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.addDuration(d1, d2))->Ok->Some
| _ => None
}
}

View File

@ -1,32 +1,7 @@
module EV = ReducerInterface_ExpressionValue module EV = ReducerInterface_ExpressionValue
type expressionValue = EV.expressionValue type expressionValue = EV.expressionValue
let dateDispatch = (call: EV.functionCall, _: DistributionOperation.env): option< let dispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| ("toString", [EvDate(t)]) => EV.EvString(DateTime.Date.toString(t))->Ok->Some
| ("makeDateFromYear", [EvNumber(year)]) =>
switch DateTime.Date.makeFromYear(year) {
| Ok(t) => EV.EvDate(t)->Ok->Some
| Error(e) => Reducer_ErrorValue.RETodo(e)->Error->Some
}
| ("dateFromNumber", [EvNumber(f)]) => EV.EvDate(DateTime.Date.fromFloat(f))->Ok->Some
| ("toNumber", [EvDate(f)]) => EV.EvNumber(DateTime.Date.toFloat(f))->Ok->Some
| ("subtract", [EvDate(d1), EvDate(d2)]) =>
switch DateTime.Date.subtract(d1, d2) {
| Ok(d) => EV.EvTimeDuration(d)->Ok
| Error(e) => Error(RETodo(e))
}->Some
| ("subtract", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.subtractDuration(d1, d2))->Ok->Some
| ("add", [EvDate(d1), EvTimeDuration(d2)]) =>
EV.EvDate(DateTime.Date.addDuration(d1, d2))->Ok->Some
| _ => None
}
}
let durationDispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>, result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => { > => {
switch call { switch call {
@ -52,19 +27,7 @@ let durationDispatch = (call: EV.functionCall, _: DistributionOperation.env): op
EV.EvTimeDuration(DateTime.Duration.multiply(d1, d2))->Ok->Some EV.EvTimeDuration(DateTime.Duration.multiply(d1, d2))->Ok->Some
| ("divide", [EvTimeDuration(d1), EvNumber(d2)]) => | ("divide", [EvTimeDuration(d1), EvNumber(d2)]) =>
EV.EvTimeDuration(DateTime.Duration.divide(d1, d2))->Ok->Some EV.EvTimeDuration(DateTime.Duration.divide(d1, d2))->Ok->Some
| ("divide", [EvTimeDuration(d1), EvTimeDuration(d2)]) => EV.EvNumber(d1 /. d2)->Ok->Some
| _ => None | _ => None
} }
} }
let dispatch = (call: EV.functionCall, env: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch dateDispatch(call, env) {
| Some(r) => Some(r)
| None =>
switch durationDispatch(call, env) {
| Some(r) => Some(r)
| None => None
}
}
}

View File

@ -4,7 +4,6 @@
*/ */
module Extra_Array = Reducer_Extra_Array module Extra_Array = Reducer_Extra_Array
module ErrorValue = Reducer_ErrorValue module ErrorValue = Reducer_ErrorValue
@genType.opaque @genType.opaque
type internalCode = Object type internalCode = Object
@ -22,6 +21,9 @@ type rec expressionValue =
| EvSymbol(string) | EvSymbol(string)
| EvDate(Js.Date.t) | EvDate(Js.Date.t)
| EvTimeDuration(float) | EvTimeDuration(float)
| EvDeclaration(lambdaDeclaration)
| EvTypeIdentifier(string)
| EvModule(record)
and record = Js.Dict.t<expressionValue> and record = Js.Dict.t<expressionValue>
and externalBindings = record and externalBindings = record
and lambdaValue = { and lambdaValue = {
@ -29,9 +31,7 @@ and lambdaValue = {
context: externalBindings, context: externalBindings,
body: internalCode, body: internalCode,
} }
and lambdaDeclaration = Declaration.declaration<lambdaValue>
@genType
let defaultExternalBindings: externalBindings = Js.Dict.empty()
type functionCall = (string, array<expressionValue>) type functionCall = (string, array<expressionValue>)
@ -55,6 +55,9 @@ let rec toString = aValue =>
| EvDistribution(dist) => GenericDist.toString(dist) | EvDistribution(dist) => GenericDist.toString(dist)
| EvDate(date) => DateTime.Date.toString(date) | EvDate(date) => DateTime.Date.toString(date)
| EvTimeDuration(t) => DateTime.Duration.toString(t) | EvTimeDuration(t) => DateTime.Duration.toString(t)
| EvDeclaration(d) => Declaration.toString(d, r => toString(EvLambda(r)))
| EvTypeIdentifier(id) => `#${id}`
| EvModule(m) => `@${m->toStringRecord}`
} }
and toStringRecord = aRecord => { and toStringRecord = aRecord => {
let pairs = let pairs =
@ -79,6 +82,9 @@ let toStringWithType = aValue =>
| EvSymbol(_) => `Symbol::${toString(aValue)}` | EvSymbol(_) => `Symbol::${toString(aValue)}`
| EvDate(_) => `Date::${toString(aValue)}` | EvDate(_) => `Date::${toString(aValue)}`
| EvTimeDuration(_) => `Date::${toString(aValue)}` | EvTimeDuration(_) => `Date::${toString(aValue)}`
| EvDeclaration(_) => `Declaration::${toString(aValue)}`
| EvTypeIdentifier(_) => `TypeIdentifier::${toString(aValue)}`
| EvModule(_) => `Module::${toString(aValue)}`
} }
let argsToString = (args: array<expressionValue>): string => { let argsToString = (args: array<expressionValue>): string => {
@ -124,6 +130,9 @@ type expressionValueType =
| EvtSymbol | EvtSymbol
| EvtDate | EvtDate
| EvtTimeDuration | EvtTimeDuration
| EvtDeclaration
| EvtTypeIdentifier
| EvtModule
type functionCallSignature = CallSignature(string, array<expressionValueType>) type functionCallSignature = CallSignature(string, array<expressionValueType>)
type functionDefinitionSignature = type functionDefinitionSignature =
@ -143,6 +152,9 @@ let valueToValueType = value =>
| EvSymbol(_) => EvtSymbol | EvSymbol(_) => EvtSymbol
| EvDate(_) => EvtDate | EvDate(_) => EvtDate
| EvTimeDuration(_) => EvtTimeDuration | EvTimeDuration(_) => EvtTimeDuration
| EvDeclaration(_) => EvtDeclaration
| EvTypeIdentifier(_) => EvtTypeIdentifier
| EvModule(_) => EvtModule
} }
let functionCallToCallSignature = (functionCall: functionCall): functionCallSignature => { let functionCallToCallSignature = (functionCall: functionCall): functionCallSignature => {
@ -164,6 +176,9 @@ let valueTypeToString = (valueType: expressionValueType): string =>
| EvtSymbol => `Symbol` | EvtSymbol => `Symbol`
| EvtDate => `Date` | EvtDate => `Date`
| EvtTimeDuration => `Duration` | EvtTimeDuration => `Duration`
| EvtDeclaration => `Declaration`
| EvtTypeIdentifier => `TypeIdentifier`
| EvtModule => `Module`
} }
let functionCallSignatureToString = (functionCallSignature: functionCallSignature): string => { let functionCallSignatureToString = (functionCallSignature: functionCallSignature): string => {

View File

@ -14,15 +14,27 @@ type expressionValue = ExpressionValue.expressionValue
Map external calls of Reducer Map external calls of Reducer
*/ */
// I expect that it's important to build this first, so it doesn't get recalculated for each tryRegistry() call.
let registry = FunctionRegistry_Library.registry
let tryRegistry = ((fnName, args): ExpressionValue.functionCall, env) => {
FunctionRegistry_Core.Registry.matchAndRun(~registry, ~fnName, ~args, ~env)->E.O2.fmap(
E.R2.errMap(_, s => Reducer_ErrorValue.RETodo(s)),
)
}
let dispatch = (call: ExpressionValue.functionCall, environment, chain): result< let dispatch = (call: ExpressionValue.functionCall, environment, chain): result<
expressionValue, expressionValue,
'e, 'e,
> => > => {
switch ReducerInterface_GenericDistribution.dispatch(call, environment) { E.A.O.firstSomeFn([
| Some(r) => r () => ReducerInterface_GenericDistribution.dispatch(call, environment),
| None => () => ReducerInterface_Date.dispatch(call, environment),
ReducerInterface_DateTime.dispatch(call, environment) |> E.O.default(chain(call, environment)) () => ReducerInterface_Duration.dispatch(call, environment),
} () => ReducerInterface_Number.dispatch(call, environment),
() => tryRegistry(call, environment),
])->E.O2.default(chain(call, environment))
}
/* /*
If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally. If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally.

View File

@ -186,8 +186,6 @@ let dispatchToGenericOutput = (
): option<DistributionOperation.outputType> => { ): option<DistributionOperation.outputType> => {
let (fnName, args) = call let (fnName, args) = call
switch (fnName, args) { switch (fnName, args) {
| ("delta", [EvNumber(f)]) =>
SymbolicDist.Float.makeSafe(f)->SymbolicConstructors.symbolicResultToOutput
| ("triangular" as fnName, [EvNumber(f1), EvNumber(f2), EvNumber(f3)]) => | ("triangular" as fnName, [EvNumber(f1), EvNumber(f2), EvNumber(f3)]) =>
SymbolicConstructors.threeFloat(fnName) SymbolicConstructors.threeFloat(fnName)
->E.R.bind(r => r(f1, f2, f3)) ->E.R.bind(r => r(f1, f2, f3))
@ -195,12 +193,23 @@ let dispatchToGenericOutput = (
| ("sample", [EvDistribution(dist)]) => Helpers.toFloatFn(#Sample, dist, ~env) | ("sample", [EvDistribution(dist)]) => Helpers.toFloatFn(#Sample, dist, ~env)
| ("sampleN", [EvDistribution(dist), EvNumber(n)]) => | ("sampleN", [EvDistribution(dist), EvNumber(n)]) =>
Some(FloatArray(GenericDist.sampleN(dist, Belt.Int.fromFloat(n)))) Some(FloatArray(GenericDist.sampleN(dist, Belt.Int.fromFloat(n))))
| ("mean", [EvDistribution(dist)]) => Helpers.toFloatFn(#Mean, dist, ~env) | (("mean" | "stdev" | "variance" | "min" | "max" | "mode") as op, [EvDistribution(dist)]) => {
let fn = switch op {
| "mean" => #Mean
| "stdev" => #Stdev
| "variance" => #Variance
| "min" => #Min
| "max" => #Max
| "mode" => #Mode
| _ => #Mean
}
Helpers.toFloatFn(fn, dist, ~env)
}
| ("integralSum", [EvDistribution(dist)]) => Helpers.toFloatFn(#IntegralSum, dist, ~env) | ("integralSum", [EvDistribution(dist)]) => Helpers.toFloatFn(#IntegralSum, dist, ~env)
| ("toString", [EvDistribution(dist)]) => Helpers.toStringFn(ToString, dist, ~env) | ("toString", [EvDistribution(dist)]) => Helpers.toStringFn(ToString, dist, ~env)
| ("toSparkline", [EvDistribution(dist)]) => | ("sparkline", [EvDistribution(dist)]) =>
Helpers.toStringFn(ToSparkline(MagicNumbers.Environment.sparklineLength), dist, ~env) Helpers.toStringFn(ToSparkline(MagicNumbers.Environment.sparklineLength), dist, ~env)
| ("toSparkline", [EvDistribution(dist), EvNumber(n)]) => | ("sparkline", [EvDistribution(dist), EvNumber(n)]) =>
Helpers.toStringFn(ToSparkline(Belt.Float.toInt(n)), dist, ~env) Helpers.toStringFn(ToSparkline(Belt.Float.toInt(n)), dist, ~env)
| ("exp", [EvDistribution(a)]) => | ("exp", [EvDistribution(a)]) =>
// https://mathjs.org/docs/reference/functions/exp.html // https://mathjs.org/docs/reference/functions/exp.html
@ -232,6 +241,8 @@ let dispatchToGenericOutput = (
Helpers.toDistFn(Scale(#Logarithm, float), dist, ~env) Helpers.toDistFn(Scale(#Logarithm, float), dist, ~env)
| ("scaleLogWithThreshold", [EvDistribution(dist), EvNumber(base), EvNumber(eps)]) => | ("scaleLogWithThreshold", [EvDistribution(dist), EvNumber(base), EvNumber(eps)]) =>
Helpers.toDistFn(Scale(#LogarithmWithThreshold(eps), base), dist, ~env) Helpers.toDistFn(Scale(#LogarithmWithThreshold(eps), base), dist, ~env)
| ("scaleMultiply", [EvDistribution(dist), EvNumber(float)]) =>
Helpers.toDistFn(Scale(#Multiply, float), dist, ~env)
| ("scalePow", [EvDistribution(dist), EvNumber(float)]) => | ("scalePow", [EvDistribution(dist), EvNumber(float)]) =>
Helpers.toDistFn(Scale(#Power, float), dist, ~env) Helpers.toDistFn(Scale(#Power, float), dist, ~env)
| ("scaleExp", [EvDistribution(dist)]) => | ("scaleExp", [EvDistribution(dist)]) =>
@ -239,12 +250,13 @@ let dispatchToGenericOutput = (
| ("cdf", [EvDistribution(dist), EvNumber(float)]) => Helpers.toFloatFn(#Cdf(float), dist, ~env) | ("cdf", [EvDistribution(dist), EvNumber(float)]) => Helpers.toFloatFn(#Cdf(float), dist, ~env)
| ("pdf", [EvDistribution(dist), EvNumber(float)]) => Helpers.toFloatFn(#Pdf(float), dist, ~env) | ("pdf", [EvDistribution(dist), EvNumber(float)]) => Helpers.toFloatFn(#Pdf(float), dist, ~env)
| ("inv", [EvDistribution(dist), EvNumber(float)]) => Helpers.toFloatFn(#Inv(float), dist, ~env) | ("inv", [EvDistribution(dist), EvNumber(float)]) => Helpers.toFloatFn(#Inv(float), dist, ~env)
| ("quantile", [EvDistribution(dist), EvNumber(float)]) =>
Helpers.toFloatFn(#Inv(float), dist, ~env)
| ("toSampleSet", [EvDistribution(dist), EvNumber(float)]) => | ("toSampleSet", [EvDistribution(dist), EvNumber(float)]) =>
Helpers.toDistFn(ToSampleSet(Belt.Int.fromFloat(float)), dist, ~env) Helpers.toDistFn(ToSampleSet(Belt.Int.fromFloat(float)), dist, ~env)
| ("toSampleSet", [EvDistribution(dist)]) => | ("toSampleSet", [EvDistribution(dist)]) =>
Helpers.toDistFn(ToSampleSet(env.sampleCount), dist, ~env) Helpers.toDistFn(ToSampleSet(env.sampleCount), dist, ~env)
| ("toInternalSampleArray", [EvDistribution(SampleSet(dist))]) => | ("toList", [EvDistribution(SampleSet(dist))]) => Some(FloatArray(SampleSetDist.T.get(dist)))
Some(FloatArray(SampleSetDist.T.get(dist)))
| ("fromSamples", [EvArray(inputArray)]) => { | ("fromSamples", [EvArray(inputArray)]) => {
let _wrapInputErrors = x => SampleSetDist.NonNumericInput(x) let _wrapInputErrors = x => SampleSetDist.NonNumericInput(x)
let parsedArray = Helpers.parseNumberArray(inputArray)->E.R2.errMap(_wrapInputErrors) let parsedArray = Helpers.parseNumberArray(inputArray)->E.R2.errMap(_wrapInputErrors)
@ -325,20 +337,5 @@ let genericOutputToReducerValue = (o: DistributionOperation.outputType): result<
| GenDistError(err) => Error(REDistributionError(err)) | GenDistError(err) => Error(REDistributionError(err))
} }
// I expect that it's important to build this first, so it doesn't get recalculated for each tryRegistry() call. let dispatch = (call: ExpressionValue.functionCall, environment) =>
let registry = FunctionRegistry_Library.registry
let tryRegistry = ((fnName, args): ExpressionValue.functionCall, env) => {
FunctionRegistry_Core.Registry.matchAndRun(~registry, ~fnName, ~args, ~env)->E.O2.fmap(
E.R2.errMap(_, s => Reducer_ErrorValue.RETodo(s)),
)
}
let dispatch = (call: ExpressionValue.functionCall, environment) => {
let regularDispatch =
dispatchToGenericOutput(call, environment)->E.O2.fmap(genericOutputToReducerValue) dispatchToGenericOutput(call, environment)->E.O2.fmap(genericOutputToReducerValue)
switch regularDispatch {
| Some(x) => Some(x)
| None => tryRegistry(call, environment)
}
}

View File

@ -0,0 +1,45 @@
module EV = ReducerInterface_ExpressionValue
type expressionValue = EV.expressionValue
module ScientificUnit = {
let nameToMultiplier = str =>
switch str {
| "n" => Some(1E-9)
| "m" => Some(1E-3)
| "k" => Some(1E3)
| "M" => Some(1E6)
| "B" => Some(1E9)
| "G" => Some(1E9)
| "T" => Some(1E12)
| "P" => Some(1E15)
| _ => None
}
let getMultiplier = (r: string) => {
let match = Js.String2.match_(r, %re(`/fromUnit_([_a-zA-Z]*)/`))
switch match {
| Some([_, unit]) => nameToMultiplier(unit)
| _ => None
}
}
}
let dispatch = (call: EV.functionCall, _: DistributionOperation.env): option<
result<expressionValue, QuriSquiggleLang.Reducer_ErrorValue.errorValue>,
> => {
switch call {
| (
("fromUnit_n"
| "fromUnit_m"
| "fromUnit_k"
| "fromUnit_M"
| "fromUnit_B"
| "fromUnit_G"
| "fromUnit_T"
| "fromUnit_P") as op,
[EvNumber(f)],
) =>
op->ScientificUnit.getMultiplier->E.O2.fmap(multiplier => EV.EvNumber(f *. multiplier)->Ok)
| _ => None
}
}

View File

@ -0,0 +1,6 @@
module Bindings = Reducer_Category_Bindings
let internalStdLib = Bindings.emptyBindings->SquiggleLibrary_Math.makeBindings
@genType
let externalStdLib = internalStdLib->Bindings.toRecord

View File

@ -0,0 +1,23 @@
module Bindings = Reducer_Category_Bindings
module Module = Reducer_Category_Module
let availableNumbers: array<(string, float)> = [
("pi", Js.Math._PI),
("e", Js.Math._E),
("ln2", Js.Math._LN2),
("ln10", Js.Math._LN10),
("log2e", Js.Math._LOG2E),
("log10e", Js.Math._LOG10E),
("sqrt2", Js.Math._SQRT2),
("sqrt1_2", Js.Math._SQRT1_2),
("phi", 1.618033988749895),
("tau", 6.283185307179586),
]
let mathBindings: Bindings.ExpressionT.bindings =
availableNumbers
->E.A2.fmap(((name, v)) => (name, ReducerInterface_ExpressionValue.EvNumber(v)))
->Belt.Map.String.fromArray
let makeBindings = (previousBindings: Bindings.t): Bindings.t =>
previousBindings->Bindings.defineModule("Math", mathBindings)

View File

@ -76,6 +76,9 @@ let distributionErrorToString = DistributionTypes.Error.toString
@genType @genType
type lambdaValue = ReducerInterface_ExpressionValue.lambdaValue type lambdaValue = ReducerInterface_ExpressionValue.lambdaValue
@genType
type lambdaDeclaration = ReducerInterface_ExpressionValue.lambdaDeclaration
@genType @genType
let defaultSamplingEnv = DistributionOperation.defaultEnv let defaultSamplingEnv = DistributionOperation.defaultEnv
@ -87,3 +90,9 @@ let defaultEnvironment = ReducerInterface_ExpressionValue.defaultEnvironment
@genType @genType
let foreignFunctionInterface = Reducer.foreignFunctionInterface let foreignFunctionInterface = Reducer.foreignFunctionInterface
@genType
type declarationArg = Declaration.arg
@genType
type declaration<'a> = Declaration.declaration<'a>

View File

@ -0,0 +1,42 @@
@genType
type arg = Float({min: float, max: float}) | Date({min: Js.Date.t, max: Js.Date.t})
@genType
type declaration<'a> = {
fn: 'a,
args: array<arg>,
}
module ContinuousFloatArg = {
let make = (min: float, max: float): arg => {
Float({min: min, max: max})
}
}
module ContinuousTimeArg = {
let make = (min: Js.Date.t, max: Js.Date.t): arg => {
Date({min: min, max: max})
}
}
module Arg = {
let toString = (arg: arg) => {
switch arg {
| Float({min, max}) =>
`Float({min: ${E.Float.with2DigitsPrecision(min)}, max: ${E.Float.with2DigitsPrecision(
max,
)}})`
| Date({min, max}) =>
`Date({min: ${DateTime.Date.toString(min)}, max: ${DateTime.Date.toString(max)}})`
}
}
}
let make = (fn: 'a, args: array<arg>): declaration<'a> => {
{fn: fn, args: args}
}
let toString = (r: declaration<'a>, fnToString): string => {
let args = r.args->E.A2.fmap(Arg.toString) |> E.A.joinWith(", ")
return`fn: ${fnToString(r.fn)}, args: [${args}]`
}

View File

@ -55,6 +55,10 @@ module Tuple2 = {
let toFnCall = (fn, (a1, a2)) => fn(a1, a2) let toFnCall = (fn, (a1, a2)) => fn(a1, a2)
} }
module Tuple3 = {
let toFnCall = (fn, (a1, a2, a3)) => fn(a1, a2, a3)
}
module O = { module O = {
let dimap = (sFn, rFn, e) => let dimap = (sFn, rFn, e) =>
switch e { switch e {
@ -203,6 +207,7 @@ module Float = {
let toFixed = Js.Float.toFixed let toFixed = Js.Float.toFixed
let toString = Js.Float.toString let toString = Js.Float.toString
let isFinite = Js.Float.isFinite let isFinite = Js.Float.isFinite
let toInt = Belt.Float.toInt
} }
module I = { module I = {
@ -535,6 +540,7 @@ module A = {
let hasBy = (r, fn) => Belt.Array.getBy(r, fn) |> O.isSome let hasBy = (r, fn) => Belt.Array.getBy(r, fn) |> O.isSome
let fold_left = Array.fold_left let fold_left = Array.fold_left
let fold_right = Array.fold_right let fold_right = Array.fold_right
let concat = Belt.Array.concat
let concatMany = Belt.Array.concatMany let concatMany = Belt.Array.concatMany
let keepMap = Belt.Array.keepMap let keepMap = Belt.Array.keepMap
let slice = Belt.Array.slice let slice = Belt.Array.slice
@ -568,6 +574,9 @@ module A = {
let tail = Belt.Array.sliceToEnd(_, 1) let tail = Belt.Array.sliceToEnd(_, 1)
let zip = Belt.Array.zip let zip = Belt.Array.zip
let unzip = Belt.Array.unzip
let zip3 = (a, b, c) =>
Belt.Array.zip(a, b)->Belt.Array.zip(c)->Belt.Array.map((((v1, v2), v3)) => (v1, v2, v3))
// This zips while taking the longest elements of each array. // This zips while taking the longest elements of each array.
let zipMaxLength = (array1, array2) => { let zipMaxLength = (array1, array2) => {
let maxLength = Int.max(length(array1), length(array2)) let maxLength = Int.max(length(array1), length(array2))
@ -714,6 +723,7 @@ module A = {
let variance = Jstat.variance let variance = Jstat.variance
let stdev = Jstat.stdev let stdev = Jstat.stdev
let sum = Jstat.sum let sum = Jstat.sum
let product = Jstat.product
let random = Js.Math.random_int let random = Js.Math.random_int
let floatCompare: (float, float) => int = compare let floatCompare: (float, float) => int = compare
@ -742,6 +752,9 @@ module A = {
let diff = (t: t): array<float> => let diff = (t: t): array<float> =>
Belt.Array.zipBy(t, Belt.Array.sliceToEnd(t, 1), (left, right) => right -. left) Belt.Array.zipBy(t, Belt.Array.sliceToEnd(t, 1), (left, right) => right -. left)
let cumsum = (t: t): array<float> => accumulate((a, b) => a +. b, t)
let cumProd = (t: t): array<float> => accumulate((a, b) => a *. b, t)
exception RangeError(string) exception RangeError(string)
let range = (min: float, max: float, n: int): array<float> => let range = (min: float, max: float, n: int): array<float> =>
switch n { switch n {
@ -865,4 +878,8 @@ module Dict = {
type t<'a> = Js.Dict.t<'a> type t<'a> = Js.Dict.t<'a>
let get = Js.Dict.get let get = Js.Dict.get
let keys = Js.Dict.keys let keys = Js.Dict.keys
let fromArray = Js.Dict.fromArray
let toArray = Js.Dict.entries
let concat = (a, b) => A.concat(toArray(a), toArray(b))->fromArray
let concatMany = ts => ts->A2.fmap(toArray)->A.concatMany->fromArray
} }

View File

@ -26,6 +26,8 @@ type distToFloatOperation = [
| #Inv(float) | #Inv(float)
| #Mean | #Mean
| #Sample | #Sample
| #Min
| #Max
] ]
module Convolution = { module Convolution = {

View File

@ -148,6 +148,11 @@ module T = {
| None => Ok(attempt) | None => Ok(attempt)
} }
} }
let makeFromZipped = (values: array<(float, float)>) => {
let (xs, ys) = E.A.unzip(values)
make(~xs, ~ys)
}
} }
module Ts = { module Ts = {

View File

@ -0,0 +1,24 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/naming-convention": "warn",
"@typescript-eslint/semi": "warn",
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": [
"out",
"dist",
"**/*.d.ts"
]
}

3
packages/vscode-ext/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/media/vendor
/out
/*.vsix

View File

@ -0,0 +1,7 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

34
packages/vscode-ext/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,34 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

Some files were not shown because too many files have changed in this diff Show More