Merge branch 'develop' into metalog

This commit is contained in:
Sam Nolan 2022-08-13 13:39:10 +01:00
commit ad73ff98cc
144 changed files with 6058 additions and 3026 deletions

View File

@ -11,3 +11,10 @@ updates:
interval: "weekly" interval: "weekly"
commit-message: commit-message:
prefix: "⬆️" prefix: "⬆️"
open-pull-requests-limit: 100
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "⬆️"

View File

@ -24,27 +24,27 @@ jobs:
steps: steps:
- id: skip_lang_check - id: skip_lang_check
name: Check if the changes are about squiggle-lang src files name: Check if the changes are about squiggle-lang src files
uses: fkirc/skip-duplicate-actions@v3.4.1 uses: fkirc/skip-duplicate-actions@v4.0.0
with: with:
paths: '["packages/squiggle-lang/**"]' paths: '["packages/squiggle-lang/**"]'
- id: skip_components_check - id: skip_components_check
name: Check if the changes are about components src files name: Check if the changes are about components src files
uses: fkirc/skip-duplicate-actions@v3.4.1 uses: fkirc/skip-duplicate-actions@v4.0.0
with: with:
paths: '["packages/components/**"]' paths: '["packages/components/**"]'
- id: skip_website_check - id: skip_website_check
name: Check if the changes are about website src files name: Check if the changes are about website src files
uses: fkirc/skip-duplicate-actions@v3.4.1 uses: fkirc/skip-duplicate-actions@v4.0.0
with: with:
paths: '["packages/website/**"]' paths: '["packages/website/**"]'
- id: skip_vscodeext_check - id: skip_vscodeext_check
name: Check if the changes are about vscode extension src files name: Check if the changes are about vscode extension src files
uses: fkirc/skip-duplicate-actions@v3.4.1 uses: fkirc/skip-duplicate-actions@v4.0.0
with: with:
paths: '["packages/vscode-ext/**"]' paths: '["packages/vscode-ext/**"]'
- id: skip_cli_check - id: skip_cli_check
name: Check if the changes are about cli src files name: Check if the changes are about cli src files
uses: fkirc/skip-duplicate-actions@v3.4.1 uses: fkirc/skip-duplicate-actions@v4.0.0
with: with:
paths: '["packages/cli/**"]' paths: '["packages/cli/**"]'
@ -58,7 +58,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/squiggle-lang working-directory: packages/squiggle-lang
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install Dependencies - name: Install Dependencies
run: cd ../../ && yarn run: cd ../../ && yarn
- name: Check rescript lint - name: Check rescript lint
@ -79,7 +79,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/squiggle-lang working-directory: packages/squiggle-lang
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 2 fetch-depth: 2
- name: Install dependencies from monorepo level - name: Install dependencies from monorepo level
@ -107,7 +107,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/components working-directory: packages/components
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Check javascript, typescript, and markdown lint - name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2 uses: creyD/prettier_action@v4.2
with: with:
@ -124,7 +124,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/components working-directory: packages/components
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install dependencies from monorepo level - name: Install dependencies from monorepo level
run: cd ../../ && yarn run: cd ../../ && yarn
- name: Build rescript codebase in squiggle-lang - name: Build rescript codebase in squiggle-lang
@ -144,7 +144,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/website working-directory: packages/website
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Check javascript, typescript, and markdown lint - name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2 uses: creyD/prettier_action@v4.2
with: with:
@ -161,7 +161,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/website working-directory: packages/website
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install dependencies from monorepo level - name: Install dependencies from monorepo level
run: cd ../../ && yarn run: cd ../../ && yarn
- name: Build rescript in squiggle-lang - name: Build rescript in squiggle-lang
@ -181,7 +181,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/vscode-ext working-directory: packages/vscode-ext
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install dependencies from monorepo level - name: Install dependencies from monorepo level
run: cd ../../ && yarn run: cd ../../ && yarn
- name: Lint the VSCode Extension source code - name: Lint the VSCode Extension source code
@ -197,7 +197,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/vscode-ext working-directory: packages/vscode-ext
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install dependencies from monorepo level - name: Install dependencies from monorepo level
run: cd ../../ && yarn run: cd ../../ && yarn
- name: Build - name: Build
@ -213,7 +213,7 @@ jobs:
shell: bash shell: bash
working-directory: packages/cli working-directory: packages/cli
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Check javascript, typescript, and markdown lint - name: Check javascript, typescript, and markdown lint
uses: creyD/prettier_action@v4.2 uses: creyD/prettier_action@v4.2
with: with:

View File

@ -33,11 +33,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -48,7 +48,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Build rescript - name: Build rescript
@ -65,4 +65,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

140
.github/workflows/release-please.yml vendored Normal file
View File

@ -0,0 +1,140 @@
name: Run Release Please
on:
push:
branches:
- develop
jobs:
pre_check:
name: Precheck for skipping redundant jobs
runs-on: ubuntu-latest
outputs:
should_skip_lang: ${{ steps.skip_lang_check.outputs.should_skip }}
should_skip_components: ${{ steps.skip_components_check.outputs.should_skip }}
should_skip_website: ${{ steps.skip_website_check.outputs.should_skip }}
should_skip_vscodeext: ${{ steps.skip_vscodeext_check.outputs.should_skip }}
should_skip_cli: ${{ steps.skip_cli_check.outputs.should_skip }}
steps:
- id: skip_lang_check
name: Check if the changes are about squiggle-lang src files
uses: fkirc/skip-duplicate-actions@v4.0.0
with:
paths: '["packages/squiggle-lang/**"]'
- id: skip_components_check
name: Check if the changes are about components src files
uses: fkirc/skip-duplicate-actions@v4.0.0
with:
paths: '["packages/components/**"]'
- id: skip_website_check
name: Check if the changes are about website src files
uses: fkirc/skip-duplicate-actions@v4.0.0
with:
paths: '["packages/website/**"]'
- id: skip_vscodeext_check
name: Check if the changes are about vscode extension src files
uses: fkirc/skip-duplicate-actions@v4.0.0
with:
paths: '["packages/vscode-ext/**"]'
- id: skip_cli_check
name: Check if the changes are about cli src files
uses: fkirc/skip-duplicate-actions@v4.0.0
with:
paths: '["packages/cli/**"]'
relplz-lang:
name: for squiggle-lang
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_lang != 'true' }}
steps:
- name: Release please (squiggle-lang)
uses: google-github-actions/release-please-action@v3
id: release
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/squiggle-lang
bump-patch-for-minor-pre-major: true
skip-github-release: true
# - name: Publish: Checkout source
# uses: actions/checkout@v2
# # these if statements ensure that a publication only occurs when
# # a new release is created:
# if: ${{ steps.release.outputs.release_created }}
# - name: Publish: Install dependencies
# run: yarn
# if: ${{ steps.release.outputs.release_created }}
# - name: Publish
# run: cd packages/squiggle-lang && yarn publish
# env:
# NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
# if: ${{ steps.release.outputs.release_created }}
relplz-components:
name: for components
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_components != 'true' }}
steps:
- name: Release please (components)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/components
bump-patch-for-minor-pre-major: true
skip-github-release: true
# - name: Publish: Checkout source
# uses: actions/checkout@v2
# # these if statements ensure that a publication only occurs when
# # a new release is created:
# if: ${{ steps.release.outputs.release_created }}
# - name: Publish: Install dependencies
# run: yarn
# if: ${{ steps.release.outputs.release_created }}
# - name: Publish
# run: cd packages/components && yarn publish
# env:
# NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
relplz-website:
name: for website
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_website != 'true' }}
steps:
- name: Release please (website)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/website
bump-patch-for-minor-pre-major: true
skip-github-release: true
relplz-vscodeext:
name: for vscode-ext
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_vscodeext != 'true' }}
steps:
- name: Release please (vscode-ext)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/vscode-ext
bump-patch-for-minor-pre-major: true
skip-github-release: true
relplz-cl:
name: for cli
runs-on: ubuntu-latest
needs: pre_check
if: ${{ needs.pre_check.outputs.should_skip_cli != 'true' }}
steps:
- name: Release please (cli)
uses: google-github-actions/release-please-action@v3
with:
token: ${{secrets.GITHUB_TOKEN}}
command: manifest-pr
path: packages/cli
bump-patch-for-minor-pre-major: true
skip-github-release: true

View File

@ -0,0 +1,7 @@
{
"packages/cli": "0.0.3",
"packages/components": "0.3.1",
"packages/squiggle-lang": "0.3.0",
"packages/vscode-ext": "0.3.1",
"packages/website": "0.3.0"
}

View File

