Merge pull request #674 from quantified-uncertainty/develop
June 10th Dev -> Master
This commit is contained in:
commit
8d9d4f397d
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
|
@ -8,6 +8,6 @@ updates:
|
|||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "⬆️"
|
||||
|
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -68,6 +68,8 @@ jobs:
|
|||
working-directory: packages/squiggle-lang
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Install dependencies from monorepo level
|
||||
run: cd ../../ && yarn
|
||||
- name: Build rescript codebase
|
||||
|
@ -98,7 +100,7 @@ jobs:
|
|||
uses: creyD/prettier_action@v4.2
|
||||
with:
|
||||
dry: true
|
||||
prettier_options: --check packages/components
|
||||
prettier_options: --check packages/components --ignore-path packages/components/.prettierignore
|
||||
|
||||
components-bundle-build:
|
||||
name: Components bundle and build
|
||||
|
@ -152,5 +154,7 @@ jobs:
|
|||
run: cd ../../ && yarn
|
||||
- name: Build rescript in squiggle-lang
|
||||
run: cd ../squiggle-lang && yarn build
|
||||
- name: Build components
|
||||
run: cd ../components && yarn build
|
||||
- name: Build website assets
|
||||
run: yarn build
|
||||
|
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -12,12 +12,6 @@
|
|||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- production
|
||||
- staging
|
||||
- develop
|
||||
schedule:
|
||||
- cron: "42 19 * * 0"
|
||||
|
||||
|
|
|
@ -11,3 +11,4 @@ packages/squiggle-lang/.nyc_output/
|
|||
packages/squiggle-lang/coverage/
|
||||
packages/squiggle-lang/.cache/
|
||||
packages/website/build/
|
||||
packages/squiggle-lang/src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
|
||||
|
|
|
@ -40,8 +40,6 @@ the packages can be found in `packages`.
|
|||
- `packages/website` is the main descriptive website for squiggle,
|
||||
it is hosted at `squiggle-language.com`.
|
||||
|
||||
The playground depends on the components library which then depends on the language. This means that if you wish to work on the components library, you will need to build (no need to bundle) the language, and as of this writing playground doesn't really work.
|
||||
|
||||
# Develop
|
||||
|
||||
For any project in the repo, begin by running `yarn` in the top level
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
dist/
|
||||
storybook-static
|
||||
src/styles/base.css
|
||||
|
|
|
@ -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 = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
This package contains the react components for squiggle. These can be used either as a library or hosted as a [storybook](https://storybook.js.org/).
|
||||
|
||||
The `@quri/squiggle-components` package offers several components and utilities for people who want to embed Squiggle components into websites.
|
||||
|
||||
# Usage in a `react` project
|
||||
|
||||
For example, in a fresh `create-react-app` project
|
||||
|
|
|
@ -1,55 +1,65 @@
|
|||
{
|
||||
"name": "@quri/squiggle-components",
|
||||
"version": "0.2.19",
|
||||
"version": "0.2.20",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.4",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^2.9.0",
|
||||
"@quri/squiggle-lang": "^0.2.8",
|
||||
"@react-hook/size": "^2.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.1.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-use": "^17.3.2",
|
||||
"react-vega": "^7.5.0",
|
||||
"styled-components": "^5.3.5",
|
||||
"react-hook-form": "^7.31.3",
|
||||
"react-use": "^17.4.0",
|
||||
"react-vega": "^7.5.1",
|
||||
"vega": "^5.22.1",
|
||||
"vega-embed": "^6.20.6",
|
||||
"vega-lite": "^5.2.0"
|
||||
"vega-lite": "^5.2.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||
"@storybook/addon-actions": "^6.4.22",
|
||||
"@storybook/addon-essentials": "^6.4.22",
|
||||
"@storybook/addon-links": "^6.4.22",
|
||||
"@storybook/builder-webpack5": "^6.4.22",
|
||||
"@storybook/manager-webpack5": "^6.4.22",
|
||||
"@storybook/node-logger": "^6.4.22",
|
||||
"@storybook/preset-create-react-app": "^4.1.0",
|
||||
"@storybook/react": "^6.4.22",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.17.12",
|
||||
"@storybook/addon-actions": "^6.5.7",
|
||||
"@storybook/addon-essentials": "^6.5.7",
|
||||
"@storybook/addon-links": "^6.5.7",
|
||||
"@storybook/builder-webpack5": "^6.5.7",
|
||||
"@storybook/manager-webpack5": "^6.5.7",
|
||||
"@storybook/node-logger": "^6.5.6",
|
||||
"@storybook/preset-create-react-app": "^4.1.2",
|
||||
"@storybook/react": "^6.5.7",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.2.0",
|
||||
"@testing-library/user-event": "^14.1.1",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^14.2.0",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/node": "^17.0.31",
|
||||
"@types/react": "^18.0.3",
|
||||
"@types/react-dom": "^18.0.2",
|
||||
"@types/node": "^17.0.40",
|
||||
"@types/react": "^18.0.9",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/styled-components": "^5.1.24",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"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-scripts": "^5.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"ts-loader": "^9.3.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.3",
|
||||
"typescript": "^4.7.3",
|
||||
"web-vitals": "^2.1.4",
|
||||
"webpack": "^5.72.0",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.8.1"
|
||||
"webpack-dev-server": "^4.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
|
||||
"build": "tsc -b && build-storybook -s public",
|
||||
"build": "tsc -b && postcss ./src/styles/main.css -o ./dist/main.css && build-storybook -s public",
|
||||
"bundle": "webpack",
|
||||
"all": "yarn bundle && yarn build",
|
||||
"lint": "prettier --check .",
|
||||
|
|
9
packages/components/postcss.config.js
Normal file
9
packages/components/postcss.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
cssnano: {},
|
||||
},
|
||||
};
|
86
packages/components/src/components/Alert.tsx
Normal file
86
packages/components/src/components/Alert.tsx
Normal 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"
|
||||
/>
|
||||
);
|
|
@ -14,13 +14,13 @@ interface CodeEditorProps {
|
|||
showGutter?: boolean;
|
||||
}
|
||||
|
||||
export let CodeEditor: FC<CodeEditorProps> = ({
|
||||
export const CodeEditor: FC<CodeEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
oneLine = false,
|
||||
showGutter = false,
|
||||
height,
|
||||
}: CodeEditorProps) => {
|
||||
}) => {
|
||||
let lineCount = value.split("\n").length;
|
||||
let id = _.uniqueId();
|
||||
return (
|
||||
|
@ -29,6 +29,7 @@ export let CodeEditor: FC<CodeEditorProps> = ({
|
|||
mode="golang"
|
||||
theme="github"
|
||||
width={"100%"}
|
||||
fontSize={14}
|
||||
height={String(height) + "px"}
|
||||
minLines={oneLine ? lineCount : undefined}
|
||||
maxLines={oneLine ? lineCount : undefined}
|
||||
|
@ -47,4 +48,3 @@ export let CodeEditor: FC<CodeEditorProps> = ({
|
|||
/>
|
||||
);
|
||||
};
|
||||
export default CodeEditor;
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
import type { Distribution } from "@quri/squiggle-lang";
|
||||
import { distributionErrorToString } from "@quri/squiggle-lang";
|
||||
import {
|
||||
Distribution,
|
||||
result,
|
||||
distributionError,
|
||||
distributionErrorToString,
|
||||
} from "@quri/squiggle-lang";
|
||||
import { Vega, VisualizationSpec } from "react-vega";
|
||||
import * as chartSpecification from "../vega-specs/spec-distributions.json";
|
||||
import { ErrorBox } from "./ErrorBox";
|
||||
import { ErrorAlert } from "./Alert";
|
||||
import { useSize } from "react-use";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
linearXScale,
|
||||
logXScale,
|
||||
linearYScale,
|
||||
expYScale,
|
||||
} from "./DistributionVegaScales";
|
||||
import styled from "styled-components";
|
||||
import { NumberShower } from "./NumberShower";
|
||||
|
||||
type DistributionChartProps = {
|
||||
distribution: Distribution;
|
||||
width?: number;
|
||||
height: number;
|
||||
/** Whether to show a summary of means, stdev, percentiles etc */
|
||||
showSummary: boolean;
|
||||
/** Whether to show the user graph controls (scale etc) */
|
||||
showControls?: boolean;
|
||||
};
|
||||
|
@ -25,64 +32,67 @@ type DistributionChartProps = {
|
|||
export const DistributionChart: React.FC<DistributionChartProps> = ({
|
||||
distribution,
|
||||
height,
|
||||
showSummary,
|
||||
width,
|
||||
showControls = false,
|
||||
}: DistributionChartProps) => {
|
||||
let [isLogX, setLogX] = React.useState(false);
|
||||
let [isExpY, setExpY] = React.useState(false);
|
||||
let shape = distribution.pointSet();
|
||||
const [sized, _] = useSize((size) => {
|
||||
if (shape.tag === "Ok") {
|
||||
let massBelow0 =
|
||||
shape.value.continuous.some((x) => x.x <= 0) ||
|
||||
shape.value.discrete.some((x) => x.x <= 0);
|
||||
let spec = buildVegaSpec(isLogX, isExpY);
|
||||
let widthProp = width ? width - 20 : size.width - 10;
|
||||
|
||||
// Check whether we should disable the checkbox
|
||||
var logCheckbox = (
|
||||
<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."
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var result = (
|
||||
<div>
|
||||
<Vega
|
||||
spec={spec}
|
||||
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
|
||||
width={widthProp}
|
||||
height={height}
|
||||
actions={false}
|
||||
/>
|
||||
{showControls && (
|
||||
<div>
|
||||
{logCheckbox}
|
||||
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var result = (
|
||||
<ErrorBox heading="Distribution Error">
|
||||
}) => {
|
||||
const [isLogX, setLogX] = React.useState(false);
|
||||
const [isExpY, setExpY] = React.useState(false);
|
||||
const shape = distribution.pointSet();
|
||||
const [sized] = useSize((size) => {
|
||||
if (shape.tag === "Error") {
|
||||
return (
|
||||
<ErrorAlert heading="Distribution Error">
|
||||
{distributionErrorToString(shape.value)}
|
||||
</ErrorBox>
|
||||
</ErrorAlert>
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
const massBelow0 =
|
||||
shape.value.continuous.some((x) => x.x <= 0) ||
|
||||
shape.value.discrete.some((x) => x.x <= 0);
|
||||
const spec = buildVegaSpec(isLogX, isExpY);
|
||||
|
||||
let widthProp = width ? width : size.width;
|
||||
if (widthProp < 20) {
|
||||
console.warn(
|
||||
`Width of Distribution is set to ${widthProp}, which is too small`
|
||||
);
|
||||
widthProp = 20;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: widthProp }}>
|
||||
<Vega
|
||||
spec={spec}
|
||||
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
|
||||
width={widthProp - 10}
|
||||
height={height}
|
||||
actions={false}
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
{showSummary && <SummaryTable distribution={distribution} />}
|
||||
</div>
|
||||
{showControls && (
|
||||
<div>
|
||||
<CheckBox
|
||||
label="Log X scale"
|
||||
value={isLogX}
|
||||
onChange={setLogX}
|
||||
// Check whether we should disable the checkbox
|
||||
{...(massBelow0
|
||||
? {
|
||||
disabled: true,
|
||||
tooltip:
|
||||
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.",
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return sized;
|
||||
};
|
||||
|
@ -105,17 +115,13 @@ interface CheckBoxProps {
|
|||
tooltip?: string;
|
||||
}
|
||||
|
||||
const Label = styled.label<{ disabled: boolean }>`
|
||||
${(props) => props.disabled && "color: #999;"}
|
||||
`;
|
||||
|
||||
export const CheckBox = ({
|
||||
export const CheckBox: React.FC<CheckBoxProps> = ({
|
||||
label,
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
tooltip,
|
||||
}: CheckBoxProps) => {
|
||||
}) => {
|
||||
return (
|
||||
<span title={tooltip}>
|
||||
<input
|
||||
|
@ -124,7 +130,85 @@ export const CheckBox = ({
|
|||
onChange={() => onChange(!value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Label disabled={disabled}>{label}</Label>
|
||||
<label className={clsx(disabled && "text-slate-400")}> {label}</label>
|
||||
</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 = {
|
||||
distribution: Distribution;
|
||||
};
|
||||
|
||||
const SummaryTable: React.FC<SummaryTableProps> = ({ distribution }) => {
|
||||
const mean = distribution.mean();
|
||||
const stdev = distribution.stdev();
|
||||
const p5 = distribution.inv(0.05);
|
||||
const p10 = distribution.inv(0.1);
|
||||
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 hasResult = (x: result<number, distributionError>): boolean =>
|
||||
x.tag === "Ok";
|
||||
|
||||
const unwrapResult = (
|
||||
x: result<number, distributionError>
|
||||
): React.ReactNode => {
|
||||
if (x.tag === "Ok") {
|
||||
return <NumberShower number={x.value} />;
|
||||
} else {
|
||||
return (
|
||||
<ErrorAlert heading="Distribution Error">
|
||||
{distributionErrorToString(x.value)}
|
||||
</ErrorAlert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<table className="border border-collapse border-slate-400">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<TableHeadCell>{"Mean"}</TableHeadCell>
|
||||
{hasResult(stdev) && <TableHeadCell>{"Stdev"}</TableHeadCell>}
|
||||
<TableHeadCell>{"5%"}</TableHeadCell>
|
||||
<TableHeadCell>{"10%"}</TableHeadCell>
|
||||
<TableHeadCell>{"25%"}</TableHeadCell>
|
||||
<TableHeadCell>{"50%"}</TableHeadCell>
|
||||
<TableHeadCell>{"75%"}</TableHeadCell>
|
||||
<TableHeadCell>{"90%"}</TableHeadCell>
|
||||
<TableHeadCell>{"95%"}</TableHeadCell>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<Cell>{unwrapResult(mean)}</Cell>
|
||||
{hasResult(stdev) && <Cell>{unwrapResult(stdev)}</Cell>}
|
||||
<Cell>{unwrapResult(p5)}</Cell>
|
||||
<Cell>{unwrapResult(p10)}</Cell>
|
||||
<Cell>{unwrapResult(p25)}</Cell>
|
||||
<Cell>{unwrapResult(p50)}</Cell>
|
||||
<Cell>{unwrapResult(p75)}</Cell>
|
||||
<Cell>{unwrapResult(p90)}</Cell>
|
||||
<Cell>{unwrapResult(p95)}</Cell>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1,115 +1,79 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
import type { Spec } from "vega";
|
||||
import type { Distribution, errorValue, result } from "@quri/squiggle-lang";
|
||||
import { createClassFromSpec } from "react-vega";
|
||||
import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
|
||||
import { DistributionChart } from "./DistributionChart";
|
||||
import { ErrorBox } from "./ErrorBox";
|
||||
import { lambdaValue, environment, runForeign } from "@quri/squiggle-lang";
|
||||
import { FunctionChart1Dist } from "./FunctionChart1Dist";
|
||||
import { FunctionChart1Number } from "./FunctionChart1Number";
|
||||
import { ErrorAlert, MessageAlert } from "./Alert";
|
||||
|
||||
let SquigglePercentilesChart = createClassFromSpec({
|
||||
spec: percentilesSpec as Spec,
|
||||
});
|
||||
|
||||
type distPlusFn = (a: number) => result<Distribution, errorValue>;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
function unwrap<a, b>(x: result<a, b>): a {
|
||||
if (x.tag === "Ok") {
|
||||
return x.value;
|
||||
} else {
|
||||
throw Error("FAILURE TO UNWRAP");
|
||||
}
|
||||
interface FunctionChartProps {
|
||||
fn: lambdaValue;
|
||||
chartSettings: FunctionChartSettings;
|
||||
environment: environment;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function mapFilter<a, b>(xs: a[], f: (x: a) => b | undefined): b[] {
|
||||
let initial: b[] = [];
|
||||
return xs.reduce((previous, current) => {
|
||||
let value: b | undefined = f(current);
|
||||
if (value !== undefined) {
|
||||
return previous.concat([value]);
|
||||
} else {
|
||||
return previous;
|
||||
}
|
||||
}, initial);
|
||||
}
|
||||
|
||||
export const FunctionChart: React.FC<{
|
||||
distPlusFn: distPlusFn;
|
||||
diagramStart: number;
|
||||
diagramStop: number;
|
||||
diagramCount: number;
|
||||
}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => {
|
||||
let [mouseOverlay, setMouseOverlay] = React.useState(0);
|
||||
function handleHover(...args) {
|
||||
setMouseOverlay(args[1]);
|
||||
}
|
||||
function handleOut() {
|
||||
setMouseOverlay(NaN);
|
||||
}
|
||||
const signalListeners = { mousemove: handleHover, mouseout: handleOut };
|
||||
let mouseItem = distPlusFn(mouseOverlay);
|
||||
let showChart =
|
||||
mouseItem.tag === "Ok" ? (
|
||||
<DistributionChart
|
||||
distribution={mouseItem.value}
|
||||
width={400}
|
||||
height={140}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
export const FunctionChart: React.FC<FunctionChartProps> = ({
|
||||
fn,
|
||||
chartSettings,
|
||||
environment,
|
||||
height,
|
||||
}) => {
|
||||
if (fn.parameters.length > 1) {
|
||||
return (
|
||||
<MessageAlert heading="Function Display Not Supported">
|
||||
Only functions with one parameter are displayed.
|
||||
</MessageAlert>
|
||||
);
|
||||
let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount);
|
||||
let valueData = mapFilter(data1, (x) => {
|
||||
let result = distPlusFn(x);
|
||||
if (result.tag === "Ok") {
|
||||
return { x: x, value: result.value };
|
||||
}
|
||||
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;
|
||||
}
|
||||
}).map(({ x, value }) => {
|
||||
return {
|
||||
x: x,
|
||||
p1: unwrap(value.inv(0.01)),
|
||||
p5: unwrap(value.inv(0.05)),
|
||||
p10: unwrap(value.inv(0.12)),
|
||||
p20: unwrap(value.inv(0.2)),
|
||||
p30: unwrap(value.inv(0.3)),
|
||||
p40: unwrap(value.inv(0.4)),
|
||||
p50: unwrap(value.inv(0.5)),
|
||||
p60: unwrap(value.inv(0.6)),
|
||||
p70: unwrap(value.inv(0.7)),
|
||||
p80: unwrap(value.inv(0.8)),
|
||||
p90: unwrap(value.inv(0.9)),
|
||||
p95: unwrap(value.inv(0.95)),
|
||||
p99: unwrap(value.inv(0.99)),
|
||||
};
|
||||
});
|
||||
};
|
||||
const validResult = getValidResult();
|
||||
const resultType =
|
||||
validResult.tag === "Ok" ? validResult.value.tag : ("Error" as const);
|
||||
|
||||
let errorData = mapFilter(data1, (x) => {
|
||||
let result = distPlusFn(x);
|
||||
if (result.tag === "Error") {
|
||||
return { x: x, error: result.value };
|
||||
}
|
||||
});
|
||||
let error2 = _.groupBy(errorData, (x) => x.error);
|
||||
return (
|
||||
<>
|
||||
<SquigglePercentilesChart
|
||||
data={{ facet: valueData }}
|
||||
actions={false}
|
||||
signalListeners={signalListeners}
|
||||
/>
|
||||
{showChart}
|
||||
{_.keysIn(error2).map((k) => (
|
||||
<ErrorBox heading={k}>
|
||||
{`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`}
|
||||
</ErrorBox>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
212
packages/components/src/components/FunctionChart1Dist.tsx
Normal file
212
packages/components/src/components/FunctionChart1Dist.tsx
Normal 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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
119
packages/components/src/components/FunctionChart1Number.tsx
Normal file
119
packages/components/src/components/FunctionChart1Number.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
49
packages/components/src/components/JsonEditor.tsx
Normal file
49
packages/components/src/components/JsonEditor.tsx
Normal 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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
const orderOfMagnitudeNum = (n: number) => {
|
||||
return Math.pow(10, n);
|
||||
|
@ -74,25 +73,23 @@ export interface NumberShowerProps {
|
|||
precision?: number;
|
||||
}
|
||||
|
||||
export let NumberShower: React.FC<NumberShowerProps> = ({
|
||||
export const NumberShower: React.FC<NumberShowerProps> = ({
|
||||
number,
|
||||
precision = 2,
|
||||
}: NumberShowerProps) => {
|
||||
let numberWithPresentation = numberShow(number, precision);
|
||||
}) => {
|
||||
const numberWithPresentation = numberShow(number, precision);
|
||||
return (
|
||||
<span>
|
||||
{numberWithPresentation.value}
|
||||
{numberWithPresentation.symbol}
|
||||
{numberWithPresentation.power ? (
|
||||
<span>
|
||||
{"\u00b710"}
|
||||
{"\u00b7" /* dot symbol */}10
|
||||
<span style={{ fontSize: "0.6em", verticalAlign: "super" }}>
|
||||
{numberWithPresentation.power}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,88 +1,103 @@
|
|||
import * as React from "react";
|
||||
import _ from "lodash";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
run,
|
||||
errorValueToString,
|
||||
squiggleExpression,
|
||||
bindings,
|
||||
samplingParams,
|
||||
environment,
|
||||
jsImports,
|
||||
defaultImports,
|
||||
defaultBindings,
|
||||
defaultEnvironment,
|
||||
declaration,
|
||||
} from "@quri/squiggle-lang";
|
||||
import { NumberShower } from "./NumberShower";
|
||||
import { DistributionChart } from "./DistributionChart";
|
||||
import { ErrorBox } from "./ErrorBox";
|
||||
import { ErrorAlert } from "./Alert";
|
||||
import { FunctionChart, FunctionChartSettings } from "./FunctionChart";
|
||||
|
||||
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;
|
||||
`,
|
||||
};
|
||||
function getRange<a>(x: declaration<a>) {
|
||||
let first = x.args[0];
|
||||
switch (first.tag) {
|
||||
case "Float": {
|
||||
return { floats: { min: first.value.min, max: first.value.max } };
|
||||
}
|
||||
case "Date": {
|
||||
return { time: { min: first.value.min, max: first.value.max } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
|
||||
let range = getRange(x);
|
||||
let min = range.floats ? range.floats.min : 0;
|
||||
let max = range.floats ? range.floats.max : 10;
|
||||
return {
|
||||
start: min,
|
||||
stop: max,
|
||||
count: 20,
|
||||
};
|
||||
}
|
||||
|
||||
interface VariableBoxProps {
|
||||
heading: string;
|
||||
children: React.ReactNode;
|
||||
showTypes?: boolean;
|
||||
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>
|
||||
<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 <>{children}</>;
|
||||
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;
|
||||
showTypes: boolean;
|
||||
/** Whether to show users graph controls (scale etc) */
|
||||
showControls?: boolean;
|
||||
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,
|
||||
}: SquiggleItemProps) => {
|
||||
chartSettings,
|
||||
environment,
|
||||
}) => {
|
||||
switch (expression.tag) {
|
||||
case "number":
|
||||
return (
|
||||
<VariableBox heading="Number" showTypes={showTypes}>
|
||||
<NumberShower precision={3} number={expression.value} />
|
||||
<div className="font-semibold text-slate-600">
|
||||
<NumberShower precision={3} number={expression.value} />
|
||||
</div>
|
||||
</VariableBox>
|
||||
);
|
||||
case "distribution": {
|
||||
|
@ -93,16 +108,13 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
|
|||
showTypes={showTypes}
|
||||
>
|
||||
{distType === "Symbolic" && showTypes ? (
|
||||
<>
|
||||
<div>{expression.value.toString()}</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div>{expression.value.toString()}</div>
|
||||
) : null}
|
||||
<DistributionChart
|
||||
distribution={expression.value}
|
||||
height={height}
|
||||
width={width}
|
||||
showSummary={showSummary}
|
||||
showControls={showControls}
|
||||
/>
|
||||
</VariableBox>
|
||||
|
@ -110,10 +122,13 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
|
|||
}
|
||||
case "string":
|
||||
return (
|
||||
<VariableBox
|
||||
heading="String"
|
||||
showTypes={showTypes}
|
||||
>{`"${expression.value}"`}</VariableBox>
|
||||
<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 (
|
||||
|
@ -124,7 +139,8 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
|
|||
case "symbol":
|
||||
return (
|
||||
<VariableBox heading="Symbol" showTypes={showTypes}>
|
||||
{expression.value}
|
||||
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
|
||||
<span className="text-slate-600">{expression.value}</span>
|
||||
</VariableBox>
|
||||
);
|
||||
case "call":
|
||||
|
@ -136,46 +152,108 @@ const SquiggleItem: React.FC<SquiggleItemProps> = ({
|
|||
case "array":
|
||||
return (
|
||||
<VariableBox heading="Array" showTypes={showTypes}>
|
||||
{expression.value.map((r) => (
|
||||
<SquiggleItem
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
height={50}
|
||||
showTypes={showTypes}
|
||||
showControls={showControls}
|
||||
/>
|
||||
{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}>
|
||||
{Object.entries(expression.value).map(([key, r]) => (
|
||||
<>
|
||||
<RecordKeyHeader>{key}</RecordKeyHeader>
|
||||
<SquiggleItem
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
height={50}
|
||||
showTypes={showTypes}
|
||||
showControls={showControls}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
<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}"`)}
|
||||
{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 (
|
||||
<ErrorBox heading="No Viewer">
|
||||
There is no viewer currently available for function types.
|
||||
</ErrorBox>
|
||||
<VariableBox heading="Function" showTypes={showTypes}>
|
||||
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
|
||||
","
|
||||
)})`}</div>
|
||||
<FunctionChart
|
||||
fn={expression.value}
|
||||
chartSettings={chartSettings}
|
||||
height={height}
|
||||
environment={{
|
||||
sampleCount: environment.sampleCount / 10,
|
||||
xyPointLength: environment.xyPointLength / 10,
|
||||
}}
|
||||
/>
|
||||
</VariableBox>
|
||||
);
|
||||
case "lambdaDeclaration": {
|
||||
return (
|
||||
<VariableBox heading="Function Declaration" showTypes={showTypes}>
|
||||
<FunctionChart
|
||||
fn={expression.value.fn}
|
||||
chartSettings={getChartSettings(expression.value)}
|
||||
height={height}
|
||||
environment={{
|
||||
sampleCount: environment.sampleCount / 10,
|
||||
xyPointLength: environment.xyPointLength / 10,
|
||||
}}
|
||||
/>
|
||||
</VariableBox>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <>Should be unreachable</>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -185,15 +263,9 @@ export interface SquiggleChartProps {
|
|||
/** If the output requires monte carlo sampling, the amount of samples */
|
||||
sampleCount?: number;
|
||||
/** The amount of points returned to draw the distribution */
|
||||
outputXYPoints?: number;
|
||||
kernelWidth?: number;
|
||||
pointDistLength?: number;
|
||||
/** If the result is a function, where the function starts */
|
||||
diagramStart?: number;
|
||||
/** If the result is a function, where the function ends */
|
||||
diagramStop?: number;
|
||||
/** If the result is a function, how many points along the function it samples */
|
||||
diagramCount?: number;
|
||||
environment?: environment;
|
||||
/** If the result is a function, where the function starts, ends and the amount of stops */
|
||||
chartSettings?: FunctionChartSettings;
|
||||
/** When the environment changes */
|
||||
onChange?(expr: squiggleExpression): void;
|
||||
/** CSS width of the element */
|
||||
|
@ -203,59 +275,51 @@ export interface SquiggleChartProps {
|
|||
bindings?: bindings;
|
||||
/** JS imported parameters */
|
||||
jsImports?: jsImports;
|
||||
/** Whether to show a summary of the distirbution */
|
||||
showSummary?: boolean;
|
||||
/** Whether to show type information about returns, default false */
|
||||
showTypes?: boolean;
|
||||
/** Whether to show graph controls (scale etc)*/
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
const ChartWrapper = styled.div`
|
||||
font-family: -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";
|
||||
`;
|
||||
const defaultChartSettings = { start: 0, stop: 10, count: 20 };
|
||||
|
||||
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
||||
squiggleString = "",
|
||||
sampleCount = 1000,
|
||||
outputXYPoints = 1000,
|
||||
environment,
|
||||
onChange = () => {},
|
||||
height = 60,
|
||||
height = 200,
|
||||
bindings = defaultBindings,
|
||||
jsImports = defaultImports,
|
||||
showSummary = false,
|
||||
width,
|
||||
showTypes = false,
|
||||
showControls = false,
|
||||
}: SquiggleChartProps) => {
|
||||
let samplingInputs: samplingParams = {
|
||||
sampleCount: sampleCount,
|
||||
xyPointLength: outputXYPoints,
|
||||
};
|
||||
let expressionResult = run(
|
||||
squiggleString,
|
||||
bindings,
|
||||
samplingInputs,
|
||||
jsImports
|
||||
);
|
||||
let internal: JSX.Element;
|
||||
if (expressionResult.tag === "Ok") {
|
||||
let expression = expressionResult.value;
|
||||
onChange(expression);
|
||||
internal = (
|
||||
<SquiggleItem
|
||||
expression={expression}
|
||||
width={width}
|
||||
height={height}
|
||||
showTypes={showTypes}
|
||||
showControls={showControls}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
internal = (
|
||||
<ErrorBox heading={"Parse Error"}>
|
||||
chartSettings = defaultChartSettings,
|
||||
}) => {
|
||||
let expressionResult = run(squiggleString, bindings, environment, jsImports);
|
||||
if (expressionResult.tag !== "Ok") {
|
||||
return (
|
||||
<ErrorAlert heading={"Parse Error"}>
|
||||
{errorValueToString(expressionResult.value)}
|
||||
</ErrorBox>
|
||||
</ErrorAlert>
|
||||
);
|
||||
}
|
||||
return <ChartWrapper>{internal}</ChartWrapper>;
|
||||
|
||||
let e = environment ?? defaultEnvironment;
|
||||
let expression = expressionResult.value;
|
||||
onChange(expression);
|
||||
return (
|
||||
<SquiggleItem
|
||||
expression={expression}
|
||||
width={width}
|
||||
height={height}
|
||||
showSummary={showSummary}
|
||||
showTypes={showTypes}
|
||||
showControls={showControls}
|
||||
chartSettings={chartSettings}
|
||||
environment={e}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
25
packages/components/src/components/SquiggleContainer.tsx
Normal file
25
packages/components/src/components/SquiggleContainer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -2,10 +2,9 @@ import * as React from "react";
|
|||
import * as ReactDOM from "react-dom";
|
||||
import { SquiggleChart } from "./SquiggleChart";
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
import styled from "styled-components";
|
||||
import type {
|
||||
squiggleExpression,
|
||||
samplingParams,
|
||||
environment,
|
||||
bindings,
|
||||
jsImports,
|
||||
} from "@quri/squiggle-lang";
|
||||
|
@ -15,17 +14,14 @@ import {
|
|||
defaultImports,
|
||||
defaultBindings,
|
||||
} from "@quri/squiggle-lang";
|
||||
import { ErrorBox } from "./ErrorBox";
|
||||
import { ErrorAlert } from "./Alert";
|
||||
import { SquiggleContainer } from "./SquiggleContainer";
|
||||
|
||||
export interface SquiggleEditorProps {
|
||||
/** The input string for squiggle */
|
||||
initialSquiggleString?: string;
|
||||
/** If the output requires monte carlo sampling, the amount of samples */
|
||||
sampleCount?: number;
|
||||
/** The amount of points returned to draw the distribution */
|
||||
outputXYPoints?: number;
|
||||
kernelWidth?: number;
|
||||
pointDistLength?: number;
|
||||
environment?: environment;
|
||||
/** If the result is a function, where the function starts */
|
||||
diagramStart?: number;
|
||||
/** If the result is a function, where the function ends */
|
||||
|
@ -43,60 +39,57 @@ export interface SquiggleEditorProps {
|
|||
/** Whether to show detail about types of the returns, default false */
|
||||
showTypes?: boolean;
|
||||
/** Whether to give users access to graph controls */
|
||||
showControls: boolean;
|
||||
showControls?: boolean;
|
||||
/** Whether to show a summary table */
|
||||
showSummary?: boolean;
|
||||
}
|
||||
|
||||
const Input = styled.div`
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.3em 0.3em;
|
||||
margin-bottom: 1em;
|
||||
`;
|
||||
|
||||
export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
|
||||
initialSquiggleString = "",
|
||||
width,
|
||||
sampleCount,
|
||||
outputXYPoints,
|
||||
kernelWidth,
|
||||
pointDistLength,
|
||||
diagramStart,
|
||||
diagramStop,
|
||||
diagramCount,
|
||||
environment,
|
||||
diagramStart = 0,
|
||||
diagramStop = 10,
|
||||
diagramCount = 20,
|
||||
onChange,
|
||||
bindings = defaultBindings,
|
||||
jsImports = defaultImports,
|
||||
showTypes = false,
|
||||
showControls = false,
|
||||
showSummary = false,
|
||||
}: SquiggleEditorProps) => {
|
||||
let [expression, setExpression] = React.useState(initialSquiggleString);
|
||||
const [expression, setExpression] = React.useState(initialSquiggleString);
|
||||
const chartSettings = {
|
||||
start: diagramStart,
|
||||
stop: diagramStop,
|
||||
count: diagramCount,
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Input>
|
||||
<CodeEditor
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
oneLine={true}
|
||||
showGutter={false}
|
||||
height={20}
|
||||
<SquiggleContainer>
|
||||
<div>
|
||||
<div className="border border-grey-200 p-2 m-4">
|
||||
<CodeEditor
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
oneLine={true}
|
||||
showGutter={false}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
<SquiggleChart
|
||||
width={width}
|
||||
environment={environment}
|
||||
squiggleString={expression}
|
||||
chartSettings={chartSettings}
|
||||
onChange={onChange}
|
||||
bindings={bindings}
|
||||
jsImports={jsImports}
|
||||
showTypes={showTypes}
|
||||
showControls={showControls}
|
||||
showSummary={showSummary}
|
||||
/>
|
||||
</Input>
|
||||
<SquiggleChart
|
||||
width={width}
|
||||
squiggleString={expression}
|
||||
sampleCount={sampleCount}
|
||||
outputXYPoints={outputXYPoints}
|
||||
kernelWidth={kernelWidth}
|
||||
pointDistLength={pointDistLength}
|
||||
diagramStart={diagramStart}
|
||||
diagramStop={diagramStop}
|
||||
diagramCount={diagramCount}
|
||||
onChange={onChange}
|
||||
bindings={bindings}
|
||||
jsImports={jsImports}
|
||||
showTypes={showTypes}
|
||||
showControls={showControls}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SquiggleContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -136,11 +129,7 @@ export interface SquigglePartialProps {
|
|||
/** The input string for squiggle */
|
||||
initialSquiggleString?: string;
|
||||
/** If the output requires monte carlo sampling, the amount of samples */
|
||||
sampleCount?: number;
|
||||
/** The amount of points returned to draw the distribution */
|
||||
outputXYPoints?: number;
|
||||
kernelWidth?: number;
|
||||
pointDistLength?: number;
|
||||
environment?: environment;
|
||||
/** If the result is a function, where the function starts */
|
||||
diagramStart?: number;
|
||||
/** If the result is a function, where the function ends */
|
||||
|
@ -161,25 +150,20 @@ export let SquigglePartial: React.FC<SquigglePartialProps> = ({
|
|||
initialSquiggleString = "",
|
||||
onChange,
|
||||
bindings = defaultBindings,
|
||||
sampleCount = 1000,
|
||||
outputXYPoints = 1000,
|
||||
environment,
|
||||
jsImports = defaultImports,
|
||||
}: SquigglePartialProps) => {
|
||||
let samplingInputs: samplingParams = {
|
||||
sampleCount: sampleCount,
|
||||
xyPointLength: outputXYPoints,
|
||||
};
|
||||
let [expression, setExpression] = React.useState(initialSquiggleString);
|
||||
let [error, setError] = React.useState<string | null>(null);
|
||||
const [expression, setExpression] = React.useState(initialSquiggleString);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
let runSquiggleAndUpdateBindings = () => {
|
||||
let squiggleResult = runPartial(
|
||||
const runSquiggleAndUpdateBindings = () => {
|
||||
const squiggleResult = runPartial(
|
||||
expression,
|
||||
bindings,
|
||||
samplingInputs,
|
||||
environment,
|
||||
jsImports
|
||||
);
|
||||
if (squiggleResult.tag == "Ok") {
|
||||
if (squiggleResult.tag === "Ok") {
|
||||
if (onChange) onChange(squiggleResult.value);
|
||||
setError(null);
|
||||
} else {
|
||||
|
@ -190,18 +174,22 @@ export let SquigglePartial: React.FC<SquigglePartialProps> = ({
|
|||
React.useEffect(runSquiggleAndUpdateBindings, [expression]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input>
|
||||
<CodeEditor
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
oneLine={true}
|
||||
showGutter={false}
|
||||
height={20}
|
||||
/>
|
||||
</Input>
|
||||
{error !== null ? <ErrorBox heading="Error">{error}</ErrorBox> : <></>}
|
||||
</div>
|
||||
<SquiggleContainer>
|
||||
<div>
|
||||
<div className="border border-grey-200 p-2 m-4">
|
||||
<CodeEditor
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
oneLine={true}
|
||||
showGutter={false}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
{error !== null ? (
|
||||
<ErrorAlert heading="Error">{error}</ErrorAlert>
|
||||
) : null}
|
||||
</div>
|
||||
</SquiggleContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,129 +1,434 @@
|
|||
import _ from "lodash";
|
||||
import React, { FC, ReactElement, useState } from "react";
|
||||
import React, { FC, Fragment, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { SquiggleChart } from "./SquiggleChart";
|
||||
import CodeEditor from "./CodeEditor";
|
||||
import styled from "styled-components";
|
||||
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import {
|
||||
ChartSquareBarIcon,
|
||||
CodeIcon,
|
||||
CogIcon,
|
||||
CurrencyDollarIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface FieldFloatProps {
|
||||
label: string;
|
||||
className?: string;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
import { defaultBindings, environment } from "@quri/squiggle-lang";
|
||||
|
||||
import { SquiggleChart } from "./SquiggleChart";
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
import { JsonEditor } from "./JsonEditor";
|
||||
import { ErrorAlert, SuccessAlert } from "./Alert";
|
||||
import { SquiggleContainer } from "./SquiggleContainer";
|
||||
|
||||
interface PlaygroundProps {
|
||||
/** The initial squiggle string to put in the playground */
|
||||
initialSquiggleString?: string;
|
||||
/** How many pixels high is the playground */
|
||||
height?: number;
|
||||
/** Whether to show the types of outputs in the playground */
|
||||
showTypes?: boolean;
|
||||
/** Whether to show the log scale controls in the playground */
|
||||
showControls?: boolean;
|
||||
/** Whether to show the summary table in the playground */
|
||||
showSummary?: boolean;
|
||||
}
|
||||
|
||||
const Input = styled.input``;
|
||||
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(),
|
||||
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();
|
||||
|
||||
const FormItem = (props: { label: string; children: ReactElement }) => (
|
||||
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>
|
||||
<label>{props.label}</label>
|
||||
{props.children}
|
||||
<header className="text-lg leading-6 font-medium text-gray-900">
|
||||
{title}
|
||||
</header>
|
||||
<div className="mt-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function FieldFloat(Props: FieldFloatProps) {
|
||||
let [contents, setContents] = useState(Props.value + "");
|
||||
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 (
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
<label className="block">
|
||||
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
|
||||
<input
|
||||
type={type}
|
||||
{...register(name)}
|
||||
className="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"
|
||||
/>
|
||||
</FormItem>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
initialSquiggleString?: string;
|
||||
height?: number;
|
||||
showTypes?: boolean;
|
||||
showControls?: boolean;
|
||||
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="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>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props2 {
|
||||
height: number;
|
||||
}
|
||||
|
||||
const ShowBox = styled.div<Props2>`
|
||||
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``;
|
||||
|
||||
let SquigglePlayground: FC<Props> = ({
|
||||
const SquigglePlayground: FC<PlaygroundProps> = ({
|
||||
initialSquiggleString = "",
|
||||
height = 300,
|
||||
height = 500,
|
||||
showTypes = false,
|
||||
showControls = false,
|
||||
}: Props) => {
|
||||
let [squiggleString, setSquiggleString] = useState(initialSquiggleString);
|
||||
let [sampleCount, setSampleCount] = useState(1000);
|
||||
let [outputXYPoints, setOutputXYPoints] = useState(1000);
|
||||
let [pointDistLength, setPointDistLength] = useState(1000);
|
||||
let [diagramStart, setDiagramStart] = useState(0);
|
||||
let [diagramStop, setDiagramStop] = useState(10);
|
||||
let [diagramCount, setDiagramCount] = useState(20);
|
||||
return (
|
||||
<ShowBox height={height}>
|
||||
<Row>
|
||||
<Col>
|
||||
<CodeEditor
|
||||
value={squiggleString}
|
||||
onChange={setSquiggleString}
|
||||
oneLine={false}
|
||||
showGutter={true}
|
||||
height={height - 3}
|
||||
showSummary = false,
|
||||
}) => {
|
||||
const [squiggleString, setSquiggleString] = useState(initialSquiggleString);
|
||||
const [importString, setImportString] = useState("{}");
|
||||
const [imports, setImports] = useState({});
|
||||
const [importsAreValid, setImportsAreValid] = useState(true);
|
||||
const { register, control } = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
sampleCount: 1000,
|
||||
xyPointLength: 1000,
|
||||
chartHeight: 150,
|
||||
showTypes: showTypes,
|
||||
showControls: showControls,
|
||||
showSummary: showSummary,
|
||||
leftSizePercent: 50,
|
||||
showSettingsPage: false,
|
||||
diagramStart: 0,
|
||||
diagramStop: 10,
|
||||
diagramCount: 20,
|
||||
},
|
||||
});
|
||||
const vars = useWatch({
|
||||
control,
|
||||
});
|
||||
const chartSettings = {
|
||||
start: Number(vars.diagramStart),
|
||||
stop: Number(vars.diagramStop),
|
||||
count: Number(vars.diagramCount),
|
||||
};
|
||||
const env: environment = {
|
||||
sampleCount: Number(vars.sampleCount),
|
||||
xyPointLength: Number(vars.xyPointLength),
|
||||
};
|
||||
const getChangeJson = (r: string) => {
|
||||
setImportString(r);
|
||||
try {
|
||||
setImports(JSON.parse(r));
|
||||
setImportsAreValid(true);
|
||||
} catch (e) {
|
||||
setImportsAreValid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const samplingSettings = (
|
||||
<div className="space-y-6 p-3 max-w-xl">
|
||||
<div>
|
||||
<InputItem
|
||||
name="sampleCount"
|
||||
type="number"
|
||||
label="Sample Count"
|
||||
register={register}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Text>
|
||||
How many samples to use for Monte Carlo simulations. This can
|
||||
occasionally be overridden by specific Squiggle programs.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<InputItem
|
||||
name="xyPointLength"
|
||||
type="number"
|
||||
register={register}
|
||||
label="Coordinate Count (For PointSet Shapes)"
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Text>
|
||||
When distributions are converted into PointSet shapes, we need to
|
||||
know how many coordinates to use.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const viewSettings = (
|
||||
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
|
||||
<HeadedSection title="General Display Settings">
|
||||
<div className="space-y-4">
|
||||
<InputItem
|
||||
name="chartHeight"
|
||||
type="number"
|
||||
register={register}
|
||||
label="Chart Height (in pixels)"
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Display maxHeight={height - 3}>
|
||||
<SquiggleChart
|
||||
squiggleString={squiggleString}
|
||||
sampleCount={sampleCount}
|
||||
outputXYPoints={outputXYPoints}
|
||||
diagramStart={diagramStart}
|
||||
diagramStop={diagramStop}
|
||||
diagramCount={diagramCount}
|
||||
pointDistLength={pointDistLength}
|
||||
height={150}
|
||||
showTypes={showTypes}
|
||||
showControls={showControls}
|
||||
<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"
|
||||
/>
|
||||
</Display>
|
||||
</Col>
|
||||
</Row>
|
||||
</ShowBox>
|
||||
<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}
|
||||
showGutter={true}
|
||||
height={150}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-1 pt-2">
|
||||
{importsAreValid ? (
|
||||
<SuccessAlert heading="Valid JSON" />
|
||||
) : (
|
||||
<ErrorAlert heading="Invalid JSON">
|
||||
You must use valid JSON in this editor.
|
||||
</ErrorAlert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HeadedSection>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<SquiggleContainer>
|
||||
<Tab.Group>
|
||||
<div className="pb-4">
|
||||
<Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
|
||||
<StyledTab name="Code" icon={CodeIcon} />
|
||||
<StyledTab name="Sampling Settings" icon={CogIcon} />
|
||||
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
|
||||
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
|
||||
</Tab.List>
|
||||
</div>
|
||||
<div className="flex" style={{ height }}>
|
||||
<div className="w-1/2">
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<div className="border border-slate-200">
|
||||
<CodeEditor
|
||||
value={squiggleString}
|
||||
onChange={setSquiggleString}
|
||||
oneLine={false}
|
||||
showGutter={true}
|
||||
height={height - 1}
|
||||
/>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>{samplingSettings}</Tab.Panel>
|
||||
<Tab.Panel>{viewSettings}</Tab.Panel>
|
||||
<Tab.Panel>{inputVariableSettings}</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</div>
|
||||
|
||||
<div className="w-1/2 p-2 pl-4">
|
||||
<div style={{ maxHeight: height }}>
|
||||
<SquiggleChart
|
||||
squiggleString={squiggleString}
|
||||
environment={env}
|
||||
chartSettings={chartSettings}
|
||||
height={vars.chartHeight}
|
||||
showTypes={vars.showTypes}
|
||||
showControls={vars.showControls}
|
||||
bindings={defaultBindings}
|
||||
jsImports={imports}
|
||||
showSummary={vars.showSummary}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Group>
|
||||
</SquiggleContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SquigglePlayground;
|
||||
export function renderSquigglePlaygroundToDom(props: Props) {
|
||||
let parent = document.createElement("div");
|
||||
export function renderSquigglePlaygroundToDom(props: PlaygroundProps) {
|
||||
const parent = document.createElement("div");
|
||||
ReactDOM.render(<SquigglePlayground {...props} />, parent);
|
||||
return parent;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@ export {
|
|||
renderSquiggleEditorToDom,
|
||||
renderSquigglePartialToDom,
|
||||
} from "./components/SquiggleEditor";
|
||||
import SquigglePlayground, {
|
||||
export {
|
||||
default as SquigglePlayground,
|
||||
renderSquigglePlaygroundToDom,
|
||||
} from "./components/SquigglePlayground";
|
||||
export { SquigglePlayground, renderSquigglePlaygroundToDom };
|
||||
export { SquiggleContainer } from "./components/SquiggleContainer";
|
||||
|
||||
export { mergeBindings } from "@quri/squiggle-lang";
|
||||
|
|
|
@ -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"}
|
|
@ -153,6 +153,34 @@ to allow large and small numbers being printed cleanly.
|
|||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Functions (Distribution Output)
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Function to Distribution"
|
||||
args={{
|
||||
squiggleString: "foo(t) = normal(t,2)*normal(5,3); foo",
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{Template.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Functions (Number Output)
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Function to Number"
|
||||
args={{
|
||||
squiggleString: "foo(t) = t^2; foo",
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{Template.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Records
|
||||
|
||||
<Canvas>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import SquigglePlayground from "../components/SquigglePlayground";
|
||||
import { Canvas, Meta, Story, Props } from "@storybook/addon-docs";
|
||||
import styled from "styled-components";
|
||||
|
||||
<Meta title="Squiggle/SquigglePlayground" component={SquigglePlayground} />
|
||||
|
||||
|
@ -16,7 +15,7 @@ including sampling settings, in squiggle.
|
|||
name="Normal"
|
||||
args={{
|
||||
initialSquiggleString: "normal(5,2)",
|
||||
height: 500,
|
||||
height: 800,
|
||||
}}
|
||||
>
|
||||
{Template.bind({})}
|
||||
|
|
510
packages/components/src/styles/base.css
Normal file
510
packages/components/src/styles/base.css
Normal file
|
@ -0,0 +1,510 @@
|
|||
.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;
|
||||
}
|
||||
|
||||
/* these styles were generated by tailwindcss-forms */
|
||||
[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
|
||||
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;
|
||||
}
|
||||
[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select: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;
|
||||
}
|
||||
input::placeholder,textarea::placeholder {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1.5em;
|
||||
}
|
||||
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
[multiple] {
|
||||
background-image: initial;
|
||||
background-position: initial;
|
||||
background-repeat: unset;
|
||||
background-size: initial;
|
||||
padding-right: 0.75rem;
|
||||
-webkit-print-color-adjust: unset;
|
||||
print-color-adjust: unset;
|
||||
}
|
||||
[type='checkbox'],[type='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;
|
||||
}
|
||||
[type='checkbox'] {
|
||||
border-radius: 0px;
|
||||
}
|
||||
[type='radio'] {
|
||||
border-radius: 100%;
|
||||
}
|
||||
[type='checkbox']:focus,[type='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);
|
||||
}
|
||||
[type='checkbox']:checked,[type='radio']:checked {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
[type='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");
|
||||
}
|
||||
[type='radio']:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
|
||||
}
|
||||
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
}
|
||||
[type='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;
|
||||
}
|
||||
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
}
|
||||
[type='file'] {
|
||||
background: unset;
|
||||
border-color: inherit;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: unset;
|
||||
line-height: inherit;
|
||||
}
|
||||
[type='file']:focus {
|
||||
outline: 1px solid ButtonText;
|
||||
outline: 1px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
}
|
8
packages/components/src/styles/main.css
Normal file
8
packages/components/src/styles/main.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
@import "./base.css";
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* necessary hack because scoped preflight in ./base.css has higher specificity */
|
||||
.ace_cursor {
|
||||
border-left: 2px solid !important;
|
||||
}
|
|
@ -23,7 +23,7 @@
|
|||
"tickOpacity": 0.0,
|
||||
"domainColor": "#fff",
|
||||
"domainOpacity": 0.0,
|
||||
"format": "~g",
|
||||
"format": ".9~s",
|
||||
"tickCount": 10
|
||||
}
|
||||
],
|
||||
|
@ -33,6 +33,7 @@
|
|||
"from": {
|
||||
"data": "con"
|
||||
},
|
||||
"interpolate": "linear",
|
||||
"encode": {
|
||||
"update": {
|
||||
"x": {
|
||||
|
@ -48,10 +49,10 @@
|
|||
"value": 0
|
||||
},
|
||||
"fill": {
|
||||
"signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: '#4C78A8'}] }"
|
||||
"value": "#739ECC"
|
||||
},
|
||||
"interpolate": {
|
||||
"value": "monotone"
|
||||
"value": "linear"
|
||||
},
|
||||
"fillOpacity": {
|
||||
"value": 1
|
||||
|
@ -82,6 +83,9 @@
|
|||
"y2": {
|
||||
"scale": "yscale",
|
||||
"value": 0
|
||||
},
|
||||
"fill": {
|
||||
"value": "#2f65a7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
91
packages/components/src/vega-specs/spec-line-chart.json
Normal file
91
packages/components/src/vega-specs/spec-line-chart.json
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -75,6 +75,7 @@
|
|||
"name": "xscale",
|
||||
"type": "linear",
|
||||
"nice": true,
|
||||
"zero": false,
|
||||
"domain": {
|
||||
"data": "facet",
|
||||
"field": "x"
|
||||
|
@ -86,10 +87,10 @@
|
|||
"type": "linear",
|
||||
"range": "height",
|
||||
"nice": true,
|
||||
"zero": true,
|
||||
"zero": false,
|
||||
"domain": {
|
||||
"data": "facet",
|
||||
"field": "p99"
|
||||
"fields": ["p1", "p99"]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -113,12 +114,14 @@
|
|||
"tickOpacity": 0.0,
|
||||
"domainColor": "#727d93",
|
||||
"domainOpacity": 0.1,
|
||||
"format": ".9~s",
|
||||
"tickCount": 5
|
||||
},
|
||||
{
|
||||
"orient": "left",
|
||||
"scale": "yscale",
|
||||
"grid": false,
|
||||
"format": ".9~s",
|
||||
"labelColor": "#727d93",
|
||||
"tickColor": "#fff",
|
||||
"tickOpacity": 0.0,
|
||||
|
|
8
packages/components/tailwind.config.js
Normal file
8
packages/components/tailwind.config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
content: ["./src/**/*.{html,tsx,ts,js,jsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
important: ".squiggle",
|
||||
plugins: [require("@tailwindcss/forms")],
|
||||
};
|
|
@ -20,7 +20,8 @@
|
|||
},
|
||||
"files": [
|
||||
"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",
|
||||
"include": ["src/**/*", "src/*"],
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
const path = require("path");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
module.exports = {
|
||||
mode: "production",
|
||||
devtool: "source-map",
|
||||
profile: true,
|
||||
entry: "./src/index.ts",
|
||||
entry: ["./src/index.ts", "./src/styles/main.css"],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
options: { projectReferences: true, transpileOnly: true },
|
||||
options: { projectReferences: true },
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ["style-loader", "css-loader"],
|
||||
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [new MiniCssExtractPlugin()],
|
||||
resolve: {
|
||||
extensions: [".js", ".tsx", ".ts"],
|
||||
alias: {
|
||||
|
|
1
packages/squiggle-lang/.gitignore
vendored
1
packages/squiggle-lang/.gitignore
vendored
|
@ -21,3 +21,4 @@ dist
|
|||
_coverage
|
||||
coverage
|
||||
.nyc_output/
|
||||
src/rescript/Reducer/Reducer_Peggy/Reducer_Peggy_GeneratedParser.js
|
||||
|
|
|
@ -3,5 +3,6 @@ lib
|
|||
*.bs.js
|
||||
*.gen.tsx
|
||||
.nyc_output/
|
||||
coverage/
|
||||
.cache/
|
||||
_coverage/
|
||||
.cache/
|
||||
Reducer_Peggy_GeneratedParser.js
|
||||
|
|
|
@ -13,6 +13,10 @@ For instance, in a javascript project, you can
|
|||
yarn add @quri/squiggle-lang
|
||||
```
|
||||
|
||||
The `@quri/squiggle-lang` package exports a single function, `run`, which given
|
||||
a string of Squiggle code, will execute the code and return any exports and the
|
||||
environment created from the squiggle code.
|
||||
|
||||
```js
|
||||
import { run } from "@quri/squiggle-lang";
|
||||
run(
|
||||
|
@ -22,6 +26,16 @@ run(
|
|||
|
||||
**However, for most use cases you'll prefer to use our [library of react components](https://www.npmjs.com/package/@quri/squiggle-components)**, and let your app transitively depend on `@quri/squiggle-lang`.
|
||||
|
||||
`run` has two optional arguments. The first optional argument allows you to set
|
||||
sampling settings for Squiggle when representing distributions. The second optional
|
||||
argument allows you to pass an environment previously created by another `run`
|
||||
call. Passing this environment will mean that all previously declared variables
|
||||
in the previous environment will be made available.
|
||||
|
||||
The return type of `run` is a bit complicated, and comes from auto generated `js`
|
||||
code that comes from rescript. We highly recommend using typescript when using
|
||||
this library to help navigate the return type.
|
||||
|
||||
# Build for development
|
||||
|
||||
We assume that you ran `yarn` at the monorepo level.
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
open Jest
|
||||
open Expect
|
||||
open TestHelpers
|
||||
open FastCheck
|
||||
open Arbitrary
|
||||
open Property.Sync
|
||||
|
||||
describe("dotSubtract", () => {
|
||||
test("mean of normal minus exponential (unit)", () => {
|
||||
let mean = 0.0
|
||||
let rate = 10.0
|
||||
exception MeanFailed
|
||||
let dotDifference = DistributionOperation.Constructors.pointwiseSubtract(
|
||||
~env,
|
||||
mkNormal(mean, 1.0),
|
||||
mkExponential(rate),
|
||||
)
|
||||
let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), dotDifference)
|
||||
let meanAnalytical =
|
||||
mean -.
|
||||
SymbolicDist.Exponential.mean({rate: rate})->E.R2.toExn(
|
||||
"On trusted input this should never happen",
|
||||
)
|
||||
switch meanResult {
|
||||
| Ok(meanValue) => meanValue->expect->toBeCloseTo(meanAnalytical)
|
||||
| Error(_) => raise(MeanFailed)
|
||||
}
|
||||
})
|
||||
/*
|
||||
It seems like this test should work, and it's plausible that
|
||||
there's some bug in `pointwiseSubtract`
|
||||
*/
|
||||
Skip.test("mean of normal minus exponential (property)", () => {
|
||||
assert_(
|
||||
property2(float_(), floatRange(1e-5, 1e5), (mean, rate) => {
|
||||
// We limit ourselves to stdev=1 so that the integral is trivial
|
||||
let dotDifference = DistributionOperation.Constructors.pointwiseSubtract(
|
||||
~env,
|
||||
mkNormal(mean, 1.0),
|
||||
mkExponential(rate),
|
||||
)
|
||||
let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), dotDifference)
|
||||
// according to algebra or random variables,
|
||||
let meanAnalytical =
|
||||
mean -.
|
||||
SymbolicDist.Exponential.mean({rate: rate})->E.R2.toExn(
|
||||
"On trusted input this should never happen",
|
||||
)
|
||||
switch meanResult {
|
||||
| Ok(meanValue) => abs_float(meanValue -. meanAnalytical) /. abs_float(meanValue) < 1e-2 // 1% relative error
|
||||
| Error(err) => err === DistributionTypes.OperationError(DivisionByZeroError)
|
||||
}
|
||||
}),
|
||||
)
|
||||
pass
|
||||
})
|
||||
})
|
|
@ -11,4 +11,15 @@ let triangularDist: DistributionTypes.genericDist = Symbolic(
|
|||
)
|
||||
let exponentialDist: DistributionTypes.genericDist = Symbolic(#Exponential({rate: 2.0}))
|
||||
let uniformDist: DistributionTypes.genericDist = Symbolic(#Uniform({low: 9.0, high: 10.0}))
|
||||
let uniformDist2: DistributionTypes.genericDist = Symbolic(#Uniform({low: 8.0, high: 11.0}))
|
||||
let floatDist: DistributionTypes.genericDist = Symbolic(#Float(1e1))
|
||||
|
||||
exception KlFailed
|
||||
exception MixtureFailed
|
||||
let float1 = 1.0
|
||||
let float2 = 2.0
|
||||
let float3 = 3.0
|
||||
let {mkDelta} = module(TestHelpers)
|
||||
let point1 = mkDelta(float1)
|
||||
let point2 = mkDelta(float2)
|
||||
let point3 = mkDelta(float3)
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
open Jest
|
||||
open Expect
|
||||
open TestHelpers
|
||||
open GenericDist_Fixtures
|
||||
|
||||
// integral from low to high of 1 / (high - low) log(normal(mean, stdev)(x) / (1 / (high - low))) dx
|
||||
let klNormalUniform = (mean, stdev, low, high): float =>
|
||||
-.Js.Math.log((high -. low) /. Js.Math.sqrt(2.0 *. MagicNumbers.Math.pi *. stdev ** 2.0)) +.
|
||||
1.0 /.
|
||||
stdev ** 2.0 *.
|
||||
(mean ** 2.0 -. (high +. low) *. mean +. (low ** 2.0 +. high *. low +. high ** 2.0) /. 3.0)
|
||||
|
||||
describe("klDivergence: continuous -> continuous -> float", () => {
|
||||
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
|
||||
|
||||
let testUniform = (lowAnswer, highAnswer, lowPrediction, highPrediction) => {
|
||||
test("of two uniforms is equal to the analytic expression", () => {
|
||||
let answer =
|
||||
uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
|
||||
let prediction =
|
||||
uniformMakeR(
|
||||
lowPrediction,
|
||||
highPrediction,
|
||||
)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
|
||||
// integral along the support of the answer of answer.pdf(x) times log of prediction.pdf(x) divided by answer.pdf(x) dx
|
||||
let analyticalKl = Js.Math.log((highPrediction -. lowPrediction) /. (highAnswer -. lowAnswer))
|
||||
let kl = E.R.liftJoin2(klDivergence, prediction, answer)
|
||||
switch kl {
|
||||
| Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=7)
|
||||
| Error(err) => {
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// The pair on the right (the answer) can be wider than the pair on the left (the prediction), but not the other way around.
|
||||
testUniform(0.0, 1.0, -1.0, 2.0)
|
||||
testUniform(0.0, 1.0, 0.0, 2.0) // equal left endpoints
|
||||
testUniform(0.0, 1.0, -1.0, 1.0) // equal rightendpoints
|
||||
testUniform(0.0, 1e1, 0.0, 1e1) // equal (klDivergence = 0)
|
||||
// testUniform(-1.0, 1.0, 0.0, 2.0)
|
||||
|
||||
test("of two normals is equal to the formula", () => {
|
||||
// This test case comes via Nuño https://github.com/quantified-uncertainty/squiggle/issues/433
|
||||
let mean1 = 4.0
|
||||
let mean2 = 1.0
|
||||
let stdev1 = 4.0
|
||||
let stdev2 = 1.0
|
||||
|
||||
let prediction =
|
||||
normalMakeR(mean1, stdev1)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
|
||||
let answer = normalMakeR(mean2, stdev2)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
|
||||
// https://stats.stackexchange.com/questions/7440/kl-divergence-between-two-univariate-gaussians
|
||||
let analyticalKl =
|
||||
Js.Math.log(stdev1 /. stdev2) +.
|
||||
(stdev2 ** 2.0 +. (mean2 -. mean1) ** 2.0) /. (2.0 *. stdev1 ** 2.0) -. 0.5
|
||||
let kl = E.R.liftJoin2(klDivergence, prediction, answer)
|
||||
|
||||
switch kl {
|
||||
| Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=3)
|
||||
| Error(err) => {
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("of a normal and a uniform is equal to the formula", () => {
|
||||
let prediction = normalDist10
|
||||
let answer = uniformDist
|
||||
let kl = klDivergence(prediction, answer)
|
||||
let analyticalKl = klNormalUniform(10.0, 2.0, 9.0, 10.0)
|
||||
switch kl {
|
||||
| Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=1)
|
||||
| Error(err) => {
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("klDivergence: discrete -> discrete -> float", () => {
|
||||
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
|
||||
let mixture = a => DistributionTypes.DistributionOperation.Mixture(a)
|
||||
let a' = [(point1, 1e0), (point2, 1e0)]->mixture->run
|
||||
let b' = [(point1, 1e0), (point2, 1e0), (point3, 1e0)]->mixture->run
|
||||
let (a, b) = switch (a', b') {
|
||||
| (Dist(a''), Dist(b'')) => (a'', b'')
|
||||
| _ => raise(MixtureFailed)
|
||||
}
|
||||
test("agrees with analytical answer when finite", () => {
|
||||
let prediction = b
|
||||
let answer = a
|
||||
let kl = klDivergence(prediction, answer)
|
||||
// Sigma_{i \in 1..2} 0.5 * log(0.5 / 0.33333)
|
||||
let analyticalKl = Js.Math.log(3.0 /. 2.0)
|
||||
switch kl {
|
||||
| Ok(kl') => kl'->expect->toBeSoCloseTo(analyticalKl, ~digits=7)
|
||||
| Error(err) =>
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
})
|
||||
test("returns infinity when infinite", () => {
|
||||
let prediction = a
|
||||
let answer = b
|
||||
let kl = klDivergence(prediction, answer)
|
||||
switch kl {
|
||||
| Ok(kl') => kl'->expect->toEqual(infinity)
|
||||
| Error(err) =>
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("klDivergence: mixed -> mixed -> float", () => {
|
||||
let klDivergence = DistributionOperation.Constructors.klDivergence(~env)
|
||||
let mixture' = a => DistributionTypes.DistributionOperation.Mixture(a)
|
||||
let mixture = a => {
|
||||
let dist' = a->mixture'->run
|
||||
switch dist' {
|
||||
| Dist(dist) => dist
|
||||
| _ => raise(MixtureFailed)
|
||||
}
|
||||
}
|
||||
let a = [(point1, 1.0), (uniformDist, 1.0)]->mixture
|
||||
let b = [(point1, 1.0), (floatDist, 1.0), (normalDist10, 1.0)]->mixture
|
||||
let c = [(point1, 1.0), (point2, 1.0), (point3, 1.0), (uniformDist, 1.0)]->mixture
|
||||
let d =
|
||||
[(point1, 1.0), (point2, 1.0), (point3, 1.0), (floatDist, 1.0), (uniformDist2, 1.0)]->mixture
|
||||
|
||||
test("finite klDivergence produces correct answer", () => {
|
||||
let prediction = b
|
||||
let answer = a
|
||||
let kl = klDivergence(prediction, answer)
|
||||
// high = 10; low = 9; mean = 10; stdev = 2
|
||||
let analyticalKlContinuousPart = klNormalUniform(10.0, 2.0, 9.0, 10.0) /. 2.0
|
||||
let analyticalKlDiscretePart = 1.0 /. 2.0 *. Js.Math.log(2.0 /. 1.0)
|
||||
switch kl {
|
||||
| Ok(kl') =>
|
||||
kl'->expect->toBeSoCloseTo(analyticalKlContinuousPart +. analyticalKlDiscretePart, ~digits=1)
|
||||
| Error(err) =>
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
})
|
||||
test("returns infinity when infinite", () => {
|
||||
let prediction = a
|
||||
let answer = b
|
||||
let kl = klDivergence(prediction, answer)
|
||||
switch kl {
|
||||
| Ok(kl') => kl'->expect->toEqual(infinity)
|
||||
| Error(err) =>
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
})
|
||||
test("finite klDivergence produces correct answer", () => {
|
||||
let prediction = d
|
||||
let answer = c
|
||||
let kl = klDivergence(prediction, answer)
|
||||
let analyticalKlContinuousPart = Js.Math.log((11.0 -. 8.0) /. (10.0 -. 9.0)) /. 4.0 // 4 = length of c' array
|
||||
let analyticalKlDiscretePart = 3.0 /. 4.0 *. Js.Math.log(4.0 /. 3.0)
|
||||
switch kl {
|
||||
| Ok(kl') =>
|
||||
kl'->expect->toBeSoCloseTo(analyticalKlContinuousPart +. analyticalKlDiscretePart, ~digits=1)
|
||||
| Error(err) =>
|
||||
Js.Console.log(DistributionTypes.Error.toString(err))
|
||||
raise(KlFailed)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("combineAlongSupportOfSecondArgument0", () => {
|
||||
// This tests the version of the function that we're NOT using. Haven't deleted the test in case we use the code later.
|
||||
test("test on two uniforms", _ => {
|
||||
let combineAlongSupportOfSecondArgument = XYShape.PointwiseCombination.combineAlongSupportOfSecondArgument0
|
||||
let lowAnswer = 0.0
|
||||
let highAnswer = 1.0
|
||||
let lowPrediction = 0.0
|
||||
let highPrediction = 2.0
|
||||
|
||||
let answer =
|
||||
uniformMakeR(lowAnswer, highAnswer)->E.R2.errMap(s => DistributionTypes.ArgumentError(s))
|
||||
let prediction =
|
||||
uniformMakeR(lowPrediction, highPrediction)->E.R2.errMap(s => DistributionTypes.ArgumentError(
|
||||
s,
|
||||
))
|
||||
let answerWrapped = E.R.fmap(a => run(FromDist(ToDist(ToPointSet), a)), answer)
|
||||
let predictionWrapped = E.R.fmap(a => run(FromDist(ToDist(ToPointSet), a)), prediction)
|
||||
|
||||
let interpolator = XYShape.XtoY.continuousInterpolator(#Stepwise, #UseZero)
|
||||
let integrand = PointSetDist_Scoring.KLDivergence.integrand
|
||||
|
||||
let result = switch (answerWrapped, predictionWrapped) {
|
||||
| (Ok(Dist(PointSet(Continuous(a)))), Ok(Dist(PointSet(Continuous(b))))) =>
|
||||
Some(combineAlongSupportOfSecondArgument(integrand, interpolator, a.xyShape, b.xyShape))
|
||||
| _ => None
|
||||
}
|
||||
result
|
||||
->expect
|
||||
->toEqual(
|
||||
Some(
|
||||
Ok({
|
||||
xs: [
|
||||
0.0,
|
||||
MagicNumbers.Epsilon.ten,
|
||||
2.0 *. MagicNumbers.Epsilon.ten,
|
||||
1.0 -. MagicNumbers.Epsilon.ten,
|
||||
1.0,
|
||||
1.0 +. MagicNumbers.Epsilon.ten,
|
||||
],
|
||||
ys: [
|
||||
-0.34657359027997264,
|
||||
-0.34657359027997264,
|
||||
-0.34657359027997264,
|
||||
-0.34657359027997264,
|
||||
-0.34657359027997264,
|
||||
infinity,
|
||||
],
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
open Jest
|
||||
open Expect
|
||||
open TestHelpers
|
||||
|
||||
describe("Scale logarithm", () => {
|
||||
/* These tests may not be important, because scalelog isn't normalized
|
||||
The first one may be failing for a number of reasons.
|
||||
*/
|
||||
Skip.test("mean of the base e scalar logarithm of an exponential(10)", () => {
|
||||
let rate = 10.0
|
||||
let scalelog = DistributionOperation.Constructors.scaleLogarithm(
|
||||
~env,
|
||||
mkExponential(rate),
|
||||
MagicNumbers.Math.e,
|
||||
)
|
||||
|
||||
let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), scalelog)
|
||||
// expected value of log of exponential distribution.
|
||||
let meanAnalytical = Js.Math.log(rate) +. 1.0
|
||||
switch meanResult {
|
||||
| Ok(meanValue) => meanValue->expect->toBeCloseTo(meanAnalytical)
|
||||
| Error(err) => err->expect->toBe(DistributionTypes.OperationError(DivisionByZeroError))
|
||||
}
|
||||
})
|
||||
let low = 10.0
|
||||
let high = 100.0
|
||||
let scalelog = DistributionOperation.Constructors.scaleLogarithm(~env, mkUniform(low, high), 2.0)
|
||||
|
||||
test("mean of the base 2 scalar logarithm of a uniform(10, 100)", () => {
|
||||
//For uniform pdf `_ => 1 / (b - a)`, the expected value of log of uniform is `integral from a to b of x * log(1 / (b -a)) dx`
|
||||
let meanResult = E.R2.bind(DistributionOperation.Constructors.mean(~env), scalelog)
|
||||
let meanAnalytical = -.Js.Math.log2(high -. low) /. 2.0 *. (high ** 2.0 -. low ** 2.0) // -. Js.Math.log2(high -. low)
|
||||
switch meanResult {
|
||||
| Ok(meanValue) => meanValue->expect->toBeCloseTo(meanAnalytical)
|
||||
| Error(err) => err->expect->toEqual(DistributionTypes.OperationError(NegativeInfinityError))
|
||||
}
|
||||
})
|
||||
})
|
|
@ -16,33 +16,41 @@ testMacro([], exampleExpression, "Ok(1)")
|
|||
|
||||
describe("bindStatement", () => {
|
||||
// A statement is bound by the bindings created by the previous statement
|
||||
testMacro([], eBindStatement(eBindings([]), exampleStatementY), "Ok((:$setBindings {} :y 1))")
|
||||
testMacro(
|
||||
[],
|
||||
eBindStatement(eBindings([]), exampleStatementY),
|
||||
"Ok((:$_setBindings_$ {} :y 1) context: {})",
|
||||
)
|
||||
// Then it answers the bindings for the next statement when reduced
|
||||
testMacroEval([], eBindStatement(eBindings([]), exampleStatementY), "Ok({y: 1})")
|
||||
// Now let's feed a binding to see what happens
|
||||
testMacro(
|
||||
[],
|
||||
eBindStatement(eBindings([("x", EvNumber(2.))]), exampleStatementX),
|
||||
"Ok((:$setBindings {x: 2} :y 2))",
|
||||
"Ok((:$_setBindings_$ {x: 2} :y 2) context: {x: 2})",
|
||||
)
|
||||
// An expression does not return a binding, thus error
|
||||
testMacro([], eBindStatement(eBindings([]), exampleExpression), "Error(Assignment expected)")
|
||||
testMacro([], eBindStatement(eBindings([]), exampleExpression), "Assignment expected")
|
||||
// When bindings from previous statement are missing the context is injected. This must be the first statement of a block
|
||||
testMacro(
|
||||
[("z", EvNumber(99.))],
|
||||
eBindStatementDefault(exampleStatementY),
|
||||
"Ok((:$setBindings {z: 99} :y 1))",
|
||||
"Ok((:$_setBindings_$ {z: 99} :y 1) context: {z: 99})",
|
||||
)
|
||||
})
|
||||
|
||||
describe("bindExpression", () => {
|
||||
// x is simply bound in the expression
|
||||
testMacro([], eBindExpression(eBindings([("x", EvNumber(2.))]), eSymbol("x")), "Ok(2)")
|
||||
testMacro(
|
||||
[],
|
||||
eBindExpression(eBindings([("x", EvNumber(2.))]), eSymbol("x")),
|
||||
"Ok(2 context: {x: 2})",
|
||||
)
|
||||
// When an let statement is the end expression then bindings are returned
|
||||
testMacro(
|
||||
[],
|
||||
eBindExpression(eBindings([("x", EvNumber(2.))]), exampleStatementY),
|
||||
"Ok((:$exportBindings (:$setBindings {x: 2} :y 1)))",
|
||||
"Ok((:$_exportBindings_$ (:$_setBindings_$ {x: 2} :y 1)) context: {x: 2})",
|
||||
)
|
||||
// Now let's reduce that expression
|
||||
testMacroEval(
|
||||
|
@ -60,37 +68,33 @@ describe("bindExpression", () => {
|
|||
|
||||
describe("block", () => {
|
||||
// Block with a single expression
|
||||
testMacro([], eBlock(list{exampleExpression}), "Ok((:$$bindExpression 1))")
|
||||
testMacro([], eBlock(list{exampleExpression}), "Ok((:$$_bindExpression_$$ 1))")
|
||||
testMacroEval([], eBlock(list{exampleExpression}), "Ok(1)")
|
||||
// Block with a single statement
|
||||
testMacro([], eBlock(list{exampleStatementY}), "Ok((:$$bindExpression (:$let :y 1)))")
|
||||
testMacro([], eBlock(list{exampleStatementY}), "Ok((:$$_bindExpression_$$ (:$_let_$ :y 1)))")
|
||||
testMacroEval([], eBlock(list{exampleStatementY}), "Ok({y: 1})")
|
||||
// Block with a statement and an expression
|
||||
testMacro(
|
||||
[],
|
||||
eBlock(list{exampleStatementY, exampleExpressionY}),
|
||||
"Ok((:$$bindExpression (:$$bindStatement (:$let :y 1)) :y))",
|
||||
"Ok((:$$_bindExpression_$$ (:$$_bindStatement_$$ (:$_let_$ :y 1)) :y))",
|
||||
)
|
||||
testMacroEval([], eBlock(list{exampleStatementY, exampleExpressionY}), "Ok(1)")
|
||||
// Block with a statement and another statement
|
||||
testMacro(
|
||||
[],
|
||||
eBlock(list{exampleStatementY, exampleStatementZ}),
|
||||
"Ok((:$$bindExpression (:$$bindStatement (:$let :y 1)) (:$let :z :y)))",
|
||||
"Ok((:$$_bindExpression_$$ (:$$_bindStatement_$$ (:$_let_$ :y 1)) (:$_let_$ :z :y)))",
|
||||
)
|
||||
testMacroEval([], eBlock(list{exampleStatementY, exampleStatementZ}), "Ok({y: 1,z: 1})")
|
||||
// Block inside a block
|
||||
testMacro(
|
||||
[],
|
||||
eBlock(list{eBlock(list{exampleExpression})}),
|
||||
"Ok((:$$bindExpression (:$$block 1)))",
|
||||
)
|
||||
testMacro([], eBlock(list{eBlock(list{exampleExpression})}), "Ok((:$$_bindExpression_$$ {1}))")
|
||||
testMacroEval([], eBlock(list{eBlock(list{exampleExpression})}), "Ok(1)")
|
||||
// Block assigned to a variable
|
||||
testMacro(
|
||||
[],
|
||||
eBlock(list{eLetStatement("z", eBlock(list{eBlock(list{exampleExpressionY})}))}),
|
||||
"Ok((:$$bindExpression (:$let :z (:$$block (:$$block :y)))))",
|
||||
"Ok((:$$_bindExpression_$$ (:$_let_$ :z {{:y}})))",
|
||||
)
|
||||
testMacroEval(
|
||||
[],
|
||||
|
@ -99,7 +103,7 @@ describe("block", () => {
|
|||
)
|
||||
// Empty block
|
||||
testMacro([], eBlock(list{}), "Ok(:undefined block)") //TODO: should be an error
|
||||
// :$$block (:$$block (:$let :y (:add :x 1)) :y)"
|
||||
// :$$_block_$$ (:$$_block_$$ (:$_let_$ :y (:add :x 1)) :y)"
|
||||
testMacro(
|
||||
[],
|
||||
eBlock(list{
|
||||
|
@ -108,9 +112,9 @@ describe("block", () => {
|
|||
eSymbol("y"),
|
||||
}),
|
||||
}),
|
||||
"Ok((:$$bindExpression (:$$block (:$let :y (:add :x 1)) :y)))",
|
||||
"Ok((:$$_bindExpression_$$ {(:$_let_$ :y (:add :x 1)); :y}))",
|
||||
)
|
||||
MyOnly.testMacroEval(
|
||||
testMacroEval(
|
||||
[("x", EvNumber(1.))],
|
||||
eBlock(list{
|
||||
eBlock(list{
|
||||
|
@ -124,17 +128,17 @@ describe("block", () => {
|
|||
|
||||
describe("lambda", () => {
|
||||
// assign a lambda to a variable
|
||||
let lambdaExpression = eFunction("$$lambda", list{eArrayString(["y"]), exampleExpressionY})
|
||||
testMacro([], lambdaExpression, "Ok(lambda(y=>internal))")
|
||||
let lambdaExpression = eFunction("$$_lambda_$$", list{eArrayString(["y"]), exampleExpressionY})
|
||||
testMacro([], lambdaExpression, "Ok(lambda(y=>internal code))")
|
||||
// call a lambda
|
||||
let callLambdaExpression = list{lambdaExpression, eNumber(1.)}->ExpressionT.EList
|
||||
testMacro([], callLambdaExpression, "Ok(((:$$lambda [y] :y) 1))")
|
||||
testMacro([], callLambdaExpression, "Ok(((:$$_lambda_$$ [y] :y) 1))")
|
||||
testMacroEval([], callLambdaExpression, "Ok(1)")
|
||||
// Parameters shadow the outer scope
|
||||
testMacroEval([("y", EvNumber(666.))], callLambdaExpression, "Ok(1)")
|
||||
// When not shadowed by the parameters, the outer scope variables are available
|
||||
let lambdaExpression = eFunction(
|
||||
"$$lambda",
|
||||
"$$_lambda_$$",
|
||||
list{eArrayString(["z"]), eFunction("add", list{eSymbol("y"), eSymbol("z")})},
|
||||
)
|
||||
let callLambdaExpression = eList(list{lambdaExpression, eNumber(1.)})
|
||||
|
|
|
@ -17,11 +17,25 @@ describe("builtin", () => {
|
|||
testEval("1-1", "Ok(0)")
|
||||
testEval("2>1", "Ok(true)")
|
||||
testEval("concat('a','b')", "Ok('ab')")
|
||||
testEval(
|
||||
"addOne(t)=t+1; toInternalSampleArray(mapSamples(fromSamples([1,2,3,4,5,6]), addOne))",
|
||||
"Ok([2,3,4,5,6,7])",
|
||||
)
|
||||
})
|
||||
|
||||
describe("builtin exception", () => {
|
||||
//It's a pity that MathJs does not return error position
|
||||
test("MathJs Exception", () =>
|
||||
expectEvalToBe("testZadanga()", "Error(JS Exception: Error: Undefined function testZadanga)")
|
||||
expectEvalToBe("testZadanga(1)", "Error(JS Exception: Error: Undefined function testZadanga)")
|
||||
)
|
||||
})
|
||||
|
||||
describe("error reporting from collection functions", () => {
|
||||
testEval("arr=[1,2,3]; map(arr, {|x| x*2})", "Ok([2,4,6])")
|
||||
testEval(
|
||||
"arr = [normal(3,2)]; map(arr, zarathsuzaWasHere)",
|
||||
"Error(zarathsuzaWasHere is not defined)",
|
||||
)
|
||||
// FIXME: returns "Error(Function not found: map(Array,Symbol))"
|
||||
// Actually this error is correct but not informative
|
||||
})
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
module Parse = Reducer_MathJs.Parse
|
||||
module Result = Belt.Result
|
||||
|
||||
open Jest
|
||||
open Expect
|
||||
|
||||
let expectParseToBe = (expr, answer) =>
|
||||
Parse.parse(expr)->Result.flatMap(Parse.castNodeType)->Parse.toStringResult->expect->toBe(answer)
|
||||
|
||||
let testParse = (expr, answer) => test(expr, () => expectParseToBe(expr, answer))
|
||||
|
||||
let testDescriptionParse = (desc, expr, answer) => test(desc, () => 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("MathJs parse", () => {
|
||||
describe("literals operators parenthesis", () => {
|
||||
testParse("1", "1")
|
||||
testParse("'hello'", "'hello'")
|
||||
testParse("true", "true")
|
||||
testParse("1+2", "add(1, 2)")
|
||||
testParse("add(1,2)", "add(1, 2)")
|
||||
testParse("(1)", "(1)")
|
||||
testParse("(1+2)", "(add(1, 2))")
|
||||
})
|
||||
|
||||
describe("multi-line", () => {
|
||||
testParse("1; 2", "{1; 2}")
|
||||
})
|
||||
|
||||
describe("variables", () => {
|
||||
testParse("x = 1", "x = 1")
|
||||
testParse("x", "x")
|
||||
testParse("x = 1; x", "{x = 1; x}")
|
||||
})
|
||||
|
||||
describe("functions", () => {
|
||||
testParse("identity(x) = x", "identity = (x) => x")
|
||||
testParse("identity(x)", "identity(x)")
|
||||
})
|
||||
|
||||
describe("arrays", () => {
|
||||
testDescriptionParse("empty", "[]", "[]")
|
||||
testDescriptionParse("define", "[0, 1, 2]", "[0, 1, 2]")
|
||||
testDescriptionParse("define with strings", "['hello', 'world']", "['hello', 'world']")
|
||||
testParse("range(0, 4)", "range(0, 4)")
|
||||
testDescriptionParse("index", "([0,1,2])[1]", "([0, 1, 2])[1]")
|
||||
})
|
||||
|
||||
describe("records", () => {
|
||||
testDescriptionParse("define", "{a: 1, b: 2}", "{a: 1, b: 2}")
|
||||
testDescriptionParse("use", "record.property", "record['property']")
|
||||
})
|
||||
|
||||
describe("comments", () => {
|
||||
testDescriptionParse("define", "1 # This is a comment", "1")
|
||||
})
|
||||
|
||||
describe("ternary operator", () => {
|
||||
testParse("1 ? 2 : 3", "ternary(1, 2, 3)")
|
||||
testParse("1 ? 2 : 3 ? 4 : 5", "ternary(1, 2, ternary(3, 4, 5))")
|
||||
})
|
||||
})
|
|
@ -0,0 +1,357 @@
|
|||
open Jest
|
||||
open Reducer_Peggy_TestHelpers
|
||||
|
||||
describe("Peggy parse", () => {
|
||||
describe("float", () => {
|
||||
testParse("1.", "{1}")
|
||||
testParse("1.1", "{1.1}")
|
||||
testParse(".1", "{0.1}")
|
||||
testParse("0.1", "{0.1}")
|
||||
testParse("1e1", "{10}")
|
||||
testParse("1e-1", "{0.1}")
|
||||
testParse(".1e1", "{1}")
|
||||
testParse("0.1e1", "{1}")
|
||||
})
|
||||
|
||||
describe("literals operators parenthesis", () => {
|
||||
// Note that there is always an outer block. Otherwise, external bindings are ignrored at the first statement
|
||||
testParse("1", "{1}")
|
||||
testParse("'hello'", "{'hello'}")
|
||||
testParse("true", "{true}")
|
||||
testParse("1+2", "{(::add 1 2)}")
|
||||
testParse("add(1,2)", "{(::add 1 2)}")
|
||||
testParse("(1)", "{1}")
|
||||
testParse("(1+2)", "{(::add 1 2)}")
|
||||
})
|
||||
|
||||
describe("unary", () => {
|
||||
testParse("-1", "{(::unaryMinus 1)}")
|
||||
testParse("!true", "{(::not true)}")
|
||||
testParse("1 + -1", "{(::add 1 (::unaryMinus 1))}")
|
||||
testParse("-a[0]", "{(::unaryMinus (::$_atIndex_$ :a 0))}")
|
||||
testParse("!a[0]", "{(::not (::$_atIndex_$ :a 0))}")
|
||||
})
|
||||
|
||||
describe("multiplicative", () => {
|
||||
testParse("1 * 2", "{(::multiply 1 2)}")
|
||||
testParse("1 / 2", "{(::divide 1 2)}")
|
||||
testParse("1 * 2 * 3", "{(::multiply (::multiply 1 2) 3)}")
|
||||
testParse("1 * 2 / 3", "{(::divide (::multiply 1 2) 3)}")
|
||||
testParse("1 / 2 * 3", "{(::multiply (::divide 1 2) 3)}")
|
||||
testParse("1 / 2 / 3", "{(::divide (::divide 1 2) 3)}")
|
||||
testParse("1 * 2 + 3 * 4", "{(::add (::multiply 1 2) (::multiply 3 4))}")
|
||||
testParse("1 * 2 - 3 * 4", "{(::subtract (::multiply 1 2) (::multiply 3 4))}")
|
||||
testParse("1 * 2 .+ 3 * 4", "{(::dotAdd (::multiply 1 2) (::multiply 3 4))}")
|
||||
testParse("1 * 2 .- 3 * 4", "{(::dotSubtract (::multiply 1 2) (::multiply 3 4))}")
|
||||
testParse("1 * 2 + 3 .* 4", "{(::add (::multiply 1 2) (::dotMultiply 3 4))}")
|
||||
testParse("1 * 2 + 3 / 4", "{(::add (::multiply 1 2) (::divide 3 4))}")
|
||||
testParse("1 * 2 + 3 ./ 4", "{(::add (::multiply 1 2) (::dotDivide 3 4))}")
|
||||
testParse("1 * 2 - 3 .* 4", "{(::subtract (::multiply 1 2) (::dotMultiply 3 4))}")
|
||||
testParse("1 * 2 - 3 / 4", "{(::subtract (::multiply 1 2) (::divide 3 4))}")
|
||||
testParse("1 * 2 - 3 ./ 4", "{(::subtract (::multiply 1 2) (::dotDivide 3 4))}")
|
||||
testParse("1 * 2 - 3 * 4^5", "{(::subtract (::multiply 1 2) (::multiply 3 (::pow 4 5)))}")
|
||||
testParse(
|
||||
"1 * 2 - 3 * 4^5^6",
|
||||
"{(::subtract (::multiply 1 2) (::multiply 3 (::pow (::pow 4 5) 6)))}",
|
||||
)
|
||||
testParse("1 * -a[-2]", "{(::multiply 1 (::unaryMinus (::$_atIndex_$ :a (::unaryMinus 2))))}")
|
||||
})
|
||||
|
||||
describe("multi-line", () => {
|
||||
testParse("x=1; 2", "{:x = {1}; 2}")
|
||||
testParse("x=1; y=2", "{:x = {1}; :y = {2}}")
|
||||
})
|
||||
|
||||
describe("variables", () => {
|
||||
testParse("x = 1", "{:x = {1}}")
|
||||
testParse("x", "{:x}")
|
||||
testParse("x = 1; x", "{:x = {1}; :x}")
|
||||
})
|
||||
|
||||
describe("functions", () => {
|
||||
testParse("identity(x) = x", "{:identity = {|:x| {:x}}}") // Function definitions become lambda assignments
|
||||
testParse("identity(x)", "{(::identity :x)}")
|
||||
})
|
||||
|
||||
describe("arrays", () => {
|
||||
testParse("[]", "{(::$_constructArray_$ ())}")
|
||||
testParse("[0, 1, 2]", "{(::$_constructArray_$ (0 1 2))}")
|
||||
testParse("['hello', 'world']", "{(::$_constructArray_$ ('hello' 'world'))}")
|
||||
testParse("([0,1,2])[1]", "{(::$_atIndex_$ (::$_constructArray_$ (0 1 2)) 1)}")
|
||||
})
|
||||
|
||||
describe("records", () => {
|
||||
testParse("{a: 1, b: 2}", "{(::$_constructRecord_$ ('a': 1 'b': 2))}")
|
||||
testParse("{1+0: 1, 2+0: 2}", "{(::$_constructRecord_$ ((::add 1 0): 1 (::add 2 0): 2))}") // key can be any expression
|
||||
testParse("record.property", "{(::$_atIndex_$ :record 'property')}")
|
||||
})
|
||||
|
||||
describe("post operators", () => {
|
||||
//function call, array and record access are post operators with higher priority than unary operators
|
||||
testParse("a==!b(1)", "{(::equal :a (::not (::b 1)))}")
|
||||
testParse("a==!b[1]", "{(::equal :a (::not (::$_atIndex_$ :b 1)))}")
|
||||
testParse("a==!b.one", "{(::equal :a (::not (::$_atIndex_$ :b 'one')))}")
|
||||
})
|
||||
|
||||
describe("comments", () => {
|
||||
testParse("1 # This is a line comment", "{1}")
|
||||
testParse("1 // This is a line comment", "{1}")
|
||||
testParse("1 /* This is a multi line comment */", "{1}")
|
||||
testParse("/* This is a multi line comment */ 1", "{1}")
|
||||
testParse(
|
||||
`
|
||||
/* This is
|
||||
a multi line
|
||||
comment */
|
||||
1`,
|
||||
"{1}",
|
||||
)
|
||||
})
|
||||
|
||||
describe("ternary operator", () => {
|
||||
testParse("true ? 2 : 3", "{(::$$_ternary_$$ true 2 3)}")
|
||||
testParse(
|
||||
"false ? 2 : false ? 4 : 5",
|
||||
"{(::$$_ternary_$$ false 2 (::$$_ternary_$$ false 4 5))}",
|
||||
) // nested ternary
|
||||
})
|
||||
|
||||
describe("if then else", () => {
|
||||
testParse("if true then 2 else 3", "{(::$$_ternary_$$ true {2} {3})}")
|
||||
testParse("if false then {2} else {3}", "{(::$$_ternary_$$ false {2} {3})}")
|
||||
testParse(
|
||||
"if false then {2} else if false then {4} else {5}",
|
||||
"{(::$$_ternary_$$ false {2} (::$$_ternary_$$ false {4} {5}))}",
|
||||
) //nested if
|
||||
})
|
||||
|
||||
describe("logical", () => {
|
||||
testParse("true || false", "{(::or true false)}")
|
||||
testParse("true && false", "{(::and true false)}")
|
||||
testParse("a * b + c", "{(::add (::multiply :a :b) :c)}") // for comparison
|
||||
testParse("a && b || c", "{(::or (::and :a :b) :c)}")
|
||||
testParse("a && b || c && d", "{(::or (::and :a :b) (::and :c :d))}")
|
||||
testParse("a && !b || c", "{(::or (::and :a (::not :b)) :c)}")
|
||||
testParse("a && b==c || d", "{(::or (::and :a (::equal :b :c)) :d)}")
|
||||
testParse("a && b!=c || d", "{(::or (::and :a (::unequal :b :c)) :d)}")
|
||||
testParse("a && !(b==c) || d", "{(::or (::and :a (::not (::equal :b :c))) :d)}")
|
||||
testParse("a && b>=c || d", "{(::or (::and :a (::largerEq :b :c)) :d)}")
|
||||
testParse("a && !(b>=c) || d", "{(::or (::and :a (::not (::largerEq :b :c))) :d)}")
|
||||
testParse("a && b<=c || d", "{(::or (::and :a (::smallerEq :b :c)) :d)}")
|
||||
testParse("a && b>c || d", "{(::or (::and :a (::larger :b :c)) :d)}")
|
||||
testParse("a && b<c || d", "{(::or (::and :a (::smaller :b :c)) :d)}")
|
||||
testParse("a && b<c[i] || d", "{(::or (::and :a (::smaller :b (::$_atIndex_$ :c :i))) :d)}")
|
||||
testParse("a && b<c.i || d", "{(::or (::and :a (::smaller :b (::$_atIndex_$ :c 'i'))) :d)}")
|
||||
testParse("a && b<c(i) || d", "{(::or (::and :a (::smaller :b (::c :i))) :d)}")
|
||||
testParse("a && b<1+2 || d", "{(::or (::and :a (::smaller :b (::add 1 2))) :d)}")
|
||||
testParse(
|
||||
"a && b<1+2*3 || d",
|
||||
"{(::or (::and :a (::smaller :b (::add 1 (::multiply 2 3)))) :d)}",
|
||||
)
|
||||
testParse(
|
||||
"a && b<1+2*-3+4 || d",
|
||||
"{(::or (::and :a (::smaller :b (::add (::add 1 (::multiply 2 (::unaryMinus 3))) 4))) :d)}",
|
||||
)
|
||||
testParse(
|
||||
"a && b<1+2*3 || d ? true : false",
|
||||
"{(::$$_ternary_$$ (::or (::and :a (::smaller :b (::add 1 (::multiply 2 3)))) :d) true false)}",
|
||||
)
|
||||
})
|
||||
|
||||
describe("pipe", () => {
|
||||
testParse("1 -> add(2)", "{(::add 1 2)}")
|
||||
testParse("-1 -> add(2)", "{(::add (::unaryMinus 1) 2)}")
|
||||
testParse("-a[1] -> add(2)", "{(::add (::unaryMinus (::$_atIndex_$ :a 1)) 2)}")
|
||||
testParse("-f(1) -> add(2)", "{(::add (::unaryMinus (::f 1)) 2)}")
|
||||
testParse("1 + 2 -> add(3)", "{(::add 1 (::add 2 3))}")
|
||||
testParse("1 -> add(2) * 3", "{(::multiply (::add 1 2) 3)}")
|
||||
testParse("1 -> subtract(2)", "{(::subtract 1 2)}")
|
||||
testParse("-1 -> subtract(2)", "{(::subtract (::unaryMinus 1) 2)}")
|
||||
testParse("1 -> subtract(2) * 3", "{(::multiply (::subtract 1 2) 3)}")
|
||||
})
|
||||
|
||||
describe("elixir pipe", () => {
|
||||
//handled together with -> so there is no need for seperate tests
|
||||
testParse("1 |> add(2)", "{(::add 1 2)}")
|
||||
})
|
||||
|
||||
describe("to", () => {
|
||||
testParse("1 to 2", "{(::credibleIntervalToDistribution 1 2)}")
|
||||
testParse("-1 to -2", "{(::credibleIntervalToDistribution (::unaryMinus 1) (::unaryMinus 2))}") // lower than unary
|
||||
testParse(
|
||||
"a[1] to a[2]",
|
||||
"{(::credibleIntervalToDistribution (::$_atIndex_$ :a 1) (::$_atIndex_$ :a 2))}",
|
||||
) // lower than post
|
||||
testParse(
|
||||
"a.p1 to a.p2",
|
||||
"{(::credibleIntervalToDistribution (::$_atIndex_$ :a 'p1') (::$_atIndex_$ :a 'p2'))}",
|
||||
) // lower than post
|
||||
testParse("1 to 2 + 3", "{(::add (::credibleIntervalToDistribution 1 2) 3)}") // higher than binary operators
|
||||
testParse(
|
||||
"1->add(2) to 3->add(4) -> add(4)",
|
||||
"{(::credibleIntervalToDistribution (::add 1 2) (::add (::add 3 4) 4))}",
|
||||
) // lower than chain
|
||||
})
|
||||
|
||||
describe("inner block", () => {
|
||||
// inner blocks are 0 argument lambdas. They can be used whenever a value is required.
|
||||
// Like lambdas they have a local scope.
|
||||
testParse("x={y=1; y}; x", "{:x = {:y = {1}; :y}; :x}")
|
||||
})
|
||||
|
||||
describe("lambda", () => {
|
||||
testParse("{|x| x}", "{{|:x| {:x}}}")
|
||||
testParse("f={|x| x}", "{:f = {{|:x| {:x}}}}")
|
||||
testParse("f(x)=x", "{:f = {|:x| {:x}}}") // Function definitions are lambda assignments
|
||||
testParse("f(x)=x ? 1 : 0", "{:f = {|:x| {(::$$_ternary_$$ :x 1 0)}}}") // Function definitions are lambda assignments
|
||||
})
|
||||
|
||||
describe("Using lambda as value", () => {
|
||||
testParse(
|
||||
"myadd(x,y)=x+y; z=myadd; z",
|
||||
"{:myadd = {|:x,:y| {(::add :x :y)}}; :z = {:myadd}; :z}",
|
||||
)
|
||||
testParse(
|
||||
"myadd(x,y)=x+y; z=[myadd]; z",
|
||||
"{:myadd = {|:x,:y| {(::add :x :y)}}; :z = {(::$_constructArray_$ (:myadd))}; :z}",
|
||||
)
|
||||
testParse(
|
||||
"myaddd(x,y)=x+y; z={x: myaddd}; z",
|
||||
"{:myaddd = {|:x,:y| {(::add :x :y)}}; :z = {(::$_constructRecord_$ ('x': :myaddd))}; :z}",
|
||||
)
|
||||
testParse("f({|x| x+1})", "{(::f {|:x| {(::add :x 1)}})}")
|
||||
testParse("map(arr, {|x| x+1})", "{(::map :arr {|:x| {(::add :x 1)}})}")
|
||||
testParse(
|
||||
"map([1,2,3], {|x| x+1})",
|
||||
"{(::map (::$_constructArray_$ (1 2 3)) {|:x| {(::add :x 1)}})}",
|
||||
)
|
||||
testParse(
|
||||
"[1,2,3]->map({|x| x+1})",
|
||||
"{(::map (::$_constructArray_$ (1 2 3)) {|:x| {(::add :x 1)}})}",
|
||||
)
|
||||
})
|
||||
describe("unit", () => {
|
||||
testParse("1m", "{(::fromUnit_m 1)}")
|
||||
testParse("1M", "{(::fromUnit_M 1)}")
|
||||
testParse("1m+2cm", "{(::add (::fromUnit_m 1) (::fromUnit_cm 2))}")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parsing new line", () => {
|
||||
testParse(
|
||||
`
|
||||
a +
|
||||
b`,
|
||||
"{(::add :a :b)}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
x=
|
||||
1`,
|
||||
"{:x = {1}}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
x=1
|
||||
y=2`,
|
||||
"{:x = {1}; :y = {2}}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
x={
|
||||
y=2;
|
||||
y }
|
||||
x`,
|
||||
"{:x = {:y = {2}; :y}; :x}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
x={
|
||||
y=2
|
||||
y }
|
||||
x`,
|
||||
"{:x = {:y = {2}; :y}; :x}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
x={
|
||||
y=2
|
||||
y
|
||||
}
|
||||
x`,
|
||||
"{:x = {:y = {2}; :y}; :x}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
x=1
|
||||
y=2
|
||||
z=3
|
||||
`,
|
||||
"{:x = {1}; :y = {2}; :z = {3}}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
f={
|
||||
x=1
|
||||
y=2
|
||||
z=3
|
||||
x+y+z
|
||||
}
|
||||
`,
|
||||
"{:f = {:x = {1}; :y = {2}; :z = {3}; (::add (::add :x :y) :z)}}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
f={
|
||||
x=1
|
||||
y=2
|
||||
z=3
|
||||
x+y+z
|
||||
}
|
||||
g=f+4
|
||||
g
|
||||
`,
|
||||
"{:f = {:x = {1}; :y = {2}; :z = {3}; (::add (::add :x :y) :z)}; :g = {(::add :f 4)}; :g}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
f =
|
||||
{
|
||||
x=1; //x
|
||||
y=2 //y
|
||||
z=
|
||||
3
|
||||
x+
|
||||
y+
|
||||
z
|
||||
}
|
||||
g =
|
||||
f +
|
||||
4
|
||||
g ->
|
||||
h ->
|
||||
p ->
|
||||
q
|
||||
`,
|
||||
"{:f = {:x = {1}; :y = {2}; :z = {3}; (::add (::add :x :y) :z)}; :g = {(::add :f 4)}; (::q (::p (::h :g)))}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
a |>
|
||||
b |>
|
||||
c |>
|
||||
d
|
||||
`,
|
||||
"{(::d (::c (::b :a)))}",
|
||||
)
|
||||
testParse(
|
||||
`
|
||||
a |>
|
||||
b |>
|
||||
c |>
|
||||
d +
|
||||
e
|
||||
`,
|
||||
"{(::add (::d (::c (::b :a))) :e)}",
|
||||
)
|
||||
})
|
|
@ -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)))}",
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,46 @@
|
|||
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, 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 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, ()))
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
open Jest
|
||||
open Reducer_Peggy_TestHelpers
|
||||
|
||||
describe("Peggy to Expression", () => {
|
||||
describe("literals operators parenthesis", () => {
|
||||
// Note that there is always an outer block. Otherwise, external bindings are ignrored at the first statement
|
||||
testToExpression("1", "{1}", ~v="1", ())
|
||||
testToExpression("'hello'", "{'hello'}", ~v="'hello'", ())
|
||||
testToExpression("true", "{true}", ~v="true", ())
|
||||
testToExpression("1+2", "{(:add 1 2)}", ~v="3", ())
|
||||
testToExpression("add(1,2)", "{(:add 1 2)}", ~v="3", ())
|
||||
testToExpression("(1)", "{1}", ())
|
||||
testToExpression("(1+2)", "{(:add 1 2)}", ())
|
||||
})
|
||||
|
||||
describe("unary", () => {
|
||||
testToExpression("-1", "{(:unaryMinus 1)}", ~v="-1", ())
|
||||
testToExpression("!true", "{(:not true)}", ~v="false", ())
|
||||
testToExpression("1 + -1", "{(:add 1 (:unaryMinus 1))}", ~v="0", ())
|
||||
testToExpression("-a[0]", "{(:unaryMinus (:$_atIndex_$ :a 0))}", ())
|
||||
})
|
||||
|
||||
describe("multi-line", () => {
|
||||
testToExpression("x=1; 2", "{(:$_let_$ :x {1}); 2}", ~v="2", ())
|
||||
testToExpression("x=1; y=2", "{(:$_let_$ :x {1}); (:$_let_$ :y {2})}", ~v="{x: 1,y: 2}", ())
|
||||
})
|
||||
|
||||
describe("variables", () => {
|
||||
testToExpression("x = 1", "{(:$_let_$ :x {1})}", ~v="{x: 1}", ())
|
||||
testToExpression("x", "{:x}", ~v=":x", ()) //TODO: value should return error
|
||||
testToExpression("x = 1; x", "{(:$_let_$ :x {1}); :x}", ~v="1", ())
|
||||
})
|
||||
|
||||
describe("functions", () => {
|
||||
testToExpression(
|
||||
"identity(x) = x",
|
||||
"{(:$_let_$ :identity (:$$_lambda_$$ [x] {:x}))}",
|
||||
~v="{identity: lambda(x=>internal code)}",
|
||||
(),
|
||||
) // Function definitions become lambda assignments
|
||||
testToExpression("identity(x)", "{(:identity :x)}", ()) // Note value returns error properly
|
||||
testToExpression(
|
||||
"f(x) = x> 2 ? 0 : 1; f(3)",
|
||||
"{(:$_let_$ :f (:$$_lambda_$$ [x] {(:$$_ternary_$$ (:larger :x 2) 0 1)})); (:f 3)}",
|
||||
~v="0",
|
||||
(),
|
||||
)
|
||||
})
|
||||
|
||||
describe("arrays", () => {
|
||||
testToExpression("[]", "{(:$_constructArray_$ ())}", ~v="[]", ())
|
||||
testToExpression("[0, 1, 2]", "{(:$_constructArray_$ (0 1 2))}", ~v="[0,1,2]", ())
|
||||
testToExpression(
|
||||
"['hello', 'world']",
|
||||
"{(:$_constructArray_$ ('hello' 'world'))}",
|
||||
~v="['hello','world']",
|
||||
(),
|
||||
)
|
||||
testToExpression("([0,1,2])[1]", "{(:$_atIndex_$ (:$_constructArray_$ (0 1 2)) 1)}", ~v="1", ())
|
||||
})
|
||||
|
||||
describe("records", () => {
|
||||
testToExpression(
|
||||
"{a: 1, b: 2}",
|
||||
"{(:$_constructRecord_$ (('a' 1) ('b' 2)))}",
|
||||
~v="{a: 1,b: 2}",
|
||||
(),
|
||||
)
|
||||
testToExpression(
|
||||
"{1+0: 1, 2+0: 2}",
|
||||
"{(:$_constructRecord_$ (((:add 1 0) 1) ((:add 2 0) 2)))}",
|
||||
(),
|
||||
) // key can be any expression
|
||||
testToExpression("record.property", "{(:$_atIndex_$ :record 'property')}", ())
|
||||
testToExpression(
|
||||
"record={property: 1}; record.property",
|
||||
"{(:$_let_$ :record {(:$_constructRecord_$ (('property' 1)))}); (:$_atIndex_$ :record 'property')}",
|
||||
~v="1",
|
||||
(),
|
||||
)
|
||||
})
|
||||
|
||||
describe("comments", () => {
|
||||
testToExpression("1 # This is a line comment", "{1}", ~v="1", ())
|
||||
testToExpression("1 // This is a line comment", "{1}", ~v="1", ())
|
||||
testToExpression("1 /* This is a multi line comment */", "{1}", ~v="1", ())
|
||||
testToExpression("/* This is a multi line comment */ 1", "{1}", ~v="1", ())
|
||||
})
|
||||
|
||||
describe("ternary operator", () => {
|
||||
testToExpression("true ? 1 : 0", "{(:$$_ternary_$$ true 1 0)}", ~v="1", ())
|
||||
testToExpression("false ? 1 : 0", "{(:$$_ternary_$$ false 1 0)}", ~v="0", ())
|
||||
testToExpression(
|
||||
"true ? 1 : false ? 2 : 0",
|
||||
"{(:$$_ternary_$$ true 1 (:$$_ternary_$$ false 2 0))}",
|
||||
~v="1",
|
||||
(),
|
||||
) // nested ternary
|
||||
testToExpression(
|
||||
"false ? 1 : false ? 2 : 0",
|
||||
"{(:$$_ternary_$$ false 1 (:$$_ternary_$$ false 2 0))}",
|
||||
~v="0",
|
||||
(),
|
||||
) // 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", () => {
|
||||
testToExpression("if true then 2 else 3", "{(:$$_ternary_$$ true {2} {3})}", ())
|
||||
testToExpression("if true then {2} else {3}", "{(:$$_ternary_$$ true {2} {3})}", ())
|
||||
testToExpression(
|
||||
"if false then {2} else if false then {4} else {5}",
|
||||
"{(:$$_ternary_$$ false {2} (:$$_ternary_$$ false {4} {5}))}",
|
||||
(),
|
||||
) //nested if
|
||||
})
|
||||
|
||||
describe("pipe", () => {
|
||||
testToExpression("1 -> add(2)", "{(:add 1 2)}", ~v="3", ())
|
||||
testToExpression("-1 -> add(2)", "{(:add (:unaryMinus 1) 2)}", ~v="1", ()) // note that unary has higher priority naturally
|
||||
testToExpression("1 -> add(2) * 3", "{(:multiply (:add 1 2) 3)}", ~v="9", ())
|
||||
})
|
||||
|
||||
describe("elixir pipe", () => {
|
||||
testToExpression("1 |> add(2)", "{(:add 1 2)}", ~v="3", ())
|
||||
})
|
||||
|
||||
// see testParse for priorities of to and credibleIntervalToDistribution
|
||||
|
||||
describe("inner block", () => {
|
||||
// inner blocks are 0 argument lambdas. They can be used whenever a value is required.
|
||||
// Like lambdas they have a local scope.
|
||||
testToExpression(
|
||||
"y=99; x={y=1; y}",
|
||||
"{(:$_let_$ :y {99}); (:$_let_$ :x {(:$_let_$ :y {1}); :y})}",
|
||||
~v="{x: 1,y: 99}",
|
||||
(),
|
||||
)
|
||||
})
|
||||
|
||||
describe("lambda", () => {
|
||||
testToExpression("{|x| x}", "{(:$$_lambda_$$ [x] {:x})}", ~v="lambda(x=>internal code)", ())
|
||||
testToExpression(
|
||||
"f={|x| x}",
|
||||
"{(:$_let_$ :f {(:$$_lambda_$$ [x] {:x})})}",
|
||||
~v="{f: lambda(x=>internal code)}",
|
||||
(),
|
||||
)
|
||||
testToExpression(
|
||||
"f(x)=x",
|
||||
"{(:$_let_$ :f (:$$_lambda_$$ [x] {:x}))}",
|
||||
~v="{f: lambda(x=>internal code)}",
|
||||
(),
|
||||
) // Function definitions are lambda assignments
|
||||
testToExpression(
|
||||
"f(x)=x ? 1 : 0",
|
||||
"{(:$_let_$ :f (:$$_lambda_$$ [x] {(:$$_ternary_$$ :x 1 0)}))}",
|
||||
~v="{f: lambda(x=>internal code)}",
|
||||
(),
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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(odds1 + odds2)",
|
||||
"{(:$_let_$ :odds1 {(:$_constructArray_$ (1 3 5))}); (:$_let_$ :odds2 {(:$_constructArray_$ (7 9))}); (:$_typeAlias_$ #odds (:$_typeModifier_memberOf_$ #number (:add :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
|
||||
})
|
|
@ -19,6 +19,9 @@ let expectParseToBe = (expr: string, answer: string) =>
|
|||
let expectEvalToBe = (expr: string, answer: string) =>
|
||||
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toBe(answer)
|
||||
|
||||
let expectEvalError = (expr: string) =>
|
||||
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toMatch("Error\(")
|
||||
|
||||
let expectEvalBindingsToBe = (expr: string, bindings: Reducer.externalBindings, answer: string) =>
|
||||
Reducer.evaluateUsingOptions(expr, ~externalBindings=Some(bindings), ~environment=None)
|
||||
->ExpressionValue.toStringResult
|
||||
|
@ -29,6 +32,7 @@ let testParseToBe = (expr, answer) => test(expr, () => expectParseToBe(expr, ans
|
|||
let testDescriptionParseToBe = (desc, expr, answer) =>
|
||||
test(desc, () => expectParseToBe(expr, answer))
|
||||
|
||||
let testEvalError = expr => test(expr, () => expectEvalError(expr))
|
||||
let testEvalToBe = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer))
|
||||
let testDescriptionEvalToBe = (desc, expr, answer) => test(desc, () => expectEvalToBe(expr, answer))
|
||||
let testEvalBindingsToBe = (expr, bindingsList, answer) =>
|
||||
|
|
|
@ -2,62 +2,10 @@
|
|||
open Jest
|
||||
open Reducer_TestHelpers
|
||||
|
||||
// describe("Parse for Bindings", () => {
|
||||
// testParseOuterToBe("x", "Ok((:$$bindExpression (:$$bindings) :x))")
|
||||
// testParseOuterToBe("x+1", "Ok((:$$bindExpression (:$$bindings) (:add :x 1)))")
|
||||
// testParseOuterToBe(
|
||||
// "y = x+1; y",
|
||||
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) :y))",
|
||||
// )
|
||||
// })
|
||||
|
||||
// describe("Parse Partial", () => {
|
||||
// testParsePartialToBe(
|
||||
// "x",
|
||||
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) :x) (:$exportVariablesExpression)))",
|
||||
// )
|
||||
// testParsePartialToBe(
|
||||
// "y=x",
|
||||
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y :x)) (:$exportVariablesExpression)))",
|
||||
// )
|
||||
// testParsePartialToBe(
|
||||
// "y=x+1",
|
||||
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) (:$exportVariablesExpression)))",
|
||||
// )
|
||||
// testParsePartialToBe(
|
||||
// "y = x+1; z = y",
|
||||
// "Ok((:$$bindExpression (:$$bindStatement (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) (:$let :z :y)) (:$exportVariablesExpression)))",
|
||||
// )
|
||||
// })
|
||||
|
||||
describe("Eval with Bindings", () => {
|
||||
testEvalBindingsToBe("x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(1)")
|
||||
testEvalBindingsToBe("x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
|
||||
testParseToBe("y = x+1; y", "Ok((:$$block (:$$block (:$let :y (:add :x 1)) :y)))")
|
||||
testParseToBe("y = x+1; y", "Ok({(:$_let_$ :y {(:add :x 1)}); :y})")
|
||||
testEvalBindingsToBe("y = x+1; y", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
|
||||
testEvalBindingsToBe("y = x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1,y: 2})")
|
||||
})
|
||||
|
||||
/*
|
||||
Partial code is a partial code fragment that is cut out from a larger code.
|
||||
Therefore it does not end with an expression.
|
||||
*/
|
||||
// describe("Eval Partial", () => {
|
||||
// testEvalPartialBindingsToBe(
|
||||
// // A partial cannot end with an expression
|
||||
// "x",
|
||||
// list{("x", ExpressionValue.EvNumber(1.))},
|
||||
// "Error(Assignment expected)",
|
||||
// )
|
||||
// testEvalPartialBindingsToBe("y=x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1,y: 1})")
|
||||
// testEvalPartialBindingsToBe(
|
||||
// "y=x+1",
|
||||
// list{("x", ExpressionValue.EvNumber(1.))},
|
||||
// "Ok({x: 1,y: 2})",
|
||||
// )
|
||||
// testEvalPartialBindingsToBe(
|
||||
// "y = x+1; z = y",
|
||||
// list{("x", ExpressionValue.EvNumber(1.))},
|
||||
// "Ok({x: 1,y: 2,z: 2})",
|
||||
// )
|
||||
// })
|
||||
|
|
|
@ -2,8 +2,8 @@ open Jest
|
|||
open Reducer_TestHelpers
|
||||
|
||||
describe("Parse function assignment", () => {
|
||||
testParseToBe("f(x)=x", "Ok((:$$block (:$let :f (:$$lambda [x] (:$$block :x)))))")
|
||||
testParseToBe("f(x)=2*x", "Ok((:$$block (:$let :f (:$$lambda [x] (:$$block (:multiply 2 :x))))))")
|
||||
testParseToBe("f(x)=x", "Ok({(:$_let_$ :f (:$$_lambda_$$ [x] {:x}))})")
|
||||
testParseToBe("f(x)=2*x", "Ok({(:$_let_$ :f (:$$_lambda_$$ [x] {(:multiply 2 :x)}))})")
|
||||
//MathJs does not allow blocks in function definitions
|
||||
})
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ describe("Arity check", () => {
|
|||
)
|
||||
testEvalToBe("f(x)=x; f(f)", "Ok(lambda(x=>internal code))")
|
||||
testEvalToBe(
|
||||
"f(x,y)=x(y); f(z)",
|
||||
"f(x,y)=x(y); f(1)",
|
||||
"Error(2 arguments expected. Instead 1 argument(s) were passed.)",
|
||||
)
|
||||
})
|
||||
|
@ -51,7 +51,7 @@ describe("call and bindings", () => {
|
|||
)
|
||||
testParseToBe(
|
||||
"f=99; g(x)=f; g(2)",
|
||||
"Ok((:$$block (:$$block (:$let :f 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(x)=x; g(x)=f(x); g(2)", "Ok(2)")
|
||||
|
@ -63,15 +63,28 @@ describe("call and bindings", () => {
|
|||
})
|
||||
|
||||
describe("function tricks", () => {
|
||||
testParseToBe(
|
||||
"f(x)=f(y)=2; f(2)",
|
||||
"Ok((:$$block (:$$block (:$let :f (:$$lambda [x] (:$$block (:$let :f (:$$lambda [y] (:$$block 2)))))) (:f 2))))",
|
||||
)
|
||||
testEvalToBe("f(x)=f(y)=2; f(2)", "Ok({f: lambda(y=>internal code),x: 2})")
|
||||
testEvalError("f(x)=f(y)=2; f(2)") //Error because chain assignment is not allowed
|
||||
testEvalToBe("y=2;g(x)=y+1;g(2)", "Ok(3)")
|
||||
testEvalToBe("y=2;g(x)=inspect(y)+1", "Ok({g: lambda(x=>internal code),y: 2})")
|
||||
MySkip.testEvalToBe("f(x) = x(x); f(f)", "????") // TODO: Infinite loop. Any solution? Catching proper exception or timeout?
|
||||
MySkip.testEvalToBe("f(x, x)=x+x; f(1,2)", "????") // TODO: Duplicate parameters
|
||||
MySkip.testEvalToBe("myadd(x,y)=x+y; z=[add]; z[0](3,2)", "????") //TODO: to fix with new parser
|
||||
MySkip.testEvalToBe("myaddd(x,y)=x+y; z={x: add}; z.x(3,2)", "????") //TODO: to fix with new parser
|
||||
testEvalToBe("myadd(x,y)=x+y; z=myadd; z", "Ok(lambda(x,y=>internal code))")
|
||||
testEvalToBe("myadd(x,y)=x+y; z=myadd; z(1, 1)", "Ok(2)")
|
||||
})
|
||||
|
||||
describe("lambda in structures", () => {
|
||||
testEvalToBe(
|
||||
"myadd(x,y)=x+y; z=[myadd]",
|
||||
"Ok({myadd: lambda(x,y=>internal code),z: [lambda(x,y=>internal code)]})",
|
||||
)
|
||||
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0]", "Ok(lambda(x,y=>internal code))")
|
||||
testEvalToBe("myadd(x,y)=x+y; z=[myadd]; z[0](3,2)", "Ok(5)")
|
||||
testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z", "Ok({x: lambda(x,y=>internal code)})")
|
||||
testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z.x", "Ok(lambda(x,y=>internal code))")
|
||||
testEvalToBe("myaddd(x,y)=x+y; z={x: myaddd}; z.x(3,2)", "Ok(5)")
|
||||
})
|
||||
|
||||
describe("ternary and bindings", () => {
|
||||
testEvalToBe("f(x)=x ? 1 : 0; f(true)", "Ok(1)")
|
||||
testEvalToBe("f(x)=x>2 ? 1 : 0; f(3)", "Ok(1)")
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ open Jest
|
|||
open Reducer_TestHelpers
|
||||
|
||||
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", () => {
|
||||
|
|
|
@ -1,55 +1,9 @@
|
|||
open Jest
|
||||
open Reducer_TestHelpers
|
||||
|
||||
describe("reducer using mathjs parse", () => {
|
||||
// Test the MathJs parser compatibility
|
||||
// Those tests toString that there is a semantic mapping from MathJs to Expression
|
||||
// Reducer.parse is called by Reducer.eval
|
||||
// See https://mathjs.org/docs/expressions/syntax.html
|
||||
// See https://mathjs.org/docs/reference/functions.html
|
||||
// Those tests toString that we are converting mathjs parse tree to what we need
|
||||
|
||||
describe("expressions", () => {
|
||||
testParseToBe("1", "Ok((:$$block 1))")
|
||||
testParseToBe("(1)", "Ok((:$$block 1))")
|
||||
testParseToBe("1+2", "Ok((:$$block (:add 1 2)))")
|
||||
testParseToBe("1+2*3", "Ok((:$$block (:add 1 (:multiply 2 3))))")
|
||||
})
|
||||
describe("arrays", () => {
|
||||
//Note. () is a empty list in Lisp
|
||||
// The only builtin structure in Lisp is list. There are no arrays
|
||||
// [1,2,3] becomes (1 2 3)
|
||||
testDescriptionParseToBe("empty", "[]", "Ok((:$$block ()))")
|
||||
testParseToBe("[1, 2, 3]", "Ok((:$$block (1 2 3)))")
|
||||
testParseToBe("['hello', 'world']", "Ok((:$$block ('hello' 'world')))")
|
||||
testDescriptionParseToBe("index", "([0,1,2])[1]", "Ok((:$$block (:$atIndex (0 1 2) (1))))")
|
||||
})
|
||||
describe("records", () => {
|
||||
testDescriptionParseToBe(
|
||||
"define",
|
||||
"{a: 1, b: 2}",
|
||||
"Ok((:$$block (:$constructRecord (('a' 1) ('b' 2)))))",
|
||||
)
|
||||
testDescriptionParseToBe(
|
||||
"use",
|
||||
"{a: 1, b: 2}.a",
|
||||
"Ok((:$$block (:$atIndex (:$constructRecord (('a' 1) ('b' 2))) ('a'))))",
|
||||
)
|
||||
})
|
||||
describe("multi-line", () => {
|
||||
testParseToBe("1; 2", "Ok((:$$block (:$$block 1 2)))")
|
||||
testParseToBe("1+1; 2+1", "Ok((:$$block (:$$block (:add 1 1) (:add 2 1))))")
|
||||
})
|
||||
describe("assignment", () => {
|
||||
testParseToBe("x=1; x", "Ok((:$$block (:$$block (:$let :x 1) :x)))")
|
||||
testParseToBe("x=1+1; x+1", "Ok((:$$block (:$$block (:$let :x (:add 1 1)) (:add :x 1))))")
|
||||
})
|
||||
})
|
||||
|
||||
describe("eval", () => {
|
||||
// All MathJs operators and functions are builtin for string, float and boolean
|
||||
// .e.g + - / * > >= < <= == /= not and or
|
||||
// See https://mathjs.org/docs/expressions/syntax.html
|
||||
// See https://mathjs.org/docs/reference/functions.html
|
||||
describe("expressions", () => {
|
||||
testEvalToBe("1", "Ok(1)")
|
||||
|
@ -70,20 +24,21 @@ describe("eval", () => {
|
|||
})
|
||||
describe("records", () => {
|
||||
test("define", () => expectEvalToBe("{a: 1, b: 2}", "Ok({a: 1,b: 2})"))
|
||||
test("index", () => expectEvalToBe("{a: 1}.a", "Ok(1)"))
|
||||
test("index not found", () => expectEvalToBe("{a: 1}.b", "Error(Record property not found: b)"))
|
||||
test("index", () => expectEvalToBe("r = {a: 1}; r.a", "Ok(1)"))
|
||||
test("index", () => expectEvalToBe("r = {a: 1}; r.b", "Error(Record property not found: b)"))
|
||||
testEvalError("{a: 1}.b") // invalid syntax
|
||||
})
|
||||
|
||||
describe("multi-line", () => {
|
||||
testEvalToBe("1; 2", "Error(Assignment expected)")
|
||||
testEvalToBe("1+1; 2+1", "Error(Assignment expected)")
|
||||
testEvalError("1; 2")
|
||||
testEvalError("1+1; 2+1")
|
||||
})
|
||||
describe("assignment", () => {
|
||||
testEvalToBe("x=1; x", "Ok(1)")
|
||||
testEvalToBe("x=1+1; x+1", "Ok(3)")
|
||||
testEvalToBe("x=1; y=x+1; y+1", "Ok(3)")
|
||||
testEvalToBe("1; x=1", "Error(Assignment expected)")
|
||||
testEvalToBe("1; 1", "Error(Assignment expected)")
|
||||
testEvalError("1; x=1")
|
||||
testEvalError("1; 1")
|
||||
testEvalToBe("x=1; x=1", "Ok({x: 1})")
|
||||
})
|
||||
})
|
||||
|
@ -94,9 +49,9 @@ describe("test exceptions", () => {
|
|||
"javascriptraise('div by 0')",
|
||||
"Error(JS Exception: Error: 'div by 0')",
|
||||
)
|
||||
testDescriptionEvalToBe(
|
||||
"rescript exception",
|
||||
"rescriptraise()",
|
||||
"Error(TODO: unhandled rescript exception)",
|
||||
)
|
||||
// testDescriptionEvalToBe(
|
||||
// "rescript exception",
|
||||
// "rescriptraise()",
|
||||
// "Error(TODO: unhandled rescript exception)",
|
||||
// )
|
||||
})
|
||||
|
|
|
@ -23,7 +23,7 @@ describe("eval on distribution functions", () => {
|
|||
testEval("-normal(5,2)", "Ok(Normal(-5,2))")
|
||||
})
|
||||
describe("to", () => {
|
||||
testEval("5 to 2", "Error(Distribution Math Error: Low value must be less than high value.)")
|
||||
testEval("5 to 2", "Error(TODO: Low value must be less than high value.)")
|
||||
testEval("to(2,5)", "Ok(Lognormal(1.1512925464970227,0.27853260523016377))")
|
||||
testEval("to(-2,2)", "Ok(Normal(0,1.2159136638235384))")
|
||||
})
|
||||
|
@ -31,6 +31,9 @@ describe("eval on distribution functions", () => {
|
|||
testEval("mean(normal(5,2))", "Ok(5)")
|
||||
testEval("mean(lognormal(1,2))", "Ok(20.085536923187668)")
|
||||
testEval("mean(gamma(5,5))", "Ok(25)")
|
||||
testEval("mean(bernoulli(0.2))", "Ok(0.2)")
|
||||
testEval("mean(bernoulli(0.8))", "Ok(0.8)")
|
||||
testEval("mean(logistic(5,1))", "Ok(5)")
|
||||
})
|
||||
describe("toString", () => {
|
||||
testEval("toString(normal(5,2))", "Ok('Normal(5,2)')")
|
||||
|
@ -120,34 +123,28 @@ describe("eval on distribution functions", () => {
|
|||
|
||||
describe("parse on distribution functions", () => {
|
||||
describe("power", () => {
|
||||
testParse("normal(5,2) ^ normal(5,1)", "Ok((:$$block (:pow (:normal 5 2) (:normal 5 1))))")
|
||||
testParse("3 ^ normal(5,1)", "Ok((:$$block (:pow 3 (:normal 5 1))))")
|
||||
testParse("normal(5,2) ^ 3", "Ok((:$$block (:pow (:normal 5 2) 3)))")
|
||||
testParse("normal(5,2) ^ normal(5,1)", "Ok({(:pow (:normal 5 2) (:normal 5 1))})")
|
||||
testParse("3 ^ normal(5,1)", "Ok({(:pow 3 (:normal 5 1))})")
|
||||
testParse("normal(5,2) ^ 3", "Ok({(:pow (:normal 5 2) 3)})")
|
||||
})
|
||||
describe("subtraction", () => {
|
||||
testParse("10 - normal(5,1)", "Ok((:$$block (:subtract 10 (:normal 5 1))))")
|
||||
testParse("normal(5,1) - 10", "Ok((:$$block (:subtract (:normal 5 1) 10)))")
|
||||
testParse("10 - normal(5,1)", "Ok({(:subtract 10 (:normal 5 1))})")
|
||||
testParse("normal(5,1) - 10", "Ok({(:subtract (:normal 5 1) 10)})")
|
||||
})
|
||||
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((:$$block (:dotSubtract (:normal 5 2) (:normal 5 1))))",
|
||||
// TODO: !!! returns "Ok((:$$block (:dotPow (:normal 5 2) (:normal 5 1))))"
|
||||
"Ok((:$$_block_$$ (:dotSubtract (: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", () => {
|
||||
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", () => {
|
||||
testParse(~skip=true, "normal(5,2) .+ normal(5,1)", "Ok((:dotAdd (:normal 5 2) (:normal 5 1)))")
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { Distribution, resultMap, defaultBindings } from "../../src/js/index";
|
||||
import {
|
||||
Distribution,
|
||||
resultMap,
|
||||
defaultBindings,
|
||||
mergeBindings,
|
||||
} from "../../src/js/index";
|
||||
import { testRun, testRunPartial } from "./TestHelpers";
|
||||
|
||||
function Ok<b>(x: b) {
|
||||
|
@ -66,6 +71,17 @@ describe("Partials", () => {
|
|||
value: 10,
|
||||
});
|
||||
});
|
||||
test("Can merge bindings from three partials", () => {
|
||||
let bindings1 = testRunPartial(`x = 1`);
|
||||
let bindings2 = testRunPartial(`y = 2`);
|
||||
let bindings3 = testRunPartial(`z = 3`);
|
||||
expect(
|
||||
testRun(`x + y + z`, mergeBindings([bindings1, bindings2, bindings3]))
|
||||
).toEqual({
|
||||
tag: "number",
|
||||
value: 6,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("JS Imports", () => {
|
||||
|
|
|
@ -51,6 +51,7 @@ let mkExponential = rate => DistributionTypes.Symbolic(#Exponential({rate: rate}
|
|||
let mkUniform = (low, high) => DistributionTypes.Symbolic(#Uniform({low: low, high: high}))
|
||||
let mkCauchy = (local, scale) => DistributionTypes.Symbolic(#Cauchy({local: local, scale: scale}))
|
||||
let mkLognormal = (mu, sigma) => DistributionTypes.Symbolic(#Lognormal({mu: mu, sigma: sigma}))
|
||||
let mkDelta = x => DistributionTypes.Symbolic(#Float(x))
|
||||
|
||||
let normalMake = SymbolicDist.Normal.make
|
||||
let betaMake = SymbolicDist.Beta.make
|
||||
|
@ -60,3 +61,13 @@ let cauchyMake = SymbolicDist.Cauchy.make
|
|||
let lognormalMake = SymbolicDist.Lognormal.make
|
||||
let triangularMake = SymbolicDist.Triangular.make
|
||||
let floatMake = SymbolicDist.Float.make
|
||||
|
||||
let fmapGenDist = symbdistres => E.R.fmap(s => DistributionTypes.Symbolic(s), symbdistres)
|
||||
let normalMakeR = (mean, stdev) => fmapGenDist(SymbolicDist.Normal.make(mean, stdev))
|
||||
let betaMakeR = (alpha, beta) => fmapGenDist(SymbolicDist.Beta.make(alpha, beta))
|
||||
let exponentialMakeR = rate => fmapGenDist(SymbolicDist.Exponential.make(rate))
|
||||
let uniformMakeR = (low, high) => fmapGenDist(SymbolicDist.Uniform.make(low, high))
|
||||
let cauchyMakeR = (local, rate) => fmapGenDist(SymbolicDist.Cauchy.make(local, rate))
|
||||
let lognormalMakeR = (mu, sigma) => fmapGenDist(SymbolicDist.Lognormal.make(mu, sigma))
|
||||
let triangularMakeR = (low, mode, high) =>
|
||||
fmapGenDist(SymbolicDist.Triangular.make(low, mode, high))
|
||||
|
|
|
@ -38,19 +38,6 @@ describe("XYShapes", () => {
|
|||
)
|
||||
})
|
||||
|
||||
describe("logScorePoint", () => {
|
||||
makeTest("When identical", XYShape.logScorePoint(30, pointSetDist1, pointSetDist1), Some(0.0))
|
||||
makeTest(
|
||||
"When similar",
|
||||
XYShape.logScorePoint(30, pointSetDist1, pointSetDist2),
|
||||
Some(1.658971191043856),
|
||||
)
|
||||
makeTest(
|
||||
"When very different",
|
||||
XYShape.logScorePoint(30, pointSetDist1, pointSetDist3),
|
||||
Some(210.3721280423322),
|
||||
)
|
||||
})
|
||||
describe("integrateWithTriangles", () =>
|
||||
makeTest(
|
||||
"integrates correctly",
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
],
|
||||
"suffix": ".bs.js",
|
||||
"namespace": true,
|
||||
"bs-dependencies": ["@glennsl/rescript-jest", "bisect_ppx"],
|
||||
"bs-dependencies": ["bisect_ppx"],
|
||||
"bs-dev-dependencies": ["@glennsl/rescript-jest", "rescript-fast-check"],
|
||||
"gentypeconfig": {
|
||||
"language": "typescript",
|
||||
"module": "commonjs",
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
{
|
||||
"name": "@quri/squiggle-lang",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.9",
|
||||
"homepage": "https://squiggle-language.com",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "yarn build:rescript && yarn build:typescript",
|
||||
"peggy": "peggy --cache",
|
||||
"build": "yarn build:peggy && yarn build:rescript && yarn build:typescript",
|
||||
"build:peggy": "find . -type f -name *.peggy -exec yarn peggy {} \\;",
|
||||
"build:rescript": "rescript build -with-deps",
|
||||
"build:typescript": "tsc",
|
||||
"bundle": "webpack",
|
||||
"start": "rescript build -w -with-deps",
|
||||
"clean": "rescript clean && rm -r dist",
|
||||
"clean": "rescript clean && rm -rf dist",
|
||||
"test:reducer": "jest __tests__/Reducer*/",
|
||||
"benchmark": "ts-node benchmark/conversion_tests.ts",
|
||||
"test": "jest",
|
||||
"test:ts": "jest __tests__/TS/",
|
||||
"test:rescript": "jest --modulePathIgnorePatterns=__tests__/TS/*",
|
||||
"test:watch": "jest --watchAll",
|
||||
"coverage:rescript": "rm -f *.coverage; yarn clean; BISECT_ENABLE=yes yarn build; yarn test:rescript; bisect-ppx-report html",
|
||||
"coverage:ts": "yarn clean; yarn build; nyc --reporter=lcov yarn test:ts",
|
||||
"coverage:rescript:ci": "yarn clean; BISECT_ENABLE=yes yarn build; yarn test:rescript; bisect-ppx-report send-to Codecov",
|
||||
"coverage:rescript": "rm -f *.coverage && yarn clean && BISECT_ENABLE=yes yarn build && yarn test:rescript && bisect-ppx-report html",
|
||||
"coverage:ts": "yarn clean && yarn build && nyc --reporter=lcov yarn test:ts",
|
||||
"coverage:rescript:ci": "yarn clean && BISECT_ENABLE=yes yarn build:rescript && yarn test:rescript && bisect-ppx-report send-to Codecov",
|
||||
"coverage:ts:ci": "yarn coverage:ts && codecov",
|
||||
"lint:rescript": "./lint.sh",
|
||||
"lint:prettier": "prettier --check .",
|
||||
|
@ -33,34 +35,35 @@
|
|||
"Rescript"
|
||||
],
|
||||
"author": "Quantified Uncertainty Research Institute",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rescript": "^9.1.4",
|
||||
"@stdlib/stats": "^0.0.13",
|
||||
"jstat": "^1.9.5",
|
||||
"mathjs": "^10.6.0",
|
||||
"pdfast": "^0.2.0",
|
||||
"mathjs": "^10.5.0"
|
||||
"rescript": "^9.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bisect_ppx": "^2.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rescript-fast-check": "^1.1.1",
|
||||
"@glennsl/rescript-jest": "^0.9.0",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@types/jest": "^27.5.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"bisect_ppx": "^2.7.1",
|
||||
"chalk": "^5.0.1",
|
||||
"codecov": "^3.8.3",
|
||||
"fast-check": "^2.25.0",
|
||||
"gentype": "^4.3.0",
|
||||
"gentype": "^4.4.0",
|
||||
"jest": "^27.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moduleserve": "^0.9.1",
|
||||
"nyc": "^15.1.0",
|
||||
"reanalyze": "^2.19.0",
|
||||
"peggy": "^2.0.1",
|
||||
"reanalyze": "^2.22.0",
|
||||
"rescript-fast-check": "^1.1.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"ts-loader": "^9.3.0",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "^4.6.3",
|
||||
"webpack": "^5.72.0",
|
||||
"ts-node": "^10.8.1",
|
||||
"typescript": "^4.7.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
"source": "./src/js/index.ts",
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { result, resultMap, Ok } from "./types";
|
||||
import {
|
||||
Constructors_mean,
|
||||
Constructors_stdev,
|
||||
Constructors_sample,
|
||||
Constructors_pdf,
|
||||
Constructors_cdf,
|
||||
|
@ -35,7 +36,7 @@ import {
|
|||
Constructors_pointwiseSubtract,
|
||||
Constructors_pointwiseLogarithm,
|
||||
Constructors_pointwisePower,
|
||||
} from "../rescript/Distributions/DistributionOperation/DistributionOperation.gen";
|
||||
} from "../rescript/Distributions/DistributionOperation.gen";
|
||||
|
||||
export type point = { x: number; y: number };
|
||||
|
||||
|
@ -69,6 +70,10 @@ export class Distribution {
|
|||
return Constructors_mean({ env: this.env }, this.t);
|
||||
}
|
||||
|
||||
stdev(): result<number, distributionError> {
|
||||
return Constructors_stdev({ env: this.env }, this.t);
|
||||
}
|
||||
|
||||
sample(): result<number, distributionError> {
|
||||
return Constructors_sample({ env: this.env }, this.t);
|
||||
}
|
||||
|
|
|
@ -1,39 +1,43 @@
|
|||
import * as _ from "lodash";
|
||||
import {
|
||||
samplingParams,
|
||||
import type {
|
||||
environment,
|
||||
expressionValue,
|
||||
externalBindings,
|
||||
errorValue,
|
||||
} from "../rescript/TypescriptInterface.gen";
|
||||
import {
|
||||
defaultEnvironment,
|
||||
evaluatePartialUsingExternalBindings,
|
||||
evaluateUsingOptions,
|
||||
externalBindings,
|
||||
expressionValue,
|
||||
errorValue,
|
||||
foreignFunctionInterface,
|
||||
} from "../rescript/TypescriptInterface.gen";
|
||||
export {
|
||||
makeSampleSetDist,
|
||||
errorValueToString,
|
||||
distributionErrorToString,
|
||||
distributionError,
|
||||
} from "../rescript/TypescriptInterface.gen";
|
||||
export type {
|
||||
samplingParams,
|
||||
errorValue,
|
||||
externalBindings as bindings,
|
||||
jsImports,
|
||||
};
|
||||
distributionError,
|
||||
declarationArg,
|
||||
declaration,
|
||||
} from "../rescript/TypescriptInterface.gen";
|
||||
export type { errorValue, externalBindings as bindings, jsImports };
|
||||
import {
|
||||
jsValueToBinding,
|
||||
jsValueToExpressionValue,
|
||||
jsValue,
|
||||
rescriptExport,
|
||||
squiggleExpression,
|
||||
convertRawToTypescript,
|
||||
lambdaValue,
|
||||
} from "./rescript_interop";
|
||||
import { result, resultMap, tag, tagged } from "./types";
|
||||
import { Distribution, shape } from "./distribution";
|
||||
|
||||
export { Distribution, squiggleExpression, result, resultMap, shape };
|
||||
export { Distribution, resultMap, defaultEnvironment };
|
||||
export type { result, shape, environment, lambdaValue, squiggleExpression };
|
||||
|
||||
export let defaultSamplingInputs: samplingParams = {
|
||||
export let defaultSamplingInputs: environment = {
|
||||
sampleCount: 10000,
|
||||
xyPointLength: 10000,
|
||||
};
|
||||
|
@ -48,7 +52,7 @@ export function run(
|
|||
let i = imports ? imports : defaultImports;
|
||||
let e = environment ? environment : defaultEnvironment;
|
||||
let res: result<expressionValue, errorValue> = evaluateUsingOptions(
|
||||
{ externalBindings: mergeImports(b, i), environment: e },
|
||||
{ externalBindings: mergeImportsWithBindings(b, i), environment: e },
|
||||
squiggleString
|
||||
);
|
||||
return resultMap(res, (x) => createTsExport(x, e));
|
||||
|
@ -67,12 +71,26 @@ export function runPartial(
|
|||
|
||||
return evaluatePartialUsingExternalBindings(
|
||||
squiggleString,
|
||||
mergeImports(b, i),
|
||||
mergeImportsWithBindings(b, i),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
function mergeImports(
|
||||
export function runForeign(
|
||||
fn: lambdaValue,
|
||||
args: jsValue[],
|
||||
environment?: environment
|
||||
): result<squiggleExpression, errorValue> {
|
||||
let e = environment ? environment : defaultEnvironment;
|
||||
let res: result<expressionValue, errorValue> = foreignFunctionInterface(
|
||||
fn,
|
||||
args.map(jsValueToExpressionValue),
|
||||
e
|
||||
);
|
||||
return resultMap(res, (x) => createTsExport(x, e));
|
||||
}
|
||||
|
||||
function mergeImportsWithBindings(
|
||||
bindings: externalBindings,
|
||||
imports: jsImports
|
||||
): externalBindings {
|
||||
|
@ -90,6 +108,12 @@ type jsImports = { [key: string]: jsValue };
|
|||
export let defaultImports: jsImports = {};
|
||||
export let defaultBindings: externalBindings = {};
|
||||
|
||||
export function mergeBindings(
|
||||
allBindings: externalBindings[]
|
||||
): externalBindings {
|
||||
return allBindings.reduce((acc, x) => ({ ...acc, ...x }));
|
||||
}
|
||||
|
||||
function createTsExport(
|
||||
x: expressionValue,
|
||||
environment: environment
|
||||
|
@ -155,5 +179,13 @@ function createTsExport(
|
|||
return tag("string", x.value);
|
||||
case "EvSymbol":
|
||||
return tag("symbol", x.value);
|
||||
case "EvDate":
|
||||
return tag("date", x.value);
|
||||
case "EvTimeDuration":
|
||||
return tag("timeDuration", x.value);
|
||||
case "EvDeclaration":
|
||||
return tag("lambdaDeclaration", x.value);
|
||||
case "EvTypeIdentifier":
|
||||
return tag("typeIdentifier", x.value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as _ from "lodash";
|
||||
import {
|
||||
import type {
|
||||
expressionValue,
|
||||
mixedShape,
|
||||
sampleSetDist,
|
||||
genericDist,
|
||||
|
@ -8,6 +9,8 @@ import {
|
|||
discreteShape,
|
||||
continuousShape,
|
||||
lambdaValue,
|
||||
lambdaDeclaration,
|
||||
declarationArg,
|
||||
} from "../rescript/TypescriptInterface.gen";
|
||||
import { Distribution } from "./distribution";
|
||||
import { tagged, tag } from "./types";
|
||||
|
@ -54,6 +57,22 @@ export type rescriptExport =
|
|||
| {
|
||||
TAG: 9; // EvSymbol
|
||||
_0: string;
|
||||
}
|
||||
| {
|
||||
TAG: 10; // EvDate
|
||||
_0: Date;
|
||||
}
|
||||
| {
|
||||
TAG: 11; // EvTimeDuration
|
||||
_0: number;
|
||||
}
|
||||
| {
|
||||
TAG: 12; // EvDeclaration
|
||||
_0: rescriptLambdaDeclaration;
|
||||
}
|
||||
| {
|
||||
TAG: 13; // EvTypeIdentifier
|
||||
_0: string;
|
||||
};
|
||||
|
||||
type rescriptDist =
|
||||
|
@ -75,6 +94,23 @@ type rescriptPointSetDist =
|
|||
_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 =
|
||||
| tagged<"symbol", string>
|
||||
| tagged<"string", string>
|
||||
|
@ -85,7 +121,13 @@ export type squiggleExpression =
|
|||
| tagged<"boolean", boolean>
|
||||
| tagged<"distribution", Distribution>
|
||||
| tagged<"number", number>
|
||||
| tagged<"record", { [key: string]: squiggleExpression }>;
|
||||
| tagged<"date", Date>
|
||||
| tagged<"timeDuration", number>
|
||||
| tagged<"lambdaDeclaration", lambdaDeclaration>
|
||||
| tagged<"record", { [key: string]: squiggleExpression }>
|
||||
| tagged<"typeIdentifier", string>;
|
||||
|
||||
export { lambdaValue };
|
||||
|
||||
export function convertRawToTypescript(
|
||||
result: rescriptExport,
|
||||
|
@ -124,6 +166,28 @@ export function convertRawToTypescript(
|
|||
return tag("string", result._0);
|
||||
case 9: // EvSymbol
|
||||
return tag("symbol", result._0);
|
||||
case 10: // EvDate
|
||||
return tag("date", result._0);
|
||||
case 11: // EvTimeDuration
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,3 +232,21 @@ export function jsValueToBinding(value: jsValue): rescriptExport {
|
|||
return { TAG: 7, _0: _.mapValues(value, jsValueToBinding) };
|
||||
}
|
||||
}
|
||||
|
||||
export function jsValueToExpressionValue(value: jsValue): expressionValue {
|
||||
if (typeof value === "boolean") {
|
||||
return { tag: "EvBool", value: value as boolean };
|
||||
} else if (typeof value === "string") {
|
||||
return { tag: "EvString", value: value as string };
|
||||
} else if (typeof value === "number") {
|
||||
return { tag: "EvNumber", value: value as number };
|
||||
} else if (Array.isArray(value)) {
|
||||
return { tag: "EvArray", value: value.map(jsValueToExpressionValue) };
|
||||
} else {
|
||||
// Record
|
||||
return {
|
||||
tag: "EvRecord",
|
||||
value: _.mapValues(value, jsValueToExpressionValue),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,14 +10,15 @@ type env = {
|
|||
}
|
||||
|
||||
let defaultEnv = {
|
||||
sampleCount: 10000,
|
||||
xyPointLength: 10000,
|
||||
sampleCount: MagicNumbers.Environment.defaultSampleCount,
|
||||
xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
|
||||
}
|
||||
|
||||
type outputType =
|
||||
| Dist(genericDist)
|
||||
| Float(float)
|
||||
| String(string)
|
||||
| FloatArray(array<float>)
|
||||
| Bool(bool)
|
||||
| GenDistError(error)
|
||||
|
||||
|
@ -128,7 +129,7 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
|||
let fromDistFn = (
|
||||
subFnName: DistributionTypes.DistributionOperation.fromDist,
|
||||
dist: genericDist,
|
||||
) => {
|
||||
): outputType => {
|
||||
let response = switch subFnName {
|
||||
| ToFloat(distToFloatOperation) =>
|
||||
GenericDist.toFloatOperation(dist, ~toPointSetFn, ~distToFloatOperation)
|
||||
|
@ -144,6 +145,19 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
|||
Dist(dist)
|
||||
}
|
||||
| ToDist(Normalize) => dist->GenericDist.normalize->Dist
|
||||
| ToScore(KLDivergence(t2)) =>
|
||||
GenericDist.Score.klDivergence(dist, t2, ~toPointSetFn)
|
||||
->E.R2.fmap(r => Float(r))
|
||||
->OutputLocal.fromResult
|
||||
| ToScore(LogScore(answer, prior)) =>
|
||||
GenericDist.Score.logScoreWithPointResolution(
|
||||
~prediction=dist,
|
||||
~answer,
|
||||
~prior,
|
||||
~toPointSetFn,
|
||||
)
|
||||
->E.R2.fmap(r => Float(r))
|
||||
->OutputLocal.fromResult
|
||||
| ToBool(IsNormalized) => dist->GenericDist.isNormalized->Bool
|
||||
| ToDist(Truncate(leftCutoff, rightCutoff)) =>
|
||||
GenericDist.truncate(~toPointSetFn, ~leftCutoff, ~rightCutoff, dist, ())
|
||||
|
@ -159,6 +173,15 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
|
|||
->GenericDist.toPointSet(~xyPointLength, ~sampleCount, ())
|
||||
->E.R2.fmap(r => Dist(PointSet(r)))
|
||||
->OutputLocal.fromResult
|
||||
| ToDist(Scale(#LogarithmWithThreshold(eps), f)) =>
|
||||
dist
|
||||
->GenericDist.pointwiseCombinationFloat(
|
||||
~toPointSetFn,
|
||||
~algebraicCombination=#LogarithmWithThreshold(eps),
|
||||
~f,
|
||||
)
|
||||
->E.R2.fmap(r => Dist(r))
|
||||
->OutputLocal.fromResult
|
||||
| ToDist(Scale(#Logarithm, f)) =>
|
||||
dist
|
||||
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination=#Logarithm, ~f)
|
||||
|
@ -242,12 +265,21 @@ module Constructors = {
|
|||
module C = DistributionTypes.Constructors.UsingDists
|
||||
open OutputLocal
|
||||
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 cdf = (~env, dist, f) => C.cdf(dist, f)->run(~env)->toFloatR
|
||||
let inv = (~env, dist, f) => C.inv(dist, f)->run(~env)->toFloatR
|
||||
let pdf = (~env, dist, f) => C.pdf(dist, f)->run(~env)->toFloatR
|
||||
let normalize = (~env, dist) => C.normalize(dist)->run(~env)->toDistR
|
||||
let isNormalized = (~env, dist) => C.isNormalized(dist)->run(~env)->toBoolR
|
||||
let klDivergence = (~env, dist1, dist2) => C.klDivergence(dist1, dist2)->run(~env)->toFloatR
|
||||
let logScoreWithPointResolution = (
|
||||
~env,
|
||||
~prediction: DistributionTypes.genericDist,
|
||||
~answer: float,
|
||||
~prior: option<DistributionTypes.genericDist>,
|
||||
) => C.logScoreWithPointResolution(~prediction, ~answer, ~prior)->run(~env)->toFloatR
|
||||
let toPointSet = (~env, dist) => C.toPointSet(dist)->run(~env)->toDistR
|
||||
let toSampleSet = (~env, dist, n) => C.toSampleSet(dist, n)->run(~env)->toDistR
|
||||
let fromSamples = (~env, xs) => C.fromSamples(xs)->run(~env)->toDistR
|
||||
|
@ -266,6 +298,8 @@ module Constructors = {
|
|||
let algebraicLogarithm = (~env, dist1, dist2) =>
|
||||
C.algebraicLogarithm(dist1, dist2)->run(~env)->toDistR
|
||||
let algebraicPower = (~env, dist1, dist2) => C.algebraicPower(dist1, dist2)->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 pointwiseAdd = (~env, dist1, dist2) => C.pointwiseAdd(dist1, dist2)->run(~env)->toDistR
|
||||
let pointwiseMultiply = (~env, dist1, dist2) =>
|
||||
C.pointwiseMultiply(dist1, dist2)->run(~env)->toDistR
|
|
@ -14,6 +14,7 @@ type outputType =
|
|||
| Dist(genericDist)
|
||||
| Float(float)
|
||||
| String(string)
|
||||
| FloatArray(array<float>)
|
||||
| Bool(bool)
|
||||
| GenDistError(error)
|
||||
|
||||
|
@ -48,6 +49,10 @@ module Constructors: {
|
|||
@genType
|
||||
let mean: (~env: env, genericDist) => result<float, error>
|
||||
@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>
|
||||
@genType
|
||||
let cdf: (~env: env, genericDist, float) => result<float, error>
|
||||
|
@ -60,6 +65,15 @@ module Constructors: {
|
|||
@genType
|
||||
let isNormalized: (~env: env, genericDist) => result<bool, error>
|
||||
@genType
|
||||
let klDivergence: (~env: env, genericDist, genericDist) => result<float, error>
|
||||
@genType
|
||||
let logScoreWithPointResolution: (
|
||||
~env: env,
|
||||
~prediction: genericDist,
|
||||
~answer: float,
|
||||
~prior: option<genericDist>,
|
||||
) => result<float, error>
|
||||
@genType
|
||||
let toPointSet: (~env: env, genericDist) => result<genericDist, error>
|
||||
@genType
|
||||
let toSampleSet: (~env: env, genericDist, int) => result<genericDist, error>
|
||||
|
@ -86,6 +100,10 @@ module Constructors: {
|
|||
@genType
|
||||
let algebraicPower: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
||||
@genType
|
||||
let scaleLogarithm: (~env: env, genericDist, float) => result<genericDist, error>
|
||||
@genType
|
||||
let scalePower: (~env: env, genericDist, float) => result<genericDist, error>
|
||||
@genType
|
||||
let pointwiseAdd: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
||||
@genType
|
||||
let pointwiseMultiply: (~env: env, genericDist, genericDist) => result<genericDist, error>
|
|
@ -30,13 +30,14 @@ module Error = {
|
|||
@genType
|
||||
let toString = (err: error): string =>
|
||||
switch err {
|
||||
| NotYetImplemented => "Function Not Yet Implemented"
|
||||
| NotYetImplemented => "Function not yet implemented"
|
||||
| Unreachable => "Unreachable"
|
||||
| DistributionVerticalShiftIsInvalid => "Distribution Vertical Shift is Invalid"
|
||||
| DistributionVerticalShiftIsInvalid => "Distribution vertical shift is invalid"
|
||||
| ArgumentError(s) => `Argument Error ${s}`
|
||||
| LogarithmOfDistributionError(s) => `Logarithm of input error: ${s}`
|
||||
| SampleSetError(TooFewSamples) => "Too Few Samples"
|
||||
| SampleSetError(NonNumericInput(err)) => `Found a non-number in input: ${err}`
|
||||
| SampleSetError(OperationError(err)) => Operation.Error.toString(err)
|
||||
| OperationError(err) => Operation.Error.toString(err)
|
||||
| PointSetConversionError(err) => SampleSetDist.pointsetConversionErrorToString(err)
|
||||
| SparklineError(err) => PointSetTypes.sparklineErrorToString(err)
|
||||
|
@ -67,11 +68,17 @@ module DistributionOperation = {
|
|||
| #Mean
|
||||
| #Sample
|
||||
| #IntegralSum
|
||||
| #Mode
|
||||
| #Stdev
|
||||
| #Min
|
||||
| #Max
|
||||
| #Variance
|
||||
]
|
||||
|
||||
type toScaleFn = [
|
||||
| #Power
|
||||
| #Logarithm
|
||||
| #LogarithmWithThreshold(float)
|
||||
]
|
||||
|
||||
type toDist =
|
||||
|
@ -90,9 +97,12 @@ module DistributionOperation = {
|
|||
| ToString
|
||||
| ToSparkline(int)
|
||||
|
||||
type toScore = KLDivergence(genericDist) | LogScore(float, option<genericDist>)
|
||||
|
||||
type fromDist =
|
||||
| ToFloat(toFloat)
|
||||
| ToDist(toDist)
|
||||
| ToScore(toScore)
|
||||
| ToDistCombination(direction, Operation.Algebraic.t, [#Dist(genericDist) | #Float(float)])
|
||||
| ToString(toString)
|
||||
| ToBool(toBool)
|
||||
|
@ -112,9 +122,16 @@ module DistributionOperation = {
|
|||
| ToFloat(#Cdf(r)) => `cdf(${E.Float.toFixed(r)})`
|
||||
| ToFloat(#Inv(r)) => `inv(${E.Float.toFixed(r)})`
|
||||
| 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(#Sample) => `sample`
|
||||
| ToFloat(#IntegralSum) => `integralSum`
|
||||
| ToScore(KLDivergence(_)) => `klDivergence`
|
||||
| ToScore(LogScore(x, _)) => `logScore against ${E.Float.toFixed(x)}`
|
||||
| ToDist(Normalize) => `normalize`
|
||||
| ToDist(ToPointSet) => `toPointSet`
|
||||
| ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})`
|
||||
|
@ -122,6 +139,8 @@ module DistributionOperation = {
|
|||
| ToDist(Inspect) => `inspect`
|
||||
| ToDist(Scale(#Power, r)) => `scalePower(${E.Float.toFixed(r)})`
|
||||
| ToDist(Scale(#Logarithm, r)) => `scaleLog(${E.Float.toFixed(r)})`
|
||||
| ToDist(Scale(#LogarithmWithThreshold(eps), r)) =>
|
||||
`scaleLogWithThreshold(${E.Float.toFixed(r)}, epsilon=${E.Float.toFixed(eps)})`
|
||||
| ToString(ToString) => `toString`
|
||||
| ToString(ToSparkline(n)) => `toSparkline(${E.I.toString(n)})`
|
||||
| ToBool(IsNormalized) => `isNormalized`
|
||||
|
@ -142,6 +161,8 @@ module Constructors = {
|
|||
module UsingDists = {
|
||||
@genType
|
||||
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 cdf = (dist, x): t => FromDist(ToFloat(#Cdf(x)), dist)
|
||||
let inv = (dist, x): t => FromDist(ToFloat(#Inv(x)), dist)
|
||||
|
@ -153,8 +174,17 @@ module Constructors = {
|
|||
let fromSamples = (xs): t => FromSamples(xs)
|
||||
let truncate = (dist, left, right): t => FromDist(ToDist(Truncate(left, right)), dist)
|
||||
let inspect = (dist): t => FromDist(ToDist(Inspect), dist)
|
||||
let klDivergence = (dist1, dist2): t => FromDist(ToScore(KLDivergence(dist2)), dist1)
|
||||
let logScoreWithPointResolution = (~prediction, ~answer, ~prior): t => FromDist(
|
||||
ToScore(LogScore(answer, prior)),
|
||||
prediction,
|
||||
)
|
||||
let scalePower = (dist, n): t => FromDist(ToDist(Scale(#Power, n)), dist)
|
||||
let scaleLogarithm = (dist, n): t => FromDist(ToDist(Scale(#Logarithm, n)), dist)
|
||||
let scaleLogarithmWithThreshold = (dist, n, eps): t => FromDist(
|
||||
ToDist(Scale(#LogarithmWithThreshold(eps), n)),
|
||||
dist,
|
||||
)
|
||||
let toString = (dist): t => FromDist(ToString(ToString), dist)
|
||||
let toSparkline = (dist, n): t => FromDist(ToString(ToSparkline(n)), dist)
|
||||
let algebraicAdd = (dist1, dist2: genericDist): t => FromDist(
|
||||
|
|
|
@ -31,6 +31,8 @@ let sampleN = (t: t, n) =>
|
|||
| SampleSet(r) => SampleSetDist.sampleN(r, n)
|
||||
}
|
||||
|
||||
let sample = (t: t) => sampleN(t, 1)->E.A.first |> E.O.toExn("Should not have happened")
|
||||
|
||||
let toSampleSetDist = (t: t, n) =>
|
||||
SampleSetDist.make(sampleN(t, n))->E.R2.errMap(DistributionTypes.Error.sampleErrorToDistErr)
|
||||
|
||||
|
@ -59,6 +61,46 @@ let integralEndY = (t: t): float =>
|
|||
|
||||
let isNormalized = (t: t): bool => Js.Math.abs_float(integralEndY(t) -. 1.0) < 1e-7
|
||||
|
||||
module Score = {
|
||||
let klDivergence = (prediction, answer, ~toPointSetFn: toPointSetFn): result<float, error> => {
|
||||
let pointSets = E.R.merge(toPointSetFn(prediction), toPointSetFn(answer))
|
||||
pointSets |> E.R2.bind(((predi, ans)) =>
|
||||
PointSetDist.T.klDivergence(predi, ans)->E.R2.errMap(x => DistributionTypes.OperationError(x))
|
||||
)
|
||||
}
|
||||
|
||||
let logScoreWithPointResolution = (
|
||||
~prediction: DistributionTypes.genericDist,
|
||||
~answer: float,
|
||||
~prior: option<DistributionTypes.genericDist>,
|
||||
~toPointSetFn: toPointSetFn,
|
||||
): result<float, error> => {
|
||||
switch prior {
|
||||
| Some(prior') =>
|
||||
E.R.merge(toPointSetFn(prior'), toPointSetFn(prediction))->E.R.bind(((
|
||||
prior'',
|
||||
prediction'',
|
||||
)) =>
|
||||
PointSetDist.T.logScoreWithPointResolution(
|
||||
~prediction=prediction'',
|
||||
~answer,
|
||||
~prior=prior''->Some,
|
||||
)->E.R2.errMap(x => DistributionTypes.OperationError(x))
|
||||
)
|
||||
| None =>
|
||||
prediction
|
||||
->toPointSetFn
|
||||
->E.R.bind(x =>
|
||||
PointSetDist.T.logScoreWithPointResolution(
|
||||
~prediction=x,
|
||||
~answer,
|
||||
~prior=None,
|
||||
)->E.R2.errMap(x => DistributionTypes.OperationError(x))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let toFloatOperation = (
|
||||
t,
|
||||
~toPointSetFn: toPointSetFn,
|
||||
|
@ -66,7 +108,7 @@ let toFloatOperation = (
|
|||
) => {
|
||||
switch distToFloatOperation {
|
||||
| #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) {
|
||||
| Symbolic(r) => SymbolicDist.T.operate(op, r)->E.R.toOption
|
||||
| _ => None
|
||||
|
@ -76,6 +118,8 @@ let toFloatOperation = (
|
|||
| (SampleSet(sampleSet), #Mean) => SampleSetDist.mean(sampleSet)->Some
|
||||
| (SampleSet(sampleSet), #Sample) => SampleSetDist.sample(sampleSet)->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
|
||||
}
|
||||
|
||||
|
@ -88,6 +132,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,7 +293,7 @@ module AlgebraicCombination = {
|
|||
let fn = Operation.Algebraic.toFn(arithmeticOperation)
|
||||
E.R.merge(toSampleSet(t1), toSampleSet(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))
|
||||
}
|
||||
|
@ -384,14 +438,12 @@ let pointwiseCombinationFloat = (
|
|||
~algebraicCombination: Operation.algebraicOperation,
|
||||
~f: float,
|
||||
): result<t, error> => {
|
||||
let m = switch algebraicCombination {
|
||||
| #Add | #Subtract => Error(DistributionTypes.DistributionVerticalShiftIsInvalid)
|
||||
| (#Multiply | #Divide | #Power | #Logarithm) as arithmeticOperation =>
|
||||
let executeCombination = arithOp =>
|
||||
toPointSetFn(t)->E.R.bind(t => {
|
||||
//TODO: Move to PointSet codebase
|
||||
let fn = (secondary, main) => Operation.Scale.toFn(arithmeticOperation, main, secondary)
|
||||
let integralSumCacheFn = Operation.Scale.toIntegralSumCacheFn(arithmeticOperation)
|
||||
let integralCacheFn = Operation.Scale.toIntegralCacheFn(arithmeticOperation)
|
||||
let fn = (secondary, main) => Operation.Scale.toFn(arithOp, main, secondary)
|
||||
let integralSumCacheFn = Operation.Scale.toIntegralSumCacheFn(arithOp)
|
||||
let integralCacheFn = Operation.Scale.toIntegralCacheFn(arithOp)
|
||||
PointSetDist.T.mapYResult(
|
||||
~integralSumCacheFn=integralSumCacheFn(f),
|
||||
~integralCacheFn=integralCacheFn(f),
|
||||
|
@ -399,6 +451,11 @@ let pointwiseCombinationFloat = (
|
|||
t,
|
||||
)->E.R2.errMap(x => DistributionTypes.OperationError(x))
|
||||
})
|
||||
let m = switch algebraicCombination {
|
||||
| #Add | #Subtract => Error(DistributionTypes.DistributionVerticalShiftIsInvalid)
|
||||
| (#Multiply | #Divide | #Power | #Logarithm) as arithmeticOperation =>
|
||||
executeCombination(arithmeticOperation)
|
||||
| #LogarithmWithThreshold(eps) => executeCombination(#LogarithmWithThreshold(eps))
|
||||
}
|
||||
m->E.R2.fmap(r => DistributionTypes.PointSet(r))
|
||||
}
|
|
@ -6,6 +6,7 @@ type scaleMultiplyFn = (t, float) => result<t, error>
|
|||
type pointwiseAddFn = (t, t) => result<t, error>
|
||||
|
||||
let sampleN: (t, int) => array<float>
|
||||
let sample: t => float
|
||||
|
||||
let toSampleSetDist: (t, int) => Belt.Result.t<QuriSquiggleLang.SampleSetDist.t, error>
|
||||
|
||||
|
@ -23,6 +24,16 @@ let toFloatOperation: (
|
|||
~distToFloatOperation: DistributionTypes.DistributionOperation.toFloat,
|
||||
) => result<float, error>
|
||||
|
||||
module Score: {
|
||||
let klDivergence: (t, t, ~toPointSetFn: toPointSetFn) => result<float, error>
|
||||
let logScoreWithPointResolution: (
|
||||
~prediction: t,
|
||||
~answer: float,
|
||||
~prior: option<t>,
|
||||
~toPointSetFn: toPointSetFn,
|
||||
) => result<float, error>
|
||||
}
|
||||
|
||||
@genType
|
||||
let toPointSet: (
|
||||
t,
|
|
@ -86,6 +86,7 @@ let stepwiseToLinear = (t: t): t =>
|
|||
|
||||
// Note: This results in a distribution with as many points as the sum of those in t1 and t2.
|
||||
let combinePointwise = (
|
||||
~combiner=XYShape.PointwiseCombination.combine,
|
||||
~integralSumCachesFn=(_, _) => None,
|
||||
~distributionType: PointSetTypes.distributionType=#PDF,
|
||||
fn: (float, float) => result<float, Operation.Error.t>,
|
||||
|
@ -119,7 +120,7 @@ let combinePointwise = (
|
|||
|
||||
let interpolator = XYShape.XtoY.continuousInterpolator(t1.interpolation, extrapolation)
|
||||
|
||||
XYShape.PointwiseCombination.combine(fn, interpolator, t1.xyShape, t2.xyShape)->E.R2.fmap(x =>
|
||||
combiner(fn, interpolator, t1.xyShape, t2.xyShape)->E.R2.fmap(x =>
|
||||
make(~integralSumCache=combinedIntegralSum, x)
|
||||
)
|
||||
}
|
||||
|
@ -269,11 +270,26 @@ module T = Dist({
|
|||
}
|
||||
let variance = (t: t): float =>
|
||||
XYShape.Analysis.getVarianceDangerously(t, mean, Analysis.getMeanOfSquares)
|
||||
|
||||
let klDivergence = (prediction: t, answer: t) => {
|
||||
let newShape = XYShape.PointwiseCombination.combineAlongSupportOfSecondArgument(
|
||||
PointSetDist_Scoring.KLDivergence.integrand,
|
||||
prediction.xyShape,
|
||||
answer.xyShape,
|
||||
)
|
||||
newShape->E.R2.fmap(x => x->make->integralEndY)
|
||||
}
|
||||
let logScoreWithPointResolution = (~prediction: t, ~answer: float, ~prior: option<t>) => {
|
||||
let priorPdf = prior->E.O2.fmap((shape, x) => XYShape.XtoY.linear(x, shape.xyShape))
|
||||
let predictionPdf = x => XYShape.XtoY.linear(x, prediction.xyShape)
|
||||
PointSetDist_Scoring.LogScoreWithPointResolution.score(~priorPdf, ~predictionPdf, ~answer)
|
||||
}
|
||||
})
|
||||
|
||||
let isNormalized = (t: t): bool => {
|
||||
let areaUnderIntegral = t |> updateIntegralCache(Some(T.integral(t))) |> T.integralEndY
|
||||
areaUnderIntegral < 1. +. 1e-7 && areaUnderIntegral > 1. -. 1e-7
|
||||
areaUnderIntegral < 1. +. MagicNumbers.Epsilon.seven &&
|
||||
areaUnderIntegral > 1. -. MagicNumbers.Epsilon.seven
|
||||
}
|
||||
|
||||
let downsampleEquallyOverX = (length, t): t =>
|
||||
|
|
|
@ -33,29 +33,22 @@ let shapeFn = (fn, t: t) => t |> getShape |> fn
|
|||
let lastY = (t: t) => t |> getShape |> XYShape.T.lastY
|
||||
|
||||
let combinePointwise = (
|
||||
~combiner=XYShape.PointwiseCombination.combine,
|
||||
~integralSumCachesFn=(_, _) => None,
|
||||
fn,
|
||||
~fn=(a, b) => Ok(a +. b),
|
||||
t1: PointSetTypes.discreteShape,
|
||||
t2: PointSetTypes.discreteShape,
|
||||
): result<PointSetTypes.discreteShape, 'e> => {
|
||||
let combinedIntegralSum = Common.combineIntegralSums(
|
||||
integralSumCachesFn,
|
||||
t1.integralSumCache,
|
||||
t2.integralSumCache,
|
||||
)
|
||||
// let combinedIntegralSum = Common.combineIntegralSums(
|
||||
// integralSumCachesFn,
|
||||
// t1.integralSumCache,
|
||||
// t2.integralSumCache,
|
||||
// )
|
||||
|
||||
// TODO: does it ever make sense to pointwise combine the integrals here?
|
||||
// It could be done for pointwise additions, but is that ever needed?
|
||||
|
||||
make(
|
||||
~integralSumCache=combinedIntegralSum,
|
||||
XYShape.PointwiseCombination.combine(
|
||||
fn,
|
||||
XYShape.XtoY.discreteInterpolator,
|
||||
t1.xyShape,
|
||||
t2.xyShape,
|
||||
)->E.R.toExn("Addition operation should never fail", _),
|
||||
)->Ok
|
||||
combiner(fn, XYShape.XtoY.discreteInterpolator, t1.xyShape, t2.xyShape)->E.R2.fmap(make)
|
||||
}
|
||||
|
||||
let reduce = (
|
||||
|
@ -63,7 +56,7 @@ let reduce = (
|
|||
fn: (float, float) => result<float, 'e>,
|
||||
discreteShapes: array<PointSetTypes.discreteShape>,
|
||||
): result<t, 'e> => {
|
||||
let merge = combinePointwise(~integralSumCachesFn, fn)
|
||||
let merge = combinePointwise(~integralSumCachesFn, ~fn)
|
||||
discreteShapes |> E.A.R.foldM(merge, empty)
|
||||
}
|
||||
|
||||
|
@ -228,4 +221,15 @@ module T = Dist({
|
|||
let getMeanOfSquares = t => t |> shapeMap(XYShape.T.square) |> mean
|
||||
XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares)
|
||||
}
|
||||
|
||||
let klDivergence = (prediction: t, answer: t) => {
|
||||
combinePointwise(
|
||||
~fn=PointSetDist_Scoring.KLDivergence.integrand,
|
||||
prediction,
|
||||
answer,
|
||||
)->E.R2.fmap(integralEndY)
|
||||
}
|
||||
let logScoreWithPointResolution = (~prediction: t, ~answer: float, ~prior: option<t>) => {
|
||||
Error(Operation.NotYetImplemented)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -33,6 +33,12 @@ module type dist = {
|
|||
|
||||
let mean: t => float
|
||||
let variance: t => float
|
||||
let klDivergence: (t, t) => result<float, Operation.Error.t>
|
||||
let logScoreWithPointResolution: (
|
||||
~prediction: t,
|
||||
~answer: float,
|
||||
~prior: option<t>,
|
||||
) => result<float, Operation.Error.t>
|
||||
}
|
||||
|
||||
module Dist = (T: dist) => {
|
||||
|
@ -55,6 +61,8 @@ module Dist = (T: dist) => {
|
|||
let mean = T.mean
|
||||
let variance = T.variance
|
||||
let integralEndY = T.integralEndY
|
||||
let klDivergence = T.klDivergence
|
||||
let logScoreWithPointResolution = T.logScoreWithPointResolution
|
||||
|
||||
let updateIntegralCache = T.updateIntegralCache
|
||||
|
||||
|
|
|
@ -36,6 +36,47 @@ let updateIntegralCache = (integralCache, t: t): t => {
|
|||
integralCache: integralCache,
|
||||
}
|
||||
|
||||
let combinePointwise = (
|
||||
~integralSumCachesFn=(_, _) => None,
|
||||
~integralCachesFn=(_, _) => None,
|
||||
fn: (float, float) => result<float, 'e>,
|
||||
t1: t,
|
||||
t2: t,
|
||||
): result<t, 'e> => {
|
||||
let reducedDiscrete =
|
||||
[t1, t2]
|
||||
|> E.A.fmap(toDiscrete)
|
||||
|> E.A.O.concatSomes
|
||||
|> Discrete.reduce(~integralSumCachesFn, fn)
|
||||
|> E.R.toExn("Theoretically unreachable state")
|
||||
|
||||
let reducedContinuous =
|
||||
[t1, t2]
|
||||
|> E.A.fmap(toContinuous)
|
||||
|> E.A.O.concatSomes
|
||||
|> Continuous.reduce(~integralSumCachesFn, fn)
|
||||
|
||||
let combinedIntegralSum = Common.combineIntegralSums(
|
||||
integralSumCachesFn,
|
||||
t1.integralSumCache,
|
||||
t2.integralSumCache,
|
||||
)
|
||||
|
||||
let combinedIntegral = Common.combineIntegrals(
|
||||
integralCachesFn,
|
||||
t1.integralCache,
|
||||
t2.integralCache,
|
||||
)
|
||||
reducedContinuous->E.R2.fmap(continuous =>
|
||||
make(
|
||||
~integralSumCache=combinedIntegralSum,
|
||||
~integralCache=combinedIntegral,
|
||||
~discrete=reducedDiscrete,
|
||||
~continuous,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
module T = Dist({
|
||||
type t = PointSetTypes.mixedShape
|
||||
type integral = PointSetTypes.continuousShape
|
||||
|
@ -259,6 +300,15 @@ module T = Dist({
|
|||
| _ => XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares)
|
||||
}
|
||||
}
|
||||
|
||||
let klDivergence = (prediction: t, answer: t) => {
|
||||
let klDiscretePart = Discrete.T.klDivergence(prediction.discrete, answer.discrete)
|
||||
let klContinuousPart = Continuous.T.klDivergence(prediction.continuous, answer.continuous)
|
||||
E.R.merge(klDiscretePart, klContinuousPart)->E.R2.fmap(t => fst(t) +. snd(t))
|
||||
}
|
||||
let logScoreWithPointResolution = (~prediction: t, ~answer: float, ~prior: option<t>) => {
|
||||
Error(Operation.NotYetImplemented)
|
||||
}
|
||||
})
|
||||
|
||||
let combineAlgebraically = (op: Operation.convolutionOperation, t1: t, t2: t): t => {
|
||||
|
|
|
@ -86,7 +86,7 @@ let combinePointwise = (
|
|||
| (Discrete(m1), Discrete(m2)) =>
|
||||
Discrete.combinePointwise(
|
||||
~integralSumCachesFn,
|
||||
fn,
|
||||
~fn,
|
||||
m1,
|
||||
m2,
|
||||
)->E.R2.fmap(x => PointSetTypes.Discrete(x))
|
||||
|
@ -195,6 +195,23 @@ module T = Dist({
|
|||
| Discrete(m) => Discrete.T.variance(m)
|
||||
| Continuous(m) => Continuous.T.variance(m)
|
||||
}
|
||||
|
||||
let klDivergence = (prediction: t, answer: t) =>
|
||||
switch (prediction, answer) {
|
||||
| (Continuous(t1), Continuous(t2)) => Continuous.T.klDivergence(t1, t2)
|
||||
| (Discrete(t1), Discrete(t2)) => Discrete.T.klDivergence(t1, t2)
|
||||
| (m1, m2) => Mixed.T.klDivergence(m1->toMixed, m2->toMixed)
|
||||
}
|
||||
|
||||
let logScoreWithPointResolution = (~prediction: t, ~answer: float, ~prior: option<t>) => {
|
||||
switch (prior, prediction) {
|
||||
| (Some(Continuous(t1)), Continuous(t2)) =>
|
||||
Continuous.T.logScoreWithPointResolution(~prediction=t2, ~answer, ~prior=t1->Some)
|
||||
| (None, Continuous(t2)) =>
|
||||
Continuous.T.logScoreWithPointResolution(~prediction=t2, ~answer, ~prior=None)
|
||||
| _ => Error(Operation.NotYetImplemented)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let pdf = (f: float, t: t) => {
|
||||
|
@ -214,9 +231,8 @@ let doN = (n, fn) => {
|
|||
}
|
||||
|
||||
let sample = (t: t): float => {
|
||||
let randomItem = Random.float(1.)
|
||||
let bar = t |> T.Integral.yToX(randomItem)
|
||||
bar
|
||||
let randomItem = Random.float(1.0)
|
||||
t |> T.Integral.yToX(randomItem)
|
||||
}
|
||||
|
||||
let isFloat = (t: t) =>
|
||||
|
@ -238,6 +254,8 @@ let operate = (distToFloatOp: Operation.distToFloatOperation, s): float =>
|
|||
| #Inv(f) => inv(f, s)
|
||||
| #Sample => sample(s)
|
||||
| #Mean => T.mean(s)
|
||||
| #Min => T.minX(s)
|
||||
| #Max => T.maxX(s)
|
||||
}
|
||||
|
||||
let toSparkline = (t: t, bucketCount): result<string, PointSetTypes.sparklineError> =>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
module KLDivergence = {
|
||||
let logFn = Js.Math.log // base e
|
||||
let integrand = (predictionElement: float, answerElement: float): result<
|
||||
float,
|
||||
Operation.Error.t,
|
||||
> =>
|
||||
// We decided that negative infinity, not an error at answerElement = 0.0, is a desirable value.
|
||||
if answerElement == 0.0 {
|
||||
Ok(0.0)
|
||||
} else if predictionElement == 0.0 {
|
||||
Ok(infinity)
|
||||
} else {
|
||||
let quot = predictionElement /. answerElement
|
||||
quot < 0.0 ? Error(Operation.ComplexNumberError) : Ok(-.answerElement *. logFn(quot))
|
||||
}
|
||||
}
|
||||
|
||||
module LogScoreWithPointResolution = {
|
||||
let logFn = Js.Math.log
|
||||
let score = (
|
||||
~priorPdf: option<float => float>,
|
||||
~predictionPdf: float => float,
|
||||
~answer: float,
|
||||
): result<float, Operation.Error.t> => {
|
||||
let numerator = answer->predictionPdf
|
||||
if numerator < 0.0 {
|
||||
Operation.PdfInvalidError->Error
|
||||
} else if numerator == 0.0 {
|
||||
infinity->Ok
|
||||
} else {
|
||||
-.(
|
||||
switch priorPdf {
|
||||
| None => numerator->logFn
|
||||
| Some(f) => {
|
||||
let priorDensityOfAnswer = f(answer)
|
||||
if priorDensityOfAnswer == 0.0 {
|
||||
neg_infinity
|
||||
} else {
|
||||
(numerator /. priorDensityOfAnswer)->logFn
|
||||
}
|
||||
}
|
||||
}
|
||||
)->Ok
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
@genType
|
||||
module Error = {
|
||||
@genType
|
||||
type sampleSetError = TooFewSamples | NonNumericInput(string)
|
||||
type sampleSetError =
|
||||
TooFewSamples | NonNumericInput(string) | OperationError(Operation.operationError)
|
||||
|
||||
let sampleSetErrorToString = (err: sampleSetError): string =>
|
||||
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)
|
||||
}
|
||||
|
||||
@genType
|
||||
|
@ -16,6 +18,16 @@ module Error = {
|
|||
switch err {
|
||||
| TooFewSamplesForConversionToPointSet => "Too Few Samples to convert to point set"
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -83,22 +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<
|
||||
t,
|
||||
sampleSetError,
|
||||
> => T.get(t)->E.A2.fmap(fn)->_fromSampleResultArray
|
||||
|
||||
//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<
|
||||
t,
|
||||
Operation.Error.t,
|
||||
> => {
|
||||
let samples = Belt.Array.zip(get(t1), get(t2))->E.A2.fmap(((a, b)) => fn(a, b))
|
||||
sampleSetError,
|
||||
> => E.A.zip(get(t1), get(t2))->E.A2.fmap(E.Tuple2.toFnCall(fn))->_fromSampleResultArray
|
||||
|
||||
// This assertion should never be reached. In order for it to be reached, one
|
||||
// of the input parameters would need to be a sample set distribution with less
|
||||
// than 6 samples. Which should be impossible due to the smart constructor.
|
||||
// I could prove this to the type system (say, creating a {first: float, second: float, ..., fifth: float, rest: array<float>}
|
||||
// But doing so would take too much time, so I'll leave it as an assertion
|
||||
E.A.R.firstErrorOrOpen(samples)->E.R2.fmap(x =>
|
||||
E.R.toExn("Input of samples should be larger than 5", make(x))
|
||||
)
|
||||
}
|
||||
let map3 = (
|
||||
~fn: (float, float, float) => result<float, Operation.Error.t>,
|
||||
~t1: t,
|
||||
~t2: t,
|
||||
~t3: t,
|
||||
): result<t, sampleSetError> =>
|
||||
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 geomean = t => T.get(t)->E.A.Floats.geomean
|
||||
|
|
|
@ -216,6 +216,50 @@ module Uniform = {
|
|||
}
|
||||
}
|
||||
|
||||
module Logistic = {
|
||||
type t = logistic
|
||||
let make = (location, scale) =>
|
||||
scale > 0.0
|
||||
? Ok(#Logistic({location: location, scale: scale}))
|
||||
: Error("Scale must be positive")
|
||||
|
||||
let pdf = (x, t: t) => Stdlib.Logistic.pdf(x, t.location, t.scale)
|
||||
let cdf = (x, t: t) => Stdlib.Logistic.cdf(x, t.location, t.scale)
|
||||
let inv = (p, t: t) => Stdlib.Logistic.quantile(p, t.location, t.scale)
|
||||
let sample = (t: t) => {
|
||||
let s = Uniform.sample({low: 0.0, high: 1.0})
|
||||
inv(s, t)
|
||||
}
|
||||
let mean = (t: t) => Ok(Stdlib.Logistic.mean(t.location, t.scale))
|
||||
let toString = ({location, scale}: t) => j`Logistic($location,$scale)`
|
||||
}
|
||||
|
||||
module Bernoulli = {
|
||||
type t = bernoulli
|
||||
let make = p =>
|
||||
p >= 0.0 && p <= 1.0
|
||||
? Ok(#Bernoulli({p: p}))
|
||||
: Error("Bernoulli parameter must be between 0 and 1")
|
||||
let pmf = (x, t: t) => Stdlib.Bernoulli.pmf(x, t.p)
|
||||
|
||||
//Bernoulli is a discrete distribution, so it doesn't really have a pdf().
|
||||
//We fake this for now with the pmf function, but this should be fixed at some point.
|
||||
let pdf = (x, t: t) => Stdlib.Bernoulli.pmf(x, t.p)
|
||||
let cdf = (x, t: t) => Stdlib.Bernoulli.cdf(x, t.p)
|
||||
let inv = (p, t: t) => Stdlib.Bernoulli.quantile(p, t.p)
|
||||
let mean = (t: t) => Ok(Stdlib.Bernoulli.mean(t.p))
|
||||
let min = (t: t) => t.p == 1.0 ? 1.0 : 0.0
|
||||
let max = (t: t) => t.p == 0.0 ? 0.0 : 1.0
|
||||
let sample = (t: t) => {
|
||||
let s = Uniform.sample({low: 0.0, high: 1.0})
|
||||
inv(s, t)
|
||||
}
|
||||
let toString = ({p}: t) => j`Bernoulli($p)`
|
||||
let toPointSetDist = ({p}: t): PointSetTypes.pointSetDist => Discrete(
|
||||
Discrete.make(~integralSumCache=Some(1.0), {xs: [0.0, 1.0], ys: [1.0 -. p, p]}),
|
||||
)
|
||||
}
|
||||
|
||||
module Gamma = {
|
||||
type t = gamma
|
||||
let make = (shape: float, scale: float) => {
|
||||
|
@ -252,6 +296,9 @@ module Float = {
|
|||
let mean = (t: t) => Ok(t)
|
||||
let sample = (t: t) => t
|
||||
let toString = (t: t) => j`Delta($t)`
|
||||
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Discrete(
|
||||
Discrete.make(~integralSumCache=Some(1.0), {xs: [t], ys: [1.0]}),
|
||||
)
|
||||
}
|
||||
|
||||
module From90thPercentile = {
|
||||
|
@ -275,9 +322,11 @@ module T = {
|
|||
| #Cauchy(n) => Cauchy.pdf(x, n)
|
||||
| #Gamma(n) => Gamma.pdf(x, n)
|
||||
| #Lognormal(n) => Lognormal.pdf(x, n)
|
||||
| #Logistic(n) => Logistic.pdf(x, n)
|
||||
| #Uniform(n) => Uniform.pdf(x, n)
|
||||
| #Beta(n) => Beta.pdf(x, n)
|
||||
| #Float(n) => Float.pdf(x, n)
|
||||
| #Bernoulli(n) => Bernoulli.pdf(x, n)
|
||||
}
|
||||
|
||||
let cdf = (x, dist) =>
|
||||
|
@ -287,10 +336,12 @@ module T = {
|
|||
| #Exponential(n) => Exponential.cdf(x, n)
|
||||
| #Cauchy(n) => Cauchy.cdf(x, n)
|
||||
| #Gamma(n) => Gamma.cdf(x, n)
|
||||
| #Logistic(n) => Logistic.cdf(x, n)
|
||||
| #Lognormal(n) => Lognormal.cdf(x, n)
|
||||
| #Uniform(n) => Uniform.cdf(x, n)
|
||||
| #Beta(n) => Beta.cdf(x, n)
|
||||
| #Float(n) => Float.cdf(x, n)
|
||||
| #Bernoulli(n) => Bernoulli.cdf(x, n)
|
||||
}
|
||||
|
||||
let inv = (x, dist) =>
|
||||
|
@ -300,10 +351,12 @@ module T = {
|
|||
| #Exponential(n) => Exponential.inv(x, n)
|
||||
| #Cauchy(n) => Cauchy.inv(x, n)
|
||||
| #Gamma(n) => Gamma.inv(x, n)
|
||||
| #Logistic(n) => Logistic.inv(x, n)
|
||||
| #Lognormal(n) => Lognormal.inv(x, n)
|
||||
| #Uniform(n) => Uniform.inv(x, n)
|
||||
| #Beta(n) => Beta.inv(x, n)
|
||||
| #Float(n) => Float.inv(x, n)
|
||||
| #Bernoulli(n) => Bernoulli.inv(x, n)
|
||||
}
|
||||
|
||||
let sample: symbolicDist => float = x =>
|
||||
|
@ -313,10 +366,12 @@ module T = {
|
|||
| #Exponential(n) => Exponential.sample(n)
|
||||
| #Cauchy(n) => Cauchy.sample(n)
|
||||
| #Gamma(n) => Gamma.sample(n)
|
||||
| #Logistic(n) => Logistic.sample(n)
|
||||
| #Lognormal(n) => Lognormal.sample(n)
|
||||
| #Uniform(n) => Uniform.sample(n)
|
||||
| #Beta(n) => Beta.sample(n)
|
||||
| #Float(n) => Float.sample(n)
|
||||
| #Bernoulli(n) => Bernoulli.sample(n)
|
||||
}
|
||||
|
||||
let doN = (n, fn) => {
|
||||
|
@ -336,10 +391,12 @@ module T = {
|
|||
| #Cauchy(n) => Cauchy.toString(n)
|
||||
| #Normal(n) => Normal.toString(n)
|
||||
| #Gamma(n) => Gamma.toString(n)
|
||||
| #Logistic(n) => Logistic.toString(n)
|
||||
| #Lognormal(n) => Lognormal.toString(n)
|
||||
| #Uniform(n) => Uniform.toString(n)
|
||||
| #Beta(n) => Beta.toString(n)
|
||||
| #Float(n) => Float.toString(n)
|
||||
| #Bernoulli(n) => Bernoulli.toString(n)
|
||||
}
|
||||
|
||||
let min: symbolicDist => float = x =>
|
||||
|
@ -349,8 +406,10 @@ module T = {
|
|||
| #Cauchy(n) => Cauchy.inv(minCdfValue, n)
|
||||
| #Normal(n) => Normal.inv(minCdfValue, n)
|
||||
| #Lognormal(n) => Lognormal.inv(minCdfValue, n)
|
||||
| #Logistic(n) => Logistic.inv(minCdfValue, n)
|
||||
| #Gamma(n) => Gamma.inv(minCdfValue, n)
|
||||
| #Uniform({low}) => low
|
||||
| #Bernoulli(n) => Bernoulli.min(n)
|
||||
| #Beta(n) => Beta.inv(minCdfValue, n)
|
||||
| #Float(n) => n
|
||||
}
|
||||
|
@ -363,7 +422,9 @@ module T = {
|
|||
| #Normal(n) => Normal.inv(maxCdfValue, n)
|
||||
| #Gamma(n) => Gamma.inv(maxCdfValue, n)
|
||||
| #Lognormal(n) => Lognormal.inv(maxCdfValue, n)
|
||||
| #Logistic(n) => Logistic.inv(maxCdfValue, n)
|
||||
| #Beta(n) => Beta.inv(maxCdfValue, n)
|
||||
| #Bernoulli(n) => Bernoulli.max(n)
|
||||
| #Uniform({high}) => high
|
||||
| #Float(n) => n
|
||||
}
|
||||
|
@ -376,8 +437,10 @@ module T = {
|
|||
| #Normal(n) => Normal.mean(n)
|
||||
| #Lognormal(n) => Lognormal.mean(n)
|
||||
| #Beta(n) => Beta.mean(n)
|
||||
| #Logistic(n) => Logistic.mean(n)
|
||||
| #Uniform(n) => Uniform.mean(n)
|
||||
| #Gamma(n) => Gamma.mean(n)
|
||||
| #Bernoulli(n) => Bernoulli.mean(n)
|
||||
| #Float(n) => Float.mean(n)
|
||||
}
|
||||
|
||||
|
@ -386,6 +449,8 @@ module T = {
|
|||
| #Cdf(f) => Ok(cdf(f, s))
|
||||
| #Pdf(f) => Ok(pdf(f, s))
|
||||
| #Inv(f) => Ok(inv(f, s))
|
||||
| #Min => Ok(min(s))
|
||||
| #Max => Ok(max(s))
|
||||
| #Sample => Ok(sample(s))
|
||||
| #Mean => mean(s)
|
||||
}
|
||||
|
@ -396,8 +461,9 @@ module T = {
|
|||
| (#ByWeight, #Uniform(n)) =>
|
||||
// In `ByWeight mode, uniform distributions get special treatment because we need two x's
|
||||
// on either side for proper rendering (just left and right of the discontinuities).
|
||||
let dx = 0.00001 *. (n.high -. n.low)
|
||||
[n.low -. dx, n.low +. dx, n.high -. dx, n.high +. dx]
|
||||
let distance = n.high -. n.low
|
||||
let dx = MagicNumbers.Epsilon.ten *. distance
|
||||
[n.low -. dx, n.low, n.low +. dx, n.high -. dx, n.high, n.high +. dx]
|
||||
| (#ByWeight, _) =>
|
||||
let ys = E.A.Floats.range(minCdfValue, maxCdfValue, n)
|
||||
ys |> E.A.fmap(y => inv(y, dist))
|
||||
|
@ -452,7 +518,8 @@ module T = {
|
|||
d: symbolicDist,
|
||||
): PointSetTypes.pointSetDist =>
|
||||
switch d {
|
||||
| #Float(v) => Discrete(Discrete.make(~integralSumCache=Some(1.0), {xs: [v], ys: [1.0]}))
|
||||
| #Float(v) => Float.toPointSetDist(v)
|
||||
| #Bernoulli(v) => Bernoulli.toPointSetDist(v)
|
||||
| _ =>
|
||||
let xs = interpolateXs(~xSelection, d, sampleCount)
|
||||
let ys = xs |> E.A.fmap(x => pdf(x, d))
|
||||
|
|
|
@ -36,6 +36,13 @@ type gamma = {
|
|||
scale: float,
|
||||
}
|
||||
|
||||
type logistic = {
|
||||
location: float,
|
||||
scale: float,
|
||||
}
|
||||
|
||||
type bernoulli = {p: float}
|
||||
|
||||
@genType
|
||||
type symbolicDist = [
|
||||
| #Normal(normal)
|
||||
|
@ -47,6 +54,8 @@ type symbolicDist = [
|
|||
| #Triangular(triangular)
|
||||
| #Gamma(gamma)
|
||||
| #Float(float)
|
||||
| #Bernoulli(bernoulli)
|
||||
| #Logistic(logistic)
|
||||
]
|
||||
|
||||
type analyticalSimplificationResult = [
|
||||
|
|
|
@ -0,0 +1,369 @@
|
|||
type expressionValue = ReducerInterface_ExpressionValue.expressionValue
|
||||
|
||||
/*
|
||||
Function Registry "Type". A type, without any other information.
|
||||
Like, #Float
|
||||
*/
|
||||
type rec frType =
|
||||
| FRTypeNumber
|
||||
| FRTypeNumeric
|
||||
| FRTypeDistOrNumber
|
||||
| FRTypeLambda
|
||||
| FRTypeRecord(frTypeRecord)
|
||||
| FRTypeDict(frType)
|
||||
| FRTypeArray(frType)
|
||||
| FRTypeString
|
||||
| FRTypeAny
|
||||
| FRTypeVariant(array<string>)
|
||||
and frTypeRecord = array<frTypeRecordParam>
|
||||
and frTypeRecordParam = (string, frType)
|
||||
|
||||
/*
|
||||
Function Registry "Value". A type, with the information of that type.
|
||||
Like, #Float(40.0)
|
||||
*/
|
||||
type rec frValue =
|
||||
| FRValueNumber(float)
|
||||
| FRValueDist(DistributionTypes.genericDist)
|
||||
| FRValueArray(array<frValue>)
|
||||
| FRValueDistOrNumber(frValueDistOrNumber)
|
||||
| FRValueRecord(frValueRecord)
|
||||
| FRValueLambda(ReducerInterface_ExpressionValue.lambdaValue)
|
||||
| FRValueString(string)
|
||||
| FRValueVariant(string)
|
||||
| FRValueAny(frValue)
|
||||
| FRValueDict(Js.Dict.t<frValue>)
|
||||
and frValueRecord = array<frValueRecordParam>
|
||||
and frValueRecordParam = (string, frValue)
|
||||
and frValueDictParam = (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>
|
||||
|
||||
module FRType = {
|
||||
type t = frType
|
||||
let rec toString = (t: t) =>
|
||||
switch t {
|
||||
| FRTypeNumber => "number"
|
||||
| FRTypeNumeric => "numeric"
|
||||
| FRTypeDistOrNumber => "frValueDistOrNumber"
|
||||
| FRTypeRecord(r) => {
|
||||
let input = ((name, frType): frTypeRecordParam) => `${name}: ${toString(frType)}`
|
||||
`record({${r->E.A2.fmap(input)->E.A2.joinWith(", ")}})`
|
||||
}
|
||||
| FRTypeArray(r) => `list(${toString(r)})`
|
||||
| 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> =>
|
||||
switch (t, r) {
|
||||
| (FRTypeAny, f) => toFrValue(f)
|
||||
| (FRTypeString, EvString(f)) => Some(FRValueString(f))
|
||||
| (FRTypeNumber, EvNumber(f)) => Some(FRValueNumber(f))
|
||||
| (FRTypeDistOrNumber, EvNumber(f)) => Some(FRValueDistOrNumber(FRValueNumber(f)))
|
||||
| (FRTypeDistOrNumber, EvDistribution(Symbolic(#Float(f)))) =>
|
||||
Some(FRValueDistOrNumber(FRValueNumber(f)))
|
||||
| (FRTypeDistOrNumber, EvDistribution(f)) => Some(FRValueDistOrNumber(FRValueDist(f)))
|
||||
| (FRTypeNumeric, EvNumber(f)) => Some(FRValueNumber(f))
|
||||
| (FRTypeNumeric, EvDistribution(Symbolic(#Float(f)))) => Some(FRValueNumber(f))
|
||||
| (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)) => {
|
||||
let getAndMatch = (name, input) =>
|
||||
E.Dict.get(record, name)->E.O.bind(matchWithExpressionValue(input))
|
||||
//All names in the type must be present. If any are missing, the corresponding
|
||||
//value will be None, and this function would return None.
|
||||
let namesAndValues: array<option<(Js.Dict.key, frValue)>> =
|
||||
recordParams->E.A2.fmap(((name, input)) =>
|
||||
getAndMatch(name, input)->E.O2.fmap(match => (name, match))
|
||||
)
|
||||
namesAndValues->E.A.O.openIfAllSome->E.O2.fmap(r => FRValueRecord(r))
|
||||
}
|
||||
| _ => 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<
|
||||
array<frValue>,
|
||||
> => {
|
||||
let isSameLength = E.A.length(inputs) == E.A.length(args)
|
||||
if !isSameLength {
|
||||
None
|
||||
} else {
|
||||
E.A.zip(inputs, args)
|
||||
->E.A2.fmap(((input, arg)) => matchWithExpressionValue(input, arg))
|
||||
->E.A.O.openIfAllSome
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This module, Matcher, is fairly lengthy. However, only two functions from it
|
||||
are meant to be used outside of it. These are findMatches and matchToDef in Matches.Registry.
|
||||
The rest of it is just called from those two functions.
|
||||
*/
|
||||
module Matcher = {
|
||||
module MatchSimple = {
|
||||
type t = DifferentName | SameNameDifferentArguments | FullMatch
|
||||
|
||||
let isFullMatch = (match: t) =>
|
||||
switch match {
|
||||
| FullMatch => true
|
||||
| _ => false
|
||||
}
|
||||
|
||||
let isNameMatchOnly = (match: t) =>
|
||||
switch match {
|
||||
| SameNameDifferentArguments => true
|
||||
| _ => false
|
||||
}
|
||||
}
|
||||
|
||||
module Match = {
|
||||
type t<'a, 'b> = DifferentName | SameNameDifferentArguments('a) | FullMatch('b)
|
||||
|
||||
let isFullMatch = (match: t<'a, 'b>): bool =>
|
||||
switch match {
|
||||
| FullMatch(_) => true
|
||||
| _ => false
|
||||
}
|
||||
|
||||
let isNameMatchOnly = (match: t<'a, 'b>) =>
|
||||
switch match {
|
||||
| SameNameDifferentArguments(_) => true
|
||||
| _ => false
|
||||
}
|
||||
}
|
||||
|
||||
module FnDefinition = {
|
||||
let matchAssumingSameName = (f: fnDefinition, args: array<expressionValue>) => {
|
||||
switch FRType.matchWithExpressionValueArray(f.inputs, args) {
|
||||
| Some(_) => MatchSimple.FullMatch
|
||||
| None => MatchSimple.SameNameDifferentArguments
|
||||
}
|
||||
}
|
||||
|
||||
let match = (f: fnDefinition, fnName: string, args: array<expressionValue>) => {
|
||||
if f.name !== fnName {
|
||||
MatchSimple.DifferentName
|
||||
} else {
|
||||
matchAssumingSameName(f, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module Function = {
|
||||
type definitionId = int
|
||||
type match = Match.t<array<definitionId>, definitionId>
|
||||
|
||||
let match = (f: function, fnName: string, args: array<expressionValue>): match => {
|
||||
let matchedDefinition = () =>
|
||||
E.A.getIndexBy(f.definitions, r =>
|
||||
MatchSimple.isFullMatch(FnDefinition.match(r, fnName, args))
|
||||
) |> E.O.fmap(r => Match.FullMatch(r))
|
||||
let getMatchedNameOnlyDefinition = () => {
|
||||
let nameMatchIndexes =
|
||||
f.definitions
|
||||
->E.A2.fmapi((index, r) =>
|
||||
MatchSimple.isNameMatchOnly(FnDefinition.match(r, fnName, args)) ? Some(index) : None
|
||||
)
|
||||
->E.A.O.concatSomes
|
||||
switch nameMatchIndexes {
|
||||
| [] => None
|
||||
| elements => Some(Match.SameNameDifferentArguments(elements))
|
||||
}
|
||||
}
|
||||
|
||||
E.A.O.firstSomeFnWithDefault(
|
||||
[matchedDefinition, getMatchedNameOnlyDefinition],
|
||||
Match.DifferentName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module RegistryMatch = {
|
||||
type match = {
|
||||
fnName: string,
|
||||
inputIndex: int,
|
||||
}
|
||||
let makeMatch = (fnName: string, inputIndex: int) => {fnName: fnName, inputIndex: inputIndex}
|
||||
}
|
||||
|
||||
module Registry = {
|
||||
let _findExactMatches = (r: registry, fnName: string, args: array<expressionValue>) => {
|
||||
let functionMatchPairs = r->E.A2.fmap(l => (l, Function.match(l, fnName, args)))
|
||||
let fullMatch = functionMatchPairs->E.A.getBy(((_, match)) => Match.isFullMatch(match))
|
||||
fullMatch->E.O.bind(((fn, match)) =>
|
||||
switch match {
|
||||
| FullMatch(index) => Some(RegistryMatch.makeMatch(fn.name, index))
|
||||
| _ => None
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let _findNameMatches = (r: registry, fnName: string, args: array<expressionValue>) => {
|
||||
let functionMatchPairs = r->E.A2.fmap(l => (l, Function.match(l, fnName, args)))
|
||||
let getNameMatches =
|
||||
functionMatchPairs
|
||||
->E.A2.fmap(((fn, match)) => Match.isNameMatchOnly(match) ? Some((fn, match)) : None)
|
||||
->E.A.O.concatSomes
|
||||
let matches =
|
||||
getNameMatches
|
||||
->E.A2.fmap(((fn, match)) =>
|
||||
switch match {
|
||||
| SameNameDifferentArguments(indexes) =>
|
||||
indexes->E.A2.fmap(index => RegistryMatch.makeMatch(fn.name, index))
|
||||
| _ => []
|
||||
}
|
||||
)
|
||||
->Belt.Array.concatMany
|
||||
E.A.toNoneIfEmpty(matches)
|
||||
}
|
||||
|
||||
let findMatches = (r: registry, fnName: string, args: array<expressionValue>) => {
|
||||
switch _findExactMatches(r, fnName, args) {
|
||||
| Some(r) => Match.FullMatch(r)
|
||||
| None =>
|
||||
switch _findNameMatches(r, fnName, args) {
|
||||
| Some(r) => Match.SameNameDifferentArguments(r)
|
||||
| None => Match.DifferentName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let matchToDef = (registry: registry, {fnName, inputIndex}: RegistryMatch.match): option<
|
||||
fnDefinition,
|
||||
> =>
|
||||
registry
|
||||
->E.A.getBy(fn => fn.name === fnName)
|
||||
->E.O.bind(fn => E.A.get(fn.definitions, inputIndex))
|
||||
}
|
||||
}
|
||||
|
||||
module FnDefinition = {
|
||||
type t = fnDefinition
|
||||
|
||||
let toString = (t: t) => {
|
||||
let inputs = t.inputs->E.A2.fmap(FRType.toString)->E.A2.joinWith(", ")
|
||||
t.name ++ `(${inputs})`
|
||||
}
|
||||
|
||||
let run = (t: t, args: array<expressionValue>, env: DistributionOperation.env) => {
|
||||
let argValues = FRType.matchWithExpressionValueArray(t.inputs, args)
|
||||
switch argValues {
|
||||
| Some(values) => t.run(values, env)
|
||||
| None => Error("Incorrect Types")
|
||||
}
|
||||
}
|
||||
|
||||
let make = (~name, ~inputs, ~run): t => {
|
||||
name: name,
|
||||
inputs: inputs,
|
||||
run: run,
|
||||
}
|
||||
}
|
||||
|
||||
module Function = {
|
||||
type t = function
|
||||
|
||||
let make = (~name, ~definitions): t => {
|
||||
name: name,
|
||||
definitions: definitions,
|
||||
}
|
||||
}
|
||||
|
||||
module Registry = {
|
||||
/*
|
||||
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
|
||||
called. However, for now, we could just call the registry last.
|
||||
*/
|
||||
let matchAndRun = (
|
||||
~registry: registry,
|
||||
~fnName: string,
|
||||
~args: array<expressionValue>,
|
||||
~env: DistributionOperation.env,
|
||||
) => {
|
||||
let matchToDef = m => Matcher.Registry.matchToDef(registry, m)
|
||||
let showNameMatchDefinitions = matches => {
|
||||
let defs =
|
||||
matches
|
||||
->E.A2.fmap(matchToDef)
|
||||
->E.A.O.concatSomes
|
||||
->E.A2.fmap(FnDefinition.toString)
|
||||
->E.A2.fmap(r => `[${r}]`)
|
||||
->E.A2.joinWith("; ")
|
||||
`There are function matches for ${fnName}(), but with different arguments: ${defs}`
|
||||
}
|
||||
switch Matcher.Registry.findMatches(registry, fnName, args) {
|
||||
| Matcher.Match.FullMatch(match) => match->matchToDef->E.O2.fmap(FnDefinition.run(_, args, env))
|
||||
| SameNameDifferentArguments(m) => Some(Error(showNameMatchDefinitions(m)))
|
||||
| _ => None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
open FunctionRegistry_Core
|
||||
|
||||
let impossibleError = "Wrong inputs / Logically impossible"
|
||||
|
||||
module Wrappers = {
|
||||
let symbolic = r => DistributionTypes.Symbolic(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 getOrError = (a, g) => E.A.get(a, g) |> E.O.toResult(impossibleError)
|
||||
|
||||
module Prepare = {
|
||||
type t = frValue
|
||||
type ts = array<frValue>
|
||||
type err = string
|
||||
|
||||
module ToValueArray = {
|
||||
module Record = {
|
||||
let twoArgs = (inputs: ts): result<ts, err> =>
|
||||
switch inputs {
|
||||
| [FRValueRecord([(_, n1), (_, n2)])] => Ok([n1, n2])
|
||||
| _ => 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module ToValueTuple = {
|
||||
let twoDistOrNumber = (values: ts): result<(frValueDistOrNumber, frValueDistOrNumber), err> => {
|
||||
switch values {
|
||||
| [FRValueDistOrNumber(a1), FRValueDistOrNumber(a2)] => Ok(a1, a2)
|
||||
| _ => Error(impossibleError)
|
||||
}
|
||||
}
|
||||
|
||||
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> => {
|
||||
switch values {
|
||||
| [FRValueDistOrNumber(a1)] => Ok(a1)
|
||||
| _ => Error(impossibleError)
|
||||
}
|
||||
}
|
||||
|
||||
module Record = {
|
||||
let twoDistOrNumber = (values: ts): result<(frValueDistOrNumber, frValueDistOrNumber), err> =>
|
||||
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 DistOrNumberToDist = {
|
||||
module Helpers = {
|
||||
let toSampleSet = (r, env: DistributionOperation.env) =>
|
||||
GenericDist.toSampleSetDist(r, env.sampleCount)
|
||||
|
||||
let mapFnResult = r =>
|
||||
switch r {
|
||||
| Ok(r) => Ok(GenericDist.sample(r))
|
||||
| Error(r) => Error(Operation.Other(r))
|
||||
}
|
||||
|
||||
let wrapSymbolic = (fn, r) => r->fn->E.R2.fmap(Wrappers.symbolic)
|
||||
|
||||
let singleVarSample = (dist, fn, env) => {
|
||||
switch toSampleSet(dist, env) {
|
||||
| Ok(dist) =>
|
||||
switch SampleSetDist.samplesMap(~fn=f => fn(f)->mapFnResult, dist) {
|
||||
| Ok(r) => Ok(DistributionTypes.SampleSet(r))
|
||||
| Error(r) => Error(DistributionTypes.Error.toString(DistributionTypes.SampleSetError(r)))
|
||||
}
|
||||
| Error(r) => Error(DistributionTypes.Error.toString(r))
|
||||
}
|
||||
}
|
||||
|
||||
let twoVarSample = (dist1, dist2, fn, env) => {
|
||||
let altFn = (a, b) => fn((a, b))->mapFnResult
|
||||
switch E.R.merge(toSampleSet(dist1, env), toSampleSet(dist2, env)) {
|
||||
| Ok((t1, t2)) =>
|
||||
switch SampleSetDist.map2(~fn=altFn, ~t1, ~t2) {
|
||||
| Ok(r) => Ok(DistributionTypes.SampleSet(r))
|
||||
| Error(r) => Error(SampleSetDist.Error.toString(r))
|
||||
}
|
||||
| Error(r) => Error(DistributionTypes.Error.toString(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let oneValue = (
|
||||
~fn: float => result<DistributionTypes.genericDist, string>,
|
||||
~value: frValueDistOrNumber,
|
||||
~env: DistributionOperation.env,
|
||||
): result<DistributionTypes.genericDist, string> => {
|
||||
switch value {
|
||||
| FRValueNumber(a1) => fn(a1)
|
||||
| FRValueDist(a1) => Helpers.singleVarSample(a1, r => fn(r), env)
|
||||
}
|
||||
}
|
||||
|
||||
let oneValueUsingSymbolicDist = (~fn, ~value) => oneValue(~fn=Helpers.wrapSymbolic(fn), ~value)
|
||||
|
||||
let twoValues = (
|
||||
~fn: ((float, float)) => result<DistributionTypes.genericDist, string>,
|
||||
~values: (frValueDistOrNumber, frValueDistOrNumber),
|
||||
~env: DistributionOperation.env,
|
||||
): result<DistributionTypes.genericDist, string> => {
|
||||
switch values {
|
||||
| (FRValueNumber(a1), FRValueNumber(a2)) => fn((a1, a2))
|
||||
| (FRValueDist(a1), FRValueNumber(a2)) => Helpers.singleVarSample(a1, r => fn((r, a2)), env)
|
||||
| (FRValueNumber(a1), FRValueDist(a2)) => Helpers.singleVarSample(a2, r => fn((a1, r)), env)
|
||||
| (FRValueDist(a1), FRValueDist(a2)) => Helpers.twoVarSample(a1, a2, fn, env)
|
||||
}
|
||||
}
|
||||
|
||||
let twoValuesUsingSymbolicDist = (~fn, ~values) =>
|
||||
twoValues(~fn=Helpers.wrapSymbolic(fn), ~values)
|
||||
}
|
||||
}
|
||||
|
||||
module TwoArgDist = {
|
||||
let process = (~fn, ~env, r) =>
|
||||
r
|
||||
->E.R.bind(Process.DistOrNumberToDist.twoValuesUsingSymbolicDist(~fn, ~values=_, ~env))
|
||||
->E.R2.fmap(Wrappers.evDistribution)
|
||||
|
||||
let make = (name, fn) => {
|
||||
FnDefinition.make(~name, ~inputs=[FRTypeDistOrNumber, FRTypeDistOrNumber], ~run=(inputs, env) =>
|
||||
inputs->Prepare.ToValueTuple.twoDistOrNumber->process(~fn, ~env)
|
||||
)
|
||||
}
|
||||
|
||||
let makeRecordP5P95 = (name, fn) => {
|
||||
FnDefinition.make(
|
||||
~name,
|
||||
~inputs=[FRTypeRecord([("p5", FRTypeDistOrNumber), ("p95", FRTypeDistOrNumber)])],
|
||||
~run=(inputs, env) => inputs->Prepare.ToValueTuple.Record.twoDistOrNumber->process(~fn, ~env),
|
||||
)
|
||||
}
|
||||
|
||||
let makeRecordMeanStdev = (name, fn) => {
|
||||
FnDefinition.make(
|
||||
~name,
|
||||
~inputs=[FRTypeRecord([("mean", FRTypeDistOrNumber), ("stdev", FRTypeDistOrNumber)])],
|
||||
~run=(inputs, env) => inputs->Prepare.ToValueTuple.Record.twoDistOrNumber->process(~fn, ~env),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module OneArgDist = {
|
||||
let process = (~fn, ~env, r) =>
|
||||
r
|
||||
->E.R.bind(Process.DistOrNumberToDist.oneValueUsingSymbolicDist(~fn, ~value=_, ~env))
|
||||
->E.R2.fmap(Wrappers.evDistribution)
|
||||
|
||||
let make = (name, fn) =>
|
||||
FnDefinition.make(~name, ~inputs=[FRTypeDistOrNumber], ~run=(inputs, 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)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
open FunctionRegistry_Core
|
||||
open FunctionRegistry_Helpers
|
||||
|
||||
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 = [
|
||||
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(
|
||||
~name="Normal",
|
||||
~definitions=[
|
||||
TwoArgDist.make("normal", twoArgs(SymbolicDist.Normal.make)),
|
||||
TwoArgDist.makeRecordP5P95("normal", r =>
|
||||
twoArgs(SymbolicDist.Normal.from90PercentCI, r)->Ok
|
||||
),
|
||||
TwoArgDist.makeRecordMeanStdev("normal", twoArgs(SymbolicDist.Normal.make)),
|
||||
],
|
||||
),
|
||||
Function.make(
|
||||
~name="Lognormal",
|
||||
~definitions=[
|
||||
TwoArgDist.make("lognormal", twoArgs(SymbolicDist.Lognormal.make)),
|
||||
TwoArgDist.makeRecordP5P95("lognormal", r =>
|
||||
twoArgs(SymbolicDist.Lognormal.from90PercentCI, r)->Ok
|
||||
),
|
||||
TwoArgDist.makeRecordMeanStdev("lognormal", twoArgs(SymbolicDist.Lognormal.fromMeanAndStdev)),
|
||||
],
|
||||
),
|
||||
Function.make(
|
||||
~name="Uniform",
|
||||
~definitions=[TwoArgDist.make("uniform", twoArgs(SymbolicDist.Uniform.make))],
|
||||
),
|
||||
Function.make(
|
||||
~name="Beta",
|
||||
~definitions=[TwoArgDist.make("beta", twoArgs(SymbolicDist.Beta.make))],
|
||||
),
|
||||
Function.make(
|
||||
~name="Cauchy",
|
||||
~definitions=[TwoArgDist.make("cauchy", twoArgs(SymbolicDist.Cauchy.make))],
|
||||
),
|
||||
Function.make(
|
||||
~name="Gamma",
|
||||
~definitions=[TwoArgDist.make("gamma", twoArgs(SymbolicDist.Gamma.make))],
|
||||
),
|
||||
Function.make(
|
||||
~name="Logistic",
|
||||
~definitions=[TwoArgDist.make("logistic", twoArgs(SymbolicDist.Logistic.make))],
|
||||
),
|
||||
Function.make(
|
||||
~name="To",
|
||||
~definitions=[
|
||||
TwoArgDist.make("to", twoArgs(SymbolicDist.From90thPercentile.make)),
|
||||
TwoArgDist.make(
|
||||
"credibleIntervalToDistribution",
|
||||
twoArgs(SymbolicDist.From90thPercentile.make),
|
||||
),
|
||||
],
|
||||
),
|
||||
Function.make(
|
||||
~name="Exponential",
|
||||
~definitions=[OneArgDist.make("exponential", SymbolicDist.Exponential.make)],
|
||||
),
|
||||
Function.make(
|
||||
~name="Bernoulli",
|
||||
~definitions=[OneArgDist.make("bernoulli", SymbolicDist.Bernoulli.make)],
|
||||
),
|
||||
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
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,46 @@
|
|||
# Function Registry
|
||||
|
||||
The function registry is a library for organizing function definitions.
|
||||
|
||||
The main interface is fairly constrained. Basically, write functions like the following, and add them to a big array.
|
||||
|
||||
```rescript
|
||||
Function.make(
|
||||
~name="Normal",
|
||||
~definitions=[
|
||||
FnDefinition.make(
|
||||
~name="Normal",
|
||||
~definitions=[
|
||||
FnDefinition.make(~name="normal", ~inputs=[FRTypeDistOrNumber, FRTypeDistOrNumber], ~run=(
|
||||
inputs,
|
||||
env,
|
||||
) =>
|
||||
inputs
|
||||
->Prepare.ToValueTuple.twoDistOrNumber
|
||||
->E.R.bind(
|
||||
Process.twoDistsOrNumbersToDistUsingSymbolicDist(
|
||||
~fn=E.Tuple2.toFnCall(SymbolicDist.Normal.make),
|
||||
~env,
|
||||
~values=_,
|
||||
),
|
||||
)
|
||||
->E.R2.fmap(Wrappers.evDistribution)
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
The Function name is just there for future documentation. The function defintions
|
||||
|
||||
## Key Files
|
||||
|
||||
**FunctionRegistry_Core**
|
||||
Key types, internal functionality, and a `Registry` module with a `matchAndRun` function to call function definitions.
|
||||
|
||||
**FunctionRegistry_Library**
|
||||
A list of all the Functions defined in the Function Registry.
|
||||
|
||||
**FunctionRegistry_Helpers**
|
||||
A list of helper functions for the FunctionRegistry_Library.
|
|
@ -6,11 +6,13 @@ module Math = {
|
|||
module Epsilon = {
|
||||
let ten = 1e-10
|
||||
let seven = 1e-7
|
||||
let five = 1e-5
|
||||
}
|
||||
|
||||
module Environment = {
|
||||
let defaultXYPointLength = 1000
|
||||
let defaultSampleCount = 10000
|
||||
let sparklineLength = 20
|
||||
}
|
||||
|
||||
module OpCost = {
|
||||
|
|
|
@ -13,6 +13,8 @@ open Reducer_ErrorValue
|
|||
DO NOT try to add external function mapping here!
|
||||
*/
|
||||
|
||||
//TODO: pow to xor
|
||||
|
||||
exception TestRescriptException
|
||||
|
||||
let callInternal = (call: functionCall, environment, reducer: ExpressionT.reducerFn): result<
|
||||
|
@ -50,6 +52,16 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
|
|||
| 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) => {
|
||||
Js.log(value->toString)
|
||||
value->Ok
|
||||
|
@ -72,6 +84,40 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
|
|||
->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 doKeepArray = (aValueArray, aLambdaValue) => {
|
||||
|
@ -99,6 +145,36 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
|
|||
rMappedList->Result.map(mappedList => mappedList->Belt.List.toArray->EvArray)
|
||||
}
|
||||
|
||||
module SampleMap = {
|
||||
type t = SampleSetDist.t
|
||||
let doLambdaCall = (aLambdaValue, list) =>
|
||||
switch Lambda.doLambdaCall(aLambdaValue, list, environment, reducer) {
|
||||
| Ok(EvNumber(f)) => Ok(f)
|
||||
| _ => Error(Operation.SampleMapNeedsNtoNFunction)
|
||||
}
|
||||
|
||||
let toType = r =>
|
||||
switch r {
|
||||
| Ok(r) => Ok(EvDistribution(SampleSet(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) => {
|
||||
aValueArray->Belt.Array.reduce(Ok(initialValue), (rAcc, elem) =>
|
||||
rAcc->Result.flatMap(acc =>
|
||||
|
@ -115,25 +191,135 @@ 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 {
|
||||
| ("$atIndex", [EvArray(aValueArray), EvArray([EvNumber(fIndex)])]) =>
|
||||
arrayAtIndex(aValueArray, fIndex)
|
||||
| ("$atIndex", [EvRecord(dict), EvArray([EvString(sIndex)])]) => recordAtIndex(dict, sIndex)
|
||||
| ("$constructRecord", [EvArray(arrayOfPairs)]) => constructRecord(arrayOfPairs)
|
||||
| ("$exportBindings", [EvRecord(externalBindings)]) => doExportBindings(externalBindings)
|
||||
| ("$setBindings", [EvRecord(externalBindings), EvSymbol(symbol), value]) =>
|
||||
| ("$_atIndex_$", [EvArray(aValueArray), EvNumber(fIndex)]) => arrayAtIndex(aValueArray, fIndex)
|
||||
| ("$_atIndex_$", [EvRecord(dict), EvString(sIndex)]) => recordAtIndex(dict, sIndex)
|
||||
| ("$_constructArray_$", [EvArray(aValueArray)]) => EvArray(aValueArray)->Ok
|
||||
| ("$_constructRecord_$", [EvArray(arrayOfPairs)]) => constructRecord(arrayOfPairs)
|
||||
| ("$_exportBindings_$", [EvRecord(externalBindings)]) => doExportBindings(externalBindings)
|
||||
| ("$_setBindings_$", [EvRecord(externalBindings), EvSymbol(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)
|
||||
| ("add", [EvArray(aValueArray), EvArray(bValueArray)]) => doAddArray(aValueArray, bValueArray)
|
||||
| ("add", [EvString(aValueString), EvString(bValueString)]) =>
|
||||
doAddString(aValueString, bValueString)
|
||||
| ("inspect", [value, EvString(label)]) => inspectLabel(value, label)
|
||||
| ("inspect", [value]) => inspect(value)
|
||||
| ("keep", [EvArray(aValueArray), EvLambda(aLambdaValue)]) =>
|
||||
doKeepArray(aValueArray, aLambdaValue)
|
||||
| ("map", [EvArray(aValueArray), EvLambda(aLambdaValue)]) => doMapArray(aValueArray, aLambdaValue)
|
||||
| ("mapSamples", [EvDistribution(SampleSet(dist)), EvLambda(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)]) =>
|
||||
doReduceArray(aValueArray, initialValue, aLambdaValue)
|
||||
| ("reduceReverse", [EvArray(aValueArray), initialValue, EvLambda(aLambdaValue)]) =>
|
||||
doReduceReverseArray(aValueArray, initialValue, aLambdaValue)
|
||||
| ("reverse", [EvArray(aValueArray)]) => aValueArray->Belt.Array.reverse->EvArray->Ok
|
||||
| call => callMathJs(call)
|
||||
| (_, [EvBool(_)])
|
||||
| (_, [EvNumber(_)])
|
||||
| (_, [EvString(_)])
|
||||
| (_, [EvBool(_), EvBool(_)])
|
||||
| (_, [EvNumber(_), EvNumber(_)])
|
||||
| (_, [EvString(_), EvString(_)]) =>
|
||||
callMathJs(call)
|
||||
| call =>
|
||||
Error(REFunctionNotFound(call->functionCallToCallSignature->functionCallSignatureToString)) // Report full type signature as error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
Macros are used to define language building blocks. They are like Lisp macros.
|
||||
*/
|
||||
module Bindings = Reducer_Expression_Bindings
|
||||
module ErrorValue = Reducer_ErrorValue
|
||||
module ExpressionBuilder = Reducer_Expression_ExpressionBuilder
|
||||
module ExpressionT = Reducer_Expression_T
|
||||
module ExpressionValue = ReducerInterface.ExpressionValue
|
||||
module ExpressionWithContext = Reducer_ExpressionWithContext
|
||||
|
@ -11,7 +13,7 @@ module Result = Belt.Result
|
|||
open Reducer_Expression_ExpressionBuilder
|
||||
|
||||
type environment = ExpressionValue.environment
|
||||
type errorValue = Reducer_ErrorValue.errorValue
|
||||
type errorValue = ErrorValue.errorValue
|
||||
type expression = ExpressionT.expression
|
||||
type expressionValue = ExpressionValue.expressionValue
|
||||
type expressionWithContext = ExpressionWithContext.expressionWithContext
|
||||
|
@ -22,84 +24,78 @@ let dispatchMacroCall = (
|
|||
environment,
|
||||
reduceExpression: ExpressionT.reducerFn,
|
||||
): result<expressionWithContext, errorValue> => {
|
||||
let doBindStatement = (bindingExpr: expression, statement: expression, environment) =>
|
||||
switch statement {
|
||||
| ExpressionT.EList(list{ExpressionT.EValue(EvCall("$let")), symbolExpr, statement}) => {
|
||||
let rExternalBindingsValue = reduceExpression(bindingExpr, bindings, environment)
|
||||
let useExpressionToSetBindings = (bindingExpr: expression, environment, statement, newCode) => {
|
||||
let rExternalBindingsValue = reduceExpression(bindingExpr, bindings, environment)
|
||||
|
||||
rExternalBindingsValue->Result.flatMap(externalBindingsValue => {
|
||||
let newBindings = Bindings.fromValue(externalBindingsValue)
|
||||
rExternalBindingsValue->Result.flatMap(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)
|
||||
rNewStatement->Result.map(boundStatement =>
|
||||
ExpressionWithContext.withContext(
|
||||
newCode(newBindings->Bindings.toExternalBindings->eRecord, boundStatement),
|
||||
newBindings,
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let rNewStatement = Bindings.replaceSymbols(newBindings, statement)
|
||||
rNewStatement->Result.map(newStatement =>
|
||||
ExpressionWithContext.withContext(
|
||||
eFunction(
|
||||
"$setBindings",
|
||||
list{newBindings->Bindings.toExternalBindings->eRecord, symbolExpr, newStatement},
|
||||
),
|
||||
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<
|
||||
expressionWithContext,
|
||||
errorValue,
|
||||
> =>
|
||||
switch statement {
|
||||
| ExpressionT.EList(list{ExpressionT.EValue(EvCall("$let")), symbolExpr, statement}) => {
|
||||
let rExternalBindingsValue = reduceExpression(bindingExpr, bindings, environment)
|
||||
> => {
|
||||
let defaultStatement = () =>
|
||||
useExpressionToSetBindings(bindingExpr, environment, statement, (
|
||||
_newBindingsExpr,
|
||||
boundStatement,
|
||||
) => boundStatement)
|
||||
|
||||
rExternalBindingsValue->Result.flatMap(externalBindingsValue => {
|
||||
let newBindings = Bindings.fromValue(externalBindingsValue)
|
||||
let rNewStatement = Bindings.replaceSymbols(newBindings, statement)
|
||||
rNewStatement->Result.map(newStatement =>
|
||||
ExpressionWithContext.withContext(
|
||||
eFunction(
|
||||
"$exportBindings",
|
||||
list{
|
||||
eFunction(
|
||||
"$setBindings",
|
||||
list{
|
||||
newBindings->Bindings.toExternalBindings->eRecord,
|
||||
symbolExpr,
|
||||
newStatement,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
newBindings,
|
||||
switch statement {
|
||||
| ExpressionT.EList(list{ExpressionT.EValue(EvCall(callName)), symbolExpr, statement}) => {
|
||||
let setBindingsFn = correspondingSetBindingsFn(callName)
|
||||
if setBindingsFn !== "" {
|
||||
useExpressionToSetBindings(bindingExpr, environment, statement, (
|
||||
newBindingsExpr,
|
||||
boundStatement,
|
||||
) =>
|
||||
eFunction(
|
||||
"$_exportBindings_$",
|
||||
list{eFunction(setBindingsFn, list{newBindingsExpr, symbolExpr, boundStatement})},
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
| _ => {
|
||||
let rExternalBindingsValue: result<expressionValue, errorValue> = reduceExpression(
|
||||
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)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
defaultStatement()
|
||||
}
|
||||
}
|
||||
| _ => defaultStatement()
|
||||
}
|
||||
}
|
||||
|
||||
let doBlock = (exprs: list<expression>, _bindings: ExpressionT.bindings, _environment): result<
|
||||
expressionWithContext,
|
||||
|
@ -139,11 +135,18 @@ let dispatchMacroCall = (
|
|||
bindings: ExpressionT.bindings,
|
||||
environment,
|
||||
): result<expressionWithContext, errorValue> => {
|
||||
let rCondition = reduceExpression(condition, bindings, environment)
|
||||
let blockCondition = ExpressionBuilder.eBlock(list{condition})
|
||||
let rCondition = reduceExpression(blockCondition, bindings, environment)
|
||||
rCondition->Result.flatMap(conditionValue =>
|
||||
switch conditionValue {
|
||||
| ExpressionValue.EvBool(false) => ExpressionWithContext.noContext(ifFalse)->Ok
|
||||
| ExpressionValue.EvBool(true) => ExpressionWithContext.noContext(ifTrue)->Ok
|
||||
| ExpressionValue.EvBool(false) => {
|
||||
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
|
||||
}
|
||||
)
|
||||
|
@ -155,31 +158,32 @@ let dispatchMacroCall = (
|
|||
> =>
|
||||
switch aList {
|
||||
| list{
|
||||
ExpressionT.EValue(EvCall("$$bindStatement")),
|
||||
ExpressionT.EValue(EvCall("$$_bindStatement_$$")),
|
||||
bindingExpr: ExpressionT.expression,
|
||||
statement,
|
||||
} =>
|
||||
doBindStatement(bindingExpr, statement, environment)
|
||||
| list{ExpressionT.EValue(EvCall("$$bindStatement")), statement} =>
|
||||
| list{ExpressionT.EValue(EvCall("$$_bindStatement_$$")), statement} =>
|
||||
// bindings of the context are used when there is no binding expression
|
||||
doBindStatement(eRecord(Bindings.toExternalBindings(bindings)), statement, environment)
|
||||
| list{
|
||||
ExpressionT.EValue(EvCall("$$bindExpression")),
|
||||
ExpressionT.EValue(EvCall("$$_bindExpression_$$")),
|
||||
bindingExpr: ExpressionT.expression,
|
||||
expression,
|
||||
} =>
|
||||
doBindExpression(bindingExpr, expression, environment)
|
||||
| list{ExpressionT.EValue(EvCall("$$bindExpression")), expression} =>
|
||||
| list{ExpressionT.EValue(EvCall("$$_bindExpression_$$")), expression} =>
|
||||
// bindings of the context are used when there is no binding expression
|
||||
doBindExpression(eRecord(Bindings.toExternalBindings(bindings)), expression, environment)
|
||||
| list{ExpressionT.EValue(EvCall("$$block")), ...exprs} => doBlock(exprs, bindings, environment)
|
||||
| list{ExpressionT.EValue(EvCall("$$_block_$$")), ...exprs} =>
|
||||
doBlock(exprs, bindings, environment)
|
||||
| list{
|
||||
ExpressionT.EValue(EvCall("$$lambda")),
|
||||
ExpressionT.EValue(EvCall("$$_lambda_$$")),
|
||||
ExpressionT.EValue(EvArrayString(parameters)),
|
||||
lambdaDefinition,
|
||||
} =>
|
||||
doLambdaDefinition(bindings, parameters, lambdaDefinition)
|
||||
| list{ExpressionT.EValue(EvCall("$$ternary")), condition, ifTrue, ifFalse} =>
|
||||
| list{ExpressionT.EValue(EvCall("$$_ternary_$$")), condition, ifTrue, ifFalse} =>
|
||||
doTernary(condition, ifTrue, ifFalse, bindings, environment)
|
||||
| _ => ExpressionWithContext.noContext(ExpressionT.EList(aList))->Ok
|
||||
}
|
||||
|
|
|
@ -4,16 +4,19 @@ type errorValue =
|
|||
| REArrayIndexNotFound(string, int)
|
||||
| REAssignmentExpected
|
||||
| REDistributionError(DistributionTypes.error)
|
||||
| REExpectedType(string)
|
||||
| REExpressionExpected
|
||||
| REFunctionExpected(string)
|
||||
| REFunctionNotFound(string)
|
||||
| REJavaScriptExn(option<string>, option<string>) // Javascript Exception
|
||||
| REMacroNotFound(string)
|
||||
| RENotAFunction(string)
|
||||
| REOperationError(Operation.operationError)
|
||||
| RERecordPropertyNotFound(string, string)
|
||||
| RESymbolNotFound(string)
|
||||
| RESyntaxError(string)
|
||||
| RETodo(string) // To do
|
||||
| REExpectedType(string)
|
||||
| REUnitNotFound(string)
|
||||
|
||||
type t = errorValue
|
||||
|
||||
|
@ -28,7 +31,9 @@ let errorToString = err =>
|
|||
| REAssignmentExpected => "Assignment expected"
|
||||
| REExpressionExpected => "Expression expected"
|
||||
| REFunctionExpected(msg) => `Function expected: ${msg}`
|
||||
| REFunctionNotFound(msg) => `Function not found: ${msg}`
|
||||
| REDistributionError(err) => `Distribution Math Error: ${DistributionTypes.Error.toString(err)}`
|
||||
| REOperationError(err) => `Math Error: ${Operation.Error.toString(err)}`
|
||||
| REJavaScriptExn(omsg, oname) => {
|
||||
let answer = "JS Exception:"
|
||||
let answer = switch oname {
|
||||
|
@ -48,4 +53,5 @@ let errorToString = err =>
|
|||
| RESyntaxError(desc) => `Syntax Error: ${desc}`
|
||||
| RETodo(msg) => `TODO: ${msg}`
|
||||
| REExpectedType(typeName) => `Expected type: ${typeName}`
|
||||
| REUnitNotFound(unitName) => `Unit not found: ${unitName}`
|
||||
}
|
||||
|
|
|
@ -18,13 +18,10 @@ type internalCode = ReducerInterface_ExpressionValue.internalCode
|
|||
type t = expression
|
||||
|
||||
/*
|
||||
Converts a MathJs code to expression
|
||||
Converts a Squigle code to expression
|
||||
*/
|
||||
let parse_ = (expr: string, parser, converter): result<t, errorValue> =>
|
||||
expr->parser->Result.flatMap(node => converter(node))
|
||||
|
||||
let parse = (mathJsCode: string): result<t, errorValue> =>
|
||||
mathJsCode->parse_(MathJs.Parse.parse, MathJs.ToExpression.fromNode)
|
||||
let parse = (peggyCode: string): result<t, errorValue> =>
|
||||
peggyCode->Reducer_Peggy_Parse.parse->Result.map(Reducer_Peggy_ToExpression.fromNode)
|
||||
|
||||
/*
|
||||
Recursively evaluate/reduce the expression (Lisp AST)
|
||||
|
@ -77,11 +74,30 @@ and reduceValueList = (valueList: list<expressionValue>, environment): result<
|
|||
'e,
|
||||
> =>
|
||||
switch valueList {
|
||||
| list{EvCall(fName), ...args} =>
|
||||
(fName, args->Belt.List.toArray)->BuiltIn.dispatch(environment, reduceExpression)
|
||||
| list{EvCall(fName), ...args} => {
|
||||
let rCheckedArgs = switch fName {
|
||||
| "$_setBindings_$" | "$_setTypeOfBindings_$" | "$_setTypeAliasBindings_$" => args->Ok
|
||||
| _ => args->Lambda.checkIfReduced
|
||||
}
|
||||
|
||||
rCheckedArgs->Result.flatMap(checkedArgs =>
|
||||
(fName, checkedArgs->Belt.List.toArray)->BuiltIn.dispatch(environment, reduceExpression)
|
||||
)
|
||||
}
|
||||
| list{EvLambda(_)} =>
|
||||
// TODO: remove on solving issue#558
|
||||
valueList
|
||||
->Lambda.checkIfReduced
|
||||
->Result.flatMap(reducedValueList =>
|
||||
reducedValueList->Belt.List.toArray->ExpressionValue.EvArray->Ok
|
||||
)
|
||||
| list{EvLambda(lamdaCall), ...args} =>
|
||||
Lambda.doLambdaCall(lamdaCall, args, environment, reduceExpression)
|
||||
args
|
||||
->Lambda.checkIfReduced
|
||||
->Result.flatMap(checkedArgs =>
|
||||
Lambda.doLambdaCall(lamdaCall, checkedArgs, environment, reduceExpression)
|
||||
)
|
||||
|
||||
| _ =>
|
||||
valueList
|
||||
->Lambda.checkIfReduced
|
||||
|
@ -116,7 +132,7 @@ let evaluateUsingOptions = (
|
|||
}
|
||||
|
||||
/*
|
||||
Evaluates MathJs code and bindings via Reducer and answers the result
|
||||
Evaluates Squiggle code and bindings via Reducer and answers the result
|
||||
*/
|
||||
let evaluate = (code: string): result<expressionValue, errorValue> => {
|
||||
evaluateUsingOptions(~environment=None, ~externalBindings=None, code)
|
||||
|
|
|
@ -22,11 +22,16 @@ let callReducer = (
|
|||
bindings: bindings,
|
||||
environment: environment,
|
||||
reducer: reducerFn,
|
||||
): result<expressionValue, errorValue> =>
|
||||
): result<expressionValue, errorValue> => {
|
||||
switch expressionWithContext {
|
||||
| ExpressionNoContext(expr) => reducer(expr, bindings, environment)
|
||||
| ExpressionWithContext(expr, context) => reducer(expr, context, environment)
|
||||
| ExpressionNoContext(expr) =>
|
||||
// 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 noContext = expression => ExpressionNoContext(expression)
|
||||
|
|
|
@ -10,13 +10,8 @@ type externalBindings = ReducerInterface_ExpressionValue.externalBindings
|
|||
|
||||
let defaultBindings: ExpressionT.bindings = Belt.Map.String.empty
|
||||
|
||||
let fromExternalBindings = (externalBindings: externalBindings): ExpressionT.bindings => {
|
||||
let keys = Js.Dict.keys(externalBindings)
|
||||
keys->Belt.Array.reduce(defaultBindings, (acc, key) => {
|
||||
let value = Js.Dict.unsafeGet(externalBindings, key)
|
||||
acc->Belt.Map.String.set(key, value)
|
||||
})
|
||||
}
|
||||
let typeAliasesKey = "_typeAliases_"
|
||||
let typeReferencesKey = "_typeReferences_"
|
||||
|
||||
let toExternalBindings = (bindings: ExpressionT.bindings): externalBindings => {
|
||||
let keys = Belt.Map.String.keysToArray(bindings)
|
||||
|
@ -27,6 +22,50 @@ let toExternalBindings = (bindings: ExpressionT.bindings): externalBindings => {
|
|||
})
|
||||
}
|
||||
|
||||
let fromExternalBindings_ = (externalBindings: externalBindings): ExpressionT.bindings => {
|
||||
let keys = Js.Dict.keys(externalBindings)
|
||||
keys->Belt.Array.reduce(defaultBindings, (acc, key) => {
|
||||
let value = Js.Dict.unsafeGet(externalBindings, key)
|
||||
acc->Belt.Map.String.set(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
let fromExternalBindings = (externalBindings: externalBindings): ExpressionT.bindings => {
|
||||
// TODO: This code will be removed in the future when maps are used instead of records. Please don't mind this function for now.
|
||||
|
||||
let internalBindings0 = fromExternalBindings_(externalBindings)
|
||||
|
||||
let oExistingTypeAliases = Belt.Map.String.get(internalBindings0, typeAliasesKey)
|
||||
let internalBindings1 = Belt.Option.mapWithDefault(
|
||||
oExistingTypeAliases,
|
||||
internalBindings0,
|
||||
existingTypeAliases => {
|
||||
let newTypeAliases = switch existingTypeAliases {
|
||||
| EvRecord(actualTypeAliases) =>
|
||||
actualTypeAliases->fromExternalBindings_->toExternalBindings->ExpressionValue.EvRecord
|
||||
| _ => existingTypeAliases
|
||||
}
|
||||
Belt.Map.String.set(internalBindings0, typeAliasesKey, newTypeAliases)
|
||||
},
|
||||
)
|
||||
|
||||
let oExistingTypeReferences = Belt.Map.String.get(internalBindings1, typeReferencesKey)
|
||||
let internalBindings2 = Belt.Option.mapWithDefault(
|
||||
oExistingTypeReferences,
|
||||
internalBindings1,
|
||||
existingTypeReferences => {
|
||||
let newTypeReferences = switch existingTypeReferences {
|
||||
| EvRecord(actualTypeReferences) =>
|
||||
actualTypeReferences->fromExternalBindings_->toExternalBindings->ExpressionValue.EvRecord
|
||||
| _ => existingTypeReferences
|
||||
}
|
||||
Belt.Map.String.set(internalBindings0, typeReferencesKey, newTypeReferences)
|
||||
},
|
||||
)
|
||||
|
||||
internalBindings2
|
||||
}
|
||||
|
||||
let fromValue = (aValue: expressionValue) =>
|
||||
switch aValue {
|
||||
| EvRecord(externalBindings) => fromExternalBindings(externalBindings)
|
||||
|
|
|
@ -14,7 +14,7 @@ let eArray = anArray => anArray->BExpressionValue.EvArray->BExpressionT.EValue
|
|||
let eArrayString = anArray => anArray->BExpressionValue.EvArrayString->BExpressionT.EValue
|
||||
|
||||
let eBindings = (anArray: array<(string, BExpressionValue.expressionValue)>) =>
|
||||
anArray->Js.Dict.fromArray->EvRecord->BExpressionT.EValue
|
||||
anArray->Js.Dict.fromArray->BExpressionValue.EvRecord->BExpressionT.EValue
|
||||
|
||||
let eBool = aBool => aBool->BExpressionValue.EvBool->BExpressionT.EValue
|
||||
|
||||
|
@ -48,19 +48,22 @@ let eSymbol = (name: string): expression => name->BExpressionValue.EvSymbol->BEx
|
|||
|
||||
let eList = (list: list<expression>): expression => list->BExpressionT.EList
|
||||
|
||||
let eBlock = (exprs: list<expression>): expression => eFunction("$$block", exprs)
|
||||
let eBlock = (exprs: list<expression>): expression => eFunction("$$_block_$$", exprs)
|
||||
|
||||
let eLetStatement = (symbol: string, valueExpression: expression): expression =>
|
||||
eFunction("$let", list{eSymbol(symbol), valueExpression})
|
||||
eFunction("$_let_$", list{eSymbol(symbol), valueExpression})
|
||||
|
||||
let eBindStatement = (bindingExpr: expression, letStatement: expression): expression =>
|
||||
eFunction("$$bindStatement", list{bindingExpr, letStatement})
|
||||
eFunction("$$_bindStatement_$$", list{bindingExpr, letStatement})
|
||||
|
||||
let eBindStatementDefault = (letStatement: expression): expression =>
|
||||
eFunction("$$bindStatement", list{letStatement})
|
||||
eFunction("$$_bindStatement_$$", list{letStatement})
|
||||
|
||||
let eBindExpression = (bindingExpr: expression, expression: expression): expression =>
|
||||
eFunction("$$bindExpression", list{bindingExpr, expression})
|
||||
eFunction("$$_bindExpression_$$", list{bindingExpr, expression})
|
||||
|
||||
let eBindExpressionDefault = (expression: expression): expression =>
|
||||
eFunction("$$bindExpression", list{expression})
|
||||
eFunction("$$_bindExpression_$$", list{expression})
|
||||
|
||||
let eTypeIdentifier = (name: string): expression =>
|
||||
name->BExpressionValue.EvTypeIdentifier->BExpressionT.EValue
|
||||
|
|
|
@ -28,6 +28,11 @@ type reducerFn = (
|
|||
*/
|
||||
let rec toString = 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) =>
|
||||
`(${Belt.List.map(aList, aValue => toString(aValue))
|
||||
->Extra.List.interperse(" ")
|
||||
|
@ -42,6 +47,12 @@ let toStringResult = codeResult =>
|
|||
| Error(m) => `Error(${Reducer_ErrorValue.errorToString(m)})`
|
||||
}
|
||||
|
||||
let toStringResultOkless = codeResult =>
|
||||
switch codeResult {
|
||||
| Ok(a) => toString(a)
|
||||
| Error(m) => `Error(${Reducer_ErrorValue.errorToString(m)})`
|
||||
}
|
||||
|
||||
let inspect = (expr: expression): expression => {
|
||||
Js.log(toString(expr))
|
||||
expr
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
module Eval = Reducer_MathJs_Eval
|
||||
module Parse = Reducer_MathJs_Parse
|
||||
module ToExpression = Reducer_MathJs_ToExpression
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
/*
|
||||
MathJs Nodes
|
||||
We make MathJs Nodes strong-typed
|
||||
*/
|
||||
module Extra = Reducer_Extra
|
||||
open Reducer_ErrorValue
|
||||
|
||||
type node = {"type": string, "isNode": bool, "comment": string}
|
||||
type arrayNode = {...node, "items": array<node>}
|
||||
type block = {"node": node}
|
||||
type blockNode = {...node, "blocks": array<block>}
|
||||
type conditionalNode = {...node, "condition": node, "trueExpr": node, "falseExpr": node}
|
||||
type constantNode = {...node, "value": unit}
|
||||
type functionAssignmentNode = {...node, "name": string, "params": array<string>, "expr": node}
|
||||
type indexNode = {...node, "dimensions": array<node>}
|
||||
type objectNode = {...node, "properties": Js.Dict.t<node>}
|
||||
type accessorNode = {...node, "object": node, "index": indexNode, "name": string}
|
||||
type parenthesisNode = {...node, "content": node}
|
||||
//rangeNode
|
||||
//relationalNode
|
||||
type symbolNode = {...node, "name": string}
|
||||
type functionNode = {...node, "fn": unit, "args": array<node>}
|
||||
type operatorNode = {...functionNode, "op": string}
|
||||
type assignmentNode = {...node, "object": symbolNode, "value": node}
|
||||
type assignmentNodeWAccessor = {...node, "object": accessorNode, "value": node}
|
||||
type assignmentNodeWIndex = {...assignmentNodeWAccessor, "index": Js.null<indexNode>}
|
||||
|
||||
external castAccessorNode: node => accessorNode = "%identity"
|
||||
external castArrayNode: node => arrayNode = "%identity"
|
||||
external castAssignmentNode: node => assignmentNode = "%identity"
|
||||
external castAssignmentNodeWAccessor: node => assignmentNodeWAccessor = "%identity"
|
||||
external castAssignmentNodeWIndex: node => assignmentNodeWIndex = "%identity"
|
||||
external castBlockNode: node => blockNode = "%identity"
|
||||
external castConditionalNode: node => conditionalNode = "%identity"
|
||||
external castConstantNode: node => constantNode = "%identity"
|
||||
external castFunctionAssignmentNode: node => functionAssignmentNode = "%identity"
|
||||
external castFunctionNode: node => functionNode = "%identity"
|
||||
external castIndexNode: node => indexNode = "%identity"
|
||||
external castObjectNode: node => objectNode = "%identity"
|
||||
external castOperatorNode: node => operatorNode = "%identity"
|
||||
external castOperatorNodeToFunctionNode: operatorNode => functionNode = "%identity"
|
||||
external castParenthesisNode: node => parenthesisNode = "%identity"
|
||||
external castSymbolNode: node => symbolNode = "%identity"
|
||||
|
||||
/*
|
||||
MathJs Parser
|
||||
*/
|
||||
@module("mathjs") external parse__: string => node = "parse"
|
||||
|
||||
let parse = (expr: string): result<node, errorValue> =>
|
||||
try {
|
||||
Ok(parse__(expr))
|
||||
} catch {
|
||||
| Js.Exn.Error(obj) => REJavaScriptExn(Js.Exn.message(obj), Js.Exn.name(obj))->Error
|
||||
}
|
||||
|
||||
type mathJsNode =
|
||||
| MjAccessorNode(accessorNode)
|
||||
| MjArrayNode(arrayNode)
|
||||
| MjAssignmentNode(assignmentNode)
|
||||
| MjBlockNode(blockNode)
|
||||
| MjConditionalNode(conditionalNode)
|
||||
| MjConstantNode(constantNode)
|
||||
| MjFunctionAssignmentNode(functionAssignmentNode)
|
||||
| MjFunctionNode(functionNode)
|
||||
| MjIndexNode(indexNode)
|
||||
| MjObjectNode(objectNode)
|
||||
| MjOperatorNode(operatorNode)
|
||||
| MjParenthesisNode(parenthesisNode)
|
||||
| MjSymbolNode(symbolNode)
|
||||
|
||||
let castNodeType = (node: node) => {
|
||||
let decideAssignmentNode = node => {
|
||||
let iNode = node->castAssignmentNodeWIndex
|
||||
if Js.null == iNode["index"] && iNode["object"]["type"] == "SymbolNode" {
|
||||
node->castAssignmentNode->MjAssignmentNode->Ok
|
||||
} else {
|
||||
RESyntaxError("Assignment to index or property not supported")->Error
|
||||
}
|
||||
}
|
||||
|
||||
switch node["type"] {
|
||||
| "AccessorNode" => node->castAccessorNode->MjAccessorNode->Ok
|
||||
| "ArrayNode" => node->castArrayNode->MjArrayNode->Ok
|
||||
| "AssignmentNode" => node->decideAssignmentNode
|
||||
| "BlockNode" => node->castBlockNode->MjBlockNode->Ok
|
||||
| "ConditionalNode" => node->castConditionalNode->MjConditionalNode->Ok
|
||||
| "ConstantNode" => node->castConstantNode->MjConstantNode->Ok
|
||||
| "FunctionAssignmentNode" => node->castFunctionAssignmentNode->MjFunctionAssignmentNode->Ok
|
||||
| "FunctionNode" => node->castFunctionNode->MjFunctionNode->Ok
|
||||
| "IndexNode" => node->castIndexNode->MjIndexNode->Ok
|
||||
| "ObjectNode" => node->castObjectNode->MjObjectNode->Ok
|
||||
| "OperatorNode" => node->castOperatorNode->MjOperatorNode->Ok
|
||||
| "ParenthesisNode" => node->castParenthesisNode->MjParenthesisNode->Ok
|
||||
| "SymbolNode" => node->castSymbolNode->MjSymbolNode->Ok
|
||||
| _ => RETodo(`Argg, unhandled MathJsNode: ${node["type"]}`)->Error
|
||||
}
|
||||
}
|
||||
|
||||
external unitAsSymbolNode: unit => symbolNode = "%identity"
|
||||
external unitAsString: unit => string = "%identity"
|
||||
|
||||
let nameOfFunctionNode = (fNode: functionNode): string => {
|
||||
let name = fNode["fn"]
|
||||
if Js.typeof(name) == "string" {
|
||||
name->unitAsString
|
||||
} else {
|
||||
(name->unitAsSymbolNode)["name"]
|
||||
}
|
||||
}
|
||||
|
||||
let rec toString = (mathJsNode: mathJsNode): string => {
|
||||
let toStringValue = (a: 'a): string =>
|
||||
if Js.typeof(a) == "string" {
|
||||
`'${Js.String.make(a)}'`
|
||||
} else {
|
||||
Js.String.make(a)
|
||||
}
|
||||
|
||||
let toStringNodeArray = (nodeArray: array<node>): string =>
|
||||
nodeArray
|
||||
->Belt.Array.map(a => toStringMathJsNode(a))
|
||||
->Extra.Array.interperse(", ")
|
||||
->Js.String.concatMany("")
|
||||
|
||||
let toStringFunctionAssignmentNode = (faNode: functionAssignmentNode): string => {
|
||||
let paramNames = Js.Array2.toString(faNode["params"])
|
||||
`${faNode["name"]} = (${paramNames}) => ${toStringMathJsNode(faNode["expr"])}`
|
||||
}
|
||||
let toStringFunctionNode = (fnode: functionNode): string =>
|
||||
`${fnode->nameOfFunctionNode}(${fnode["args"]->toStringNodeArray})`
|
||||
|
||||
let toStringObjectEntry = ((key: string, value: node)): string =>
|
||||
`${key}: ${value->toStringMathJsNode}`
|
||||
|
||||
let toStringObjectNode = (oNode: objectNode): string =>
|
||||
`{${oNode["properties"]
|
||||
->Js.Dict.entries
|
||||
->Belt.Array.map(entry => entry->toStringObjectEntry)
|
||||
->Extra.Array.interperse(", ")
|
||||
->Js.String.concatMany("")}}`
|
||||
|
||||
let toStringIndexNode = (iNode: indexNode): string =>
|
||||
iNode["dimensions"]
|
||||
->Belt.Array.map(each => toStringResult(each->castNodeType))
|
||||
->Js.String.concatMany("")
|
||||
|
||||
let toStringSymbolNode = (sNode: symbolNode): string => sNode["name"]
|
||||
|
||||
let toStringBlocks = (blocks: array<block>): string =>
|
||||
blocks
|
||||
->Belt.Array.map(each => each["node"]->castNodeType->toStringResult)
|
||||
->Extra.Array.interperse("; ")
|
||||
->Js.String.concatMany("")
|
||||
|
||||
switch mathJsNode {
|
||||
| MjAccessorNode(aNode) =>
|
||||
`${aNode["object"]->toStringMathJsNode}[${aNode["index"]->toStringIndexNode}]`
|
||||
| MjArrayNode(aNode) => `[${aNode["items"]->toStringNodeArray}]`
|
||||
| MjAssignmentNode(aNode) =>
|
||||
`${aNode["object"]->toStringSymbolNode} = ${aNode["value"]->toStringMathJsNode}`
|
||||
| MjBlockNode(bNode) => `{${bNode["blocks"]->toStringBlocks}}`
|
||||
| MjConditionalNode(cNode) =>
|
||||
`ternary(${toStringMathJsNode(cNode["condition"])}, ${toStringMathJsNode(
|
||||
cNode["trueExpr"],
|
||||
)}, ${toStringMathJsNode(cNode["falseExpr"])})`
|
||||
| MjConstantNode(cNode) => cNode["value"]->toStringValue
|
||||
| MjFunctionAssignmentNode(faNode) => faNode->toStringFunctionAssignmentNode
|
||||
| MjFunctionNode(fNode) => fNode->toStringFunctionNode
|
||||
| MjIndexNode(iNode) => iNode->toStringIndexNode
|
||||
| MjObjectNode(oNode) => oNode->toStringObjectNode
|
||||
| MjOperatorNode(opNode) => opNode->castOperatorNodeToFunctionNode->toStringFunctionNode
|
||||
| MjParenthesisNode(pNode) => `(${toStringMathJsNode(pNode["content"])})`
|
||||
| MjSymbolNode(sNode) => sNode->toStringSymbolNode
|
||||
}
|
||||
}
|
||||
and toStringResult = (rMathJsNode: result<mathJsNode, errorValue>): string =>
|
||||
switch rMathJsNode {
|
||||
| Error(e) => errorToString(e)
|
||||
| Ok(mathJsNode) => toString(mathJsNode)
|
||||
}
|
||||
and toStringMathJsNode = node => node->castNodeType->toStringResult
|
|
@ -1,154 +0,0 @@
|
|||
/* * WARNING. DO NOT EDIT, BEAUTIFY, COMMENT ON OR REFACTOR THIS CODE.
|
||||
We will stop using MathJs parser and
|
||||
this whole file will go to trash
|
||||
**/
|
||||
module ErrorValue = Reducer_ErrorValue
|
||||
module ExpressionBuilder = Reducer_Expression_ExpressionBuilder
|
||||
module ExpressionT = Reducer_Expression_T
|
||||
module ExpressionValue = ReducerInterface.ExpressionValue
|
||||
module JavaScript = Reducer_Js
|
||||
module Parse = Reducer_MathJs_Parse
|
||||
module Result = Belt.Result
|
||||
|
||||
type errorValue = ErrorValue.errorValue
|
||||
type expression = ExpressionT.expression
|
||||
type expressionValue = ExpressionValue.expressionValue
|
||||
|
||||
let blockToNode = block => block["node"]
|
||||
|
||||
let rec fromInnerNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
|
||||
Parse.castNodeType(mathJsNode)->Result.flatMap(typedMathJsNode => {
|
||||
let fromNodeList = (nodeList: list<Parse.node>): result<list<expression>, 'e> =>
|
||||
Belt.List.reduceReverse(nodeList, Ok(list{}), (racc, currNode) =>
|
||||
racc->Result.flatMap(acc =>
|
||||
fromInnerNode(currNode)->Result.map(currCode => list{currCode, ...acc})
|
||||
)
|
||||
)
|
||||
|
||||
let caseFunctionNode = fNode => {
|
||||
let rLispArgs = fNode["args"]->Belt.List.fromArray->fromNodeList
|
||||
rLispArgs->Result.map(lispArgs =>
|
||||
ExpressionBuilder.eFunction(fNode->Parse.nameOfFunctionNode, lispArgs)
|
||||
)
|
||||
}
|
||||
|
||||
let caseObjectNode = oNode => {
|
||||
let fromObjectEntries = entryList => {
|
||||
let rargs = Belt.List.reduceReverse(entryList, Ok(list{}), (
|
||||
racc,
|
||||
(key: string, value: Parse.node),
|
||||
) =>
|
||||
racc->Result.flatMap(acc =>
|
||||
fromInnerNode(value)->Result.map(valueExpression => {
|
||||
let entryCode =
|
||||
list{ExpressionBuilder.eString(key), valueExpression}->ExpressionT.EList
|
||||
list{entryCode, ...acc}
|
||||
})
|
||||
)
|
||||
)
|
||||
rargs->Result.flatMap(args =>
|
||||
ExpressionBuilder.eFunction("$constructRecord", list{ExpressionT.EList(args)})->Ok
|
||||
) // $constructRecord gets a single argument: List of key-value paiers
|
||||
}
|
||||
|
||||
oNode["properties"]->Js.Dict.entries->Belt.List.fromArray->fromObjectEntries
|
||||
}
|
||||
|
||||
let caseIndexNode = iNode => {
|
||||
let rpropertyCodeList = Belt.List.reduceReverse(
|
||||
iNode["dimensions"]->Belt.List.fromArray,
|
||||
Ok(list{}),
|
||||
(racc, currentPropertyMathJsNode) =>
|
||||
racc->Result.flatMap(acc =>
|
||||
fromInnerNode(currentPropertyMathJsNode)->Result.map(propertyCode => list{
|
||||
propertyCode,
|
||||
...acc,
|
||||
})
|
||||
),
|
||||
)
|
||||
rpropertyCodeList->Result.map(propertyCodeList => ExpressionT.EList(propertyCodeList))
|
||||
}
|
||||
|
||||
let caseAccessorNode = (objectNode, indexNode) => {
|
||||
caseIndexNode(indexNode)->Result.flatMap(indexCode => {
|
||||
fromInnerNode(objectNode)->Result.flatMap(objectCode =>
|
||||
ExpressionBuilder.eFunction("$atIndex", list{objectCode, indexCode})->Ok
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let caseBlock = (nodesArray: array<Parse.node>): result<expression, errorValue> => {
|
||||
let rStatements: result<list<expression>, 'a> =
|
||||
nodesArray
|
||||
->Belt.List.fromArray
|
||||
->Belt.List.reduceReverse(Ok(list{}), (racc, currNode) =>
|
||||
racc->Result.flatMap(acc =>
|
||||
fromInnerNode(currNode)->Result.map(currCode => list{currCode, ...acc})
|
||||
)
|
||||
)
|
||||
rStatements->Result.map(statements => ExpressionBuilder.eBlock(statements))
|
||||
}
|
||||
|
||||
let caseAssignmentNode = aNode => {
|
||||
let symbolName = aNode["object"]["name"]
|
||||
let rValueExpression = fromInnerNode(aNode["value"])
|
||||
rValueExpression->Result.map(valueExpression =>
|
||||
ExpressionBuilder.eLetStatement(symbolName, valueExpression)
|
||||
)
|
||||
}
|
||||
|
||||
let caseFunctionAssignmentNode = faNode => {
|
||||
let symbol = faNode["name"]->ExpressionBuilder.eSymbol
|
||||
let rValueExpression = fromInnerNode(faNode["expr"])
|
||||
|
||||
rValueExpression->Result.flatMap(valueExpression => {
|
||||
let lispParams = ExpressionBuilder.eArrayString(faNode["params"])
|
||||
let valueBlock = ExpressionBuilder.eBlock(list{valueExpression})
|
||||
let lambda = ExpressionBuilder.eFunction("$$lambda", list{lispParams, valueBlock})
|
||||
ExpressionBuilder.eFunction("$let", list{symbol, lambda})->Ok
|
||||
})
|
||||
}
|
||||
|
||||
let caseArrayNode = aNode => {
|
||||
aNode["items"]->Belt.List.fromArray->fromNodeList->Result.map(list => ExpressionT.EList(list))
|
||||
}
|
||||
|
||||
let caseConditionalNode = cndNode => {
|
||||
let rCondition = fromInnerNode(cndNode["condition"])
|
||||
let rTrueExpr = fromInnerNode(cndNode["trueExpr"])
|
||||
let rFalse = fromInnerNode(cndNode["falseExpr"])
|
||||
|
||||
rCondition->Result.flatMap(condition =>
|
||||
rTrueExpr->Result.flatMap(trueExpr =>
|
||||
rFalse->Result.flatMap(falseExpr =>
|
||||
ExpressionBuilder.eFunction("$$ternary", list{condition, trueExpr, falseExpr})->Ok
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let rFinalExpression: result<expression, errorValue> = switch typedMathJsNode {
|
||||
| MjAccessorNode(aNode) => caseAccessorNode(aNode["object"], aNode["index"])
|
||||
| MjArrayNode(aNode) => caseArrayNode(aNode)
|
||||
| MjAssignmentNode(aNode) => caseAssignmentNode(aNode)
|
||||
| MjSymbolNode(sNode) => {
|
||||
let expr: expression = ExpressionBuilder.eSymbol(sNode["name"])
|
||||
let rExpr: result<expression, errorValue> = expr->Ok
|
||||
rExpr
|
||||
}
|
||||
| MjBlockNode(bNode) => bNode["blocks"]->Js.Array2.map(blockToNode)->caseBlock
|
||||
| MjConditionalNode(cndNode) => caseConditionalNode(cndNode)
|
||||
| MjConstantNode(cNode) =>
|
||||
cNode["value"]->JavaScript.Gate.jsToEv->Result.flatMap(v => v->ExpressionT.EValue->Ok)
|
||||
| MjFunctionAssignmentNode(faNode) => caseFunctionAssignmentNode(faNode)
|
||||
| MjFunctionNode(fNode) => fNode->caseFunctionNode
|
||||
| MjIndexNode(iNode) => caseIndexNode(iNode)
|
||||
| MjObjectNode(oNode) => caseObjectNode(oNode)
|
||||
| MjOperatorNode(opNode) => opNode->Parse.castOperatorNodeToFunctionNode->caseFunctionNode
|
||||
| MjParenthesisNode(pNode) => pNode["content"]->fromInnerNode
|
||||
}
|
||||
rFinalExpression
|
||||
})
|
||||
|
||||
let fromNode = (node: Parse.node): result<expression, errorValue> =>
|
||||
fromInnerNode(node)->Result.map(expr => ExpressionBuilder.eBlock(list{expr}))
|
|
@ -0,0 +1,449 @@
|
|||
// Try in https://peggyjs.org/online
|
||||
|
||||
{{
|
||||
var toFunction = {
|
||||
'-': 'subtract',
|
||||
'->': 'pipe',
|
||||
'!=': 'unequal',
|
||||
'.-': 'dotSubtract',
|
||||
'.*': 'dotMultiply',
|
||||
'./': 'dotDivide',
|
||||
'.^': 'dotPow',
|
||||
'.+': 'dotAdd',
|
||||
'*': 'multiply',
|
||||
'/': 'divide',
|
||||
'&&': 'and',
|
||||
'^': 'pow', // or xor
|
||||
'+': 'add',
|
||||
'<': 'smaller',
|
||||
'<=': 'smallerEq',
|
||||
'==': 'equal',
|
||||
'>': 'larger',
|
||||
'>=': 'largerEq',
|
||||
'||': 'or',
|
||||
'to': 'credibleIntervalToDistribution',
|
||||
}
|
||||
|
||||
var unaryToFunction = {
|
||||
'-': 'unaryMinus',
|
||||
'!': 'not',
|
||||
'.-': 'unaryDotMinus',
|
||||
}
|
||||
|
||||
var postOperatorToFunction = {
|
||||
'.': '$_atIndex_$',
|
||||
'()': '$$_applyAll_$$',
|
||||
'[]': '$_atIndex_$',
|
||||
}
|
||||
|
||||
function makeFunctionCall(fn, args) {
|
||||
if (fn === '$$_applyAll_$$') {
|
||||
// Any list of values is applied from left to right anyway.
|
||||
// Like in Haskell and Lisp.
|
||||
// So we remove the redundant $$_applyAll_$$.
|
||||
if (args[0].type === "Identifier") {args[0].type = "CallIdentifier"}
|
||||
return nodeExpression(args)
|
||||
} else {
|
||||
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 nodeInteger(value) {return {type: 'Integer', value: value}}
|
||||
function nodeKeyValue(key, value) {
|
||||
if (key.type === 'Identifier') {key.type = 'String'}
|
||||
return {type: 'KeyValue', key: key, value: value}}
|
||||
function nodeLambda(args, body) {return {type: 'Lambda', args: args, body: body}}
|
||||
function nodeLetStatment(variable, value) {return {type: 'LetStatement', variable: variable, 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 nodeTypeIdentifier(typeValue) {return {type: 'TypeIdentifier', value: typeValue}}
|
||||
}}
|
||||
|
||||
start
|
||||
// = _nl start:typeExpression _nl finalComment? {return start}
|
||||
= _nl start:outerBlock _nl finalComment? {return start}
|
||||
|
||||
zeroOMoreArgumentsBlockOrExpression = innerBlockOrExpression / lambda
|
||||
|
||||
outerBlock
|
||||
= statements:array_statements finalExpression: (statementSeparator @expression)?
|
||||
{ if (finalExpression != null) { statements.push(finalExpression) }
|
||||
return nodeBlock(statements) }
|
||||
/ finalExpression: expression
|
||||
{ return nodeBlock([finalExpression])}
|
||||
|
||||
innerBlockOrExpression
|
||||
= quotedInnerBlock
|
||||
/ finalExpression: expression
|
||||
{ return nodeBlock([finalExpression])}
|
||||
|
||||
quotedInnerBlock
|
||||
= '{' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}'
|
||||
{ statements.push(finalExpression)
|
||||
return nodeBlock(statements) }
|
||||
/ '{' _nl finalExpression: expression _nl '}'
|
||||
{ return nodeBlock([finalExpression]) }
|
||||
|
||||
array_statements
|
||||
= head:statement tail:(statementSeparator @array_statements )
|
||||
{ return [head, ...tail] }
|
||||
/ head:statement
|
||||
{ return [head] }
|
||||
|
||||
statement
|
||||
= letStatement
|
||||
/ defunStatement
|
||||
/ typeStatement
|
||||
|
||||
letStatement
|
||||
= variable:identifier _ assignmentOp _nl value:zeroOMoreArgumentsBlockOrExpression
|
||||
{ return nodeLetStatment(variable, value) }
|
||||
|
||||
defunStatement
|
||||
= variable:identifier '(' _nl args:array_parameters _nl ')' _ assignmentOp _nl body:innerBlockOrExpression
|
||||
{ var value = nodeLambda(args, body)
|
||||
return nodeLetStatment(variable, value) }
|
||||
|
||||
assignmentOp "assignment" = '='
|
||||
|
||||
array_parameters
|
||||
= head:dollarIdentifier tail:(_ ',' _nl @dollarIdentifier)*
|
||||
{ return [head, ...tail]; }
|
||||
|
||||
expression = ifthenelse / ternary / logicalAdditive
|
||||
|
||||
ifthenelse
|
||||
= 'if' __nl condition:logicalAdditive
|
||||
__nl 'then' __nl trueExpression:innerBlockOrExpression
|
||||
__nl 'else' __nl falseExpression:(ifthenelse/innerBlockOrExpression)
|
||||
{ return nodeTernary(condition, trueExpression, falseExpression) }
|
||||
|
||||
ternary
|
||||
= condition:logicalAdditive _ '?' _nl trueExpression:logicalAdditive _ ':' _nl falseExpression:(ternary/logicalAdditive)
|
||||
{ return nodeTernary(condition, trueExpression, falseExpression) }
|
||||
|
||||
logicalAdditive
|
||||
= head:logicalMultiplicative tail:(_ operator:logicalAdditiveOp _nl arg:logicalMultiplicative {return {operator: operator, right: arg}})*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(toFunction[element.operator], [result, element.right])
|
||||
}, head)}
|
||||
|
||||
logicalAdditiveOp "operator" = '||'
|
||||
|
||||
// start binary operators
|
||||
logicalMultiplicative
|
||||
= head:equality tail:(_ operator:logicalMultiplicativeOp _nl arg:equality {return {operator: operator, right: arg}})*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(toFunction[element.operator], [result, element.right])
|
||||
}, head)}
|
||||
|
||||
logicalMultiplicativeOp "operator" = '&&'
|
||||
|
||||
equality
|
||||
= left:relational _ operator:equalityOp _nl right:relational
|
||||
{ return makeFunctionCall(toFunction[operator], [left, right])}
|
||||
/ relational
|
||||
|
||||
equalityOp "operator" = '=='/'!='
|
||||
|
||||
relational
|
||||
= left:additive _ operator:relationalOp _nl right:additive
|
||||
{ return makeFunctionCall(toFunction[operator], [left, right])}
|
||||
/ additive
|
||||
|
||||
relationalOp "operator" = '<='/'<'/'>='/'>'
|
||||
|
||||
additive
|
||||
= head:multiplicative tail:(_ operator:additiveOp _nl arg:multiplicative {return {operator: operator, right: arg}})*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(toFunction[element.operator], [result, element.right])
|
||||
}, head)}
|
||||
|
||||
additiveOp "operator" = '+' / '-' / '.+' / '.-'
|
||||
|
||||
multiplicative
|
||||
= head:power tail:(_ operator:multiplicativeOp _nl arg:power {return {operator: operator, right: arg}})*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(toFunction[element.operator], [result, element.right])
|
||||
}, head)}
|
||||
|
||||
multiplicativeOp "operator" = '*' / '/' / '.*' / './'
|
||||
|
||||
power
|
||||
= head:credibleInterval tail:(_ operator:powerOp _nl arg:credibleInterval {return {operator: operator, right: arg}})*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(toFunction[element.operator], [result, element.right])
|
||||
}, head)}
|
||||
|
||||
powerOp "operator" = '^' / '.^'
|
||||
|
||||
credibleInterval
|
||||
= head:chainFunctionCall tail:(__ operator:credibleIntervalOp __nl arg:chainFunctionCall {return {operator: operator, right: arg}})*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(toFunction[element.operator], [result, element.right])
|
||||
}, head)}
|
||||
|
||||
credibleIntervalOp "operator" = 'to'
|
||||
|
||||
chainFunctionCall
|
||||
= head:unary tail:(_ ('->'/'|>') _nl chained:chainedFunction {return chained})*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(element.fnName, [result, ...element.args])
|
||||
}, head)}
|
||||
|
||||
chainedFunction
|
||||
= fn:dollarIdentifier '(' _nl args:array_functionArguments _nl ')'
|
||||
{ return {fnName: fn.value, args: args}}
|
||||
/ fn:dollarIdentifier '(' _nl ')'
|
||||
{ return {fnName: fn.value, args: []}}
|
||||
/ fn:dollarIdentifier
|
||||
{ return {fnName: fn.value, args: []}}
|
||||
|
||||
// end of binary operators
|
||||
|
||||
unary
|
||||
= unaryOperator:unaryOperator _nl right:(unary/postOperator)
|
||||
{ return apply(unaryToFunction[unaryOperator], right)}
|
||||
/ postOperator
|
||||
|
||||
unaryOperator "unary operator"
|
||||
= ('-' / '.-' / '!' )
|
||||
|
||||
postOperator = indexedValue
|
||||
|
||||
indexedValue
|
||||
= collectionElement
|
||||
/ atom
|
||||
|
||||
collectionElement
|
||||
= head:atom &('['/'('/'.')
|
||||
tail:(
|
||||
_ '[' _nl arg:expression _nl ']' {return {fn: postOperatorToFunction['[]'], args: [arg]}}
|
||||
/ _ '(' _nl args:array_functionArguments _nl ')' {return {fn: postOperatorToFunction['()'], args: args}}
|
||||
/ '.' arg:$dollarIdentifier {return {fn: postOperatorToFunction['[]'], args: [nodeString(arg)]}}
|
||||
)*
|
||||
{ return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(element.fn, [result, ...element.args])
|
||||
}, head)}
|
||||
|
||||
array_functionArguments
|
||||
= head:expression tail:(_ ',' _nl @expression)*
|
||||
{ return [head, ...tail]; }
|
||||
|
||||
atom
|
||||
= '(' _nl expression:expression _nl ')' {return expression}
|
||||
/ basicValue
|
||||
|
||||
basicValue = valueConstructor / basicLiteral
|
||||
|
||||
basicLiteral
|
||||
= string
|
||||
/ number
|
||||
/ boolean
|
||||
/ dollarIdentifierWithModule
|
||||
/ dollarIdentifier
|
||||
|
||||
dollarIdentifierWithModule 'identifier'
|
||||
= head:moduleIdentifier
|
||||
tail:('.' _nl @moduleIdentifier)* '.' _nl
|
||||
final:dollarIdentifier
|
||||
{ tail.push(final);
|
||||
return tail.reduce(function(result, element) {
|
||||
return makeFunctionCall(postOperatorToFunction['[]'], [result, element])
|
||||
}, head)}
|
||||
|
||||
identifier 'identifier'
|
||||
= ([_a-z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
|
||||
|
||||
unitIdentifier 'identifier'
|
||||
= ([_a-zA-Z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
|
||||
|
||||
dollarIdentifier '$identifier'
|
||||
= ([\$_a-z]+[\$_a-z0-9]i*) {return nodeIdentifier(text())}
|
||||
|
||||
moduleIdentifier 'identifier'
|
||||
= ([A-Z]+[_a-z0-9]i*) {return nodeIdentifier(text())}
|
||||
|
||||
|
||||
string 'string'
|
||||
= characters:("'" @([^'])* "'") {return nodeString(characters.join(''))}
|
||||
/ characters:('"' @([^"])* '"') {return nodeString(characters.join(''))}
|
||||
|
||||
number = number:(float / integer) unit:unitIdentifier?
|
||||
{
|
||||
if (unit === null)
|
||||
{ return number }
|
||||
else
|
||||
{ return apply('fromUnit_'+unit.value, number)
|
||||
}
|
||||
}
|
||||
|
||||
integer 'integer'
|
||||
= d+ !"\." ![e]i
|
||||
{ return nodeInteger(parseInt(text()))}
|
||||
|
||||
float 'float'
|
||||
= $(((d+ "\." d*) / ("\." d+)) floatExponent? / d+ floatExponent)
|
||||
{ return nodeFloat(parseFloat(text()))}
|
||||
|
||||
floatExponent = [e]i '-'? d+
|
||||
d = [0-9]
|
||||
|
||||
boolean 'boolean'
|
||||
= ('true'/'false')
|
||||
{ return nodeBoolean(text() === 'true')}
|
||||
|
||||
valueConstructor
|
||||
= recordConstructor
|
||||
/ arrayConstructor
|
||||
/ lambda
|
||||
/ quotedInnerBlock
|
||||
|
||||
lambda
|
||||
= '{' _nl '|' _nl args:array_parameters _nl '|' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}'
|
||||
{ statements.push(finalExpression)
|
||||
return nodeLambda(args, nodeBlock(statements)) }
|
||||
/ '{' _nl '|' _nl args:array_parameters _nl '|' _nl finalExpression: expression _nl '}'
|
||||
{ return nodeLambda(args, nodeBlock([finalExpression])) }
|
||||
|
||||
arrayConstructor 'array'
|
||||
= '[' _nl ']'
|
||||
{ return constructArray([]); }
|
||||
/ '[' _nl args:array_elements _nl ']'
|
||||
{ return constructArray(args); }
|
||||
|
||||
array_elements
|
||||
= head:expression tail:(_ ',' _nl @expression)*
|
||||
{ return [head, ...tail]; }
|
||||
|
||||
recordConstructor 'record'
|
||||
= '{' _nl args:array_recordArguments _nl '}'
|
||||
{ return constructRecord(args); }
|
||||
|
||||
array_recordArguments
|
||||
= head:keyValuePair tail:(_ ',' _nl @keyValuePair)*
|
||||
{ return [head, ...tail]; }
|
||||
|
||||
keyValuePair
|
||||
= key:expression _ ':' _nl value:expression
|
||||
{ return nodeKeyValue(key, value)}
|
||||
|
||||
// Separators
|
||||
|
||||
_ 'whitespace'
|
||||
= whiteSpaceCharactersOrComment*
|
||||
|
||||
_nl 'whitespace or newline'
|
||||
= (whiteSpaceCharactersOrComment / commentOrNewLine)*
|
||||
|
||||
__ 'whitespace'
|
||||
= whiteSpaceCharactersOrComment+
|
||||
|
||||
__nl 'whitespace or newline'
|
||||
= (whiteSpaceCharactersOrComment / commentOrNewLine )+
|
||||
|
||||
statementSeparator 'statement separator'
|
||||
= _ (';'/ commentOrNewLine)+ _nl
|
||||
|
||||
commentOrNewLine = finalComment? newLine
|
||||
|
||||
finalComment "line comment"
|
||||
= _ ('//'/'#') @([^\r\n]*)
|
||||
|
||||
whiteSpaceCharactersOrComment = whiteSpaceCharacters / delimitedComment
|
||||
|
||||
delimitedComment "comment"
|
||||
= '/*' @([^*]*) '*/'
|
||||
|
||||
whiteSpaceCharacters = [ \t]
|
||||
|
||||
newLine "newline"
|
||||
= [\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
|
|
@ -0,0 +1,114 @@
|
|||
module Extra = Reducer_Extra
|
||||
open Reducer_ErrorValue
|
||||
|
||||
type node = {"type": string}
|
||||
|
||||
@module("./Reducer_Peggy_GeneratedParser.js") external parse__: string => node = "parse"
|
||||
|
||||
let parse = (expr: string): result<node, errorValue> =>
|
||||
try {
|
||||
Ok(parse__(expr))
|
||||
} catch {
|
||||
| Js.Exn.Error(obj) => REJavaScriptExn(Js.Exn.message(obj), Js.Exn.name(obj))->Error
|
||||
}
|
||||
|
||||
type nodeBlock = {...node, "statements": array<node>}
|
||||
type nodeBoolean = {...node, "value": bool}
|
||||
type nodeCallIdentifier = {...node, "value": string}
|
||||
type nodeExpression = {...node, "nodes": array<node>}
|
||||
type nodeFloat = {...node, "value": float}
|
||||
type nodeIdentifier = {...node, "value": string}
|
||||
type nodeInteger = {...node, "value": int}
|
||||
type nodeKeyValue = {...node, "key": node, "value": node}
|
||||
type nodeLambda = {...node, "args": array<nodeIdentifier>, "body": nodeBlock}
|
||||
type nodeLetStatement = {...node, "variable": nodeIdentifier, "value": node}
|
||||
type nodeString = {...node, "value": string}
|
||||
type nodeTernary = {...node, "condition": node, "trueExpression": node, "falseExpression": node}
|
||||
type nodeTypeIdentifier = {...node, "value": string}
|
||||
|
||||
type peggyNode =
|
||||
| PgNodeBlock(nodeBlock)
|
||||
| PgNodeBoolean(nodeBoolean)
|
||||
| PgNodeCallIdentifier(nodeCallIdentifier)
|
||||
| PgNodeExpression(nodeExpression)
|
||||
| PgNodeFloat(nodeFloat)
|
||||
| PgNodeIdentifier(nodeIdentifier)
|
||||
| PgNodeInteger(nodeInteger)
|
||||
| PgNodeKeyValue(nodeKeyValue)
|
||||
| PgNodeLambda(nodeLambda)
|
||||
| PgNodeLetStatement(nodeLetStatement)
|
||||
| PgNodeString(nodeString)
|
||||
| PgNodeTernary(nodeTernary)
|
||||
| PgNodeTypeIdentifier(nodeTypeIdentifier)
|
||||
|
||||
external castNodeBlock: node => nodeBlock = "%identity"
|
||||
external castNodeBoolean: node => nodeBoolean = "%identity"
|
||||
external castNodeCallIdentifier: node => nodeCallIdentifier = "%identity"
|
||||
external castNodeExpression: node => nodeExpression = "%identity"
|
||||
external castNodeFloat: node => nodeFloat = "%identity"
|
||||
external castNodeIdentifier: node => nodeIdentifier = "%identity"
|
||||
external castNodeInteger: node => nodeInteger = "%identity"
|
||||
external castNodeKeyValue: node => nodeKeyValue = "%identity"
|
||||
external castNodeLambda: node => nodeLambda = "%identity"
|
||||
external castNodeLetStatement: node => nodeLetStatement = "%identity"
|
||||
external castNodeString: node => nodeString = "%identity"
|
||||
external castNodeTernary: node => nodeTernary = "%identity"
|
||||
external castNodeTypeIdentifier: node => nodeTypeIdentifier = "%identity"
|
||||
|
||||
exception UnsupportedPeggyNodeType(string) // This should never happen; programming error
|
||||
let castNodeType = (node: node) =>
|
||||
switch node["type"] {
|
||||
| "Block" => node->castNodeBlock->PgNodeBlock
|
||||
| "Boolean" => node->castNodeBoolean->PgNodeBoolean
|
||||
| "CallIdentifier" => node->castNodeCallIdentifier->PgNodeCallIdentifier
|
||||
| "Expression" => node->castNodeExpression->PgNodeExpression
|
||||
| "Float" => node->castNodeFloat->PgNodeFloat
|
||||
| "Identifier" => node->castNodeIdentifier->PgNodeIdentifier
|
||||
| "Integer" => node->castNodeInteger->PgNodeInteger
|
||||
| "KeyValue" => node->castNodeKeyValue->PgNodeKeyValue
|
||||
| "Lambda" => node->castNodeLambda->PgNodeLambda
|
||||
| "LetStatement" => node->castNodeLetStatement->PgNodeLetStatement
|
||||
| "String" => node->castNodeString->PgNodeString
|
||||
| "Ternary" => node->castNodeTernary->PgNodeTernary
|
||||
| "TypeIdentifier" => node->castNodeTypeIdentifier->PgNodeTypeIdentifier
|
||||
| _ => raise(UnsupportedPeggyNodeType(node["type"]))
|
||||
}
|
||||
|
||||
let rec pgToString = (peggyNode: peggyNode): string => {
|
||||
let argsToString = (args: array<nodeIdentifier>): string =>
|
||||
args->Js.Array2.map(arg => PgNodeIdentifier(arg)->pgToString)->Js.Array2.toString
|
||||
|
||||
let nodesToStringUsingSeparator = (nodes: array<node>, separator: string): string =>
|
||||
nodes->Js.Array2.map(toString)->Extra.Array.interperse(separator)->Js.String.concatMany("")
|
||||
|
||||
switch peggyNode {
|
||||
| PgNodeBlock(node) => "{" ++ node["statements"]->nodesToStringUsingSeparator("; ") ++ "}"
|
||||
| PgNodeBoolean(node) => node["value"]->Js.String.make
|
||||
| PgNodeCallIdentifier(node) => `::${Js.String.make(node["value"])}` // This is an identifier also but for function names
|
||||
| PgNodeExpression(node) => "(" ++ node["nodes"]->nodesToStringUsingSeparator(" ") ++ ")"
|
||||
| PgNodeFloat(node) => node["value"]->Js.String.make
|
||||
| PgNodeIdentifier(node) => `:${node["value"]}`
|
||||
| PgNodeInteger(node) => node["value"]->Js.String.make
|
||||
| PgNodeKeyValue(node) => toString(node["key"]) ++ ": " ++ toString(node["value"])
|
||||
| PgNodeLambda(node) =>
|
||||
"{|" ++ node["args"]->argsToString ++ "| " ++ pgToString(PgNodeBlock(node["body"])) ++ "}"
|
||||
| PgNodeLetStatement(node) =>
|
||||
pgToString(PgNodeIdentifier(node["variable"])) ++ " = " ++ toString(node["value"])
|
||||
| PgNodeString(node) => `'${node["value"]->Js.String.make}'`
|
||||
| PgNodeTernary(node) =>
|
||||
"(::$$_ternary_$$ " ++
|
||||
toString(node["condition"]) ++
|
||||
" " ++
|
||||
toString(node["trueExpression"]) ++
|
||||
" " ++
|
||||
toString(node["falseExpression"]) ++ ")"
|
||||
| PgNodeTypeIdentifier(node) => `#${node["value"]}`
|
||||
}
|
||||
}
|
||||
and toString = (node: node): string => node->castNodeType->pgToString
|
||||
|
||||
let toStringResult = (rNode: result<node, errorValue>): string =>
|
||||
switch rNode {
|
||||
| Ok(node) => toString(node)
|
||||
| Error(error) => `Error(${errorToString(error)})`
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
module ExpressionBuilder = Reducer_Expression_ExpressionBuilder
|
||||
module ExpressionT = Reducer_Expression_T
|
||||
module Parse = Reducer_Peggy_Parse
|
||||
|
||||
type expression = ExpressionT.expression
|
||||
|
||||
let rec fromNode = (node: Parse.node): expression => {
|
||||
let caseBlock = nodeBlock =>
|
||||
ExpressionBuilder.eBlock(nodeBlock["statements"]->Js.Array2.map(fromNode)->Belt.List.fromArray)
|
||||
|
||||
let caseLambda = (nodeLambda: Parse.nodeLambda): expression => {
|
||||
let args =
|
||||
nodeLambda["args"]
|
||||
->Js.Array2.map((argNode: Parse.nodeIdentifier) => argNode["value"])
|
||||
->ExpressionBuilder.eArrayString
|
||||
let body = nodeLambda["body"]->caseBlock
|
||||
ExpressionBuilder.eFunction("$$_lambda_$$", list{args, body})
|
||||
}
|
||||
|
||||
switch Parse.castNodeType(node) {
|
||||
| PgNodeBlock(nodeBlock) => caseBlock(nodeBlock)
|
||||
| PgNodeBoolean(nodeBoolean) => ExpressionBuilder.eBool(nodeBoolean["value"])
|
||||
| PgNodeCallIdentifier(nodeCallIdentifier) => ExpressionBuilder.eCall(nodeCallIdentifier["value"])
|
||||
| PgNodeExpression(nodeExpression) =>
|
||||
ExpressionT.EList(nodeExpression["nodes"]->Js.Array2.map(fromNode)->Belt.List.fromArray)
|
||||
| PgNodeFloat(nodeFloat) => ExpressionBuilder.eNumber(nodeFloat["value"])
|
||||
| PgNodeIdentifier(nodeIdentifier) => ExpressionBuilder.eSymbol(nodeIdentifier["value"])
|
||||
| PgNodeInteger(nodeInteger) => ExpressionBuilder.eNumber(Belt.Int.toFloat(nodeInteger["value"]))
|
||||
| PgNodeKeyValue(nodeKeyValue) =>
|
||||
ExpressionT.EList(list{fromNode(nodeKeyValue["key"]), fromNode(nodeKeyValue["value"])})
|
||||
| PgNodeLambda(nodeLambda) => caseLambda(nodeLambda)
|
||||
| PgNodeLetStatement(nodeLetStatement) =>
|
||||
ExpressionBuilder.eLetStatement(
|
||||
nodeLetStatement["variable"]["value"],
|
||||
fromNode(nodeLetStatement["value"]),
|
||||
)
|
||||
| PgNodeString(nodeString) => ExpressionBuilder.eString(nodeString["value"])
|
||||
| PgNodeTernary(nodeTernary) =>
|
||||
ExpressionBuilder.eFunction(
|
||||
"$$_ternary_$$",
|
||||
list{
|
||||
fromNode(nodeTernary["condition"]),
|
||||
fromNode(nodeTernary["trueExpression"]),
|
||||
fromNode(nodeTernary["falseExpression"]),
|
||||
},
|
||||
)
|
||||
| PgNodeTypeIdentifier(nodeTypeIdentifier) =>
|
||||
ExpressionBuilder.eTypeIdentifier(nodeTypeIdentifier["value"])
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user