@ -12,8 +12,8 @@ _An estimation language_.
- [Gallery](https://www.squiggle-language.com/docs/Discussions/Gallery) - [Gallery](https://www.squiggle-language.com/docs/Discussions/Gallery)
- [Squiggle playground](https://squiggle-language.com/playground) - [Squiggle playground](https://squiggle-language.com/playground)
- [Language basics](https://www.squiggle-language.com/docs/Features/Language) - [Language basics](https://www.squiggle-language.com/docs/Guides/Language)
- [Squiggle functions source of truth](https://www.squiggle-language.com/docs/Features/Functions) - [Squiggle functions source of truth](https://www.squiggle-language.com/docs/Guides/Functions)
- [Known bugs](https://www.squiggle-language.com/docs/Discussions/Bugs) - [Known bugs](https://www.squiggle-language.com/docs/Discussions/Bugs)
- [Original lesswrong sequence](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3) - [Original lesswrong sequence](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3)
- [Author your squiggle models as Observable notebooks](https://observablehq.com/@hazelfire/squiggle) - [Author your squiggle models as Observable notebooks](https://observablehq.com/@hazelfire/squiggle)

View File

@ -54,7 +54,6 @@ export function DynamicSquiggleChart({ squiggleString }) {
width={445} width={445}
height={200} height={200}
showSummary={true} showSummary={true}
showTypes={true}
/> />
); );
} }

View File

@ -1,43 +1,45 @@
{ {
"name": "@quri/squiggle-components", "name": "@quri/squiggle-components",
"version": "0.2.23", "version": "0.3.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/react-dom": "^1.0.0",
"@floating-ui/react-dom-interactions": "^0.9.1",
"@headlessui/react": "^1.6.6", "@headlessui/react": "^1.6.6",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^2.9.6", "@hookform/resolvers": "^2.9.7",
"@quri/squiggle-lang": "^0.2.8", "@quri/squiggle-lang": "^0.3.0",
"@react-hook/size": "^2.1.2", "@react-hook/size": "^2.1.2",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"framer-motion": "^6.5.1", "framer-motion": "^7.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.1.0", "react": "^18.1.0",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-hook-form": "^7.33.1", "react-hook-form": "^7.34.0",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"react-vega": "^7.6.0", "react-vega": "^7.6.0",
"vega": "^5.22.1", "vega": "^5.22.1",
"vega-embed": "^6.21.0", "vega-embed": "^6.21.0",
"vega-lite": "^5.3.0", "vega-lite": "^5.4.0",
"vscode-uri": "^3.0.3", "vscode-uri": "^3.0.3",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.18.6",
"@storybook/addon-actions": "^6.5.9", "@storybook/addon-actions": "^6.5.9",
"@storybook/addon-essentials": "^6.5.9", "@storybook/addon-essentials": "^6.5.10",
"@storybook/addon-links": "^6.5.9", "@storybook/addon-links": "^6.5.10",
"@storybook/builder-webpack5": "^6.5.9", "@storybook/builder-webpack5": "^6.5.10",
"@storybook/manager-webpack5": "^6.5.9", "@storybook/manager-webpack5": "^6.5.10",
"@storybook/node-logger": "^6.5.9", "@storybook/node-logger": "^6.5.9",
"@storybook/preset-create-react-app": "^4.1.2", "@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.9", "@storybook/react": "^6.5.10",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.6", "@testing-library/user-event": "^14.4.2",
"@types/jest": "^27.5.0", "@types/jest": "^27.5.0",
"@types/lodash": "^4.14.182", "@types/lodash": "^4.14.182",
"@types/node": "^18.0.6", "@types/node": "^18.6.4",
"@types/react": "^18.0.9", "@types/react": "^18.0.9",
"@types/styled-components": "^5.1.24", "@types/styled-components": "^5.1.24",
"@types/webpack": "^5.28.0", "@types/webpack": "^5.28.0",
@ -49,12 +51,12 @@
"react": "^18.1.0", "react": "^18.1.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"tailwindcss": "^3.1.6", "tailwindcss": "^3.1.8",
"ts-loader": "^9.3.0", "ts-loader": "^9.3.0",
"tsconfig-paths-webpack-plugin": "^3.5.2", "tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"webpack": "^5.73.0", "webpack": "^5.74.0",
"webpack-cli": "^4.10.0", "webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3" "webpack-dev-server": "^4.9.3"
}, },

View File

@ -8,26 +8,24 @@ import {
import { Vega } from "react-vega"; import { Vega } from "react-vega";
import { ErrorAlert } from "./Alert"; import { ErrorAlert } from "./Alert";
import { useSize } from "react-use"; import { useSize } from "react-use";
import clsx from "clsx";
import { import {
buildVegaSpec, buildVegaSpec,
DistributionChartSpecOptions, DistributionChartSpecOptions,
} from "../lib/distributionSpecBuilder"; } from "../lib/distributionSpecBuilder";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
import { hasMassBelowZero } from "../lib/distributionUtils";
export type DistributionPlottingSettings = { export type DistributionPlottingSettings = {
/** Whether to show a summary of means, stdev, percentiles etc */ /** Whether to show a summary of means, stdev, percentiles etc */
showSummary: boolean; showSummary: boolean;
/** Whether to show the user graph controls (scale etc) */ actions?: boolean;
showControls: boolean;
} & DistributionChartSpecOptions; } & DistributionChartSpecOptions;
export type DistributionChartProps = { export type DistributionChartProps = {
distribution: Distribution; distribution: Distribution;
width?: number; width?: number;
height: number; height: number;
actions?: boolean;
} & DistributionPlottingSettings; } & DistributionPlottingSettings;
export const DistributionChart: React.FC<DistributionChartProps> = (props) => { export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
@ -36,17 +34,9 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
height, height,
showSummary, showSummary,
width, width,
showControls,
logX, logX,
expY,
actions = false, actions = false,
} = props; } = props;
const [isLogX, setLogX] = React.useState(logX);
const [isExpY, setExpY] = React.useState(expY);
React.useEffect(() => setLogX(logX), [logX]);
React.useEffect(() => setExpY(expY), [expY]);
const shape = distribution.pointSet(); const shape = distribution.pointSet();
const [sized] = useSize((size) => { const [sized] = useSize((size) => {
if (shape.tag === "Error") { if (shape.tag === "Error") {
@ -57,9 +47,6 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
); );
} }
const massBelow0 =
shape.value.continuous.some((x) => x.x <= 0) ||
shape.value.discrete.some((x) => x.x <= 0);
const spec = buildVegaSpec(props); const spec = buildVegaSpec(props);
let widthProp = width ? width : size.width; let widthProp = width ? width : size.width;
@ -72,7 +59,11 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
return ( return (
<div style={{ width: widthProp }}> <div style={{ width: widthProp }}>
{!(isLogX && massBelow0) ? ( {logX && hasMassBelowZero(shape.value) ? (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
) : (
<Vega <Vega
spec={spec} spec={spec}
data={{ con: shape.value.continuous, dis: shape.value.discrete }} data={{ con: shape.value.continuous, dis: shape.value.discrete }}
@ -80,67 +71,16 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
height={height} height={height}
actions={actions} actions={actions}
/> />
) : (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
)} )}
<div className="flex justify-center"> <div className="flex justify-center">
{showSummary && <SummaryTable distribution={distribution} />} {showSummary && <SummaryTable distribution={distribution} />}
</div> </div>
{showControls && (
<div>
<CheckBox
label="Log X scale"
value={isLogX}
onChange={setLogX}
// Check whether we should disable the checkbox
{...(massBelow0
? {
disabled: true,
tooltip:
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.",
}
: {})}
/>
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
</div>
)}
</div> </div>
); );
}); });
return sized; return sized;
}; };
interface CheckBoxProps {
label: string;
onChange: (x: boolean) => void;
value: boolean;
disabled?: boolean;
tooltip?: string;
}
export const CheckBox: React.FC<CheckBoxProps> = ({
label,
onChange,
value,
disabled = false,
tooltip,
}) => {
return (
<span title={tooltip}>
<input
type="checkbox"
checked={value}
onChange={() => onChange(!value)}
disabled={disabled}
className="form-checkbox"
/>
<label className={clsx(disabled && "text-slate-400")}> {label}</label>
</span>
);
};
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({ const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
children, children,
}) => ( }) => (

View File

@ -1,5 +1,10 @@
import * as React from "react"; import * as React from "react";
import { lambdaValue, environment, runForeign } from "@quri/squiggle-lang"; import {
lambdaValue,
environment,
runForeign,
errorValueToString,
} from "@quri/squiggle-lang";
import { FunctionChart1Dist } from "./FunctionChart1Dist"; import { FunctionChart1Dist } from "./FunctionChart1Dist";
import { FunctionChart1Number } from "./FunctionChart1Number"; import { FunctionChart1Number } from "./FunctionChart1Number";
import { DistributionPlottingSettings } from "./DistributionChart"; import { DistributionPlottingSettings } from "./DistributionChart";
@ -45,10 +50,16 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
} }
}; };
const validResult = getValidResult(); const validResult = getValidResult();
const resultType =
validResult.tag === "Ok" ? validResult.value.tag : ("Error" as const);
switch (resultType) { if (validResult.tag === "Error") {
return (
<ErrorAlert heading="Error">
{errorValueToString(validResult.value)}
</ErrorAlert>
);
}
switch (validResult.value.tag) {
case "distribution": case "distribution":
return ( return (
<FunctionChart1Dist <FunctionChart1Dist
@ -68,15 +79,11 @@ export const FunctionChart: React.FC<FunctionChartProps> = ({
height={height} height={height}
/> />
); );
case "Error":
return (
<ErrorAlert heading="Error">The function failed to be run</ErrorAlert>
);
default: default:
return ( return (
<MessageAlert heading="Function Display Not Supported"> <MessageAlert heading="Function Display Not Supported">
There is no function visualization for this type of output:{" "} There is no function visualization for this type of output:{" "}
<span className="font-bold">{resultType}</span> <span className="font-bold">{validResult.value.tag}</span>
</MessageAlert> </MessageAlert>
); );
} }

View File

@ -88,7 +88,7 @@ let getPercentiles = ({ chartSettings, fn, environment }) => {
let chartPointsData: point[] = chartPointsToRender.map((x) => { let chartPointsData: point[] = chartPointsToRender.map((x) => {
let result = runForeign(fn, [x], environment); let result = runForeign(fn, [x], environment);
if (result.tag === "Ok") { if (result.tag === "Ok") {
if (result.value.tag == "distribution") { if (result.value.tag === "distribution") {
return { x, value: { tag: "Ok", value: result.value.value } }; return { x, value: { tag: "Ok", value: result.value.value } };
} else { } else {
return { return {
@ -165,12 +165,14 @@ export const FunctionChart1Dist: React.FC<FunctionChart1DistProps> = ({
setMouseOverlay(NaN); setMouseOverlay(NaN);
} }
const signalListeners = { mousemove: handleHover, mouseout: handleOut }; const signalListeners = { mousemove: handleHover, mouseout: handleOut };
//TODO: This custom error handling is a bit hacky and should be improved.
let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay let mouseItem: result<squiggleExpression, errorValue> = !!mouseOverlay
? runForeign(fn, [mouseOverlay], environment) ? runForeign(fn, [mouseOverlay], environment)
: { : {
tag: "Error", tag: "Error",
value: { value: {
tag: "REExpectedType", tag: "RETodo",
value: "Hover x-coordinate returned NaN. Expected a number.", value: "Hover x-coordinate returned NaN. Expected a number.",
}, },
}; };

View File

@ -39,13 +39,10 @@ interface FunctionChart1NumberProps {
type point = { x: number; value: result<number, string> }; type point = { x: number; value: result<number, string> };
let getFunctionImage = ({ chartSettings, fn, environment }) => { 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( let chartPointsToRender = _rangeByCount(
chartSettings.start, chartSettings.start,
chartSettings.stop, chartSettings.stop,
adjustedCount chartSettings.count
); );
let chartPointsData: point[] = chartPointsToRender.map((x) => { let chartPointsData: point[] = chartPointsToRender.map((x) => {

View File

@ -9,12 +9,13 @@ import {
defaultEnvironment, defaultEnvironment,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { useSquiggle } from "../lib/hooks"; import { useSquiggle } from "../lib/hooks";
import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; import { SquiggleViewer } from "./SquiggleViewer";
import { SquiggleItem } from "./SquiggleItem";
export interface SquiggleChartProps { export interface SquiggleChartProps {
/** The input string for squiggle */ /** The input string for squiggle */
code?: string; code?: string;
/** Allows to re-run the code if code hasn't changed */
executionId?: number;
/** If the output requires monte carlo sampling, the amount of samples */ /** If the output requires monte carlo sampling, the amount of samples */
sampleCount?: number; sampleCount?: number;
/** The amount of points returned to draw the distribution */ /** The amount of points returned to draw the distribution */
@ -36,10 +37,6 @@ export interface SquiggleChartProps {
jsImports?: jsImports; jsImports?: jsImports;
/** Whether to show a summary of the distribution */ /** Whether to show a summary of the distribution */
showSummary?: boolean; showSummary?: boolean;
/** Whether to show type information about returns, default false */
showTypes?: boolean;
/** Whether to show graph controls (scale etc)*/
showControls?: boolean;
/** Set the x scale to be logarithmic by deault */ /** Set the x scale to be logarithmic by deault */
logX?: boolean; logX?: boolean;
/** Set the y scale to be exponential by deault */ /** Set the y scale to be exponential by deault */
@ -56,6 +53,7 @@ export interface SquiggleChartProps {
maxX?: number; maxX?: number;
/** Whether to show vega actions to the user, so they can copy the chart spec */ /** Whether to show vega actions to the user, so they can copy the chart spec */
distributionChartActions?: boolean; distributionChartActions?: boolean;
enableLocalSettings?: boolean;
} }
const defaultOnChange = () => {}; const defaultOnChange = () => {};
@ -63,6 +61,7 @@ const defaultOnChange = () => {};
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo( export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
({ ({
code = "", code = "",
executionId = 0,
environment, environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200, height = 200,
@ -70,19 +69,18 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
jsImports = defaultImports, jsImports = defaultImports,
showSummary = false, showSummary = false,
width, width,
showTypes = false,
showControls = false,
logX = false, logX = false,
expY = false, expY = false,
diagramStart = 0, diagramStart = 0,
diagramStop = 10, diagramStop = 10,
diagramCount = 100, diagramCount = 20,
tickFormat, tickFormat,
minX, minX,
maxX, maxX,
color, color,
title, title,
distributionChartActions, distributionChartActions,
enableLocalSettings = false,
}) => { }) => {
const result = useSquiggle({ const result = useSquiggle({
code, code,
@ -90,14 +88,10 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
environment, environment,
jsImports, jsImports,
onChange, onChange,
executionId,
}); });
if (result.tag !== "Ok") { const distributionPlotSettings = {
return <SquiggleErrorAlert error={result.value} />;
}
let distributionPlotSettings = {
showControls,
showSummary, showSummary,
logX, logX,
expY, expY,
@ -109,21 +103,21 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
actions: distributionChartActions, actions: distributionChartActions,
}; };
let chartSettings = { const chartSettings = {
start: diagramStart, start: diagramStart,
stop: diagramStop, stop: diagramStop,
count: diagramCount, count: diagramCount,
}; };
return ( return (
<SquiggleItem <SquiggleViewer
expression={result.value} result={result}
width={width} width={width}
height={height} height={height}
distributionPlotSettings={distributionPlotSettings} distributionPlotSettings={distributionPlotSettings}
showTypes={showTypes}
chartSettings={chartSettings} chartSettings={chartSettings}
environment={environment ?? defaultEnvironment} environment={environment ?? defaultEnvironment}
enableLocalSettings={enableLocalSettings}
/> />
); );
} }

View File

@ -13,6 +13,7 @@ const SquiggleContext = React.createContext<SquiggleContextShape>({
export const SquiggleContainer: React.FC<Props> = ({ children }) => { export const SquiggleContainer: React.FC<Props> = ({ children }) => {
const context = useContext(SquiggleContext); const context = useContext(SquiggleContext);
if (context.containerized) { if (context.containerized) {
return <>{children}</>; return <>{children}</>;
} else { } else {

View File

@ -0,0 +1,52 @@
import React from "react";
import { SquiggleEditor } from "./SquiggleEditor";
import type { SquiggleEditorProps } from "./SquiggleEditor";
import { runPartial, defaultBindings } from "@quri/squiggle-lang";
import type {
result,
errorValue,
bindings as bindingsType,
} from "@quri/squiggle-lang";
function resultDefault(x: result<bindingsType, errorValue>): bindingsType {
switch (x.tag) {
case "Ok":
return x.value;
case "Error":
return defaultBindings;
}
}
export type SquiggleEditorWithImportedBindingsProps = SquiggleEditorProps & {
bindingsImportUrl: string;
};
export const SquiggleEditorWithImportedBindings: React.FC<
SquiggleEditorWithImportedBindingsProps
> = (props) => {
const { bindingsImportUrl, ...editorProps } = props;
const [bindingsResult, setBindingsResult] = React.useState({
tag: "Ok",
value: defaultBindings,
} as result<bindingsType, errorValue>);
React.useEffect(() => {
async function retrieveBindings(fileName: string) {
let contents = await fetch(fileName).then((response) => {
return response.text();
});
setBindingsResult(
runPartial(
contents,
editorProps.bindings,
editorProps.environment,
editorProps.jsImports
)
);
}
retrieveBindings(bindingsImportUrl);
}, [bindingsImportUrl]);
const deliveredBindings = resultDefault(bindingsResult);
return (
<SquiggleEditor {...{ ...editorProps, bindings: deliveredBindings }} />
);
};

View File

@ -1,281 +0,0 @@
import * as React from "react";
import {
squiggleExpression,
environment,
declaration,
} from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower";
import {
DistributionChart,
DistributionPlottingSettings,
} from "./DistributionChart";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart";
function getRange<a>(x: declaration<a>) {
const first = x.args[0];
switch (first.tag) {
case "Float": {
return { floats: { min: first.value.min, max: first.value.max } };
}
case "Date": {
return { time: { min: first.value.min, max: first.value.max } };
}
}
}
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
const range = getRange(x);
const min = range.floats ? range.floats.min : 0;
const max = range.floats ? range.floats.max : 10;
return {
start: min,
stop: max,
count: 20,
};
}
interface VariableBoxProps {
heading: string;
children: React.ReactNode;
showTypes: boolean;
}
export const VariableBox: React.FC<VariableBoxProps> = ({
heading = "Error",
children,
showTypes = false,
}) => {
if (showTypes) {
return (
<div className="bg-white border border-grey-200 m-2">
<div className="border-b border-grey-200 p-3">
<header className="font-mono">{heading}</header>
</div>
<div className="p-3">{children}</div>
</div>
);
} else {
return <div>{children}</div>;
}
};
export interface SquiggleItemProps {
/** The input string for squiggle */
expression: squiggleExpression;
width?: number;
height: number;
distributionPlotSettings: DistributionPlottingSettings;
/** Whether to show type information */
showTypes: boolean;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
}
export const SquiggleItem: React.FC<SquiggleItemProps> = ({
expression,
width,
height,
distributionPlotSettings,
showTypes = false,
chartSettings,
environment,
}) => {
switch (expression.tag) {
case "number":
return (
<VariableBox heading="Number" showTypes={showTypes}>
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={expression.value} />
</div>
</VariableBox>
);
case "distribution": {
const distType = expression.value.type();
return (
<VariableBox
heading={`Distribution (${distType})`}
showTypes={showTypes}
>
{distType === "Symbolic" && showTypes ? (
<div>{expression.value.toString()}</div>
) : null}
<DistributionChart
distribution={expression.value}
{...distributionPlotSettings}
height={height}
width={width}
/>
</VariableBox>
);
}
case "string":
return (
<VariableBox heading="String" showTypes={showTypes}>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold">
{expression.value}
</span>
<span className="text-slate-400">"</span>
</VariableBox>
);
case "boolean":
return (
<VariableBox heading="Boolean" showTypes={showTypes}>
{expression.value.toString()}
</VariableBox>
);
case "symbol":
return (
<VariableBox heading="Symbol" showTypes={showTypes}>
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span>
</VariableBox>
);
case "call":
return (
<VariableBox heading="Call" showTypes={showTypes}>
{expression.value}
</VariableBox>
);
case "array":
return (
<VariableBox heading="Array" showTypes={showTypes}>
{expression.value.map((r, i) => (
<div key={i} className="flex pt-1">
<div className="flex-none bg-slate-100 rounded-sm px-1">
<header className="text-slate-400 font-mono">{i}</header>
</div>
<div className="px-2 mb-2 grow">
<SquiggleItem
key={i}
expression={r}
width={width !== undefined ? width - 20 : width}
height={50}
distributionPlotSettings={distributionPlotSettings}
showTypes={showTypes}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div>
))}
</VariableBox>
);
case "record":
return (
<VariableBox heading="Record" showTypes={showTypes}>
<div className="space-y-3">
{Object.entries(expression.value).map(([key, r]) => (
<div key={key} className="flex space-x-2">
<div className="flex-none">
<header className="text-slate-500 font-mono">{key}:</header>
</div>
<div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
<SquiggleItem
expression={r}
width={width !== undefined ? width - 20 : width}
height={height / 3}
showTypes={showTypes}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div>
))}
</div>
</VariableBox>
);
case "arraystring":
return (
<VariableBox heading="Array String" showTypes={showTypes}>
{expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox>
);
case "date":
return (
<VariableBox heading="Date" showTypes={showTypes}>
{expression.value.toDateString()}
</VariableBox>
);
case "timeDuration": {
return (
<VariableBox heading="Time Duration" showTypes={showTypes}>
<NumberShower precision={3} number={expression.value} />
</VariableBox>
);
}
case "lambda":
return (
<VariableBox heading="Function" showTypes={showTypes}>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
","
)})`}</div>
<FunctionChart
fn={expression.value}
chartSettings={chartSettings}
distributionPlotSettings={distributionPlotSettings}
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)}
distributionPlotSettings={distributionPlotSettings}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
}
case "module": {
return (
<VariableBox heading="Module" showTypes={showTypes}>
<div className="space-y-3">
{Object.entries(expression.value)
.filter(([key, r]) => key !== "Math")
.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}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div>
))}
</div>
</VariableBox>
);
}
default: {
return (
<div>
<span>No display for type: </span>{" "}
<span className="font-semibold text-slate-600">{expression.tag}</span>
</div>
);
}
}
};

View File

@ -1,11 +1,19 @@
import React, { FC, useState, useEffect, useMemo } from "react"; import React, {
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; FC,
useState,
useEffect,
useMemo,
useRef,
useCallback,
} from "react";
import { useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { useMaybeControlledValue } from "../lib/hooks"; import { useMaybeControlledValue, useRunnerState } from "../lib/hooks";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { import {
ChartSquareBarIcon, ChartSquareBarIcon,
CheckCircleIcon, CheckCircleIcon,
ClipboardCopyIcon,
CodeIcon, CodeIcon,
CogIcon, CogIcon,
CurrencyDollarIcon, CurrencyDollarIcon,
@ -24,105 +32,53 @@ import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert"; import { ErrorAlert, SuccessAlert } from "./Alert";
import { SquiggleContainer } from "./SquiggleContainer"; import { SquiggleContainer } from "./SquiggleContainer";
import { Toggle } from "./ui/Toggle"; import { Toggle } from "./ui/Toggle";
import { Checkbox } from "./ui/Checkbox";
import { StyledTab } from "./ui/StyledTab"; import { StyledTab } from "./ui/StyledTab";
import { InputItem } from "./ui/InputItem";
import { Text } from "./ui/Text";
import { ViewSettings, viewSettingsSchema } from "./ViewSettings";
import { HeadedSection } from "./ui/HeadedSection";
import {
defaultColor,
defaultTickFormat,
} from "../lib/distributionSpecBuilder";
import { Button } from "./ui/Button";
type PlaygroundProps = SquiggleChartProps & { type PlaygroundProps = SquiggleChartProps & {
/** The initial squiggle string to put in the playground */ /** The initial squiggle string to put in the playground */
defaultCode?: string; defaultCode?: string;
/** How many pixels high is the playground */
onCodeChange?(expr: string): void; onCodeChange?(expr: string): void;
/* When settings change */ /* When settings change */
onSettingsChange?(settings: any): void; onSettingsChange?(settings: any): void;
/** Should we show the editor? */ /** Should we show the editor? */
showEditor?: boolean; showEditor?: boolean;
/** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */
showShareButton?: boolean;
}; };
const schema = yup.object({}).shape({ const schema = yup
sampleCount: yup .object({})
.number() .shape({
.required() sampleCount: yup
.positive() .number()
.integer() .required()
.default(1000) .positive()
.min(10) .integer()
.max(1000000), .default(1000)
xyPointLength: yup .min(10)
.number() .max(1000000),
.required() xyPointLength: yup
.positive() .number()
.integer() .required()
.default(1000) .positive()
.min(10) .integer()
.max(10000), .default(1000)
chartHeight: yup.number().required().positive().integer().default(350), .min(10)
leftSizePercent: yup .max(10000),
.number() })
.required() .concat(viewSettingsSchema);
.positive()
.integer()
.min(10)
.max(100)
.default(50),
showTypes: yup.boolean().required(),
showControls: yup.boolean().required(),
showSummary: yup.boolean().required(),
showEditor: yup.boolean().required(),
logX: yup.boolean().required(),
expY: yup.boolean().required(),
tickFormat: yup.string().default(".9~s"),
title: yup.string(),
color: yup.string().default("#739ECC").required(),
minX: yup.number(),
maxX: yup.number(),
distributionChartActions: 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),
});
type FormFields = yup.InferType<typeof schema>; type FormFields = yup.InferType<typeof schema>;
const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({
title,
children,
}) => (
<div>
<header className="text-lg leading-6 font-medium text-gray-900">
{title}
</header>
<div className="mt-4">{children}</div>
</div>
);
const Text: FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);
function InputItem<T>({
name,
label,
type,
register,
}: {
name: Path<T>;
label: string;
type: "number" | "text" | "color";
register: UseFormRegister<T>;
}) {
return (
<label className="block">
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input
type={type}
{...register(name, { valueAsNumber: type === "number" })}
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</label>
);
}
const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register, register,
}) => ( }) => (
@ -158,128 +114,6 @@ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
</div> </div>
); );
const ViewSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register,
}) => (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
<Checkbox
name="showTypes"
register={register}
label="Show information about displayed types"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="distributionChartActions"
label="Show vega chart controls"
/>
<Checkbox
register={register}
name="showControls"
label="Show toggles to adjust scale of x and y axes"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
<InputItem
name="minX"
type="number"
register={register}
label="Min X Value"
/>
<InputItem
name="maxX"
type="number"
register={register}
label="Max X Value"
/>
<InputItem
name="title"
type="text"
register={register}
label="Title"
/>
<InputItem
name="tickFormat"
type="text"
register={register}
label="Tick Format"
/>
<InputItem
name="color"
type="color"
register={register}
label="Color"
/>
</div>
</HeadedSection>
</div>
<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 InputVariablesSettings: React.FC<{ const InputVariablesSettings: React.FC<{
initialImports: any; // TODO - any json type initialImports: any; // TODO - any json type
setImports: (imports: any) => void; setImports: (imports: any) => void;
@ -361,69 +195,59 @@ const RunControls: React.FC<{
icons={[CheckCircleIcon, PauseIcon]} icons={[CheckCircleIcon, PauseIcon]}
status={autorunMode} status={autorunMode}
onChange={onAutorunModeChange} onChange={onAutorunModeChange}
spinIcon={autorunMode && isRunning}
/> />
</div> </div>
); );
}; };
const useRunnerState = (code: string) => { const ShareButton: React.FC = () => {
const [autorunMode, setAutorunMode] = useState(true); const [isCopied, setIsCopied] = useState(false);
const [renderedCode, setRenderedCode] = useState(code); // used in manual run mode only const copy = () => {
const [isRunning, setIsRunning] = useState(false); // used in manual run mode only navigator.clipboard.writeText((window.top || window).location.href);
setIsCopied(true);
// This part is tricky and fragile; we need to re-render first to make sure that the icon is spinning, setTimeout(() => setIsCopied(false), 1000);
// and only then evaluate the squiggle code (which freezes the UI).
// Also note that `useEffect` execution order matters here.
// Hopefully it'll all go away after we make squiggle code evaluation async.
useEffect(() => {
if (renderedCode === code && isRunning) {
// It's not possible to put this after `setRenderedCode(code)` below because React would apply
// `setIsRunning` and `setRenderedCode` together and spinning icon will disappear immediately.
setIsRunning(false);
}
}, [renderedCode, code, isRunning]);
useEffect(() => {
if (!autorunMode && isRunning) {
setRenderedCode(code); // TODO - force run even if code hasn't changed
}
}, [autorunMode, code, isRunning]);
const run = () => {
// The rest will be handled by useEffects above, but we need to update the spinner first.
setIsRunning(true);
};
return {
run,
renderedCode: autorunMode ? code : renderedCode,
isRunning,
autorunMode,
setAutorunMode: (newValue: boolean) => {
if (!newValue) setRenderedCode(code);
setAutorunMode(newValue);
},
}; };
return (
<div className="w-36">
<Button onClick={copy} wide>
{isCopied ? (
"Copied to clipboard!"
) : (
<div className="flex items-center space-x-1">
<ClipboardCopyIcon className="w-4 h-4" />
<span>Copy share link</span>
</div>
)}
</Button>
</div>
);
}; };
type PlaygroundContextShape = {
getLeftPanelElement: () => HTMLDivElement | undefined;
};
export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
getLeftPanelElement: () => undefined,
});
export const SquigglePlayground: FC<PlaygroundProps> = ({ export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "", defaultCode = "",
height = 500, height = 500,
showTypes = false,
showControls = false,
showSummary = false, showSummary = false,
logX = false, logX = false,
expY = false, expY = false,
title, title,
minX, minX,
maxX, maxX,
color = "#739ECC", color = defaultColor,
tickFormat = ".9~s", tickFormat = defaultTickFormat,
distributionChartActions, distributionChartActions,
code: controlledCode, code: controlledCode,
onCodeChange, onCodeChange,
onSettingsChange, onSettingsChange,
showEditor = true, showEditor = true,
showShareButton = false,
}) => { }) => {
const [code, setCode] = useMaybeControlledValue({ const [code, setCode] = useMaybeControlledValue({
value: controlledCode, value: controlledCode,
@ -439,8 +263,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
sampleCount: 1000, sampleCount: 1000,
xyPointLength: 1000, xyPointLength: 1000,
chartHeight: 150, chartHeight: 150,
showTypes,
showControls,
logX, logX,
expY, expY,
title, title,
@ -451,8 +273,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
distributionChartActions, distributionChartActions,
showSummary, showSummary,
showEditor, showEditor,
leftSizePercent: 50,
showSettingsPage: false,
diagramStart: 0, diagramStart: 0,
diagramStop: 10, diagramStop: 10,
diagramCount: 20, diagramCount: 20,
@ -474,18 +294,32 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
[vars.sampleCount, vars.xyPointLength] [vars.sampleCount, vars.xyPointLength]
); );
const { run, autorunMode, setAutorunMode, isRunning, renderedCode } = const {
useRunnerState(code); run,
autorunMode,
setAutorunMode,
isRunning,
renderedCode,
executionId,
} = useRunnerState(code);
const squiggleChart = ( const squiggleChart =
<SquiggleChart renderedCode === "" ? null : (
code={renderedCode} <div className="relative">
environment={env} {isRunning ? (
{...vars} <div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
bindings={defaultBindings} ) : null}
jsImports={imports} <SquiggleChart
/> code={renderedCode}
); executionId={executionId}
environment={env}
{...vars}
bindings={defaultBindings}
jsImports={imports}
enableLocalSettings={true}
/>
</div>
);
const firstTab = vars.showEditor ? ( const firstTab = vars.showEditor ? (
<div className="border border-slate-200"> <div className="border border-slate-200">
@ -509,7 +343,15 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<SamplingSettings register={register} /> <SamplingSettings register={register} />
</StyledTab.Panel> </StyledTab.Panel>
<StyledTab.Panel> <StyledTab.Panel>
<ViewSettings register={register} /> <ViewSettings
register={
// This is dangerous, but doesn't cause any problems.
// I tried to make `ViewSettings` generic (to allow it to accept any extension of a settings schema), but it didn't work.
register as unknown as UseFormRegister<
yup.InferType<typeof viewSettingsSchema>
>
}
/>
</StyledTab.Panel> </StyledTab.Panel>
<StyledTab.Panel> <StyledTab.Panel>
<InputVariablesSettings <InputVariablesSettings
@ -520,41 +362,57 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
</StyledTab.Panels> </StyledTab.Panels>
); );
const leftPanelRef = useRef<HTMLDivElement | null>(null);
const withEditor = ( const withEditor = (
<div className="flex mt-2"> <div className="flex mt-2">
<div className="w-1/2">{tabs}</div> <div
className="w-1/2 relative"
style={{ minHeight: height }}
ref={leftPanelRef}
>
{tabs}
</div>
<div className="w-1/2 p-2 pl-4">{squiggleChart}</div> <div className="w-1/2 p-2 pl-4">{squiggleChart}</div>
</div> </div>
); );
const withoutEditor = <div className="mt-3">{tabs}</div>; const withoutEditor = <div className="mt-3">{tabs}</div>;
console.log(vars); const getLeftPanelElement = useCallback(() => {
return leftPanelRef.current ?? undefined;
}, []);
return ( return (
<SquiggleContainer> <SquiggleContainer>
<StyledTab.Group> <PlaygroundContext.Provider value={{ getLeftPanelElement }}>
<div className="pb-4"> <StyledTab.Group>
<div className="flex justify-between items-center mt-2"> <div className="pb-4">
<StyledTab.List> <div className="flex justify-between items-center">
<StyledTab <StyledTab.List>
name={vars.showEditor ? "Code" : "Display"} <StyledTab
icon={vars.showEditor ? CodeIcon : EyeIcon} name={vars.showEditor ? "Code" : "Display"}
/> icon={vars.showEditor ? CodeIcon : EyeIcon}
<StyledTab name="Sampling Settings" icon={CogIcon} /> />
<StyledTab name="View Settings" icon={ChartSquareBarIcon} /> <StyledTab name="Sampling Settings" icon={CogIcon} />
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} /> <StyledTab name="View Settings" icon={ChartSquareBarIcon} />
</StyledTab.List> <StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
<RunControls </StyledTab.List>
autorunMode={autorunMode} <div className="flex space-x-2 items-center">
isStale={renderedCode !== code} <RunControls
run={run} autorunMode={autorunMode}
isRunning={isRunning} isStale={renderedCode !== code}
onAutorunModeChange={setAutorunMode} run={run}
/> isRunning={isRunning}
onAutorunModeChange={setAutorunMode}
/>
{showShareButton && <ShareButton />}
</div>
</div>
{vars.showEditor ? withEditor : withoutEditor}
</div> </div>
{vars.showEditor ? withEditor : withoutEditor} </StyledTab.Group>
</div> </PlaygroundContext.Provider>
</StyledTab.Group>
</SquiggleContainer> </SquiggleContainer>
); );
}; };

View File

@ -0,0 +1,304 @@
import React from "react";
import { squiggleExpression, declaration } from "@quri/squiggle-lang";
import { NumberShower } from "../NumberShower";
import { DistributionChart } from "../DistributionChart";
import { FunctionChart, FunctionChartSettings } from "../FunctionChart";
import clsx from "clsx";
import { VariableBox } from "./VariableBox";
import { ItemSettingsMenu } from "./ItemSettingsMenu";
import { hasMassBelowZero } from "../../lib/distributionUtils";
import { MergedItemSettings } from "./utils";
function getRange<a>(x: declaration<a>) {
const first = x.args[0];
switch (first.tag) {
case "Float": {
return { floats: { min: first.value.min, max: first.value.max } };
}
case "Date": {
return { time: { min: first.value.min, max: first.value.max } };
}
}
}
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
const range = getRange(x);
const min = range.floats ? range.floats.min : 0;
const max = range.floats ? range.floats.max : 10;
return {
start: min,
stop: max,
count: 20,
};
}
const VariableList: React.FC<{
path: string[];
heading: string;
children: (settings: MergedItemSettings) => React.ReactNode;
}> = ({ path, heading, children }) => (
<VariableBox path={path} heading={heading}>
{(settings) => (
<div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}>
{children(settings)}
</div>
)}
</VariableBox>
);
export interface Props {
/** The output of squiggle's run */
expression: squiggleExpression;
/** Path to the current item, e.g. `['foo', 'bar', '3']` for `foo.bar[3]`; can be empty on the top-level item. */
path: string[];
width?: number;
}
export const ExpressionViewer: React.FC<Props> = ({
path,
expression,
width,
}) => {
if (typeof expression !== "object") {
return (
<VariableList path={path} heading="Error">
{() => `Unknown expression: ${expression}`}
</VariableList>
);
}
switch (expression.tag) {
case "number":
return (
<VariableBox path={path} heading="Number">
{() => (
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={expression.value} />
</div>
)}
</VariableBox>
);
case "distribution": {
const distType = expression.value.type();
return (
<VariableBox
path={path}
heading={`Distribution (${distType})\n${
distType === "Symbolic" ? expression.value.toString() : ""
}`}
renderSettingsMenu={({ onChange }) => {
const shape = expression.value.pointSet();
return (
<ItemSettingsMenu
path={path}
onChange={onChange}
disableLogX={
shape.tag === "Ok" && hasMassBelowZero(shape.value)
}
withFunctionSettings={false}
/>
);
}}
>
{(settings) => {
return (
<DistributionChart
distribution={expression.value}
{...settings.distributionPlotSettings}
height={settings.height}
width={width}
/>
);
}}
</VariableBox>
);
}
case "string":
return (
<VariableBox path={path} heading="String">
{() => (
<>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold font-mono">
{expression.value}
</span>
<span className="text-slate-400">"</span>
</>
)}
</VariableBox>
);
case "boolean":
return (
<VariableBox path={path} heading="Boolean">
{() => expression.value.toString()}
</VariableBox>
);
case "symbol":
return (
<VariableBox path={path} heading="Symbol">
{() => (
<>
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span>
</>
)}
</VariableBox>
);
case "call":
return (
<VariableBox path={path} heading="Call">
{() => expression.value}
</VariableBox>
);
case "arraystring":
return (
<VariableBox path={path} heading="Array String">
{() => expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox>
);
case "date":
return (
<VariableBox path={path} heading="Date">
{() => expression.value.toDateString()}
</VariableBox>
);
case "void":
return (
<VariableBox path={path} heading="Void">
{() => "Void"}
</VariableBox>
);
case "timeDuration": {
return (
<VariableBox path={path} heading="Time Duration">
{() => <NumberShower precision={3} number={expression.value} />}
</VariableBox>
);
}
case "lambda":
return (
<VariableBox
path={path}
heading="Function"
renderSettingsMenu={({ onChange }) => {
return (
<ItemSettingsMenu
path={path}
onChange={onChange}
withFunctionSettings={true}
/>
);
}}
>
{(settings) => (
<>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
","
)})`}</div>
<FunctionChart
fn={expression.value}
chartSettings={settings.chartSettings}
distributionPlotSettings={settings.distributionPlotSettings}
height={settings.height}
environment={{
sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10,
}}
/>
</>
)}
</VariableBox>
);
case "lambdaDeclaration": {
return (
<VariableBox
path={path}
heading="Function Declaration"
renderSettingsMenu={({ onChange }) => {
return (
<ItemSettingsMenu
onChange={onChange}
path={path}
withFunctionSettings={true}
/>
);
}}
>
{(settings) => (
<FunctionChart
fn={expression.value.fn}
chartSettings={getChartSettings(expression.value)}
distributionPlotSettings={settings.distributionPlotSettings}
height={settings.height}
environment={{
sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10,
}}
/>
)}
</VariableBox>
);
}
case "module": {
return (
<VariableList path={path} heading="Module">
{(settings) =>
Object.entries(expression.value)
.filter(([key, r]) => !key.match(/^(Math|System)\./))
.map(([key, r]) => (
<ExpressionViewer
key={key}
path={[...path, key]}
expression={r}
width={width !== undefined ? width - 20 : width}
/>
))
}
</VariableList>
);
}
case "record":
return (
<VariableList path={path} heading="Record">
{(settings) =>
Object.entries(expression.value).map(([key, r]) => (
<ExpressionViewer
key={key}
path={[...path, key]}
expression={r}
width={width !== undefined ? width - 20 : width}
/>
))
}
</VariableList>
);
case "array":
return (
<VariableList path={path} heading="Array">
{(settings) =>
expression.value.map((r, i) => (
<ExpressionViewer
key={i}
path={[...path, String(i)]}
expression={r}
width={width !== undefined ? width - 20 : width}
/>
))
}
</VariableList>
);
default: {
return (
<VariableList path={path} heading="Error">
{() => (
<div>
<span>No display for type: </span>{" "}
<span className="font-semibold text-slate-600">
{expression.tag}
</span>
</div>
)}
</VariableList>
);
}
}
};

View File

@ -0,0 +1,168 @@
import { CogIcon } from "@heroicons/react/solid";
import React, { useContext, useRef, useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Modal } from "../ui/Modal";
import { ViewSettings, viewSettingsSchema } from "../ViewSettings";
import { Path, pathAsString } from "./utils";
import { ViewerContext } from "./ViewerContext";
import {
defaultColor,
defaultTickFormat,
} from "../../lib/distributionSpecBuilder";
import { PlaygroundContext } from "../SquigglePlayground";
type Props = {
path: Path;
onChange: () => void;
disableLogX?: boolean;
withFunctionSettings: boolean;
};
const ItemSettingsModal: React.FC<
Props & { close: () => void; resetScroll: () => void }
> = ({
path,
onChange,
disableLogX,
withFunctionSettings,
close,
resetScroll,
}) => {
const { setSettings, getSettings, getMergedSettings } =
useContext(ViewerContext);
const mergedSettings = getMergedSettings(path);
const { register, watch } = useForm({
resolver: yupResolver(viewSettingsSchema),
defaultValues: {
// this is a mess and should be fixed
showEditor: true, // doesn't matter
chartHeight: mergedSettings.height,
showSummary: mergedSettings.distributionPlotSettings.showSummary,
logX: mergedSettings.distributionPlotSettings.logX,
expY: mergedSettings.distributionPlotSettings.expY,
tickFormat:
mergedSettings.distributionPlotSettings.format || defaultTickFormat,
title: mergedSettings.distributionPlotSettings.title,
color: mergedSettings.distributionPlotSettings.color || defaultColor,
minX: mergedSettings.distributionPlotSettings.minX,
maxX: mergedSettings.distributionPlotSettings.maxX,
distributionChartActions: mergedSettings.distributionPlotSettings.actions,
diagramStart: mergedSettings.chartSettings.start,
diagramStop: mergedSettings.chartSettings.stop,
diagramCount: mergedSettings.chartSettings.count,
},
});
useEffect(() => {
const subscription = watch((vars) => {
const settings = getSettings(path); // get the latest version
setSettings(path, {
...settings,
distributionPlotSettings: {
showSummary: vars.showSummary,
logX: vars.logX,
expY: vars.expY,
format: vars.tickFormat,
title: vars.title,
color: vars.color,
minX: vars.minX,
maxX: vars.maxX,
actions: vars.distributionChartActions,
},
chartSettings: {
start: vars.diagramStart,
stop: vars.diagramStop,
count: vars.diagramCount,
},
});
onChange();
});
return () => subscription.unsubscribe();
}, [getSettings, setSettings, onChange, path, watch]);
const { getLeftPanelElement } = useContext(PlaygroundContext);
return (
<Modal container={getLeftPanelElement()} close={close}>
<Modal.Header>
Chart settings
{path.length ? (
<>
{" for "}
<span
title="Scroll to item"
className="cursor-pointer"
onClick={resetScroll}
>
{pathAsString(path)}
</span>{" "}
</>
) : (
""
)}
</Modal.Header>
<Modal.Body>
<ViewSettings
register={register}
withShowEditorSetting={false}
withFunctionSettings={withFunctionSettings}
disableLogXSetting={disableLogX}
/>
</Modal.Body>
</Modal>
);
};
export const ItemSettingsMenu: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
const { enableLocalSettings, setSettings, getSettings } =
useContext(ViewerContext);
const ref = useRef<HTMLDivElement | null>(null);
if (!enableLocalSettings) {
return null;
}
const settings = getSettings(props.path);
const resetScroll = () => {
if (!ref.current) return;
window.scroll({
top: ref.current.getBoundingClientRect().y + window.scrollY,
behavior: "smooth",
});
};
return (
<div className="flex gap-2" ref={ref}>
<CogIcon
className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => setIsOpen(!isOpen)}
/>
{settings.distributionPlotSettings || settings.chartSettings ? (
<button
onClick={() => {
setSettings(props.path, {
...settings,
distributionPlotSettings: undefined,
chartSettings: undefined,
});
props.onChange();
}}
className="text-xs px-1 py-0.5 rounded bg-slate-300"
>
Reset settings
</button>
) : null}
{isOpen ? (
<ItemSettingsModal
{...props}
close={() => setIsOpen(false)}
resetScroll={resetScroll}
/>
) : null}
</div>
);
};

View File

@ -0,0 +1,79 @@
import React, { useContext, useReducer } from "react";
import { Tooltip } from "../ui/Tooltip";
import { LocalItemSettings, MergedItemSettings } from "./utils";
import { ViewerContext } from "./ViewerContext";
type SettingsMenuParams = {
onChange: () => void; // used to notify VariableBox that settings have changed, so that VariableBox could re-render itself
};
type VariableBoxProps = {
path: string[];
heading: string;
renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode;
children: (settings: MergedItemSettings) => React.ReactNode;
};
export const VariableBox: React.FC<VariableBoxProps> = ({
path,
heading = "Error",
renderSettingsMenu,
children,
}) => {
const { setSettings, getSettings, getMergedSettings } =
useContext(ViewerContext);
// Since ViewerContext doesn't keep the actual settings, VariableBox won't rerender when setSettings is called.
// So we use `forceUpdate` to force rerendering.
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
const settings = getSettings(path);
const setSettingsAndUpdate = (newSettings: LocalItemSettings) => {
setSettings(path, newSettings);
forceUpdate();
};
const toggleCollapsed = () => {
setSettingsAndUpdate({ ...settings, collapsed: !settings.collapsed });
};
const isTopLevel = path.length === 0;
const name = isTopLevel ? "Result" : path[path.length - 1];
return (
<div>
<header className="inline-flex space-x-1">
<Tooltip text={heading}>
<span
className="text-slate-500 font-mono text-sm cursor-pointer"
onClick={toggleCollapsed}
>
{name}:
</span>
</Tooltip>
{settings.collapsed ? (
<span
className="rounded p-0.5 bg-slate-200 text-slate-500 font-mono text-xs cursor-pointer"
onClick={toggleCollapsed}
>
...
</span>
) : renderSettingsMenu ? (
renderSettingsMenu({ onChange: forceUpdate })
) : null}
</header>
{settings.collapsed ? null : (
<div className="flex w-full">
{path.length ? (
<div
className="border-l-2 border-slate-200 hover:border-indigo-600 w-4 cursor-pointer"
onClick={toggleCollapsed}
></div>
) : null}
<div className="grow">{children(getMergedSettings(path))}</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,35 @@
import { defaultEnvironment } from "@quri/squiggle-lang";
import React from "react";
import { LocalItemSettings, MergedItemSettings, Path } from "./utils";
type ViewerContextShape = {
// Note that we don't store settings themselves in the context (that would cause rerenders of the entire tree on each settings update).
// Instead, we keep settings in local state and notify the global context via setSettings to pass them down the component tree again if it got rebuilt from scratch.
// See ./SquiggleViewer.tsx and ./VariableBox.tsx for other implementation details on this.
getSettings(path: Path): LocalItemSettings;
getMergedSettings(path: Path): MergedItemSettings;
setSettings(path: Path, value: LocalItemSettings): void;
enableLocalSettings: boolean; // show local settings icon in the UI
};
export const ViewerContext = React.createContext<ViewerContextShape>({
getSettings: () => ({ collapsed: false }),
getMergedSettings: () => ({
collapsed: false,
// copy-pasted from SquiggleChart
chartSettings: {
start: 0,
stop: 10,
count: 100,
},
distributionPlotSettings: {
showSummary: false,
logX: false,
expY: false,
},
environment: defaultEnvironment,
height: 150,
}),
setSettings() {},
enableLocalSettings: false,
});

View File

@ -0,0 +1,100 @@
import React, { useCallback, useRef } from "react";
import { environment } from "@quri/squiggle-lang";
import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart";
import { ExpressionViewer } from "./ExpressionViewer";
import { ViewerContext } from "./ViewerContext";
import {
LocalItemSettings,
MergedItemSettings,
Path,
pathAsString,
} from "./utils";
import { useSquiggle } from "../../lib/hooks";
import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
type Props = {
/** The output of squiggle's run */
result: ReturnType<typeof useSquiggle>;
width?: number;
height: number;
distributionPlotSettings: DistributionPlottingSettings;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
enableLocalSettings?: boolean;
};
type Settings = {
[k: string]: LocalItemSettings;
};
const defaultSettings: LocalItemSettings = { collapsed: false };
export const SquiggleViewer: React.FC<Props> = ({
result,
width,
height,
distributionPlotSettings,
chartSettings,
environment,
enableLocalSettings = false,
}) => {
// can't store settings in the state because we don't want to rerender the entire tree on every change
const settingsRef = useRef<Settings>({});
const getSettings = useCallback(
(path: Path) => {
return settingsRef.current[pathAsString(path)] || defaultSettings;
},
[settingsRef]
);
const setSettings = useCallback(
(path: Path, value: LocalItemSettings) => {
settingsRef.current[pathAsString(path)] = value;
},
[settingsRef]
);
const getMergedSettings = useCallback(
(path: Path) => {
const localSettings = getSettings(path);
const result: MergedItemSettings = {
distributionPlotSettings: {
...distributionPlotSettings,
...(localSettings.distributionPlotSettings || {}),
},
chartSettings: {
...chartSettings,
...(localSettings.chartSettings || {}),
},
environment: {
...environment,
...(localSettings.environment || {}),
},
height: localSettings.height || height,
};
return result;
},
[distributionPlotSettings, chartSettings, environment, height, getSettings]
);
return (
<ViewerContext.Provider
value={{
getSettings,
setSettings,
getMergedSettings,
enableLocalSettings,
}}
>
{result.tag === "Ok" ? (
<ExpressionViewer path={[]} expression={result.value} width={width} />
) : (
<SquiggleErrorAlert error={result.value} />
)}
</ViewerContext.Provider>
);
};

View File

@ -0,0 +1,22 @@
import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart";
import { environment } from "@quri/squiggle-lang";
export type LocalItemSettings = {
collapsed: boolean;
distributionPlotSettings?: Partial<DistributionPlottingSettings>;
chartSettings?: Partial<FunctionChartSettings>;
height?: number;
environment?: Partial<environment>;
};
export type MergedItemSettings = {
distributionPlotSettings: DistributionPlottingSettings;
chartSettings: FunctionChartSettings;
height: number;
environment: environment;
};
export type Path = string[];
export const pathAsString = (path: Path) => path.join(".");

View File

@ -0,0 +1,163 @@
import React from "react";
import * as yup from "yup";
import { UseFormRegister } from "react-hook-form";
import { InputItem } from "./ui/InputItem";
import { Checkbox } from "./ui/Checkbox";
import { HeadedSection } from "./ui/HeadedSection";
import { Text } from "./ui/Text";
import {
defaultColor,
defaultTickFormat,
} from "../lib/distributionSpecBuilder";
export const viewSettingsSchema = yup.object({}).shape({
chartHeight: yup.number().required().positive().integer().default(350),
showSummary: yup.boolean().required(),
showEditor: yup.boolean().required(),
logX: yup.boolean().required(),
expY: yup.boolean().required(),
tickFormat: yup.string().default(defaultTickFormat),
title: yup.string(),
color: yup.string().default(defaultColor).required(),
minX: yup.number(),
maxX: yup.number(),
distributionChartActions: yup.boolean(),
diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0),
diagramCount: yup.number().required().positive().integer().default(20).min(2),
});
type FormFields = yup.InferType<typeof viewSettingsSchema>;
// This component is used in two places: for global settings in SquigglePlayground, and for item-specific settings in modal dialogs.
export const ViewSettings: React.FC<{
withShowEditorSetting?: boolean;
withFunctionSettings?: boolean;
disableLogXSetting?: boolean;
register: UseFormRegister<FormFields>;
}> = ({
withShowEditorSetting = true,
withFunctionSettings = true,
disableLogXSetting,
register,
}) => {
return (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
{withShowEditorSetting ? (
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
) : null}
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
disabled={disableLogXSetting}
tooltip={
disableLogXSetting
? "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
: undefined
}
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="distributionChartActions"
label="Show vega chart controls"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
<InputItem
name="minX"
type="number"
register={register}
label="Min X Value"
/>
<InputItem
name="maxX"
type="number"
register={register}
label="Max X Value"
/>
<InputItem
name="title"
type="text"
register={register}
label="Title"
/>
<InputItem
name="tickFormat"
type="text"
register={register}
label="Tick Format"
/>
<InputItem
name="color"
type="color"
register={register}
label="Color"
/>
</div>
</HeadedSection>
</div>
{withFunctionSettings ? (
<div className="pt-8">
<HeadedSection title="Function Display Settings">
<div className="space-y-6">
<Text>
When displaying functions of single variables that return
numbers or distributions, we need to use defaults for the
x-axis. We need to select a minimum and maximum value of x to
sample, and a number n of the number of points to sample.
</Text>
<div className="space-y-4">
<InputItem
type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div>
</HeadedSection>
</div>
) : null}
</div>
);
};

View File

@ -0,0 +1,22 @@
import clsx from "clsx";
import React from "react";
type Props = {
onClick: () => void;
children: React.ReactNode;
wide?: boolean; // stretch the button horizontally
};
export const Button: React.FC<Props> = ({ onClick, wide, children }) => {
return (
<button
className={clsx(
"rounded-md py-1.5 px-2 bg-slate-500 text-white text-xs font-semibold flex items-center justify-center space-x-1",
wide && "w-full"
)}
onClick={onClick}
>
{children}
</button>
);
};

View File

@ -1,3 +1,4 @@
import clsx from "clsx";
import React from "react"; import React from "react";
import { Path, UseFormRegister } from "react-hook-form"; import { Path, UseFormRegister } from "react-hook-form";
@ -5,20 +6,32 @@ export function Checkbox<T>({
name, name,
label, label,
register, register,
disabled,
tooltip,
}: { }: {
name: Path<T>; name: Path<T>;
label: string; label: string;
register: UseFormRegister<T>; register: UseFormRegister<T>;
disabled?: boolean;
tooltip?: string;
}) { }) {
return ( return (
<label className="flex items-center"> <label className="flex items-center" title={tooltip}>
<input <input
type="checkbox" type="checkbox"
disabled={disabled}
{...register(name)} {...register(name)}
className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/> />
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */} {/* 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> <div
className={clsx(
"ml-3 text-sm font-medium",
disabled ? "text-gray-400" : "text-gray-700"
)}
>
{label}
</div>
</label> </label>
); );
} }

View File

@ -0,0 +1,13 @@
import React from "react";
export const HeadedSection: React.FC<{
title: string;
children: React.ReactNode;
}> = ({ title, children }) => (
<div>
<header className="text-lg leading-6 font-medium text-gray-900">
{title}
</header>
<div className="mt-4">{children}</div>
</div>
);

View File

@ -0,0 +1,25 @@
import React from "react";
import { Path, UseFormRegister } from "react-hook-form";
export function InputItem<T>({
name,
label,
type,
register,
}: {
name: Path<T>;
label: string;
type: "number" | "text" | "color";
register: UseFormRegister<T>;
}) {
return (
<label className="block">
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input
type={type}
{...register(name, { valueAsNumber: type === "number" })}
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</label>
);
}

View File

@ -0,0 +1,184 @@
import { motion } from "framer-motion";
import React, { useContext } from "react";
import * as ReactDOM from "react-dom";
import { XIcon } from "@heroicons/react/solid";
import clsx from "clsx";
import { useWindowScroll, useWindowSize } from "react-use";
type ModalContextShape = {
close: () => void;
};
const ModalContext = React.createContext<ModalContextShape>({
close: () => undefined,
});
const Overlay: React.FC = () => {
const { close } = useContext(ModalContext);
return (
<motion.div
className="absolute inset-0 -z-10 bg-black"
initial={{ opacity: 0 }}
animate={{ opacity: 0.1 }}
onClick={close}
/>
);
};
const ModalHeader: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
const { close } = useContext(ModalContext);
return (
<header className="px-5 py-3 border-b border-gray-200 font-bold flex items-center justify-between">
<div>{children}</div>
<button
className="px-1 bg-transparent cursor-pointer text-gray-700 hover:text-accent-500"
type="button"
onClick={close}
>
<XIcon className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500" />
</button>
</header>
);
};
// TODO - get rid of forwardRef, support `focus` and `{...hotkeys}` via smart props
const ModalBody = React.forwardRef<
HTMLDivElement,
JSX.IntrinsicElements["div"]
>(function ModalBody(props, ref) {
return <div ref={ref} className="px-5 py-3 overflow-auto" {...props} />;
});
const ModalFooter: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="px-5 py-3 border-t border-gray-200">{children}</div>
);
const ModalWindow: React.FC<{
children: React.ReactNode;
container?: HTMLElement;
}> = ({ children, container }) => {
// This component works in two possible modes:
// 1. container mode - the modal is rendered inside a container element
// 2. centered mode - the modal is rendered in the middle of the screen
// The mode is determined by the presence of the `container` prop and by whether the available space is large enough to fit the modal.
// Necessary for container mode - need to reposition the modal on scroll and resize events.
useWindowSize();
useWindowScroll();
let position:
| {
left: number;
top: number;
maxWidth: number;
maxHeight: number;
transform: string;
}
| undefined;
// If available space in `visibleRect` is smaller than these, fallback to positioning in the middle of the screen.
const minWidth = 384;
const minHeight = 300;
const offset = 8;
const naturalWidth = 576; // maximum possible width; modal tries to take this much space, but can be smaller
if (container) {
const { clientWidth: screenWidth, clientHeight: screenHeight } =
document.documentElement;
const rect = container?.getBoundingClientRect();
const visibleRect = {
left: Math.max(rect.left, 0),
right: Math.min(rect.right, screenWidth),
top: Math.max(rect.top, 0),
bottom: Math.min(rect.bottom, screenHeight),
};
const maxWidth = visibleRect.right - visibleRect.left - 2 * offset;
const maxHeight = visibleRect.bottom - visibleRect.top - 2 * offset;
const center = {
left: visibleRect.left + (visibleRect.right - visibleRect.left) / 2,
top: visibleRect.top + (visibleRect.bottom - visibleRect.top) / 2,
};
position = {
left: center.left,
top: center.top,
transform: "translate(-50%, -50%)",
maxWidth,
maxHeight,
};
if (maxWidth < minWidth || maxHeight < minHeight) {
position = undefined; // modal is hard to fit in the container, fallback to positioning it in the middle of the screen
}
}
return (
<div
className={clsx(
"bg-white rounded-md shadow-toast flex flex-col overflow-auto border",
position ? "fixed" : null
)}
style={{
width: naturalWidth,
...(position ?? {
maxHeight: "calc(100% - 20px)",
maxWidth: "calc(100% - 20px)",
width: naturalWidth,
}),
}}
>
{children}
</div>
);
};
type ModalType = React.FC<{
children: React.ReactNode;
container?: HTMLElement; // if specified, modal will be positioned over the visible part of the container, if it's not too small
close: () => void;
}> & {
Body: typeof ModalBody;
Footer: typeof ModalFooter;
Header: typeof ModalHeader;
};
export const Modal: ModalType = ({ children, container, close }) => {
const [el] = React.useState(() => document.createElement("div"));
React.useEffect(() => {
document.body.appendChild(el);
return () => {
document.body.removeChild(el);
};
}, [el]);
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
close();
}
};
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, [close]);
const modal = (
<ModalContext.Provider value={{ close }}>
<div className="squiggle">
<div className="fixed inset-0 z-40 flex justify-center items-center">
<Overlay />
<ModalWindow container={container}>{children}</ModalWindow>
</div>
</div>
</ModalContext.Provider>
);
return ReactDOM.createPortal(modal, container || el);
};
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
Modal.Header = ModalHeader;

View File

@ -0,0 +1,5 @@
import React from "react";
export const Text: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);

View File

@ -1,5 +1,5 @@
import { RefreshIcon } from "@heroicons/react/solid";
import clsx from "clsx"; import clsx from "clsx";
import { motion } from "framer-motion";
import React from "react"; import React from "react";
type IconType = (props: React.ComponentProps<"svg">) => JSX.Element; type IconType = (props: React.ComponentProps<"svg">) => JSX.Element;
@ -9,19 +9,19 @@ type Props = {
onChange: (status: boolean) => void; onChange: (status: boolean) => void;
texts: [string, string]; texts: [string, string];
icons: [IconType, IconType]; icons: [IconType, IconType];
spinIcon?: boolean;
}; };
export const Toggle: React.FC<Props> = ({ export const Toggle: React.FC<Props> = ({
texts: [onText, offText],
icons: [OnIcon, OffIcon],
status, status,
onChange, onChange,
texts: [onText, offText],
icons: [OnIcon, OffIcon],
spinIcon,
}) => { }) => {
const CurrentIcon = status ? OnIcon : OffIcon; const CurrentIcon = status ? OnIcon : OffIcon;
return ( return (
<motion.button <button
layout
transition={{ duration: 0.2 }}
className={clsx( className={clsx(
"rounded-md py-0.5 bg-slate-500 text-white text-xs font-semibold flex items-center space-x-1", "rounded-md py-0.5 bg-slate-500 text-white text-xs font-semibold flex items-center space-x-1",
status ? "bg-slate-500" : "bg-gray-400", status ? "bg-slate-500" : "bg-gray-400",
@ -30,12 +30,18 @@ export const Toggle: React.FC<Props> = ({
)} )}
onClick={() => onChange(!status)} onClick={() => onChange(!status)}
> >
<motion.div layout transition={{ duration: 0.2 }}> <div className="relative w-6 h-6" key={String(spinIcon)}>
<CurrentIcon className="w-6 h-6" /> <CurrentIcon
</motion.div> className={clsx(
<motion.span layout transition={{ duration: 0.2 }}> "w-6 h-6 absolute opacity-100",
{status ? onText : offText} spinIcon && "animate-hide"
</motion.span> )}
</motion.button> />
{spinIcon && (
<RefreshIcon className="w-6 h-6 absolute opacity-0 animate-appear-and-spin" />
)}
</div>
<span>{status ? onText : offText}</span>
</button>
); );
}; };

View File

@ -0,0 +1,64 @@
import React, { cloneElement, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import {
flip,
shift,
useDismiss,
useFloating,
useHover,
useInteractions,
useRole,
} from "@floating-ui/react-dom-interactions";
interface Props {
text: string;
children: JSX.Element;
}
export const Tooltip: React.FC<Props> = ({ text, children }) => {
const [isOpen, setIsOpen] = useState(false);
const { x, y, reference, floating, strategy, context } = useFloating({
placement: "top",
open: isOpen,
onOpenChange: setIsOpen,
middleware: [shift(), flip()],
});
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context),
useRole(context, { role: "tooltip" }),
useDismiss(context),
]);
return (
<>
{cloneElement(
children,
getReferenceProps({ ref: reference, ...children.props })
)}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
{...getFloatingProps({
ref: floating,
className:
"text-xs p-2 border border-gray-300 rounded bg-white z-10",
style: {
position: strategy,
top: y ?? 0,
left: x ?? 0,
},
})}
>
<div className="font-mono whitespace-pre">{text}</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};

View File

@ -2,5 +2,6 @@ export { SquiggleChart } from "./components/SquiggleChart";
export { SquiggleEditor, SquigglePartial } from "./components/SquiggleEditor"; export { SquiggleEditor, SquigglePartial } from "./components/SquiggleEditor";
export { SquigglePlayground } from "./components/SquigglePlayground"; export { SquigglePlayground } from "./components/SquigglePlayground";
export { SquiggleContainer } from "./components/SquiggleContainer"; export { SquiggleContainer } from "./components/SquiggleContainer";
export { SquiggleEditorWithImportedBindings } from "./components/SquiggleEditorWithImportedBindings";
export { mergeBindings } from "@quri/squiggle-lang"; export { mergeBindings } from "@quri/squiggle-lang";

View File

@ -100,12 +100,15 @@ export let expYScale: PowScale = {
}, },
}; };
export const defaultTickFormat = ".9~s";
export const defaultColor = "#739ECC";
export function buildVegaSpec( export function buildVegaSpec(
specOptions: DistributionChartSpecOptions specOptions: DistributionChartSpecOptions
): VisualizationSpec { ): VisualizationSpec {
let { let {
format = ".9~s", format = defaultTickFormat,
color = "#739ECC", color = defaultColor,
title, title,
minX, minX,
maxX, maxX,
@ -223,7 +226,7 @@ export function buildVegaSpec(
}, },
size: [{ value: 100 }], size: [{ value: 100 }],
tooltip: { tooltip: {
signal: "datum.y", signal: "{ probability: datum.y, value: datum.x }",
}, },
}, },
update: { update: {

View File

@ -0,0 +1,5 @@
import { shape } from "@quri/squiggle-lang";
export const hasMassBelowZero = (shape: shape) =>
shape.continuous.some((x) => x.x <= 0) ||
shape.discrete.some((x) => x.x <= 0);

View File

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

View File

@ -0,0 +1,22 @@
import { useState } from "react";
type ControlledValueArgs<T> = {
value?: T;
defaultValue: T;
onChange?: (x: T) => void;
};
export function useMaybeControlledValue<T>(
args: ControlledValueArgs<T>
): [T, (x: T) => void] {
let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue);
let value = args.value ?? uncontrolledValue;
let onChange = (newValue: T) => {
if (args.value === undefined) {
// uncontrolled mode
setUncontrolledValue(newValue);
}
args.onChange?.(newValue);
};
return [value, onChange];
}

View File

@ -0,0 +1,100 @@
import { useLayoutEffect, useReducer } from "react";
type State = {
autorunMode: boolean;
renderedCode: string;
// "prepared" is for rendering a spinner; "run" for executing squiggle code; then it gets back to "none" on the next render
runningState: "none" | "prepared" | "run";
executionId: number;
};
const buildInitialState = (code: string): State => ({
autorunMode: true,
renderedCode: "",
runningState: "none",
executionId: 1,
});
type Action =
| {
type: "SET_AUTORUN_MODE";
value: boolean;
code: string;
}
| {
type: "PREPARE_RUN";
}
| {
type: "RUN";
code: string;
}
| {
type: "STOP_RUN";
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_AUTORUN_MODE":
return {
...state,
autorunMode: action.value,
};
case "PREPARE_RUN":
return {
...state,
runningState: "prepared",
};
case "RUN":
return {
...state,
runningState: "run",
renderedCode: action.code,
executionId: state.executionId + 1,
};
case "STOP_RUN":
return {
...state,
runningState: "none",
};
}
};
export const useRunnerState = (code: string) => {
const [state, dispatch] = useReducer(reducer, buildInitialState(code));
useLayoutEffect(() => {
if (state.runningState === "prepared") {
// this is necessary for async playground loading - otherwise it executes the code synchronously on the initial load
// (it's surprising that this is necessary, but empirically it _is_ necessary, both with `useEffect` and `useLayoutEffect`)
setTimeout(() => {
dispatch({ type: "RUN", code });
}, 0);
} else if (state.runningState === "run") {
dispatch({ type: "STOP_RUN" });
}
}, [state.runningState, code]);
const run = () => {
// The rest will be handled by dispatches above on following renders, but we need to update the spinner first.
dispatch({ type: "PREPARE_RUN" });
};
if (
state.autorunMode &&
state.renderedCode !== code &&
state.runningState === "none"
) {
run();
}
return {
run,
autorunMode: state.autorunMode,
renderedCode: state.renderedCode,
isRunning: state.runningState !== "none",
executionId: state.executionId,
setAutorunMode: (newValue: boolean) => {
dispatch({ type: "SET_AUTORUN_MODE", value: newValue, code });
},
};
};

View File

@ -5,10 +5,11 @@ import {
run, run,
runPartial, runPartial,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo } from "react";
type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = { type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
code: string; code: string;
executionId?: number;
bindings?: bindings; bindings?: bindings;
jsImports?: jsImports; jsImports?: jsImports;
environment?: environment; environment?: environment;
@ -21,7 +22,15 @@ const useSquiggleAny = <T extends ReturnType<typeof run | typeof runPartial>>(
) => { ) => {
const result: T = useMemo<T>( const result: T = useMemo<T>(
() => f(args.code, args.bindings, args.environment, args.jsImports), () => f(args.code, args.bindings, args.environment, args.jsImports),
[f, args.code, args.bindings, args.environment, args.jsImports] // eslint-disable-next-line react-hooks/exhaustive-deps
[
f,
args.code,
args.bindings,
args.environment,
args.jsImports,
args.executionId,
]
); );
const { onChange } = args; const { onChange } = args;
@ -42,23 +51,3 @@ export const useSquigglePartial = (
export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => { export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => {
return useSquiggleAny(args, run); return useSquiggleAny(args, run);
}; };
type ControlledValueArgs<T> = {
value?: T;
defaultValue: T;
onChange?: (x: T) => void;
};
export function useMaybeControlledValue<T>(
args: ControlledValueArgs<T>
): [T, (x: T) => void] {
let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue);
let value = args.value ?? uncontrolledValue;
let onChange = (newValue: T) => {
if (args.value === undefined) {
// uncontrolled mode
setUncontrolledValue(newValue);
}
args.onChange?.(newValue);
};
return [value, onChange];
}

View File

@ -43,7 +43,7 @@ could be continuous, discrete or mixed.
<Story <Story
name="Continuous Pointset" name="Continuous Pointset"
args={{ args={{
code: "toPointSet(normal(5,2))", code: "PointSet.fromDist(normal(5,2))",
width, width,
}} }}
> >
@ -57,7 +57,7 @@ could be continuous, discrete or mixed.
<Story <Story
name="Continuous SampleSet" name="Continuous SampleSet"
args={{ args={{
code: "toSampleSet(normal(5,2), 1000)", code: "SampleSet.fromDist(normal(5,2))",
width, width,
}} }}
> >

View File

@ -21,3 +21,16 @@ including sampling settings, in squiggle.
{Template.bind({})} {Template.bind({})}
</Story> </Story>
</Canvas> </Canvas>
<Canvas>
<Story
name="With share button"
args={{
defaultCode: "normal(5,2)",
height: 800,
showShareButton: true,
}}
>
{Template.bind({})}
</Story>
</Canvas>

View File

@ -5,6 +5,27 @@ module.exports = {
}, },
important: ".squiggle", important: ".squiggle",
theme: { theme: {
extend: {}, extend: {
animation: {
"appear-and-spin":
"spin 1s linear infinite, squiggle-appear 0.2s forwards",
"semi-appear": "squiggle-semi-appear 0.2s forwards",
hide: "squiggle-hide 0.2s forwards",
},
keyframes: {
"squiggle-appear": {
from: { opacity: 0 },
to: { opacity: 1 },
},
"squiggle-semi-appear": {
from: { opacity: 0 },
to: { opacity: 0.5 },
},
"squiggle-hide": {
from: { opacity: 1 },
to: { opacity: 0 },
},
},
},
}, },
}; };

View File

@ -20,7 +20,7 @@ environment created from the squiggle code.
```js ```js
import { run } from "@quri/squiggle-lang"; import { run } from "@quri/squiggle-lang";
run( run(
"normal(0, 1) * fromSamples([-3,-2,-1,1,2,3,3,3,4,9]" "normal(0, 1) * SampleSet.fromList([-3, 2,-1,1,2,3,3,3,4,9])"
).value.value.toSparkline().value; ).value.value.toSparkline().value;
``` ```

View File

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

View File

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

View File

@ -20,7 +20,7 @@ describe("Peggy parse type", () => {
"{(::$_typeOf_$ :f (::$_typeFunction_$ (::$_constructArray_$ (#number #number #number))))}", "{(::$_typeOf_$ :f (::$_typeFunction_$ (::$_constructArray_$ (#number #number #number))))}",
) )
}) })
describe("high priority modifier", () => { describe("high priority contract", () => {
testParse( testParse(
"answer: number<-min<-max(100)|string", "answer: number<-min<-max(100)|string",
"{(::$_typeOf_$ :answer (::$_typeOr_$ (::$_constructArray_$ ((::$_typeModifier_max_$ (::$_typeModifier_min_$ #number) 100) #string))))}", "{(::$_typeOf_$ :answer (::$_typeOr_$ (::$_constructArray_$ ((::$_typeModifier_max_$ (::$_typeModifier_min_$ #number) 100) #string))))}",
@ -30,7 +30,7 @@ describe("Peggy parse type", () => {
"{(::$_typeOf_$ :answer (::$_typeModifier_memberOf_$ #number (::$_constructArray_$ (1 3 5))))}", "{(::$_typeOf_$ :answer (::$_typeModifier_memberOf_$ #number (::$_constructArray_$ (1 3 5))))}",
) )
}) })
describe("low priority modifier", () => { describe("low priority contract", () => {
testParse( testParse(
"answer: number | string $ opaque", "answer: number | string $ opaque",
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}", "{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
@ -63,14 +63,14 @@ describe("Peggy parse type", () => {
"{(::$_typeOf_$ :weekend (::$_typeOr_$ (::$_constructArray_$ ((::$_typeConstructor_$ #Saturday (::$_constructArray_$ ())) (::$_typeConstructor_$ #Sunday (::$_constructArray_$ ()))))))}", "{(::$_typeOf_$ :weekend (::$_typeOr_$ (::$_constructArray_$ ((::$_typeConstructor_$ #Saturday (::$_constructArray_$ ())) (::$_typeConstructor_$ #Sunday (::$_constructArray_$ ()))))))}",
) )
}) })
describe("type paranthesis", () => { describe("type parenthesis", () => {
//$ is introduced to avoid paranthesis //$ is introduced to avoid parenthesis
testParse( testParse(
"answer: (number|string)<-opaque", "answer: (number|string)<-opaque",
"{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}", "{(::$_typeOf_$ :answer (::$_typeModifier_opaque_$ (::$_typeOr_$ (::$_constructArray_$ (#number #string)))))}",
) )
}) })
describe("squiggle expressions in type modifiers", () => { describe("squiggle expressions in type contracts", () => {
testParse( testParse(
"odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))", "odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))",
"{:odds1 = {(::$_constructArray_$ (1 3 5))}; :odds2 = {(::$_constructArray_$ (7 9))}; (::$_typeAlias_$ #odds (::$_typeModifier_memberOf_$ #number (::concat :odds1 :odds2)))}", "{:odds1 = {(::$_constructArray_$ (1 3 5))}; :odds2 = {(::$_constructArray_$ (7 9))}; (::$_typeAlias_$ #odds (::$_typeModifier_memberOf_$ #number (::concat :odds1 :odds2)))}",

View File

@ -3,11 +3,10 @@ module InternalExpressionValue = ReducerInterface_InternalExpressionValue
open Jest open Jest
open Reducer_Peggy_TestHelpers open Reducer_Peggy_TestHelpers
open Expect
describe("Peggy to Expression", () => { describe("Peggy to Expression", () => {
describe("literals operators parenthesis", () => { describe("literals operators parenthesis", () => {
// Note that there is always an outer block. Otherwise, external bindings are ignrored at the first statement // Note that there is always an outer block. Otherwise, external bindings are ignored at the first statement
testToExpression("1", "{1}", ~v="1", ()) testToExpression("1", "{1}", ~v="1", ())
testToExpression("'hello'", "{'hello'}", ~v="'hello'", ()) testToExpression("'hello'", "{'hello'}", ~v="'hello'", ())
testToExpression("true", "{true}", ~v="true", ()) testToExpression("true", "{true}", ~v="true", ())

View File

@ -40,7 +40,7 @@ describe("Peggy Types to Expression", () => {
(), (),
) )
}) })
describe("high priority modifier", () => { describe("high priority contract", () => {
testToExpression( testToExpression(
"answer: number<-min(1)<-max(100)|string", "answer: number<-min(1)<-max(100)|string",
"{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ ((:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 100) #string))))}", "{(:$_typeOf_$ :answer (:$_typeOr_$ (:$_constructArray_$ ((:$_typeModifier_max_$ (:$_typeModifier_min_$ #number 1) 100) #string))))}",
@ -78,7 +78,7 @@ describe("Peggy Types to Expression", () => {
(), (),
) )
}) })
describe("low priority modifier", () => { describe("low priority contract", () => {
testToExpression( testToExpression(
"answer: number | string $ opaque", "answer: number | string $ opaque",
"{(:$_typeOf_$ :answer (:$_typeModifier_opaque_$ (:$_typeOr_$ (:$_constructArray_$ (#number #string)))))}", "{(:$_typeOf_$ :answer (:$_typeModifier_opaque_$ (:$_typeOr_$ (:$_constructArray_$ (#number #string)))))}",
@ -86,7 +86,7 @@ describe("Peggy Types to Expression", () => {
(), (),
) )
}) })
describe("squiggle expressions in type modifiers", () => { describe("squiggle expressions in type contracts", () => {
testToExpression( testToExpression(
"odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))", "odds1 = [1,3,5]; odds2 = [7, 9]; type odds = number<-memberOf(concat(odds1, odds2))",
"{(:$_let_$ :odds1 {(:$_constructArray_$ (1 3 5))}); (:$_let_$ :odds2 {(:$_constructArray_$ (7 9))}); (:$_typeAlias_$ #odds (:$_typeModifier_memberOf_$ #number (:concat :odds1 :odds2)))}", "{(:$_let_$ :odds1 {(:$_constructArray_$ (1 3 5))}); (:$_let_$ :odds2 {(:$_constructArray_$ (7 9))}); (:$_typeAlias_$ #odds (:$_typeModifier_memberOf_$ #number (:concat :odds1 :odds2)))}",

View File

@ -0,0 +1,20 @@
open Jest
open Reducer_Peggy_TestHelpers
describe("Peggy void", () => {
//literal
testToExpression("()", "{()}", ~v="()", ())
testToExpression(
"fn()=1",
"{(:$_let_$ :fn (:$$_lambda_$$ [_] {1}))}",
~v="@{fn: lambda(_=>internal code)}",
(),
)
testToExpression("fn()=1; fn()", "{(:$_let_$ :fn (:$$_lambda_$$ [_] {1})); (:fn ())}", ~v="1", ())
testToExpression(
"fn(a)=(); call fn(1)",
"{(:$_let_$ :fn (:$$_lambda_$$ [a] {()})); (:$_let_$ :_ {(:fn 1)})}",
~v="@{_: (),fn: lambda(a=>internal code)}",
(),
)
})

View File

@ -0,0 +1,52 @@
module Expression = Reducer_Expression
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module Bindings = Reducer_Bindings
module T = Reducer_Type_T
module TypeCompile = Reducer_Type_Compile
open Jest
open Expect
let myIevEval = (aTypeSourceCode: string) =>
TypeCompile.ievFromTypeExpression(aTypeSourceCode, Expression.reduceExpression)
let myIevEvalToString = (aTypeSourceCode: string) =>
myIevEval(aTypeSourceCode)->InternalExpressionValue.toStringResult
let myIevExpectEqual = (aTypeSourceCode, answer) =>
expect(myIevEvalToString(aTypeSourceCode))->toEqual(answer)
let myIevTest = (test, aTypeSourceCode, answer) =>
test(aTypeSourceCode, () => myIevExpectEqual(aTypeSourceCode, answer))
let myTypeEval = (aTypeSourceCode: string) =>
TypeCompile.fromTypeExpression(aTypeSourceCode, Expression.reduceExpression)
let myTypeEvalToString = (aTypeSourceCode: string) => myTypeEval(aTypeSourceCode)->T.toStringResult
let myTypeExpectEqual = (aTypeSourceCode, answer) =>
expect(myTypeEvalToString(aTypeSourceCode))->toEqual(answer)
let myTypeTest = (test, aTypeSourceCode, answer) =>
test(aTypeSourceCode, () => myTypeExpectEqual(aTypeSourceCode, answer))
// | ItTypeIdentifier(string)
myTypeTest(test, "number", "number")
myTypeTest(test, "(number)", "number")
// | ItModifiedType({modifiedType: iType})
myIevTest(test, "number<-min(0)", "Ok({min: 0,typeIdentifier: #number,typeTag: 'typeIdentifier'})")
myTypeTest(test, "number<-min(0)", "number<-min(0)")
// | ItTypeOr({typeOr: array<iType>})
myTypeTest(test, "number | string", "(number | string)")
// | ItTypeFunction({inputs: array<iType>, output: iType})
myTypeTest(test, "number => number => number", "(number => number => number)")
// | ItTypeArray({element: iType})
myIevTest(test, "[number]", "Ok({element: #number,typeTag: 'typeArray'})")
myTypeTest(test, "[number]", "[number]")
// | ItTypeTuple({elements: array<iType>})
myTypeTest(test, "[number, string]", "[number, string]")
// | ItTypeRecord({properties: Belt.Map.String.t<iType>})
myIevTest(
test,
"{age: number, name: string}",
"Ok({properties: {age: #number,name: #string},typeTag: 'typeRecord'})",
)
myTypeTest(test, "{age: number, name: string}", "{age: number, name: string}")

View File

@ -0,0 +1,41 @@
module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T
module ErrorValue = Reducer_ErrorValue
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module Bindings = Reducer_Bindings
module T = Reducer_Type_T
module TypeChecker = Reducer_Type_TypeChecker
open Jest
open Expect
let checkArgumentsSourceCode = (aTypeSourceCode: string, sourceCode: string): result<
'v,
ErrorValue.t,
> => {
let reducerFn = Expression.reduceExpression
let rResult =
Reducer.parse(sourceCode)->Belt.Result.flatMap(expr =>
reducerFn(expr, Bindings.emptyBindings, InternalExpressionValue.defaultEnvironment)
)
rResult->Belt.Result.flatMap(result =>
switch result {
| IEvArray(args) => TypeChecker.checkArguments(aTypeSourceCode, args, reducerFn)
| _ => Js.Exn.raiseError("Arguments has to be an array")
}
)
}
let myCheckArguments = (aTypeSourceCode: string, sourceCode: string): string =>
switch checkArgumentsSourceCode(aTypeSourceCode, sourceCode) {
| Ok(_) => "Ok"
| Error(error) => ErrorValue.errorToString(error)
}
let myCheckArgumentsExpectEqual = (aTypeSourceCode, sourceCode, answer) =>
expect(myCheckArguments(aTypeSourceCode, sourceCode))->toEqual(answer)
let myCheckArgumentsTest = (test, aTypeSourceCode, sourceCode, answer) =>
test(aTypeSourceCode, () => myCheckArgumentsExpectEqual(aTypeSourceCode, sourceCode, answer))
myCheckArgumentsTest(test, "number=>number=>number", "[1,2]", "Ok")

View File

@ -0,0 +1,72 @@
module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T
module ErrorValue = Reducer_ErrorValue
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module Bindings = Reducer_Bindings
module T = Reducer_Type_T
module TypeChecker = Reducer_Type_TypeChecker
open Jest
open Expect
// In development, you are expected to use TypeChecker.isTypeOf(aTypeSourceCode, result, reducerFn).
// isTypeOfSourceCode is written to use strings instead of expression values.
let isTypeOfSourceCode = (aTypeSourceCode: string, sourceCode: string): result<
'v,
ErrorValue.t,
> => {
let reducerFn = Expression.reduceExpression
let rResult =
Reducer.parse(sourceCode)->Belt.Result.flatMap(expr =>
reducerFn(expr, Bindings.emptyBindings, InternalExpressionValue.defaultEnvironment)
)
rResult->Belt.Result.flatMap(result => TypeChecker.isTypeOf(aTypeSourceCode, result, reducerFn))
}
let myTypeCheck = (aTypeSourceCode: string, sourceCode: string): string =>
switch isTypeOfSourceCode(aTypeSourceCode, sourceCode) {
| Ok(_) => "Ok"
| Error(error) => ErrorValue.errorToString(error)
}
let myTypeCheckExpectEqual = (aTypeSourceCode, sourceCode, answer) =>
expect(myTypeCheck(aTypeSourceCode, sourceCode))->toEqual(answer)
let myTypeCheckTest = (test, aTypeSourceCode, sourceCode, answer) =>
test(aTypeSourceCode, () => myTypeCheckExpectEqual(aTypeSourceCode, sourceCode, answer))
myTypeCheckTest(test, "number", "1", "Ok")
myTypeCheckTest(test, "number", "'2'", "Expected type: number but got: '2'")
myTypeCheckTest(test, "string", "3", "Expected type: string but got: 3")
myTypeCheckTest(test, "string", "'a'", "Ok")
myTypeCheckTest(test, "[number]", "[1,2,3]", "Ok")
myTypeCheckTest(test, "[number]", "['a','a','a']", "Expected type: number but got: 'a'")
myTypeCheckTest(test, "[number]", "[1,'a',3]", "Expected type: number but got: 'a'")
myTypeCheckTest(test, "[number, string]", "[1,'a']", "Ok")
myTypeCheckTest(test, "[number, string]", "[1, 2]", "Expected type: string but got: 2")
myTypeCheckTest(
test,
"[number, string, string]",
"[1,'a']",
"Expected type: [number, string, string] but got: [1,'a']",
)
myTypeCheckTest(
test,
"[number, string]",
"[1,'a', 3]",
"Expected type: [number, string] but got: [1,'a',3]",
)
myTypeCheckTest(test, "{age: number, name: string}", "{age: 1, name: 'a'}", "Ok")
myTypeCheckTest(
test,
"{age: number, name: string}",
"{age: 1, name: 'a', job: 'IT'}",
"Expected type: {age: number, name: string} but got: {age: 1,job: 'IT',name: 'a'}",
)
myTypeCheckTest(test, "number | string", "1", "Ok")
myTypeCheckTest(test, "date | string", "1", "Expected type: (date | string) but got: 1")
myTypeCheckTest(test, "number<-min(10)", "10", "Ok")
myTypeCheckTest(test, "number<-min(10)", "0", "Expected type: number<-min(10) but got: 0")
myTypeCheckTest(test, "any", "0", "Ok")
myTypeCheckTest(test, "any", "'a'", "Ok")

View File

@ -0,0 +1,123 @@
open Jest
open Expect
module DispatchT = Reducer_Dispatch_T
module Expression = Reducer_Expression
module ExpressionT = Reducer_Expression_T
module TypeCompile = Reducer_Type_Compile
module TypeChecker = Reducer_Type_TypeChecker
open ReducerInterface_InternalExpressionValue
type errorValue = Reducer_ErrorValue.errorValue
// Let's build a function to replace switch statements
// In dispatchChainPiece, we execute an return the result of execution if there is a type match.
// Otherwise we return None so that the call chain can continue.
// So we want to build a function like
// dispatchChainPiece = (call: functionCall, environment): option<result<internalExpressionValue, errorValue>>
// Now lets make the dispatchChainPiece itself.
// Note that I am not passing the reducer to the dispatchChainPiece as an argument because it is in the context anyway.
// Keep in mind that reducerFn is necessary for map/reduce so dispatchChainPiece should have a reducerFn in context.
let makeMyDispatchChainPiece = (reducer: ExpressionT.reducerFn): DispatchT.dispatchChainPiece => {
// Let's have a pure implementations
module Implementation = {
let stringConcat = (a: string, b: string): string => Js.String2.concat(a, b)
let arrayConcat = (
a: Js.Array2.t<internalExpressionValue>,
b: Js.Array2.t<internalExpressionValue>,
): Js.Array2.t<internalExpressionValue> => Js.Array2.concat(a, b)
let plot = _r => "yey, plotted"
}
let extractStringString = args =>
switch args {
| [IEvString(a), IEvString(b)] => (a, b)
| _ => raise(Reducer_Exception.ImpossibleException("extractStringString developer error"))
}
let extractArrayArray = args =>
switch args {
| [IEvArray(a), IEvArray(b)] => (a, b)
| _ => raise(Reducer_Exception.ImpossibleException("extractArrayArray developer error"))
}
// Let's bridge the pure implementation to expression values
module Bridge = {
let stringConcat: DispatchT.genericIEvFunction = (args, _environment) => {
let (a, b) = extractStringString(args)
Implementation.stringConcat(a, b)->IEvString->Ok
}
let arrayConcat: DispatchT.genericIEvFunction = (args, _environment) => {
let (a, b) = extractArrayArray(args)
Implementation.arrayConcat(a, b)->IEvArray->Ok
}
let plot: DispatchT.genericIEvFunction = (args, _environment) => {
switch args {
// Just assume that we are doing the business of extracting and converting the deep record
| [IEvRecord(_)] => Implementation.plot({"title": "This is a plot"})->IEvString->Ok
| _ => raise(Reducer_Exception.ImpossibleException("plot developer error"))
}
}
}
// concat functions are to illustrate polymoprhism. And the plot function is to illustrate complex types
let jumpTable = [
(
"concat",
TypeCompile.fromTypeExpressionExn("string=>string=>string", reducer),
Bridge.stringConcat,
),
(
"concat",
TypeCompile.fromTypeExpressionExn("[any]=>[any]=>[any]", reducer),
Bridge.arrayConcat,
),
(
"plot",
TypeCompile.fromTypeExpressionExn(
// Nested complex types are available
// records {property: type}
// arrays [type]
// tuples [type, type]
// <- type contracts are available naturally and they become part of dispatching
// Here we are not enumerating the possibilities because type checking has a dedicated test
"{title: string, line: {width: number, color: string}}=>string",
reducer,
),
Bridge.plot,
),
]
//Here we are creating a dispatchChainPiece function that will do the actual dispatch from the jumpTable
Reducer_Dispatch_ChainPiece.makeFromTypes(jumpTable)
}
// And finally, let's write a library dispatch for our external library
// Exactly the same as the one used in real life
let _dispatch = (
call: functionCall,
environment,
reducer: Reducer_Expression_T.reducerFn,
chain,
): result<internalExpressionValue, 'e> => {
let dispatchChainPiece = makeMyDispatchChainPiece(reducer)
dispatchChainPiece(call, environment)->E.O2.defaultFn(() => chain(call, environment, reducer))
}
// What is important about this implementation?
// A) Exactly the same function jump table can be used to create type guarded lambda functions
// Guarded lambda functions will be the basis of the next version of Squiggle
// B) Complicated recursive record types are not a problem.
describe("Type Dispatch", () => {
let reducerFn = Expression.reduceExpression
let dispatchChainPiece = makeMyDispatchChainPiece(reducerFn)
test("stringConcat", () => {
let call: functionCall = ("concat", [IEvString("hello"), IEvString("world")])
let result = dispatchChainPiece(call, defaultEnvironment)
expect(result)->toEqual(Some(Ok(IEvString("helloworld"))))
})
})

View File

@ -1,15 +1,6 @@
open Jest open Jest
open Reducer_TestHelpers open Reducer_TestHelpers
describe("map reduce", () => {
testEvalToBe("double(x)=2*x; arr=[1,2,3]; map(arr, double)", "Ok([2,4,6])")
testEvalToBe("myadd(acc,x)=acc+x; arr=[1,2,3]; reduce(arr, 0, myadd)", "Ok(6)")
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; reduce(arr, 0, change)", "Ok(15)")
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; reduceReverse(arr, 0, change)", "Ok(9)")
testEvalToBe("arr=[1,2,3]; reverse(arr)", "Ok([3,2,1])")
testEvalToBe("check(x)=(x==2);arr=[1,2,3]; filter(arr,check)", "Ok([2])")
})
Skip.describe("map reduce (sam)", () => { Skip.describe("map reduce (sam)", () => {
testEvalToBe("addone(x)=x+1; map(2, addone)", "Error???") testEvalToBe("addone(x)=x+1; map(2, addone)", "Error???")
testEvalToBe("addone(x)=x+1; map(2, {x: addone})", "Error???") testEvalToBe("addone(x)=x+1; map(2, {x: addone})", "Error???")

View File

@ -10,5 +10,5 @@ describe("Evaluate ternary operator", () => {
testEvalToBe("false ? 'YES' : 'NO'", "Ok('NO')") testEvalToBe("false ? 'YES' : 'NO'", "Ok('NO')")
testEvalToBe("2 > 1 ? 'YES' : 'NO'", "Ok('YES')") testEvalToBe("2 > 1 ? 'YES' : 'NO'", "Ok('YES')")
testEvalToBe("2 <= 1 ? 'YES' : 'NO'", "Ok('NO')") testEvalToBe("2 <= 1 ? 'YES' : 'NO'", "Ok('NO')")
testEvalToBe("1+1 ? 'YES' : 'NO'", "Error(Expected type: Boolean)") testEvalToBe("1+1 ? 'YES' : 'NO'", "Error(Expected type: Boolean but got: )")
}) })

View File

@ -27,6 +27,15 @@ describe("eval", () => {
test("index", () => expectEvalToBe("r = {a: 1}; r.a", "Ok(1)")) test("index", () => expectEvalToBe("r = {a: 1}; r.a", "Ok(1)"))
test("index", () => expectEvalToBe("r = {a: 1}; r.b", "Error(Record property not found: b)")) test("index", () => expectEvalToBe("r = {a: 1}; r.b", "Error(Record property not found: b)"))
testEvalError("{a: 1}.b") // invalid syntax testEvalError("{a: 1}.b") // invalid syntax
test("always the same property ending", () =>
expectEvalToBe(
`{
a: 1,
b: 2,
}`,
"Ok({a: 1,b: 2})",
)
)
}) })
describe("multi-line", () => { describe("multi-line", () => {

View File

@ -42,12 +42,6 @@ describe("eval on distribution functions", () => {
describe("normalize", () => { describe("normalize", () => {
testEval("normalize(normal(5,2))", "Ok(Normal(5,2))") testEval("normalize(normal(5,2))", "Ok(Normal(5,2))")
}) })
describe("toPointSet", () => {
testEval("toPointSet(normal(5,2))", "Ok(Point Set Distribution)")
})
describe("toSampleSet", () => {
testEval("toSampleSet(normal(5,2), 100)", "Ok(Sample Set Distribution)")
})
describe("add", () => { describe("add", () => {
testEval("add(normal(5,2), normal(10,2))", "Ok(Normal(15,2.8284271247461903))") testEval("add(normal(5,2), normal(10,2))", "Ok(Normal(15,2.8284271247461903))")
testEval("add(normal(5,2), lognormal(10,2))", "Ok(Sample Set Distribution)") testEval("add(normal(5,2), lognormal(10,2))", "Ok(Sample Set Distribution)")

View File

@ -0,0 +1,98 @@
open Jest
open Expect
open Reducer_TestHelpers
let expectEvalToBeOk = (expr: string) =>
Reducer.evaluate(expr)->Reducer_Helpers.rRemoveDefaultsExternal->E.R.isOk->expect->toBe(true)
let registry = FunctionRegistry_Library.registry
let examples = E.A.to_list(FunctionRegistry_Core.Registry.allExamples(registry))
describe("FunctionRegistry Library", () => {
describe("Regular tests", () => {
testEvalToBe("List.make(3, 'HI')", "Ok(['HI','HI','HI'])")
testEvalToBe("make(3, 'HI')", "Error(Function not found: make(Number,String))")
testEvalToBe("List.upTo(1,3)", "Ok([1,2,3])")
testEvalToBe("List.first([3,5,8])", "Ok(3)")
testEvalToBe("List.last([3,5,8])", "Ok(8)")
testEvalToBe("List.reverse([3,5,8])", "Ok([8,5,3])")
testEvalToBe("double(x)=2*x; arr=[1,2,3]; List.map(arr, double)", "Ok([2,4,6])")
testEvalToBe("double(x)=2*x; arr=[1,2,3]; map(arr, double)", "Ok([2,4,6])")
testEvalToBe("myadd(acc,x)=acc+x; arr=[1,2,3]; List.reduce(arr, 0, myadd)", "Ok(6)")
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; List.reduce(arr, 0, change)", "Ok(15)")
testEvalToBe("change(acc,x)=acc*x+x; arr=[1,2,3]; List.reduceReverse(arr, 0, change)", "Ok(9)")
testEvalToBe("check(x)=(x==2);arr=[1,2,3]; List.filter(arr,check)", "Ok([2])")
testEvalToBe("arr=[1,2,3]; List.reverse(arr)", "Ok([3,2,1])")
testEvalToBe("Dist.normal(5,2)", "Ok(Normal(5,2))")
testEvalToBe("normal(5,2)", "Ok(Normal(5,2))")
testEvalToBe("normal({mean:5,stdev:2})", "Ok(Normal(5,2))")
testEvalToBe("-2 to 4", "Ok(Normal(1,1.8238704957353074))")
testEvalToBe("pointMass(5)", "Ok(PointMass(5))")
testEvalToBe("Number.floor(5.5)", "Ok(5)")
testEvalToBe("Number.ceil(5.5)", "Ok(6)")
testEvalToBe("floor(5.5)", "Ok(5)")
testEvalToBe("ceil(5.5)", "Ok(6)")
testEvalToBe("Number.abs(5.5)", "Ok(5.5)")
testEvalToBe("abs(5.5)", "Ok(5.5)")
testEvalToBe("Number.exp(10)", "Ok(22026.465794806718)")
testEvalToBe("Number.log10(10)", "Ok(1)")
testEvalToBe("Number.log2(10)", "Ok(3.321928094887362)")
testEvalToBe("Number.sum([2,5,3])", "Ok(10)")
testEvalToBe("sum([2,5,3])", "Ok(10)")
testEvalToBe("Number.product([2,5,3])", "Ok(30)")
testEvalToBe("Number.min([2,5,3])", "Ok(2)")
testEvalToBe("Number.max([2,5,3])", "Ok(5)")
testEvalToBe("Number.mean([0,5,10])", "Ok(5)")
testEvalToBe("Number.geomean([1,5,18])", "Ok(4.481404746557164)")
testEvalToBe("Number.stdev([0,5,10,15])", "Ok(5.5901699437494745)")
testEvalToBe("Number.variance([0,5,10,15])", "Ok(31.25)")
testEvalToBe("Number.sort([10,0,15,5])", "Ok([0,5,10,15])")
testEvalToBe("Number.cumsum([1,5,3])", "Ok([1,6,9])")
testEvalToBe("Number.cumprod([1,5,3])", "Ok([1,5,15])")
testEvalToBe("Number.diff([1,5,3])", "Ok([4,-2])")
testEvalToBe(
"Dist.logScore({estimate: normal(5,2), answer: normal(5.2,1), prior: normal(5.5,3)})",
"Ok(-0.33591375663884876)",
)
testEvalToBe(
"Dist.logScore({estimate: normal(5,2), answer: normal(5.2,1)})",
"Ok(0.32244107041564646)",
)
testEvalToBe("Dist.logScore({estimate: normal(5,2), answer: 4.5})", "Ok(1.6433360626394853)")
testEvalToBe("Dist.klDivergence(normal(5,2), normal(5,1.5))", "Ok(0.06874342818671068)")
testEvalToBe("SampleSet.fromList([3,5,2,3,5,2,3,5,2,3,3,5])", "Ok(Sample Set Distribution)")
testEvalToBe("SampleSet.fromList([3,5,2,3,5,2,3,5,2,3,3,5])", "Ok(Sample Set Distribution)")
testEvalToBe("SampleSet.fromFn({|| sample(normal(5,2))})", "Ok(Sample Set Distribution)")
testEvalToBe(
"addOne(t)=t+1; SampleSet.toList(SampleSet.map(SampleSet.fromList([1,2,3,4,5,6]), addOne))",
"Ok([2,3,4,5,6,7])",
)
testEvalToBe(
"SampleSet.toList(SampleSet.mapN([SampleSet.fromList([1,2,3,4,5,6]), SampleSet.fromList([6, 5, 4, 3, 2, 1])], {|x| x[0] > x[1] ? x[0] : x[1]}))",
"Ok([6,5,4,4,5,6])",
)
})
describe("Fn auto-testing", () => {
testAll("tests of validity", examples, r => {
expectEvalToBeOk(r)
})
testAll(
"tests of type",
E.A.to_list(
FunctionRegistry_Core.Registry.allExamplesWithFns(registry)->E.A2.filter(((fn, _)) =>
E.O.isSome(fn.output)
),
),
((fn, example)) => {
let responseType =
example
->Reducer.evaluate
->E.R2.fmap(ReducerInterface_InternalExpressionValue.externalValueToValueType)
let expectedOutputType = fn.output |> E.O.toExn("")
expect(responseType)->toEqual(Ok(expectedOutputType))
},
)
})
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@quri/squiggle-lang", "name": "@quri/squiggle-lang",
"version": "0.2.11", "version": "0.3.0",
"homepage": "https://squiggle-language.com", "homepage": "https://squiggle-language.com",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@ -20,6 +20,7 @@
"test:ts": "jest __tests__/TS/", "test:ts": "jest __tests__/TS/",
"test:rescript": "jest --modulePathIgnorePatterns=__tests__/TS/*", "test:rescript": "jest --modulePathIgnorePatterns=__tests__/TS/*",
"test:watch": "jest --watchAll", "test:watch": "jest --watchAll",
"test:fnRegistry": "jest __tests__/SquiggleLibrary/SquiggleLibrary_FunctionRegistryLibrary_test.bs.js",
"coverage:rescript": "rm -f *.coverage && yarn clean && BISECT_ENABLE=yes yarn build && yarn test:rescript && bisect-ppx-report html", "coverage:rescript": "rm -f *.coverage && yarn clean && BISECT_ENABLE=yes yarn build && yarn test:rescript && bisect-ppx-report html",
"coverage:ts": "yarn clean && yarn build && nyc --reporter=lcov yarn test:ts", "coverage:ts": "yarn clean && yarn build && nyc --reporter=lcov yarn test:ts",
"coverage:rescript:ci": "yarn clean && BISECT_ENABLE=yes yarn build:rescript && yarn test:rescript && bisect-ppx-report send-to Codecov", "coverage:rescript:ci": "yarn clean && BISECT_ENABLE=yes yarn build:rescript && yarn test:rescript && bisect-ppx-report send-to Codecov",
@ -31,6 +32,7 @@
"format:prettier": "prettier --write .", "format:prettier": "prettier --write .",
"format": "yarn format:rescript && yarn format:prettier", "format": "yarn format:rescript && yarn format:prettier",
"prepack": "yarn build && yarn test && yarn bundle", "prepack": "yarn build && yarn test && yarn bundle",
"all:rescript": "yarn build:rescript && yarn test:rescript && yarn format:rescript",
"all": "yarn build && yarn bundle && yarn test" "all": "yarn build && yarn bundle && yarn test"
}, },
"keywords": [ "keywords": [
@ -42,7 +44,7 @@
"@stdlib/stats": "^0.0.13", "@stdlib/stats": "^0.0.13",
"jstat": "^1.9.5", "jstat": "^1.9.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mathjs": "^10.6.4", "mathjs": "^11.0.1",
"pdfast": "^0.2.0" "pdfast": "^0.2.0"
}, },
"devDependencies": { "devDependencies": {
@ -53,7 +55,7 @@
"bisect_ppx": "^2.7.1", "bisect_ppx": "^2.7.1",
"chalk": "^5.0.1", "chalk": "^5.0.1",
"codecov": "^3.8.3", "codecov": "^3.8.3",
"fast-check": "^3.0.1", "fast-check": "^3.1.1",
"gentype": "^4.5.0", "gentype": "^4.5.0",
"jest": "^27.5.1", "jest": "^27.5.1",
"moduleserve": "^0.9.1", "moduleserve": "^0.9.1",
@ -67,7 +69,7 @@
"ts-loader": "^9.3.0", "ts-loader": "^9.3.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"webpack": "^5.73.0", "webpack": "^5.74.0",
"webpack-cli": "^4.10.0" "webpack-cli": "^4.10.0"
}, },
"source": "./src/js/index.ts", "source": "./src/js/index.ts",

View File

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

View File

@ -18,6 +18,7 @@ import { tagged, tag } from "./types";
// Raw rescript types. // Raw rescript types.
export type rescriptExport = export type rescriptExport =
| 0 // EvVoid
| { | {
TAG: 0; // EvArray TAG: 0; // EvArray
_0: rescriptExport[]; _0: rescriptExport[];
@ -131,7 +132,8 @@ export type squiggleExpression =
| tagged<"record", { [key: string]: squiggleExpression }> | tagged<"record", { [key: string]: squiggleExpression }>
| tagged<"type", { [key: string]: squiggleExpression }> | tagged<"type", { [key: string]: squiggleExpression }>
| tagged<"typeIdentifier", string> | tagged<"typeIdentifier", string>
| tagged<"module", { [key: string]: squiggleExpression }>; | tagged<"module", { [key: string]: squiggleExpression }>
| tagged<"void", string>;
export { lambdaValue }; export { lambdaValue };
@ -139,6 +141,10 @@ export function convertRawToTypescript(
result: rescriptExport, result: rescriptExport,
environment: environment environment: environment
): squiggleExpression { ): squiggleExpression {
if (typeof result === "number") {
// EvVoid
return tag("void", "");
}
switch (result.TAG) { switch (result.TAG) {
case 0: // EvArray case 0: // EvArray
return tag( return tag(

View File

@ -250,7 +250,7 @@ module T = Dist({
let downsample = (length, t): t => let downsample = (length, t): t =>
t |> shapeMap(XYShape.XsConversion.proportionByProbabilityMass(length, integral(t).xyShape)) t |> shapeMap(XYShape.XsConversion.proportionByProbabilityMass(length, integral(t).xyShape))
let integralEndY = (t: t) => t.integralSumCache |> E.O.default(t |> integral |> lastY) let integralEndY = (t: t) => t.integralSumCache |> E.O.defaultFn(() => t |> integral |> lastY)
let integralXtoY = (f, t: t) => t |> integral |> shapeFn(XYShape.XtoY.linear(f)) let integralXtoY = (f, t: t) => t |> integral |> shapeFn(XYShape.XtoY.linear(f))
let integralYtoX = (f, t: t) => t |> integral |> shapeFn(XYShape.YtoX.linear(f)) let integralYtoX = (f, t: t) => t |> integral |> shapeFn(XYShape.YtoX.linear(f))
let toContinuous = t => Some(t) let toContinuous = t => Some(t)

View File

@ -158,7 +158,8 @@ module T = Dist({
Continuous.make(~interpolation=#Stepwise, integralShape) Continuous.make(~interpolation=#Stepwise, integralShape)
} }
let integralEndY = (t: t) => t.integralSumCache |> E.O.default(t |> integral |> Continuous.lastY) let integralEndY = (t: t) =>
t.integralSumCache |> E.O.defaultFn(() => t |> integral |> Continuous.lastY)
let minX = shapeFn(XYShape.T.minX) let minX = shapeFn(XYShape.T.minX)
let maxX = shapeFn(XYShape.T.maxX) let maxX = shapeFn(XYShape.T.maxX)
let toDiscreteProbabilityMassFraction = _ => 1.0 let toDiscreteProbabilityMassFraction = _ => 1.0

View File

@ -13,9 +13,11 @@ let buildSimple = (
~discrete: option<PointSetTypes.discreteShape>, ~discrete: option<PointSetTypes.discreteShape>,
): option<PointSetTypes.pointSetDist> => { ): option<PointSetTypes.pointSetDist> => {
let continuous = let continuous =
continuous |> E.O.default(Continuous.make(~integralSumCache=Some(0.0), {xs: [], ys: []})) continuous |> E.O.defaultFn(() =>
Continuous.make(~integralSumCache=Some(0.0), {xs: [], ys: []})
)
let discrete = let discrete =
discrete |> E.O.default(Discrete.make(~integralSumCache=Some(0.0), {xs: [], ys: []})) discrete |> E.O.defaultFn(() => Discrete.make(~integralSumCache=Some(0.0), {xs: [], ys: []}))
let cLength = continuous |> Continuous.getShape |> XYShape.T.xs |> E.A.length let cLength = continuous |> Continuous.getShape |> XYShape.T.xs |> E.A.length
let dLength = discrete |> Discrete.getShape |> XYShape.T.xs |> E.A.length let dLength = discrete |> Discrete.getShape |> XYShape.T.xs |> E.A.length
switch (cLength, dLength) { switch (cLength, dLength) {

View File

@ -117,6 +117,11 @@ let map3 = (
): result<t, sampleSetError> => ): result<t, sampleSetError> =>
E.A.zip3(get(t1), get(t2), get(t3))->E.A2.fmap(E.Tuple3.toFnCall(fn))->_fromSampleResultArray E.A.zip3(get(t1), get(t2), get(t3))->E.A2.fmap(E.Tuple3.toFnCall(fn))->_fromSampleResultArray
let mapN = (~fn: array<float> => result<float, Operation.Error.t>, ~t1: array<t>): result<
t,
sampleSetError,
> => E.A.transpose(E.A.fmap(get, t1))->E.A2.fmap(fn)->_fromSampleResultArray
let mean = t => T.get(t)->E.A.Floats.mean let mean = t => T.get(t)->E.A.Floats.mean
let geomean = t => T.get(t)->E.A.Floats.geomean let geomean = t => T.get(t)->E.A.Floats.geomean
let mode = t => T.get(t)->E.A.Floats.mode let mode = t => T.get(t)->E.A.Floats.mode

View File

@ -1,4 +1,5 @@
type internalExpressionValue = ReducerInterface_InternalExpressionValue.t type internalExpressionValue = ReducerInterface_InternalExpressionValue.t
type internalExpressionValueType = ReducerInterface_InternalExpressionValue.internalExpressionValueType
/* /*
Function Registry "Type". A type, without any other information. Function Registry "Type". A type, without any other information.
@ -42,18 +43,27 @@ and frValueDistOrNumber = FRValueNumber(float) | FRValueDist(DistributionTypes.g
type fnDefinition = { type fnDefinition = {
name: string, name: string,
inputs: array<frType>, inputs: array<frType>,
run: (array<frValue>, GenericDist.env) => result<internalExpressionValue, string>, run: (
array<internalExpressionValue>,
array<frValue>,
GenericDist.env,
Reducer_Expression_T.reducerFn,
) => result<internalExpressionValue, string>,
} }
type function = { type function = {
name: string, name: string,
definitions: array<fnDefinition>, definitions: array<fnDefinition>,
examples: option<string>, requiresNamespace: bool,
nameSpace: string,
output: option<internalExpressionValueType>,
examples: array<string>,
description: option<string>, description: option<string>,
isExperimental: bool, isExperimental: bool,
} }
type registry = array<function> type fnNameDict = Js.Dict.t<array<function>>
type registry = {functions: array<function>, fnNameDict: fnNameDict}
module FRType = { module FRType = {
type t = frType type t = frType
@ -175,6 +185,9 @@ module FRType = {
This module, Matcher, is fairly lengthy. However, only two functions from it 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. 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. The rest of it is just called from those two functions.
Update: This really should be completely re-done sometime, and tested. It works, but it's pretty messy. I'm sure
there are internal bugs, but the end functionality works, so I'm not too worried.
*/ */
module Matcher = { module Matcher = {
module MatchSimple = { module MatchSimple = {
@ -230,53 +243,82 @@ module Matcher = {
type definitionId = int type definitionId = int
type match = Match.t<array<definitionId>, definitionId> type match = Match.t<array<definitionId>, definitionId>
let match = (f: function, fnName: string, args: array<internalExpressionValue>): match => { let match = (
let matchedDefinition = () => f: function,
E.A.getIndexBy(f.definitions, r => nameSpace: option<string>,
MatchSimple.isFullMatch(FnDefinition.match(r, fnName, args)) fnName: string,
) |> E.O.fmap(r => Match.FullMatch(r)) args: array<internalExpressionValue>,
let getMatchedNameOnlyDefinition = () => { ): match => {
let nameMatchIndexes = switch nameSpace {
f.definitions | Some(ns) if ns !== f.nameSpace => Match.DifferentName
->E.A2.fmapi((index, r) => | _ => {
MatchSimple.isNameMatchOnly(FnDefinition.match(r, fnName, args)) ? Some(index) : None 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,
) )
->E.A.O.concatSomes
switch nameMatchIndexes {
| [] => None
| elements => Some(Match.SameNameDifferentArguments(elements))
} }
} }
E.A.O.firstSomeFnWithDefault(
[matchedDefinition, getMatchedNameOnlyDefinition],
Match.DifferentName,
)
} }
} }
module RegistryMatch = { module RegistryMatch = {
type match = { type match = {
nameSpace: string,
fnName: string, fnName: string,
inputIndex: int, inputIndex: int,
} }
let makeMatch = (fnName: string, inputIndex: int) => {fnName: fnName, inputIndex: inputIndex} let makeMatch = (nameSpace: string, fnName: string, inputIndex: int) => {
nameSpace: nameSpace,
fnName: fnName,
inputIndex: inputIndex,
}
} }
module Registry = { module Registry = {
let _findExactMatches = (r: registry, fnName: string, args: array<internalExpressionValue>) => { let _findExactMatches = (
let functionMatchPairs = r->E.A2.fmap(l => (l, Function.match(l, fnName, args))) r: registry,
nameSpace: option<string>,
fnName: string,
args: array<internalExpressionValue>,
) => {
let functionMatchPairs =
r.functions->E.A2.fmap(l => (l, Function.match(l, nameSpace, fnName, args)))
let fullMatch = functionMatchPairs->E.A.getBy(((_, match)) => Match.isFullMatch(match)) let fullMatch = functionMatchPairs->E.A.getBy(((_, match)) => Match.isFullMatch(match))
fullMatch->E.O.bind(((fn, match)) => fullMatch->E.O.bind(((fn, match)) =>
switch match { switch match {
| FullMatch(index) => Some(RegistryMatch.makeMatch(fn.name, index)) | FullMatch(index) => Some(RegistryMatch.makeMatch(fn.nameSpace, fn.name, index))
| _ => None | _ => None
} }
) )
} }
let _findNameMatches = (r: registry, fnName: string, args: array<internalExpressionValue>) => { let _findNameMatches = (
let functionMatchPairs = r->E.A2.fmap(l => (l, Function.match(l, fnName, args))) r: registry,
nameSpace: option<string>,
fnName: string,
args: array<internalExpressionValue>,
) => {
let functionMatchPairs =
r.functions->E.A2.fmap(l => (l, Function.match(l, nameSpace, fnName, args)))
let getNameMatches = let getNameMatches =
functionMatchPairs functionMatchPairs
->E.A2.fmap(((fn, match)) => Match.isNameMatchOnly(match) ? Some((fn, match)) : None) ->E.A2.fmap(((fn, match)) => Match.isNameMatchOnly(match) ? Some((fn, match)) : None)
@ -286,7 +328,7 @@ module Matcher = {
->E.A2.fmap(((fn, match)) => ->E.A2.fmap(((fn, match)) =>
switch match { switch match {
| SameNameDifferentArguments(indexes) => | SameNameDifferentArguments(indexes) =>
indexes->E.A2.fmap(index => RegistryMatch.makeMatch(fn.name, index)) indexes->E.A2.fmap(index => RegistryMatch.makeMatch(fn.nameSpace, fn.name, index))
| _ => [] | _ => []
} }
) )
@ -295,21 +337,28 @@ module Matcher = {
} }
let findMatches = (r: registry, fnName: string, args: array<internalExpressionValue>) => { let findMatches = (r: registry, fnName: string, args: array<internalExpressionValue>) => {
switch _findExactMatches(r, fnName, args) { let fnNameInParts = Js.String.split(".", fnName)
let fnToSearch = E.A.get(fnNameInParts, 1) |> E.O.default(fnNameInParts[0])
let nameSpace = E.A.length(fnNameInParts) > 1 ? Some(fnNameInParts[0]) : None
switch _findExactMatches(r, nameSpace, fnToSearch, args) {
| Some(r) => Match.FullMatch(r) | Some(r) => Match.FullMatch(r)
| None => | None =>
switch _findNameMatches(r, fnName, args) { switch _findNameMatches(r, nameSpace, fnToSearch, args) {
| Some(r) => Match.SameNameDifferentArguments(r) | Some(r) => Match.SameNameDifferentArguments(r)
| None => Match.DifferentName | None => Match.DifferentName
} }
} }
} }
let matchToDef = (registry: registry, {fnName, inputIndex}: RegistryMatch.match): option< let matchToDef = (
fnDefinition, registry: registry,
> => {nameSpace, fnName, inputIndex}: RegistryMatch.match,
registry ): option<fnDefinition> =>
->E.A.getBy(fn => fn.name === fnName) registry.functions
->E.A.getBy(fn => {
nameSpace === fn.nameSpace && fnName === fn.name
})
->E.O.bind(fn => E.A.get(fn.definitions, inputIndex)) ->E.O.bind(fn => E.A.get(fn.definitions, inputIndex))
} }
} }
@ -322,15 +371,28 @@ module FnDefinition = {
t.name ++ `(${inputs})` t.name ++ `(${inputs})`
} }
let run = (t: t, args: array<internalExpressionValue>, env: GenericDist.env) => { let isMatch = (t: t, args: array<internalExpressionValue>) => {
let argValues = FRType.matchWithExpressionValueArray(t.inputs, args) let argValues = FRType.matchWithExpressionValueArray(t.inputs, args)
switch argValues { switch argValues {
| Some(values) => t.run(values, env) | Some(_) => true
| None => false
}
}
let run = (
t: t,
args: array<internalExpressionValue>,
env: GenericDist.env,
reducer: Reducer_Expression_T.reducerFn,
) => {
let argValues = FRType.matchWithExpressionValueArray(t.inputs, args)
switch argValues {
| Some(values) => t.run(args, values, env, reducer)
| None => Error("Incorrect Types") | None => Error("Incorrect Types")
} }
} }
let make = (~name, ~inputs, ~run): t => { let make = (~name, ~inputs, ~run, ()): t => {
name: name, name: name,
inputs: inputs, inputs: inputs,
run: run, run: run,
@ -343,16 +405,29 @@ module Function = {
type functionJson = { type functionJson = {
name: string, name: string,
definitions: array<string>, definitions: array<string>,
examples: option<string>, examples: array<string>,
description: option<string>, description: option<string>,
isExperimental: bool, isExperimental: bool,
} }
let make = (~name, ~definitions, ~examples=?, ~description=?, ~isExperimental=false, ()): t => { let make = (
~name,
~nameSpace,
~requiresNamespace,
~definitions,
~examples=?,
~output=?,
~description=?,
~isExperimental=false,
(),
): t => {
name: name, name: name,
nameSpace: nameSpace,
definitions: definitions, definitions: definitions,
examples: examples, output: output,
examples: examples |> E.O.default([]),
isExperimental: isExperimental, isExperimental: isExperimental,
requiresNamespace: requiresNamespace,
description: description, description: description,
} }
@ -365,22 +440,65 @@ module Function = {
} }
} }
module NameSpace = {
type t = {name: string, functions: array<function>}
let definitions = (t: t) => t.functions->E.A2.fmap(f => f.definitions)->E.A.concatMany
let uniqueFnNames = (t: t) => definitions(t)->E.A2.fmap(r => r.name)->E.A.uniq
let nameToDefinitions = (t: t, name: string) => definitions(t)->E.A2.filter(r => r.name == name)
}
module Registry = { module Registry = {
let toJson = (r: registry) => r->E.A2.fmap(Function.toJson) let toJson = (r: registry) => r.functions->E.A2.fmap(Function.toJson)
let allExamples = (r: registry) => r.functions->E.A2.fmap(r => r.examples)->E.A.concatMany
let allExamplesWithFns = (r: registry) =>
r.functions->E.A2.fmap(fn => fn.examples->E.A2.fmap(example => (fn, example)))->E.A.concatMany
let _buildFnNameDict = (r: array<function>): fnNameDict => {
let allDefinitionsWithFns =
r
->E.A2.fmap(fn => fn.definitions->E.A2.fmap(definitions => (fn, definitions)))
->E.A.concatMany
let functionsWithFnNames =
allDefinitionsWithFns
->E.A2.fmap(((fn, def)) => {
let nameWithNamespace = `${fn.nameSpace}.${def.name}`
let nameWithoutNamespace = def.name
fn.requiresNamespace
? [(nameWithNamespace, fn)]
: [(nameWithNamespace, fn), (nameWithoutNamespace, fn)]
})
->E.A.concatMany
let uniqueNames = functionsWithFnNames->E.A2.fmap(((name, _)) => name)->E.A.uniq
let cacheAsArray: array<(string, array<function>)> = uniqueNames->E.A2.fmap(uniqueName => {
let relevantItems =
E.A2.filter(functionsWithFnNames, ((defName, _)) => defName == uniqueName)->E.A2.fmap(
E.Tuple2.second,
)
(uniqueName, relevantItems)
})
cacheAsArray->Js.Dict.fromArray
}
let make = (fns: array<function>): registry => {
let dict = _buildFnNameDict(fns)
{functions: fns, fnNameDict: dict}
}
/* /*
There's a (potential+minor) bug here: If a function definition is called outside of the calls There's a (potential+minor) bug here: If a function definition is called outside of the calls
to the registry, then it's possible that there could be a match after the registry is to the registry, then it's possible that there could be a match after the registry is
called. However, for now, we could just call the registry last. called. However, for now, we could just call the registry last.
*/ */
let matchAndRun = ( let _matchAndRun = (
~registry: registry, ~registry: registry,
~fnName: string, ~fnName: string,
~args: array<internalExpressionValue>, ~args: array<internalExpressionValue>,
~env: GenericDist.env, ~env: GenericDist.env,
~reducer: Reducer_Expression_T.reducerFn,
) => { ) => {
let relevantFunctions = Js.Dict.get(registry.fnNameDict, fnName) |> E.O.default([])
let modified = {functions: relevantFunctions, fnNameDict: registry.fnNameDict}
let matchToDef = m => Matcher.Registry.matchToDef(registry, m) let matchToDef = m => Matcher.Registry.matchToDef(registry, m)
//Js.log(toSimple(registry))
let showNameMatchDefinitions = matches => { let showNameMatchDefinitions = matches => {
let defs = let defs =
matches matches
@ -391,10 +509,23 @@ module Registry = {
->E.A2.joinWith("; ") ->E.A2.joinWith("; ")
`There are function matches for ${fnName}(), but with different arguments: ${defs}` `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)) switch Matcher.Registry.findMatches(modified, fnName, args) {
| Matcher.Match.FullMatch(match) =>
match->matchToDef->E.O2.fmap(FnDefinition.run(_, args, env, reducer))
| SameNameDifferentArguments(m) => Some(Error(showNameMatchDefinitions(m))) | SameNameDifferentArguments(m) => Some(Error(showNameMatchDefinitions(m)))
| _ => None | _ => None
} }
} }
let dispatch = (
registry,
(fnName, args): ReducerInterface_InternalExpressionValue.functionCall,
env,
reducer: Reducer_Expression_T.reducerFn,
) => {
_matchAndRun(~registry, ~fnName, ~args, ~env, ~reducer)->E.O2.fmap(
E.R2.errMap(_, s => Reducer_ErrorValue.RETodo(s)),
)
}
} }

View File

@ -4,6 +4,8 @@ let impossibleError = "Wrong inputs / Logically impossible"
module Wrappers = { module Wrappers = {
let symbolic = r => DistributionTypes.Symbolic(r) let symbolic = r => DistributionTypes.Symbolic(r)
let pointSet = r => DistributionTypes.PointSet(r)
let sampleSet = r => DistributionTypes.SampleSet(r)
let evDistribution = r => ReducerInterface_InternalExpressionValue.IEvDistribution(r) let evDistribution = r => ReducerInterface_InternalExpressionValue.IEvDistribution(r)
let evNumber = r => ReducerInterface_InternalExpressionValue.IEvNumber(r) let evNumber = r => ReducerInterface_InternalExpressionValue.IEvNumber(r)
let evArray = r => ReducerInterface_InternalExpressionValue.IEvArray(r) let evArray = r => ReducerInterface_InternalExpressionValue.IEvArray(r)
@ -214,71 +216,27 @@ module Process = {
} }
} }
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 = { module ArrayNumberDist = {
let make = (name, fn) => { let make = (name, fn) => {
FnDefinition.make(~name, ~inputs=[FRTypeArray(FRTypeNumber)], ~run=(inputs, _) => FnDefinition.make(
Prepare.ToTypedArray.numbers(inputs) ~name,
->E.R.bind(r => E.A.length(r) === 0 ? Error("List is empty") : Ok(r)) ~inputs=[FRTypeArray(FRTypeNumber)],
->E.R.bind(fn) ~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) => { let make2 = (name, fn) => {
FnDefinition.make(~name, ~inputs=[FRTypeArray(FRTypeAny)], ~run=(inputs, _) => FnDefinition.make(
Prepare.ToTypedArray.numbers(inputs) ~name,
->E.R.bind(r => E.A.length(r) === 0 ? Error("List is empty") : Ok(r)) ~inputs=[FRTypeArray(FRTypeAny)],
->E.R.bind(fn) ~run=(_, inputs, _, _) =>
Prepare.ToTypedArray.numbers(inputs)
->E.R.bind(r => E.A.length(r) === 0 ? Error("List is empty") : Ok(r))
->E.R.bind(fn),
(),
) )
} }
} }
module NumberToNumber = {
let make = (name, fn) =>
FnDefinition.make(~name, ~inputs=[FRTypeNumber], ~run=(inputs, _) => {
inputs
->getOrError(0)
->E.R.bind(Prepare.oneNumber)
->E.R2.fmap(fn)
->E.R2.fmap(Wrappers.evNumber)
})
}

View File

@ -1,586 +1,13 @@
open FunctionRegistry_Core let fnList = Belt.Array.concatMany([
open FunctionRegistry_Helpers FR_Dict.library,
FR_Dist.library,
FR_Fn.library,
FR_Sampleset.library,
FR_List.library,
FR_Number.library,
FR_Pointset.library,
FR_Scoring.library,
])
let twoArgs = E.Tuple2.toFnCall let registry = FunctionRegistry_Core.Registry.make(fnList)
let dispatch = FunctionRegistry_Core.Registry.dispatch(registry)
module Declaration = {
let frType = FRTypeRecord([
("fn", FRTypeLambda),
("inputs", FRTypeArray(FRTypeRecord([("min", FRTypeNumber), ("max", FRTypeNumber)]))),
])
let fromExpressionValue = (e: frValue): result<internalExpressionValue, 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_InternalExpressionValue.IEvDeclaration(
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_InternalExpressionValue.IEvDistribution(
PointSet(makeDist(r)),
))
expressionValue
}
let registryStart = [
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",
~examples=`normal(5,1)
normal({p5: 4, p95: 10})
normal({mean: 5, stdev: 2})`,
~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",
~examples=`lognormal(0.5, 0.8)
lognormal({p5: 4, p95: 10})
lognormal({mean: 5, stdev: 2})`,
~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",
~examples=`uniform(10, 12)`,
~definitions=[TwoArgDist.make("uniform", twoArgs(SymbolicDist.Uniform.make))],
(),
),
Function.make(
~name="Beta",
~examples=`beta(20, 25)
beta({mean: 0.39, stdev: 0.1})`,
~definitions=[
TwoArgDist.make("beta", twoArgs(SymbolicDist.Beta.make)),
TwoArgDist.makeRecordMeanStdev("beta", twoArgs(SymbolicDist.Beta.fromMeanAndStdev)),
],
(),
),
Function.make(
~name="Cauchy",
~examples=`cauchy(5, 1)`,
~definitions=[TwoArgDist.make("cauchy", twoArgs(SymbolicDist.Cauchy.make))],
(),
),
Function.make(
~name="Gamma",
~examples=`gamma(5, 1)`,
~definitions=[TwoArgDist.make("gamma", twoArgs(SymbolicDist.Gamma.make))],
(),
),
Function.make(
~name="Logistic",
~examples=`logistic(5, 1)`,
~definitions=[TwoArgDist.make("logistic", twoArgs(SymbolicDist.Logistic.make))],
(),
),
Function.make(
~name="Metalog",
~examples=`metalog([1, 2, 3])`,
~definitions=[
ArrayNumberDist.make("metalog", x =>
SymbolicDist.Metalog.make(x)->E.R2.fmap(x => Wrappers.symbolic(x)->Wrappers.evDistribution)
),
],
(),
),
Function.make(
~name="To (Distribution)",
~examples=`5 to 10
to(5,10)
-5 to 5`,
~definitions=[
TwoArgDist.make("to", twoArgs(SymbolicDist.From90thPercentile.make)),
TwoArgDist.make(
"credibleIntervalToDistribution",
twoArgs(SymbolicDist.From90thPercentile.make),
),
],
(),
),
Function.make(
~name="Exponential",
~examples=`exponential(2)`,
~definitions=[OneArgDist.make("exponential", SymbolicDist.Exponential.make)],
(),
),
Function.make(
~name="Bernoulli",
~examples=`bernoulli(0.5)`,
~definitions=[OneArgDist.make("bernoulli", SymbolicDist.Bernoulli.make)],
(),
),
Function.make(
~name="PointMass",
~examples=`pointMass(0.5)`,
~definitions=[OneArgDist.make("pointMass", SymbolicDist.Float.makeSafe)],
(),
),
Function.make(
~name="toContinuousPointSet",
~description="Converts a set of points to a continuous distribution",
~examples=`toContinuousPointSet([
{x: 0, y: 0.1},
{x: 1, y: 0.2},
{x: 2, y: 0.15},
{x: 3, y: 0.1}
])`,
~definitions=[
FnDefinition.make(
~name="toContinuousPointSet",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(inputs, _) => inputsTodist(inputs, r => Continuous(Continuous.make(r))),
),
],
(),
),
Function.make(
~name="toDiscretePointSet",
~description="Converts a set of points to a discrete distribution",
~examples=`toDiscretePointSet([
{x: 0, y: 0.1},
{x: 1, y: 0.2},
{x: 2, y: 0.15},
{x: 3, y: 0.1}
])`,
~definitions=[
FnDefinition.make(
~name="toDiscretePointSet",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(inputs, _) => inputsTodist(inputs, r => Discrete(Discrete.make(r))),
),
],
(),
),
Function.make(
~name="Declaration (Continuous Function)",
~description="Adds metadata to a function of the input ranges. Works now for numeric and date inputs. This is useful when making predictions. It allows you to limit the domain that your prediction will be used and scored within.",
~examples=`declareFn({
fn: {|a,b| a },
inputs: [
{min: 0, max: 100},
{min: 30, max: 50}
]
})`,
~definitions=[
FnDefinition.make(~name="declareFn", ~inputs=[Declaration.frType], ~run=(inputs, _) => {
inputs->E.A.unsafe_get(0)->Declaration.fromExpressionValue
}),
],
~isExperimental=true,
(),
),
Function.make(
~name="Floor",
~definitions=[NumberToNumber.make("floor", Js.Math.floor_float)],
(),
),
Function.make(
~name="Ceiling",
~definitions=[NumberToNumber.make("ceil", Js.Math.ceil_float)],
(),
),
Function.make(
~name="Absolute Value",
~definitions=[NumberToNumber.make("abs", Js.Math.abs_float)],
(),
),
Function.make(~name="Exponent", ~definitions=[NumberToNumber.make("exp", Js.Math.exp)], ()),
Function.make(~name="Log", ~definitions=[NumberToNumber.make("log", Js.Math.log)], ()),
Function.make(
~name="Log Base 10",
~definitions=[NumberToNumber.make("log10", Js.Math.log10)],
(),
),
Function.make(~name="Log Base 2", ~definitions=[NumberToNumber.make("log2", Js.Math.log2)], ()),
Function.make(~name="Round", ~definitions=[NumberToNumber.make("round", Js.Math.round)], ()),
Function.make(
~name="Sum",
~definitions=[ArrayNumberDist.make("sum", r => r->E.A.Floats.sum->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Product",
~definitions=[
ArrayNumberDist.make("product", r => r->E.A.Floats.product->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="Min",
~definitions=[ArrayNumberDist.make("min", r => r->E.A.Floats.min->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Max",
~definitions=[ArrayNumberDist.make("max", r => r->E.A.Floats.max->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Mean",
~definitions=[ArrayNumberDist.make("mean", r => r->E.A.Floats.mean->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Geometric Mean",
~definitions=[
ArrayNumberDist.make("geomean", r => r->E.A.Floats.geomean->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="Standard Deviation",
~definitions=[ArrayNumberDist.make("stdev", r => r->E.A.Floats.stdev->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="Variance",
~definitions=[
ArrayNumberDist.make("variance", r => r->E.A.Floats.stdev->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="First",
~definitions=[
ArrayNumberDist.make2("first", r =>
r->E.A.first |> E.O.toResult(impossibleError) |> E.R.fmap(Wrappers.evNumber)
),
],
(),
),
Function.make(
~name="Last",
~definitions=[
ArrayNumberDist.make2("last", r =>
r->E.A.last |> E.O.toResult(impossibleError) |> E.R.fmap(Wrappers.evNumber)
),
],
(),
),
Function.make(
~name="Sort",
~definitions=[
ArrayNumberDist.make("sort", r =>
r->E.A.Floats.sort->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Reverse",
~definitions=[
ArrayNumberDist.make("reverse", r =>
r->Belt_Array.reverse->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Cumulative Sum",
~definitions=[
ArrayNumberDist.make("cumsum", r =>
r->E.A.Floats.cumsum->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Cumulative Prod",
~definitions=[
ArrayNumberDist.make("cumprod", r =>
r->E.A.Floats.cumsum->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Diff",
~definitions=[
ArrayNumberDist.make("diff", r =>
r->E.A.Floats.diff->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="Dict.merge",
~definitions=[
FnDefinition.make(
~name="merge",
~inputs=[FRTypeDict(FRTypeAny), FRTypeDict(FRTypeAny)],
~run=(inputs, _) => {
switch inputs {
| [FRValueDict(d1), FRValueDict(d2)] => {
let newDict =
E.Dict.concat(d1, d2) |> Js.Dict.map((. r) =>
FunctionRegistry_Core.FRType.matchReverse(r)
)
newDict->Js.Dict.entries->Belt.Map.String.fromArray->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(r => r->Js.Dict.entries->Belt.Map.String.fromArray)
->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(Belt.Map.String.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
)
),
],
(),
),
]
let runScoring = (estimate, answer, prior, env) => {
GenericDist.Score.logScore(~estimate, ~answer, ~prior, ~env)
->E.R2.fmap(FunctionRegistry_Helpers.Wrappers.evNumber)
->E.R2.errMap(DistributionTypes.Error.toString)
}
let scoreFunctions = [
Function.make(
~name="Score",
~definitions=[
FnDefinition.make(
~name="logScore",
~inputs=[
FRTypeRecord([
("estimate", FRTypeDist),
("answer", FRTypeDistOrNumber),
("prior", FRTypeDist),
]),
],
~run=(inputs, env) => {
switch FunctionRegistry_Helpers.Prepare.ToValueArray.Record.threeArgs(inputs) {
| Ok([FRValueDist(estimate), FRValueDistOrNumber(FRValueDist(d)), FRValueDist(prior)]) =>
runScoring(estimate, Score_Dist(d), Some(prior), env)
| Ok([
FRValueDist(estimate),
FRValueDistOrNumber(FRValueNumber(d)),
FRValueDist(prior),
]) =>
runScoring(estimate, Score_Scalar(d), Some(prior), env)
| Error(e) => Error(e)
| _ => Error(FunctionRegistry_Helpers.impossibleError)
}
},
),
FnDefinition.make(
~name="logScore",
~inputs=[FRTypeRecord([("estimate", FRTypeDist), ("answer", FRTypeDistOrNumber)])],
~run=(inputs, env) => {
switch FunctionRegistry_Helpers.Prepare.ToValueArray.Record.twoArgs(inputs) {
| Ok([FRValueDist(estimate), FRValueDistOrNumber(FRValueDist(d))]) =>
runScoring(estimate, Score_Dist(d), None, env)
| Ok([FRValueDist(estimate), FRValueDistOrNumber(FRValueNumber(d))]) =>
runScoring(estimate, Score_Scalar(d), None, env)
| Error(e) => Error(e)
| _ => Error(FunctionRegistry_Helpers.impossibleError)
}
},
),
FnDefinition.make(~name="klDivergence", ~inputs=[FRTypeDist, FRTypeDist], ~run=(
inputs,
env,
) => {
switch inputs {
| [FRValueDist(estimate), FRValueDist(d)] => runScoring(estimate, Score_Dist(d), None, env)
| _ => Error(FunctionRegistry_Helpers.impossibleError)
}
}),
],
(),
),
]
let registry = E.A.append(registryStart, scoreFunctions)

View File

@ -0,0 +1,170 @@
open FunctionRegistry_Core
open FunctionRegistry_Helpers
let nameSpace = "Dict"
module Internals = {
type t = ReducerInterface_InternalExpressionValue.map
let keys = (a: t): internalExpressionValue => IEvArray(
Belt.Map.String.keysToArray(a)->E.A2.fmap(Wrappers.evString),
)
let values = (a: t): internalExpressionValue => IEvArray(Belt.Map.String.valuesToArray(a))
let toList = (a: t): internalExpressionValue =>
Belt.Map.String.toArray(a)
->E.A2.fmap(((key, value)) => Wrappers.evArray([IEvString(key), value]))
->Wrappers.evArray
let fromList = (items: array<internalExpressionValue>): result<internalExpressionValue, string> =>
items
->E.A2.fmap(item => {
switch (item: internalExpressionValue) {
| IEvArray([IEvString(string), value]) => (string, value)->Ok
| _ => Error(impossibleError)
}
})
->E.A.R.firstErrorOrOpen
->E.R2.fmap(Belt.Map.String.fromArray)
->E.R2.fmap(Wrappers.evRecord)
let merge = (a: t, b: t): internalExpressionValue => IEvRecord(
Belt.Map.String.merge(a, b, (_, _, c) => c),
)
//Belt.Map.String has a function for mergeMany, but I couldn't understand how to use it yet.
let mergeMany = (a: array<t>): internalExpressionValue => {
let mergedValues =
a->E.A2.fmap(Belt.Map.String.toArray)->Belt.Array.concatMany->Belt.Map.String.fromArray
IEvRecord(mergedValues)
}
}
let library = [
Function.make(
~name="merge",
~nameSpace,
~requiresNamespace=true,
~output=EvtRecord,
~examples=[`Dict.merge({a: 1, b: 2}, {c: 3, d: 4})`],
~definitions=[
FnDefinition.make(
~name="merge",
~inputs=[FRTypeDict(FRTypeAny), FRTypeDict(FRTypeAny)],
~run=(inputs, _, _, _) => {
switch inputs {
| [IEvRecord(d1), IEvRecord(d2)] => Internals.merge(d1, d2)->Ok
| _ => Error(impossibleError)
}
},
(),
),
],
(),
),
//TODO: Change to use new mergeMany() function.
Function.make(
~name="mergeMany",
~nameSpace,
~requiresNamespace=true,
~output=EvtRecord,
~examples=[`Dict.mergeMany([{a: 1, b: 2}, {c: 3, d: 4}])`],
~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(r => r->Js.Dict.entries->Belt.Map.String.fromArray)
->E.R2.fmap(Wrappers.evRecord),
(),
),
],
(),
),
Function.make(
~name="keys",
~nameSpace,
~requiresNamespace=true,
~output=EvtArray,
~examples=[`Dict.keys({a: 1, b: 2})`],
~definitions=[
FnDefinition.make(
~name="keys",
~inputs=[FRTypeDict(FRTypeAny)],
~run=(inputs, _, _, _) =>
switch inputs {
| [IEvRecord(d1)] => Internals.keys(d1)->Ok
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="values",
~nameSpace,
~requiresNamespace=true,
~output=EvtArray,
~examples=[`Dict.values({a: 1, b: 2})`],
~definitions=[
FnDefinition.make(
~name="values",
~inputs=[FRTypeDict(FRTypeAny)],
~run=(inputs, _, _, _) =>
switch inputs {
| [IEvRecord(d1)] => Internals.values(d1)->Ok
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="toList",
~nameSpace,
~requiresNamespace=true,
~output=EvtArray,
~examples=[`Dict.toList({a: 1, b: 2})`],
~definitions=[
FnDefinition.make(
~name="toList",
~inputs=[FRTypeDict(FRTypeAny)],
~run=(inputs, _, _, _) =>
switch inputs {
| [IEvRecord(dict)] => dict->Internals.toList->Ok
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="fromList",
~nameSpace,
~requiresNamespace=true,
~output=EvtRecord,
~examples=[`Dict.fromList([["a", 1], ["b", 2]])`],
~definitions=[
FnDefinition.make(
~name="fromList",
~inputs=[FRTypeArray(FRTypeArray(FRTypeAny))],
~run=(inputs, _, _, _) => {
switch inputs {
| [IEvArray(items)] => Internals.fromList(items)
| _ => Error(impossibleError)
}
},
(),
),
],
(),
),
]

View File

@ -0,0 +1,165 @@
open FunctionRegistry_Core
open FunctionRegistry_Helpers
let twoArgs = E.Tuple2.toFnCall
module DistributionCreation = {
let nameSpace = "Dist"
let output = ReducerInterface_InternalExpressionValue.EvtDistribution
let requiresNamespace = false
let fnMake = (~name, ~examples, ~definitions) => {
Function.make(~name, ~nameSpace, ~output, ~examples, ~definitions, ~requiresNamespace, ())
}
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),
(),
)
}
let library = [
fnMake(
~name="normal",
~examples=["normal(5,1)", "normal({p5: 4, p95: 10})", "normal({mean: 5, stdev: 2})"],
~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)),
],
),
fnMake(
~name="lognormal",
~examples=[
"lognormal(0.5, 0.8)",
"lognormal({p5: 4, p95: 10})",
"lognormal({mean: 5, stdev: 2})",
],
~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),
),
],
),
fnMake(
~name="uniform",
~examples=[`uniform(10, 12)`],
~definitions=[TwoArgDist.make("uniform", twoArgs(SymbolicDist.Uniform.make))],
),
fnMake(
~name="beta",
~examples=[`beta(20, 25)`, `beta({mean: 0.39, stdev: 0.1})`],
~definitions=[
TwoArgDist.make("beta", twoArgs(SymbolicDist.Beta.make)),
TwoArgDist.makeRecordMeanStdev("beta", twoArgs(SymbolicDist.Beta.fromMeanAndStdev)),
],
),
fnMake(
~name="cauchy",
~examples=[`cauchy(5, 1)`],
~definitions=[TwoArgDist.make("cauchy", twoArgs(SymbolicDist.Cauchy.make))],
),
fnMake(
~name="gamma",
~examples=[`gamma(5, 1)`],
~definitions=[TwoArgDist.make("gamma", twoArgs(SymbolicDist.Gamma.make))],
),
fnMake(
~name="logistic",
~examples=[`logistic(5, 1)`],
~definitions=[TwoArgDist.make("logistic", twoArgs(SymbolicDist.Logistic.make))],
),
fnMake(
~name="metalog",
~examples=[`metalog([1, 2, 3])`],
~definitions=[
ArrayNumberDist.make("metalog", x =>
SymbolicDist.Metalog.make(x)->E.R2.fmap(x =>
Wrappers.symbolic(x)->Wrappers.evDistribution
)
),
],
),
fnMake(
~name="to (distribution)",
~examples=[`5 to 10`, `to(5,10)`, `-5 to 5`],
~definitions=[
TwoArgDist.make("to", twoArgs(SymbolicDist.From90thPercentile.make)),
TwoArgDist.make(
"credibleIntervalToDistribution",
twoArgs(SymbolicDist.From90thPercentile.make),
),
],
),
fnMake(
~name="exponential",
~examples=[`exponential(2)`],
~definitions=[OneArgDist.make("exponential", SymbolicDist.Exponential.make)],
),
fnMake(
~name="bernoulli",
~examples=[`bernoulli(0.5)`],
~definitions=[OneArgDist.make("bernoulli", SymbolicDist.Bernoulli.make)],
),
fnMake(
~name="pointMass",
~examples=[`pointMass(0.5)`],
~definitions=[OneArgDist.make("pointMass", SymbolicDist.Float.makeSafe)],
),
]
}
let library = DistributionCreation.library

View File

@ -0,0 +1,62 @@
open FunctionRegistry_Core
open FunctionRegistry_Helpers
module Declaration = {
let frType = FRTypeRecord([
("fn", FRTypeLambda),
("inputs", FRTypeArray(FRTypeRecord([("min", FRTypeNumber), ("max", FRTypeNumber)]))),
])
let fromExpressionValue = (e: frValue): result<internalExpressionValue, 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_InternalExpressionValue.IEvDeclaration(
Declaration.make(lambda, args),
))
}
| Error(r) => Error(r)
| Ok(_) => Error(FunctionRegistry_Helpers.impossibleError)
}
}
}
let nameSpace = "Function"
let library = [
Function.make(
~name="declare",
~nameSpace,
~requiresNamespace=true,
~output=EvtDeclaration,
~description="Adds metadata to a function of the input ranges. Works now for numeric and date inputs. This is useful when making predictions. It allows you to limit the domain that your prediction will be used and scored within.",
~examples=[
`Function.declare({
fn: {|a,b| a },
inputs: [
{min: 0, max: 100},
{min: 30, max: 50}
]
})`,
],
~isExperimental=true,
~definitions=[
FnDefinition.make(
~name="declare",
~inputs=[Declaration.frType],
~run=(_, inputs, _, _) => {
inputs->getOrError(0)->E.R.bind(Declaration.fromExpressionValue)
},
(),
),
],
(),
),
]

View File

@ -0,0 +1,265 @@
open FunctionRegistry_Core
open FunctionRegistry_Helpers
let nameSpace = "List"
let requiresNamespace = true
module Internals = {
let makeFromNumber = (
n: float,
value: internalExpressionValue,
): internalExpressionValue => IEvArray(Belt.Array.make(E.Float.toInt(n), value))
let upTo = (low: float, high: float): internalExpressionValue => IEvArray(
E.A.Floats.range(low, high, (high -. low +. 1.0)->E.Float.toInt)->E.A2.fmap(Wrappers.evNumber),
)
let first = (v: array<internalExpressionValue>): result<internalExpressionValue, string> =>
v->E.A.first |> E.O.toResult("No first element")
let last = (v: array<internalExpressionValue>): result<internalExpressionValue, string> =>
v->E.A.last |> E.O.toResult("No last element")
let reverse = (array: array<internalExpressionValue>): internalExpressionValue => IEvArray(
Belt.Array.reverse(array),
)
let map = (array: array<internalExpressionValue>, environment, eLambdaValue, reducer): result<
ReducerInterface_InternalExpressionValue.t,
Reducer_ErrorValue.errorValue,
> => {
let rMappedList = array->E.A.reduceReverse(Ok(list{}), (rAcc, elem) =>
rAcc->E.R.bind(_, acc => {
let rNewElem = Reducer_Expression_Lambda.doLambdaCall(
eLambdaValue,
list{elem},
environment,
reducer,
)
rNewElem->E.R2.fmap(newElem => list{newElem, ...acc})
})
)
rMappedList->E.R2.fmap(mappedList => mappedList->Belt.List.toArray->Wrappers.evArray)
}
let reduce = (aValueArray, initialValue, aLambdaValue, environment, reducer) => {
aValueArray->E.A.reduce(Ok(initialValue), (rAcc, elem) =>
rAcc->E.R.bind(_, acc =>
Reducer_Expression_Lambda.doLambdaCall(aLambdaValue, list{acc, elem}, environment, reducer)
)
)
}
let reduceReverse = (aValueArray, initialValue, aLambdaValue, environment, reducer) => {
aValueArray->Belt.Array.reduceReverse(Ok(initialValue), (rAcc, elem) =>
rAcc->Belt.Result.flatMap(acc =>
Reducer_Expression_Lambda.doLambdaCall(aLambdaValue, list{acc, elem}, environment, reducer)
)
)
}
let filter = (aValueArray, aLambdaValue, environment, reducer) => {
let rMappedList = aValueArray->Belt.Array.reduceReverse(Ok(list{}), (rAcc, elem) =>
rAcc->E.R.bind(_, acc => {
let rNewElem = Reducer_Expression_Lambda.doLambdaCall(
aLambdaValue,
list{elem},
environment,
reducer,
)
rNewElem->E.R2.fmap(newElem => {
switch newElem {
| IEvBool(true) => list{elem, ...acc}
| _ => acc
}
})
})
)
rMappedList->E.R2.fmap(mappedList => mappedList->Belt.List.toArray->Wrappers.evArray)
}
}
let library = [
Function.make(
~name="make",
~nameSpace,
~requiresNamespace=true,
~output=EvtArray,
~examples=[`List.make(2, "testValue")`],
~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="make",
~inputs=[FRTypeNumber, FRTypeAny],
~run=(inputs, _, _, _) => {
switch inputs {
| [IEvNumber(number), value] => Internals.makeFromNumber(number, value)->Ok
| _ => Error(impossibleError)
}
},
(),
),
],
(),
),
Function.make(
~name="upTo",
~nameSpace,
~requiresNamespace=true,
~output=EvtArray,
~examples=[`List.upTo(1,4)`],
~definitions=[
FnDefinition.make(
~name="upTo",
~inputs=[FRTypeNumber, FRTypeNumber],
~run=(_, inputs, _, _) =>
inputs
->Prepare.ToValueTuple.twoNumbers
->E.R2.fmap(((low, high)) => Internals.upTo(low, high)),
(),
),
],
(),
),
Function.make(
~name="first",
~nameSpace,
~requiresNamespace=true,
~examples=[`List.first([1,4,5])`],
~definitions=[
FnDefinition.make(
~name="first",
~inputs=[FRTypeArray(FRTypeAny)],
~run=(inputs, _, _, _) =>
switch inputs {
| [IEvArray(array)] => Internals.first(array)
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="last",
~nameSpace,
~requiresNamespace=true,
~examples=[`List.last([1,4,5])`],
~definitions=[
FnDefinition.make(
~name="last",
~inputs=[FRTypeArray(FRTypeAny)],
~run=(inputs, _, _, _) =>
switch inputs {
| [IEvArray(array)] => Internals.last(array)
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="reverse",
~nameSpace,
~output=EvtArray,
~requiresNamespace=false,
~examples=[`List.reverse([1,4,5])`],
~definitions=[
FnDefinition.make(
~name="reverse",
~inputs=[FRTypeArray(FRTypeAny)],
~run=(inputs, _, _, _) =>
switch inputs {
| [IEvArray(array)] => Internals.reverse(array)->Ok
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="map",
~nameSpace,
~output=EvtArray,
~requiresNamespace=false,
~examples=[`List.map([1,4,5], {|x| x+1})`],
~definitions=[
FnDefinition.make(
~name="map",
~inputs=[FRTypeArray(FRTypeAny), FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [IEvArray(array), IEvLambda(lambda)] =>
Internals.map(array, env, lambda, reducer)->E.R2.errMap(_ => "Error!")
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="reduce",
~nameSpace,
~requiresNamespace=false,
~examples=[`List.reduce([1,4,5], 2, {|acc, el| acc+el})`],
~definitions=[
FnDefinition.make(
~name="reduce",
~inputs=[FRTypeArray(FRTypeAny), FRTypeAny, FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [IEvArray(array), initialValue, IEvLambda(lambda)] =>
Internals.reduce(array, initialValue, lambda, env, reducer)->E.R2.errMap(_ => "Error!")
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="reduceReverse",
~nameSpace,
~requiresNamespace=false,
~examples=[`List.reduceReverse([1,4,5], 2, {|acc, el| acc-el})`],
~definitions=[
FnDefinition.make(
~name="reduceReverse",
~inputs=[FRTypeArray(FRTypeAny), FRTypeAny, FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [IEvArray(array), initialValue, IEvLambda(lambda)] =>
Internals.reduceReverse(array, initialValue, lambda, env, reducer)->E.R2.errMap(_ =>
"Error!"
)
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="filter",
~nameSpace,
~requiresNamespace=false,
~examples=[`List.filter([1,4,5], {|x| x>3})`],
~definitions=[
FnDefinition.make(
~name="filter",
~inputs=[FRTypeArray(FRTypeAny), FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [IEvArray(array), IEvLambda(lambda)] =>
Internals.filter(array, lambda, env, reducer)->E.R2.errMap(_ => "Error!")
| _ => Error(impossibleError)
},
(),
),
],
(),
),
]

View File

@ -0,0 +1,226 @@
open FunctionRegistry_Core
open FunctionRegistry_Helpers
let nameSpace = "Number"
let requiresNamespace = false
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)
},
(),
)
}
let library = [
Function.make(
~name="floor",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`floor(3.5)`],
~definitions=[NumberToNumber.make("floor", Js.Math.floor_float)],
(),
),
Function.make(
~name="ceiling",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`ceil(3.5)`],
~definitions=[NumberToNumber.make("ceil", Js.Math.ceil_float)],
(),
),
Function.make(
~name="absolute value",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`abs(3.5)`],
~definitions=[NumberToNumber.make("abs", Js.Math.abs_float)],
(),
),
Function.make(
~name="exponent",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`exp(3.5)`],
~definitions=[NumberToNumber.make("exp", Js.Math.exp)],
(),
),
Function.make(
~name="log",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`log(3.5)`],
~definitions=[NumberToNumber.make("log", Js.Math.log)],
(),
),
Function.make(
~name="log base 10",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`log10(3.5)`],
~definitions=[NumberToNumber.make("log10", Js.Math.log10)],
(),
),
Function.make(
~name="log base 2",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`log2(3.5)`],
~definitions=[NumberToNumber.make("log2", Js.Math.log2)],
(),
),
Function.make(
~name="round",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`round(3.5)`],
~definitions=[NumberToNumber.make("round", Js.Math.round)],
(),
),
Function.make(
~name="sum",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`sum([3,5,2])`],
~definitions=[ArrayNumberDist.make("sum", r => r->E.A.Floats.sum->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="product",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`product([3,5,2])`],
~definitions=[
ArrayNumberDist.make("product", r => r->E.A.Floats.product->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="min",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`min([3,5,2])`],
~definitions=[ArrayNumberDist.make("min", r => r->E.A.Floats.min->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="max",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`max([3,5,2])`],
~definitions=[ArrayNumberDist.make("max", r => r->E.A.Floats.max->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="mean",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`mean([3,5,2])`],
~definitions=[ArrayNumberDist.make("mean", r => r->E.A.Floats.mean->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="geometric mean",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`geomean([3,5,2])`],
~definitions=[
ArrayNumberDist.make("geomean", r => r->E.A.Floats.geomean->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="standard deviation",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`stdev([3,5,2,3,5])`],
~definitions=[ArrayNumberDist.make("stdev", r => r->E.A.Floats.stdev->Wrappers.evNumber->Ok)],
(),
),
Function.make(
~name="variance",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[`variance([3,5,2,3,5])`],
~definitions=[
ArrayNumberDist.make("variance", r => r->E.A.Floats.variance->Wrappers.evNumber->Ok),
],
(),
),
Function.make(
~name="sort",
~nameSpace,
~requiresNamespace,
~output=EvtArray,
~examples=[`sort([3,5,2,3,5])`],
~definitions=[
ArrayNumberDist.make("sort", r =>
r->E.A.Floats.sort->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="cumulative sum",
~nameSpace,
~requiresNamespace,
~output=EvtArray,
~examples=[`cumsum([3,5,2,3,5])`],
~definitions=[
ArrayNumberDist.make("cumsum", r =>
r->E.A.Floats.cumSum->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="cumulative prod",
~nameSpace,
~requiresNamespace,
~output=EvtArray,
~examples=[`cumprod([3,5,2,3,5])`],
~definitions=[
ArrayNumberDist.make("cumprod", r =>
r->E.A.Floats.cumProd->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
Function.make(
~name="diff",
~nameSpace,
~requiresNamespace,
~output=EvtArray,
~examples=[`diff([3,5,2,3,5])`],
~definitions=[
ArrayNumberDist.make("diff", r =>
r->E.A.Floats.diff->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
),
],
(),
),
]

View File

@ -0,0 +1,102 @@
open FunctionRegistry_Core
open FunctionRegistry_Helpers
let nameSpace = "PointSet"
let requiresNamespace = true
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_InternalExpressionValue.IEvDistribution(
PointSet(makeDist(r)),
))
expressionValue
}
let library = [
Function.make(
~name="fromDist",
~nameSpace,
~requiresNamespace=true,
~examples=[`PointSet.fromDist(normal(5,2))`],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="fromDist",
~inputs=[FRTypeDist],
~run=(_, inputs, env, _) =>
switch inputs {
| [FRValueDist(dist)] =>
GenericDist.toPointSet(
dist,
~xyPointLength=env.xyPointLength,
~sampleCount=env.sampleCount,
(),
)
->E.R2.fmap(Wrappers.pointSet)
->E.R2.fmap(Wrappers.evDistribution)
->E.R2.errMap(_ => "")
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="makeContinuous",
~nameSpace,
~requiresNamespace,
~examples=[
`PointSet.makeContinuous([
{x: 0, y: 0.2},
{x: 1, y: 0.7},
{x: 2, y: 0.8},
{x: 3, y: 0.2}
])`,
],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="makeContinuous",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(_, inputs, _, _) => inputsTodist(inputs, r => Continuous(Continuous.make(r))),
(),
),
],
(),
),
Function.make(
~name="makeDiscrete",
~nameSpace,
~requiresNamespace,
~examples=[
`PointSet.makeDiscrete([
{x: 0, y: 0.2},
{x: 1, y: 0.7},
{x: 2, y: 0.8},
{x: 3, y: 0.2}
])`,
],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="makeDiscrete",
~inputs=[FRTypeArray(FRTypeRecord([("x", FRTypeNumeric), ("y", FRTypeNumeric)]))],
~run=(_, inputs, _, _) => inputsTodist(inputs, r => Discrete(Discrete.make(r))),
(),
),
],
(),
),
]

View File

@ -0,0 +1,272 @@
open FunctionRegistry_Core
open FunctionRegistry_Helpers
let nameSpace = "SampleSet"
let requiresNamespace = true
module Internal = {
type t = SampleSetDist.t
let doLambdaCall = (aLambdaValue, list, environment, reducer) =>
switch Reducer_Expression_Lambda.doLambdaCall(aLambdaValue, list, environment, reducer) {
| Ok(IEvNumber(f)) => Ok(f)
| _ => Error(Operation.SampleMapNeedsNtoNFunction)
}
let toType = (r): result<
ReducerInterface_InternalExpressionValue.t,
Reducer_ErrorValue.errorValue,
> =>
switch r {
| Ok(r) => Ok(Wrappers.evDistribution(SampleSet(r)))
| Error(r) => Error(REDistributionError(SampleSetError(r)))
}
//TODO: I don't know why this seems to need at least one input
let fromFn = (
aLambdaValue,
env: ReducerInterface_InternalExpressionValue.environment,
reducer,
) => {
let sampleCount = env.sampleCount
let fn = r => doLambdaCall(aLambdaValue, list{IEvNumber(r)}, env, reducer)
Belt_Array.makeBy(sampleCount, r => fn(r->Js.Int.toFloat))->E.A.R.firstErrorOrOpen
}
let map1 = (sampleSetDist: t, aLambdaValue, env, reducer) => {
let fn = r => doLambdaCall(aLambdaValue, list{IEvNumber(r)}, env, reducer)
SampleSetDist.samplesMap(~fn, sampleSetDist)->toType
}
let map2 = (t1: t, t2: t, aLambdaValue, env, reducer) => {
let fn = (a, b) => doLambdaCall(aLambdaValue, list{IEvNumber(a), IEvNumber(b)}, env, reducer)
SampleSetDist.map2(~fn, ~t1, ~t2)->toType
}
let map3 = (t1: t, t2: t, t3: t, aLambdaValue, env, reducer) => {
let fn = (a, b, c) =>
doLambdaCall(aLambdaValue, list{IEvNumber(a), IEvNumber(b), IEvNumber(c)}, env, reducer)
SampleSetDist.map3(~fn, ~t1, ~t2, ~t3)->toType
}
let parseSampleSetArray = (arr: array<internalExpressionValue>): option<
array<SampleSetDist.t>,
> => {
let parseSampleSet = (value: internalExpressionValue): option<SampleSetDist.t> =>
switch value {
| IEvDistribution(SampleSet(dist)) => Some(dist)
| _ => None
}
E.A.O.openIfAllSome(E.A.fmap(parseSampleSet, arr))
}
let mapN = (aValueArray: array<internalExpressionValue>, aLambdaValue, env, reducer) => {
switch parseSampleSetArray(aValueArray) {
| Some(t1) =>
let fn = a =>
doLambdaCall(
aLambdaValue,
list{IEvArray(E.A.fmap(x => Wrappers.evNumber(x), a))},
env,
reducer,
)
SampleSetDist.mapN(~fn, ~t1)->toType
| None => Error(REFunctionNotFound(""))
}
}
}
let library = [
Function.make(
~name="fromDist",
~nameSpace,
~requiresNamespace=true,
~examples=[`SampleSet.fromDist(normal(5,2))`],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="fromDist",
~inputs=[FRTypeDist],
~run=(_, inputs, env, _) =>
switch inputs {
| [FRValueDist(dist)] =>
GenericDist.toSampleSetDist(dist, env.sampleCount)
->E.R2.fmap(Wrappers.sampleSet)
->E.R2.fmap(Wrappers.evDistribution)
->E.R2.errMap(_ => "")
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="fromList",
~nameSpace,
~requiresNamespace=true,
~examples=[`SampleSet.fromList([3,5,2,3,5,2,3,5,2,3,3,5,3,2,3,1,1,3])`],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="fromList",
~inputs=[FRTypeArray(FRTypeNumber)],
~run=(_, inputs, _, _) => {
let sampleSet =
Prepare.ToTypedArray.numbers(inputs) |> E.R2.bind(r =>
SampleSetDist.make(r)->E.R2.errMap(_ => "AM I HERE? WHYERE AMI??")
)
sampleSet->E.R2.fmap(Wrappers.sampleSet)->E.R2.fmap(Wrappers.evDistribution)
},
(),
),
],
(),
),
Function.make(
~name="toList",
~nameSpace,
~requiresNamespace=true,
~examples=[`SampleSet.toList(SampleSet.fromDist(normal(5,2)))`],
~output=ReducerInterface_InternalExpressionValue.EvtArray,
~definitions=[
FnDefinition.make(
~name="toList",
~inputs=[FRTypeDist],
~run=(inputs, _, _, _) =>
switch inputs {
| [IEvDistribution(SampleSet(dist))] =>
dist->E.A2.fmap(Wrappers.evNumber)->Wrappers.evArray->Ok
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="fromFn",
~nameSpace,
~requiresNamespace=true,
~examples=[`SampleSet.fromFn({|| sample(normal(5,2))})`],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="fromFn",
~inputs=[FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [IEvLambda(lambda)] =>
switch Internal.fromFn(lambda, env, reducer) {
| Ok(r) => Ok(r->Wrappers.sampleSet->Wrappers.evDistribution)
| Error(_) => Error("issue")
}
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="map",
~nameSpace,
~requiresNamespace,
~examples=[`SampleSet.map(SampleSet.fromDist(normal(5,2)), {|x| x + 1})`],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="map",
~inputs=[FRTypeDist, FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [IEvDistribution(SampleSet(dist)), IEvLambda(lambda)] =>
Internal.map1(dist, lambda, env, reducer)->E.R2.errMap(_ => "")
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="map2",
~nameSpace,
~requiresNamespace,
~examples=[
`SampleSet.map2(SampleSet.fromDist(normal(5,2)), SampleSet.fromDist(normal(5,2)), {|x, y| x + y})`,
],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="map2",
~inputs=[FRTypeDist, FRTypeDist, FRTypeLambda],
~run=(inputs, _, env, reducer) => {
switch inputs {
| [
IEvDistribution(SampleSet(dist1)),
IEvDistribution(SampleSet(dist2)),
IEvLambda(lambda),
] =>
Internal.map2(dist1, dist2, lambda, env, reducer)->E.R2.errMap(_ => "")
| _ => Error(impossibleError)
}
},
(),
),
],
(),
),
Function.make(
~name="map3",
~nameSpace,
~requiresNamespace,
~examples=[
`SampleSet.map3(SampleSet.fromDist(normal(5,2)), SampleSet.fromDist(normal(5,2)), SampleSet.fromDist(normal(5,2)), {|x, y, z| max([x,y,z])})`,
],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="map3",
~inputs=[FRTypeDist, FRTypeDist, FRTypeDist, FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [
IEvDistribution(SampleSet(dist1)),
IEvDistribution(SampleSet(dist2)),
IEvDistribution(SampleSet(dist3)),
IEvLambda(lambda),
] =>
Internal.map3(dist1, dist2, dist3, lambda, env, reducer)->E.R2.errMap(_ => "")
| _ => Error(impossibleError)
},
(),
),
],
(),
),
Function.make(
~name="mapN",
~nameSpace,
~requiresNamespace,
~examples=[
`SampleSet.mapN([SampleSet.fromDist(normal(5,2)), SampleSet.fromDist(normal(5,2)), SampleSet.fromDist(normal(5,2))], {|x| max(x)})`,
],
~output=ReducerInterface_InternalExpressionValue.EvtDistribution,
~definitions=[
FnDefinition.make(
~name="mapN",
~inputs=[FRTypeArray(FRTypeDist), FRTypeLambda],
~run=(inputs, _, env, reducer) =>
switch inputs {
| [IEvArray(dists), IEvLambda(lambda)] =>
Internal.mapN(dists, lambda, env, reducer)->E.R2.errMap(_e => {
"AHHH doesn't work"
})
| _ => Error(impossibleError)
},
(),
),
],
(),
),
]

View File

@ -0,0 +1,89 @@
open FunctionRegistry_Core
let nameSpace = "Dist"
let requiresNamespace = true
let runScoring = (estimate, answer, prior, env) => {
GenericDist.Score.logScore(~estimate, ~answer, ~prior, ~env)
->E.R2.fmap(FunctionRegistry_Helpers.Wrappers.evNumber)
->E.R2.errMap(DistributionTypes.Error.toString)
}
let library = [
Function.make(
~name="logScore",
~nameSpace,
~requiresNamespace,
~output=EvtNumber,
~examples=[
"Dist.logScore({estimate: normal(5,2), answer: normal(5.2,1), prior: normal(5.5,3)})",
"Dist.logScore({estimate: normal(5,2), answer: normal(5.2,1)})",
"Dist.logScore({estimate: normal(5,2), answer: 4.5})",
],
~definitions=[
FnDefinition.make(
~name="logScore",
~inputs=[
FRTypeRecord([
("estimate", FRTypeDist),
("answer", FRTypeDistOrNumber),
("prior", FRTypeDist),
]),
],
~run=(_, inputs, env, _) => {
switch FunctionRegistry_Helpers.Prepare.ToValueArray.Record.threeArgs(inputs) {
| Ok([FRValueDist(estimate), FRValueDistOrNumber(FRValueDist(d)), FRValueDist(prior)]) =>
runScoring(estimate, Score_Dist(d), Some(prior), env)
| Ok([
FRValueDist(estimate),
FRValueDistOrNumber(FRValueNumber(d)),
FRValueDist(prior),
]) =>
runScoring(estimate, Score_Scalar(d), Some(prior), env)
| Error(e) => Error(e)
| _ => Error(FunctionRegistry_Helpers.impossibleError)
}
},
(),
),
FnDefinition.make(
~name="logScore",
~inputs=[FRTypeRecord([("estimate", FRTypeDist), ("answer", FRTypeDistOrNumber)])],
~run=(_, inputs, env, _) => {
switch FunctionRegistry_Helpers.Prepare.ToValueArray.Record.twoArgs(inputs) {
| Ok([FRValueDist(estimate), FRValueDistOrNumber(FRValueDist(d))]) =>
runScoring(estimate, Score_Dist(d), None, env)
| Ok([FRValueDist(estimate), FRValueDistOrNumber(FRValueNumber(d))]) =>
runScoring(estimate, Score_Scalar(d), None, env)
| Error(e) => Error(e)
| _ => Error(FunctionRegistry_Helpers.impossibleError)
}
},
(),
),
],
(),
),
Function.make(
~name="klDivergence",
~nameSpace,
~output=EvtNumber,
~requiresNamespace,
~examples=["Dist.klDivergence(normal(5,2), normal(5,1.5))"],
~definitions=[
FnDefinition.make(
~name="klDivergence",
~inputs=[FRTypeDist, FRTypeDist],
~run=(_, inputs, env, _) => {
switch inputs {
| [FRValueDist(estimate), FRValueDist(d)] =>
runScoring(estimate, Score_Dist(d), None, env)
| _ => Error(FunctionRegistry_Helpers.impossibleError)
}
},
(),
),
],
(),
),
]

View File

@ -1,3 +1,6 @@
// Only Bindings as the global module is supported
// Other module operations such as import export will be prepreocessed jobs
module ExpressionT = Reducer_Expression_T module ExpressionT = Reducer_Expression_T
module InternalExpressionValue = ReducerInterface_InternalExpressionValue module InternalExpressionValue = ReducerInterface_InternalExpressionValue
open Reducer_ErrorValue open Reducer_ErrorValue

View File

@ -10,8 +10,8 @@ open ReducerInterface_InternalExpressionValue
open Reducer_ErrorValue open Reducer_ErrorValue
/* /*
MathJs provides default implementations for builtins MathJs provides default implementations for built-ins
This is where all the expected builtins like + = * / sin cos log ln etc are handled This is where all the expected built-ins like + = * / sin cos log ln etc are handled
DO NOT try to add external function mapping here! DO NOT try to add external function mapping here!
*/ */
@ -95,33 +95,7 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
let doExportBindings = (bindings: nameSpace) => bindings->Bindings.toExpressionValue->Ok let doExportBindings = (bindings: nameSpace) => bindings->Bindings.toExpressionValue->Ok
let doKeepArray = (aValueArray, aLambdaValue) => {
let rMappedList = aValueArray->Belt.Array.reduceReverse(Ok(list{}), (rAcc, elem) =>
rAcc->Result.flatMap(acc => {
let rNewElem = Lambda.doLambdaCall(aLambdaValue, list{elem}, environment, reducer)
rNewElem->Result.map(newElem =>
switch newElem {
| IEvBool(true) => list{elem, ...acc}
| _ => acc
}
)
})
)
rMappedList->Result.map(mappedList => mappedList->Belt.List.toArray->IEvArray)
}
let doMapArray = (aValueArray, aLambdaValue) => {
let rMappedList = aValueArray->Belt.Array.reduceReverse(Ok(list{}), (rAcc, elem) =>
rAcc->Result.flatMap(acc => {
let rNewElem = Lambda.doLambdaCall(aLambdaValue, list{elem}, environment, reducer)
rNewElem->Result.map(newElem => list{newElem, ...acc})
})
)
rMappedList->Result.map(mappedList => mappedList->Belt.List.toArray->IEvArray)
}
module SampleMap = { module SampleMap = {
type t = SampleSetDist.t
let doLambdaCall = (aLambdaValue, list) => let doLambdaCall = (aLambdaValue, list) =>
switch Lambda.doLambdaCall(aLambdaValue, list, environment, reducer) { switch Lambda.doLambdaCall(aLambdaValue, list, environment, reducer) {
| Ok(IEvNumber(f)) => Ok(f) | Ok(IEvNumber(f)) => Ok(f)
@ -134,37 +108,26 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
| Error(r) => Error(REDistributionError(SampleSetError(r))) | Error(r) => Error(REDistributionError(SampleSetError(r)))
} }
let map1 = (sampleSetDist: t, aLambdaValue) => { let parseSampleSetArray = (arr: array<internalExpressionValue>): option<
let fn = r => doLambdaCall(aLambdaValue, list{IEvNumber(r)}) array<SampleSetDist.t>,
toType(SampleSetDist.samplesMap(~fn, sampleSetDist)) > => {
let parseSampleSet = (value: internalExpressionValue): option<SampleSetDist.t> =>
switch value {
| IEvDistribution(SampleSet(dist)) => Some(dist)
| _ => None
}
E.A.O.openIfAllSome(E.A.fmap(parseSampleSet, arr))
} }
let map2 = (t1: t, t2: t, aLambdaValue) => { let _mapN = (aValueArray: array<internalExpressionValue>, aLambdaValue) => {
let fn = (a, b) => doLambdaCall(aLambdaValue, list{IEvNumber(a), IEvNumber(b)}) switch parseSampleSetArray(aValueArray) {
SampleSetDist.map2(~fn, ~t1, ~t2)->toType | Some(t1) =>
let fn = a => doLambdaCall(aLambdaValue, list{IEvArray(E.A.fmap(x => IEvNumber(x), a))})
SampleSetDist.mapN(~fn, ~t1)->toType
| None =>
Error(REFunctionNotFound(call->functionCallToCallSignature->functionCallSignatureToString))
}
} }
let map3 = (t1: t, t2: t, t3: t, aLambdaValue) => {
let fn = (a, b, c) =>
doLambdaCall(aLambdaValue, list{IEvNumber(a), IEvNumber(b), IEvNumber(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 =>
Lambda.doLambdaCall(aLambdaValue, list{acc, elem}, environment, reducer)
)
)
}
let doReduceReverseArray = (aValueArray, initialValue, aLambdaValue) => {
aValueArray->Belt.Array.reduceReverse(Ok(initialValue), (rAcc, elem) =>
rAcc->Result.flatMap(acc =>
Lambda.doLambdaCall(aLambdaValue, list{acc, elem}, environment, reducer)
)
)
} }
switch call { switch call {
@ -198,43 +161,13 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
| ("$_typeFunction_$", [IEvArray(arr)]) => TypeBuilder.typeFunction(arr) | ("$_typeFunction_$", [IEvArray(arr)]) => TypeBuilder.typeFunction(arr)
| ("$_typeTuple_$", [IEvArray(elems)]) => TypeBuilder.typeTuple(elems) | ("$_typeTuple_$", [IEvArray(elems)]) => TypeBuilder.typeTuple(elems)
| ("$_typeArray_$", [elem]) => TypeBuilder.typeArray(elem) | ("$_typeArray_$", [elem]) => TypeBuilder.typeArray(elem)
| ("$_typeRecord_$", [IEvArray(arrayOfPairs)]) => TypeBuilder.typeRecord(arrayOfPairs) | ("$_typeRecord_$", [IEvRecord(propertyMap)]) => TypeBuilder.typeRecord(propertyMap)
| ("concat", [IEvArray(aValueArray), IEvArray(bValueArray)]) => | ("concat", [IEvArray(aValueArray), IEvArray(bValueArray)]) =>
doAddArray(aValueArray, bValueArray) doAddArray(aValueArray, bValueArray)
| ("concat", [IEvString(aValueString), IEvString(bValueString)]) => | ("concat", [IEvString(aValueString), IEvString(bValueString)]) =>
doAddString(aValueString, bValueString) doAddString(aValueString, bValueString)
| ("inspect", [value, IEvString(label)]) => inspectLabel(value, label) | ("inspect", [value, IEvString(label)]) => inspectLabel(value, label)
| ("inspect", [value]) => inspect(value) | ("inspect", [value]) => inspect(value)
| ("filter", [IEvArray(aValueArray), IEvLambda(aLambdaValue)]) =>
doKeepArray(aValueArray, aLambdaValue)
| ("map", [IEvArray(aValueArray), IEvLambda(aLambdaValue)]) =>
doMapArray(aValueArray, aLambdaValue)
| ("mapSamples", [IEvDistribution(SampleSet(dist)), IEvLambda(aLambdaValue)]) =>
SampleMap.map1(dist, aLambdaValue)
| (
"mapSamples2",
[
IEvDistribution(SampleSet(dist1)),
IEvDistribution(SampleSet(dist2)),
IEvLambda(aLambdaValue),
],
) =>
SampleMap.map2(dist1, dist2, aLambdaValue)
| (
"mapSamples3",
[
IEvDistribution(SampleSet(dist1)),
IEvDistribution(SampleSet(dist2)),
IEvDistribution(SampleSet(dist3)),
IEvLambda(aLambdaValue),
],
) =>
SampleMap.map3(dist1, dist2, dist3, aLambdaValue)
| ("reduce", [IEvArray(aValueArray), initialValue, IEvLambda(aLambdaValue)]) =>
doReduceArray(aValueArray, initialValue, aLambdaValue)
| ("reduceReverse", [IEvArray(aValueArray), initialValue, IEvLambda(aLambdaValue)]) =>
doReduceReverseArray(aValueArray, initialValue, aLambdaValue)
| ("reverse", [IEvArray(aValueArray)]) => aValueArray->Belt.Array.reverse->IEvArray->Ok
| (_, [IEvBool(_)]) | (_, [IEvBool(_)])
| (_, [IEvNumber(_)]) | (_, [IEvNumber(_)])
| (_, [IEvString(_)]) | (_, [IEvString(_)])
@ -246,7 +179,6 @@ let callInternal = (call: functionCall, environment, reducer: ExpressionT.reduce
Error(REFunctionNotFound(call->functionCallToCallSignature->functionCallSignatureToString)) // Report full type signature as error Error(REFunctionNotFound(call->functionCallToCallSignature->functionCallSignatureToString)) // Report full type signature as error
} }
} }
/* /*
Reducer uses Result monad while reducing expressions Reducer uses Result monad while reducing expressions
*/ */
@ -255,11 +187,10 @@ let dispatch = (call: functionCall, environment, reducer: ExpressionT.reducerFn)
errorValue, errorValue,
> => > =>
try { try {
let callInternalWithReducer = (call, environment) => callInternal(call, environment, reducer)
let (fn, args) = call let (fn, args) = call
// There is a bug that prevents string match in patterns // There is a bug that prevents string match in patterns
// So we have to recreate a copy of the string // So we have to recreate a copy of the string
ExternalLibrary.dispatch((Js.String.make(fn), args), environment, callInternalWithReducer) ExternalLibrary.dispatch((Js.String.make(fn), args), environment, reducer, callInternal)
} catch { } catch {
| Js.Exn.Error(obj) => REJavaScriptExn(Js.Exn.message(obj), Js.Exn.name(obj))->Error | Js.Exn.Error(obj) => REJavaScriptExn(Js.Exn.message(obj), Js.Exn.name(obj))->Error
| _ => RETodo("unhandled rescript exception")->Error | _ => RETodo("unhandled rescript exception")->Error

View File

@ -144,7 +144,7 @@ let dispatchMacroCall = (
let ifTrueBlock = eBlock(list{ifTrue}) let ifTrueBlock = eBlock(list{ifTrue})
ExpressionWithContext.withContext(ifTrueBlock, bindings)->Ok ExpressionWithContext.withContext(ifTrueBlock, bindings)->Ok
} }
| _ => REExpectedType("Boolean")->Error | _ => REExpectedType("Boolean", "")->Error
} }
) )
} }

View File

@ -0,0 +1,19 @@
module TypeChecker = Reducer_Type_TypeChecker
module T = Reducer_Dispatch_T
open ReducerInterface_InternalExpressionValue
type errorValue = Reducer_ErrorValue.errorValue
let makeFromTypes = jumpTable => {
let dispatchChainPiece: T.dispatchChainPiece = ((fnName, fnArgs): functionCall, environment) => {
let jumpTableEntry = jumpTable->Js.Array2.find(elem => {
let (candidName, candidType, _) = elem
candidName == fnName && TypeChecker.checkITypeArgumentsBool(candidType, fnArgs)
})
switch jumpTableEntry {
| Some((_, _, bridgeFn)) => bridgeFn(fnArgs, environment)->Some
| _ => None
}
}
dispatchChainPiece
}

View File

@ -0,0 +1,20 @@
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module ExpressionT = Reducer_Expression_T
// Each piece of the dispatch chain computes the result or returns None so that the chain can continue
type dispatchChainPiece = (
InternalExpressionValue.functionCall,
InternalExpressionValue.environment,
) => option<result<InternalExpressionValue.t, Reducer_ErrorValue.errorValue>>
type dispatchChainPieceWithReducer = (
InternalExpressionValue.functionCall,
InternalExpressionValue.environment,
ExpressionT.reducerFn,
) => option<result<InternalExpressionValue.t, Reducer_ErrorValue.errorValue>>
// This is a switch statement case implementation: get the arguments and compute the result
type genericIEvFunction = (
array<InternalExpressionValue.t>,
InternalExpressionValue.environment,
) => result<InternalExpressionValue.t, Reducer_ErrorValue.errorValue>

View File

@ -7,7 +7,7 @@ type errorValue =
| REArrayIndexNotFound(string, int) | REArrayIndexNotFound(string, int)
| REAssignmentExpected | REAssignmentExpected
| REDistributionError(DistributionTypes.error) | REDistributionError(DistributionTypes.error)
| REExpectedType(string) | REExpectedType(string, string)
| REExpressionExpected | REExpressionExpected
| REFunctionExpected(string) | REFunctionExpected(string)
| REFunctionNotFound(string) | REFunctionNotFound(string)
@ -55,6 +55,6 @@ let errorToString = err =>
| RESymbolNotFound(symbolName) => `${symbolName} is not defined` | RESymbolNotFound(symbolName) => `${symbolName} is not defined`
| RESyntaxError(desc, _) => `Syntax Error: ${desc}` | RESyntaxError(desc, _) => `Syntax Error: ${desc}`
| RETodo(msg) => `TODO: ${msg}` | RETodo(msg) => `TODO: ${msg}`
| REExpectedType(typeName) => `Expected type: ${typeName}` | REExpectedType(typeName, valueString) => `Expected type: ${typeName} but got: ${valueString}`
| REUnitNotFound(unitName) => `Unit not found: ${unitName}` | REUnitNotFound(unitName) => `Unit not found: ${unitName}`
} }

View File

@ -1,3 +1,3 @@
// There are switch stament cases in the code which are impossible to reach by design. // There are switch statement cases in the code which are impossible to reach by design.
// ImpossibleException is a sign of programming error. // ImpossibleException is a sign of programming error.
exception ImpossibleException exception ImpossibleException(string)

View File

@ -82,3 +82,5 @@ let eIdentifier = (name: string): expression =>
let eTypeIdentifier = (name: string): expression => let eTypeIdentifier = (name: string): expression =>
name->BInternalExpressionValue.IEvTypeIdentifier->BExpressionT.EValue name->BInternalExpressionValue.IEvTypeIdentifier->BExpressionT.EValue
let eVoid: expression = BInternalExpressionValue.IEvVoid->BExpressionT.EValue

View File

@ -30,12 +30,12 @@ let rec toString = expression =>
switch expression { switch expression {
| EList(list{EValue(IEvCall("$$_block_$$")), ...statements}) => | EList(list{EValue(IEvCall("$$_block_$$")), ...statements}) =>
`{${Belt.List.map(statements, aValue => toString(aValue)) `{${Belt.List.map(statements, aValue => toString(aValue))
->Extra.List.interperse("; ") ->Extra.List.intersperse("; ")
->Belt.List.toArray ->Belt.List.toArray
->Js.String.concatMany("")}}` ->Js.String.concatMany("")}}`
| EList(aList) => | EList(aList) =>
`(${Belt.List.map(aList, aValue => toString(aValue)) `(${Belt.List.map(aList, aValue => toString(aValue))
->Extra.List.interperse(" ") ->Extra.List.intersperse(" ")
->Belt.List.toArray ->Belt.List.toArray
->Js.String.concatMany("")})` ->Js.String.concatMany("")})`
| EValue(aValue) => InternalExpressionValue.toString(aValue) | EValue(aValue) => InternalExpressionValue.toString(aValue)

View File

@ -3,5 +3,5 @@
*/ */
module ExtraList = Reducer_Extra_List module ExtraList = Reducer_Extra_List
let interperse = (anArray, seperator) => let intersperse = (anArray, seperator) =>
anArray->Belt.List.fromArray->ExtraList.interperse(seperator)->Belt.List.toArray anArray->Belt.List.fromArray->ExtraList.intersperse(seperator)->Belt.List.toArray

View File

@ -1,9 +1,9 @@
/* /*
Insert seperator between the elements of a list Insert seperator between the elements of a list
*/ */
let rec interperse = (aList, seperator) => let rec intersperse = (aList, seperator) =>
switch aList { switch aList {
| list{} => list{} | list{} => list{}
| list{a} => list{a} | list{a} => list{a}
| list{a, ...rest} => list{a, seperator, ...interperse(rest, seperator)} | list{a, ...rest} => list{a, seperator, ...intersperse(rest, seperator)}
} }

View File

@ -5,13 +5,12 @@
}} }}
start start
// = _nl start:typeExpression _nl finalComment? {return start}
= _nl start:outerBlock _nl finalComment? {return start} = _nl start:outerBlock _nl finalComment? {return start}
zeroOMoreArgumentsBlockOrExpression = innerBlockOrExpression / lambda zeroOMoreArgumentsBlockOrExpression = innerBlockOrExpression / lambda
outerBlock outerBlock
= statements:array_statements finalExpression: (statementSeparator @expression)? = statements:array_statements finalExpression: (statementSeparator @expression)?
{ if (finalExpression != null) { statements.push(finalExpression) } { if (finalExpression != null) { statements.push(finalExpression) }
return h.nodeBlock(statements) } return h.nodeBlock(statements) }
/ finalExpression: expression / finalExpression: expression
@ -24,28 +23,34 @@ innerBlockOrExpression
quotedInnerBlock quotedInnerBlock
= '{' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}' = '{' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}'
{ statements.push(finalExpression) { statements.push(finalExpression)
return h.nodeBlock(statements) } return h.nodeBlock(statements) }
/ '{' _nl finalExpression: expression _nl '}' / '{' _nl finalExpression: expression _nl '}'
{ return h.nodeBlock([finalExpression]) } { return h.nodeBlock([finalExpression]) }
array_statements array_statements
= head:statement tail:(statementSeparator @array_statements ) = head:statement tail:(statementSeparator @array_statements )
{ return [head, ...tail] } { return [head, ...tail] }
/ head:statement / head:statement
{ return [head] } { return [head] }
statement statement
= letStatement = letStatement
/ defunStatement / defunStatement
/ typeStatement / typeStatement
/ voidStatement
voidStatement
= "call" _nl value:zeroOMoreArgumentsBlockOrExpression
{ var variable = h.nodeIdentifier("_", location());
return h.nodeLetStatement(variable, value); }
letStatement letStatement
= variable:identifier _ assignmentOp _nl value:zeroOMoreArgumentsBlockOrExpression = variable:variable _ assignmentOp _nl value:zeroOMoreArgumentsBlockOrExpression
{ return h.nodeLetStatement(variable, value) } { return h.nodeLetStatement(variable, value) }
defunStatement defunStatement
= variable:identifier '(' _nl args:array_parameters _nl ')' _ assignmentOp _nl body:innerBlockOrExpression = variable:variable '(' _nl args:array_parameters _nl ')' _ assignmentOp _nl body:innerBlockOrExpression
{ var value = h.nodeLambda(args, body) { var value = h.nodeLambda(args, body)
return h.nodeLetStatement(variable, value) } return h.nodeLetStatement(variable, value) }
@ -53,13 +58,15 @@ defunStatement
array_parameters array_parameters
= head:dollarIdentifier tail:(_ ',' _nl @dollarIdentifier)* = head:dollarIdentifier tail:(_ ',' _nl @dollarIdentifier)*
{ return [head, ...tail]; } { return [head, ...tail]; }
/ ""
{ return [h.nodeIdentifier("_", location())]; }
expression = ifthenelse / ternary / logicalAdditive expression = ifthenelse / ternary / logicalAdditive
ifthenelse ifthenelse
= 'if' __nl condition:logicalAdditive = 'if' __nl condition:logicalAdditive
__nl 'then' __nl trueExpression:innerBlockOrExpression __nl 'then' __nl trueExpression:innerBlockOrExpression
__nl 'else' __nl falseExpression:(ifthenelse/innerBlockOrExpression) __nl 'else' __nl falseExpression:(ifthenelse/innerBlockOrExpression)
{ return h.nodeTernary(condition, trueExpression, falseExpression) } { return h.nodeTernary(condition, trueExpression, falseExpression) }
@ -88,8 +95,8 @@ equality
= left:relational _ operator:equalityOp _nl right:relational = left:relational _ operator:equalityOp _nl right:relational
{ return h.makeFunctionCall(h.toFunction[operator], [left, right])} { return h.makeFunctionCall(h.toFunction[operator], [left, right])}
/ relational / relational
equalityOp "operator" = '=='/'!=' equalityOp "operator" = '=='/'!='
relational relational
= left:additive _ operator:relationalOp _nl right:additive = left:additive _ operator:relationalOp _nl right:additive
@ -137,11 +144,11 @@ chainFunctionCall
}, head)} }, head)}
chainedFunction chainedFunction
= fn:dollarIdentifier '(' _nl args:array_functionArguments _nl ')' = fn:variable '(' _nl args:array_functionArguments _nl ')'
{ return {fnName: fn.value, args: args}} { return {fnName: fn.value, args: args}}
/ fn:dollarIdentifier '(' _nl ')' / fn:variable '(' _nl ')'
{ return {fnName: fn.value, args: []}} { return {fnName: fn.value, args: []}}
/ fn:dollarIdentifier / fn:variable
{ return {fnName: fn.value, args: []}} { return {fnName: fn.value, args: []}}
// end of binary operators // end of binary operators
@ -172,19 +179,26 @@ collectionElement
array_functionArguments array_functionArguments
= head:expression tail:(_ ',' _nl @expression)* = head:expression tail:(_ ',' _nl @expression)*
{ return [head, ...tail]; } { return [head, ...tail]; }
/ ""
{return [h.nodeVoid()];}
atom atom
= '(' _nl expression:expression _nl ')' {return expression} = '(' _nl expression:expression _nl ')' {return expression}
/ basicValue / basicValue
basicValue = valueConstructor / basicLiteral basicValue = valueConstructor / basicLiteral
basicLiteral basicLiteral
= string = string
/ number / number
/ boolean / boolean
/ dollarIdentifierWithModule / variable
/ dollarIdentifier / voidLiteral
voidLiteral 'void'
= "()" {return h.nodeVoid();}
variable = dollarIdentifierWithModule / dollarIdentifier
dollarIdentifierWithModule 'identifier' dollarIdentifierWithModule 'identifier'
= head:$moduleIdentifier = head:$moduleIdentifier
@ -195,7 +209,7 @@ dollarIdentifierWithModule 'identifier'
modifiers.unshift(head) modifiers.unshift(head)
modifiers.push(final) modifiers.push(final)
let modifiedIdentifier = modifiers.join('.') let modifiedIdentifier = modifiers.join('.')
return h.nodeIdentifier(modifiedIdentifier) return h.nodeIdentifier(modifiedIdentifier, location())
} }
identifier 'identifier' identifier 'identifier'
@ -232,8 +246,8 @@ float 'float'
= $(((d+ "\." d*) / ("\." d+)) floatExponent? / d+ floatExponent) = $(((d+ "\." d*) / ("\." d+)) floatExponent? / d+ floatExponent)
{ return h.nodeFloat(parseFloat(text()))} { return h.nodeFloat(parseFloat(text()))}
floatExponent = [e]i '-'? d+ floatExponent = [e]i '-'? d+
d = [0-9] d = [0-9]
boolean 'boolean' boolean 'boolean'
= ('true'/'false') = ('true'/'false')
@ -247,10 +261,10 @@ valueConstructor
lambda lambda
= '{' _nl '|' _nl args:array_parameters _nl '|' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}' = '{' _nl '|' _nl args:array_parameters _nl '|' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}'
{ statements.push(finalExpression) { statements.push(finalExpression)
return h.nodeLambda(args, h.nodeBlock(statements)) } return h.nodeLambda(args, h.nodeBlock(statements)) }
/ '{' _nl '|' _nl args:array_parameters _nl '|' _nl finalExpression: expression _nl '}' / '{' _nl '|' _nl args:array_parameters _nl '|' _nl finalExpression: expression _nl '}'
{ return h.nodeLambda(args, h.nodeBlock([finalExpression])) } { return h.nodeLambda(args, h.nodeBlock([finalExpression])) }
arrayConstructor 'array' arrayConstructor 'array'
= '[' _nl ']' = '[' _nl ']'
@ -263,9 +277,13 @@ arrayConstructor 'array'
{ return [head, ...tail]; } { return [head, ...tail]; }
recordConstructor 'record' recordConstructor 'record'
= '{' _nl args:array_recordArguments _nl '}' = '{' _nl args:array_recordArguments _nl end_of_record
{ return h.constructRecord(args); } { return h.constructRecord(args); }
end_of_record
= '}'
/ ',' _nl '}'
array_recordArguments array_recordArguments
= head:keyValuePair tail:(_ ',' _nl @keyValuePair)* = head:keyValuePair tail:(_ ',' _nl @keyValuePair)*
{ return [head, ...tail]; } { return [head, ...tail]; }
@ -289,7 +307,7 @@ __nl 'whitespace or newline'
= (whiteSpaceCharactersOrComment / commentOrNewLine )+ = (whiteSpaceCharactersOrComment / commentOrNewLine )+
statementSeparator 'statement separator' statementSeparator 'statement separator'
= _ (';'/ commentOrNewLine)+ _nl = _ (';'/ commentOrNewLine)+ _nl
commentOrNewLine = finalComment? newLine commentOrNewLine = finalComment? newLine

View File

@ -34,6 +34,7 @@ type nodeModuleIdentifier = {...node, "value": string}
type nodeString = {...node, "value": string} type nodeString = {...node, "value": string}
type nodeTernary = {...node, "condition": node, "trueExpression": node, "falseExpression": node} type nodeTernary = {...node, "condition": node, "trueExpression": node, "falseExpression": node}
type nodeTypeIdentifier = {...node, "value": string} type nodeTypeIdentifier = {...node, "value": string}
type nodeVoid = node
type peggyNode = type peggyNode =
| PgNodeBlock(nodeBlock) | PgNodeBlock(nodeBlock)
@ -50,6 +51,7 @@ type peggyNode =
| PgNodeString(nodeString) | PgNodeString(nodeString)
| PgNodeTernary(nodeTernary) | PgNodeTernary(nodeTernary)
| PgNodeTypeIdentifier(nodeTypeIdentifier) | PgNodeTypeIdentifier(nodeTypeIdentifier)
| PgNodeVoid(nodeVoid)
external castNodeBlock: node => nodeBlock = "%identity" external castNodeBlock: node => nodeBlock = "%identity"
external castNodeBoolean: node => nodeBoolean = "%identity" external castNodeBoolean: node => nodeBoolean = "%identity"
@ -65,6 +67,7 @@ external castNodeModuleIdentifier: node => nodeModuleIdentifier = "%identity"
external castNodeString: node => nodeString = "%identity" external castNodeString: node => nodeString = "%identity"
external castNodeTernary: node => nodeTernary = "%identity" external castNodeTernary: node => nodeTernary = "%identity"
external castNodeTypeIdentifier: node => nodeTypeIdentifier = "%identity" external castNodeTypeIdentifier: node => nodeTypeIdentifier = "%identity"
external castNodeVoid: node => nodeVoid = "%identity"
exception UnsupportedPeggyNodeType(string) // This should never happen; programming error exception UnsupportedPeggyNodeType(string) // This should never happen; programming error
let castNodeType = (node: node) => let castNodeType = (node: node) =>
@ -83,6 +86,7 @@ let castNodeType = (node: node) =>
| "String" => node->castNodeString->PgNodeString | "String" => node->castNodeString->PgNodeString
| "Ternary" => node->castNodeTernary->PgNodeTernary | "Ternary" => node->castNodeTernary->PgNodeTernary
| "TypeIdentifier" => node->castNodeTypeIdentifier->PgNodeTypeIdentifier | "TypeIdentifier" => node->castNodeTypeIdentifier->PgNodeTypeIdentifier
| "Void" => node->castNodeVoid->PgNodeVoid
| _ => raise(UnsupportedPeggyNodeType(node["type"])) | _ => raise(UnsupportedPeggyNodeType(node["type"]))
} }
@ -91,7 +95,7 @@ let rec pgToString = (peggyNode: peggyNode): string => {
args->Js.Array2.map(arg => PgNodeIdentifier(arg)->pgToString)->Js.Array2.toString args->Js.Array2.map(arg => PgNodeIdentifier(arg)->pgToString)->Js.Array2.toString
let nodesToStringUsingSeparator = (nodes: array<node>, separator: string): string => let nodesToStringUsingSeparator = (nodes: array<node>, separator: string): string =>
nodes->Js.Array2.map(toString)->Extra.Array.interperse(separator)->Js.String.concatMany("") nodes->Js.Array2.map(toString)->Extra.Array.intersperse(separator)->Js.String.concatMany("")
switch peggyNode { switch peggyNode {
| PgNodeBlock(node) => "{" ++ node["statements"]->nodesToStringUsingSeparator("; ") ++ "}" | PgNodeBlock(node) => "{" ++ node["statements"]->nodesToStringUsingSeparator("; ") ++ "}"
@ -116,6 +120,7 @@ let rec pgToString = (peggyNode: peggyNode): string => {
" " ++ " " ++
toString(node["falseExpression"]) ++ ")" toString(node["falseExpression"]) ++ ")"
| PgNodeTypeIdentifier(node) => `#${node["value"]}` | PgNodeTypeIdentifier(node) => `#${node["value"]}`
| PgNodeVoid(_node) => "()"
} }
} }
and toString = (node: node): string => node->castNodeType->pgToString and toString = (node: node): string => node->castNodeType->pgToString

View File

@ -48,5 +48,6 @@ let rec fromNode = (node: Parse.node): expression => {
) )
| PgNodeTypeIdentifier(nodeTypeIdentifier) => | PgNodeTypeIdentifier(nodeTypeIdentifier) =>
ExpressionBuilder.eTypeIdentifier(nodeTypeIdentifier["value"]) ExpressionBuilder.eTypeIdentifier(nodeTypeIdentifier["value"])
| PgNodeVoid(_) => ExpressionBuilder.eVoid
} }
} }

View File

@ -213,3 +213,7 @@ export function nodeTernary(
export function nodeTypeIdentifier(typeValue: string) { export function nodeTypeIdentifier(typeValue: string) {
return { type: "TypeIdentifier", value: typeValue }; return { type: "TypeIdentifier", value: typeValue };
} }
export function nodeVoid() {
return { type: "Void" };
}

View File

@ -0,0 +1,49 @@
module ErrorValue = Reducer_ErrorValue
module ExpressionT = Reducer_Expression_T
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module Bindings = Reducer_Bindings
module T = Reducer_Type_T
let ievFromTypeExpression = (
typeExpressionSourceCode: string,
reducerFn: ExpressionT.reducerFn,
): result<InternalExpressionValue.t, ErrorValue.t> => {
let sIndex = "compiled"
let sourceCode = `type ${sIndex}=${typeExpressionSourceCode}`
Reducer_Expression.parse(sourceCode)->Belt.Result.flatMap(expr => {
let rContext = reducerFn(
expr,
Bindings.emptyBindings,
InternalExpressionValue.defaultEnvironment,
)
Belt.Result.map(rContext, context =>
switch context {
| IEvBindings(nameSpace) =>
switch Bindings.getType(nameSpace, sIndex) {
| Some(value) => value
| None => raise(Reducer_Exception.ImpossibleException("Reducer_Type_Compile-none"))
}
| _ => raise(Reducer_Exception.ImpossibleException("Reducer_Type_Compile-raise"))
}
)
})
}
let fromTypeExpression = (
typeExpressionSourceCode: string,
reducerFn: ExpressionT.reducerFn,
): result<T.t, ErrorValue.t> => {
ievFromTypeExpression(
(typeExpressionSourceCode: string),
(reducerFn: ExpressionT.reducerFn),
)->Belt.Result.map(T.fromIEvValue)
}
let fromTypeExpressionExn = (
typeExpressionSourceCode: string,
reducerFn: ExpressionT.reducerFn,
): T.t =>
switch fromTypeExpression(typeExpressionSourceCode, reducerFn) {
| Ok(value) => value
| _ => `Cannot compile ${typeExpressionSourceCode}`->Reducer_Exception.ImpossibleException->raise
}

View File

@ -0,0 +1,53 @@
module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module T = Reducer_Type_T
let isMin = (modifierArg: InternalExpressionValue.t, aValue: InternalExpressionValue.t): bool => {
let pair = (modifierArg, aValue)
switch pair {
| (IEvNumber(a), IEvNumber(b)) => a <= b
| _ => false
}
}
let isMax = (modifierArg: InternalExpressionValue.t, aValue: InternalExpressionValue.t): bool => {
let pair = (modifierArg, aValue)
switch pair {
| (IEvNumber(a), IEvNumber(b)) => a >= b
| _ => false
}
}
let isMemberOf = (
modifierArg: InternalExpressionValue.t,
aValue: InternalExpressionValue.t,
): bool => {
let pair = (modifierArg, aValue)
switch pair {
| (ievA, IEvArray(b)) => Js.Array2.includes(b, ievA)
| _ => false
}
}
let checkModifier = (
key: string,
modifierArg: InternalExpressionValue.t,
aValue: InternalExpressionValue.t,
): bool =>
switch key {
| "min" => isMin(modifierArg, aValue)
| "max" => isMax(modifierArg, aValue)
| "isMemberOf" => isMemberOf(modifierArg, aValue)
| _ => false
}
let checkModifiers = (
contracts: Belt.Map.String.t<InternalExpressionValue.t>,
aValue: InternalExpressionValue.t,
): bool => {
contracts->Belt.Map.String.reduce(true, (acc, key, modifierArg) =>
switch acc {
| true => checkModifier(key, modifierArg, aValue)
| _ => acc
}
)
}

View File

@ -3,13 +3,42 @@ open InternalExpressionValue
type rec iType = type rec iType =
| ItTypeIdentifier(string) | ItTypeIdentifier(string)
| ItModifiedType({modifiedType: iType}) | ItModifiedType({modifiedType: iType, contracts: Belt.Map.String.t<InternalExpressionValue.t>})
| ItTypeOr({typeOr: array<iType>}) | ItTypeOr({typeOr: array<iType>})
| ItTypeFunction({inputs: array<iType>, output: iType}) | ItTypeFunction({inputs: array<iType>, output: iType})
| ItTypeArray({element: iType}) | ItTypeArray({element: iType})
| ItTypeTuple({elements: array<iType>}) | ItTypeTuple({elements: array<iType>})
| ItTypeRecord({properties: Belt.Map.String.t<iType>}) | ItTypeRecord({properties: Belt.Map.String.t<iType>})
type t = iType
type typeErrorValue = TypeMismatch(t, InternalExpressionValue.t)
let rec toString = (t: t): string => {
switch t {
| ItTypeIdentifier(s) => s
| ItModifiedType({modifiedType, contracts}) =>
`${toString(modifiedType)}${contracts->Belt.Map.String.reduce("", (acc, k, v) =>
Js.String2.concatMany(acc, ["<-", k, "(", InternalExpressionValue.toString(v), ")"])
)}`
| ItTypeOr({typeOr}) => `(${Js.Array2.map(typeOr, toString)->Js.Array2.joinWith(" | ")})`
| ItTypeFunction({inputs, output}) =>
`(${inputs->Js.Array2.map(toString)->Js.Array2.joinWith(" => ")} => ${toString(output)})`
| ItTypeArray({element}) => `[${toString(element)}]`
| ItTypeTuple({elements}) => `[${Js.Array2.map(elements, toString)->Js.Array2.joinWith(", ")}]`
| ItTypeRecord({properties}) =>
`{${properties
->Belt.Map.String.toArray
->Js.Array2.map(((k, v)) => Js.String2.concatMany(k, [": ", toString(v)]))
->Js.Array2.joinWith(", ")}}`
}
}
let toStringResult = (rt: result<t, ErrorValue.t>) =>
switch rt {
| Ok(t) => toString(t)
| Error(e) => ErrorValue.errorToString(e)
}
let rec fromTypeMap = typeMap => { let rec fromTypeMap = typeMap => {
let default = IEvString("") let default = IEvString("")
let evTypeTag: InternalExpressionValue.t = Belt.Map.String.getWithDefault( let evTypeTag: InternalExpressionValue.t = Belt.Map.String.getWithDefault(
@ -52,31 +81,39 @@ let rec fromTypeMap = typeMap => {
"properties", "properties",
default, default,
) )
//TODO: map type modifiers
switch evTypeTag { let contracts =
| IEvString("typeIdentifier") => ItModifiedType({modifiedType: fromIEvValue(evTypeIdentifier)}) typeMap->Belt.Map.String.keep((k, _v) => ["min", "max", "memberOf"]->Js.Array2.includes(k))
let makeIt = switch evTypeTag {
| IEvString("typeIdentifier") => fromIEvValue(evTypeIdentifier)
| IEvString("typeOr") => ItTypeOr({typeOr: fromIEvArray(evTypeOr)}) | IEvString("typeOr") => ItTypeOr({typeOr: fromIEvArray(evTypeOr)})
| IEvString("typeFunction") => | IEvString("typeFunction") =>
ItTypeFunction({inputs: fromIEvArray(evInputs), output: fromIEvValue(evOutput)}) ItTypeFunction({inputs: fromIEvArray(evInputs), output: fromIEvValue(evOutput)})
| IEvString("typeArray") => ItTypeArray({element: fromIEvValue(evElement)}) | IEvString("typeArray") => ItTypeArray({element: fromIEvValue(evElement)})
| IEvString("typeTuple") => ItTypeTuple({elements: fromIEvArray(evElements)}) | IEvString("typeTuple") => ItTypeTuple({elements: fromIEvArray(evElements)})
| IEvString("typeRecord") => ItTypeRecord({properties: fromIEvRecord(evProperties)}) | IEvString("typeRecord") => ItTypeRecord({properties: fromIEvRecord(evProperties)})
| _ => raise(Reducer_Exception.ImpossibleException) | _ => raise(Reducer_Exception.ImpossibleException("Reducer_Type_T-evTypeTag"))
} }
Belt.Map.String.isEmpty(contracts)
? makeIt
: ItModifiedType({modifiedType: makeIt, contracts: contracts})
} }
and fromIEvValue = (ievValue: InternalExpressionValue.t) =>
and fromIEvValue = (ievValue: InternalExpressionValue.t): iType =>
switch ievValue { switch ievValue {
| IEvTypeIdentifier(typeIdentifier) => ItTypeIdentifier({typeIdentifier}) | IEvTypeIdentifier(typeIdentifier) => ItTypeIdentifier({typeIdentifier})
| IEvType(typeMap) => fromTypeMap(typeMap) | IEvType(typeMap) => fromTypeMap(typeMap)
| _ => raise(Reducer_Exception.ImpossibleException) | _ => raise(Reducer_Exception.ImpossibleException("Reducer_Type_T-ievValue"))
} }
and fromIEvArray = (ievArray: InternalExpressionValue.t) => and fromIEvArray = (ievArray: InternalExpressionValue.t) =>
switch ievArray { switch ievArray {
| IEvArray(array) => array->Belt.Array.map(fromIEvValue) | IEvArray(array) => array->Belt.Array.map(fromIEvValue)
| _ => raise(Reducer_Exception.ImpossibleException) | _ => raise(Reducer_Exception.ImpossibleException("Reducer_Type_T-ievArray"))
} }
and fromIEvRecord = (ievRecord: InternalExpressionValue.t) => and fromIEvRecord = (ievRecord: InternalExpressionValue.t) =>
switch ievRecord { switch ievRecord {
| IEvRecord(record) => record->Belt.Map.String.map(fromIEvValue) | IEvRecord(record) => record->Belt.Map.String.map(fromIEvValue)
| _ => raise(Reducer_Exception.ImpossibleException) | _ => raise(Reducer_Exception.ImpossibleException("Reducer_Type_T-ievRecord"))
} }

View File

@ -56,7 +56,7 @@ let typeFunction = anArray => {
let typeArray = element => { let typeArray = element => {
let newRecord = Belt.Map.String.fromArray([ let newRecord = Belt.Map.String.fromArray([
("typeTag", IEvString("typeTuple")), ("typeTag", IEvString("typeArray")),
("element", element), ("element", element),
]) ])
newRecord->IEvType->Ok newRecord->IEvType->Ok
@ -64,22 +64,14 @@ let typeArray = element => {
let typeTuple = anArray => { let typeTuple = anArray => {
let newRecord = Belt.Map.String.fromArray([ let newRecord = Belt.Map.String.fromArray([
("typeTag", IEvString("typeArray")), ("typeTag", IEvString("typeTuple")),
("elements", IEvArray(anArray)), ("elements", IEvArray(anArray)),
]) ])
newRecord->IEvType->Ok newRecord->IEvType->Ok
} }
let typeRecord = arrayOfPairs => { let typeRecord = propertyMap => {
let newProperties = let newProperties = propertyMap->IEvRecord
Belt.Array.map(arrayOfPairs, pairValue =>
switch pairValue {
| IEvArray([IEvString(key), valueValue]) => (key, valueValue)
| _ => ("wrong key type", pairValue->toStringWithType->IEvString)
}
)
->Belt.Map.String.fromArray
->IEvRecord
let newRecord = Belt.Map.String.fromArray([ let newRecord = Belt.Map.String.fromArray([
("typeTag", IEvString("typeRecord")), ("typeTag", IEvString("typeRecord")),
("properties", newProperties), ("properties", newProperties),

View File

@ -1,81 +1,180 @@
module ExpressionT = Reducer_Expression_T module ExpressionT = Reducer_Expression_T
module InternalExpressionValue = ReducerInterface_InternalExpressionValue module InternalExpressionValue = ReducerInterface_InternalExpressionValue
module T = Reducer_Type_T module T = Reducer_Type_T
module TypeBuilder = Reducer_Type_TypeBuilder module TypeContracts = Reducer_Type_Contracts
open InternalExpressionValue open InternalExpressionValue
type typeErrorValue = let rec isITypeOf = (anIType: T.iType, aValue): result<bool, T.typeErrorValue> => {
| TypeError(T.iType, InternalExpressionValue.t)
| TypeErrorWithPosition(T.iType, InternalExpressionValue.t, int)
| TypeErrorWithProperty(T.iType, InternalExpressionValue.t, string)
let rec isOfResolvedIType = (anIType: T.iType, aValue): result<bool, typeErrorValue> => {
let caseTypeIdentifier = (anUpperTypeName, aValue) => { let caseTypeIdentifier = (anUpperTypeName, aValue) => {
let aTypeName = anUpperTypeName->Js.String2.toLowerCase let aTypeName = anUpperTypeName->Js.String2.toLowerCase
let valueTypeName = aValue->valueToValueType->valueTypeToString->Js.String2.toLowerCase switch aTypeName {
switch aTypeName === valueTypeName { | "any" => Ok(true)
| true => Ok(true) | _ => {
| false => TypeError(anIType, aValue)->Error let valueTypeName = aValue->valueToValueType->valueTypeToString->Js.String2.toLowerCase
switch aTypeName == valueTypeName {
| true => Ok(true)
| false => T.TypeMismatch(anIType, aValue)->Error
}
}
} }
} }
let _caseRecord = (anIType, evValue, propertyMap, map) => { let caseRecord = (anIType, propertyMap: Belt.Map.String.t<T.iType>, evValue) =>
Belt.Map.String.reduce(propertyMap, Ok(true), (acc, property, propertyType) => { switch evValue {
| IEvRecord(aRecord) =>
if (
Js.Array2.length(propertyMap->Belt.Map.String.keysToArray) ==
Js.Array2.length(aRecord->Belt.Map.String.keysToArray)
) {
Belt.Map.String.reduce(propertyMap, Ok(true), (acc, property, propertyType) => {
Belt.Result.flatMap(acc, _ =>
switch Belt.Map.String.get(aRecord, property) {
| Some(propertyValue) => isITypeOf(propertyType, propertyValue)
| None => T.TypeMismatch(anIType, evValue)->Error
}
)
})
} else {
T.TypeMismatch(anIType, evValue)->Error
}
| _ => T.TypeMismatch(anIType, evValue)->Error
}
let caseArray = (anIType, elementType, evValue) =>
switch evValue {
| IEvArray(anArray) =>
Belt.Array.reduce(anArray, Ok(true), (acc, element) =>
Belt.Result.flatMap(acc, _ =>
switch isITypeOf(elementType, element) {
| Ok(_) => Ok(true)
| Error(error) => error->Error
}
)
)
| _ => T.TypeMismatch(anIType, evValue)->Error
}
let caseTuple = (anIType, elementTypes, evValue) =>
switch evValue {
| IEvArray(anArray) =>
if Js.Array2.length(elementTypes) == Js.Array2.length(anArray) {
let zipped = Belt.Array.zip(elementTypes, anArray)
Belt.Array.reduce(zipped, Ok(true), (acc, (elementType, element)) =>
switch acc {
| Ok(_) =>
switch isITypeOf(elementType, element) {
| Ok(_) => acc
| Error(error) => Error(error)
}
| _ => acc
}
)
} else {
T.TypeMismatch(anIType, evValue)->Error
}
| _ => T.TypeMismatch(anIType, evValue)->Error
}
let caseOr = (anIType, anITypeArray, evValue) =>
switch Belt.Array.reduce(anITypeArray, Ok(false), (acc, anIType) =>
Belt.Result.flatMap(acc, _ => Belt.Result.flatMap(acc, _ =>
switch Belt.Map.String.get(map, property) { switch acc {
| Some(propertyValue) => isOfResolvedIType(propertyType, propertyValue) | Ok(false) =>
| None => TypeErrorWithProperty(anIType, evValue, property)->Error switch isITypeOf(anIType, evValue) {
| Ok(_) => Ok(true)
| Error(_) => acc
}
| _ => acc
} }
) )
}) ) {
} | Ok(true) => Ok(true)
let _caseArray = (anIType, evValue, elementType, anArray) => { | Ok(false) => T.TypeMismatch(anIType, evValue)->Error
Belt.Array.reduceWithIndex(anArray, Ok(true), (acc, element, index) => { | Error(error) => Error(error)
switch isOfResolvedIType(elementType, element) { }
| Ok(_) => acc
| Error(_) => TypeErrorWithPosition(anIType, evValue, index)->Error let caseModifiedType = (
anIType: T.iType,
modifiedType: T.iType,
contracts: Belt.Map.String.t<InternalExpressionValue.t>,
aValue: InternalExpressionValue.t,
) => {
isITypeOf(modifiedType, aValue)->Belt.Result.flatMap(_result => {
if TypeContracts.checkModifiers(contracts, aValue) {
Ok(true)
} else {
T.TypeMismatch(anIType, aValue)->Error
} }
}) })
} }
switch anIType { switch anIType {
| ItTypeIdentifier(name) => caseTypeIdentifier(name, aValue) | ItTypeIdentifier(name) => caseTypeIdentifier(name, aValue)
// TODO: Work in progress. Code is commented to make an a release of other features | ItModifiedType({modifiedType, contracts}) =>
// | ItModifiedType({modifiedType: anIType}) => raise(Reducer_Exception.ImpossibleException) caseModifiedType(anIType, modifiedType, contracts, aValue) //{modifiedType: iType, contracts: Belt.Map.String.t<InternalExpressionValue.t>}
// | ItTypeOr({typeOr: anITypeArray}) => raise(Reducer_Exception.ImpossibleException) | ItTypeOr({typeOr}) => caseOr(anIType, typeOr, aValue)
// | ItTypeFunction({inputs: anITypeArray, output: anIType}) => | ItTypeFunction(_) =>
// raise(Reducer_Exception.ImpossibleException) raise(
// | ItTypeArray({element: anIType}) => raise(Reducer_Exception.ImpossibleException) Reducer_Exception.ImpossibleException(
// | ItTypeTuple({elements: anITypeArray}) => raise(Reducer_Exception.ImpossibleException) "Reducer_TypeChecker-functions are without a type at the moment",
// | ItTypeRecord({properties: anITypeMap}) => raise(Reducer_Exception.ImpossibleException) ),
| _ => raise(Reducer_Exception.ImpossibleException) )
| ItTypeArray({element}) => caseArray(anIType, element, aValue)
| ItTypeTuple({elements}) => caseTuple(anIType, elements, aValue)
| ItTypeRecord({properties}) => caseRecord(anIType, properties, aValue)
} }
} }
let isOfResolvedType = (aType: InternalExpressionValue.t, aValue): result<bool, typeErrorValue> => let isTypeOf = (
aType->T.fromIEvValue->isOfResolvedIType(aValue) typeExpressionSourceCode: string,
aValue: InternalExpressionValue.t,
reducerFn: ExpressionT.reducerFn,
): result<InternalExpressionValue.t, ErrorValue.t> => {
switch typeExpressionSourceCode->Reducer_Type_Compile.fromTypeExpression(reducerFn) {
| Ok(anIType) =>
switch isITypeOf(anIType, aValue) {
| Ok(_) => Ok(aValue)
| Error(T.TypeMismatch(anIType, evValue)) =>
Error(
ErrorValue.REExpectedType(anIType->T.toString, evValue->InternalExpressionValue.toString),
)
}
| Error(error) => Error(error) // Directly propagating - err => err - causes type mismatch
}
}
// TODO: Work in progress. Code is commented to make an a release of other features let checkITypeArguments = (anIType: T.iType, args: array<InternalExpressionValue.t>): result<
// let checkArguments = ( bool,
// evFunctionType: InternalExpressionValue.t, T.typeErrorValue,
// args: array<InternalExpressionValue.t>, > => {
// ) => { switch anIType {
// let functionType = switch evFunctionType { | T.ItTypeFunction({inputs}) => isITypeOf(T.ItTypeTuple({elements: inputs}), args->IEvArray)
// | IEvRecord(functionType) => functionType | _ => T.TypeMismatch(anIType, args->IEvArray)->Error
// | _ => raise(Reducer_Exception.ImpossibleException) }
// } }
// let evInputs = functionType->Belt.Map.String.getWithDefault("inputs", []->IEvArray)
// let inputs = switch evInputs {
// | IEvArray(inputs) => inputs
// | _ => raise(Reducer_Exception.ImpossibleException)
// }
// let rTupleType = TypeBuilder.typeTuple(inputs)
// Belt.Result.flatMap(rTupleType, tuppleType => isOfResolvedType(tuppleType, args->IEvArray))
// }
// let compileTypeExpression = (typeExpression: string, bindings: ExpressionT.bindings, reducerFn: ExpressionT.reducerFn) => { let checkITypeArgumentsBool = (anIType: T.iType, args: array<InternalExpressionValue.t>): bool => {
// statement = `type compiled=${typeExpression}` switch checkITypeArguments(anIType, args) {
| Ok(_) => true
| _ => false
}
}
// } let checkArguments = (
typeExpressionSourceCode: string,
//TODO: asGuard args: array<InternalExpressionValue.t>,
reducerFn: ExpressionT.reducerFn,
): result<InternalExpressionValue.t, ErrorValue.t> => {
switch typeExpressionSourceCode->Reducer_Type_Compile.fromTypeExpression(reducerFn) {
| Ok(anIType) =>
switch checkITypeArguments(anIType, args) {
| Ok(_) => Ok(args->IEvArray)
| Error(T.TypeMismatch(anIType, evValue)) =>
Error(
ErrorValue.REExpectedType(anIType->T.toString, evValue->InternalExpressionValue.toString),
)
}
| Error(error) => Error(error) // Directly propagating - err => err - causes type mismatch
}
}

View File

@ -7,6 +7,9 @@ module ErrorValue = Reducer_ErrorValue
@genType.opaque @genType.opaque
type internalCode = Object type internalCode = Object
@genType.opaque
type hiddenNameSpace = Object
@genType @genType
type rec externalExpressionValue = type rec externalExpressionValue =
| EvArray(array<externalExpressionValue>) | EvArray(array<externalExpressionValue>)
@ -25,15 +28,18 @@ type rec externalExpressionValue =
| EvTypeIdentifier(string) | EvTypeIdentifier(string)
| EvModule(record) | EvModule(record)
| EvType(record) | EvType(record)
| EvVoid
and record = Js.Dict.t<externalExpressionValue> and record = Js.Dict.t<externalExpressionValue>
and externalBindings = record
and lambdaValue = { and lambdaValue = {
parameters: array<string>, parameters: array<string>,
context: externalBindings, context: hiddenNameSpace,
body: internalCode, body: internalCode,
} }
and lambdaDeclaration = Declaration.declaration<lambdaValue> and lambdaDeclaration = Declaration.declaration<lambdaValue>
@genType
type externalBindings = record
@genType @genType
type t = externalExpressionValue type t = externalExpressionValue
@ -63,6 +69,7 @@ let rec toString = aValue =>
| EvTimeDuration(t) => DateTime.Duration.toString(t) | EvTimeDuration(t) => DateTime.Duration.toString(t)
| EvType(t) => `type${t->toStringRecord}` | EvType(t) => `type${t->toStringRecord}`
| EvTypeIdentifier(id) => `#${id}` | EvTypeIdentifier(id) => `#${id}`
| EvVoid => `()`
} }
and toStringRecord = aRecord => { and toStringRecord = aRecord => {
let pairs = let pairs =

View File

@ -1,40 +1,24 @@
module InternalExpressionValue = ReducerInterface_InternalExpressionValue module InternalExpressionValue = ReducerInterface_InternalExpressionValue
type internalExpressionValue = InternalExpressionValue.t type internalExpressionValue = InternalExpressionValue.t
// module Sample = {
// // In real life real libraries should be somewhere else
// /*
// For an example of mapping polymorphic custom functions. To be deleted after real integration
// */
// let customAdd = (a: float, b: float): float => {a +. b}
// }
/* /*
Map external calls of Reducer Map external calls of Reducer
*/ */
let dispatch = (
// I expect that it's important to build this first, so it doesn't get recalculated for each tryRegistry() call. call: InternalExpressionValue.functionCall,
let registry = FunctionRegistry_Library.registry environment,
reducer: Reducer_Expression_T.reducerFn,
let tryRegistry = ((fnName, args): InternalExpressionValue.functionCall, env) => { chain,
FunctionRegistry_Core.Registry.matchAndRun(~registry, ~fnName, ~args, ~env)->E.O2.fmap( ): result<internalExpressionValue, 'e> => {
E.R2.errMap(_, s => Reducer_ErrorValue.RETodo(s)),
)
}
let dispatch = (call: InternalExpressionValue.functionCall, environment, chain): result<
internalExpressionValue,
'e,
> => {
E.A.O.firstSomeFn([ E.A.O.firstSomeFn([
() => ReducerInterface_GenericDistribution.dispatch(call, environment), () => ReducerInterface_GenericDistribution.dispatch(call, environment),
() => ReducerInterface_Date.dispatch(call, environment), () => ReducerInterface_Date.dispatch(call, environment),
() => ReducerInterface_Duration.dispatch(call, environment), () => ReducerInterface_Duration.dispatch(call, environment),
() => ReducerInterface_Number.dispatch(call, environment), () => ReducerInterface_Number.dispatch(call, environment),
() => tryRegistry(call, environment), () => FunctionRegistry_Library.dispatch(call, environment, reducer),
])->E.O2.default(chain(call, environment)) ])->E.O2.defaultFn(() => chain(call, environment, reducer))
} }
/* /*
If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally. If your dispatch is too big you can divide it into smaller dispatches and pass the call so that it gets called finally.

View File

@ -233,19 +233,6 @@ let dispatchToGenericOutput = (call: IEV.functionCall, env: GenericDist.env): op
| ("inv", [IEvDistribution(dist), IEvNumber(float)]) => Helpers.toFloatFn(#Inv(float), dist, ~env) | ("inv", [IEvDistribution(dist), IEvNumber(float)]) => Helpers.toFloatFn(#Inv(float), dist, ~env)
| ("quantile", [IEvDistribution(dist), IEvNumber(float)]) => | ("quantile", [IEvDistribution(dist), IEvNumber(float)]) =>
Helpers.toFloatFn(#Inv(float), dist, ~env) Helpers.toFloatFn(#Inv(float), dist, ~env)
| ("toSampleSet", [IEvDistribution(dist), IEvNumber(float)]) =>
Helpers.toDistFn(ToSampleSet(Belt.Int.fromFloat(float)), dist, ~env)
| ("toSampleSet", [IEvDistribution(dist)]) =>
Helpers.toDistFn(ToSampleSet(env.sampleCount), dist, ~env)
| ("toList", [IEvDistribution(SampleSet(dist))]) => Some(FloatArray(SampleSetDist.T.get(dist)))
| ("fromSamples", [IEvArray(inputArray)]) => {
let _wrapInputErrors = x => SampleSetDist.NonNumericInput(x)
let parsedArray = Helpers.parseNumberArray(inputArray)->E.R2.errMap(_wrapInputErrors)
switch parsedArray {
| Ok(array) => DistributionOperation.run(FromSamples(array), ~env)
| Error(e) => GenDistError(SampleSetError(e))
}->Some
}
| ("inspect", [IEvDistribution(dist)]) => Helpers.toDistFn(Inspect, dist, ~env) | ("inspect", [IEvDistribution(dist)]) => Helpers.toDistFn(Inspect, dist, ~env)
| ("truncateLeft", [IEvDistribution(dist), IEvNumber(float)]) => | ("truncateLeft", [IEvDistribution(dist), IEvNumber(float)]) =>
Helpers.toDistFn(Truncate(Some(float), None), dist, ~env) Helpers.toDistFn(Truncate(Some(float), None), dist, ~env)

View File

@ -23,6 +23,7 @@ type rec t =
| IEvTimeDuration(float) | IEvTimeDuration(float)
| IEvType(map) | IEvType(map)
| IEvTypeIdentifier(string) | IEvTypeIdentifier(string)
| IEvVoid
and map = Belt.Map.String.t<t> and map = Belt.Map.String.t<t>
and nameSpace = NameSpace(Belt.Map.String.t<t>) and nameSpace = NameSpace(Belt.Map.String.t<t>)
and lambdaValue = { and lambdaValue = {
@ -36,6 +37,29 @@ type internalExpressionValue = t
type functionCall = (string, array<t>) type functionCall = (string, array<t>)
module Internal = {
module NameSpace = {
external castNameSpaceToHidden: nameSpace => ExternalExpressionValue.hiddenNameSpace =
"%identity"
external castHiddenToNameSpace: ExternalExpressionValue.hiddenNameSpace => nameSpace =
"%identity"
}
module Lambda = {
let toInternal = (v: ExternalExpressionValue.lambdaValue): lambdaValue => {
let p = v.parameters
let c = v.context->NameSpace.castHiddenToNameSpace
let b = v.body
{parameters: p, context: c, body: b}
}
and toExternal = (v: lambdaValue): ExternalExpressionValue.lambdaValue => {
let p = v.parameters
let c = v.context->NameSpace.castNameSpaceToHidden
let b = v.body
{parameters: p, context: c, body: b}
}
}
}
let rec toString = aValue => let rec toString = aValue =>
switch aValue { switch aValue {
| IEvArray(anArray) => { | IEvArray(anArray) => {
@ -60,6 +84,7 @@ let rec toString = aValue =>
| IEvType(aMap) => aMap->toStringMap | IEvType(aMap) => aMap->toStringMap
| IEvTimeDuration(t) => DateTime.Duration.toString(t) | IEvTimeDuration(t) => DateTime.Duration.toString(t)
| IEvTypeIdentifier(id) => `#${id}` | IEvTypeIdentifier(id) => `#${id}`
| IEvVoid => `()`
} }
and toStringMap = aMap => { and toStringMap = aMap => {
let pairs = let pairs =
@ -84,7 +109,7 @@ let toStringWithType = aValue =>
| IEvDeclaration(_) => `Declaration::${toString(aValue)}` | IEvDeclaration(_) => `Declaration::${toString(aValue)}`
| IEvDistribution(_) => `Distribution::${toString(aValue)}` | IEvDistribution(_) => `Distribution::${toString(aValue)}`
| IEvLambda(_) => `Lambda::${toString(aValue)}` | IEvLambda(_) => `Lambda::${toString(aValue)}`
| IEvBindings(_) => `Module::${toString(aValue)}` | IEvBindings(_) => `Bindings::${toString(aValue)}`
| IEvNumber(_) => `Number::${toString(aValue)}` | IEvNumber(_) => `Number::${toString(aValue)}`
| IEvRecord(_) => `Record::${toString(aValue)}` | IEvRecord(_) => `Record::${toString(aValue)}`
| IEvString(_) => `String::${toString(aValue)}` | IEvString(_) => `String::${toString(aValue)}`
@ -92,6 +117,7 @@ let toStringWithType = aValue =>
| IEvTimeDuration(_) => `Date::${toString(aValue)}` | IEvTimeDuration(_) => `Date::${toString(aValue)}`
| IEvType(_) => `Type::${toString(aValue)}` | IEvType(_) => `Type::${toString(aValue)}`
| IEvTypeIdentifier(_) => `TypeIdentifier::${toString(aValue)}` | IEvTypeIdentifier(_) => `TypeIdentifier::${toString(aValue)}`
| IEvVoid => `Void`
} }
let argsToString = (args: array<t>): string => { let argsToString = (args: array<t>): string => {
@ -135,6 +161,7 @@ type internalExpressionValueType =
| EvtTimeDuration | EvtTimeDuration
| EvtType | EvtType
| EvtTypeIdentifier | EvtTypeIdentifier
| EvtVoid
type functionCallSignature = CallSignature(string, array<internalExpressionValueType>) type functionCallSignature = CallSignature(string, array<internalExpressionValueType>)
type functionDefinitionSignature = type functionDefinitionSignature =
@ -158,6 +185,28 @@ let valueToValueType = value =>
| IEvTimeDuration(_) => EvtTimeDuration | IEvTimeDuration(_) => EvtTimeDuration
| IEvType(_) => EvtType | IEvType(_) => EvtType
| IEvTypeIdentifier(_) => EvtTypeIdentifier | IEvTypeIdentifier(_) => EvtTypeIdentifier
| IEvVoid => EvtVoid
}
let externalValueToValueType = (value: ExternalExpressionValue.t) =>
switch value {
| EvArray(_) => EvtArray
| EvArrayString(_) => EvtArrayString
| EvBool(_) => EvtBool
| EvCall(_) => EvtCall
| EvDate(_) => EvtDate
| EvDeclaration(_) => EvtDeclaration
| EvDistribution(_) => EvtDistribution
| EvLambda(_) => EvtLambda
| EvModule(_) => EvtModule
| EvNumber(_) => EvtNumber
| EvRecord(_) => EvtRecord
| EvString(_) => EvtString
| EvSymbol(_) => EvtSymbol
| EvTimeDuration(_) => EvtTimeDuration
| EvType(_) => EvtType
| EvTypeIdentifier(_) => EvtTypeIdentifier
| EvVoid => EvtVoid
} }
let functionCallToCallSignature = (functionCall: functionCall): functionCallSignature => { let functionCallToCallSignature = (functionCall: functionCall): functionCallSignature => {
@ -183,6 +232,7 @@ let valueTypeToString = (valueType: internalExpressionValueType): string =>
| EvtTimeDuration => `Duration` | EvtTimeDuration => `Duration`
| EvtType => `Type` | EvtType => `Type`
| EvtTypeIdentifier => `TypeIdentifier` | EvtTypeIdentifier => `TypeIdentifier`
| EvtVoid => `Void`
} }
let functionCallSignatureToString = (functionCallSignature: functionCallSignature): string => { let functionCallSignatureToString = (functionCallSignature: functionCallSignature): string => {
@ -212,16 +262,12 @@ let rec toExternal = (iev: t): ExternalExpressionValue.t => {
| IEvType(v) => v->mapToExternal->EvType | IEvType(v) => v->mapToExternal->EvType
| IEvTypeIdentifier(v) => EvTypeIdentifier(v) | IEvTypeIdentifier(v) => EvTypeIdentifier(v)
| IEvBindings(v) => v->nameSpaceToTypeScriptBindings->EvModule | IEvBindings(v) => v->nameSpaceToTypeScriptBindings->EvModule
| IEvVoid => EvVoid
} }
} }
and mapToExternal = v => and mapToExternal = v =>
v->Belt.Map.String.map(e => toExternal(e))->Belt.Map.String.toArray->Js.Dict.fromArray v->Belt.Map.String.map(e => toExternal(e))->Belt.Map.String.toArray->Js.Dict.fromArray
and lambdaValueToExternal = v => { and lambdaValueToExternal = Internal.Lambda.toExternal
let p = v.parameters
let c = v.context->nameSpaceToTypeScriptBindings
let b = v.body
{parameters: p, context: c, body: b}
}
and nameSpaceToTypeScriptBindings = ( and nameSpaceToTypeScriptBindings = (
nameSpace: nameSpace, nameSpace: nameSpace,
): ReducerInterface_ExternalExpressionValue.externalBindings => { ): ReducerInterface_ExternalExpressionValue.externalBindings => {
@ -251,16 +297,12 @@ let rec toInternal = (ev: ExternalExpressionValue.t): t => {
| EvTimeDuration(v) => IEvTimeDuration(v) | EvTimeDuration(v) => IEvTimeDuration(v)
| EvType(v) => v->recordToInternal->IEvType | EvType(v) => v->recordToInternal->IEvType
| EvTypeIdentifier(v) => IEvTypeIdentifier(v) | EvTypeIdentifier(v) => IEvTypeIdentifier(v)
| EvVoid => IEvVoid
} }
} }
and recordToInternal = v => and recordToInternal = v =>
v->Js.Dict.entries->Belt.Map.String.fromArray->Belt.Map.String.map(e => toInternal(e)) v->Js.Dict.entries->Belt.Map.String.fromArray->Belt.Map.String.map(e => toInternal(e))
and lambdaValueToInternal = v => { and lambdaValueToInternal = Internal.Lambda.toInternal
let p = v.parameters
let c = v.context->nameSpaceFromTypeScriptBindings
let b = v.body
{parameters: p, context: c, body: b}
}
and nameSpaceFromTypeScriptBindings = ( and nameSpaceFromTypeScriptBindings = (
r: ReducerInterface_ExternalExpressionValue.externalBindings, r: ReducerInterface_ExternalExpressionValue.externalBindings,
): nameSpace => ): nameSpace =>

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